Browse Source

电子签自动落印代码编写,采购服务合同会签推送

fyz 3 weeks ago
parent
commit
5b3b3b0c4f

+ 171 - 172
mjava-sdzl/src/main/java/com/malk/sdzl/common/OmFormContentMapper.java

@@ -18,195 +18,194 @@ public final class OmFormContentMapper {
 
     static {
         addForm("FORM-F702D5AE142C49B9883C113B1C540DB0XBZD", "OM收入会签单", new String[][] {
-                {"OA编码", "textField_mpxojd0l"},
-                {"所属板块", "textField_mpcdx36z"},
-                {"考核主体", "textField_mpcdx377"},
-                {"考核主体编码", "textField_mpdo38zv"},
-                {"核算主体", "textField_mpcdx370"},
-                {"成本中心", "textField_mpcf7nz6"},
+                {"code", "textField_mpxojd0l"},
+                {"section", "textField_mpcdx36z"},
+                {"assessmentSubject", "textField_mpcdx377"},
+                {"assessmentSubjectCode", "textField_mpdo38zv"},
+                {"accountingEntity", "textField_mpcdx370"},
+                {"costCenterApplicant", "textField_mpcf7nz6"},
                 {"成本中心编码", "textField_mtv45kai"},
-                {"经办人", "textField_mtv45kam"},
-                {"日期", "dateField_mpdo38zu"},
-                {"我方签约公司名称", "textField_mpdo38zw"},
-                {"合同名称", "textField_mtv45kaj"},
-                {"对方单位名称", "textField_mpcf7nzi"},
-                {"客户类型", "selectField_mpdo3904"},
-                {"隶属集团", "textField_mpdo3905"},
-                {"合同类型", "selectField_mpdo3906"},
-                {"是否为续约客户", "selectField_mpdo390b"},
-                {"EBS对方单位名称", "textField_mpdo390h"},
-                {"EBS对方客户编码", "textField_mpdo390i"},
-                {"合同所属部门", "textField_mtv45kal"},
-                {"合同总金额", "numberField_mpceu15v"},
-                {"文件附件", "attachmentField_mpcf7o00"},
-                {"备注", "textareaField_mpcf7nzq"},
-                {"我方盖章日期", "dateField_mpdo390p"},
-                {"盖章状态", "selectField_mpdo390q"},
-                {"盖章合同扫描件", "attachmentField_mpdo390s"},
-                {"流水号", "serialNumberField_mti00v0s"},
-                {"合同归类", "textField_mpcf7nzj"},
+                {"operator", "textField_mtv45kam"},
+                {"date", "dateField_mpdo38zu"},
+                {"ourContractedCompany", "textField_mpdo38zw"},
+                {"contractName", "textField_mtv45kaj"},
+                {"nameOfTheOppositeUnit", "textField_mpcf7nzi"},
+                {"customerType", "selectField_mpdo3904"},
+                {"affiliationGroup", "textField_mpdo3905"},
+                {"contractType", "selectField_mpdo3906"},
+                {"isItARenewalCustomer", "selectField_mpdo390b"},
+                {"ebsOppositeUnitName", "textField_mpdo390h"},
+                {"ebsCustomerCode", "textField_mpdo390i"},
+                {"contractDepartment", "textField_mtv45kal"},
+                {"totalContractAmount", "numberField_mpceu15v"},
+                {"annexToTheDocument", "attachmentField_mpcf7o00"},
+                {"remarks", "textareaField_mpcf7nzq"},
+                {"ourStamp", "dateField_mpdo390p"},
+                {"stampStatus", "selectField_mpdo390q"},
+                {"sealContractScan", "attachmentField_mpdo390s"},
+                {"contractNo", "serialNumberField_mti00v0s"},
+                {"contractClassification", "textField_mpcf7nzj"},
                 {"机构负责人", "employeeField_mtv5azsq"},
         });
         addForm("FORM-1E1AFAE97BEA48A8BA71CD914CEB6684J8FF", "OM合同作废终止申请单", new String[][] {
-                {"类型", "selectField_mtv9x0nk"},
-                {"所属板块", "selectField_goz3vq9"},
-                {"考核主体", "selectField_g9kc21s"},
-                {"编码", "serialNumberField_mtv9x0nj"},
-                {"核算主体", "selectField_tafovni"},
-                {"成本中心", "selectField_j69kuio"},
-                {"选择CRIC合同", "associationFormField_mtbeqiyh"},
-                {"CRIC合同编号", "textField_mtv9x0np"},
-                {"客户名称", "textField_mpcf7nzi"},
-                {"经办人", "employeeField_mpcdx37w"},
-                {"合同金额", "numberField_mpceu15v"},
-                {"所属部门", "departmentSelectField_mpcf7nzh"},
-                {"原盖章日期", "dateField_mpcf7nzc"},
+                {"type", "selectField_mtv9x0nk"},
+                {"section", "selectField_goz3vq9"},
+                {"assessmentSubject", "selectField_g9kc21s"},
+                {"code", "serialNumberField_mtv9x0nj"},
+                {"accountingEntity", "selectField_tafovni"},
+                {"costCenter", "selectField_j69kuio"},
+                {"originalContractNo", "textField_mtv9x0np"},
+                {"customerName", "textField_mpcf7nzi"},
+                {"operator", "employeeField_mpcdx37w"},
+                {"amountOfContract", "numberField_mpceu15v"},
+                {"department", "departmentSelectField_mpcf7nzh"},
+                {"originalStampDate", "dateField_mpcf7nzc"},
                 {"终止日期", "dateField_mpcdx37x"},
-                {"若有项目成本,是否与合同同步作废", "radioField_mpcf7nzo"},
-                {"若有项目成本,已支付金额:", "numberField_mpcf7nzp"},
-                {"原因(详细说明)", "textareaField_mpcf7nzq"},
-                {"是否重签", "radioField_mpcf7nzv"},
-                {"新合同金额", "numberField_mpcf7nzz"},
-                {"新合同名称", "textField_mpcf7nzy"},
-                {"附件", "attachmentField_mpcf7o00"},
-                {"备注", "textareaField_mpcf7o01"},
-                {"所属条线", "textField_mpcf7nz1"},
-                {"编码", "textField_mpxoy4gz"},
+                {"whetherCanceledContract", "radioField_mpcf7nzo"},
+                {"amountPaid", "numberField_mpcf7nzp"},
+                {"reasondetailedDescription", "textareaField_mpcf7nzq"},
+                {"whetherToResign", "radioField_mpcf7nzv"},
+                {"newContractAmount", "numberField_mpcf7nzz"},
+                {"newContractName", "textField_mpcf7nzy"},
+                {"enclosure", "attachmentField_mpcf7o00"},
+                {"remarks", "textareaField_mpcf7o01"},
+                {"line", "textField_mpcf7nz1"},
+                {"code", "textField_mpxoy4gz"},
                 {"考核主体编码", "textField_mpcdx39z"},
-                {"CRIC合同名称", "textField_mtv9x0nr"},
+                {"contractName", "textField_mtv9x0nr"},
                 {"成本中心编码", "textField_mpcdx3a3"},
-                {"法务", "employeeField_msptgxhl"},
+//                {"法务", "employeeField_msptgxhl"},
         });
         addForm("FORM-C73526894F3647F690DECD5F16EE67AF4FKX", "OM收款单", new String[][] {
-                {"所属板块", "selectField_x1tbicc"},
-                {"核算主体", "selectField_tafovni"},
-                {"客户", "associationFormField_mtv6c4pv"},
-                {"入账日期", "dateField_mpcdx37x"},
-                {"收款银行", "associationFormField_mtv6c4pt"},
-                {"收款金额", "numberField_mpceu15v"},
-                {"经办人", "employeeField_mpcdx37w"},
-                {"收入信息", "tableField_mpcdx39v"},
-                {"收入类别", "selectField_mtv6c4px"},
-                {"科目编码", "textField_mpcdx39x"},
-                {"子表科目编码校验", "numberField_mq6eh339"},
-                {"考核主体", "selectField_mtv6c4pz"},
-                {"考核主体编码", "textField_mpcdx39z"},
-                {"项目", "selectField_mtv6c4q1"},
-                {"项目编码", "textField_mpcdx3a1"},
-                {"成本中心", "selectField_mtv6c4q2"},
-                {"成本中心编码", "textField_mpcdx3a3"},
-                {"金额", "numberField_mpcdx3a4"},
-                {"汇票金额", "numberField_mpceu15w"},
-                {"说明", "textareaField_mpceu15x"},
-                {"选择合同", "associationFormField_mtv6c4q3"},
-                {"cric合同编号", "textField_mpceu160"},
-                {"备注", "textareaField_mpceu16o"},
-                {"科目编码校验", "numberField_mq6eh337"},
+                {"section", "selectField_x1tbicc"},
+                {"accountingEntity", "selectField_tafovni"},
+                {"customer", "associationFormField_mtv6c4pv"},
+                {"dateOfEntry", "dateField_mpcdx37x"},
+                {"collectionBank", "associationFormField_mtv6c4pt"},
+                {"collectionAmount", "numberField_mpceu15v"},
+                {"operator", "employeeField_mpcdx37w"},
+                {"incomeInformation", "tableField_mpcdx39v"},
+                {"revenueCategory", "selectField_mtv6c4px"},
+                {"subjectCode", "textField_mpcdx39x"},
+                {"subAccountCodeVerification", "numberField_mq6eh339"},
+                {"incomeSubject", "selectField_mtv6c4pz"},
+                {"incomeCode", "textField_mpcdx39z"},
+                {"project", "selectField_mtv6c4q1"},
+                {"itemCode", "textField_mpcdx3a1"},
+                {"costCenter", "selectField_mtv6c4q2"},
+                {"costCenterCode", "textField_mpcdx3a3"},
+                {"amountOfMoney", "numberField_mpcdx3a4"},
+                {"incomeBillAmount", "numberField_mpceu15w"},
+                {"explain", "textareaField_mpceu15x"},
+//                {"选择合同", "associationFormField_mtv6c4q3"},
+                {"cricContractNumber", "textField_mpceu160"},
+                {"remarks", "textareaField_mpceu16o"},
+                {"accountCodeVerification", "numberField_mq6eh337"},
                 {"全称", "textField_mtv6c4pu"},
-                {"核算主体编码", "textField_mq6erof9"},
-                {"冲销方式", "textField_mpcdx37c"},
-                {"核算主体", "textField_mpcdx370"},
-                {"所属板块", "textField_mpcdx36z"},
-                {"转账信息", "tableField_mpceu163"},
-                {"转账类别", "textField_mpceu164"},
-                {"转账类别编码", "textField_mpceu165"},
-                {"考核主体", "textField_mpceu166"},
-                {"考核主体编码", "textField_mpceu167"},
-                {"项目", "textField_mpceu169"},
-                {"项目编码", "textField_mpceu168"},
-                {"成本中心", "textField_mpceu16a"},
-                {"成本中心编码", "textField_mpceu16b"},
-                {"金额", "numberField_mpceu16c"},
-                {"汇票金额", "numberField_mpceu16d"},
-                {"票据收入类型", "selectField_mpxozyvs"},
-                {"说明", "textareaField_mpceu16e"},
-                {"转账票据合计", "numberField_mpceu16f"},
-                {"转账类别合计", "numberField_mpceu16h"},
-                {"应收票据基础信息登记表", "textField_mpcdx38t"},
-                {"票据金额", "numberField_mpceu16m"},
-                {"票据状态", "textField_mpceu16n"},
-                {"收入票据合计", "numberField_mpcdx3aj"},
-                {"收入合计", "numberField_mpcdx3ak"},
-                {"收入类会计", "employeeField_mpcdx3b2"},
-                {"出纳", "employeeField_mpcdx3b3"},
-                {"新房OM回款核查", "textField_mq6e84og"},
-                {"与OM差异的原因", "textField_mpceu16p"},
-                {"流程结束日期", "dateField_mpceu16r"},
-                {"会计", "textareaField_mq6e84oi"},
-                {"二租结算对接人", "textareaField_mq6e84ok"},
-                {"营销合伙人", "textareaField_mq6e84on"},
-                {"渠道集团结算", "textareaField_mq6e84om"},
+                {"accountingSubjectCode", "textField_mq6erof9"},
+                {"eliminationMode", "textField_mpcdx37c"},
+                {"accountingEntity", "textField_mpcdx370"},
+                {"section", "textField_mpcdx36z"},
+                {"transferInformation", "tableField_mpceu163"},
+                {"transferCategory", "textField_mpceu164"},
+                {"transferCategoryCode", "textField_mpceu165"},
+                {"transferAssessment", "textField_mpceu166"},
+                {"transferAssessmentCode", "textField_mpceu167"},
+                {"transferProject", "textField_mpceu169"},
+                {"transferProjectCode", "textField_mpceu168"},
+                {"projectCostCenter", "textField_mpceu16a"},
+                {"projectCostCenterCode", "textField_mpceu16b"},
+                {"projectAmount", "numberField_mpceu16c"},
+                {"projectBillAmount", "numberField_mpceu16d"},
+                {"noteIncomeType", "selectField_mpxozyvs"},
+                {"projectExplain", "textareaField_mpceu16e"},
+                {"totalTransferNotes", "numberField_mpceu16f"},
+                {"totalTransferCategories", "numberField_mpceu16h"},
+                {"billsRegistrationForm", "textField_mpcdx38t"},
+                {"billAmount", "numberField_mpceu16m"},
+                {"billStatus", "textField_mpceu16n"},
+                {"totalRevenueNotes", "numberField_mpcdx3aj"},
+                {"totalRevenueCategory", "numberField_mpcdx3ak"},
+                {"incomeAccounting", "employeeField_mpcdx3b2"},
+                {"cashier", "employeeField_mpcdx3b3"},
+                {"omReturnNewHouse", "textField_mq6e84og"},
+                {"omDifferenceReasons", "textField_mpceu16p"},
+                {"processEndDate", "dateField_mpceu16r"},
+                {"accountant", "textareaField_mq6e84oi"},
+                {"secondRentPerson", "textareaField_mq6e84ok"},
+                {"marketingPartner", "textareaField_mq6e84on"},
+                {"channelGroupSettlement", "textareaField_mq6e84om"},
         });
         addForm("FORM-A10F4E77BF0D4A1E9216626A4C57AFE6P7GP", "OM开票申请单", new String[][] {
                 {"开票申请流水号", "serialNumberField_mtv3my6j"},
-                {"经办人", "employeeField_mpcdx37w"},
-                {"所属板块", "selectField_goz3vq9"},
-                {"核算主体", "selectField_tafovni"},
-                {"关联客户", "associationFormField_mtbeqiye"},
-                {"入账日期", "dateField_mpcdx37x"},
-                {"税务登记联系电话", "textField_mpcdx383"},
-                {"统一社会信用代码", "textField_mpcdx384"},
-                {"税务登记地址", "textField_mpcdx38b"},
-                {"税务开户银行名称", "textField_mpcdx385"},
-                {"税务开户银行账号", "textField_mpcdx387"},
-                {"开票类型", "radioField_mpcdx38c"},
-                {"CRIC合同名称", "associationFormField_mtbeqiyh"},
+                {"operator", "employeeField_mpcdx37w"},
+                {"section", "selectField_goz3vq9"},
+                {"accountingEntity", "selectField_tafovni"},
+                {"fullNameOfCustomer", "associationFormField_mtbeqiye"},
+                {"dateOfEntry", "dateField_mpcdx37x"},
+                {"taxRegistrationPhone", "textField_mpcdx383"},
+                {"taxRegistrationNumber", "textField_mpcdx384"},
+                {"taxRegistrationAddress", "textField_mpcdx38b"},
+                {"nameOfTaxBank", "textField_mpcdx385"},
+                {"taxAccountBankAccount", "textField_mpcdx387"},
+                {"invoicingType", "radioField_mpcdx38c"},
+                {"contractName", "associationFormField_mtbeqiyh"},
                 {"合同主键", "textField_mpcdx37o"},
-                {"CRIC合同编号", "textField_mpcdx38g"},
-                {"CRIC合同名称", "textField_mpcdx38e"},
-                {"CRIC合同-客户名称", "textField_mpcdx38j"},
-                {"CRIC合同-归属地", "textField_mpcdx38l"},
-                {"CRIC合同金额", "numberField_mpcdx393"},
-                {"CRIC开票内容", "selectField_mtcb9a4h"},
-                {"CRIC", "tableField_mpcdx394"},
-                {"关联产品", "associationFormField_mtbeqiyj"},
-                {"CRIC合同编号", "textField_mpcdx396"},
-                {"CRIC产品名称", "textField_mpcdx397"},
-                {"CRIC子项目名称", "textField_mpcdx398"},
-                {"CRIC执行金额", "numberField_mpcdx399"},
+                {"cricContractNumber", "textField_mpcdx38g"},
+                {"cricContractName", "textField_mpcdx38e"},
+                {"cricContractcustomerName", "textField_mpcdx38j"},
+                {"cricContracthome", "textField_mpcdx38l"},
+                {"cricContractAmount", "numberField_mpcdx393"},
+                {"cricBillingContent", "selectField_mtcb9a4h"},
+                {"cric", "tableField_mpcdx394"},
+//                {"关联产品", "associationFormField_mtbeqiyj"},
+                {"cricContractNo", "textField_mpcdx396"},
+                {"cricProductName", "textField_mpcdx397"},
+                {"cricSubprojectName", "textField_mpcdx398"},
+                {"cricExecutionAmount", "numberField_mpcdx399"},
                 {"CRIC开票金额", "numberField_mtbeqiyk"},
                 {"CRIC业绩金额", "numberField_mtbeqiyl"},
-                {"CRIC已开票金额", "numberField_mpcdx39a"},
-                {"CRIC未开票金额", "numberField_mpcdx39c"},
-                {"CRIC已到账金额", "numberField_mpcdx39b"},
-                {"CRIC本次开票金额", "numberField_mpcdx39d"},
-                {"本次开票金额合计", "numberField_mpcdx39e"},
-                {"票据信息", "tableField_mpcdx39v"},
-                {"收入类别", "selectField_mtv8djh4"},
-                {"收入类别编码", "textField_mpcdx39x"},
-                {"考核主体", "selectField_g9kc21s"},
-                {"考核主体编码", "textField_mpcdx39z"},
-                {"成本中心", "selectField_j69kuio"},
-                {"成本中心编码", "textField_mpcdx3a3"},
-                {"项目", "selectField_mrsytbp6"},
-                {"项目编码", "textField_mpcdx3a1"},
-                {"含税开票金额", "numberField_mpcdx3a4"},
-                {"税率(%)", "selectField_mtv8djh5"},
-                {"金额(不含税)", "numberField_mpcdx3a6"},
-                {"税额", "numberField_mpcdx3a7"},
-                {"税额调整项", "numberField_mpcdx3a8"},
-                {"是否内部结算", "selectField_mtv8djh7"},
-                {"借方科目", "textField_mpcdx3ab"},
-                {"借方科目编码", "textField_mpcdx3ac"},
-                {"中转科目", "textField_mpcdx3ad"},
-                {"中转科目编码", "textField_mpcdx3ae"},
-                {"含税开票金额合计", "numberField_mpcdx3aj"},
-                {"金额(不含税)合计", "numberField_mpcdx3ak"},
-                {"税额合计", "numberField_mpcdx3al"},
-                {"发票备注", "textField_mpcdx38t"},
-                {"发票编码", "textField_mpcdx38v"},
-                {"全电发票交付方式", "selectField_mtv8djh9"},
-                {"全电发票收件邮箱", "textField_mpcdx37t"},
-                {"备注", "textareaField_mpcdx3ba"},
+                {"cricInvoicedAmount", "numberField_mpcdx39a"},
+                {"cricUninvoicedAmount", "numberField_mpcdx39c"},
+                {"amountReceivedByCric", "numberField_mpcdx39b"},
+                {"cricThisInvoicingAmount", "numberField_mpcdx39d"},
+                {"totalAmountInvoicing", "numberField_mpcdx39e"},
+                {"billInformation", "tableField_mpcdx39v"},
+                {"revenueCategory", "selectField_mtv8djh4"},
+                {"revenueCategoryCode", "textField_mpcdx39x"},
+                {"assessmentSubject", "selectField_g9kc21s"},
+                {"billAssessmentSubjectCode", "textField_mpcdx39z"},
+                {"costCenter", "selectField_j69kuio"},
+                {"costCenterCode", "textField_mpcdx3a3"},
+                {"project", "selectField_mrsytbp6"},
+                {"itemCode", "textField_mpcdx3a1"},
+                {"invoicingAmountIncludTax", "numberField_mpcdx3a4"},
+                {"taxRate", "selectField_mtv8djh5"},
+                {"amountexcludingTax", "numberField_mpcdx3a6"},
+                {"taxAmount", "numberField_mpcdx3a7"},
+                {"taxAdjustment", "numberField_mpcdx3a8"},
+                {"internalSettlement", "selectField_mtv8djh7"},
+                {"debitAccount", "textField_mpcdx3ab"},
+                {"debitAccountCode", "textField_mpcdx3ac"},
+                {"transferSubjects", "textField_mpcdx3ad"},
+                {"transferAccountCode", "textField_mpcdx3ae"},
+                {"totalAmountIncludTax", "numberField_mpcdx3aj"},
+                {"totalAmountExcludingTax", "numberField_mpcdx3ak"},
+                {"totalTaxAmount", "numberField_mpcdx3al"},
+                {"invoiceRemarks", "textField_mpcdx38t"},
+                {"invoiceCode", "textField_mpcdx38v"},
+                {"allpowerDeliveryMethod", "selectField_mtv8djh9"},
+                {"allpowerReceiveMailbox", "textField_mpcdx37t"},
+                {"remarks", "textareaField_mpcdx3ba"},
                 {"附件", "attachmentField_mtv8djha"},
-                {"收入类会计", "employeeField_mpcdx3b2"},
-                {"出纳", "employeeField_mpcdx3b3"},
-                {"渠道集团结算", "employeeField_mpcdx3b5"},
-                {"会计", "employeeField_mpcdx3b7"},
-                {"电子发票收件手机", "textField_mpcdx3ar"},
-                {"电子发票收件邮箱", "textField_mpcdx3at"},
-                {"OA编码", "textField_mpxp3omf"},
-                {"税费类型", "selectField_mtbeqiyg"},
+                {"incomeAccounting", "employeeField_mpcdx3b2"},
+                {"cashier", "employeeField_mpcdx3b3"},
+                {"channelGroupSettlement", "employeeField_mpcdx3b5"},
+                {"accountant", "employeeField_mpcdx3b7"},
+                {"eInvoiceReceiptMobile", "textField_mpcdx3ar"},
+                {"eInvoiceReceiptMailbox", "textField_mpcdx3at"},
+                {"oaCoding", "textField_mpxp3omf"},
+                {"taxType", "selectField_mtbeqiyg"},
         });
     }
 

+ 50 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/config/ImageConfig.java

@@ -0,0 +1,50 @@
+package com.malk.sdzl.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 图片生成相关配置属性
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "yida.image")
+public class ImageConfig {
+
+    /** 图片保存的本地目录 */
+    private String saveDir ;
+
+    /** 图片访问的基础URL */
+    private String baseUrl ;
+
+    /** 图片访问路径前缀 */
+    private String urlPrefix = "/images";
+
+    /** 图片格式:png / jpg */
+    private String format = "png";
+
+    /** 标题字体大小 */
+    private int titleFontSize = 16;
+
+    /** 表头字体大小 */
+    private int headerFontSize = 13;
+
+    /** 单元格字体大小 */
+    private int cellFontSize = 12;
+
+    /** 单元格内边距 */
+    private int cellPadding = 8;
+
+    /** 最小列宽 */
+    private int minColWidth = 80;
+
+    /** 最大列宽 */
+    private int maxColWidth = 300;
+
+    /** 默认行高 */
+    private int rowHeight = 30;
+
+    /** 图片背景色 */
+    private String background = "#FFFFFF";
+}

+ 34 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/controller/YqbOrderPushController.java

@@ -0,0 +1,34 @@
+package com.malk.sdzl.controller;
+
+import com.malk.sdzl.entity.dto.YqbOrderPushRequest;
+import com.malk.sdzl.entity.dto.YqbOrderPushResult;
+import com.malk.sdzl.service.YqbOrderPushService;
+import com.malk.server.common.McR;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+
+/** 宜搭采购合同推送易企报单据接口。 */
+@Slf4j
+@RestController
+@RequestMapping("yqb/order")
+public class YqbOrderPushController {
+
+    @Autowired
+    private YqbOrderPushService yqbOrderPushService;
+
+    /**
+     * POST /api/sdzl/yqb/order/push
+     * 请求体:{"formInstanceId":"FINST-xxx"}
+     */
+    @PostMapping("/push")
+    public McR pushOrder(@RequestBody YqbOrderPushRequest request) {
+        log.info("开始根据宜搭表单生成易企报单据,formInstanceId={}",
+                request == null ? null : request.getFormInstanceId());
+        YqbOrderPushResult result = yqbOrderPushService.pushOrder(request);
+        return McR.success(result);
+    }
+}

+ 39 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/entity/dto/FieldDefinitionDTO.java

@@ -0,0 +1,39 @@
+package com.malk.sdzl.entity.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * 字段定义 DTO
+ *
+ * <p>描述子表单中每个子字段的元信息,用于图片渲染时显示列标题和格式化数据。</p>
+ */
+@Data
+public class FieldDefinitionDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 字段ID,如 selectField_mmad6jci */
+    @NotBlank(message = "字段ID不能为空")
+    private String fieldId;
+
+    /** 字段标签(列标题),如 "项目名称" */
+    @NotBlank(message = "字段标签不能为空")
+    private String label;
+
+    /**
+     * 字段类型(宜搭组件标识),如:
+     * TextField, NumberField, SelectField, DateField, EmployeeField 等
+     * 用于后端做特殊格式化处理
+     */
+    private String type;
+
+    /**
+     * 字段选项列表(仅 SelectField / RadioField / CheckboxField 等选择类组件适用)
+     * JSON 字符串格式,例如:"[\"内部\",\"外部\"]"
+     * 后端可据此将选项 value 映射为显示文本
+     */
+    private String options;
+}

+ 51 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/entity/dto/YqbCreateOrderRequest.java

@@ -0,0 +1,51 @@
+package com.malk.sdzl.entity.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/** 易企报“生成单据”接口请求。 */
+@Data
+public class YqbCreateOrderRequest implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private String submitterId;
+    private List<Order> orderList = new ArrayList<>();
+
+    public Map<String, Object> toRequestBody() {
+        Map<String, Object> body = new LinkedHashMap<>();
+        body.put("submitterId", submitterId);
+
+        List<Map<String, Object>> orders = new ArrayList<>();
+        for (Order order : orderList) {
+            Map<String, Object> orderBody = new LinkedHashMap<>();
+            orderBody.put("id", order.getId());
+            orderBody.put("templateCode", order.getTemplateCode());
+            orderBody.put("pushOrderStatus", order.getPushOrderStatus());
+            orderBody.put("submitAmount", order.getSubmitAmount());
+            orderBody.put("approvalUser", order.getApprovalUser());
+            orderBody.put("orderComponentList", order.getOrderComponentList());
+            orders.add(orderBody);
+        }
+        body.put("orderList", orders);
+        return body;
+    }
+
+    @Data
+    public static class Order implements Serializable {
+        private static final long serialVersionUID = 1L;
+
+        private String id;
+        private String templateCode;
+        private String pushOrderStatus;
+        private BigDecimal submitAmount;
+        private String approvalUser;
+        private List<YqbOrderComponent> orderComponentList = new ArrayList<>();
+    }
+}

+ 22 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/entity/dto/YqbOrderComponent.java

@@ -0,0 +1,22 @@
+package com.malk.sdzl.entity.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/** 易企报单据组件值。 */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class YqbOrderComponent implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 易企报模板组件编码。 */
+    private String code;
+
+    /** 组件绑定值;普通组件为字符串,附件组件为下载地址列表。 */
+    private Object bindingVal;
+}

+ 15 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/entity/dto/YqbOrderPushRequest.java

@@ -0,0 +1,15 @@
+package com.malk.sdzl.entity.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/** 根据宜搭表单实例生成并推送易企报单据的请求。 */
+@Data
+public class YqbOrderPushRequest implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 宜搭表单实例 ID。 */
+    private String formInstanceId;
+}

+ 20 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/entity/dto/YqbOrderPushResult.java

@@ -0,0 +1,20 @@
+package com.malk.sdzl.entity.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/** 宜搭表单生成并推送易企报单据的结果。 */
+@Data
+public class YqbOrderPushResult implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private String formInstanceId;
+    private String templateCode;
+    private int componentCount;
+    private List<YqbOrderComponent> orderComponentList = new ArrayList<>();
+    private YqbApiResponse<Object> yqbResponse;
+}

+ 49 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/service/EqbService.java

@@ -0,0 +1,49 @@
+package com.malk.sdzl.service;
+
+import com.malk.server.common.McR;
+
+import java.util.Map;
+
+public interface EqbService {
+
+    void autoAgree(String signFlowId,String processInstanceId);
+
+    McR sign(Map map);
+
+    /**
+     * 上传本地合同或宜搭表单附件,确认服务端文件就绪后发起签署;本方法同步等待整个调用流程结束。
+     * 每个文件的状态轮询最长90秒,此时间不包含上传文件和创建签署流程的耗时。
+     *
+     * @param params 请求参数,约定如下:
+     *               表单模式需传formUuid和formInstanceId,使用请求中的formUuid匹配json/party-fields.json;
+     *               formInstanceId用于查询宜搭实例,formData中无需包含表单UUID;
+     *               甲乙丙丁方编号有值时,分别按serialNumberField_mrlipwdo查询相对方登记表并生成signers;
+     *               空编号跳过,登记信息缺失或不唯一时停止;此模式忽略调用方传入的signers;
+     *               读取两个约定的附件组件,先下载到eqb.downloadFilePath,再逐文件上传;
+     *               全部附件确认就绪后合并发起一次签署,每次调用只为该表单创建一个流程;
+     *               不传formUuid和formInstanceId时,fileName必填,为该目录内的文件名(包含真实扩展名);
+     *               本地文件模式的signers必填,为1至10个签署方,个人传psnSignerInfo,企业手动签署传orgSignerInfo及经办人;
+     *               表单模式当前统一设置autoSign=true、freeMode=true,不传企业经办人;自然人因e签宝不支持自动签署而在上传前报错;
+     *               跨企业自动签署仍需具备e签宝印章授权,自由签章与自动签署组合的实际行为需联调确认;
+     *               signFlowConfig可选,主题默认使用文件名(表单模式取第一个附件名),自动开启流程,autoFinish默认true;
+     *               convertToPDF、convertToHTML、convertToOFD可选且互斥,默认false,非PDF合同需指定转换格式;
+     *               未传签署区时默认自由签章,每位签署方的签署区配置会应用到每个附件,fileId由服务端绑定;
+     *               当任一签署区normalSignFieldConfig.autoSign=true时,必须传顶层autoSignKeywords数组;
+     *               关键字数组按自动签署区在signers中的出现顺序一一匹配。服务端在文件上传完成后调用
+     *               e签宝关键字定位接口,并将首个命中坐标写入signFieldPosition(positionPage、positionX、positionY);
+     *               自动定位签署区不能设置freeMode=true,signFieldStyle仅支持1(单页签章);
+     *               同一流程最多50个文件、总大小500MB,展开后的签署区总数不能超过300个。
+     * @return 成功时data包含fileId、fileName、fileSize(原文件字节数)、fileStatus及signFlowId;
+     *         失败业务码:400参数错误、404本地文件不可用、504文件状态轮询超时、500其他处理错误。
+     *         返回签署流程ID仅表示流程已创建,不代表签署方已经完成签署。
+     *         表单模式的data包含formInstance、统一的signFlowId和files,files逐项返回附件来源及上传结果;
+     *         成功时各附件共享同一个signFlowId;创建失败时已上传附件保持UPLOADED状态,便于排查;
+     *         附件模式未指定转换开关时,非PDF文件默认转PDF;任一附件处理失败均不创建签署流程。
+     */
+    McR uploadFile(Map<String, Object> params);
+
+    /**
+     * 获取签署流程中已签署文件的临时下载地址。
+     */
+    Map getSignedFileDownloadUrls(String signFlowId);
+}

File diff suppressed because it is too large
+ 2260 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/service/Impl/EqbServiceImpl.java


+ 26 - 1
mjava-sdzl/src/main/java/com/malk/sdzl/service/Impl/PayServiceImpl.java

@@ -1751,7 +1751,32 @@ public class PayServiceImpl implements PayService {
             throw new IllegalStateException("宜搭未查询到流程表单数据,processInstanceId="
                     + request.getProcessInstanceId());
         }
+        Object processTask = ydClient.queryData(YDParam.builder()
+                .appType(appType)
+                .systemToken(systemToken)
+                .processInstanceId(request.getProcessInstanceId())
+                .build(), YDConf.FORM_QUERY.retrieve_approval_record).getResult();
+        JSONArray jsonArray = JSONArray.parseArray(processTask.toString());
 
+        JSONObject processTaskData = jsonArray.stream()
+                .map(obj -> (JSONObject) obj)
+                .filter(jsonObj -> {
+                    String dateStr = jsonObj.getString("operateTimeGMT");
+                    return dateStr != null && !dateStr.trim().isEmpty();
+                })
+                .sorted(Comparator.comparing(
+                        jsonObj -> OffsetDateTime.parse(jsonObj.getString("operateTimeGMT"),DateTimeFormatter.ISO_OFFSET_DATE_TIME),
+                        Comparator.reverseOrder()
+                ))
+                .findFirst().get();
+        if (ObjectUtil.isNotNull(processTaskData)){
+            request.setColId(UtilMap.getString(processTaskData,"taskId"));
+            request.setFlowState(UtilMap.getString(processTaskData,"action"));
+            request.setMessage(UtilMap.getString(processTaskData,"remark"));
+//            bodyMap.put("colId",processTaskData.get("taskId"));
+//            bodyMap.put("message",processTaskData.get("remark"));
+//            bodyMap.put("flowState",processTaskData.get("action"));
+        }
         Map responseData = asMap(formResponse.getData());
         Map responseResult = asMap(formResponse.getResult());
         Map formData = formResponse.getFormData();
@@ -1791,7 +1816,7 @@ public class PayServiceImpl implements PayService {
         }
         if (isBlank(request.getOaCode())) {
             request.setOaCode(firstString(formData,
-                    "oaCode", "oaCoding", "code",
+                    "serialNumberField_mudxcsdb",
                     "textField_mpxojd0l", "textField_mpxnklyj"));
             if (isBlank(request.getOaCode())) {
                 request.setOaCode(firstString(responseData, "oaCode", "oaCoding", "code"));

+ 388 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/service/Impl/YqbOrderPushServiceImpl.java

@@ -0,0 +1,388 @@
+package com.malk.sdzl.service.Impl;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.sdzl.common.YqbApiClient;
+import com.malk.sdzl.entity.dto.YqbApiResponse;
+import com.malk.sdzl.entity.dto.YqbCreateOrderRequest;
+import com.malk.sdzl.entity.dto.YqbOrderComponent;
+import com.malk.sdzl.entity.dto.YqbOrderPushRequest;
+import com.malk.sdzl.entity.dto.YqbOrderPushResult;
+import com.malk.sdzl.service.YqbOrderPushService;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.dingtalk.DDR_New;
+import com.malk.service.aliwork.YDClient;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/** 宜搭采购合同表单推送易企报单据服务。 */
+@Slf4j
+@Service
+public class YqbOrderPushServiceImpl implements YqbOrderPushService {
+
+    private static final String CREATE_ORDER_PATH = "/reimburse/order/createOrder.rjson";
+    private static final String FORM_UUID = "FORM-15D01921";
+    private static final String TEMPLATE_CODE = "payable";
+    private static final String PUSH_ORDER_STATUS = "3";
+    private static final String AMOUNT_FIELD_ID = "numberField_mpgp9h5c";
+    private static final String APPROVAL_USER_FIELD_ID = "employeeField_mt2q233g";
+    private static final String DATE_FIELD_PREFIX = "dateField";
+    private static final String ATTACHMENT_FIELD_PREFIX = "attachmentField";
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter
+            .ofPattern("yyyy-MM-dd")
+            .withZone(ZoneId.of("Asia/Shanghai"));
+
+    /**
+     * 来自“宜搭_FORM-15D01921_未隐藏字段清单.xlsx”和“有成字段标识.xlsx”的精确同名映射。
+     * 布尔值为 true 时优先读取宜搭成员组件返回的 fieldId_id。
+     */
+    private static final List<FieldMapping> FIELD_MAPPINGS = Collections.unmodifiableList(Arrays.asList(
+            mapping("selectField_x1tbicc", "1305945736675848205"),//所属板块
+            mapping("selectField_mrivw3rx", "1305945736675848204"),//核算主体
+            mapping("selectField_mrivw3rv", "1305945736675848203"),//考核主体
+            mapping("selectField_mrivw3rz", "1305945736675848202"),//成本中心
+            memberMapping("employeeField_mpgn2182", "1302720250164277267"),
+            mapping("textField_mpgn2183", "1302720250164277266"),
+            mapping("selectField_mrszgkej", "1305945736675848201"),//核算项目
+            mapping("textField_mrsztw4u", "1305945736675848207"),
+            mapping("textField_mpgp9h56", "title"),//合同名称
+            mapping("selectField_mpgp9h5b", "1302720250164277265"),
+            mapping("numberField_mpgp9h5c", "amount"),//合同总金额
+            mapping("originator", "proposer"),//申请人
+            mapping("radioField_mpgp9h5h", "1270067169387868173"),
+            mapping("radioField_mpgp9h5j", "1270071560023044097"),
+            mapping("selectField_mtuyhtqn", "1302720250164277264"),
+            mapping("textField_mpgp9h60", "1302720250164277263"),
+            mapping("selectField_mrsztw4n", "1302720250164277262"),
+            mapping("dateField_mpgp9h6g", "1302720250164277261"),
+            mapping("dateField_mpgp9h6h", "1302720250164277260"),
+//            mapping("dateField_mpgp9h6i", "contractDate"),//提交日期
+            mapping("dateField_mpgp9h6i", "1302720250164277259"),
+            mapping("dateField_mpgp9h6j", "1302720250164277258"),
+            mapping("dateField_mpgp9h6k", "1302720250164277257"),
+            mapping("radioField_mtuyhtqo", "1302720250164277256"),
+            mapping("attachmentField_mpgp9h6l", "attachment"),
+            mapping("attachmentField_mpgp9h77", "1302720250164277255"),
+            mapping("attachmentField_mtuz6lsw", "1302720250164277254"),
+            mapping("textareaField_mpgp9h6m", "1302720250164277253"),
+            mapping("selectField_mpgp9h6r", "1302720250164277252"),
+            mapping("cascadeDateField_mpgp9h6s", "1302720250164277251"),
+            mapping("selectField_mpgp9h6x", "1302720250164277250"),
+            mapping("textField_mpgp9h74", "1302720250164277249"),
+            mapping("textField_mpgp9h76", "1302722528027865107"),
+            mapping("attachmentField_mpgp9h6z", "1302722528027865106")
+    ));
+
+    @Autowired
+    private YDClient ydClient;
+
+    @Autowired
+    private YqbApiClient yqbApiClient;
+
+    @Value("${aliwork.appType}")
+    private String appType;
+
+    @Value("${aliwork.systemToken}")
+    private String systemToken;
+
+    @Override
+    public YqbOrderPushResult pushOrder(YqbOrderPushRequest request) {
+        String formInstanceId = validateAndGetFormInstanceId(request);
+        validateYidaConfig();
+
+        DDR_New<?> form = ydClient.queryData(YDParam.builder()
+                .appType(appType)
+                .systemToken(systemToken)
+                .formUuid(FORM_UUID)
+                .formInstanceId(formInstanceId)
+                .build(), YDConf.FORM_QUERY.retrieve_id);
+        if (form == null || form.getFormData() == null) {
+            throw new IllegalStateException("查询宜搭表单失败或未返回表单数据,formInstanceId=" + formInstanceId);
+        }
+
+        Map<String, Object> formData = form.getFormData();
+        String submitterId = getSubmitterId(form.getOriginator());
+//        String approvalUser = extractFirstId(getPreferredMemberValue(formData, APPROVAL_USER_FIELD_ID));
+//        if (isBlank(approvalUser)) {
+//            throw new IllegalArgumentException("宜搭表单的主管字段为空,无法设置易企报首节点审批人");
+//        }
+        String approvalUser = "11";
+
+        BigDecimal submitAmount = parseAmount(formData.get(AMOUNT_FIELD_ID));
+        List<YqbOrderComponent> components = buildOrderComponents(formData);
+        YqbOrderComponent yqbOrderComponent = new YqbOrderComponent();
+        yqbOrderComponent.setCode("proposer");
+        yqbOrderComponent.setBindingVal(submitterId);
+
+        components.add(yqbOrderComponent);
+
+        YqbOrderComponent yqbOrderComponent2 = new YqbOrderComponent();
+        yqbOrderComponent2.setCode("contractDate");
+        yqbOrderComponent2.setBindingVal(LocalDate.now().format(DATE_FORMATTER));
+
+        components.add(yqbOrderComponent2);
+
+
+        YqbCreateOrderRequest createRequest = new YqbCreateOrderRequest();
+        createRequest.setSubmitterId(submitterId);
+        YqbCreateOrderRequest.Order order = new YqbCreateOrderRequest.Order();
+        order.setId(formInstanceId);
+        order.setTemplateCode(TEMPLATE_CODE);
+        order.setPushOrderStatus(PUSH_ORDER_STATUS);
+        order.setSubmitAmount(submitAmount);
+        order.setApprovalUser(approvalUser);
+        order.setOrderComponentList(components);
+        createRequest.setOrderList(Collections.singletonList(order));
+
+        YqbApiResponse<Object> response = yqbApiClient.post(
+                CREATE_ORDER_PATH, createRequest.toRequestBody(), "生成单据");
+        if (!response.isSuccess()) {
+            throw new IllegalStateException("易企报生成单据失败:result=" + response.getResult()
+                    + ",message=" + response.getMessage());
+        }
+
+        YqbOrderPushResult result = new YqbOrderPushResult();
+        result.setFormInstanceId(formInstanceId);
+        result.setTemplateCode(TEMPLATE_CODE);
+        result.setComponentCount(components.size());
+        result.setOrderComponentList(components);
+        result.setYqbResponse(response);
+        log.info("宜搭表单推送易企报成功,formInstanceId={},componentCount={},result={}",
+                formInstanceId, components.size(), response.getResult());
+        return result;
+    }
+
+    private List<YqbOrderComponent> buildOrderComponents(Map<String, Object> formData) {
+        List<YqbOrderComponent> components = new ArrayList<>();
+        for (FieldMapping mapping : FIELD_MAPPINGS) {
+            Object value = mapping.memberField
+                    ? getPreferredMemberValue(formData, mapping.fieldId)
+                    : formData.get(mapping.fieldId);
+            if (isEmptyValue(value)) {
+                continue;
+            }
+            Object bindingValue = toBindingValue(mapping, value);
+            if (isEmptyValue(bindingValue)) {
+                continue;
+            }
+            components.add(new YqbOrderComponent(mapping.componentCode, bindingValue));
+        }
+        return components;
+    }
+
+    private Object getPreferredMemberValue(Map<String, Object> formData, String fieldId) {
+        Object idValue = formData.get(fieldId + "_id");
+        return isEmptyValue(idValue) ? formData.get(fieldId) : idValue;
+    }
+
+    private String getSubmitterId(Map originator) {
+        Object userId = originator == null ? null : originator.get("userId");
+        String submitterId = userId == null ? null : String.valueOf(userId).trim();
+        if (isBlank(submitterId)) {
+            throw new IllegalStateException("宜搭表单未返回发起人 userId,无法设置易企报提交人");
+        }
+        return submitterId;
+    }
+
+    private BigDecimal parseAmount(Object amountValue) {
+        if (isEmptyValue(amountValue)) {
+            throw new IllegalArgumentException("宜搭表单的合同总金额字段为空");
+        }
+        if (amountValue instanceof BigDecimal) {
+            return (BigDecimal) amountValue;
+        }
+        if (amountValue instanceof Number) {
+            return new BigDecimal(amountValue.toString());
+        }
+        String amountText = String.valueOf(amountValue).trim().replace(",", "");
+        try {
+            return new BigDecimal(amountText);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("宜搭表单的合同总金额不是有效数字:" + amountValue, e);
+        }
+    }
+
+    private String extractFirstId(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Collection) {
+            for (Object item : (Collection<?>) value) {
+                String id = extractFirstId(item);
+                if (!isBlank(id)) {
+                    return id;
+                }
+            }
+            return null;
+        }
+        if (value instanceof Map) {
+            Map<?, ?> map = (Map<?, ?>) value;
+            for (String key : Arrays.asList("userId", "id", "value", "staffId")) {
+                String id = extractFirstId(map.get(key));
+                if (!isBlank(id)) {
+                    return id;
+                }
+            }
+            return null;
+        }
+        String text = String.valueOf(value).trim();
+        if (isBlank(text)) {
+            return null;
+        }
+        if ((text.startsWith("[") && text.endsWith("]"))
+                || (text.startsWith("{") && text.endsWith("}"))) {
+            try {
+                return extractFirstId(JSON.parse(text));
+            } catch (Exception ignored) {
+                // 不是有效 JSON 时按普通字符串处理。
+            }
+        }
+        return text;
+    }
+
+    private Object toBindingValue(FieldMapping mapping, Object value) {
+        if (mapping.fieldId.startsWith(DATE_FIELD_PREFIX)) {
+            Long epochMillis = getEpochMillis(value);
+            if (epochMillis != null) {
+                return DATE_FORMATTER.format(Instant.ofEpochMilli(epochMillis));
+            }
+        }
+        if (mapping.fieldId.startsWith(ATTACHMENT_FIELD_PREFIX)) {
+            return getAttachmentTemporaryUrls(mapping.fieldId, value);
+        }
+        if (value instanceof Map || value instanceof Collection || value.getClass().isArray()) {
+            return JSON.toJSONString(value);
+        }
+        return String.valueOf(value);
+    }
+
+    private List<String> getAttachmentTemporaryUrls(String fieldId, Object value) {
+        Object attachmentValue = value;
+        if (value instanceof String) {
+            String text = ((String) value).trim();
+            if (text.isEmpty()) {
+                return Collections.emptyList();
+            }
+            try {
+                attachmentValue = JSON.parse(text);
+            } catch (Exception e) {
+                log.warn("宜搭附件组件值不是有效 JSON,已跳过:{}", text);
+                return Collections.emptyList();
+            }
+        }
+
+        List<String> temporaryUrls = new ArrayList<>();
+        collectAttachmentTemporaryUrls(fieldId, attachmentValue, temporaryUrls);
+        return temporaryUrls;
+    }
+
+    private void collectAttachmentTemporaryUrls(String fieldId, Object attachmentValue,
+                                                List<String> temporaryUrls) {
+        if (attachmentValue instanceof Collection) {
+            for (Object attachment : (Collection<?>) attachmentValue) {
+                collectAttachmentTemporaryUrls(fieldId, attachment, temporaryUrls);
+            }
+            return;
+        }
+        if (!(attachmentValue instanceof Map)) {
+            return;
+        }
+        Object downloadUrlValue = ((Map<?, ?>) attachmentValue).get("downloadUrl");
+        if (downloadUrlValue == null) {
+            return;
+        }
+        String downloadUrl = String.valueOf(downloadUrlValue).trim();
+        if (!downloadUrl.isEmpty()) {
+            String temporaryUrl = ydClient.convertTemporaryUrl(downloadUrl, appType, systemToken);
+            if (isBlank(temporaryUrl)) {
+                throw new IllegalStateException("宜搭附件字段" + fieldId + "换取临时免登地址失败");
+            }
+            temporaryUrls.add(temporaryUrl);
+        }
+    }
+
+    private Long getEpochMillis(Object value) {
+        if (value instanceof Number) {
+            return ((Number) value).longValue();
+        }
+        if (!(value instanceof String)) {
+            return null;
+        }
+        String text = ((String) value).trim();
+        if (!text.matches("-?\\d{12,14}")) {
+            return null;
+        }
+        try {
+            return Long.valueOf(text);
+        } catch (NumberFormatException ignored) {
+            return null;
+        }
+    }
+
+    private boolean isEmptyValue(Object value) {
+        if (value == null) {
+            return true;
+        }
+        if (value instanceof String) {
+            return ((String) value).trim().isEmpty();
+        }
+        if (value instanceof Collection) {
+            return ((Collection<?>) value).isEmpty();
+        }
+        if (value instanceof Map) {
+            return ((Map<?, ?>) value).isEmpty();
+        }
+        return false;
+    }
+
+    private String validateAndGetFormInstanceId(YqbOrderPushRequest request) {
+        if (request == null || isBlank(request.getFormInstanceId())) {
+            throw new IllegalArgumentException("formInstanceId不能为空");
+        }
+        return request.getFormInstanceId().trim();
+    }
+
+    private void validateYidaConfig() {
+        if (isBlank(appType) || isBlank(systemToken)) {
+            throw new IllegalStateException("未配置aliwork.appType或aliwork.systemToken");
+        }
+    }
+
+    private boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+
+    private static FieldMapping mapping(String fieldId, String componentCode) {
+        return new FieldMapping(fieldId, componentCode, false);
+    }
+
+    private static FieldMapping memberMapping(String fieldId, String componentCode) {
+        return new FieldMapping(fieldId, componentCode, true);
+    }
+
+    private static class FieldMapping {
+        private final String fieldId;
+        private final String componentCode;
+        private final boolean memberField;
+
+        private FieldMapping(String fieldId, String componentCode, boolean memberField) {
+            this.fieldId = fieldId;
+            this.componentCode = componentCode;
+            this.memberField = memberField;
+        }
+    }
+}

+ 10 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/service/YqbOrderPushService.java

@@ -0,0 +1,10 @@
+package com.malk.sdzl.service;
+
+import com.malk.sdzl.entity.dto.YqbOrderPushRequest;
+import com.malk.sdzl.entity.dto.YqbOrderPushResult;
+
+public interface YqbOrderPushService {
+
+    /** 查询宜搭表单实例并生成易企报单据。 */
+    YqbOrderPushResult pushOrder(YqbOrderPushRequest request);
+}

+ 262 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/util/HTTPHelper.java

@@ -0,0 +1,262 @@
+package com.malk.sdzl.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
+
+public class HTTPHelper {
+    // slf4j日志记录器
+    private static final Logger LOG = LoggerFactory.getLogger(HTTPHelper.class);
+
+    /***
+     * 向指定URL发送GET方法的请求
+     *
+     * @param apiUrl
+     * @param encoding
+     * @return
+     * @throws Exception
+     */
+    public static String sendGet(String apiUrl, HashMap<String, Object> param,
+                                 LinkedHashMap<String, String> headers, String encoding) throws Exception {
+        // 获得响应内容
+        String http_RespContent = null;
+        HttpURLConnection httpURLConnection = null;
+        int http_StatusCode = 0;
+        String http_RespMessage = null;
+        try {
+            // 实际请求完整Url
+            StringBuffer fullUrl = new StringBuffer();
+            if (null != param) {
+                if (0 != param.size()) {
+                    StringBuffer params = new StringBuffer();
+                    for (Entry<String, Object> entry : param.entrySet()) {
+                        params.append(entry.getKey());
+                        params.append("=");
+                        params.append(entry.getValue().toString());
+                        params.append("&");
+                    }
+                    if (params.length() > 0) {
+                        params.deleteCharAt(params.lastIndexOf("&"));
+                    }
+                    fullUrl.append(apiUrl).append((params.length() > 0) ? "?" + params.toString() : "");
+                } else {
+                    fullUrl.append(apiUrl);
+                }
+            } else {
+                fullUrl.append(apiUrl);
+            }
+
+            LOG.info(">>>> 实际请求Url: " + fullUrl.toString());
+
+            // 建立连接
+            URL url = new URL(fullUrl.toString());
+            httpURLConnection = (HttpURLConnection) url.openConnection();
+            // 需要输出
+            httpURLConnection.setDoOutput(true);
+            // 需要输入
+            httpURLConnection.setDoInput(true);
+            // 不允许缓存
+            httpURLConnection.setUseCaches(false);
+            // HTTP请求方式
+            httpURLConnection.setRequestMethod("GET");
+            // 设置Headers
+            if (null != headers) {
+                for (String key : headers.keySet()) {
+                    httpURLConnection.setRequestProperty(key, headers.get(key));
+                }
+            }
+            // 连接会话
+            httpURLConnection.connect();
+            // 获得响应状态(HTTP状态码)
+            http_StatusCode = httpURLConnection.getResponseCode();
+            // 获得响应消息(HTTP状态码描述)
+            http_RespMessage = httpURLConnection.getResponseMessage();
+            // 获得响应内容
+            if (HttpURLConnection.HTTP_OK == http_StatusCode) {
+                // 返回响应结果
+                http_RespContent = getResponseContent(httpURLConnection);
+            } else {
+                // 返回非200状态时响应结果
+                http_RespContent = getErrorResponseContent(httpURLConnection);
+                String msg =
+                        MessageFormat.format("请求失败: Http状态码 = {0} , {1}", http_StatusCode, http_RespMessage);
+                LOG.info(msg);
+            }
+        } catch (UnknownHostException e) {
+            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
+            Exception ex = new Exception(message);
+            ex.initCause(e);
+            throw ex;
+        } catch (MalformedURLException e) {
+            String message = MessageFormat.format("格式错误的URL: {0}", e.getMessage());
+            Exception ex = new Exception(message);
+            ex.initCause(e);
+            throw ex;
+        } catch (IOException e) {
+            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
+            Exception ex = new Exception(message);
+            ex.initCause(e);
+            throw ex;
+        } catch (Exception e) {
+            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
+            Exception ex = new Exception(message);
+            ex.initCause(e);
+            throw ex;
+        } finally {
+            if (null != httpURLConnection) {
+                httpURLConnection.disconnect();
+            }
+        }
+        return http_RespContent;
+    }
+
+    /***
+     * 向指定URL发送POST方法的请求
+     *
+     * @param apiUrl
+     * @param data
+     * @param encoding
+     * @return
+     * @throws Exception
+     */
+    public static String sendPOST(String apiUrl, String data, LinkedHashMap<String, String> headers,
+                                  String encoding) throws Exception {
+        // 获得响应内容
+        String http_RespContent = null;
+        HttpURLConnection httpURLConnection = null;
+        int http_StatusCode = 0;
+        String http_RespMessage = null;
+        try {
+            // 建立连接
+            URL url = new URL(apiUrl);
+            httpURLConnection = (HttpURLConnection) url.openConnection();
+            // 需要输出
+            httpURLConnection.setDoOutput(true);
+            // 需要输入
+            httpURLConnection.setDoInput(true);
+            // 不允许缓存
+            httpURLConnection.setUseCaches(false);
+            // HTTP请求方式
+            httpURLConnection.setRequestMethod("POST");
+            // 设置Headers
+            if (null != headers) {
+                for (String key : headers.keySet()) {
+                    httpURLConnection.setRequestProperty(key, headers.get(key));
+                }
+            }
+            // 连接会话
+            httpURLConnection.connect();
+            // 建立输入流,向指向的URL传入参数
+            DataOutputStream dos = new DataOutputStream(httpURLConnection.getOutputStream());
+            // 设置请求参数
+            dos.write(data.getBytes(encoding));
+            dos.flush();
+            dos.close();
+            // 获得响应状态(HTTP状态码)
+            http_StatusCode = httpURLConnection.getResponseCode();
+            // 获得响应消息(HTTP状态码描述)
+            http_RespMessage = httpURLConnection.getResponseMessage();
+            // 获得响应内容
+            if (HttpURLConnection.HTTP_OK == http_StatusCode) {
+                // 返回响应结果
+                http_RespContent = getResponseContent(httpURLConnection);
+            } else {
+                // 返回非200状态时响应结果
+                http_RespContent = getErrorResponseContent(httpURLConnection);
+                String msg =
+                        MessageFormat.format("请求失败: Http状态码 = {0} , {1}", http_StatusCode, http_RespMessage);
+                LOG.info(msg);
+            }
+        } catch (UnknownHostException e) {
+            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
+            Exception ex = new Exception(message);
+            ex.initCause(e);
+            throw ex;
+        } catch (MalformedURLException e) {
+            String message = MessageFormat.format("格式错误的URL: {0}", e.getMessage());
+            Exception ex = new Exception(message);
+            ex.initCause(e);
+            throw ex;
+        } catch (IOException e) {
+            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
+            Exception ex = new Exception(message);
+            ex.initCause(e);
+            throw ex;
+        } catch (Exception e) {
+            String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage());
+            Exception ex = new Exception(message);
+            ex.initCause(e);
+            throw ex;
+        } finally {
+            if (null != httpURLConnection) {
+                httpURLConnection.disconnect();
+            }
+        }
+        return http_RespContent;
+    }
+
+    /***
+     * 读取HttpResponse响应内容
+     *
+     * @param httpURLConnection
+     * @return
+     * @throws UnsupportedEncodingException
+     * @throws IOException
+     */
+    private static String getResponseContent(HttpURLConnection httpURLConnection)
+            throws UnsupportedEncodingException, IOException {
+        StringBuffer contentBuffer = null;
+        BufferedReader responseReader = null;
+        try {
+            contentBuffer = new StringBuffer();
+            String readLine = null;
+            responseReader =
+                    new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(), "UTF-8"));
+            while ((readLine = responseReader.readLine()) != null) {
+                contentBuffer.append(readLine);
+            }
+        } finally {
+            if (null != responseReader) {
+                responseReader.close();
+            }
+        }
+        return contentBuffer.toString();
+    }
+
+    /***
+     * 读取HttpResponse响应内容
+     *
+     * @param httpURLConnection
+     * @return
+     * @throws UnsupportedEncodingException
+     * @throws IOException
+     */
+    private static String getErrorResponseContent(HttpURLConnection httpURLConnection)
+            throws UnsupportedEncodingException, IOException {
+        StringBuffer contentBuffer = null;
+        BufferedReader responseReader = null;
+        try {
+            contentBuffer = new StringBuffer();
+            String readLine = null;
+            responseReader =
+                    new BufferedReader(new InputStreamReader(httpURLConnection.getErrorStream(), "UTF-8"));
+            while ((readLine = responseReader.readLine()) != null) {
+                contentBuffer.append(readLine);
+            }
+        } finally {
+            if (null != responseReader) {
+                responseReader.close();
+            }
+        }
+        return contentBuffer.toString();
+    }
+}

+ 627 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/util/TableImageGenerator.java

@@ -0,0 +1,627 @@
+package com.malk.sdzl.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.malk.sdzl.config.ImageConfig;
+import com.malk.sdzl.entity.dto.FieldDefinitionDTO;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.List;
+
+/**
+ * 表格图片生成器
+ *
+ * <p>使用 Java AWT Graphics2D 将宜搭子表数据渲染为表格图片。</p>
+ * <p>支持:中文字体自适应、列宽自适应、长文本自动换行、表头样式、隔行变色。</p>
+ *
+ * <p>JDK 1.8 兼容,无外部图片库依赖。</p>
+ */
+public class TableImageGenerator {
+
+    // ===== 颜色常量 =====
+    private static final Color HEADER_BG = Color.WHITE;
+    private static final Color HEADER_FG = Color.BLACK;
+    private static final Color ROW_BG_EVEN = Color.WHITE;
+    private static final Color ROW_BG_ODD = Color.WHITE;
+    private static final Color GRID_COLOR = new Color(220, 223, 230);
+    private static final Color TEXT_COLOR = new Color(48, 49, 51);
+    private static final Color TITLE_COLOR = new Color(48, 49, 51);
+
+    // ===== 字体候选列表(按优先级,覆盖 Windows / Linux 常见中文字体) =====
+    private static final String[] FONT_CANDIDATES = {
+            "Microsoft YaHei",   // Windows 微软雅黑
+            "SimHei",            // Windows 黑体
+            "SimSun",            // Windows 宋体
+            "Noto Sans CJK SC", // Linux 思源黑体
+            "WenQuanYi Micro Hei", // Linux 文泉驿微米黑
+            "SansSerif"          // 通用回退
+    };
+
+    private final ImageConfig config;
+    private final Font titleFont;
+    private final Font headerFont;
+    private final Font cellFont;
+
+    public TableImageGenerator(ImageConfig config) {
+        this.config = config;
+        String fontName = resolveChineseFont();
+        this.titleFont = new Font(fontName, Font.BOLD, config.getTitleFontSize());
+        this.headerFont = new Font(fontName, Font.BOLD, config.getHeaderFontSize());
+        this.cellFont = new Font(fontName, Font.PLAIN, config.getCellFontSize());
+    }
+
+    /**
+     * 解析可用的中文字体
+     */
+    private String resolveChineseFont() {
+        // 中文字符测试
+        char testChar = '\u4e2d'; // "中"
+        for (String name : FONT_CANDIDATES) {
+            Font f = new Font(name, Font.PLAIN, 12);
+            if (f.canDisplay(testChar)) {
+                return name;
+            }
+        }
+        return Font.SANS_SERIF;
+    }
+
+    /**
+     * 生成表格图片
+     *
+     * @param title       表格标题(可为 null,如子表名称)
+     * @param fieldDefs   字段定义列表(决定列顺序和列标题)
+     * @param dataRows    子表原始数据(每行一个 Map)
+     * @return 生成的 BufferedImage
+     */
+    public BufferedImage generate(String title,
+                                  List<FieldDefinitionDTO> fieldDefs,
+                                  List<Map<String, Object>> dataRows) {
+
+        // 1. 准备表格数据:列标题 + 字符串化的行数据
+        List<String> headers = new ArrayList<>();
+        List<String> fieldIds = new ArrayList<>();
+        for (FieldDefinitionDTO fd : fieldDefs) {
+            headers.add(fd.getLabel());
+            fieldIds.add(fd.getFieldId());
+        }
+
+        List<List<String>> stringRows = new ArrayList<>();
+        for (Map<String, Object> row : dataRows) {
+            List<String> stringRow = new ArrayList<>();
+            for (int i = 0; i < fieldIds.size(); i++) {
+                String fieldId = fieldIds.get(i);
+                FieldDefinitionDTO fd = fieldDefs.get(i);
+                Object value = row.get(fieldId);
+                stringRow.add(formatCellValue(value, fd));
+            }
+            stringRows.add(stringRow);
+        }
+
+        // 2. 计算列宽
+        int[] colWidths = calculateColumnWidths(headers, stringRows);
+
+        // 3. 计算行高(考虑文本换行)
+        int titleHeight = (title != null && !title.isEmpty()) ? config.getTitleFontSize() + 24 : 0;
+        int[] rowHeights = calculateRowHeights(headers, stringRows, colWidths);
+        int headerHeight = config.getRowHeight();
+
+        // 4. 计算图片总尺寸
+        int totalWidth = config.getCellPadding() * 2; // 左右边距
+        for (int w : colWidths) {
+            totalWidth += w;
+        }
+
+        int totalHeight = titleHeight + headerHeight;
+        for (int h : rowHeights) {
+            totalHeight += h;
+        }
+
+        // 确保最小尺寸
+        totalWidth = Math.max(totalWidth, 200);
+        totalHeight = Math.max(totalHeight, 100);
+
+        // 5. 创建图片
+        BufferedImage image = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_RGB);
+        Graphics2D g2d = image.createGraphics();
+
+        // 抗锯齿
+        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+
+        // 6. 绘制背景
+        Color bgColor = parseColor(config.getBackground(), Color.WHITE);
+        g2d.setColor(bgColor);
+        g2d.fillRect(0, 0, totalWidth, totalHeight);
+
+        int y = 0;
+
+        // 7. 绘制标题
+        if (titleHeight > 0) {
+            g2d.setColor(TITLE_COLOR);
+            g2d.setFont(titleFont);
+            FontMetrics titleFm = g2d.getFontMetrics();
+            int titleX = config.getCellPadding();
+            int titleY = y + (titleHeight - titleFm.getHeight()) / 2 + titleFm.getAscent();
+            g2d.drawString(title, titleX, titleY);
+            y += titleHeight;
+
+            // 标题下方的分割线
+            g2d.setColor(GRID_COLOR);
+            g2d.drawLine(0, y - 1, totalWidth, y - 1);
+        }
+
+        // 8. 绘制表头
+        g2d.setColor(HEADER_BG);
+        g2d.fillRect(0, y, totalWidth, headerHeight);
+
+        g2d.setColor(HEADER_FG);
+        g2d.setFont(headerFont);
+        FontMetrics headerFm = g2d.getFontMetrics();
+
+        int x = config.getCellPadding();
+        for (int i = 0; i < headers.size(); i++) {
+            String headerText = headers.get(i);
+            int textWidth = headerFm.stringWidth(headerText);
+            int availWidth = colWidths[i] - config.getCellPadding() * 2;
+            // 表头不换行,超长则截断
+            if (textWidth > availWidth) {
+                headerText = truncateText(headerText, availWidth, headerFm);
+            }
+            int textY = y + (headerHeight - headerFm.getHeight()) / 2 + headerFm.getAscent();
+            g2d.drawString(headerText, x + config.getCellPadding(), textY);
+            x += colWidths[i];
+        }
+        y += headerHeight;
+
+        // 9. 绘制数据行
+        g2d.setFont(cellFont);
+        FontMetrics cellFm = g2d.getFontMetrics();
+
+        for (int rowIdx = 0; rowIdx < stringRows.size(); rowIdx++) {
+            List<String> rowData = stringRows.get(rowIdx);
+            int rowHeight = rowHeights[rowIdx];
+
+            // 隔行变色
+            Color rowBg = (rowIdx % 2 == 0) ? ROW_BG_EVEN : ROW_BG_ODD;
+            g2d.setColor(rowBg);
+            g2d.fillRect(0, y, totalWidth, rowHeight);
+
+            // 绘制每个单元格
+            int cellX = config.getCellPadding();
+            for (int colIdx = 0; colIdx < rowData.size(); colIdx++) {
+                String cellText = rowData.get(colIdx);
+                int availWidth = colWidths[colIdx] - config.getCellPadding() * 2;
+
+                // 文本换行处理
+                List<String> lines = wrapText(cellText, availWidth, cellFm);
+
+                g2d.setColor(TEXT_COLOR);
+                int lineY = y + config.getCellPadding() + cellFm.getAscent();
+                int maxLines = Math.max(1, (rowHeight - config.getCellPadding() * 2) / cellFm.getHeight());
+                for (int lineIdx = 0; lineIdx < Math.min(lines.size(), maxLines); lineIdx++) {
+                    String line = lines.get(lineIdx);
+                    // 最后一行如果截断了,加省略号
+                    if (lineIdx == maxLines - 1 && lines.size() > maxLines && !line.endsWith("...")) {
+                        line = truncateText(line, availWidth, cellFm);
+                    }
+                    g2d.drawString(line, cellX + config.getCellPadding(), lineY);
+                    lineY += cellFm.getHeight();
+                }
+
+                cellX += colWidths[colIdx];
+            }
+
+            y += rowHeight;
+        }
+
+        // 10. 绘制网格线
+        drawGridLines(g2d, totalWidth, titleHeight, headerHeight, rowHeights, colWidths);
+
+        g2d.dispose();
+        return image;
+    }
+
+    // ===== 私有方法 =====
+
+    /**
+     * 计算各列宽度(基于表头和内容,限制在 min ~ max 范围内)
+     */
+    private int[] calculateColumnWidths(List<String> headers, List<List<String>> rows) {
+        int[] widths = new int[headers.size()];
+        // 使用临时 BufferedImage 获取 FontMetrics
+        BufferedImage temp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
+        Graphics2D g = temp.createGraphics();
+        g.setFont(cellFont);
+        FontMetrics cellFm = g.getFontMetrics();
+        g.setFont(headerFont);
+        FontMetrics headerFm = g.getFontMetrics();
+
+        int padding = config.getCellPadding() * 2;
+
+        for (int i = 0; i < headers.size(); i++) {
+            int maxContent = headerFm.stringWidth(headers.get(i)) + padding;
+            for (List<String> row : rows) {
+                if (i < row.size()) {
+                    String cellText = row.get(i);
+                    // 对于多行文本,取第一行宽度作为估算
+                    int textWidth = cellFm.stringWidth(cellText);
+                    // 如果超长,按最大列宽估算
+                    if (textWidth > config.getMaxColWidth() - padding) {
+                        textWidth = config.getMaxColWidth() - padding;
+                    }
+                    int cellWidth = textWidth + padding;
+                    if (cellWidth > maxContent) {
+                        maxContent = cellWidth;
+                    }
+                }
+            }
+            widths[i] = Math.max(config.getMinColWidth(), Math.min(maxContent, config.getMaxColWidth()));
+        }
+
+        g.dispose();
+        return widths;
+    }
+
+    /**
+     * 计算各行高度(考虑文本换行)
+     */
+    private int[] calculateRowHeights(List<String> headers, List<List<String>> rows, int[] colWidths) {
+        BufferedImage temp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
+        Graphics2D g = temp.createGraphics();
+        g.setFont(cellFont);
+        FontMetrics cellFm = g.getFontMetrics();
+
+        int[] heights = new int[rows.size()];
+        int defaultHeight = config.getRowHeight();
+        int padding = config.getCellPadding() * 2;
+        int lineHeight = cellFm.getHeight();
+
+        for (int rowIdx = 0; rowIdx < rows.size(); rowIdx++) {
+            List<String> row = rows.get(rowIdx);
+            int maxLines = 1;
+            for (int colIdx = 0; colIdx < row.size() && colIdx < colWidths.length; colIdx++) {
+                String cellText = row.get(colIdx);
+                int availWidth = colWidths[colIdx] - padding;
+                List<String> lines = wrapText(cellText, availWidth, cellFm);
+                if (lines.size() > maxLines) {
+                    maxLines = lines.size();
+                }
+            }
+            int calculatedHeight = padding + maxLines * lineHeight;
+            heights[rowIdx] = Math.max(defaultHeight, calculatedHeight);
+        }
+
+        g.dispose();
+        return heights;
+    }
+
+    /**
+     * 文本换行:将文本按可用宽度拆分为多行
+     */
+    private List<String> wrapText(String text, int availWidth, FontMetrics fm) {
+        List<String> lines = new ArrayList<>();
+        if (text == null || text.isEmpty()) {
+            lines.add("");
+            return lines;
+        }
+        if (availWidth <= 0) {
+            lines.add(text);
+            return lines;
+        }
+
+        // 按已有换行符分割
+        String[] paragraphs = text.split("\n");
+        for (String para : paragraphs) {
+            if (para.isEmpty()) {
+                lines.add("");
+                continue;
+            }
+            if (fm.stringWidth(para) <= availWidth) {
+                lines.add(para);
+                continue;
+            }
+            // 逐字符拆分(兼容中英文混合)
+            StringBuilder current = new StringBuilder();
+            for (int i = 0; i < para.length(); i++) {
+                char c = para.charAt(i);
+                String candidate = current.toString() + c;
+                if (fm.stringWidth(candidate) > availWidth) {
+                    if (current.length() > 0) {
+                        lines.add(current.toString());
+                        current = new StringBuilder();
+                    }
+                    // 即使单个字符也超宽,也强制加入
+                    current.append(c);
+                } else {
+                    current.append(c);
+                }
+            }
+            if (current.length() > 0) {
+                lines.add(current.toString());
+            }
+        }
+        return lines;
+    }
+
+    /**
+     * 截断文本并添加省略号
+     */
+    private String truncateText(String text, int availWidth, FontMetrics fm) {
+        if (text == null || text.isEmpty()) {
+            return "";
+        }
+        String ellipsis = "...";
+        int ellipsisWidth = fm.stringWidth(ellipsis);
+        if (availWidth <= ellipsisWidth) {
+            return ellipsis;
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < text.length(); i++) {
+            String candidate = sb.toString() + text.charAt(i);
+            if (fm.stringWidth(candidate) + ellipsisWidth > availWidth) {
+                break;
+            }
+            sb.append(text.charAt(i));
+        }
+        return sb.toString() + ellipsis;
+    }
+
+    /**
+     * 绘制网格线
+     */
+    private void drawGridLines(Graphics2D g2d, int totalWidth, int titleHeight,
+                               int headerHeight, int[] rowHeights, int[] colWidths) {
+        g2d.setColor(GRID_COLOR);
+        g2d.setStroke(new BasicStroke(1f));
+
+        int startY = titleHeight;
+
+        // 水平线:表头底部 + 每行底部
+        int y = startY + headerHeight;
+        g2d.drawLine(0, y, totalWidth, y);
+        for (int h : rowHeights) {
+            y += h;
+            g2d.drawLine(0, y, totalWidth, y);
+        }
+
+        // 表头与数据之间的分隔线(稍粗)
+        g2d.setColor(new Color(200, 203, 210));
+        g2d.setStroke(new BasicStroke(1.5f));
+        g2d.drawLine(0, startY + headerHeight, totalWidth, startY + headerHeight);
+        g2d.setStroke(new BasicStroke(1f));
+        g2d.setColor(GRID_COLOR);
+
+        // 垂直线
+        int x = config.getCellPadding();
+        for (int i = 0; i < colWidths.length; i++) {
+            x += colWidths[i];
+            if (i < colWidths.length - 1) {
+                g2d.drawLine(x, startY, x, y);
+            }
+        }
+
+        // 外边框
+        g2d.drawRect(1, startY - 1, totalWidth - 3, y - startY);
+    }
+
+    /**
+     * 格式化单元格值
+     * <p>根据字段类型将原始值转换为显示文本</p>
+     */
+    @SuppressWarnings("unchecked")
+    private String formatCellValue(Object value, FieldDefinitionDTO fieldDef) {
+        if (value == null) {
+            return "";
+        }
+
+        String type = fieldDef.getType();
+        if (type == null) {
+            type = "";
+        }
+
+        try {
+            switch (type) {
+                case "NumberField":
+                case "MoneyField":
+                    return formatNumber(value, type);
+
+                case "DateField":
+                case "DateTimeField":
+                case "TimeField":
+                    return formatDate(value);
+
+                case "EmployeeField":
+                    return formatEmployee(value);
+
+                case "DepartmentField":
+                    return formatDepartment(value);
+
+                case "AttachmentField":
+                case "ImageUploadField":
+                    return formatAttachment(value);
+
+                case "CheckboxField":
+                case "MultiSelectField":
+                    return formatMultiValue(value);
+
+                case "AddressField":
+                    return formatAddress(value);
+
+                default:
+                    // 默认:如果是字符串直接返回,否则尝试 JSON 解析取 name/title
+                    if (value instanceof String) {
+                        return (String) value;
+                    }
+                    if (value instanceof Number) {
+                        return String.valueOf(value);
+                    }
+                    if (value instanceof Boolean) {
+                        return (Boolean) value ? "是" : "否";
+                    }
+                    // 尝试解析为 JSON 对象,提取常见的显示字段
+                    return extractDisplayName(value);
+            }
+        } catch (Exception e) {
+            // 格式化失败,返回原始 toString
+            return String.valueOf(value);
+        }
+    }
+
+    private String formatNumber(Object value, String type) {
+        if (value instanceof Number) {
+            double d = ((Number) value).doubleValue();
+            if (d == Math.floor(d)) {
+                return String.valueOf((long) d);
+            }
+            if ("MoneyField".equals(type)) {
+                return String.format("%.2f", d);
+            }
+            return String.valueOf(d);
+        }
+        return String.valueOf(value);
+    }
+
+    private String formatDate(Object value) {
+        if (value instanceof Number) {
+            long ts = ((Number) value).longValue();
+            // 宜搭日期通常是毫秒时间戳
+            return new SimpleDateFormat("yyyy-MM-dd").format(new Date(ts));
+        }
+        return String.valueOf(value);
+    }
+
+    private String formatEmployee(Object value) {
+        // 宜搭 EmployeeField 返回值可能是 JSON 数组: [{"value":"xxx","label":"张三"}]
+        if (value instanceof String) {
+            String str = (String) value;
+            try {
+                if (str.startsWith("[")) {
+                    JSONArray arr = JSON.parseArray(str);
+                    StringBuilder sb = new StringBuilder();
+                    for (int i = 0; i < arr.size(); i++) {
+                        JSONObject obj = arr.getJSONObject(i);
+                        if (sb.length() > 0) sb.append(", ");
+                        sb.append(obj.getString("label"));
+                    }
+                    return sb.toString();
+                }
+            } catch (Exception ignored) {
+            }
+            return str;
+        }
+        return extractDisplayName(value);
+    }
+
+    private String formatDepartment(Object value) {
+        return formatEmployee(value); // 结构类似
+    }
+
+    private String formatAttachment(Object value) {
+        if (value instanceof String) {
+            String str = (String) value;
+            try {
+                if (str.startsWith("[")) {
+                    JSONArray arr = JSON.parseArray(str);
+                    return arr.size() + " 个文件";
+                }
+            } catch (Exception ignored) {
+            }
+        }
+        return String.valueOf(value);
+    }
+
+    private String formatMultiValue(Object value) {
+        if (value instanceof String) {
+            String str = (String) value;
+            try {
+                if (str.startsWith("[")) {
+                    JSONArray arr = JSON.parseArray(str);
+                    StringBuilder sb = new StringBuilder();
+                    for (int i = 0; i < arr.size(); i++) {
+                        if (sb.length() > 0) sb.append(", ");
+                        Object item = arr.get(i);
+                        if (item instanceof JSONObject) {
+                            sb.append(((JSONObject) item).getString("label"));
+                        } else {
+                            sb.append(String.valueOf(item));
+                        }
+                    }
+                    return sb.toString();
+                }
+            } catch (Exception ignored) {
+            }
+            return str;
+        }
+        if (value instanceof List) {
+            return String.join(", ", (List<String>) value);
+        }
+        return String.valueOf(value);
+    }
+
+    private String formatAddress(Object value) {
+        if (value instanceof String) {
+            String str = (String) value;
+            try {
+                JSONObject obj = JSON.parseObject(str);
+                // 宜搭地址格式: {"province":"xxx","city":"xxx","district":"xxx","address":"xxx"}
+                StringBuilder sb = new StringBuilder();
+                String[] keys = {"province", "city", "district", "address"};
+                for (String key : keys) {
+                    String v = obj.getString(key);
+                    if (v != null && !v.isEmpty()) {
+                        sb.append(v);
+                    }
+                }
+                return sb.toString();
+            } catch (Exception ignored) {
+            }
+            return str;
+        }
+        return String.valueOf(value);
+    }
+
+    /**
+     * 从复杂对象中提取显示名称
+     */
+    private String extractDisplayName(Object value) {
+        if (value instanceof Map) {
+            Map<String, Object> map = (Map<String, Object>) value;
+            // 尝试常见的字段名
+            for (String key : new String[]{"label", "name", "title", "text", "value"}) {
+                Object v = map.get(key);
+                if (v != null) {
+                    return String.valueOf(v);
+                }
+            }
+        }
+        if (value instanceof List) {
+            List<?> list = (List<?>) value;
+            StringBuilder sb = new StringBuilder();
+            for (Object item : list) {
+                if (sb.length() > 0) sb.append(", ");
+                sb.append(extractDisplayName(item));
+            }
+            return sb.toString();
+        }
+        return String.valueOf(value);
+    }
+
+    /**
+     * 解析十六进制颜色字符串
+     */
+    private Color parseColor(String hex, Color defaultColor) {
+        if (hex == null || hex.isEmpty()) {
+            return defaultColor;
+        }
+        try {
+            return Color.decode(hex);
+        } catch (NumberFormatException e) {
+            return defaultColor;
+        }
+    }
+}

+ 11 - 1
mjava-sdzl/src/main/resources/application-prod.yml

@@ -68,4 +68,14 @@ aliwork:
   appType: "APP_YYYEKIZY2HIWCG1NZZDY"
   systemToken: "14G668D1DCV5TU1EG98ZX6XRO47H33RN9DCPMKC1"
 
-
+eqb:
+  #  downloadFilePath: C:\\Users\\EDY\\Desktop\\项目\\华高\\电子签\\files\\
+  downloadFilePath: /home/server/mankalong/file/sign/
+  #  signatoryPsnId: c3fb35cb7e574baf97ed9f8917c6327b #签署人 陈伟东
+  signatoryPsnId: 95945af8f0864d15bf186e7808693176 #签署人 俞静妙
+  apiHost: https://openapi.esign.cn
+  psnName: 方煜浙
+  psnIDCardNum: 332624199910200013
+  psnIDCardType: CRED_PSN_CH_IDCARD
+  appId: 5112105688
+  appKey: d41e3ebbc5ba045927bdd9d10862d392