Kaynağa Gözat

会计、评估OMS开票功能相关代码

huangguoce 1 ay önce
ebeveyn
işleme
f9d45c890c
90 değiştirilmiş dosya ile 12434 ekleme ve 305 silme
  1. 5 0
      jeeplus-api/jeeplus-public-modules-api/src/main/java/com/jeeplus/sys/factory/WorkAttachmentApiFallbackFactory.java
  2. 11 4
      jeeplus-api/jeeplus-public-modules-api/src/main/java/com/jeeplus/sys/feign/IWorkAttachmentApi.java
  3. 15 0
      jeeplus-api/jeeplus-system-api/src/main/java/com/jeeplus/flowable/factory/AssessApiFallbackFactory.java
  4. 22 0
      jeeplus-api/jeeplus-system-api/src/main/java/com/jeeplus/flowable/feign/IAssessApi.java
  5. 1 1
      jeeplus-api/jeeplus-system-api/src/main/java/com/jeeplus/sys/factory/UserApiFallbackFactory.java
  6. 3 3
      jeeplus-api/jeeplus-system-api/src/main/java/com/jeeplus/sys/feign/IUserApi.java
  7. 45 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/config/FinanceOmsConfig.java
  8. 309 9
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/controller/FinanceInvoiceController.java
  9. 41 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/FinanceInvoiceTaxClassificationCode.java
  10. 61 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/Additional.java
  11. 106 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/BuildingInfo.java
  12. 190 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/DifferenceVoucher.java
  13. 95 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/GoodsTransport.java
  14. 44 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/HarvesterInfo.java
  15. 48 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownAdditional.java
  16. 85 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownBuildingInfo.java
  17. 86 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownGoodsTransport.java
  18. 34 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownHarvesterInfo.java
  19. 230 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownItem.java
  20. 68 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownJointBuyer.java
  21. 132 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownRealPropertyRentInfo.java
  22. 157 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownRealPropertySellInfo.java
  23. 132 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownTravellerTransport.java
  24. 51 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownUsedCar.java
  25. 173 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownUsedCarSales.java
  26. 182 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownVehicle.java
  27. 106 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownVehicleVesselInfo.java
  28. 872 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSInvoiceDetailInfo.java
  29. 1467 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceOMSImportInfo.java
  30. 78 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/JointBuyer.java
  31. 131 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/OMSAccessTokenInfo.java
  32. 77 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/OMSInvoiceResultDownloadData.java
  33. 389 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/OrderItem.java
  34. 164 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/RealPropertyRentInfo.java
  35. 177 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/RealPropertySellInfo.java
  36. 148 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/TravellerTransport.java
  37. 56 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/UsedCar.java
  38. 187 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/UsedCarSale.java
  39. 194 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/Vehicle.java
  40. 159 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/VehicleVesselInfo.java
  41. 444 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSAllScenarioRedInvoiceInfo.java
  42. 298 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSApplyItem.java
  43. 131 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSFastRedInvoiceInfo.java
  44. 254 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSRedInvoiceConfirmApplyItem.java
  45. 52 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSRedInvoiceConfirmInfo.java
  46. 86 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSRedInvoiceConfirmQueryRequest.java
  47. 356 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSRedInvoiceConfirmResponse.java
  48. 5 1
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/mapper/FinanceInvoiceDetailMapper.java
  49. 46 4
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/mapper/FinanceInvoiceMapper.java
  50. 6 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/mapper/xml/FinanceInvoiceDetailMapper.xml
  51. 60 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/mapper/xml/FinanceInvoiceMapper.xml
  52. 400 6
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/FinanceInvoiceService.java
  53. 221 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/OMS/InvoiceDownloadService.java
  54. 778 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/OMS/OMSDisposeService.java
  55. 222 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/OMS/RedInvoiceDownloadService.java
  56. 180 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/OMS/RedInvoiceRetryScheduledService.java
  57. 421 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/OMS/RedInvoiceScheduledService.java
  58. 23 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/dto/FinanceInvoiceDTO.java
  59. 50 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/thread/ApprovalThread.java
  60. 59 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/thread/RedApprovalThread.java
  61. 268 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/FileHandlingUtil.java
  62. 63 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/FileUtil.java
  63. 149 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/HttpPostJsonUtil.java
  64. 395 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/OMSNationUtil.java
  65. 86 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/SpringContextHolderAccess.java
  66. 109 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/ZipUtils.java
  67. 3 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/workContract/mapper/WorkContractInfoMapper.java
  68. 16 0
      jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/workContract/mapper/xml/WorkContractInfoMapper.xml
  69. 45 0
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/config/OmsConfig.java
  70. 16 43
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/controller/CwFinanceInvoiceController.java
  71. 8 0
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/mapper/CwFinanceInvoiceMapper.java
  72. 33 3
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/mapper/xml/CwFinanceInvoiceMapper.xml
  73. 114 71
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/CwFinanceInvoiceService.java
  74. 26 9
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/OMS/InvoiceDownloadService.java
  75. 147 26
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/OMS/OMSDisposeService.java
  76. 24 9
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/OMS/RedInvoiceDownloadService.java
  77. 1 3
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/OMS/RedInvoiceRetryScheduledService.java
  78. 63 13
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/OMS/RedInvoiceScheduledService.java
  79. 7 0
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/dto/CwFinanceInvoiceDTO.java
  80. 3 2
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/thread/ApprovalThread.java
  81. 2 2
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/thread/RedApprovalThread.java
  82. 6 13
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/util/OMS/FileHandlingUtil.java
  83. 26 37
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/util/OMS/OMSNationUtil.java
  84. 86 0
      jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/util/OMS/SpringContextHolderFinance.java
  85. 9 0
      jeeplus-modules/jeeplus-public-modules/src/main/java/com/jeeplus/pubmodules/oss/controller/OssFileController.java
  86. 6 0
      jeeplus-modules/jeeplus-public-modules/src/main/java/com/jeeplus/pubmodules/oss/feign/WorkAttachmentApiImpl.java
  87. 63 3
      jeeplus-modules/jeeplus-public-modules/src/main/java/com/jeeplus/pubmodules/oss/service/OSSClientService.java
  88. 2 2
      jeeplus-modules/jeeplus-system/src/main/java/com/jeeplus/sys/feign/UserApiImpl.java
  89. 5 37
      jeeplus-modules/jeeplus-system/src/main/java/com/jeeplus/sys/utils/ALiYunSmsUtil.java
  90. 30 4
      jeeplus-modules/jeeplus-xxl-job-executor-sample/src/main/java/com/xxl/job/executor/service/jobhandler/SampleXxlJob.java

+ 5 - 0
jeeplus-api/jeeplus-public-modules-api/src/main/java/com/jeeplus/sys/factory/WorkAttachmentApiFallbackFactory.java

@@ -82,6 +82,11 @@ public class WorkAttachmentApiFallbackFactory implements FallbackFactory <IWorkA
 
             }
 
+            @Override
+            public void downloadFolderFromOss(String prefix, String localDir) {
+
+            }
+
             public byte[] downBytesByStream(String key, String fileName) {
                 return null;
             }

+ 11 - 4
jeeplus-api/jeeplus-public-modules-api/src/main/java/com/jeeplus/sys/feign/IWorkAttachmentApi.java

@@ -1,12 +1,8 @@
 package com.jeeplus.sys.feign;
 
-import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
 import com.jeeplus.common.constant.AppNameConstants;
 import com.jeeplus.sys.domain.WorkAttachmentInfo;
 import com.jeeplus.sys.factory.WorkAttachmentApiFallbackFactory;
-import com.jeeplus.sys.service.dto.UserDTO;
-import com.jeeplus.sys.service.dto.WorkAttachmentInfoDTO;
-import org.apache.ibatis.annotations.Param;
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.web.bind.annotation.*;
 
@@ -99,6 +95,17 @@ public interface IWorkAttachmentApi {
                                @RequestParam(value = "fileName")String fileName,
                                @RequestParam(value = "downFileStr")String downFileStr);
 
+
+    /**
+     * 根据路径下载所有文件到本地指定文件夹
+     */
+    @RequestMapping(value = "/oss/file/downloadFolderFromOss", method = RequestMethod.POST)
+    void downloadFolderFromOss(@RequestParam(value = "prefix")String prefix,
+                               @RequestParam(value = "localDir")String localDir);
+
+
+
+
     /**
      * 附件转流
      * @param key

+ 15 - 0
jeeplus-api/jeeplus-system-api/src/main/java/com/jeeplus/flowable/factory/AssessApiFallbackFactory.java

@@ -85,6 +85,21 @@ public class AssessApiFallbackFactory implements FallbackFactory<IAssessApi> {
             public String getReimbursementCreateUserIdByProcessInstanceId(String id) {
                 return "";
             }
+
+            @Override
+            public void processInvoiceDownloadTasks() {
+
+            }
+
+            @Override
+            public void processRedInvoiceScheduledTask() {
+
+            }
+
+            @Override
+            public void redInvoiceRetryScheduledTask() {
+
+            }
         };
     }
 }

+ 22 - 0
jeeplus-api/jeeplus-system-api/src/main/java/com/jeeplus/flowable/feign/IAssessApi.java

@@ -92,4 +92,26 @@ public interface IAssessApi {
     @GetMapping(value = "/reimbursement/info/getReimbursementCreateUserIdByProcessInstanceId")
     String getReimbursementCreateUserIdByProcessInstanceId(@RequestParam(value = "id") String id);
 
+    /**
+     * 用于发票开票获取数电票信息处理
+     * 和开票系统相关的定时任务
+     */
+    @GetMapping(value = "/finance/invoice/processInvoiceDownloadTasks")
+    void processInvoiceDownloadTasks();
+
+    /**
+     * 用于发票开票红冲定时任务信息处理
+     * 和开票系统相关的定时任务
+     */
+    @GetMapping(value = "/finance/invoice/processRedInvoiceScheduledTask")
+    void processRedInvoiceScheduledTask();
+
+
+    /**
+     * 用于发票开票时报9998错误时进行重新处理的方法
+     * 和开票系统相关的定时任务
+     */
+    @GetMapping(value = "/finance/invoice/redInvoiceRetryScheduledTask")
+    void redInvoiceRetryScheduledTask();
+
 }

+ 1 - 1
jeeplus-api/jeeplus-system-api/src/main/java/com/jeeplus/sys/factory/UserApiFallbackFactory.java

@@ -59,7 +59,7 @@ public class UserApiFallbackFactory implements FallbackFactory <IUserApi> {
             }
 
             @Override
-            public Map<String, Object> sendEntryRandomCodesByInvoice(String phoneNumbers, String name, String officeName, String tenantId) throws Exception {
+            public Map<String, Object> sendEntryRandomCodesByInvoice(String phoneNumbers, String number,String smsCode) throws Exception {
                 return null;
             }
 

+ 3 - 3
jeeplus-api/jeeplus-system-api/src/main/java/com/jeeplus/sys/feign/IUserApi.java

@@ -73,12 +73,12 @@ public interface IUserApi {
     /**
      * 开票短信通知(正式员工)
      * @param mobile
-     * @param name
-     * @param officeName
+     * @param number 发票申请编号
+     * @param smsCode 短信模板编号
      * @return
      */
     @GetMapping(value = BASE_URL + "/sendEntryRandomCodesByInvoice")
-    Map<String ,Object> sendEntryRandomCodesByInvoice(@RequestParam(value = "mobile")String mobile, @RequestParam(value = "name")String name, @RequestParam(value = "officeName")String officeName,  @RequestParam(value = "tenantId")String tenantId) throws Exception;
+    Map<String ,Object> sendEntryRandomCodesByInvoice(@RequestParam(value = "mobile")String mobile, @RequestParam(value = "number")String number, @RequestParam(value = "smsCode")String smsCode) throws Exception;
 
 
 

+ 45 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/config/FinanceOmsConfig.java

@@ -0,0 +1,45 @@
+package com.jeeplus.assess.invoice.config;
+
+import lombok.Getter;
+import org.springframework.stereotype.Component;
+
+@Getter
+@Component
+public class FinanceOmsConfig {
+
+    /** OMS AppId */
+    public static  final String appId = "hyc1";
+
+    /** OMS AppKey */
+    public static  final String appKey = "hyc1";
+
+    /** 组织编码 */
+    public static  final String deptCode = "500102204228315131";
+
+    /** 销方名称 */
+    public static  final String sellerName = "深圳市松胜电子有限公司";
+
+    /** 销售方纳税人识别号 */
+    public static  final String sellerTaxno = "500102204228315131";
+
+    /** 销售方银行名称 */
+    public static  final String bankName = "中信银行南京龙江支行";
+
+    /** 销售方银行账号 */
+    public static  final String bankAccount = "7329010182600006811";
+
+    /** 访问接口地址前缀 */
+    public static  final String omsUrl = "https://oms-sandbox.einvoice.js.cn:7079";
+    //public static  final String omsUrl = "https://www.oms.ejinshui-cloud.com:8899";
+
+    /** 用于判定是否开启oms开票流程事件 */
+    public static  final boolean omsEnabled = true;
+
+    public static final String directory = "/attachment-file";
+
+    /** 成功短信模板编号 */
+    public static  final String successSmsCode = "SMS_501960223";
+    /** 失败短信模板编号 */
+    public static  final String errorSmsCode = "SMS_501645266";
+
+}

+ 309 - 9
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/controller/FinanceInvoiceController.java

@@ -1,26 +1,36 @@
 package com.jeeplus.assess.invoice.controller;
 
-import cn.hutool.log.Log;
+import cn.hutool.extra.spring.SpringUtil;
 import com.alibaba.fastjson.JSON;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.jeeplus.aop.demo.annotation.DemoMode;
-import com.jeeplus.assess.program.configuration.projectList.domain.ProgramProjectListInfo;
+import com.jeeplus.assess.invoice.service.OMS.*;
+import com.jeeplus.assess.invoice.utils.OMS.FileUtil;
 import com.jeeplus.assess.projectRecords.Utils.EasyPoiUtil;
+import com.jeeplus.common.TokenProvider;
 import com.jeeplus.common.excel.ExcelOptions;
 import com.jeeplus.common.excel.ExportMode;
+import com.jeeplus.common.redis.RedisUtils;
 import com.jeeplus.common.utils.ResponseUtil;
 import com.jeeplus.assess.invoice.service.FinanceInvoiceService;
 import com.jeeplus.assess.invoice.service.dto.FinanceDTO;
 import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
 import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDetailDTO;
 import com.jeeplus.logging.annotation.ApiLog;
+import com.jeeplus.sys.feign.IUserApi;
+import com.jeeplus.sys.feign.IWorkAttachmentApi;
+import com.jeeplus.sys.service.dto.UserDTO;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.log4j.Log4j2;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -28,16 +38,11 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.io.*;
 import java.lang.reflect.Field;
 import java.lang.reflect.Type;
 import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
 import java.util.stream.Collectors;
 
 @Api("财务管理-发票")
@@ -49,6 +54,17 @@ public class FinanceInvoiceController {
     @Resource
     private FinanceInvoiceService financeInvoiceService;
 
+    @Resource
+    private InvoiceDownloadService invoiceDownloadService;
+
+    @Resource
+    private RedInvoiceDownloadService redInvoiceDownloadService;
+
+    @Resource
+    private RedInvoiceScheduledService redInvoiceScheduledService;
+
+    @Resource
+    private RedInvoiceRetryScheduledService redInvoiceRetryScheduledService;
     /**
      * 查询发票列表
      * @param financeInvoiceDTO
@@ -371,4 +387,288 @@ public class FinanceInvoiceController {
         List<FinanceInvoiceDTO> invoiceList = financeInvoiceService.getExceedFinance();
         return JSON.toJSONString(invoiceList);
     }
+
+
+    /**
+     * 给accessToken查询的有效时间设置为1天
+     */
+    @Autowired
+    private OMSDisposeService omsDisposeService;
+    //private static final String appId = Global.getConfig("omsAppId");
+    //private static final String appKey = Global.getConfig("omsAppKey");
+    //private static final String deptCode = Global.getConfig("omsDeptCode");
+
+    /**
+     * 开具蓝票
+     * OMS发票测试 完整最终版【最终最终定稿,完全匹配你的所有要求】
+     * 精准码值规则:
+     * 0000=成功执行下载 | 9998=5次重试/30秒 | 0003=清token从头执行 | 0001/0002/其他码=直接执行兜底方法修改系统信息
+     * @param workInvoiceId   对应的应该是发票管理中的 id  即 work_invoice表中需要开票的id(这个id应该被传到redis中进行记录,方便后期回调的时候进行处理)
+     * @return
+     */
+    @GetMapping(value = "/invoiceOMSView")
+    @Transactional(readOnly = false)
+    public Map<String,Object> invoiceOMSView(@RequestParam("invoiceId") String workInvoiceId){
+        Map<String,Object> map = new HashMap<>();
+        // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
+        omsDisposeService.doInvoiceBusiness(map, workInvoiceId, "21");
+        return map;
+    }
+
+
+    /**
+     * 开具红票(快速红冲)
+     * OMS发票测试 完整最终版【最终最终定稿,完全匹配你的所有要求】
+     * 精准码值规则:
+     * 0000=成功执行下载 | 9998=5次重试/30秒 | 0003=清token从头执行 | 0001/0002/其他码=直接执行兜底方法修改系统信息
+     */
+    @GetMapping(value = "/invoiceFastRedOMSView")
+    @Transactional(readOnly = false)
+    public Map<String,Object> invoiceFastRedOMSView(@Param("allEinvno")String allEinvno, @Param("workInvoiceId")String workInvoiceId) {
+        Map<String,Object> map = new HashMap<>();
+        // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
+        omsDisposeService.doFastRedInvoiceBusiness(map, allEinvno, workInvoiceId,"21");
+        return map;
+    }
+
+
+
+
+    /**
+     * 开具红票(全场景红冲,包含已入账红冲处理。我方发起)
+     * @param workInvoiceId   申请单号
+     * @param redInvoiceRelevancyId 开红票对应蓝票的invoiceid
+     * @param originalInvno   原蓝票发票号码(数电票号码)
+     * @return
+     */
+    @GetMapping(value = "/invoiceAllScenarioRedOMSView")
+    @Transactional(readOnly = false)
+    public Map<String,Object> invoiceAllScenarioRedOMSView(@RequestParam("workInvoiceId")String workInvoiceId,@RequestParam("redInvoiceRelevancyId") String redInvoiceRelevancyId,@RequestParam("originalInvno") String originalInvno) {
+        Map<String,Object> map = new HashMap<>();
+        // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
+        omsDisposeService.doAllScenarioRedInvoiceBusiness(map, workInvoiceId, redInvoiceRelevancyId, originalInvno,"21");
+        return map;
+    }
+
+    /**
+     * 临时访问接口
+     * @param redisKeyPrefix
+     */
+    /**
+     * 临时访问接口:按前缀批量删除Redis key
+     * @param redisKeyPrefix 传入Redis key前缀(如OMS_invoice_download)
+     * @return 包含操作状态和key集合的结果
+     */
+    @GetMapping(value = "/deleteRedisByKey")
+    public Map<String, Object> deleteRedisByKey(String redisKeyPrefix) {
+        Map<String, Object> resultMap = new HashMap<>();
+        // 初始化返回结果:默认操作失败
+        resultMap.put("success", false);
+        resultMap.put("code", 500);
+        resultMap.put("msg", "删除失败,未知异常");
+        RedisUtils jedis = null;
+
+        try {
+            // 1. 获取Redis连接
+            jedis = RedisUtils.getInstance();
+            // 测试用:查询删除前的key集合(硬编码,可保留)
+            Set<String> downloadTaskKeys1 = jedis.keys(redisKeyPrefix + ":*");
+            resultMap.put("downloadTaskKeys1", downloadTaskKeys1);
+
+            // 2. 全量判空:连接非空 + 前缀非空非空串
+            if (jedis == null) {
+                resultMap.put("msg", "获取Redis连接失败");
+                return resultMap;
+            }
+            if (redisKeyPrefix == null || redisKeyPrefix.trim().isEmpty()) {
+                resultMap.put("msg", "Redis key前缀不能为空");
+                return resultMap;
+            }
+
+            // 3. 拼接通配符,匹配前缀下所有key
+            String redisPattern = redisKeyPrefix.trim() + "*";
+            // 4. 查询匹配的key集合
+            Set<String> matchKeys = jedis.keys(redisPattern);
+            if (matchKeys == null || matchKeys.isEmpty()) {
+                // 无匹配key,返回成功提示
+                resultMap.put("success", true);
+                resultMap.put("code", 200);
+                resultMap.put("msg", "未查询到匹配的Redis key,无需删除");
+                // 查询删除后(实际无删除)的key集合
+                Set<String> downloadTaskKeys2 = jedis.keys(redisKeyPrefix + ":*");
+                resultMap.put("downloadTaskKeys2", downloadTaskKeys2);
+                return resultMap;
+            }
+
+            // 5. 批量删除:Set转数组,调用del方法
+            String[] keyArray = matchKeys.toArray(new String[0]);
+            Collection<String> keyCollection = Arrays.asList(keyArray);
+            jedis.delete(keyCollection);
+
+            // 6. 关键:用有效连接查询删除后的真实key集合
+            Set<String> downloadTaskKeys2 = jedis.keys(redisKeyPrefix + ":*");
+            resultMap.put("downloadTaskKeys2", downloadTaskKeys2);
+
+            // 7. 更新返回结果:删除成功
+            resultMap.put("success", true);
+            resultMap.put("code", 200);
+            resultMap.put("msg", "成功删除" + matchKeys.size() + "个Redis key");
+
+        } catch (Exception e) {
+            // 捕获所有Redis操作异常,记录日志(建议添加日志框架,如logback/log4j2)
+            e.printStackTrace(); // 临时用,生产环境替换为日志记录
+            resultMap.put("msg", "Redis操作异常:" + e.getMessage());
+        } finally {
+            // 仅释放资源:关闭Jedis连接,归还到连接池
+            if (jedis != null) {
+                try {
+                    //jedis.close();
+                } catch (Exception e) {
+                    e.printStackTrace(); // 关闭连接异常也记录
+                }
+            }
+        }
+        return resultMap;
+    }
+
+
+    /**
+     * 以下是提供给定时任务调用的
+     */
+    /**
+     * 用于发票开票获取数电票信息处理
+     * 和开票系统相关的定时任务
+     */
+    @GetMapping(value = "/processInvoiceDownloadTasks")
+    @Transactional(readOnly = false)
+    //@Scheduled(cron = "0 */1 * * * ?")
+    public void processInvoiceDownloadTasks() {
+        invoiceDownloadService.processInvoiceDownloadTasks();
+        redInvoiceDownloadService.processRedInvoiceDownloadTasks();
+    }
+
+    /**
+     * 用于发票开票红冲定时任务信息处理
+     * 和开票系统相关的定时任务
+     */
+    @GetMapping(value = "/processRedInvoiceScheduledTask")
+    @Transactional(readOnly = false)
+    //@Scheduled(cron = "0 */1 * * * ?")
+    public void processRedInvoiceScheduledTask() {
+        redInvoiceScheduledService.processAllRedInvoiceTasks();
+    }
+
+    /**
+     * 用于发票开票时报9998错误时进行重新处理的方法
+     * 和开票系统相关的定时任务
+     */
+    @GetMapping(value = "/redInvoiceRetryScheduledTask")
+    @Transactional(readOnly = false)
+    //@Scheduled(cron = "0 */1 * * * ?")
+    public void redInvoiceRetryScheduledTask() {
+        redInvoiceRetryScheduledService.handleInvoice9998RetryTask();
+    }
+
+
+    /**
+     * 下载附件(文件没有时间戳前缀)
+     */
+    @GetMapping("/downLoadOMSInvoiceAttachzip")
+    public void downLoadOMSInvoiceAttachzip(@RequestParam("fileUrl")String fileUrl, HttpServletResponse response) {
+        long timestamp = System.currentTimeMillis();
+        UserDTO userDTO = SpringUtil.getBean(IUserApi.class).getByToken(TokenProvider.getCurrentToken());
+        String filePath = null;
+        if (System.getProperty("os.name").toLowerCase().contains("win")) {
+            filePath = "D:/attachment-file/" + userDTO.getName() + timestamp;
+        } else {
+            filePath = "/attachment-file/" + userDTO.getName() + timestamp;
+        }
+
+        File folder = new File(filePath);
+        if (!folder.exists()) {
+            if (folder.mkdirs()) {
+                System.out.println("文件夹创建成功!");
+            } else {
+                System.out.println("文件夹创建失败!");
+            }
+        } else {
+            System.out.println("文件夹已存在!");
+        }
+        String path = filePath + "/";
+
+        try {
+            //文件下载到指定文件夹
+            String aliyunUrl = "http://cdn.gangwaninfo.com";
+            String file = aliyunUrl + fileUrl;
+            SpringUtil.getBean(IWorkAttachmentApi.class).downloadFolderFromOss(fileUrl, path );
+            //文件打包压缩成zip
+            FileUtil.zipFolder(filePath, filePath + ".zip");
+            String downUrl = filePath + ".zip";
+
+            OutputStream outputStream = null;
+            InputStream inputStream = null;
+            BufferedInputStream bufferedInputStream = null;
+            byte[] bytes = new byte[1024];
+            File downFile = new File(downUrl);
+            String downFileName = downFile.getName();
+            // 获取输出流
+            try {
+                response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(downFileName, "UTF-8"));
+                // 以流的形式返回文件
+                response.setContentType("application/octet-stream;charset=utf-8");
+                inputStream = new FileInputStream(downFile);
+                bufferedInputStream = new BufferedInputStream(inputStream);
+                outputStream = response.getOutputStream();
+                int i = bufferedInputStream.read(bytes);
+                while (i != -1) {
+                    outputStream.write(bytes, 0, i);
+                    i = bufferedInputStream.read(bytes);
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            } finally {
+                try {
+                    if (inputStream != null) {
+                        inputStream.close();
+                    }
+                    if (outputStream != null) {
+                        outputStream.flush();
+                    }
+                    if (bufferedInputStream != null) {
+                        bufferedInputStream.close();
+                    }
+
+                    //根据路径创建文件对象
+                    File downloadFile = new File(filePath);
+                    deleteFile(downloadFile);
+
+                    //根据路径创建文件对象
+                    File downFileZip = new File(downUrl);
+                    //路径是个文件且不为空时删除文件
+                    if (downFileZip.isFile() && downFileZip.exists()) {
+                        downFileZip.delete();
+                    }
+
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static void deleteFile(File folder) {
+        if (folder.isDirectory()) {
+            File[] files = folder.listFiles();
+            if (files != null) { // 如果文件夹为空,files可能为null
+                for (File file : files) {
+                    deleteFile(file); // 递归删除子文件夹和文件
+                }
+            }
+        }
+        folder.delete(); // 删除空文件夹或者文件
+    }
+
 }

+ 41 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/FinanceInvoiceTaxClassificationCode.java

@@ -0,0 +1,41 @@
+package com.jeeplus.assess.invoice.domain;
+
+
+import com.jeeplus.core.domain.BaseEntity;
+import lombok.Data;
+
+
+@Data
+public class FinanceInvoiceTaxClassificationCode extends BaseEntity {
+
+    /**
+     * 项目名称
+     */
+    private String goodName;
+
+    /**
+     * 税率
+     */
+    private Double taxRate;
+
+    /**
+     * 含税标志(0:不含税;1:含税)
+     */
+    private String priceTax;
+
+    /**
+     * 商品税收分类编码
+     */
+    private String goodsTaxno;
+
+    /**
+     * 商品和服务分类简称
+     */
+    private String goodsTypeAbbreviation;
+
+    /**
+     * 关联id
+     */
+    private String billingContentId;
+
+}

+ 61 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/Additional.java

@@ -0,0 +1,61 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+
+
+/**
+ * 附加要素信息(按需传值,如果不为空则在发票备注展示附加要素信息;仅对开票类型为蓝票且发票类型为电子发票(增值税专用发票)、电子发票(普通发票)生效)
+ */
+public class Additional {
+    /**
+     * 附加要素名称
+     * 必填,40个字节(一个汉字占2个字节)
+     */
+    @NotBlank(message = "附加要素名称additionalName不能为空")
+    private String additionalName;
+
+    /**
+     * 附加要素类型
+     * 必填,可选值:string(字符型)、number(数值型)、date(日期型,格式yyyy-mm-dd)
+     */
+    @NotBlank(message = "附加要素类型additionalType不能为空")
+    @Pattern(regexp = "^string|number|date$",
+            message = "附加要素类型additionalType只能传入:string(字符型)、number(数值型)、date(日期型)")
+    private String additionalType;
+
+    /**
+     * 附加要素值
+     * 必填,500个字节
+     * 若类型为date时,格式必须为yyyy-mm-dd
+     */
+    @NotBlank(message = "附加要素值additionalValue不能为空")
+    @Pattern(regexp = "^$|^\\d{4}-\\d{2}-\\d{2}$",
+            message = "当附加要素类型为date时,值的格式必须为yyyy-mm-dd")
+    private String additionalValue;
+
+    public String getAdditionalName() {
+        return additionalName;
+    }
+
+    public void setAdditionalName(String additionalName) {
+        this.additionalName = additionalName;
+    }
+
+    public String getAdditionalType() {
+        return additionalType;
+    }
+
+    public void setAdditionalType(String additionalType) {
+        this.additionalType = additionalType;
+    }
+
+    public String getAdditionalValue() {
+        return additionalValue;
+    }
+
+    public void setAdditionalValue(String additionalValue) {
+        this.additionalValue = additionalValue;
+    }
+}

+ 106 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/BuildingInfo.java

@@ -0,0 +1,106 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+
+/**
+ * buildingInfo:开具建筑服务特定要素的数电发票才需要传(specificFactor 为 03 时);注:数电建筑服务发票 只能有 一条明细 且 规格型号、计量单位、数量、单价都不能有值
+ * (只有在开具建筑服务特定要素发票时需要传 buildingInfo 模块,不开具建筑服务特定要素不传buildingInfo 模块)
+ */
+public class BuildingInfo {
+    /**
+     * 建筑服务发生地
+     * 必填,需与行政区划名称一致,最大100字符
+     */
+    @NotBlank(message = "建筑服务发生地buildingAddress不能为空")
+    @Size(max = 100, message = "建筑服务发生地buildingAddress不能超过100字符")
+    private String buildingAddress;
+
+    /**
+     * 详细地址
+     * 建筑服务发生地+详细地址总长度最大120字符
+     */
+    @Size(max = 120, message = "详细地址detailedAddress不能超过120字符")
+    private String detailedAddress;
+
+    /**
+     * 土地增值税项目编号
+     * 最大16字符
+     */
+    @Size(max = 16, message = "土地增值税项目编号landVatItemNo不能超过16字符")
+    private String landVatItemNo;
+
+    /**
+     * 建筑项目名称
+     * 必填,最大80字符
+     */
+    @NotBlank(message = "建筑项目名称itemName不能为空")
+    @Size(max = 80, message = "建筑项目名称itemName不能超过80字符")
+    private String itemName;
+
+    /**
+     * 跨地(市)标志
+     * 必填,0:否 1:是
+     */
+    @NotBlank(message = "跨地(市)标志crossCityFlag不能为空")
+    @Pattern(regexp = "^0|1$", message = "跨地(市)标志crossCityFlag只能传入:0(否)、1(是)")
+    private String crossCityFlag;
+
+    /**
+     * 跨区域涉税事项报验管理编号
+     * 跨地(市)标志为1时必填,最大50字符
+     */
+    @Size(max = 50, message = "跨区域涉税事项报验管理编号crossCityCode不能超过50字符")
+    private String crossCityCode;
+
+    public String getBuildingAddress() {
+        return buildingAddress;
+    }
+
+    public void setBuildingAddress(String buildingAddress) {
+        this.buildingAddress = buildingAddress;
+    }
+
+    public String getDetailedAddress() {
+        return detailedAddress;
+    }
+
+    public void setDetailedAddress(String detailedAddress) {
+        this.detailedAddress = detailedAddress;
+    }
+
+    public String getLandVatItemNo() {
+        return landVatItemNo;
+    }
+
+    public void setLandVatItemNo(String landVatItemNo) {
+        this.landVatItemNo = landVatItemNo;
+    }
+
+    public String getItemName() {
+        return itemName;
+    }
+
+    public void setItemName(String itemName) {
+        this.itemName = itemName;
+    }
+
+    public String getCrossCityFlag() {
+        return crossCityFlag;
+    }
+
+    public void setCrossCityFlag(String crossCityFlag) {
+        this.crossCityFlag = crossCityFlag;
+    }
+
+    public String getCrossCityCode() {
+        return crossCityCode;
+    }
+
+    public void setCrossCityCode(String crossCityCode) {
+        this.crossCityCode = crossCityCode;
+    }
+}

+ 190 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/DifferenceVoucher.java

@@ -0,0 +1,190 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Digits;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+import java.math.BigDecimal;
+
+/**
+ * 差额凭证信息子实体类
+ * 差额征税、差额开票时必传
+ */
+public class DifferenceVoucher {
+
+    /**
+     * 序号
+     * 必填,从1开始依次增加,3位字符
+     */
+    @NotBlank(message = "序号detailIndex不能为空")
+    @Pattern(regexp = "^[1-9]\\d{0,2}$",
+            message = "序号必须从1开始依次增加,且为1-3位数字")
+    private String detailIndex;
+
+    /**
+     * 凭证类型
+     * 必填,可选值:01(数电发票)、02(增值税专用发票)、03(增值税普通发票)、04(营业税发票)、05(财政票据)、06(法院裁决书)、07(契税完税凭证)、08(其他发票类)、09(其他扣除凭证)
+     */
+    @NotBlank(message = "凭证类型voucherType不能为空")
+    @Pattern(regexp = "^01|02|03|04|05|06|07|08|09$",
+            message = "凭证类型不在合法范围内")
+    private String voucherType;
+
+    /**
+     * 数电票号码
+     * 凭证类型为01时必传,最大20字符
+     */
+    @Size(max = 20, message = "数电票号码长度不能超过20字符")
+    private String allEinVno;
+
+    /**
+     * 发票代码
+     * 凭证类型为02、03、04时必传,最大12字符
+     */
+    @Size(max = 12, message = "发票代码长度不能超过12字符")
+    private String invcode;
+
+    /**
+     * 发票号码
+     * 凭证类型为02、03、04时必传,最大8字符
+     */
+    @Size(max = 8, message = "发票号码长度不能超过8字符")
+    private String invno;
+
+    /**
+     * 凭证号码
+     * 最大20字符
+     */
+    @Size(max = 20, message = "凭证号码长度不能超过20字符")
+    private String voucherNumber;
+
+    /**
+     * 开具日期
+     * 凭证类型为01、02、03、04时必传,格式为yyyy-MM-dd
+     */
+    @Pattern(regexp = "^$|^\\d{4}-\\d{2}-\\d{2}$",
+            message = "开具日期格式错误,必须为yyyy-MM-dd")
+    private String invoiceTime;
+
+    /**
+     * 凭证金额
+     * 必填,最多18位整数,保留2位小数
+     */
+    @NotNull(message = "凭证金额totalTax不能为空")
+    @Digits(integer = 18, fraction = 2, message = "凭证金额格式错误,最多18位整数,保留2位小数")
+    private BigDecimal totalTax;
+
+    /**
+     * 本次扣除金额
+     * 必填,不能大于凭证金额,最多18位整数,保留2位小数
+     */
+    @NotNull(message = "本次扣除金额deduction不能为空")
+    @Digits(integer = 18, fraction = 2, message = "本次扣除金额格式错误,最多18位整数,保留2位小数")
+    private BigDecimal deduction;
+
+    /**
+     * 备注
+     * 凭证类型为08、09时必传,最大100字符
+     */
+    @Size(max = 100, message = "备注长度不能超过100字符")
+    private String remarks;
+
+    /**
+     * 凭证来源
+     * 必填,可选值:1(手工录入)、2(勾选录入)、3(模板录入);同一张发票内保持一致
+     */
+    @NotBlank(message = "凭证来源voucherSource不能为空")
+    @Pattern(regexp = "^1|2|3$",
+            message = "凭证来源只能传入:1(手工录入)、2(勾选录入)、3(模板录入)")
+    private String voucherSource;
+
+    public String getDetailIndex() {
+        return detailIndex;
+    }
+
+    public void setDetailIndex(String detailIndex) {
+        this.detailIndex = detailIndex;
+    }
+
+    public String getVoucherType() {
+        return voucherType;
+    }
+
+    public void setVoucherType(String voucherType) {
+        this.voucherType = voucherType;
+    }
+
+    public String getAllEinVno() {
+        return allEinVno;
+    }
+
+    public void setAllEinVno(String allEinVno) {
+        this.allEinVno = allEinVno;
+    }
+
+    public String getInvcode() {
+        return invcode;
+    }
+
+    public void setInvcode(String invcode) {
+        this.invcode = invcode;
+    }
+
+    public String getInvno() {
+        return invno;
+    }
+
+    public void setInvno(String invno) {
+        this.invno = invno;
+    }
+
+    public String getVoucherNumber() {
+        return voucherNumber;
+    }
+
+    public void setVoucherNumber(String voucherNumber) {
+        this.voucherNumber = voucherNumber;
+    }
+
+    public String getInvoiceTime() {
+        return invoiceTime;
+    }
+
+    public void setInvoiceTime(String invoiceTime) {
+        this.invoiceTime = invoiceTime;
+    }
+
+    public BigDecimal getTotalTax() {
+        return totalTax;
+    }
+
+    public void setTotalTax(BigDecimal totalTax) {
+        this.totalTax = totalTax;
+    }
+
+    public BigDecimal getDeduction() {
+        return deduction;
+    }
+
+    public void setDeduction(BigDecimal deduction) {
+        this.deduction = deduction;
+    }
+
+    public String getRemarks() {
+        return remarks;
+    }
+
+    public void setRemarks(String remarks) {
+        this.remarks = remarks;
+    }
+
+    public String getVoucherSource() {
+        return voucherSource;
+    }
+
+    public void setVoucherSource(String voucherSource) {
+        this.voucherSource = voucherSource;
+    }
+}

+ 95 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/GoodsTransport.java

@@ -0,0 +1,95 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+/**
+ * 货物运输服务特定要素子实体类
+ * 仅在开具货物运输服务特定要素的数电发票(specificFactor为04时)需要传入
+ * 最多2000行,至少1行
+ */
+public class GoodsTransport {
+
+    /**
+     * 运输工具种类
+     * 必填,可选值:1(铁路运输)、2(公路运输)、3(水路运输)、4(航空运输)、5(管道运输)
+     */
+    @NotBlank(message = "运输工具种类transportTool不能为空")
+    @Pattern(regexp = "^1|2|3|4|5$",
+            message = "运输工具种类transportTool只能传入:1-5的数字,对应铁路/公路/水路/航空/管道运输")
+    private String transportTool;
+
+    /**
+     * 运输工具牌号
+     * 必填,最大40字符
+     */
+    @NotBlank(message = "运输工具牌号transportToolNum不能为空")
+    @Size(max = 40, message = "运输工具牌号transportToolNum不能超过40字符")
+    private String transportToolNum;
+
+    /**
+     * 起运地
+     * 必填,最大80字符
+     */
+    @NotBlank(message = "起运地origin不能为空")
+    @Size(max = 80, message = "起运地origin不能超过80字符")
+    private String origin;
+
+    /**
+     * 到达地
+     * 必填,最大80字符
+     */
+    @NotBlank(message = "到达地destination不能为空")
+    @Size(max = 80, message = "到达地destination不能超过80字符")
+    private String destination;
+
+    /**
+     * 货物运输名称
+     * 必填,最大80字符
+     */
+    @NotBlank(message = "货物运输名称goodsName不能为空")
+    @Size(max = 80, message = "货物运输名称goodsName不能超过80字符")
+    private String goodsName;
+
+    public String getTransportTool() {
+        return transportTool;
+    }
+
+    public void setTransportTool(String transportTool) {
+        this.transportTool = transportTool;
+    }
+
+    public String getTransportToolNum() {
+        return transportToolNum;
+    }
+
+    public void setTransportToolNum(String transportToolNum) {
+        this.transportToolNum = transportToolNum;
+    }
+
+    public String getOrigin() {
+        return origin;
+    }
+
+    public void setOrigin(String origin) {
+        this.origin = origin;
+    }
+
+    public String getDestination() {
+        return destination;
+    }
+
+    public void setDestination(String destination) {
+        this.destination = destination;
+    }
+
+    public String getGoodsName() {
+        return goodsName;
+    }
+
+    public void setGoodsName(String goodsName) {
+        this.goodsName = goodsName;
+    }
+}

+ 44 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/HarvesterInfo.java

@@ -0,0 +1,44 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+import javax.validation.constraints.Size;
+
+/**
+ * 拖拉机和联合收割机信息子实体类
+ * 仅在开具拖拉机和联合收割机特定要素的数电发票(specificFactor为13时)需要传入
+ *
+ * 业务规则:
+ * 1. 当发票用于登记时:`engineCode`(发动机号码)和 `chassisCode`(底盘号/机架号)至少填写一个,且最多只能有一组;
+ *    同时发票明细只能有一行(或一对折扣明细行)。
+ * 2. 当发票不用于登记时:此模块必须为空,发票可以有多行明细。
+ */
+public class HarvesterInfo {
+
+    /**
+     * 发动机号码
+     * 选填,最大40字符;用于登记时与底盘号至少填一个
+     */
+    @Size(max = 40, message = "发动机号码长度不能超过40字符")
+    private String engineCode;
+
+    /**
+     * 底盘号/机架号
+     * 选填,最大40字符;用于登记时与发动机号码至少填一个
+     */
+    @Size(max = 40, message = "底盘号长度不能超过40字符")
+    private String chassisCode;
+
+    public String getEngineCode() {
+        return engineCode;
+    }
+
+    public void setEngineCode(String engineCode) {
+        this.engineCode = engineCode;
+    }
+
+    public String getChassisCode() {
+        return chassisCode;
+    }
+
+    public void setChassisCode(String chassisCode) {
+        this.chassisCode = chassisCode;
+    }
+}

+ 48 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownAdditional.java

@@ -0,0 +1,48 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+/**
+ * 附加要素信息列表项
+ * 按需传值,附加要素类型支持数值型、字符串型、日期型
+ */
+public class OMSDownAdditional {
+    /**
+     * 附加要素名称
+     * 非必填,40个字节
+     */
+    private String additionalName;
+
+    /**
+     * 附加要素类型
+     * 非必填,6个字节,可选值:number(数值型)、string(字符型)、date(日期型,格式yyyy-mm-dd)
+     */
+    private String additionalType;
+
+    /**
+     * 附加要素值
+     * 非必填,500个字节,根据附加要素类型传对应格式的值
+     */
+    private String additionalValue;
+
+    public String getAdditionalName() {
+        return additionalName;
+    }
+
+    public void setAdditionalName(String additionalName) {
+        this.additionalName = additionalName;
+    }
+
+    public String getAdditionalType() {
+        return additionalType;
+    }
+
+    public void setAdditionalType(String additionalType) {
+        this.additionalType = additionalType;
+    }
+
+    public String getAdditionalValue() {
+        return additionalValue;
+    }
+
+    public void setAdditionalValue(String additionalValue) {
+        this.additionalValue = additionalValue;
+    }
+}

+ 85 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownBuildingInfo.java

@@ -0,0 +1,85 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+
+/**
+ * 建筑服务信息实体类
+ * 用于发票中建筑服务相关的特定信息
+ */
+public class OMSDownBuildingInfo {
+    /**
+     * 建筑服务发生地
+     * 必填,100个字节
+     */
+    @NotBlank(message = "建筑服务发生地buildingAddress不能为空")
+    private String buildingAddress;
+
+    /**
+     * 详细地址
+     * 非必填,120个字节
+     */
+    private String detailedAddress;
+
+    /**
+     * 土地增值税项目编号
+     * 非必填,16个字节
+     */
+    private String landVatItemNo;
+
+    /**
+     * 建筑项目名称
+     * 必填,80个字节
+     */
+    @NotBlank(message = "建筑项目名称itemName不能为空")
+    private String itemName;
+
+    /**
+     * 跨地(市)标志
+     * 必填,1个字节,可选值:0-否,1-是
+     */
+    @NotBlank(message = "跨地(市)标志crossCityFlag不能为空")
+    @Pattern(regexp = "^[01]$", message = "跨地(市)标志crossCityFlag只能是0或1")
+    private String crossCityFlag;
+
+    public String getBuildingAddress() {
+        return buildingAddress;
+    }
+
+    public void setBuildingAddress(String buildingAddress) {
+        this.buildingAddress = buildingAddress;
+    }
+
+    public String getDetailedAddress() {
+        return detailedAddress;
+    }
+
+    public void setDetailedAddress(String detailedAddress) {
+        this.detailedAddress = detailedAddress;
+    }
+
+    public String getLandVatItemNo() {
+        return landVatItemNo;
+    }
+
+    public void setLandVatItemNo(String landVatItemNo) {
+        this.landVatItemNo = landVatItemNo;
+    }
+
+    public String getItemName() {
+        return itemName;
+    }
+
+    public void setItemName(String itemName) {
+        this.itemName = itemName;
+    }
+
+    public String getCrossCityFlag() {
+        return crossCityFlag;
+    }
+
+    public void setCrossCityFlag(String crossCityFlag) {
+        this.crossCityFlag = crossCityFlag;
+    }
+}

+ 86 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownGoodsTransport.java

@@ -0,0 +1,86 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+/**
+ * 货物运输信息实体类
+ * 用于发票中货物运输相关的特定信息
+ */
+public class OMSDownGoodsTransport {
+    /**
+     * 运输工具种类
+     * 必填,1个字节,可选值:1-铁路运输,2-公路运输,3-水路运输,4-航空运输,5-管道运输
+     */
+    @NotBlank(message = "运输工具种类transportTool不能为空")
+    @Pattern(regexp = "^[1-5]$", message = "运输工具种类transportTool只能是1-5的数字")
+    private String transportTool;
+
+    /**
+     * 运输工具牌号
+     * 必填,40个字节
+     */
+    @NotBlank(message = "运输工具牌号transportToolNum不能为空")
+    private String transportToolNum;
+
+    /**
+     * 起运地
+     * 必填,80个字节
+     */
+    @NotBlank(message = "起运地origin不能为空")
+    private String origin;
+
+    /**
+     * 到达地
+     * 必填,80个字节
+     */
+    @NotBlank(message = "到达地destination不能为空")
+    private String destination;
+
+    /**
+     * 货物运输名称
+     * 必填,80个字节
+     */
+    @NotBlank(message = "货物运输名称goodsName不能为空")
+    private String goodsName;
+
+    public String getTransportTool() {
+        return transportTool;
+    }
+
+    public void setTransportTool(String transportTool) {
+        this.transportTool = transportTool;
+    }
+
+    public String getTransportToolNum() {
+        return transportToolNum;
+    }
+
+    public void setTransportToolNum(String transportToolNum) {
+        this.transportToolNum = transportToolNum;
+    }
+
+    public String getOrigin() {
+        return origin;
+    }
+
+    public void setOrigin(String origin) {
+        this.origin = origin;
+    }
+
+    public String getDestination() {
+        return destination;
+    }
+
+    public void setDestination(String destination) {
+        this.destination = destination;
+    }
+
+    public String getGoodsName() {
+        return goodsName;
+    }
+
+    public void setGoodsName(String goodsName) {
+        this.goodsName = goodsName;
+    }
+}

+ 34 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownHarvesterInfo.java

@@ -0,0 +1,34 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+/**
+ * 拖拉机与联合收割机信息实体类
+ * 用于发票中拖拉机与联合收割机相关的特定信息
+ */
+public class OMSDownHarvesterInfo {
+    /**
+     * 发动机号码
+     * 非必填,40个字节
+     */
+    private String engineCode;
+
+    /**
+     * 底盘号
+     * 非必填,40个字节
+     */
+    private String chassisCode;
+
+    public String getEngineCode() {
+        return engineCode;
+    }
+
+    public void setEngineCode(String engineCode) {
+        this.engineCode = engineCode;
+    }
+
+    public String getChassisCode() {
+        return chassisCode;
+    }
+
+    public void setChassisCode(String chassisCode) {
+        this.chassisCode = chassisCode;
+    }
+}

+ 230 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownItem.java

@@ -0,0 +1,230 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+/**
+ * 发票下行明细信息实体类
+ * 发票核心明细项,与发票主表一一关联
+ */
+public class OMSDownItem {
+    /**
+     * 行号
+     * 必填,8个字节
+     */
+    @NotBlank(message = "行号lineCode不能为空")
+    private String lineCode;
+
+    /**
+     * 发票行性质
+     * 必填,2个字节
+     */
+    @NotBlank(message = "发票行性质lineType不能为空")
+    private String lineType;
+
+    /**
+     * 商品名称
+     * 必填,90个字节(一个汉字占2个字节)
+     */
+    @NotBlank(message = "商品名称goodsName不能为空")
+    private String goodsName;
+
+    /**
+     * 规格型号
+     * 非必填,40个字节
+     */
+    private String model;
+
+    /**
+     * 计量单位
+     * 非必填,20个字节
+     */
+    private String unit;
+
+    /**
+     * 数量
+     * 非必填,高精度数值,保留10位小数
+     */
+    private BigDecimal qty;
+
+    /**
+     * 单价
+     * 非必填,高精度数值,保留10位小数
+     */
+    private BigDecimal price;
+
+    /**
+     * 金额(不含税)
+     * 必填,高精度金额,保留2位小数
+     */
+    @NotNull(message = "金额amount不能为空")
+    private BigDecimal amount;
+
+    /**
+     * 税率
+     * 必填,高精度数值,保留3位小数
+     */
+    @NotNull(message = "税率taxrate不能为空")
+    private BigDecimal taxrate;
+
+    /**
+     * 税额
+     * 必填,高精度金额,保留2位小数
+     */
+    @NotNull(message = "税额tax不能为空")
+    private BigDecimal tax;
+
+    /**
+     * 含税金额
+     * 必填,高精度金额,保留2位小数
+     */
+    @NotNull(message = "含税金额taxamount不能为空")
+    private BigDecimal taxamount;
+
+    /**
+     * 优惠政策标识
+     * 非必填,2个字节
+     */
+    private String taxpre;
+
+    /**
+     * 税收分类编码
+     * 必填,19个字节
+     */
+    @NotBlank(message = "税收分类编码goodstaxno不能为空")
+    private String goodstaxno;
+
+    /**
+     * 扣除额
+     * 非必填,高精度金额,保留2位小数
+     */
+    private BigDecimal deduction;
+
+    /**
+     * 税收分类编码简称
+     * 非必填,100个字节
+     */
+    private String codeAbb;
+
+    public String getLineCode() {
+        return lineCode;
+    }
+
+    public void setLineCode(String lineCode) {
+        this.lineCode = lineCode;
+    }
+
+    public String getLineType() {
+        return lineType;
+    }
+
+    public void setLineType(String lineType) {
+        this.lineType = lineType;
+    }
+
+    public String getGoodsName() {
+        return goodsName;
+    }
+
+    public void setGoodsName(String goodsName) {
+        this.goodsName = goodsName;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public void setUnit(String unit) {
+        this.unit = unit;
+    }
+
+    public BigDecimal getQty() {
+        return qty;
+    }
+
+    public void setQty(BigDecimal qty) {
+        this.qty = qty;
+    }
+
+    public BigDecimal getPrice() {
+        return price;
+    }
+
+    public void setPrice(BigDecimal price) {
+        this.price = price;
+    }
+
+    public BigDecimal getAmount() {
+        return amount;
+    }
+
+    public void setAmount(BigDecimal amount) {
+        this.amount = amount;
+    }
+
+    public BigDecimal getTaxrate() {
+        return taxrate;
+    }
+
+    public void setTaxrate(BigDecimal taxrate) {
+        this.taxrate = taxrate;
+    }
+
+    public BigDecimal getTax() {
+        return tax;
+    }
+
+    public void setTax(BigDecimal tax) {
+        this.tax = tax;
+    }
+
+    public BigDecimal getTaxamount() {
+        return taxamount;
+    }
+
+    public void setTaxamount(BigDecimal taxamount) {
+        this.taxamount = taxamount;
+    }
+
+    public String getTaxpre() {
+        return taxpre;
+    }
+
+    public void setTaxpre(String taxpre) {
+        this.taxpre = taxpre;
+    }
+
+    public String getGoodstaxno() {
+        return goodstaxno;
+    }
+
+    public void setGoodstaxno(String goodstaxno) {
+        this.goodstaxno = goodstaxno;
+    }
+
+    public BigDecimal getDeduction() {
+        return deduction;
+    }
+
+    public void setDeduction(BigDecimal deduction) {
+        this.deduction = deduction;
+    }
+
+    public String getCodeAbb() {
+        return codeAbb;
+    }
+
+    public void setCodeAbb(String codeAbb) {
+        this.codeAbb = codeAbb;
+    }
+}

+ 68 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownJointBuyer.java

@@ -0,0 +1,68 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+import org.hibernate.validator.constraints.NotBlank;
+
+/**
+ * 共同购买方集合实体类
+ * 用于发票中多方共同购买商品/服务的信息
+ */
+public class OMSDownJointBuyer {
+    /**
+     * 行号
+     * 必填,5个字节
+     */
+    @NotBlank(message = "行号lineCode不能为空")
+    private String lineCode;
+
+    /**
+     * 共同购买方名称
+     * 必填,100个字节
+     */
+    @NotBlank(message = "共同购买方名称buyerName不能为空")
+    private String buyerName;
+
+    /**
+     * 共同购买方证件类型
+     * 必填,3个字节,详见附录3证件类型代码表
+     */
+    @NotBlank(message = "共同购买方证件类型idCardType不能为空")
+    private String idCardType;
+
+    /**
+     * 共同购买方证件号码
+     * 必填,29个字节
+     */
+    @NotBlank(message = "共同购买方证件号码idCardNo不能为空")
+    private String idCardNo;
+
+    public String getLineCode() {
+        return lineCode;
+    }
+
+    public void setLineCode(String lineCode) {
+        this.lineCode = lineCode;
+    }
+
+    public String getBuyerName() {
+        return buyerName;
+    }
+
+    public void setBuyerName(String buyerName) {
+        this.buyerName = buyerName;
+    }
+
+    public String getIdCardType() {
+        return idCardType;
+    }
+
+    public void setIdCardType(String idCardType) {
+        this.idCardType = idCardType;
+    }
+
+    public String getIdCardNo() {
+        return idCardNo;
+    }
+
+    public void setIdCardNo(String idCardNo) {
+        this.idCardNo = idCardNo;
+    }
+}

+ 132 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownRealPropertyRentInfo.java

@@ -0,0 +1,132 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+
+/**
+ * 不动产租赁服务信息实体类
+ * 用于发票中不动产租赁相关的特定信息
+ */
+public class OMSDownRealPropertyRentInfo {
+    /**
+     * 不动产地址(省)
+     * 必填
+     */
+    @NotBlank(message = "不动产地址(省)realPropertyAddress不能为空")
+    private String realPropertyAddress;
+
+    /**
+     * 不动产地址(市)
+     * 非必填
+     */
+    private String realPropertyAddressCity;
+
+    /**
+     * 详细地址
+     * 非必填,120个字节
+     */
+    private String detailAddress;
+
+    /**
+     * 租赁开始日期
+     * 必填,10个字节,格式为yyyy-MM-dd或yyyy-MM-dd HH:mm
+     */
+    @NotBlank(message = "租赁开始日期rentStartDate不能为空")
+    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}( \\d{2}:\\d{2})?$", message = "租赁开始日期格式必须为yyyy-MM-dd或yyyy-MM-dd HH:mm")
+    private String rentStartDate;
+
+    /**
+     * 租赁结束日期
+     * 必填,10个字节,格式为yyyy-MM-dd或yyyy-MM-dd HH:mm
+     */
+    @NotBlank(message = "租赁结束日期rentEndDate不能为空")
+    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}( \\d{2}:\\d{2})?$", message = "租赁结束日期格式必须为yyyy-MM-dd或yyyy-MM-dd HH:mm")
+    private String rentEndDate;
+
+    /**
+     * 跨地(市)标志
+     * 必填,1个字节,可选值:0-否,1-是
+     */
+    @NotBlank(message = "跨地(市)标志crossCityFlag不能为空")
+    @Pattern(regexp = "^[01]$", message = "跨地(市)标志crossCityFlag只能是0或1")
+    private String crossCityFlag;
+
+    /**
+     * 产权证书/不动产权证号
+     * 非必填,40个字节
+     */
+    private String realPropertyCertificate;
+
+    /**
+     * 面积单位
+     * 必填,2个字节,可选值:1-平方千米,2-平方米,3-公顷,4-亩,5-hm²,6-km²,7-m²,8-米(铁路线与管道等使用)
+     */
+    @NotBlank(message = "面积单位unit不能为空")
+    @Pattern(regexp = "^[1-8]$", message = "面积单位unit只能是1-8的数字")
+    private String unit;
+
+    public String getRealPropertyAddress() {
+        return realPropertyAddress;
+    }
+
+    public void setRealPropertyAddress(String realPropertyAddress) {
+        this.realPropertyAddress = realPropertyAddress;
+    }
+
+    public String getRealPropertyAddressCity() {
+        return realPropertyAddressCity;
+    }
+
+    public void setRealPropertyAddressCity(String realPropertyAddressCity) {
+        this.realPropertyAddressCity = realPropertyAddressCity;
+    }
+
+    public String getDetailAddress() {
+        return detailAddress;
+    }
+
+    public void setDetailAddress(String detailAddress) {
+        this.detailAddress = detailAddress;
+    }
+
+    public String getRentStartDate() {
+        return rentStartDate;
+    }
+
+    public void setRentStartDate(String rentStartDate) {
+        this.rentStartDate = rentStartDate;
+    }
+
+    public String getRentEndDate() {
+        return rentEndDate;
+    }
+
+    public void setRentEndDate(String rentEndDate) {
+        this.rentEndDate = rentEndDate;
+    }
+
+    public String getCrossCityFlag() {
+        return crossCityFlag;
+    }
+
+    public void setCrossCityFlag(String crossCityFlag) {
+        this.crossCityFlag = crossCityFlag;
+    }
+
+    public String getRealPropertyCertificate() {
+        return realPropertyCertificate;
+    }
+
+    public void setRealPropertyCertificate(String realPropertyCertificate) {
+        this.realPropertyCertificate = realPropertyCertificate;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public void setUnit(String unit) {
+        this.unit = unit;
+    }
+}

+ 157 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownRealPropertySellInfo.java

@@ -0,0 +1,157 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+import java.math.BigDecimal;
+
+/**
+ * 不动产销售信息实体类
+ * 用于发票中不动产销售相关的特定信息
+ */
+public class OMSDownRealPropertySellInfo {
+    /**
+     * 不动产单元代码/网签合同备案编号
+     * 非必填,28个字节
+     */
+    private String realPropertyContractNumber;
+
+    /**
+     * 不动产地址(省)
+     * 必填
+     */
+    @NotBlank(message = "不动产地址(省)realPropertyAddress不能为空")
+    private String realPropertyAddress;
+
+    /**
+     * 不动产地址(市)
+     * 非必填
+     */
+    private String realPropertyAddressCity;
+
+    /**
+     * 详细地址
+     * 非必填,120个字节
+     */
+    private String detailAddress;
+
+    /**
+     * 跨地(市)标志
+     * 必填,1个字节,可选值:0-否,1-是
+     */
+    @NotBlank(message = "跨地(市)标志crossCityFlag不能为空")
+    @Pattern(regexp = "^[01]$", message = "跨地(市)标志crossCityFlag只能是0或1")
+    private String crossCityFlag;
+
+    /**
+     * 土地增值税项目编号
+     * 非必填,18个字节
+     */
+    private String incrementTaxNumber;
+
+    /**
+     * 核定计税价格
+     * 非必填,20个字节
+     */
+    private BigDecimal price;
+
+    /**
+     * 实际成交税金额
+     * 非必填,20个字节
+     */
+    private BigDecimal taxAmount;
+
+    /**
+     * 产权证书/不动产权证号
+     * 非必填,40个字节
+     */
+    private String realPropertyCertificate;
+
+    /**
+     * 面积单位
+     * 必填,1个字节,可选值:1-平方千米,2-平方米,3-公顷,4-亩,5-hm²,6-km²,7-m²,8-米(铁路线与管道等使用)
+     */
+    @NotBlank(message = "面积单位unit不能为空")
+    @Pattern(regexp = "^[1-8]$", message = "面积单位unit只能是1-8的数字")
+    private String unit;
+
+    public String getRealPropertyContractNumber() {
+        return realPropertyContractNumber;
+    }
+
+    public void setRealPropertyContractNumber(String realPropertyContractNumber) {
+        this.realPropertyContractNumber = realPropertyContractNumber;
+    }
+
+    public String getRealPropertyAddress() {
+        return realPropertyAddress;
+    }
+
+    public void setRealPropertyAddress(String realPropertyAddress) {
+        this.realPropertyAddress = realPropertyAddress;
+    }
+
+    public String getRealPropertyAddressCity() {
+        return realPropertyAddressCity;
+    }
+
+    public void setRealPropertyAddressCity(String realPropertyAddressCity) {
+        this.realPropertyAddressCity = realPropertyAddressCity;
+    }
+
+    public String getDetailAddress() {
+        return detailAddress;
+    }
+
+    public void setDetailAddress(String detailAddress) {
+        this.detailAddress = detailAddress;
+    }
+
+    public String getCrossCityFlag() {
+        return crossCityFlag;
+    }
+
+    public void setCrossCityFlag(String crossCityFlag) {
+        this.crossCityFlag = crossCityFlag;
+    }
+
+    public String getIncrementTaxNumber() {
+        return incrementTaxNumber;
+    }
+
+    public void setIncrementTaxNumber(String incrementTaxNumber) {
+        this.incrementTaxNumber = incrementTaxNumber;
+    }
+
+    public BigDecimal getPrice() {
+        return price;
+    }
+
+    public void setPrice(BigDecimal price) {
+        this.price = price;
+    }
+
+    public BigDecimal getTaxAmount() {
+        return taxAmount;
+    }
+
+    public void setTaxAmount(BigDecimal taxAmount) {
+        this.taxAmount = taxAmount;
+    }
+
+    public String getRealPropertyCertificate() {
+        return realPropertyCertificate;
+    }
+
+    public void setRealPropertyCertificate(String realPropertyCertificate) {
+        this.realPropertyCertificate = realPropertyCertificate;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public void setUnit(String unit) {
+        this.unit = unit;
+    }
+}

+ 132 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownTravellerTransport.java

@@ -0,0 +1,132 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+
+/**
+ * 旅客运输信息实体类
+ * 用于发票中旅客运输相关的特定信息
+ */
+public class OMSDownTravellerTransport {
+    /**
+     * 出行人
+     * 必填,20个字节
+     */
+    @NotBlank(message = "出行人traveller不能为空")
+    private String traveller;
+
+    /**
+     * 出行日期
+     * 必填,10个字节,格式为yyyy-MM-dd
+     */
+    @NotBlank(message = "出行日期travelDate不能为空")
+    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "出行日期格式必须为yyyy-MM-dd")
+    private String travelDate;
+
+    /**
+     * 出行人证件类型
+     * 必填,10个字节,可选值包括101-组织机构代码证、201-居民身份证等
+     */
+    @NotBlank(message = "出行人证件类型travellerCardType不能为空")
+    private String travellerCardType;
+
+    /**
+     * 出行人证件号码
+     * 必填,20个字节
+     */
+    @NotBlank(message = "出行人证件号码travellerCardNo不能为空")
+    private String travellerCardNo;
+
+    /**
+     * 出行地
+     * 必填,80个字节
+     */
+    @NotBlank(message = "出行地travelPlace不能为空")
+    private String travelPlace;
+
+    /**
+     * 到达地
+     * 必填,80个字节
+     */
+    @NotBlank(message = "到达地arrivePlace不能为空")
+    private String arrivePlace;
+
+    /**
+     * 交通工具类型
+     * 必填,2个字节,可选值:1-飞机,2-火车,3-长途汽车,4-公共交通,5-出租车,6-汽车,7-船舶,9-其他
+     */
+    @NotBlank(message = "交通工具类型vehicleType不能为空")
+    @Pattern(regexp = "^[1345679]$|^2$", message = "交通工具类型vehicleType只能是指定的数字")
+    private String vehicleType;
+
+    /**
+     * 交通工具等级
+     * 非必填,20个字节
+     */
+    private String vehicleLevel;
+
+    public String getTraveller() {
+        return traveller;
+    }
+
+    public void setTraveller(String traveller) {
+        this.traveller = traveller;
+    }
+
+    public String getTravelDate() {
+        return travelDate;
+    }
+
+    public void setTravelDate(String travelDate) {
+        this.travelDate = travelDate;
+    }
+
+    public String getTravellerCardType() {
+        return travellerCardType;
+    }
+
+    public void setTravellerCardType(String travellerCardType) {
+        this.travellerCardType = travellerCardType;
+    }
+
+    public String getTravellerCardNo() {
+        return travellerCardNo;
+    }
+
+    public void setTravellerCardNo(String travellerCardNo) {
+        this.travellerCardNo = travellerCardNo;
+    }
+
+    public String getTravelPlace() {
+        return travelPlace;
+    }
+
+    public void setTravelPlace(String travelPlace) {
+        this.travelPlace = travelPlace;
+    }
+
+    public String getArrivePlace() {
+        return arrivePlace;
+    }
+
+    public void setArrivePlace(String arrivePlace) {
+        this.arrivePlace = arrivePlace;
+    }
+
+    public String getVehicleType() {
+        return vehicleType;
+    }
+
+    public void setVehicleType(String vehicleType) {
+        this.vehicleType = vehicleType;
+    }
+
+    public String getVehicleLevel() {
+        return vehicleLevel;
+    }
+
+    public void setVehicleLevel(String vehicleLevel) {
+        this.vehicleLevel = vehicleLevel;
+    }
+}

+ 51 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownUsedCar.java

@@ -0,0 +1,51 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+import org.hibernate.validator.constraints.NotBlank;
+
+/**
+ * 二手车发票关联的二手车销售统一发票信息实体类
+ * 用于关联二手车销售发票的数电/纸质发票信息
+ */
+public class OMSDownUsedCar {
+    /**
+     * 关联的二手车销售统一发票数电发票号码
+     * 必填,20个字节
+     */
+    @NotBlank(message = "关联的数电发票号码allEinVno不能为空")
+    private String allEinVno;
+
+    /**
+     * 关联的二手车销售统一发票纸质发票代码
+     * 非必填,12/10个字节
+     */
+    private String invcode;
+
+    /**
+     * 关联的二手车销售统一发票纸质发票号码
+     * 非必填,8个字节
+     */
+    private String invno;
+
+    public String getAllEinVno() {
+        return allEinVno;
+    }
+
+    public void setAllEinVno(String allEinVno) {
+        this.allEinVno = allEinVno;
+    }
+
+    public String getInvcode() {
+        return invcode;
+    }
+
+    public void setInvcode(String invcode) {
+        this.invcode = invcode;
+    }
+
+    public String getInvno() {
+        return invno;
+    }
+
+    public void setInvno(String invno) {
+        this.invno = invno;
+    }
+}

+ 173 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownUsedCarSales.java

@@ -0,0 +1,173 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+import org.hibernate.validator.constraints.NotBlank;
+
+/**
+ * 二手车销售统一发票信息实体类
+ * 用于发票中二手车销售相关的特定信息
+ */
+public class OMSDownUsedCarSales {
+    /**
+     * 车牌照号
+     * 必填,20个字节
+     */
+    @NotBlank(message = "车牌照号carNo不能为空")
+    private String carNo;
+
+    /**
+     * 登记证号
+     * 必填,20个字节
+     */
+    @NotBlank(message = "登记证号registrationNo不能为空")
+    private String registrationNo;
+
+    /**
+     * 车辆识别代号/车架号码
+     * 必填,23个字节
+     */
+    @NotBlank(message = "车辆识别代号/车架号码frameNo不能为空")
+    private String frameNo;
+
+    /**
+     * 厂牌型号
+     * 必填,60个字节
+     */
+    @NotBlank(message = "厂牌型号brandModel不能为空")
+    private String brandModel;
+
+    /**
+     * 转入地车辆管理所名称
+     * 必填,80个字节
+     */
+    @NotBlank(message = "转入地车辆管理所名称transferInVehicleRegistrationOffice不能为空")
+    private String transferInVehicleRegistrationOffice;
+
+    /**
+     * 经营单位名称
+     * 必填,100个字节
+     */
+    @NotBlank(message = "经营单位名称businessEntityName不能为空")
+    private String businessEntityName;
+
+    /**
+     * 经营单位纳税人识别号
+     * 必填,20个字节
+     */
+    @NotBlank(message = "经营单位纳税人识别号businessEntityTaxno不能为空")
+    private String businessEntityTaxno;
+
+    /**
+     * 经营单位地址
+     * 必填,100个字节
+     */
+    @NotBlank(message = "经营单位地址businessEntityAddr不能为空")
+    private String businessEntityAddr;
+
+    /**
+     * 经营单位电话
+     * 必填,11个字节
+     */
+    @NotBlank(message = "经营单位电话businessEntityPhone不能为空")
+    private String businessEntityPhone;
+
+    /**
+     * 经营单位开户银行
+     * 必填,100个字节
+     */
+    @NotBlank(message = "经营单位开户银行businessEntityBank不能为空")
+    private String businessEntityBank;
+
+    /**
+     * 经营单位银行账号
+     * 必填,60个字节
+     */
+    @NotBlank(message = "经营单位银行账号businessEntityBankAccount不能为空")
+    private String businessEntityBankAccount;
+
+    public String getCarNo() {
+        return carNo;
+    }
+
+    public void setCarNo(String carNo) {
+        this.carNo = carNo;
+    }
+
+    public String getRegistrationNo() {
+        return registrationNo;
+    }
+
+    public void setRegistrationNo(String registrationNo) {
+        this.registrationNo = registrationNo;
+    }
+
+    public String getFrameNo() {
+        return frameNo;
+    }
+
+    public void setFrameNo(String frameNo) {
+        this.frameNo = frameNo;
+    }
+
+    public String getBrandModel() {
+        return brandModel;
+    }
+
+    public void setBrandModel(String brandModel) {
+        this.brandModel = brandModel;
+    }
+
+    public String getTransferInVehicleRegistrationOffice() {
+        return transferInVehicleRegistrationOffice;
+    }
+
+    public void setTransferInVehicleRegistrationOffice(String transferInVehicleRegistrationOffice) {
+        this.transferInVehicleRegistrationOffice = transferInVehicleRegistrationOffice;
+    }
+
+    public String getBusinessEntityName() {
+        return businessEntityName;
+    }
+
+    public void setBusinessEntityName(String businessEntityName) {
+        this.businessEntityName = businessEntityName;
+    }
+
+    public String getBusinessEntityTaxno() {
+        return businessEntityTaxno;
+    }
+
+    public void setBusinessEntityTaxno(String businessEntityTaxno) {
+        this.businessEntityTaxno = businessEntityTaxno;
+    }
+
+    public String getBusinessEntityAddr() {
+        return businessEntityAddr;
+    }
+
+    public void setBusinessEntityAddr(String businessEntityAddr) {
+        this.businessEntityAddr = businessEntityAddr;
+    }
+
+    public String getBusinessEntityPhone() {
+        return businessEntityPhone;
+    }
+
+    public void setBusinessEntityPhone(String businessEntityPhone) {
+        this.businessEntityPhone = businessEntityPhone;
+    }
+
+    public String getBusinessEntityBank() {
+        return businessEntityBank;
+    }
+
+    public void setBusinessEntityBank(String businessEntityBank) {
+        this.businessEntityBank = businessEntityBank;
+    }
+
+    public String getBusinessEntityBankAccount() {
+        return businessEntityBankAccount;
+    }
+
+    public void setBusinessEntityBankAccount(String businessEntityBankAccount) {
+        this.businessEntityBankAccount = businessEntityBankAccount;
+    }
+}

+ 182 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownVehicle.java

@@ -0,0 +1,182 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+import java.math.BigDecimal;
+
+/**
+ * 机动车销售统一发票信息实体类
+ * 用于发票中机动车销售相关的特定信息
+ */
+public class OMSDownVehicle {
+    /**
+     * 车架号码
+     * 必填,17个字节
+     */
+    @NotBlank(message = "车架号码frameNo不能为空")
+    private String frameNo;
+
+    /**
+     * 产地
+     * 非必填,10个字节
+     */
+    private String origin;
+
+    /**
+     * 进口证明书号
+     * 非必填,16个字节
+     */
+    private String importCertificateNo;
+
+    /**
+     * 商检单号
+     * 非必填,60个字节
+     */
+    private String inspectionNo;
+
+    /**
+     * 吨位
+     * 非必填,10个字节
+     */
+    private BigDecimal tonnage;
+
+    /**
+     * 限乘人数
+     * 非必填,11个字节
+     */
+    private Integer limitNum;
+
+    /**
+     * 完税凭证号码
+     * 非必填,100个字节
+     */
+    private String taxPaymentReceiptNo;
+
+    /**
+     * 厂牌型号
+     * 非必填,100个字节
+     */
+    private String brandModel;
+
+    /**
+     * 合格证号
+     * 非必填,100个字节
+     */
+    private String passCertificate;
+
+    /**
+     * 发动机号码
+     * 非必填,100个字节
+     */
+    private String engineCode;
+
+    /**
+     * 生产企业名称
+     * 非必填,300个字节
+     */
+    private String productionCompanyName;
+
+    /**
+     * 制造或进口日期
+     * 非必填,10个字节,格式为yyyy-MM-dd
+     */
+    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "制造或进口日期格式必须为yyyy-MM-dd")
+    private String vehicleProdDate;
+
+    public String getFrameNo() {
+        return frameNo;
+    }
+
+    public void setFrameNo(String frameNo) {
+        this.frameNo = frameNo;
+    }
+
+    public String getOrigin() {
+        return origin;
+    }
+
+    public void setOrigin(String origin) {
+        this.origin = origin;
+    }
+
+    public String getImportCertificateNo() {
+        return importCertificateNo;
+    }
+
+    public void setImportCertificateNo(String importCertificateNo) {
+        this.importCertificateNo = importCertificateNo;
+    }
+
+    public String getInspectionNo() {
+        return inspectionNo;
+    }
+
+    public void setInspectionNo(String inspectionNo) {
+        this.inspectionNo = inspectionNo;
+    }
+
+    public BigDecimal getTonnage() {
+        return tonnage;
+    }
+
+    public void setTonnage(BigDecimal tonnage) {
+        this.tonnage = tonnage;
+    }
+
+    public Integer getLimitNum() {
+        return limitNum;
+    }
+
+    public void setLimitNum(Integer limitNum) {
+        this.limitNum = limitNum;
+    }
+
+    public String getTaxPaymentReceiptNo() {
+        return taxPaymentReceiptNo;
+    }
+
+    public void setTaxPaymentReceiptNo(String taxPaymentReceiptNo) {
+        this.taxPaymentReceiptNo = taxPaymentReceiptNo;
+    }
+
+    public String getBrandModel() {
+        return brandModel;
+    }
+
+    public void setBrandModel(String brandModel) {
+        this.brandModel = brandModel;
+    }
+
+    public String getPassCertificate() {
+        return passCertificate;
+    }
+
+    public void setPassCertificate(String passCertificate) {
+        this.passCertificate = passCertificate;
+    }
+
+    public String getEngineCode() {
+        return engineCode;
+    }
+
+    public void setEngineCode(String engineCode) {
+        this.engineCode = engineCode;
+    }
+
+    public String getProductionCompanyName() {
+        return productionCompanyName;
+    }
+
+    public void setProductionCompanyName(String productionCompanyName) {
+        this.productionCompanyName = productionCompanyName;
+    }
+
+    public String getVehicleProdDate() {
+        return vehicleProdDate;
+    }
+
+    public void setVehicleProdDate(String vehicleProdDate) {
+        this.vehicleProdDate = vehicleProdDate;
+    }
+}

+ 106 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSDownVehicleVesselInfo.java

@@ -0,0 +1,106 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+import java.math.BigDecimal;
+
+/**
+ * 代收车船税信息实体类
+ * 用于发票中代收车船税相关的特定信息
+ */
+public class OMSDownVehicleVesselInfo {
+    /**
+     * 保险单号
+     * 非必填,40个字节
+     */
+    private String policyNo;
+
+    /**
+     * 车牌号
+     * 非必填,40个字节
+     */
+    private String carNo;
+
+    /**
+     * 税款所属期
+     * 非必填,15个字节
+     */
+    private String taxPeriod;
+
+    /**
+     * 代收车船税金额
+     * 非必填,保留2位小数
+     */
+    private BigDecimal amount;
+
+    /**
+     * 滞纳金金额
+     * 非必填,保留2位小数
+     */
+    private BigDecimal lateFeeAmount;
+
+    /**
+     * 金额合计
+     * 非必填,保留2位小数
+     */
+    private BigDecimal totalamount;
+
+    /**
+     * 车辆识别代码/车架号码
+     * 非必填,17个字节
+     */
+    private String frameNo;
+
+    public String getPolicyNo() {
+        return policyNo;
+    }
+
+    public void setPolicyNo(String policyNo) {
+        this.policyNo = policyNo;
+    }
+
+    public String getCarNo() {
+        return carNo;
+    }
+
+    public void setCarNo(String carNo) {
+        this.carNo = carNo;
+    }
+
+    public String getTaxPeriod() {
+        return taxPeriod;
+    }
+
+    public void setTaxPeriod(String taxPeriod) {
+        this.taxPeriod = taxPeriod;
+    }
+
+    public BigDecimal getAmount() {
+        return amount;
+    }
+
+    public void setAmount(BigDecimal amount) {
+        this.amount = amount;
+    }
+
+    public BigDecimal getLateFeeAmount() {
+        return lateFeeAmount;
+    }
+
+    public void setLateFeeAmount(BigDecimal lateFeeAmount) {
+        this.lateFeeAmount = lateFeeAmount;
+    }
+
+    public BigDecimal getTotalamount() {
+        return totalamount;
+    }
+
+    public void setTotalamount(BigDecimal totalamount) {
+        this.totalamount = totalamount;
+    }
+
+    public String getFrameNo() {
+        return frameNo;
+    }
+
+    public void setFrameNo(String frameNo) {
+        this.frameNo = frameNo;
+    }
+}

+ 872 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceDown/OMSInvoiceDetailInfo.java

@@ -0,0 +1,872 @@
+package com.jeeplus.assess.invoice.domain.OMS.InvoiceDown;
+
+import com.google.common.collect.Lists;
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * OMS发票详情主信息实体类
+ * 发票主表信息,与OMSDownItem发票明细表为一对多关系
+ */
+public class OMSInvoiceDetailInfo {
+    /**
+     * 发票代码
+     * 非必填,12个字节
+     */
+    private String invcode;
+
+    /**
+     * 发票号码
+     * 非必填,8个字节
+     */
+    private String invno;
+
+    /**
+     * 数电发票号码
+     * 必填,20个字节
+     */
+    @NotBlank(message = "数电发票号码allEinVno不能为空")
+    private String allEinVno;
+
+    /**
+     * 订单号
+     * 必填,50个字节
+     */
+    @NotBlank(message = "订单号orderno不能为空")
+    private String orderno;
+
+    /**
+     * 购买方名称
+     * 必填,100个字节
+     */
+    @NotBlank(message = "购买方名称buyerName不能为空")
+    private String buyerName;
+
+    /**
+     * 购买方纳税人识别号
+     * 非必填,20个字节
+     */
+    private String buyerTaxno;
+
+    /**
+     * 购买方地址
+     * 非必填,140个字节
+     */
+    private String buyerAddr;
+
+    /**
+     * 购买方电话
+     * 非必填,20个字节
+     */
+    private String buyerPhone;
+
+    /**
+     * 购买方开户行名称
+     * 非必填,50个字节
+     */
+    private String buyerBank;
+
+    /**
+     * 购买方开户行银行账号
+     * 非必填,50个字节
+     */
+    private String buyerBankaccount;
+
+    /**
+     * 购买方经办人
+     * 非必填,16个字节
+     */
+    private String buyerHandler;
+
+    /**
+     * 经办人身份证件号码
+     * 非必填,20个字节
+     */
+    private String buyerHandlerIdcardno;
+
+    /**
+     * 经办人联系电话
+     * 非必填,20个字节
+     */
+    private String buyerHandlerPhone;
+
+    /**
+     * 销售方名称
+     * 必填,100个字节
+     */
+    @NotBlank(message = "销售方名称sellerName不能为空")
+    private String sellerName;
+
+    /**
+     * 销售方纳税人识别号
+     * 非必填,20个字节
+     */
+    private String sellerTaxno;
+
+    /**
+     * 销售方地址
+     * 非必填,140个字节
+     */
+    private String sellerAddr;
+
+    /**
+     * 销售方电话
+     * 非必填,20个字节
+     */
+    private String sellerPhone;
+
+    /**
+     * 销售方开户行名称
+     * 非必填,50个字节
+     */
+    private String sellerBank;
+
+    /**
+     * 销售方开户行银行账号
+     * 非必填,50个字节
+     */
+    private String sellerBankaccount;
+
+    /**
+     * 订单来源
+     * 非必填,2个字节
+     */
+    private String orderSource;
+
+    /**
+     * 开票类型
+     * 必填,2个字节
+     */
+    @NotBlank(message = "开票类型invKind不能为空")
+    private String invKind;
+
+    /**
+     * 发票类型
+     * 必填,2个字节
+     */
+    @NotBlank(message = "发票类型invType不能为空")
+    private String invType;
+
+    /**
+     * 开票日期
+     * 必填
+     */
+    @NotNull(message = "开票日期invDate不能为空")
+    private Date invDate;
+
+    /**
+     * 原蓝票发票代码
+     * 非必填,12个字节
+     */
+    private String originalInvcode;
+
+    /**
+     * 原蓝票发票号码
+     * 非必填,20个字节
+     */
+    private String originalInvno;
+
+    /**
+     * 红字确认信息单编号
+     * 非必填,32个字节
+     */
+    private String redNo;
+
+    /**
+     * 红字发票标签
+     * 非必填,2个字节
+     */
+    private String redLabel;
+
+    /**
+     * 备注
+     * 非必填,200个字节
+     */
+    private String remarks;
+
+    /**
+     * 合计金额(不含税)
+     * 必填,保留2位小数
+     */
+    @NotNull(message = "合计金额(不含税)totalAmount不能为空")
+    private BigDecimal totalAmount;
+
+    /**
+     * 合计税额
+     * 必填,保留2位小数
+     */
+    @NotNull(message = "合计税额totalTax不能为空")
+    private BigDecimal totalTax;
+
+    /**
+     * 价税合计
+     * 必填,保留2位小数
+     */
+    @NotNull(message = "价税合计totalTaxamount不能为空")
+    private BigDecimal totalTaxamount;
+
+    /**
+     * 差额征税标签
+     * 非必填,2个字节
+     */
+    private String diffFlag;
+
+    /**
+     * 开票人
+     * 必填,8个字节
+     */
+    @NotBlank(message = "开票人drawer不能为空")
+    private String drawer;
+
+    /**
+     * 收款人
+     * 非必填,20个字节
+     */
+    private String payee;
+
+    /**
+     * 复核人
+     * 非必填,20个字节
+     */
+    private String checker;
+
+    /**
+     * 开票人证件号码
+     * 非必填,30个字节
+     */
+    private String drawerIdcardno;
+
+    /**
+     * 开票人证件类型
+     * 非必填,40个字节
+     */
+    private String drawerIdcardtype;
+
+    /**
+     * 收款银行名称
+     * 非必填,40个字节
+     */
+    private String payeeBank;
+
+    /**
+     * 收款银行账号
+     * 非必填,40个字节
+     */
+    private String payeeBankaccount;
+
+    /**
+     * 收购标识
+     * 非必填,2个字节
+     */
+    private String purchaseLabel;
+
+    /**
+     * 结算方式
+     * 非必填,2个字节
+     */
+    private String settlementType;
+
+    /**
+     * 特定要素
+     * 非必填,2个字节
+     */
+    private String specificFactor;
+
+    /**
+     * 发票状态
+     * 必填,1个字节
+     */
+    @NotBlank(message = "发票状态invStatus不能为空")
+    private String invStatus;
+
+    /**
+     * 发票pdf文件地址
+     * 非必填
+     */
+    private String pdfUrl;
+
+    /**
+     * 发票ofd文件地址
+     * 非必填
+     */
+    private String ofdUrl;
+
+    /**
+     * 发票xml文件地址
+     * 非必填
+     */
+    private String xmlUrl;
+
+    /**
+     * 失败原因
+     * 非必填
+     */
+    private String failCause;
+
+
+    // ======================== 以下所有关联实体 全部改为 List集合类型 ========================
+
+
+    /**
+     * 发票明细行信息集合
+     * 非必填 发票核心明细项 一对多关联
+     */
+    private List<OMSDownItem> items =  Lists.newArrayList();
+
+    /**
+     * 附加要素信息列表
+     * 非必填 发票备注展示附加要素信息
+     */
+    private List<OMSDownAdditional> additionalList =  Lists.newArrayList();
+
+    /**
+     * 建筑服务信息集合
+     * 非必填 建筑服务类发票专用
+     */
+    private List<OMSDownBuildingInfo> buildingInfo =  Lists.newArrayList();
+
+    /**
+     * 货物运输信息集合
+     * 非必填 货物运输类发票专用
+     */
+    private List<OMSDownGoodsTransport> goodsTransports =  Lists.newArrayList();
+
+    /**
+     * 旅客运输信息集合
+     * 非必填 旅客运输类发票专用
+     */
+    private List<OMSDownTravellerTransport> travellerTransports =  Lists.newArrayList();
+
+    /**
+     * 不动产租赁服务信息集合
+     * 非必填 不动产租赁类发票专用
+     */
+    private List<OMSDownRealPropertyRentInfo> realPropertyRentInfo =  Lists.newArrayList();
+
+    /**
+     * 不动产销售信息集合
+     * 非必填 不动产销售类发票专用
+     */
+    private List<OMSDownRealPropertySellInfo> realPropertySellInfo =  Lists.newArrayList();
+
+    /**
+     * 代收车船税信息集合
+     * 非必填 车船税代收类发票专用
+     */
+    private List<OMSDownVehicleVesselInfo> vehicleVesselInfo =  Lists.newArrayList();
+
+    /**
+     * 拖拉机与联合收割机信息集合
+     * 非必填 农机类发票专用
+     */
+    private List<OMSDownHarvesterInfo> harvesterInfo =  Lists.newArrayList();
+
+    /**
+     * 机动车销售统一发票信息集合
+     * 非必填 机动车销售类发票专用
+     */
+    private List<OMSDownVehicle> vehicle =  Lists.newArrayList();
+
+    /**
+     * 二手车销售统一发票信息集合
+     * 非必填 二手车销售类发票专用
+     */
+    private List<OMSDownUsedCarSales> usedCarSales =  Lists.newArrayList();
+
+    /**
+     * 二手车发票关联信息集合
+     * 非必填 二手车发票关联专用
+     */
+    private List<OMSDownUsedCar> usedCar =  Lists.newArrayList();
+
+    /**
+     * 共同购买方信息集合
+     * 非必填 多方共同购买时传入
+     */
+    private List<OMSDownJointBuyer> jointBuyer =  Lists.newArrayList();
+
+
+    public String getInvcode() {
+        return invcode;
+    }
+
+    public void setInvcode(String invcode) {
+        this.invcode = invcode;
+    }
+
+    public String getInvno() {
+        return invno;
+    }
+
+    public void setInvno(String invno) {
+        this.invno = invno;
+    }
+
+    public String getAllEinVno() {
+        return allEinVno;
+    }
+
+    public void setAllEinVno(String allEinVno) {
+        this.allEinVno = allEinVno;
+    }
+
+    public String getOrderno() {
+        return orderno;
+    }
+
+    public void setOrderno(String orderno) {
+        this.orderno = orderno;
+    }
+
+    public String getBuyerName() {
+        return buyerName;
+    }
+
+    public void setBuyerName(String buyerName) {
+        this.buyerName = buyerName;
+    }
+
+    public String getBuyerTaxno() {
+        return buyerTaxno;
+    }
+
+    public void setBuyerTaxno(String buyerTaxno) {
+        this.buyerTaxno = buyerTaxno;
+    }
+
+    public String getBuyerAddr() {
+        return buyerAddr;
+    }
+
+    public void setBuyerAddr(String buyerAddr) {
+        this.buyerAddr = buyerAddr;
+    }
+
+    public String getBuyerPhone() {
+        return buyerPhone;
+    }
+
+    public void setBuyerPhone(String buyerPhone) {
+        this.buyerPhone = buyerPhone;
+    }
+
+    public String getBuyerBank() {
+        return buyerBank;
+    }
+
+    public void setBuyerBank(String buyerBank) {
+        this.buyerBank = buyerBank;
+    }
+
+    public String getBuyerBankaccount() {
+        return buyerBankaccount;
+    }
+
+    public void setBuyerBankaccount(String buyerBankaccount) {
+        this.buyerBankaccount = buyerBankaccount;
+    }
+
+    public String getBuyerHandler() {
+        return buyerHandler;
+    }
+
+    public void setBuyerHandler(String buyerHandler) {
+        this.buyerHandler = buyerHandler;
+    }
+
+    public String getBuyerHandlerIdcardno() {
+        return buyerHandlerIdcardno;
+    }
+
+    public void setBuyerHandlerIdcardno(String buyerHandlerIdcardno) {
+        this.buyerHandlerIdcardno = buyerHandlerIdcardno;
+    }
+
+    public String getBuyerHandlerPhone() {
+        return buyerHandlerPhone;
+    }
+
+    public void setBuyerHandlerPhone(String buyerHandlerPhone) {
+        this.buyerHandlerPhone = buyerHandlerPhone;
+    }
+
+    public String getSellerName() {
+        return sellerName;
+    }
+
+    public void setSellerName(String sellerName) {
+        this.sellerName = sellerName;
+    }
+
+    public String getSellerTaxno() {
+        return sellerTaxno;
+    }
+
+    public void setSellerTaxno(String sellerTaxno) {
+        this.sellerTaxno = sellerTaxno;
+    }
+
+    public String getSellerAddr() {
+        return sellerAddr;
+    }
+
+    public void setSellerAddr(String sellerAddr) {
+        this.sellerAddr = sellerAddr;
+    }
+
+    public String getSellerPhone() {
+        return sellerPhone;
+    }
+
+    public void setSellerPhone(String sellerPhone) {
+        this.sellerPhone = sellerPhone;
+    }
+
+    public String getSellerBank() {
+        return sellerBank;
+    }
+
+    public void setSellerBank(String sellerBank) {
+        this.sellerBank = sellerBank;
+    }
+
+    public String getSellerBankaccount() {
+        return sellerBankaccount;
+    }
+
+    public void setSellerBankaccount(String sellerBankaccount) {
+        this.sellerBankaccount = sellerBankaccount;
+    }
+
+    public String getOrderSource() {
+        return orderSource;
+    }
+
+    public void setOrderSource(String orderSource) {
+        this.orderSource = orderSource;
+    }
+
+    public String getInvKind() {
+        return invKind;
+    }
+
+    public void setInvKind(String invKind) {
+        this.invKind = invKind;
+    }
+
+    public String getInvType() {
+        return invType;
+    }
+
+    public void setInvType(String invType) {
+        this.invType = invType;
+    }
+
+    public Date getInvDate() {
+        return invDate;
+    }
+
+    public void setInvDate(Date invDate) {
+        this.invDate = invDate;
+    }
+
+    public String getOriginalInvcode() {
+        return originalInvcode;
+    }
+
+    public void setOriginalInvcode(String originalInvcode) {
+        this.originalInvcode = originalInvcode;
+    }
+
+    public String getOriginalInvno() {
+        return originalInvno;
+    }
+
+    public void setOriginalInvno(String originalInvno) {
+        this.originalInvno = originalInvno;
+    }
+
+    public String getRedNo() {
+        return redNo;
+    }
+
+    public void setRedNo(String redNo) {
+        this.redNo = redNo;
+    }
+
+    public String getRedLabel() {
+        return redLabel;
+    }
+
+    public void setRedLabel(String redLabel) {
+        this.redLabel = redLabel;
+    }
+
+    public String getRemarks() {
+        return remarks;
+    }
+
+    public void setRemarks(String remarks) {
+        this.remarks = remarks;
+    }
+
+    public BigDecimal getTotalAmount() {
+        return totalAmount;
+    }
+
+    public void setTotalAmount(BigDecimal totalAmount) {
+        this.totalAmount = totalAmount;
+    }
+
+    public BigDecimal getTotalTax() {
+        return totalTax;
+    }
+
+    public void setTotalTax(BigDecimal totalTax) {
+        this.totalTax = totalTax;
+    }
+
+    public BigDecimal getTotalTaxamount() {
+        return totalTaxamount;
+    }
+
+    public void setTotalTaxamount(BigDecimal totalTaxamount) {
+        this.totalTaxamount = totalTaxamount;
+    }
+
+    public String getDiffFlag() {
+        return diffFlag;
+    }
+
+    public void setDiffFlag(String diffFlag) {
+        this.diffFlag = diffFlag;
+    }
+
+    public String getDrawer() {
+        return drawer;
+    }
+
+    public void setDrawer(String drawer) {
+        this.drawer = drawer;
+    }
+
+    public String getPayee() {
+        return payee;
+    }
+
+    public void setPayee(String payee) {
+        this.payee = payee;
+    }
+
+    public String getChecker() {
+        return checker;
+    }
+
+    public void setChecker(String checker) {
+        this.checker = checker;
+    }
+
+    public String getDrawerIdcardno() {
+        return drawerIdcardno;
+    }
+
+    public void setDrawerIdcardno(String drawerIdcardno) {
+        this.drawerIdcardno = drawerIdcardno;
+    }
+
+    public String getDrawerIdcardtype() {
+        return drawerIdcardtype;
+    }
+
+    public void setDrawerIdcardtype(String drawerIdcardtype) {
+        this.drawerIdcardtype = drawerIdcardtype;
+    }
+
+    public String getPayeeBank() {
+        return payeeBank;
+    }
+
+    public void setPayeeBank(String payeeBank) {
+        this.payeeBank = payeeBank;
+    }
+
+    public String getPayeeBankaccount() {
+        return payeeBankaccount;
+    }
+
+    public void setPayeeBankaccount(String payeeBankaccount) {
+        this.payeeBankaccount = payeeBankaccount;
+    }
+
+    public String getPurchaseLabel() {
+        return purchaseLabel;
+    }
+
+    public void setPurchaseLabel(String purchaseLabel) {
+        this.purchaseLabel = purchaseLabel;
+    }
+
+    public String getSettlementType() {
+        return settlementType;
+    }
+
+    public void setSettlementType(String settlementType) {
+        this.settlementType = settlementType;
+    }
+
+    public String getSpecificFactor() {
+        return specificFactor;
+    }
+
+    public void setSpecificFactor(String specificFactor) {
+        this.specificFactor = specificFactor;
+    }
+
+    public String getInvStatus() {
+        return invStatus;
+    }
+
+    public void setInvStatus(String invStatus) {
+        this.invStatus = invStatus;
+    }
+
+    public String getPdfUrl() {
+        return pdfUrl;
+    }
+
+    public void setPdfUrl(String pdfUrl) {
+        this.pdfUrl = pdfUrl;
+    }
+
+    public String getOfdUrl() {
+        return ofdUrl;
+    }
+
+    public void setOfdUrl(String ofdUrl) {
+        this.ofdUrl = ofdUrl;
+    }
+
+    public String getXmlUrl() {
+        return xmlUrl;
+    }
+
+    public void setXmlUrl(String xmlUrl) {
+        this.xmlUrl = xmlUrl;
+    }
+
+    public String getFailCause() {
+        return failCause;
+    }
+
+    public void setFailCause(String failCause) {
+        this.failCause = failCause;
+    }
+
+    public List<OMSDownItem> getItems() {
+        return items;
+    }
+
+    public void setItems(List<OMSDownItem> items) {
+        this.items = items;
+    }
+
+    public List<OMSDownAdditional> getAdditionalList() {
+        return additionalList;
+    }
+
+    public void setAdditionalList(List<OMSDownAdditional> additionalList) {
+        this.additionalList = additionalList;
+    }
+
+    public List<OMSDownBuildingInfo> getBuildingInfo() {
+        return buildingInfo;
+    }
+
+    public void setBuildingInfo(List<OMSDownBuildingInfo> buildingInfo) {
+        this.buildingInfo = buildingInfo;
+    }
+
+    public List<OMSDownGoodsTransport> getGoodsTransports() {
+        return goodsTransports;
+    }
+
+    public void setGoodsTransports(List<OMSDownGoodsTransport> goodsTransports) {
+        this.goodsTransports = goodsTransports;
+    }
+
+    public List<OMSDownTravellerTransport> getTravellerTransports() {
+        return travellerTransports;
+    }
+
+    public void setTravellerTransports(List<OMSDownTravellerTransport> travellerTransports) {
+        this.travellerTransports = travellerTransports;
+    }
+
+    public List<OMSDownRealPropertyRentInfo> getRealPropertyRentInfo() {
+        return realPropertyRentInfo;
+    }
+
+    public void setRealPropertyRentInfo(List<OMSDownRealPropertyRentInfo> realPropertyRentInfo) {
+        this.realPropertyRentInfo = realPropertyRentInfo;
+    }
+
+    public List<OMSDownRealPropertySellInfo> getRealPropertySellInfo() {
+        return realPropertySellInfo;
+    }
+
+    public void setRealPropertySellInfo(List<OMSDownRealPropertySellInfo> realPropertySellInfo) {
+        this.realPropertySellInfo = realPropertySellInfo;
+    }
+
+    public List<OMSDownVehicleVesselInfo> getVehicleVesselInfo() {
+        return vehicleVesselInfo;
+    }
+
+    public void setVehicleVesselInfo(List<OMSDownVehicleVesselInfo> vehicleVesselInfo) {
+        this.vehicleVesselInfo = vehicleVesselInfo;
+    }
+
+    public List<OMSDownHarvesterInfo> getHarvesterInfo() {
+        return harvesterInfo;
+    }
+
+    public void setHarvesterInfo(List<OMSDownHarvesterInfo> harvesterInfo) {
+        this.harvesterInfo = harvesterInfo;
+    }
+
+    public List<OMSDownVehicle> getVehicle() {
+        return vehicle;
+    }
+
+    public void setVehicle(List<OMSDownVehicle> vehicle) {
+        this.vehicle = vehicle;
+    }
+
+    public List<OMSDownUsedCarSales> getUsedCarSales() {
+        return usedCarSales;
+    }
+
+    public void setUsedCarSales(List<OMSDownUsedCarSales> usedCarSales) {
+        this.usedCarSales = usedCarSales;
+    }
+
+    public List<OMSDownUsedCar> getUsedCar() {
+        return usedCar;
+    }
+
+    public void setUsedCar(List<OMSDownUsedCar> usedCar) {
+        this.usedCar = usedCar;
+    }
+
+    public List<OMSDownJointBuyer> getJointBuyer() {
+        return jointBuyer;
+    }
+
+    public void setJointBuyer(List<OMSDownJointBuyer> jointBuyer) {
+        this.jointBuyer = jointBuyer;
+    }
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 1467 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/InvoiceOMSImportInfo.java


+ 78 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/JointBuyer.java

@@ -0,0 +1,78 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+/**
+ * 共同购买方集合子实体类
+ * 仅在不动产销售特定要素(specificFactor为05)且多方共同购买标签(jointBuyerFlag为Y)时需传值
+ * 不可超过100行,行号从1开始升序
+ */
+public class JointBuyer {
+
+    /**
+     * 行号
+     * 必填,从1开始依次增加,最大5位字符
+     */
+    @NotBlank(message = "共同购买方行号lineCode不能为空")
+    @Pattern(regexp = "^[1-9]\\d{0,4}$",
+            message = "行号必须从1开始依次增加,且为1-5位数字")
+    private String lineCode;
+
+    /**
+     * 共同购买方名称
+     * 必填,最大100字符
+     */
+    @NotBlank(message = "共同购买方名称buyerName不能为空")
+    @Size(max = 100, message = "共同购买方名称长度不能超过100字符")
+    private String buyerName;
+
+    /**
+     * 共同购买方证件类型
+     * 必填,最大3字符,详见附录3身份证件类型代码表
+     */
+    @NotBlank(message = "共同购买方证件类型idCardType不能为空")
+    @Size(max = 3, message = "证件类型长度不能超过3字符")
+    private String idCardType;
+
+    /**
+     * 共同购买方证件号码
+     * 必填,最大20字符
+     */
+    @NotBlank(message = "共同购买方证件号码idCardNo不能为空")
+    @Size(max = 20, message = "证件号码长度不能超过20字符")
+    private String idCardNo;
+
+    public String getLineCode() {
+        return lineCode;
+    }
+
+    public void setLineCode(String lineCode) {
+        this.lineCode = lineCode;
+    }
+
+    public String getBuyerName() {
+        return buyerName;
+    }
+
+    public void setBuyerName(String buyerName) {
+        this.buyerName = buyerName;
+    }
+
+    public String getIdCardType() {
+        return idCardType;
+    }
+
+    public void setIdCardType(String idCardType) {
+        this.idCardType = idCardType;
+    }
+
+    public String getIdCardNo() {
+        return idCardNo;
+    }
+
+    public void setIdCardNo(String idCardNo) {
+        this.idCardNo = idCardNo;
+    }
+}

+ 131 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/OMSAccessTokenInfo.java

@@ -0,0 +1,131 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+import com.alibaba.fastjson.annotation.JSONField;
+
+import java.io.Serializable;
+
+public class OMSAccessTokenInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @JSONField(name = "appId")
+    private String appId;
+
+    @JSONField(name = "appKey")
+    private String appKey;
+
+    @JSONField(name = "exchangeId")
+    private String exchangeId;
+
+    @JSONField(name = "accessToken")
+    private String accessToken;
+
+    @JSONField(name = "result")
+    private Result result;
+
+    @JSONField(name = "data")
+    private Object data;
+
+    // 无参构造方法 (JSON反序列化必须有)
+    public OMSAccessTokenInfo() {
+    }
+
+    // ==================== 所有字段的getter & setter ====================
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getAppKey() {
+        return appKey;
+    }
+
+    public void setAppKey(String appKey) {
+        this.appKey = appKey;
+    }
+
+    public String getExchangeId() {
+        return exchangeId;
+    }
+
+    public void setExchangeId(String exchangeId) {
+        this.exchangeId = exchangeId;
+    }
+
+    public String getAccessToken() {
+        return accessToken;
+    }
+
+    public void setAccessToken(String accessToken) {
+        this.accessToken = accessToken;
+    }
+
+    public Result getResult() {
+        return result;
+    }
+
+    public void setResult(Result result) {
+        this.result = result;
+    }
+
+    public Object getData() {
+        return data;
+    }
+
+    public void setData(Object data) {
+        this.data = data;
+    }
+
+    // toString 方便日志打印和调试
+    @Override
+    public String toString() {
+        return "OMSAccessTokenInfo{" +
+                "appId='" + appId + '\'' +
+                ", appKey='" + appKey + '\'' +
+                ", exchangeId='" + exchangeId + '\'' +
+                ", accessToken='" + accessToken + '\'' +
+                ", result=" + result +
+                ", data=" + data +
+                '}';
+    }
+
+    // ==================== 嵌套的Result子实体类 ====================
+    public static class Result implements Serializable {
+        private static final long serialVersionUID = 1L;
+
+        @JSONField(name = "code")
+        private String code;
+
+        @JSONField(name = "message")
+        private String message;
+
+        public Result() {
+        }
+
+        public String getCode() {
+            return code;
+        }
+
+        public void setCode(String code) {
+            this.code = code;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public void setMessage(String message) {
+            this.message = message;
+        }
+
+        @Override
+        public String toString() {
+            return "Result{" +
+                    "code='" + code + '\'' +
+                    ", message='" + message + '\'' +
+                    '}';
+        }
+    }
+}

+ 77 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/OMSInvoiceResultDownloadData.java

@@ -0,0 +1,77 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+import com.alibaba.fastjson.annotation.JSONField;
+
+import java.io.Serializable;
+
+/**
+ * OMS发票结果下载数据实体类
+ * 字段对应表格定义:组织代码、订单号、是否查询明细
+ */
+public class OMSInvoiceResultDownloadData implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 组织代码(所属组织唯一标识,长度20,必填)
+     */
+    @JSONField(name = "deptCode")
+    private String deptCode;
+
+    /**
+     * 订单号(订单唯一标识,长度50,必填)
+     */
+    @JSONField(name = "orderno")
+    private String orderno;
+
+    /**
+     * 是否查询明细(0:否,1:是,长度1,必填)
+     */
+    @JSONField(name = "isDetail")
+    private String isDetail;
+
+    // 无参构造(JSON反序列化必备)
+    public OMSInvoiceResultDownloadData() {
+    }
+
+    // 全参构造(方便快速创建对象)
+    public OMSInvoiceResultDownloadData(String deptCode, String orderno, String isDetail) {
+        this.deptCode = deptCode;
+        this.orderno = orderno;
+        this.isDetail = isDetail;
+    }
+
+    // ==================== 字段对应的getter & setter ====================
+    public String getDeptCode() {
+        return deptCode;
+    }
+
+    public void setDeptCode(String deptCode) {
+        this.deptCode = deptCode;
+    }
+
+    public String getOrderno() {
+        return orderno;
+    }
+
+    public void setOrderno(String orderno) {
+        this.orderno = orderno;
+    }
+
+    public String getIsDetail() {
+        return isDetail;
+    }
+
+    public void setIsDetail(String isDetail) {
+        this.isDetail = isDetail;
+    }
+
+    // toString方法(方便日志打印/调试)
+    @Override
+    public String toString() {
+        return "OMSInvoiceResultDownloadData{" +
+                "deptCode='" + deptCode + '\'' +
+                ", orderno='" + orderno + '\'' +
+                ", isDetail='" + isDetail + '\'' +
+                '}';
+    }
+}

+ 389 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/OrderItem.java

@@ -0,0 +1,389 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Digits;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.math.BigDecimal;
+
+/**
+ * 订单明细表信息
+ */
+public class OrderItem {
+
+    /**
+     * 行号
+     * 明细行行号;仅作为唯一标识使用;最终发票中的明细顺序以请求报文中的明细顺序为准
+     * 非空必填,字符串类型
+     */
+    @NotBlank(message = "明细行号lineCode不能为空")
+    private String lineCode;
+
+    /**
+     * 对应蓝字发票明细行号
+     * 红票必传 蓝字票非必填
+     */
+    private String oriLineCode;
+
+    /**
+     * 发票行性质
+     * 00:正常行 01:折扣行 02:被折扣行
+     * 必填 + 只能传指定的3个值
+     */
+    @NotBlank(message = "发票行性质lineType不能为空")
+    @Pattern(regexp = "^00|01|02$", message = "发票行性质lineType只能传入:00(正常行)、01(折扣行)、02(被折扣行)")
+    private String lineType;
+
+    /**
+     * 商品代码
+     * 商品唯一标识为商品代码时必传
+     */
+    private String goodsCode;
+
+    /**
+     * 商品名称
+     * 商品唯一标识包含商品名称时必传 | 业务核心字段 非空必填
+     */
+    @NotBlank(message = "商品名称goodsName不能为空")
+    private String goodsName;
+
+    /**
+     * 规格型号
+     * 商品唯一标识包含规格型号时必传;特定要素为14、15、31时,此字段必填,填写车辆识别代号/车架号码
+     */
+    private String model;
+
+    /**
+     * 计量单位
+     * 特定要素为14、15、31时,填写“辆”
+     */
+    private String unit;
+
+    /**
+     * 数量
+     * 数字,最大12位整数,小数最多4位;红字发票时为负数;特定要素为14、15、31时,填写1
+     * 必填 + 数值精度严格校验
+     */
+    @NotNull(message = "商品数量qty不能为空")
+    @Digits(integer = 12, fraction = 4, message = "数量qty格式错误,最大12位整数,最多保留4位小数")
+    private BigDecimal qty;
+
+    /**
+     * 单价
+     * 数字,最大12位整数,小数最多4位
+     * 必填 + 数值精度严格校验
+     */
+    @NotNull(message = "商品单价price不能为空")
+    @Digits(integer = 12, fraction = 4, message = "单价price格式错误,最大12位整数,最多保留4位小数")
+    private BigDecimal price;
+
+    /**
+     * 单价含税标识
+     * 0:不含税 1:含税;传0时price字段代表不含税单价,传1时price字段代表含税单价;默认为0 不含税
+     * 必填 + 只能传0/1
+     */
+    @NotBlank(message = "单价含税标识priceTaxFlag不能为空")
+    @Pattern(regexp = "^0|1$", message = "单价含税标识priceTaxFlag只能传入:0(不含税)、1(含税)")
+    private String priceTaxFlag;
+
+    /**
+     * 税率
+     * 必填 + 小数点后三位有效数字
+     */
+    @NotNull(message = "税率taxrate不能为空")
+    @Digits(integer = 3, fraction = 3, message = "税率taxrate格式错误,最多3位整数,保留3位小数")
+    private BigDecimal taxrate;
+
+    /**
+     * 金额
+     * 不可为空 + 小数点后两位有效数字
+     */
+    @NotNull(message = "金额amount不能为空")
+    @Digits(integer = 12, fraction = 2, message = "金额amount格式错误,最多12位整数,保留2位小数")
+    private BigDecimal amount;
+
+    /**
+     * 税额
+     * 不可为空 + 小数点后两位有效数字
+     */
+    @NotNull(message = "税额tax不能为空")
+    @Digits(integer = 12, fraction = 2, message = "税额tax格式错误,最多12位整数,保留2位小数")
+    private BigDecimal tax;
+
+    /**
+     * 含税金额
+     * 小数点后两位有效数字
+     */
+    @Digits(integer = 12, fraction = 2, message = "含税金额taxamount格式错误,最多12位整数,保留2位小数")
+    private BigDecimal taxamount;
+
+    /**
+     * 优惠政策标识
+     * 01:简易征收;02:稀土产品;03:免税 等指定值,非必填但传值则校验合法性
+     */
+    @Pattern(regexp = "^$|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18",
+            message = "优惠政策标识taxpre传入值不在合法范围内")
+    private String taxpre;
+
+    /**
+     * 税收分类编码
+     * 商品唯一标识包含税收分类编码时必传
+     */
+    private String goodstaxno;
+
+    /**
+     * 扣除额
+     * 保留两位小数
+     */
+    @Digits(integer = 12, fraction = 2, message = "扣除额deduction格式错误,最多12位整数,保留2位小数")
+    private BigDecimal deduction;
+
+    /**
+     * 煤炭种类
+     * 煤炭商品必填; 0100:政府保供煤 0201:长协煤等,非必填但传值则校验合法性
+     */
+    @Pattern(regexp = "^$|0100|0201|0202|0203|0204|0300", message = "煤炭种类coalType传入值不在合法范围内")
+    private String coalType;
+
+    /**
+     * 明细备用1-10 预留字段
+     */
+    private String itemspare1;
+    private String itemspare2;
+    private String itemspare3;
+    private String itemspare4;
+    private String itemspare5;
+    private String itemspare6;
+    private String itemspare7;
+    private String itemspare8;
+    private String itemspare9;
+    private String itemspare10;
+
+
+
+    public String getLineCode() {
+        return lineCode;
+    }
+
+    public void setLineCode(String lineCode) {
+        this.lineCode = lineCode;
+    }
+
+    public String getOriLineCode() {
+        return oriLineCode;
+    }
+
+    public void setOriLineCode(String oriLineCode) {
+        this.oriLineCode = oriLineCode;
+    }
+
+    public String getLineType() {
+        return lineType;
+    }
+
+    public void setLineType(String lineType) {
+        this.lineType = lineType;
+    }
+
+    public String getGoodsCode() {
+        return goodsCode;
+    }
+
+    public void setGoodsCode(String goodsCode) {
+        this.goodsCode = goodsCode;
+    }
+
+    public String getGoodsName() {
+        return goodsName;
+    }
+
+    public void setGoodsName(String goodsName) {
+        this.goodsName = goodsName;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public void setUnit(String unit) {
+        this.unit = unit;
+    }
+
+    public BigDecimal getQty() {
+        return qty;
+    }
+
+    public void setQty(BigDecimal qty) {
+        this.qty = qty;
+    }
+
+    public BigDecimal getPrice() {
+        return price;
+    }
+
+    public void setPrice(BigDecimal price) {
+        this.price = price;
+    }
+
+    public String getPriceTaxFlag() {
+        return priceTaxFlag;
+    }
+
+    public void setPriceTaxFlag(String priceTaxFlag) {
+        this.priceTaxFlag = priceTaxFlag;
+    }
+
+    public BigDecimal getTaxrate() {
+        return taxrate;
+    }
+
+    public void setTaxrate(BigDecimal taxrate) {
+        this.taxrate = taxrate;
+    }
+
+    public BigDecimal getAmount() {
+        return amount;
+    }
+
+    public void setAmount(BigDecimal amount) {
+        this.amount = amount;
+    }
+
+    public BigDecimal getTax() {
+        return tax;
+    }
+
+    public void setTax(BigDecimal tax) {
+        this.tax = tax;
+    }
+
+    public BigDecimal getTaxamount() {
+        return taxamount;
+    }
+
+    public void setTaxamount(BigDecimal taxamount) {
+        this.taxamount = taxamount;
+    }
+
+    public String getTaxpre() {
+        return taxpre;
+    }
+
+    public void setTaxpre(String taxpre) {
+        this.taxpre = taxpre;
+    }
+
+    public String getGoodstaxno() {
+        return goodstaxno;
+    }
+
+    public void setGoodstaxno(String goodstaxno) {
+        this.goodstaxno = goodstaxno;
+    }
+
+    public BigDecimal getDeduction() {
+        return deduction;
+    }
+
+    public void setDeduction(BigDecimal deduction) {
+        this.deduction = deduction;
+    }
+
+    public String getCoalType() {
+        return coalType;
+    }
+
+    public void setCoalType(String coalType) {
+        this.coalType = coalType;
+    }
+
+    public String getItemspare1() {
+        return itemspare1;
+    }
+
+    public void setItemspare1(String itemspare1) {
+        this.itemspare1 = itemspare1;
+    }
+
+    public String getItemspare2() {
+        return itemspare2;
+    }
+
+    public void setItemspare2(String itemspare2) {
+        this.itemspare2 = itemspare2;
+    }
+
+    public String getItemspare3() {
+        return itemspare3;
+    }
+
+    public void setItemspare3(String itemspare3) {
+        this.itemspare3 = itemspare3;
+    }
+
+    public String getItemspare4() {
+        return itemspare4;
+    }
+
+    public void setItemspare4(String itemspare4) {
+        this.itemspare4 = itemspare4;
+    }
+
+    public String getItemspare5() {
+        return itemspare5;
+    }
+
+    public void setItemspare5(String itemspare5) {
+        this.itemspare5 = itemspare5;
+    }
+
+    public String getItemspare6() {
+        return itemspare6;
+    }
+
+    public void setItemspare6(String itemspare6) {
+        this.itemspare6 = itemspare6;
+    }
+
+    public String getItemspare7() {
+        return itemspare7;
+    }
+
+    public void setItemspare7(String itemspare7) {
+        this.itemspare7 = itemspare7;
+    }
+
+    public String getItemspare8() {
+        return itemspare8;
+    }
+
+    public void setItemspare8(String itemspare8) {
+        this.itemspare8 = itemspare8;
+    }
+
+    public String getItemspare9() {
+        return itemspare9;
+    }
+
+    public void setItemspare9(String itemspare9) {
+        this.itemspare9 = itemspare9;
+    }
+
+    public String getItemspare10() {
+        return itemspare10;
+    }
+
+    public void setItemspare10(String itemspare10) {
+        this.itemspare10 = itemspare10;
+    }
+}

+ 164 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/RealPropertyRentInfo.java

@@ -0,0 +1,164 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+/**
+ * 不动产经营租赁服务特定要素子实体类
+ * 仅在开具不动产经营租赁服务特定要素的数电发票(specificFactor为06时)需要传入
+ * 订单明细的规格型号和计量单位不能有值;特定要素信息需与订单明细行数量、顺序保持一致
+ */
+public class RealPropertyRentInfo {
+
+    /**
+     * 不动产地址(省)
+     * 必填,需以省、自治区、直辖市、北京市、天津市、上海市、重庆市任意一个关键词结尾
+     */
+    @NotBlank(message = "不动产地址(省)realPropertyAddress不能为空")
+    @Size(max = 100, message = "不动产地址(省)不能超过100字符")
+    @Pattern(regexp = ".+(省|自治区|直辖市|北京市|天津市|上海市|重庆市)$",
+            message = "不动产地址(省)必须以省、自治区、直辖市、北京市、天津市、上海市、重庆市任意一个关键词结尾")
+    private String realPropertyAddress;
+
+    /**
+     * 不动产地址(市)
+     * 需以市、盟、自治州、地区、区、县任意一个关键词结尾
+     */
+    @Size(max = 20, message = "不动产地址(市)不能超过20字符")
+    @Pattern(regexp = "^$|.+(市|盟|自治州|地区|区|县)$",
+            message = "不动产地址(市)必须以市、盟、自治州、地区、区、县任意一个关键词结尾")
+    private String realPropertyAddressCity;
+
+    /**
+     * 详细地址
+     * 必填,需包含街、路、村、乡、镇、道、巷、号关键词
+     */
+    @NotBlank(message = "详细地址detailAddress不能为空")
+    @Size(max = 120, message = "详细地址不能超过120字符")
+    @Pattern(regexp = ".+(街|路|村|乡|镇|道|巷|号)",
+            message = "详细地址必须包含街、路、村、乡、镇、道、巷、号任意一个关键词")
+    private String detailAddress;
+
+    /**
+     * 租赁开始日期
+     * 必填,格式为yyyy-MM-dd HH:mm;不能晚于租赁结束日期
+     */
+    @NotBlank(message = "租赁开始日期rentStartDate不能为空")
+    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$",
+            message = "租赁开始日期格式错误,应为yyyy-MM-dd HH:mm")
+    private String rentStartDate;
+
+    /**
+     * 租赁结束日期
+     * 必填,格式为yyyy-MM-dd HH:mm;不能早于租赁开始日期
+     */
+    @NotBlank(message = "租赁结束日期rentEndDate不能为空")
+    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$",
+            message = "租赁结束日期格式错误,应为yyyy-MM-dd HH:mm")
+    private String rentEndDate;
+
+    /**
+     * 跨地(市)标志
+     * 必填,0:否 1:是
+     */
+    @NotBlank(message = "跨地(市)标志crossCityFlag不能为空")
+    @Pattern(regexp = "^0|1$", message = "跨地(市)标志只能传入:0(否)、1(是)")
+    private String crossCityFlag;
+
+    /**
+     * 产权证书/不动产权证号
+     * 最大40字符
+     */
+    @Size(max = 40, message = "产权证书/不动产权证号不能超过40字符")
+    private String realPropertyCertificate;
+
+    /**
+     * 面积单位
+     * 必填,可选值:1(平方千米)、2(平方米)、3(公顷)、4(亩)、5(hm²)、6(km²)、7(m²)、8(米)
+     */
+    @NotBlank(message = "面积单位unit不能为空")
+    @Pattern(regexp = "^1|2|3|4|5|6|7|8$",
+            message = "面积单位只能传入:1(平方千米)、2(平方米)、3(公顷)、4(亩)、5(hm²)、6(km²)、7(m²)、8(米)")
+    private String unit;
+
+    /**
+     * 车牌号
+     * 税收分类编码为3040502020200000000时必填,最大20字符
+     */
+    @Size(max = 20, message = "车牌号不能超过20字符")
+    private String carNo;
+
+    public String getRealPropertyAddress() {
+        return realPropertyAddress;
+    }
+
+    public void setRealPropertyAddress(String realPropertyAddress) {
+        this.realPropertyAddress = realPropertyAddress;
+    }
+
+    public String getRealPropertyAddressCity() {
+        return realPropertyAddressCity;
+    }
+
+    public void setRealPropertyAddressCity(String realPropertyAddressCity) {
+        this.realPropertyAddressCity = realPropertyAddressCity;
+    }
+
+    public String getDetailAddress() {
+        return detailAddress;
+    }
+
+    public void setDetailAddress(String detailAddress) {
+        this.detailAddress = detailAddress;
+    }
+
+    public String getRentStartDate() {
+        return rentStartDate;
+    }
+
+    public void setRentStartDate(String rentStartDate) {
+        this.rentStartDate = rentStartDate;
+    }
+
+    public String getRentEndDate() {
+        return rentEndDate;
+    }
+
+    public void setRentEndDate(String rentEndDate) {
+        this.rentEndDate = rentEndDate;
+    }
+
+    public String getCrossCityFlag() {
+        return crossCityFlag;
+    }
+
+    public void setCrossCityFlag(String crossCityFlag) {
+        this.crossCityFlag = crossCityFlag;
+    }
+
+    public String getRealPropertyCertificate() {
+        return realPropertyCertificate;
+    }
+
+    public void setRealPropertyCertificate(String realPropertyCertificate) {
+        this.realPropertyCertificate = realPropertyCertificate;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public void setUnit(String unit) {
+        this.unit = unit;
+    }
+
+    public String getCarNo() {
+        return carNo;
+    }
+
+    public void setCarNo(String carNo) {
+        this.carNo = carNo;
+    }
+}

+ 177 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/RealPropertySellInfo.java

@@ -0,0 +1,177 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Digits;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+import java.math.BigDecimal;
+
+/**
+ * 不动产销售特定要素子实体类
+ * 仅在开具不动产销售特定要素的数电发票(specificFactor为05时)需要传入
+ * 此时商品只能有一行明细,且规格型号、计量单位都不能有值
+ */
+public class RealPropertySellInfo {
+
+    /**
+     * 不动产单元代码/网签合同备案编号
+     * 最大28字符
+     */
+    @Size(max = 28, message = "不动产单元代码/网签合同备案编号不能超过28字符")
+    private String realPropertyContractNumber;
+
+    /**
+     * 不动产地址(省)
+     * 必填,需以省、自治区、直辖市、北京市、天津市、上海市、重庆市任意一个关键词结尾
+     */
+    @NotBlank(message = "不动产地址(省)realPropertyAddress不能为空")
+    @Size(max = 100, message = "不动产地址(省)不能超过100字符")
+    @Pattern(regexp = ".+(省|自治区|直辖市|北京市|天津市|上海市|重庆市)$",
+            message = "不动产地址(省)必须以省、自治区、直辖市、北京市、天津市、上海市、重庆市任意一个关键词结尾")
+    private String realPropertyAddress;
+
+    /**
+     * 不动产地址(市)
+     * 需以市、盟、自治州、地区、区、县任意一个关键词结尾
+     */
+    @Size(max = 20, message = "不动产地址(市)不能超过20字符")
+    @Pattern(regexp = "^$|.+(市|盟|自治州|地区|区|县)$",
+            message = "不动产地址(市)必须以市、盟、自治州、地区、区、县任意一个关键词结尾")
+    private String realPropertyAddressCity;
+
+    /**
+     * 详细地址
+     * 必填,需包含街、路、村、乡、镇、道、巷、号关键词
+     */
+    @NotBlank(message = "详细地址detailAddress不能为空")
+    @Size(max = 120, message = "详细地址不能超过120字符")
+    @Pattern(regexp = ".+(街|路|村|乡|镇|道|巷|号)",
+            message = "详细地址必须包含街、路、村、乡、镇、道、巷、号任意一个关键词")
+    private String detailAddress;
+
+    /**
+     * 跨地(市)标志
+     * 必填,0:否 1:是
+     */
+    @NotBlank(message = "跨地(市)标志crossCityFlag不能为空")
+    @Pattern(regexp = "^0|1$", message = "跨地(市)标志只能传入:0(否)、1(是)")
+    private String crossCityFlag;
+
+    /**
+     * 土地增值税项目编号
+     * 最大18字符
+     */
+    @Size(max = 18, message = "土地增值税项目编号不能超过18字符")
+    private String incrementTaxNumber;
+
+    /**
+     * 核定计税价格
+     * 小数点后保留两位小数
+     */
+    @Digits(integer = 20, fraction = 2, message = "核定计税价格格式错误,最多20位整数,保留2位小数")
+    private BigDecimal price;
+
+    /**
+     * 实际成交税金额
+     * 小数点后保留两位小数;当核定计税价格有值时必填
+     */
+    @Digits(integer = 20, fraction = 2, message = "实际成交税金额格式错误,最多20位整数,保留2位小数")
+    private BigDecimal taxAmount;
+
+    /**
+     * 产权证书/不动产权证号
+     * 最大40字符
+     */
+    @Size(max = 40, message = "产权证书/不动产权证号不能超过40字符")
+    private String realPropertyCertificate;
+
+    /**
+     * 面积单位
+     * 必填,可选值:1(平方千米)、2(平方米)、3(公顷)、4(亩)、5(hm²)、6(km²)、7(m²)、8(米)
+     */
+    @NotBlank(message = "面积单位unit不能为空")
+    @Pattern(regexp = "^1|2|3|4|5|6|7|8$",
+            message = "面积单位只能传入:1(平方千米)、2(平方米)、3(公顷)、4(亩)、5(hm²)、6(km²)、7(m²)、8(米)")
+    private String unit;
+
+    public String getRealPropertyContractNumber() {
+        return realPropertyContractNumber;
+    }
+
+    public void setRealPropertyContractNumber(String realPropertyContractNumber) {
+        this.realPropertyContractNumber = realPropertyContractNumber;
+    }
+
+    public String getRealPropertyAddress() {
+        return realPropertyAddress;
+    }
+
+    public void setRealPropertyAddress(String realPropertyAddress) {
+        this.realPropertyAddress = realPropertyAddress;
+    }
+
+    public String getRealPropertyAddressCity() {
+        return realPropertyAddressCity;
+    }
+
+    public void setRealPropertyAddressCity(String realPropertyAddressCity) {
+        this.realPropertyAddressCity = realPropertyAddressCity;
+    }
+
+    public String getDetailAddress() {
+        return detailAddress;
+    }
+
+    public void setDetailAddress(String detailAddress) {
+        this.detailAddress = detailAddress;
+    }
+
+    public String getCrossCityFlag() {
+        return crossCityFlag;
+    }
+
+    public void setCrossCityFlag(String crossCityFlag) {
+        this.crossCityFlag = crossCityFlag;
+    }
+
+    public String getIncrementTaxNumber() {
+        return incrementTaxNumber;
+    }
+
+    public void setIncrementTaxNumber(String incrementTaxNumber) {
+        this.incrementTaxNumber = incrementTaxNumber;
+    }
+
+    public BigDecimal getPrice() {
+        return price;
+    }
+
+    public void setPrice(BigDecimal price) {
+        this.price = price;
+    }
+
+    public BigDecimal getTaxAmount() {
+        return taxAmount;
+    }
+
+    public void setTaxAmount(BigDecimal taxAmount) {
+        this.taxAmount = taxAmount;
+    }
+
+    public String getRealPropertyCertificate() {
+        return realPropertyCertificate;
+    }
+
+    public void setRealPropertyCertificate(String realPropertyCertificate) {
+        this.realPropertyCertificate = realPropertyCertificate;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public void setUnit(String unit) {
+        this.unit = unit;
+    }
+}

+ 148 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/TravellerTransport.java

@@ -0,0 +1,148 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+/**
+ * 旅客运输服务特定要素子实体类
+ * 仅在开具旅客运输服务特定要素的数电发票(specificFactor为09时)需要传入
+ * 最多2000行,至少1行;若任意字段填写则触发全量必填校验
+ */
+public class TravellerTransport {
+
+    /**
+     * 出行人
+     * 必填,最大20字符
+     */
+    @NotBlank(message = "出行人traveller不能为空")
+    @Size(max = 20, message = "出行人长度不能超过20字符")
+    private String traveller;
+
+    /**
+     * 出行日期
+     * 必填,格式为yyyy-MM-dd
+     */
+    @NotBlank(message = "出行日期travelDate不能为空")
+    @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$",
+            message = "出行日期格式错误,必须为yyyy-MM-dd")
+    private String travelDate;
+
+    /**
+     * 出行人证件类型
+     * 必填,可选值:101(组织机构代码证)、102(营业执照)、103(税务登记证)、199(其他单位证件)、201(居民身份证)等
+     */
+    @NotBlank(message = "出行人证件类型travellerCardType不能为空")
+    @Pattern(regexp = "^101|102|103|199|201|202|203|204|205|206|207|208|210|212|213|214|215|216|299$",
+            message = "出行人证件类型不在合法范围内")
+    private String travellerCardType;
+
+    /**
+     * 出行人证件号码
+     * 必填,最大20字符
+     */
+    @NotBlank(message = "出行人证件号码travellerCardNo不能为空")
+    @Size(max = 20, message = "出行人证件号码长度不能超过20字符")
+    private String travellerCardNo;
+
+    /**
+     * 出行地
+     * 必填,最大80字符
+     */
+    @NotBlank(message = "出行地travelPlace不能为空")
+    @Size(max = 80, message = "出行地长度不能超过80字符")
+    private String travelPlace;
+
+    /**
+     * 到达地
+     * 必填,最大80字符
+     */
+    @NotBlank(message = "到达地arrivePlace不能为空")
+    @Size(max = 80, message = "到达地长度不能超过80字符")
+    private String arrivePlace;
+
+    /**
+     * 交通工具类型
+     * 必填,可选值:1(飞机)、2(火车)、3(长途汽车)、4(公共交通)、5(出租车)、6(汽车)、7(船舶)、9(其他)
+     */
+    @NotBlank(message = "交通工具类型vehicleType不能为空")
+    @Pattern(regexp = "^1|2|3|4|5|6|7|9$",
+            message = "交通工具类型只能传入:1(飞机)、2(火车)、3(长途汽车)、4(公共交通)、5(出租车)、6(汽车)、7(船舶)、9(其他)")
+    private String vehicleType;
+
+    /**
+     * 交通工具等级
+     * 当交通工具类型为火车、飞机、船舶时必填,对应可选值:
+     * 火车:一等座/二等座/软席/硬席
+     * 飞机:公务舱/头等舱/经济舱
+     * 船舶:一等舱/二等舱/三等舱
+     */
+    @Pattern(regexp = "^$|一等座|二等座|软席|硬席|公务舱|头等舱|经济舱|一等舱|二等舱|三等舱",
+            message = "交通工具等级不在合法范围内")
+    private String vehicleLevel;
+
+    public String getTraveller() {
+        return traveller;
+    }
+
+    public void setTraveller(String traveller) {
+        this.traveller = traveller;
+    }
+
+    public String getTravelDate() {
+        return travelDate;
+    }
+
+    public void setTravelDate(String travelDate) {
+        this.travelDate = travelDate;
+    }
+
+    public String getTravellerCardType() {
+        return travellerCardType;
+    }
+
+    public void setTravellerCardType(String travellerCardType) {
+        this.travellerCardType = travellerCardType;
+    }
+
+    public String getTravellerCardNo() {
+        return travellerCardNo;
+    }
+
+    public void setTravellerCardNo(String travellerCardNo) {
+        this.travellerCardNo = travellerCardNo;
+    }
+
+    public String getTravelPlace() {
+        return travelPlace;
+    }
+
+    public void setTravelPlace(String travelPlace) {
+        this.travelPlace = travelPlace;
+    }
+
+    public String getArrivePlace() {
+        return arrivePlace;
+    }
+
+    public void setArrivePlace(String arrivePlace) {
+        this.arrivePlace = arrivePlace;
+    }
+
+    public String getVehicleType() {
+        return vehicleType;
+    }
+
+    public void setVehicleType(String vehicleType) {
+        this.vehicleType = vehicleType;
+    }
+
+    public String getVehicleLevel() {
+        return vehicleLevel;
+    }
+
+    public void setVehicleLevel(String vehicleLevel) {
+        this.vehicleLevel = vehicleLevel;
+    }
+}

+ 56 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/UsedCar.java

@@ -0,0 +1,56 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Size;
+
+/**
+ * 二手车发票关联的二手车销售统一发票信息子实体类
+ * 特定要素为31时必传,15时选传,只能有一组信息;二手车发票明细只能有一行
+ */
+public class UsedCar {
+    /**
+     * 关联的二手车销售统一发票数电发票号码
+     * 必填,最大20字符
+     */
+    @NotBlank(message = "关联的二手车销售统一发票数电发票号码allEinVno不能为空")
+    @Size(max = 20, message = "关联的数电发票号码长度不能超过20字符")
+    private String allEinVno;
+
+    /**
+     * 关联的二手车销售统一发票纸质发票代码
+     * 选填,最大12字符(或10字符)
+     */
+    @Size(max = 12, message = "关联的纸质发票代码长度不能超过12字符")
+    private String invcode;
+
+    /**
+     * 关联的二手车销售统一发票纸质发票号码
+     * 选填,最大8字符
+     */
+    @Size(max = 8, message = "关联的纸质发票号码长度不能超过8字符")
+    private String invno;
+
+    public String getAllEinVno() {
+        return allEinVno;
+    }
+
+    public void setAllEinVno(String allEinVno) {
+        this.allEinVno = allEinVno;
+    }
+
+    public String getInvcode() {
+        return invcode;
+    }
+
+    public void setInvcode(String invcode) {
+        this.invcode = invcode;
+    }
+
+    public String getInvno() {
+        return invno;
+    }
+
+    public void setInvno(String invno) {
+        this.invno = invno;
+    }
+}

+ 187 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/UsedCarSale.java

@@ -0,0 +1,187 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Size;
+
+/**
+ * 二手车销售统一发票信息子实体类
+ * 发票类型为84(数电票二手车销售统一发票)、88(数电纸票二手车销售统一发票)时必传
+ */
+public class UsedCarSale {
+
+    /**
+     * 车牌照号
+     * 必填,最大20字符
+     */
+    @NotBlank(message = "车牌照号carNo不能为空")
+    @Size(max = 20, message = "车牌照号长度不能超过20字符")
+    private String carNo;
+
+    /**
+     * 登记证号
+     * 必填,最大20字符
+     */
+    @NotBlank(message = "登记证号registrationNo不能为空")
+    @Size(max = 20, message = "登记证号长度不能超过20字符")
+    private String registrationNo;
+
+    /**
+     * 车辆识别代号/车架号码
+     * 必填,最大23字符
+     */
+    @NotBlank(message = "车辆识别代号/车架号码frameNo不能为空")
+    @Size(max = 23, message = "车辆识别代号长度不能超过23字符")
+    private String frameNo;
+
+    /**
+     * 厂牌型号
+     * 必填,最大60字符
+     */
+    @NotBlank(message = "厂牌型号brandModel不能为空")
+    @Size(max = 60, message = "厂牌型号长度不能超过60字符")
+    private String brandModel;
+
+    /**
+     * 转入地车辆管理所名称
+     * 必填,最大80字符
+     */
+    @NotBlank(message = "转入地车辆管理所名称transferInVehicleRegistrationOffice不能为空")
+    @Size(max = 80, message = "转入地车辆管理所名称长度不能超过80字符")
+    private String transferInVehicleRegistrationOffice;
+
+    /**
+     * 经营单位名称
+     * 必填,最大100字符
+     */
+    @NotBlank(message = "经营单位名称businessEntityName不能为空")
+    @Size(max = 100, message = "经营单位名称长度不能超过100字符")
+    private String businessEntityName;
+
+    /**
+     * 经营单位纳税人识别号
+     * 必填,最大20字符
+     */
+    @NotBlank(message = "经营单位纳税人识别号businessEntityTaxno不能为空")
+    @Size(max = 20, message = "经营单位纳税人识别号长度不能超过20字符")
+    private String businessEntityTaxno;
+
+    /**
+     * 经营单位地址
+     * 必填,最大100字符
+     */
+    @NotBlank(message = "经营单位地址businessEntityAddr不能为空")
+    @Size(max = 100, message = "经营单位地址长度不能超过100字符")
+    private String businessEntityAddr;
+
+    /**
+     * 经营单位电话
+     * 必填,最大11字符
+     */
+    @NotBlank(message = "经营单位电话businessEntityPhone不能为空")
+    @Size(max = 11, message = "经营单位电话长度不能超过11字符")
+    private String businessEntityPhone;
+
+    /**
+     * 经营单位开户银行
+     * 必填,最大100字符
+     */
+    @NotBlank(message = "经营单位开户银行businessEntityBank不能为空")
+    @Size(max = 100, message = "经营单位开户银行长度不能超过100字符")
+    private String businessEntityBank;
+
+    /**
+     * 经营单位银行账号
+     * 必填,最大60字符
+     */
+    @NotBlank(message = "经营单位银行账号businessEntityBankAccount不能为空")
+    @Size(max = 60, message = "经营单位银行账号长度不能超过60字符")
+    private String businessEntityBankAccount;
+
+    public String getCarNo() {
+        return carNo;
+    }
+
+    public void setCarNo(String carNo) {
+        this.carNo = carNo;
+    }
+
+    public String getRegistrationNo() {
+        return registrationNo;
+    }
+
+    public void setRegistrationNo(String registrationNo) {
+        this.registrationNo = registrationNo;
+    }
+
+    public String getFrameNo() {
+        return frameNo;
+    }
+
+    public void setFrameNo(String frameNo) {
+        this.frameNo = frameNo;
+    }
+
+    public String getBrandModel() {
+        return brandModel;
+    }
+
+    public void setBrandModel(String brandModel) {
+        this.brandModel = brandModel;
+    }
+
+    public String getTransferInVehicleRegistrationOffice() {
+        return transferInVehicleRegistrationOffice;
+    }
+
+    public void setTransferInVehicleRegistrationOffice(String transferInVehicleRegistrationOffice) {
+        this.transferInVehicleRegistrationOffice = transferInVehicleRegistrationOffice;
+    }
+
+    public String getBusinessEntityName() {
+        return businessEntityName;
+    }
+
+    public void setBusinessEntityName(String businessEntityName) {
+        this.businessEntityName = businessEntityName;
+    }
+
+    public String getBusinessEntityTaxno() {
+        return businessEntityTaxno;
+    }
+
+    public void setBusinessEntityTaxno(String businessEntityTaxno) {
+        this.businessEntityTaxno = businessEntityTaxno;
+    }
+
+    public String getBusinessEntityAddr() {
+        return businessEntityAddr;
+    }
+
+    public void setBusinessEntityAddr(String businessEntityAddr) {
+        this.businessEntityAddr = businessEntityAddr;
+    }
+
+    public String getBusinessEntityPhone() {
+        return businessEntityPhone;
+    }
+
+    public void setBusinessEntityPhone(String businessEntityPhone) {
+        this.businessEntityPhone = businessEntityPhone;
+    }
+
+    public String getBusinessEntityBank() {
+        return businessEntityBank;
+    }
+
+    public void setBusinessEntityBank(String businessEntityBank) {
+        this.businessEntityBank = businessEntityBank;
+    }
+
+    public String getBusinessEntityBankAccount() {
+        return businessEntityBankAccount;
+    }
+
+    public void setBusinessEntityBankAccount(String businessEntityBankAccount) {
+        this.businessEntityBankAccount = businessEntityBankAccount;
+    }
+}

+ 194 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/Vehicle.java

@@ -0,0 +1,194 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+/**
+ * 机动车销售统一发票信息子实体类
+ * 发票类型为83、87时必传
+ */
+public class Vehicle {
+
+    /**
+     * 厂牌型号
+     * 选填,最大140字符;进口车选填,为空时使用车架号码关联的厂牌型号;国产车不填写
+     */
+    @Size(max = 140, message = "厂牌型号长度不能超过140字符")
+    private String brandModel;
+
+    /**
+     * 合格证号
+     * 选填,最大50字符;乐企通道不填写,为空时使用车架号码关联的合格证号
+     */
+    @Size(max = 50, message = "合格证号长度不能超过50字符")
+    private String passCertificate;
+
+    /**
+     * 发动机号码
+     * 选填,最大140字符;乐企通道不填写,为空时使用车架号码关联的发动机号码
+     */
+    @Size(max = 140, message = "发动机号码长度不能超过140字符")
+    private String engineCode;
+
+    /**
+     * 车架号码
+     * 必填,17字符
+     */
+    @NotBlank(message = "车架号码frameNo不能为空")
+    @Size(min = 17, max = 17, message = "车架号码必须为17字符")
+    private String frameNo;
+
+    /**
+     * 产地
+     * 选填,最大10字符
+     */
+    @Size(max = 10, message = "产地长度不能超过10字符")
+    private String origin;
+
+    /**
+     * 进口证明书号
+     * 选填,最大16字符;乐企通道不填写,当进口证明书号不为空时,合格证号将为空
+     */
+    @Size(max = 16, message = "进口证明书号长度不能超过16字符")
+    private String importCertificateNo;
+
+    /**
+     * 商检单号
+     * 选填,最大60字符
+     */
+    @Size(max = 60, message = "商检单号长度不能超过60字符")
+    private String inspectionNo;
+
+    /**
+     * 吨位
+     * 选填,最大10字符;乐企通道不填写
+     */
+    @Size(max = 10, message = "吨位长度不能超过10字符")
+    private String tonnage;
+
+    /**
+     * 限乘人数
+     * 选填,最大11字符;乐企通道不填写
+     */
+    @Size(max = 11, message = "限乘人数长度不能超过11字符")
+    private String limitNum;
+
+    /**
+     * 完税凭证号码
+     * 选填,最大100字符
+     */
+    @Size(max = 100, message = "完税凭证号码长度不能超过100字符")
+    private String taxPaymentReceiptNo;
+
+    /**
+     * 生产企业名称
+     * 选填,最大300字符;为空时使用车架号码关联的生产企业名称
+     */
+    @Size(max = 300, message = "生产企业名称长度不能超过300字符")
+    private String productionCompanyName;
+
+    /**
+     * 制造或进口日期
+     * 选填,格式为yyyy-mm-dd;为空时使用车架号码关联的车辆制造日期
+     */
+    @Pattern(regexp = "^$|^\\d{4}-\\d{2}-\\d{2}$",
+            message = "制造或进口日期格式错误,必须为yyyy-mm-dd")
+    private String vehicleProdDate;
+
+    public String getBrandModel() {
+        return brandModel;
+    }
+
+    public void setBrandModel(String brandModel) {
+        this.brandModel = brandModel;
+    }
+
+    public String getPassCertificate() {
+        return passCertificate;
+    }
+
+    public void setPassCertificate(String passCertificate) {
+        this.passCertificate = passCertificate;
+    }
+
+    public String getEngineCode() {
+        return engineCode;
+    }
+
+    public void setEngineCode(String engineCode) {
+        this.engineCode = engineCode;
+    }
+
+    public String getFrameNo() {
+        return frameNo;
+    }
+
+    public void setFrameNo(String frameNo) {
+        this.frameNo = frameNo;
+    }
+
+    public String getOrigin() {
+        return origin;
+    }
+
+    public void setOrigin(String origin) {
+        this.origin = origin;
+    }
+
+    public String getImportCertificateNo() {
+        return importCertificateNo;
+    }
+
+    public void setImportCertificateNo(String importCertificateNo) {
+        this.importCertificateNo = importCertificateNo;
+    }
+
+    public String getInspectionNo() {
+        return inspectionNo;
+    }
+
+    public void setInspectionNo(String inspectionNo) {
+        this.inspectionNo = inspectionNo;
+    }
+
+    public String getTonnage() {
+        return tonnage;
+    }
+
+    public void setTonnage(String tonnage) {
+        this.tonnage = tonnage;
+    }
+
+    public String getLimitNum() {
+        return limitNum;
+    }
+
+    public void setLimitNum(String limitNum) {
+        this.limitNum = limitNum;
+    }
+
+    public String getTaxPaymentReceiptNo() {
+        return taxPaymentReceiptNo;
+    }
+
+    public void setTaxPaymentReceiptNo(String taxPaymentReceiptNo) {
+        this.taxPaymentReceiptNo = taxPaymentReceiptNo;
+    }
+
+    public String getProductionCompanyName() {
+        return productionCompanyName;
+    }
+
+    public void setProductionCompanyName(String productionCompanyName) {
+        this.productionCompanyName = productionCompanyName;
+    }
+
+    public String getVehicleProdDate() {
+        return vehicleProdDate;
+    }
+
+    public void setVehicleProdDate(String vehicleProdDate) {
+        this.vehicleProdDate = vehicleProdDate;
+    }
+}

+ 159 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/VehicleVesselInfo.java

@@ -0,0 +1,159 @@
+package com.jeeplus.assess.invoice.domain.OMS;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Digits;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+import java.math.BigDecimal;
+
+/**
+ * 代收车船税信息子实体类
+ * 仅在开具代收车船税特定要素的数电发票(specificFactor为07时)需要传入
+ * 且只能有一组,发票明细只能有一行(或一对折扣明细行)
+ */
+public class VehicleVesselInfo {
+
+    /**
+     * 保险单号
+     * 必填,最大40字符
+     */
+    @NotBlank(message = "保险单号policyNo不能为空")
+    @Size(max = 40, message = "保险单号长度不能超过40字符")
+    private String policyNo;
+
+    /**
+     * 车牌号
+     * 必填,最大40字符
+     */
+    @NotBlank(message = "车牌号carNo不能为空")
+    @Size(max = 40, message = "车牌号长度不能超过40字符")
+    private String carNo;
+
+    /**
+     * 税款所属期
+     * 必填,格式为:YYYY-MM YYYY-MM(如2023-11 2023-12),用空格分隔
+     */
+    @NotBlank(message = "税款所属期taxPeriod不能为空")
+    @Pattern(regexp = "^\\d{4}-\\d{2} \\d{4}-\\d{2}$",
+            message = "税款所属期格式错误,必须为YYYY-MM YYYY-MM(如2023-11 2023-12)")
+    private String taxPeriod;
+
+    /**
+     * 代收车船税金额
+     * 必填,最多16位整数,保留2位小数
+     */
+    @NotBlank(message = "代收车船税金额amount不能为空")
+    @Digits(integer = 16, fraction = 2, message = "代收车船税金额格式错误,最多16位整数,保留2位小数")
+    private BigDecimal amount;
+
+    /**
+     * 滞纳金金额
+     * 必填,最多16位整数,保留2位小数
+     */
+    @NotBlank(message = "滞纳金金额lateFeeAmount不能为空")
+    @Digits(integer = 16, fraction = 2, message = "滞纳金金额格式错误,最多16位整数,保留2位小数")
+    private BigDecimal lateFeeAmount;
+
+    /**
+     * 金额合计
+     * 必填,最多16位整数,保留2位小数
+     */
+    @NotBlank(message = "金额合计totalamount不能为空")
+    @Digits(integer = 16, fraction = 2, message = "金额合计格式错误,最多16位整数,保留2位小数")
+    private BigDecimal totalamount;
+
+    /**
+     * 车辆识别代码/车架号码
+     * 必填,17字符
+     */
+    @NotBlank(message = "车辆识别代码frameNo不能为空")
+    @Size(min = 17, max = 17, message = "车辆识别代码必须为17字符")
+    private String frameNo;
+
+    /**
+     * 发动机号码
+     * 选填,最大40字符
+     */
+    @Size(max = 40, message = "发动机号码长度不能超过40字符")
+    private String engineCode;
+
+    /**
+     * 底盘号
+     * 选填,最大40字符
+     */
+    @Size(max = 40, message = "底盘号长度不能超过40字符")
+    private String chassisCode;
+
+    public String getPolicyNo() {
+        return policyNo;
+    }
+
+    public void setPolicyNo(String policyNo) {
+        this.policyNo = policyNo;
+    }
+
+    public String getCarNo() {
+        return carNo;
+    }
+
+    public void setCarNo(String carNo) {
+        this.carNo = carNo;
+    }
+
+    public String getTaxPeriod() {
+        return taxPeriod;
+    }
+
+    public void setTaxPeriod(String taxPeriod) {
+        this.taxPeriod = taxPeriod;
+    }
+
+    public BigDecimal getAmount() {
+        return amount;
+    }
+
+    public void setAmount(BigDecimal amount) {
+        this.amount = amount;
+    }
+
+    public BigDecimal getLateFeeAmount() {
+        return lateFeeAmount;
+    }
+
+    public void setLateFeeAmount(BigDecimal lateFeeAmount) {
+        this.lateFeeAmount = lateFeeAmount;
+    }
+
+    public BigDecimal getTotalamount() {
+        return totalamount;
+    }
+
+    public void setTotalamount(BigDecimal totalamount) {
+        this.totalamount = totalamount;
+    }
+
+    public String getFrameNo() {
+        return frameNo;
+    }
+
+    public void setFrameNo(String frameNo) {
+        this.frameNo = frameNo;
+    }
+
+    public String getEngineCode() {
+        return engineCode;
+    }
+
+    public void setEngineCode(String engineCode) {
+        this.engineCode = engineCode;
+    }
+
+    public String getChassisCode() {
+        return chassisCode;
+    }
+
+    public void setChassisCode(String chassisCode) {
+        this.chassisCode = chassisCode;
+    }
+}

+ 444 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSAllScenarioRedInvoiceInfo.java

@@ -0,0 +1,444 @@
+package com.jeeplus.assess.invoice.domain.OMS.fastRed;
+
+import com.google.common.collect.Lists;
+
+import javax.validation.constraints.Pattern;
+import java.util.List;
+
+/**
+ * OMS全场景红票信息实体类
+ * 对应业务场景:全场景发票冲红操作的参数封装
+ * 所有字段均采用 @Pattern 正则校验,规则:^$ 表示允许为空,| 分隔所有合法有效值
+ */
+public class OMSAllScenarioRedInvoiceInfo {
+
+    /**
+     * 组织代码(必填)
+     * 所属组织唯一标识,长度1-20位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,20}$", message = "组织代码deptCode为必填项,长度不能超过20位")
+    private String deptCode;
+
+    /**
+     * 申请单号(必填)
+     * 唯一标识,长度1-32位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,32}$", message = "申请单号applyNo为必填项,长度不能超过32位")
+    private String applyNo;
+
+    /**
+     * 申请方身份(必填)
+     * 0:销方 1:购方
+     */
+    @Pattern(regexp = "0|1", message = "申请方身份applyIdentity为必填项,只能传入0或1")
+    private String applyIdentity;
+
+    /**
+     * 销售方纳税人识别号(必填)
+     * 长度1-20位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,20}$", message = "销售方纳税人识别号sellerTaxno为必填项,长度不能超过20位")
+    private String sellerTaxno;
+
+    /**
+     * 销售方名称(必填)
+     * 长度1-100位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,100}$", message = "销售方名称sellerName为必填项,长度不能超过100位")
+    private String sellerName;
+
+    /**
+     * 购买方纳税人识别号(非必填)
+     * 长度1-20位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,20}$", message = "购买方纳税人识别号buyerTaxno只能传入:空 或 长度不超过20位的任意字符")
+    private String buyerTaxno;
+
+    /**
+     * 购买方名称(必填)
+     * 长度1-100位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,100}$", message = "购买方名称buyerName为必填项,长度不能超过100位")
+    private String buyerName;
+
+    /**
+     * 原蓝票发票代码(非必填)
+     * 原蓝票为增值税发票时必传,空或12位纯数字
+     */
+    @Pattern(regexp = "^$|^\\d{12}$", message = "原蓝票发票代码originalInvcode只能传入:空 或 12位纯数字")
+    private String originalInvcode;
+
+    /**
+     * 原蓝票发票号码(必填)
+     * 数电票传20位,增值税票传8位纯数字
+     */
+    @Pattern(regexp = "^\\d{8}$|^\\d{20}$", message = "原蓝票发票号码originalInvno为必填项,只能传入8位或20位纯数字")
+    private String originalInvno;
+
+    /**
+     * 原蓝票发票类型(必填)
+     * 可选值:01-数电票(增值税专用发票)、02-数电票(普通发票)、03-数电纸质发票(增值税专用发票)、04-数电纸质发票(普通发票)、
+     * 83-数电票(机动车销售统一发票)、84-数电票(二手车销售统一发票)、87-数电纸质发票(机动车销售统一发票)、
+     * 88-数电纸质发票(二手车销售统一发票)、05-增值税电子普通发票、06-增值税普通发票、07-增值税专用发票、
+     * 08-增值税电子专用发票、09-机动车统一销售发票、15-二手车统一销售发票
+     */
+    @Pattern(regexp = "01|02|03|04|83|84|87|88|05|06|07|08|09|15",
+            message = "原蓝票发票类型originalInvType为必填项,只能传入指定枚举值")
+    private String originalInvType;
+
+    /**
+     * 原蓝票金额(必填)
+     * 正数,保留两位小数
+     */
+    @Pattern(regexp = "^\\d+(\\.\\d{1,2})?$", message = "原蓝票金额originalInvAmount为必填项,只能传入正数且保留两位小数")
+    private String originalInvAmount;
+
+    /**
+     * 原蓝票税额(必填)
+     * 正数,保留两位小数
+     */
+    @Pattern(regexp = "^\\d+(\\.\\d{1,2})?$", message = "原蓝票税额originalInvTax为必填项,只能传入正数且保留两位小数")
+    private String originalInvTax;
+
+    /**
+     * 原蓝票开票日期(非必填)
+     * 格式:yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd
+     */
+    @Pattern(regexp = "^$|^\\d{4}-\\d{2}-\\d{2}( \\d{2}:\\d{2}:\\d{2})?$",
+            message = "原蓝票开票日期originalInvDate只能传入:空 或 格式为yyyy-MM-dd HH:mm:ss/yyyy-MM-dd的日期")
+    private String originalInvDate;
+
+    /**
+     * 冲红原因(必填)
+     * 01:开票有误 02:销货退回 03:服务中止 04:销售折让
+     */
+    @Pattern(regexp = "01|02|03|04", message = "冲红原因redReason为必填项,只能传入01/02/03/04")
+    private String redReason;
+
+    /**
+     * 合计金额(不含税)(必填)
+     * 负数,保留两位小数
+     */
+    @Pattern(regexp = "^-\\d+(\\.\\d{1,2})?$", message = "合计金额(不含税)totalAmount为必填项,只能传入负数且保留两位小数")
+    private String totalAmount;
+
+    /**
+     * 合计税额(必填)
+     * 负数,保留两位小数
+     */
+    @Pattern(regexp = "^-\\d+(\\.\\d{1,2})?$", message = "合计税额totalTax为必填项,只能传入负数且保留两位小数")
+    private String totalTax;
+
+    /**
+     * 价税合计(必填)
+     * 负数,保留两位小数
+     */
+    @Pattern(regexp = "^-\\d+(\\.\\d{1,2})?$", message = "价税合计totalTaxamount为必填项,只能传入负数且保留两位小数")
+    private String totalTaxamount;
+
+    /**
+     * 原蓝票特定要素类型(非必填)
+     * 可选值:01-成品油发票、02-稀土发票、03-建筑服务发票、04-货物运输服务发票、05-不动产销售服务发票、
+     * 06-不动产租赁服务发票、07-代收车船税、08-通行费、09-旅客运输服务发票、10-医疗服务(住院)发票、
+     * 11-医疗服务(门诊)发票、12-自产农产品销售发票、13-拖拉机和联合收割机发票、14-机动车、15-二手车、
+     * 16-农产品收购发票、17-光伏收购发票、18-卷烟发票、31-二手车*、32-电子烟、51-二手车正向开具、52-二手车反向开具
+     */
+    @Pattern(regexp = "^$|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|31|32|51|52",
+            message = "原蓝票特定要素类型oriSpecificFactor只能传入:空 或 指定枚举值")
+    private String oriSpecificFactor;
+
+    /**
+     * 备用1(非必填)
+     * 预留字段,长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "备用1spare1只能传入:空 或 长度不超过100位的任意字符")
+    private String spare1;
+
+    /**
+     * 备用2(非必填)
+     * 预留字段,长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "备用2spare2只能传入:空 或 长度不超过100位的任意字符")
+    private String spare2;
+
+    /**
+     * 备用3(非必填)
+     * 预留字段,长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "备用3spare3只能传入:空 或 长度不超过100位的任意字符")
+    private String spare3;
+
+    /**
+     * 备用4(非必填)
+     * 预留字段,长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "备用4spare4只能传入:空 或 长度不超过100位的任意字符")
+    private String spare4;
+
+    /**
+     * 备用5(非必填)
+     * 预留字段,长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "备用5spare5只能传入:空 或 长度不超过100位的任意字符")
+    private String spare5;
+
+    /**
+     * 开票人(非必填)
+     * 长度1-20位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,20}$", message = "开票人drawer只能传入:空 或 长度不超过20位的任意字符")
+    private String drawer;
+
+    /**
+     * 操作员(非必填)
+     * 长度1-20位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,20}$", message = "操作员operator只能传入:空 或 长度不超过20位的任意字符")
+    private String operator;
+
+    /**
+     * 是否自动开票(非必填)
+     * N:否 Y:是,默认N
+     */
+    @Pattern(regexp = "^$|N|Y", message = "是否自动开票autoMakeInv只能传入:空 或 N 或 Y")
+    private String autoMakeInv;
+
+    /**
+     * 红字发票类型(非必填)
+     * 原蓝票为增值税纸质发票时选填,可选值:01-数电票(增值税专用发票)、02-数电票(普通发票)、
+     * 03-数电纸质发票(增值税专用发票)、04-数电纸质发票(普通发票)、83-数电票(机动车销售统一发票)、
+     * 84-数电票(二手车销售统一发票)、87-数电纸质发票(机动车销售统一发票)、88-数电纸质发票(二手车销售统一发票)
+     */
+    @Pattern(regexp = "^$|01|02|03|04|83|84|87|88",
+            message = "红字发票类型redInvType只能传入:空 或 指定枚举值")
+    private String redInvType;
+
+    /**
+     * 红字发票详情信息
+     */
+    List<OMSApplyItem> applyItems = Lists.newArrayList();
+
+    public String getDeptCode() {
+        return deptCode;
+    }
+
+    public void setDeptCode(String deptCode) {
+        this.deptCode = deptCode;
+    }
+
+    public String getApplyNo() {
+        return applyNo;
+    }
+
+    public void setApplyNo(String applyNo) {
+        this.applyNo = applyNo;
+    }
+
+    public String getApplyIdentity() {
+        return applyIdentity;
+    }
+
+    public void setApplyIdentity(String applyIdentity) {
+        this.applyIdentity = applyIdentity;
+    }
+
+    public String getSellerTaxno() {
+        return sellerTaxno;
+    }
+
+    public void setSellerTaxno(String sellerTaxno) {
+        this.sellerTaxno = sellerTaxno;
+    }
+
+    public String getSellerName() {
+        return sellerName;
+    }
+
+    public void setSellerName(String sellerName) {
+        this.sellerName = sellerName;
+    }
+
+    public String getBuyerTaxno() {
+        return buyerTaxno;
+    }
+
+    public void setBuyerTaxno(String buyerTaxno) {
+        this.buyerTaxno = buyerTaxno;
+    }
+
+    public String getBuyerName() {
+        return buyerName;
+    }
+
+    public void setBuyerName(String buyerName) {
+        this.buyerName = buyerName;
+    }
+
+    public String getOriginalInvcode() {
+        return originalInvcode;
+    }
+
+    public void setOriginalInvcode(String originalInvcode) {
+        this.originalInvcode = originalInvcode;
+    }
+
+    public String getOriginalInvno() {
+        return originalInvno;
+    }
+
+    public void setOriginalInvno(String originalInvno) {
+        this.originalInvno = originalInvno;
+    }
+
+    public String getOriginalInvType() {
+        return originalInvType;
+    }
+
+    public void setOriginalInvType(String originalInvType) {
+        this.originalInvType = originalInvType;
+    }
+
+    public String getOriginalInvAmount() {
+        return originalInvAmount;
+    }
+
+    public void setOriginalInvAmount(String originalInvAmount) {
+        this.originalInvAmount = originalInvAmount;
+    }
+
+    public String getOriginalInvTax() {
+        return originalInvTax;
+    }
+
+    public void setOriginalInvTax(String originalInvTax) {
+        this.originalInvTax = originalInvTax;
+    }
+
+    public String getOriginalInvDate() {
+        return originalInvDate;
+    }
+
+    public void setOriginalInvDate(String originalInvDate) {
+        this.originalInvDate = originalInvDate;
+    }
+
+    public String getRedReason() {
+        return redReason;
+    }
+
+    public void setRedReason(String redReason) {
+        this.redReason = redReason;
+    }
+
+    public String getTotalAmount() {
+        return totalAmount;
+    }
+
+    public void setTotalAmount(String totalAmount) {
+        this.totalAmount = totalAmount;
+    }
+
+    public String getTotalTax() {
+        return totalTax;
+    }
+
+    public void setTotalTax(String totalTax) {
+        this.totalTax = totalTax;
+    }
+
+    public String getTotalTaxamount() {
+        return totalTaxamount;
+    }
+
+    public void setTotalTaxamount(String totalTaxamount) {
+        this.totalTaxamount = totalTaxamount;
+    }
+
+    public String getOriSpecificFactor() {
+        return oriSpecificFactor;
+    }
+
+    public void setOriSpecificFactor(String oriSpecificFactor) {
+        this.oriSpecificFactor = oriSpecificFactor;
+    }
+
+    public String getSpare1() {
+        return spare1;
+    }
+
+    public void setSpare1(String spare1) {
+        this.spare1 = spare1;
+    }
+
+    public String getSpare2() {
+        return spare2;
+    }
+
+    public void setSpare2(String spare2) {
+        this.spare2 = spare2;
+    }
+
+    public String getSpare3() {
+        return spare3;
+    }
+
+    public void setSpare3(String spare3) {
+        this.spare3 = spare3;
+    }
+
+    public String getSpare4() {
+        return spare4;
+    }
+
+    public void setSpare4(String spare4) {
+        this.spare4 = spare4;
+    }
+
+    public String getSpare5() {
+        return spare5;
+    }
+
+    public void setSpare5(String spare5) {
+        this.spare5 = spare5;
+    }
+
+    public String getDrawer() {
+        return drawer;
+    }
+
+    public void setDrawer(String drawer) {
+        this.drawer = drawer;
+    }
+
+    public String getOperator() {
+        return operator;
+    }
+
+    public void setOperator(String operator) {
+        this.operator = operator;
+    }
+
+    public String getAutoMakeInv() {
+        return autoMakeInv;
+    }
+
+    public void setAutoMakeInv(String autoMakeInv) {
+        this.autoMakeInv = autoMakeInv;
+    }
+
+    public String getRedInvType() {
+        return redInvType;
+    }
+
+    public void setRedInvType(String redInvType) {
+        this.redInvType = redInvType;
+    }
+
+    public List<OMSApplyItem> getApplyItems() {
+        return applyItems;
+    }
+
+    public void setApplyItems(List<OMSApplyItem> applyItems) {
+        this.applyItems = applyItems;
+    }
+}

+ 298 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSApplyItem.java

@@ -0,0 +1,298 @@
+package com.jeeplus.assess.invoice.domain.OMS.fastRed;
+import javax.validation.constraints.Pattern;
+import java.math.BigDecimal;
+
+/**
+ * OMS发票冲红申请明细实体类
+ * 对应业务场景:发票冲红申请的商品明细参数封装
+ * 所有字段均采用 @Pattern 正则校验,规则:^$ 表示允许为空,| 分隔所有合法有效值
+ */
+public class OMSApplyItem {
+
+    /**
+     * 行号(必填)
+     * 明细行号,仅作为唯一标识使用;最终确认单中的明细顺序以请求报文中的明细顺序为准
+     * 长度8位,支持数字或字符
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,8}$", message = "行号lineCode为必填项,长度不能超过8位")
+    private String lineCode;
+
+    /**
+     * 商品名称(必填)
+     * 长度1-90位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,90}$", message = "商品名称goodsName为必填项,长度不能超过90位")
+    private String goodsName;
+
+    /**
+     * 规格型号(非必填)
+     * 原蓝票类型为机动车销售统一发票或特定要素为14、15、31时必传车架号;长度1-40位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,40}$", message = "规格型号model只能传入:空 或 长度不超过40位的任意字符(特定场景下需传车架号)")
+    private String model;
+
+    /**
+     * 计量单位(非必填)
+     * 特定要素为14、15、31时,填写“辆”;长度1-20位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,20}$", message = "计量单位unit只能传入:空 或 长度不超过20位的任意字符(特定场景下需填“辆”)")
+    private String unit;
+
+    /**
+     * 数量(非必填)
+     * 负数,最大12位整数(小数最多4位)或整数最多3位(小数最多13位);特定要素为14、15、31时填写-1
+     */
+    @Pattern(regexp = "^$|-\\d+(\\.\\d{1,13})?$", message = "数量qty只能传入:空 或 负数(格式如-123.45,整数最多12位/小数最多13位,特定场景下填-1)")
+    private BigDecimal qty;
+
+    /**
+     * 单价(非必填)
+     * 数字,最大12位整数(小数最多4位)或整数最多3位(小数最多13位)
+     */
+    @Pattern(regexp = "^$|\\d+(\\.\\d{1,13})?$", message = "单价price只能传入:空 或 数字(格式如123.45,整数最多12位/小数最多13位)")
+    private BigDecimal price;
+
+    /**
+     * 含税单价(非必填)
+     * 数字,最大12位整数(小数最多4位)或整数最多3位(小数最多13位)
+     */
+    @Pattern(regexp = "^$|\\d+(\\.\\d{1,13})?$", message = "含税单价taxprice只能传入:空 或 数字(格式如123.45,整数最多12位/小数最多13位)")
+    private BigDecimal taxprice;
+
+    /**
+     * 金额(不含税)(必填)
+     * 负数,保留两位小数
+     */
+    @Pattern(regexp = "^-\\d+(\\.\\d{2})$", message = "金额(不含税)amount为必填项,只能传入负数且保留两位小数(如-123.45)")
+    private BigDecimal amount;
+
+    /**
+     * 税率(必填)
+     * 最多3位小数,格式如0.13、13.00
+     */
+    @Pattern(regexp = "^\\d+(\\.\\d{1,3})$", message = "税率taxrate为必填项,最多保留3位小数(如0.13、13.00)")
+    private BigDecimal taxrate;
+
+    /**
+     * 税额(必填)
+     * 负数,保留两位小数
+     */
+    @Pattern(regexp = "^-\\d+(\\.\\d{2})$", message = "税额tax为必填项,只能传入负数且保留两位小数(如-12.34)")
+    private BigDecimal tax;
+
+    /**
+     * 含税金额(必填)
+     * 负数,保留两位小数
+     */
+    @Pattern(regexp = "^-\\d+(\\.\\d{2})$", message = "含税金额taxamount为必填项,只能传入负数且保留两位小数(如-135.79)")
+    private BigDecimal taxamount;
+
+    /**
+     * 优惠政策标识(非必填)
+     * 可选值:01-简易征收;02-稀土产品;03-免税;04-不征税;05-先征后退;06-100%先征后退;07-50%先征后退;08-按3%简易征收;09-按5%简易征收;10-按5%简易征收减按1.5%计征;11-即征即退30%;12-即征即退50%;13-即征即退70%;14-即征即退100%;15-超税负3%即征即退;16-超税负8%即征即退;17-超税负12%即征即退;18-超税负6%即征即退
+     */
+    @Pattern(regexp = "^$|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18",
+            message = "优惠政策标识taxpre只能传入:空 或 指定枚举值(如01-简易征收)")
+    private String taxpre;
+
+    /**
+     * 税收分类编码(必填)
+     * 长度1-19位,纯数字
+     */
+    @Pattern(regexp = "^\\d{1,19}$", message = "税收分类编码goodstaxno为必填项,只能传入1-19位纯数字")
+    private String goodstaxno;
+
+    /**
+     * 对应蓝票明细行序号(必填)
+     * 长度8位,纯数字
+     */
+    @Pattern(regexp = "^\\d{8}$", message = "对应蓝票明细行序号oriLineCode为必填项,只能传入8位纯数字")
+    private String oriLineCode;
+
+    /**
+     * 明细备用1(非必填)
+     * 预留字段,长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "明细备用1itemspare1只能传入:空 或 长度不超过100位的任意字符")
+    private String itemspare1;
+
+    /**
+     * 明细备用2(非必填)
+     * 预留字段,长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "明细备用2itemspare2只能传入:空 或 长度不超过100位的任意字符")
+    private String itemspare2;
+
+    /**
+     * 明细备用3(非必填)
+     * 预留字段,长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "明细备用3itemspare3只能传入:空 或 长度不超过100位的任意字符")
+    private String itemspare3;
+
+    /**
+     * 明细备用4(非必填)
+     * 预留字段,长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "明细备用4itemspare4只能传入:空 或 长度不超过100位的任意字符")
+    private String itemspare4;
+
+    /**
+     * 明细备用5(非必填)
+     * 预留字段,长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "明细备用5itemspare5只能传入:空 或 长度不超过100位的任意字符")
+    private String itemspare5;
+
+    public String getLineCode() {
+        return lineCode;
+    }
+
+    public void setLineCode(String lineCode) {
+        this.lineCode = lineCode;
+    }
+
+    public String getGoodsName() {
+        return goodsName;
+    }
+
+    public void setGoodsName(String goodsName) {
+        this.goodsName = goodsName;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public void setUnit(String unit) {
+        this.unit = unit;
+    }
+
+    public BigDecimal getQty() {
+        return qty;
+    }
+
+    public void setQty(BigDecimal qty) {
+        this.qty = qty;
+    }
+
+    public BigDecimal getPrice() {
+        return price;
+    }
+
+    public void setPrice(BigDecimal price) {
+        this.price = price;
+    }
+
+    public BigDecimal getTaxprice() {
+        return taxprice;
+    }
+
+    public void setTaxprice(BigDecimal taxprice) {
+        this.taxprice = taxprice;
+    }
+
+    public BigDecimal getAmount() {
+        return amount;
+    }
+
+    public void setAmount(BigDecimal amount) {
+        this.amount = amount;
+    }
+
+    public BigDecimal getTaxrate() {
+        return taxrate;
+    }
+
+    public void setTaxrate(BigDecimal taxrate) {
+        this.taxrate = taxrate;
+    }
+
+    public BigDecimal getTax() {
+        return tax;
+    }
+
+    public void setTax(BigDecimal tax) {
+        this.tax = tax;
+    }
+
+    public BigDecimal getTaxamount() {
+        return taxamount;
+    }
+
+    public void setTaxamount(BigDecimal taxamount) {
+        this.taxamount = taxamount;
+    }
+
+    public String getTaxpre() {
+        return taxpre;
+    }
+
+    public void setTaxpre(String taxpre) {
+        this.taxpre = taxpre;
+    }
+
+    public String getGoodstaxno() {
+        return goodstaxno;
+    }
+
+    public void setGoodstaxno(String goodstaxno) {
+        this.goodstaxno = goodstaxno;
+    }
+
+    public String getOriLineCode() {
+        return oriLineCode;
+    }
+
+    public void setOriLineCode(String oriLineCode) {
+        this.oriLineCode = oriLineCode;
+    }
+
+    public String getItemspare1() {
+        return itemspare1;
+    }
+
+    public void setItemspare1(String itemspare1) {
+        this.itemspare1 = itemspare1;
+    }
+
+    public String getItemspare2() {
+        return itemspare2;
+    }
+
+    public void setItemspare2(String itemspare2) {
+        this.itemspare2 = itemspare2;
+    }
+
+    public String getItemspare3() {
+        return itemspare3;
+    }
+
+    public void setItemspare3(String itemspare3) {
+        this.itemspare3 = itemspare3;
+    }
+
+    public String getItemspare4() {
+        return itemspare4;
+    }
+
+    public void setItemspare4(String itemspare4) {
+        this.itemspare4 = itemspare4;
+    }
+
+    public String getItemspare5() {
+        return itemspare5;
+    }
+
+    public void setItemspare5(String itemspare5) {
+        this.itemspare5 = itemspare5;
+    }
+}

+ 131 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSFastRedInvoiceInfo.java

@@ -0,0 +1,131 @@
+package com.jeeplus.assess.invoice.domain.OMS.fastRed;
+import javax.validation.constraints.Pattern;
+
+/**
+ * OMS快捷红票信息实体类
+ * 对应业务场景:发票冲红操作的参数封装
+ * 所有字段均采用 @Pattern 正则校验,规则:^$ 表示允许为空,| 分隔所有合法有效值
+ */
+public class OMSFastRedInvoiceInfo {
+
+    /**
+     * 组织代码(必填)
+     * 规则:非空 + 最大长度20位,支持任意字符
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,20}$", message = "组织代码deptCode为必填项,且长度不能超过20位")
+    private String deptCode;
+
+    /**
+     * 发票代码
+     * 规则:可为空,若传值则为12位纯数字
+     */
+    @Pattern(regexp = "^$|^\\d{12}$", message = "发票代码invcode只能传入:空 或 12位纯数字")
+    private String invcode;
+
+    /**
+     * 发票号码
+     * 规则:可为空,若传值则为8位纯数字
+     */
+    @Pattern(regexp = "^$|^\\d{8}$", message = "发票号码invno只能传入:空 或 8位纯数字")
+    private String invno;
+
+    /**
+     * 数电发票号码
+     * 规则:可为空,若传值则为20位纯数字
+     */
+    @Pattern(regexp = "^$|^\\d{20}$", message = "数电发票号码allEinvno只能传入:空 或 20位纯数字")
+    private String allEinvno;
+
+    /**
+     * 冲红原因
+     * 规则:可为空,若传值只能是 01/02/03/04
+     * 可选值:01-开票有误,02-销货退回,03-服务中止,04-销售折让
+     */
+    @Pattern(regexp = "^$|01|02|03|04", message = "冲红原因redReason只能传入:空 或 01 或 02 或 03 或 04")
+    private String redReason;
+
+    /**
+     * 红字确认信息单编号
+     * 规则:可为空,若传值则为32位纯数字
+     */
+    @Pattern(regexp = "^$|^\\d{32}$", message = "红字确认信息单编号redNo只能传入:空 或 32位纯数字")
+    private String redNo;
+
+    /**
+     * 红字发票类型
+     * 规则:可为空,若传值为指定枚举值
+     */
+    @Pattern(regexp = "^$|01|02|03|04|83|84|87|88", message = "红字发票类型redInvType只能传入:空 或 01 或 02 或 03 或 04 或 83 或 84 或 87 或 88")
+    private String redInvType;
+
+    /**
+     * 开票员
+     * 规则:可为空,若传值则最大长度20位,支持任意字符
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,20}$", message = "开票员drawer只能传入:空 或 长度不超过20位的任意字符")
+    private String drawer;
+
+    public String getDeptCode() {
+        return deptCode;
+    }
+
+    public void setDeptCode(String deptCode) {
+        this.deptCode = deptCode;
+    }
+
+    public String getInvcode() {
+        return invcode;
+    }
+
+    public void setInvcode(String invcode) {
+        this.invcode = invcode;
+    }
+
+    public String getInvno() {
+        return invno;
+    }
+
+    public void setInvno(String invno) {
+        this.invno = invno;
+    }
+
+    public String getAllEinvno() {
+        return allEinvno;
+    }
+
+    public void setAllEinvno(String allEinvno) {
+        this.allEinvno = allEinvno;
+    }
+
+    public String getRedReason() {
+        return redReason;
+    }
+
+    public void setRedReason(String redReason) {
+        this.redReason = redReason;
+    }
+
+    public String getRedNo() {
+        return redNo;
+    }
+
+    public void setRedNo(String redNo) {
+        this.redNo = redNo;
+    }
+
+    public String getRedInvType() {
+        return redInvType;
+    }
+
+    public void setRedInvType(String redInvType) {
+        this.redInvType = redInvType;
+    }
+
+    public String getDrawer() {
+        return drawer;
+    }
+
+    public void setDrawer(String drawer) {
+        this.drawer = drawer;
+    }
+}

+ 254 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSRedInvoiceConfirmApplyItem.java

@@ -0,0 +1,254 @@
+package com.jeeplus.assess.invoice.domain.OMS.fastRed;
+import javax.validation.constraints.Pattern;
+
+/**
+ * OMS红字确认单返回接口的商品明细子类
+ * 对应业务场景:红字确认单返回数据中的商品明细信息
+ * 所有字段均采用 @Pattern 正则校验,规则:^$ 表示允许为空,| 分隔所有合法有效值
+ */
+public class OMSRedInvoiceConfirmApplyItem {
+
+    /**
+     * 商品名称(必填)
+     * 长度1-90位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,90}$", message = "商品名称goodsName为必填项,长度不能超过90位")
+    private String goodsName;
+
+    /**
+     * 规格型号(非必填)
+     * 特定要素为14、15、31时,此字段为车辆识别代号/车架号码;长度1-40位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,40}$", message = "规格型号model只能传入:空 或 长度不超过40位的任意字符(特定场景下为车架号)")
+    private String model;
+
+    /**
+     * 计量单位(非必填)
+     * 长度1-20位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,20}$", message = "计量单位unit只能传入:空 或 长度不超过20位的任意字符")
+    private String unit;
+
+    /**
+     * 数量(非必填)
+     * 整数最多20位,小数最多10位;冲红场景下可为负数
+     */
+    @Pattern(regexp = "^$|^-?\\d+(\\.\\d{1,10})?$", message = "数量qty只能传入:空 或 整数最多20位、小数最多10位的数字(可正可负)")
+    private String qty;
+
+    /**
+     * 单价(非必填)
+     * 整数最多20位,小数最多10位
+     */
+    @Pattern(regexp = "^$|^\\d+(\\.\\d{1,10})?$", message = "单价price只能传入:空 或 整数最多20位、小数最多10位的数字")
+    private String price;
+
+    /**
+     * 金额(不含税)(必填)
+     * 负数,保留两位小数
+     */
+    @Pattern(regexp = "^-\\d+(\\.\\d{2})$", message = "金额(不含税)amount为必填项,只能传入负数且保留两位小数(如-123.45)")
+    private String amount;
+
+    /**
+     * 税率(必填)
+     * 整数最多16位,小数最多3位
+     */
+    @Pattern(regexp = "^\\d+(\\.\\d{1,3})$", message = "税率taxrate为必填项,最多保留3位小数(如0.13、13.00)")
+    private String taxrate;
+
+    /**
+     * 税额(必填)
+     * 负数,保留两位小数
+     */
+    @Pattern(regexp = "^-\\d+(\\.\\d{2})$", message = "税额tax为必填项,只能传入负数且保留两位小数(如-12.34)")
+    private String tax;
+
+    /**
+     * 优惠政策标识(非必填)
+     * 可选值:01-简易征收;02-稀土产品;03-免税;04-不征税;05-先征后退;06-100%先征后退;07-50%先征后退;
+     * 08-按3%简易征收;09-按5%简易征收;10-按5%简易征收减按1.5%计征;11-即征即退30%;12-即征即退50%;
+     * 13-即征即退70%;14-即征即退100%;15-超税负3%即征即退;16-超税负8%即征即退;17-超税负12%即征即退;18-超税负6%即征即退
+     */
+    @Pattern(regexp = "^$|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18",
+            message = "优惠政策标识taxpre只能传入:空 或 指定枚举值(如01-简易征收)")
+    private String taxpre;
+
+    /**
+     * 税收分类编码(必填)
+     * 长度1-19位,纯数字
+     */
+    @Pattern(regexp = "^\\d{1,19}$", message = "税收分类编码goodstaxno为必填项,只能传入1-19位纯数字")
+    private String goodstaxno;
+
+    /**
+     * 税收分类编码简称(非必填)
+     * 长度1-100位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,100}$", message = "税收分类编码简称codeAbb只能传入:空 或 长度不超过100位的任意字符")
+    private String codeAbb;
+
+    /**
+     * 项目名称(非必填)
+     * 不定长,最多1000位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,1000}$", message = "项目名称detailGoodsName只能传入:空 或 长度不超过1000位的任意字符")
+    private String detailGoodsName;
+
+    /**
+     * 行号(非必填)
+     * 长度1-8位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,8}$", message = "行号lineCode只能传入:空 或 长度不超过8位的任意字符")
+    private String lineCode;
+
+    /**
+     * 对应蓝字发票明细行号(非必填)
+     * 长度8位,纯数字
+     */
+    @Pattern(regexp = "^$|^\\d{8}$", message = "对应蓝字发票明细行号oriLineCode只能传入:空 或 8位纯数字")
+    private String oriLineCode;
+
+    /**
+     * 原蓝票开票日期(非必填)
+     * 格式:yyyy-mm-dd hh:mm:ss
+     */
+    @Pattern(regexp = "^$|^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$",
+            message = "原蓝票开票日期originalInvDate只能传入:空 或 格式为yyyy-mm-dd hh:mm:ss的日期")
+    private String originalInvDate;
+
+    /**
+     * 开票失败原因(非必填)
+     * 不定长,最多1000位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,1000}$", message = "开票失败原因failCause只能传入:空 或 长度不超过1000位的任意字符")
+    private String failCause;
+
+    public String getGoodsName() {
+        return goodsName;
+    }
+
+    public void setGoodsName(String goodsName) {
+        this.goodsName = goodsName;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public void setUnit(String unit) {
+        this.unit = unit;
+    }
+
+    public String getQty() {
+        return qty;
+    }
+
+    public void setQty(String qty) {
+        this.qty = qty;
+    }
+
+    public String getPrice() {
+        return price;
+    }
+
+    public void setPrice(String price) {
+        this.price = price;
+    }
+
+    public String getAmount() {
+        return amount;
+    }
+
+    public void setAmount(String amount) {
+        this.amount = amount;
+    }
+
+    public String getTaxrate() {
+        return taxrate;
+    }
+
+    public void setTaxrate(String taxrate) {
+        this.taxrate = taxrate;
+    }
+
+    public String getTax() {
+        return tax;
+    }
+
+    public void setTax(String tax) {
+        this.tax = tax;
+    }
+
+    public String getTaxpre() {
+        return taxpre;
+    }
+
+    public void setTaxpre(String taxpre) {
+        this.taxpre = taxpre;
+    }
+
+    public String getGoodstaxno() {
+        return goodstaxno;
+    }
+
+    public void setGoodstaxno(String goodstaxno) {
+        this.goodstaxno = goodstaxno;
+    }
+
+    public String getCodeAbb() {
+        return codeAbb;
+    }
+
+    public void setCodeAbb(String codeAbb) {
+        this.codeAbb = codeAbb;
+    }
+
+    public String getDetailGoodsName() {
+        return detailGoodsName;
+    }
+
+    public void setDetailGoodsName(String detailGoodsName) {
+        this.detailGoodsName = detailGoodsName;
+    }
+
+    public String getLineCode() {
+        return lineCode;
+    }
+
+    public void setLineCode(String lineCode) {
+        this.lineCode = lineCode;
+    }
+
+    public String getOriLineCode() {
+        return oriLineCode;
+    }
+
+    public void setOriLineCode(String oriLineCode) {
+        this.oriLineCode = oriLineCode;
+    }
+
+    public String getOriginalInvDate() {
+        return originalInvDate;
+    }
+
+    public void setOriginalInvDate(String originalInvDate) {
+        this.originalInvDate = originalInvDate;
+    }
+
+    public String getFailCause() {
+        return failCause;
+    }
+
+    public void setFailCause(String failCause) {
+        this.failCause = failCause;
+    }
+}

+ 52 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSRedInvoiceConfirmInfo.java

@@ -0,0 +1,52 @@
+package com.jeeplus.assess.invoice.domain.OMS.fastRed;
+import javax.validation.constraints.Pattern;
+
+/**
+ * OMS红字确认单信息实体类
+ * 对应业务场景:红字确认单的状态处理与信息封装
+ * 所有字段均采用 @Pattern 正则校验,规则:^$ 表示允许为空,| 分隔所有合法有效值
+ */
+public class OMSRedInvoiceConfirmInfo {
+
+    /**
+     * 组织代码(必填)
+     * 所属组织唯一标识,长度1-20位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,20}$", message = "组织代码deptCode为必填项,长度不能超过20位")
+    private String deptCode;
+
+    /**
+     * 红字确认单uuid(必填)
+     * 申请成功后返回的流水号,长度1-32位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,32}$", message = "红字确认单uuid redUuid为必填项,长度不能超过32位")
+    private String redUuid;
+
+    /**
+     * 红字确认信息表状态(必填)
+     * 01:同意 02:不同意 03:撤销
+     */
+    @Pattern(regexp = "01|02|03", message = "红字确认信息表状态confirmStatus为必填项,只能传入01(同意)、02(不同意)、03(撤销)")
+    private String confirmStatus;
+
+    /**
+     * 处理理由(非必填)
+     * 长度1-200位,填写处理的原因说明
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,200}$", message = "处理理由confirmReason只能传入:空 或 长度不超过200位的任意字符")
+    private String confirmReason;
+
+    /**
+     * 请求方身份(必填)
+     * 0:销方 1:购方
+     */
+    @Pattern(regexp = "0|1", message = "请求方身份applyIdentity为必填项,只能传入0(销方)或1(购方)")
+    private String applyIdentity;
+
+    /**
+     * 开票员(非必填)
+     * 长度1-20位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,20}$", message = "开票员drawer只能传入:空 或 长度不超过20位的任意字符")
+    private String drawer;
+}

+ 86 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSRedInvoiceConfirmQueryRequest.java

@@ -0,0 +1,86 @@
+package com.jeeplus.assess.invoice.domain.OMS.fastRed;
+
+import javax.validation.constraints.Pattern;
+
+/**
+ * OMS红字确认单查询接口请求实体类
+ * 对应业务场景:业务系统从OMS系统查询申请的红字确认单数据
+ * 所有字段均采用 @Pattern 正则校验,规则:^$ 表示允许为空,| 分隔所有合法有效值
+ */
+public class OMSRedInvoiceConfirmQueryRequest {
+
+    /**
+     * 组织代码(必填)
+     * 所属组织唯一标识,长度1-20位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,20}$", message = "组织代码deptCode为必填项,长度不能超过20位")
+    private String deptCode;
+
+    /**
+     * 申请单号(非必填)
+     * 申请单号、红字确认单id、红字确认信息单编号不可同时为空;长度1-50位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,50}$", message = "申请单号applyNo只能传入:空 或 长度不超过50位的任意字符(需与redUuid/redNo至少一个同时传值)")
+    private String applyNo;
+
+    /**
+     * 红字确认单id(非必填)
+     * 申请单号、红字确认单id、红字确认信息单编号不可同时为空;长度1-32位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,32}$", message = "红字确认单id redUuid只能传入:空 或 长度不超过32位的任意字符(需与applyNo/redNo至少一个同时传值)")
+    private String redUuid;
+
+    /**
+     * 红字确认信息单编号(非必填)
+     * 申请单号、红字确认单id、红字确认信息单编号不可同时为空;长度1-32位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,32}$", message = "红字确认信息单编号redNo只能传入:空 或 长度不超过32位的任意字符(需与applyNo/redUuid至少一个同时传值)")
+    private String redNo;
+
+    /**
+     * 请求方身份(必填)
+     * 0:销方 1:购方
+     */
+    @Pattern(regexp = "0|1", message = "请求方身份applyIdentity为必填项,只能传入0(销方)或1(购方)")
+    private String applyIdentity;
+
+    public String getDeptCode() {
+        return deptCode;
+    }
+
+    public void setDeptCode(String deptCode) {
+        this.deptCode = deptCode;
+    }
+
+    public String getApplyNo() {
+        return applyNo;
+    }
+
+    public void setApplyNo(String applyNo) {
+        this.applyNo = applyNo;
+    }
+
+    public String getRedUuid() {
+        return redUuid;
+    }
+
+    public void setRedUuid(String redUuid) {
+        this.redUuid = redUuid;
+    }
+
+    public String getRedNo() {
+        return redNo;
+    }
+
+    public void setRedNo(String redNo) {
+        this.redNo = redNo;
+    }
+
+    public String getApplyIdentity() {
+        return applyIdentity;
+    }
+
+    public void setApplyIdentity(String applyIdentity) {
+        this.applyIdentity = applyIdentity;
+    }
+}

+ 356 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/domain/OMS/fastRed/OMSRedInvoiceConfirmResponse.java

@@ -0,0 +1,356 @@
+package com.jeeplus.assess.invoice.domain.OMS.fastRed;
+
+import javax.validation.constraints.Pattern;
+import java.util.List;
+
+/**
+ * OMS红字确认单返回接口信息实体类
+ * 对应业务场景:红字确认单查询接口的返回数据封装
+ * 所有字段均采用 @Pattern 正则校验,规则:^$ 表示允许为空,| 分隔所有合法有效值
+ */
+public class OMSRedInvoiceConfirmResponse {
+
+    /**
+     * 红字确认信息单编号(非必填)
+     * 长度1-32位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,32}$", message = "红字确认信息单编号redNo只能传入:空 或 长度不超过32位的任意字符")
+    private String redNo;
+
+    /**
+     * 红字确认单Id(非必填)
+     * 长度1-32位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,32}$", message = "红字确认单Id redUuid只能传入:空 或 长度不超过32位的任意字符")
+    private String redUuid;
+
+    /**
+     * 申请方身份(必填)
+     * 0:销方 1:购方
+     */
+    @Pattern(regexp = "0|1", message = "申请方身份applyIdentity为必填项,只能传入0(销方)或1(购方)")
+    private String applyIdentity;
+
+    /**
+     * 销售方纳税人识别号(必填)
+     * 长度1-20位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,20}$", message = "销售方纳税人识别号sellerTaxNo为必填项,长度不能超过20位")
+    private String sellerTaxNo;
+
+    /**
+     * 销售方名称(必填)
+     * 长度1-100位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,100}$", message = "销售方名称sellerName为必填项,长度不能超过100位")
+    private String sellerName;
+
+    /**
+     * 购买方纳税人识别号(非必填)
+     * 长度1-20位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,20}$", message = "购买方纳税人识别号buyerTaxno只能传入:空 或 长度不超过20位的任意字符")
+    private String buyerTaxno;
+
+    /**
+     * 购买方名称(必填)
+     * 长度1-100位
+     */
+    @Pattern(regexp = "^[\\s\\S]{1,100}$", message = "购买方名称buyerName为必填项,长度不能超过100位")
+    private String buyerName;
+
+    /**
+     * 冲红原因(必填)
+     * 01:开票有误 02:销货退回 03:服务中止 04:销售折让
+     */
+    @Pattern(regexp = "01|02|03|04", message = "冲红原因redReason为必填项,只能传入01/02/03/04")
+    private String redReason;
+
+    /**
+     * 红字确认单状态(必填)
+     * 01-无需确认、02-销方录入待购方确认、03-购方录入待销方确认、04-购销双方已确认
+     * 05-作废(销方录入购方否认)、06-作废(购方录入销方否认)、07-作废(超72小时未确认)
+     * 08-作废(发起方已撤销)、09-作废(确认后撤销)、10-作废(异常凭证)、15-申请中、16-申请失败
+     */
+    @Pattern(regexp = "01|02|03|04|05|06|07|08|09|10|15|16",
+            message = "红字确认单状态confirmStatus为必填项,只能传入指定枚举值")
+    private String confirmStatus;
+
+    /**
+     * 红字发票开具状态(必填)
+     * 1:未开票 2:开票失败 3:已开票 4:开票中
+     */
+    @Pattern(regexp = "1|2|3|4", message = "红字发票开具状态makeStatus为必填项,只能传入1/2/3/4")
+    private String makeStatus;
+
+    /**
+     * 合计金额(不含税)(必填)
+     * 负数,保留两位小数
+     */
+    @Pattern(regexp = "^-\\d+(\\.\\d{2})$", message = "合计金额(不含税)totalAmount为必填项,只能传入负数且保留两位小数(如-123.45)")
+    private String totalAmount;
+
+    /**
+     * 合计税额(必填)
+     * 负数,保留两位小数
+     */
+    @Pattern(regexp = "^-\\d+(\\.\\d{2})$", message = "合计税额totalTax为必填项,只能传入负数且保留两位小数(如-12.34)")
+    private String totalTax;
+
+    /**
+     * 原蓝票发票类型(必填)
+     * 01-数电票(增值税专用发票)、02-数电票(普通发票)、03-数电纸质发票(增值税专用发票)
+     * 04-数电纸质发票(普通发票)、83-数电票(机动车销售统一发票)、84-数电票(二手车销售统一发票)
+     * 87-数电纸质发票(机动车销售统一发票)、88-数电纸质发票(二手车销售统一发票)
+     * 05-增值税电子普通发票、06-增值税普通发票、07-增值税专用发票、08-增值税电子专用发票
+     * 09-机动车统一销售发票、15-二手车统一销售发票
+     */
+    @Pattern(regexp = "01|02|03|04|83|84|87|88|05|06|07|08|09|15",
+            message = "原蓝票发票类型originalInvType为必填项,只能传入指定枚举值")
+    private String originalInvType;
+
+    /**
+     * 原蓝票发票代码(非必填)
+     * 12位或10位纯数字
+     */
+    @Pattern(regexp = "^$|^\\d{10}$|^\\d{12}$", message = "原蓝票发票代码originalInvcode只能传入:空 或 10位/12位纯数字")
+    private String originalInvcode;
+
+    /**
+     * 原蓝票发票号码(非必填)
+     * 20位或8位纯数字
+     */
+    @Pattern(regexp = "^$|^\\d{8}$|^\\d{20}$", message = "原蓝票发票号码originalInvno只能传入:空 或 8位/20位纯数字")
+    private String originalInvno;
+
+    /**
+     * 原蓝票开票日期(非必填)
+     * 格式:yyyy-MM-dd HH:mm:ss
+     */
+    @Pattern(regexp = "^$|^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$",
+            message = "原蓝票开票日期originalInvDate只能传入:空 或 格式为yyyy-MM-dd HH:mm:ss的日期")
+    private String originalInvDate;
+
+    /**
+     * 原蓝票特定要素(非必填)
+     * 01-成品油发票、02-稀土发票、03-建筑服务发票、04-货物运输服务发票、05-不动产销售服务发票
+     * 06-不动产租赁服务发票、07-代收车船税、08-通行费、09-旅客运输服务发票、10-医疗服务(住院)发票
+     * 11-医疗服务(门诊)发票、12-自产农产品销售发票、13-拖拉机和联合收割机发票、14-机动车
+     * 15-二手车、16-农产品收购发票、17-光伏收购发票、18-卷烟发票、31-二手车*、32-电子烟
+     * 51-二手车正向开具、52-二手车反向开具
+     */
+    @Pattern(regexp = "^$|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|31|32|51|52",
+            message = "原蓝票特定要素oriSpecificFactor只能传入:空 或 指定枚举值")
+    private String oriSpecificFactor;
+
+    /**
+     * 红字发票订单号(非必填)
+     * 当请求方身份为0(销方)且红字确认单已开票时有值;长度1-50位
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,50}$", message = "红字发票订单号redInvOrderNo只能传入:空 或 长度不超过50位的任意字符")
+    private String redInvOrderNo;
+
+    /**
+     * 开票失败原因(非必填)
+     * 不定长,填写开票失败的具体原因
+     */
+    @Pattern(regexp = "^$|^[\\s\\S]{1,1000}$", message = "开票失败原因failCause只能传入:空 或 长度不超过1000位的任意字符")
+    private String failCause;
+
+    /**
+     * 红字确认单申请时间(非必填)
+     * 格式:yyyy-MM-dd HH:mm:ss
+     */
+    @Pattern(regexp = "^$|^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$",
+            message = "红字确认单申请时间applyTime只能传入:空 或 格式为yyyy-MM-dd HH:mm:ss的日期")
+    private String applyTime;
+
+    /**
+     * 确认时间(非必填)
+     * 格式:yyyy-MM-dd HH:mm:ss
+     */
+    @Pattern(regexp = "^$|^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$",
+            message = "确认时间confirmTime只能传入:空 或 格式为yyyy-MM-dd HH:mm:ss的日期")
+    private String confirmTime;
+
+    /**
+     * 商品类
+     */
+    private List<OMSRedInvoiceConfirmApplyItem> applyItems;
+
+    public String getRedNo() {
+        return redNo;
+    }
+
+    public void setRedNo(String redNo) {
+        this.redNo = redNo;
+    }
+
+    public String getRedUuid() {
+        return redUuid;
+    }
+
+    public void setRedUuid(String redUuid) {
+        this.redUuid = redUuid;
+    }
+
+    public String getApplyIdentity() {
+        return applyIdentity;
+    }
+
+    public void setApplyIdentity(String applyIdentity) {
+        this.applyIdentity = applyIdentity;
+    }
+
+    public String getSellerTaxNo() {
+        return sellerTaxNo;
+    }
+
+    public void setSellerTaxNo(String sellerTaxNo) {
+        this.sellerTaxNo = sellerTaxNo;
+    }
+
+    public String getSellerName() {
+        return sellerName;
+    }
+
+    public void setSellerName(String sellerName) {
+        this.sellerName = sellerName;
+    }
+
+    public String getBuyerTaxno() {
+        return buyerTaxno;
+    }
+
+    public void setBuyerTaxno(String buyerTaxno) {
+        this.buyerTaxno = buyerTaxno;
+    }
+
+    public String getBuyerName() {
+        return buyerName;
+    }
+
+    public void setBuyerName(String buyerName) {
+        this.buyerName = buyerName;
+    }
+
+    public String getRedReason() {
+        return redReason;
+    }
+
+    public void setRedReason(String redReason) {
+        this.redReason = redReason;
+    }
+
+    public String getConfirmStatus() {
+        return confirmStatus;
+    }
+
+    public void setConfirmStatus(String confirmStatus) {
+        this.confirmStatus = confirmStatus;
+    }
+
+    public String getMakeStatus() {
+        return makeStatus;
+    }
+
+    public void setMakeStatus(String makeStatus) {
+        this.makeStatus = makeStatus;
+    }
+
+    public String getTotalAmount() {
+        return totalAmount;
+    }
+
+    public void setTotalAmount(String totalAmount) {
+        this.totalAmount = totalAmount;
+    }
+
+    public String getTotalTax() {
+        return totalTax;
+    }
+
+    public void setTotalTax(String totalTax) {
+        this.totalTax = totalTax;
+    }
+
+    public String getOriginalInvType() {
+        return originalInvType;
+    }
+
+    public void setOriginalInvType(String originalInvType) {
+        this.originalInvType = originalInvType;
+    }
+
+    public String getOriginalInvcode() {
+        return originalInvcode;
+    }
+
+    public void setOriginalInvcode(String originalInvcode) {
+        this.originalInvcode = originalInvcode;
+    }
+
+    public String getOriginalInvno() {
+        return originalInvno;
+    }
+
+    public void setOriginalInvno(String originalInvno) {
+        this.originalInvno = originalInvno;
+    }
+
+    public String getOriginalInvDate() {
+        return originalInvDate;
+    }
+
+    public void setOriginalInvDate(String originalInvDate) {
+        this.originalInvDate = originalInvDate;
+    }
+
+    public String getOriSpecificFactor() {
+        return oriSpecificFactor;
+    }
+
+    public void setOriSpecificFactor(String oriSpecificFactor) {
+        this.oriSpecificFactor = oriSpecificFactor;
+    }
+
+    public String getRedInvOrderNo() {
+        return redInvOrderNo;
+    }
+
+    public void setRedInvOrderNo(String redInvOrderNo) {
+        this.redInvOrderNo = redInvOrderNo;
+    }
+
+    public String getFailCause() {
+        return failCause;
+    }
+
+    public void setFailCause(String failCause) {
+        this.failCause = failCause;
+    }
+
+    public String getApplyTime() {
+        return applyTime;
+    }
+
+    public void setApplyTime(String applyTime) {
+        this.applyTime = applyTime;
+    }
+
+    public String getConfirmTime() {
+        return confirmTime;
+    }
+
+    public void setConfirmTime(String confirmTime) {
+        this.confirmTime = confirmTime;
+    }
+
+    public List<OMSRedInvoiceConfirmApplyItem> getApplyItems() {
+        return applyItems;
+    }
+
+    public void setApplyItems(List<OMSRedInvoiceConfirmApplyItem> applyItems) {
+        this.applyItems = applyItems;
+    }
+}

+ 5 - 1
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/mapper/FinanceInvoiceDetailMapper.java

@@ -9,7 +9,11 @@ import org.apache.ibatis.annotations.Mapper;
  */
 @Mapper
 public interface FinanceInvoiceDetailMapper extends BaseMapper<FinanceInvoiceDetail> {
-
+    /**
+     * 根据开票id修改number值
+     * @param invoice
+     */
+    void updateNumberById(FinanceInvoiceDetail invoice);
 }
 
 

+ 46 - 4
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/mapper/FinanceInvoiceMapper.java

@@ -1,22 +1,20 @@
 package com.jeeplus.assess.invoice.mapper;
 
-import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.Constants;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.jeeplus.assess.invoice.domain.FinanceInvoice;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jeeplus.assess.invoice.domain.FinanceInvoiceTaxClassificationCode;
 import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
-import com.jeeplus.assess.program.configuration.fileDict.domain.ProgramFileDict;
-import com.jeeplus.assess.program.configuration.fileDict.service.dto.ProgramFileDictDTO;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
 
 /**
- * @Entity com.jeeplus.test.finance.invoice.domain.FinanceInvoice
+ * @Entity
  */
 @Mapper
 //@InterceptorIgnore(tenantLine = "true")
@@ -37,6 +35,50 @@ public interface FinanceInvoiceMapper extends BaseMapper<FinanceInvoice> {
     List<FinanceInvoiceDTO> getExceedFinance();
 
     String getInvoiceNumberStr(String id);
+
+
+
+    /**
+     * 修改开票获取accesstoken失败结果
+     * @param workInvoice
+     * @return
+     */
+    Integer updateAccessTokenErrorById(FinanceInvoiceDTO workInvoice);
+
+    /**
+     * 修改开票获取的url以及发票号结果
+     * @param workInvoice
+     * @return
+     */
+    Integer updateOmsUrlById(FinanceInvoiceDTO workInvoice);
+
+    /**
+     * 修改订单提交接口返回值信息
+     * @param workInvoice
+     * @return
+     */
+    Integer updateOrderForGoodsResultStrById(FinanceInvoiceDTO workInvoice);
+
+    /**
+     * 修改红字确认单返回结果信息
+     * @param workInvoice
+     * @return
+     */
+    Integer updateConfirmationSlipResultStr(FinanceInvoiceDTO workInvoice);
+
+    /**
+     * 查询税收分类对应信息
+     * @param workInvoice
+     * @return
+     */
+    FinanceInvoiceTaxClassificationCode getBillingContentDetail(FinanceInvoiceDTO workInvoice);
+
+    /**
+     * 修改开票信息对应个节点redis保存的数据
+     * @param workInvoice
+     * @return
+     */
+    Integer updateRedInvoiceJsonByWorkInvoiceId(FinanceInvoiceDTO workInvoice);
 }
 
 

+ 6 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/mapper/xml/FinanceInvoiceDetailMapper.xml

@@ -39,4 +39,10 @@
         fid.tax,
         fid.all_amount
     </sql>
+
+    <update id="updateNumberById">
+        UPDATE finance_invoice_detail SET
+            number = #{number}
+        WHERE invoice_id = #{invoiceId} and del_flag = 0
+    </update>
 </mapper>

+ 60 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/mapper/xml/FinanceInvoiceMapper.xml

@@ -433,4 +433,64 @@
     <select id="getInvoiceNumberStr" resultType="java.lang.String">
 		select group_concat(number) from finance_invoice_detail where invoice_id = #{invoiceId}
 	</select>
+
+
+    <update id="updateAccessTokenErrorById">
+        UPDATE finance_invoice SET
+                                      oms_access_token_error = #{omsAccessTokenError},
+                                      oms_access_token = #{omsAccessToken},
+                                      oms_error_message = #{omsErrorMessage},
+                                      status = #{status}
+        WHERE id = #{id}
+    </update>
+
+
+    <update id="updateOmsUrlById">
+        UPDATE finance_invoice SET
+                                      oms_ein_vno = #{omsEinVno},
+                                      oms_ofd_url = #{omsOfdUrl},
+                                      oms_pdf_url = #{omsPdfUrl},
+                                      oms_xml_url = #{omsXmlUrl},
+                                      status = #{status}
+        WHERE id = #{id}
+    </update>
+
+
+    <update id="updateOrderForGoodsResultStrById">
+        UPDATE finance_invoice SET
+            order_for_goods_result_str = #{orderForGoodsResultStr}
+        WHERE id = #{id}
+    </update>
+
+
+    <update id="updateConfirmationSlipResultStr">
+        UPDATE finance_invoice SET
+            confirmation_slip_result_str = #{confirmationSlipResultStr}
+        WHERE id = #{id}
+    </update>
+
+    <select id="getBillingContentDetail" resultType="com.jeeplus.assess.invoice.domain.FinanceInvoiceTaxClassificationCode">
+        select * from finance_invoice_tax_classification_code where del_flag = 0 and billing_content_id = #{billingType}
+    </select>
+
+
+    <update id="updateRedInvoiceJsonByWorkInvoiceId">
+        UPDATE finance_invoice
+        <!-- 用MyBatis的<set>标签替换原生SET,自动处理首字段逗号问题 -->
+        <set>
+            <if test="null != blueDownloadInvoiceJson and '' != blueDownloadInvoiceJson">
+                blue_download_invoice_json = #{blueDownloadInvoiceJson}
+            </if>
+            <if test="null != retryInvoiceJson and '' != retryInvoiceJson">
+                retry_invoice_json = #{retryInvoiceJson}
+            </if>
+            <if test="null != redInvoiceJson and '' != redInvoiceJson">
+                red_invoice_json = #{redInvoiceJson}
+            </if>
+            <if test="null != redDownloadInvoiceJson and '' != redDownloadInvoiceJson">
+                red_download_invoice_json = #{redDownloadInvoiceJson}
+            </if>
+        </set>
+        WHERE id = #{id}
+    </update>
 </mapper>

+ 400 - 6
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/FinanceInvoiceService.java

@@ -11,6 +11,10 @@ import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.google.common.collect.Lists;
+import com.jeeplus.assess.invoice.domain.OMS.InvoiceDown.OMSInvoiceDetailInfo;
+import com.jeeplus.assess.invoice.thread.ApprovalThread;
+import com.jeeplus.assess.invoice.thread.RedApprovalThread;
+import com.jeeplus.assess.invoice.utils.OMS.FileHandlingUtil;
 import com.jeeplus.common.TokenProvider;
 import com.jeeplus.core.query.QueryWrapperGenerator;
 import com.jeeplus.flowable.feign.IFlowableApi;
@@ -19,11 +23,9 @@ import com.jeeplus.sys.domain.WorkAttachmentInfo;
 import com.jeeplus.sys.feign.IRoleApi;
 import com.jeeplus.sys.feign.IUserApi;
 import com.jeeplus.sys.feign.IWorkAttachmentApi;
-//import com.jeeplus.sys.service.UserService;
+import com.jeeplus.sys.service.dto.OfficeDTO;
 import com.jeeplus.sys.service.dto.RoleDTO;
 import com.jeeplus.sys.service.dto.UserDTO;
-import com.jeeplus.sys.service.dto.WorkAttachmentInfoDTO;
-//import com.jeeplus.sys.utils.UserUtils;
 import com.jeeplus.assess.invoice.domain.*;
 import com.jeeplus.assess.invoice.mapper.*;
 import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceBaseDTO;
@@ -31,10 +33,8 @@ import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
 import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDetailDTO;
 import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceReceivablesDTO;
 import com.jeeplus.assess.invoice.service.mapstruct.*;
-//import com.jeeplus.pubmodules.serialNumTpl.service.SerialnumTplService;
-//import com.jeeplus.pubmodules.oss.mapper.OssServiceMapper;
 import com.jeeplus.assess.workContract.mapper.WorkContractInfoMapper;
-//import com.jeeplus.pubmodules.oss.service.dto.WorkAttachmentDto;
+import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
@@ -45,8 +45,11 @@ import java.math.BigDecimal;
 import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
+import static com.jeeplus.assess.invoice.config.FinanceOmsConfig.*;
+
 @Service
 @Transactional(rollbackFor = Exception.class)
 public class FinanceInvoiceService extends ServiceImpl<FinanceInvoiceMapper, FinanceInvoice> {
@@ -72,6 +75,8 @@ public class FinanceInvoiceService extends ServiceImpl<FinanceInvoiceMapper, Fin
     @Resource
     private IFlowableApi flowTaskService;
 
+    Pattern mobilePattern = Pattern.compile("^1[3-9]\\d{9}$");
+
     public List<String> getSearchList(ArrayList<String> searchIdList, List<String> ids) {
         List<String> newSearchIdList = searchIdList.stream().filter(item -> {
             AtomicInteger num = new AtomicInteger();
@@ -312,6 +317,28 @@ public class FinanceInvoiceService extends ServiceImpl<FinanceInvoiceMapper, Fin
             }
 
         });
+
+
+        List<String> idList = Lists.newArrayList();
+        //对数据进行处理
+        for (FinanceInvoiceDTO invoice : pageList.getRecords()) {
+            if(StringUtils.isNotBlank(invoice.getStatus()) && "5".equals(invoice.getStatus())){
+                //获取所有id,并用这些id查询对应的oms文件(仅需一条即可)
+                idList.add(invoice.getId());
+            }
+        }
+
+        List<WorkAttachmentInfo> omsInvoiceFileList  = SpringUtil.getBean(IWorkAttachmentApi.class).getByAttachmentIdListAndFlag(idList);
+        for (FinanceInvoiceDTO invoice : pageList.getRecords()) {
+            for (WorkAttachmentInfo workattachment : omsInvoiceFileList) {
+                if(invoice.getId().equals(workattachment.getAttachmentId())){
+                    invoice.setOmsAttachmentUrl(workattachment.getUrl());
+                    break;
+                }
+            }
+        }
+
+
         return pageList;
     }
 
@@ -387,11 +414,29 @@ public class FinanceInvoiceService extends ServiceImpl<FinanceInvoiceMapper, Fin
         if(CollectionUtil.isNotEmpty(fileList)){
             financeInvoiceDTO.setWorkAttachmentDtoList(fileList);
         }
+        // 查询发票文件
+        List<WorkAttachmentInfo> invoiceList = workContractInfoMapper.findInvoiceList(id);
+        if (CollectionUtil.isNotEmpty(invoiceList)) {
+            for (WorkAttachmentInfo i : invoiceList) {
+                i.setCreateBy(SpringUtil.getBean ( IUserApi.class ).getById(i.getBy()));
+            }
+        }
+        if(CollectionUtil.isNotEmpty(invoiceList)){
+            financeInvoiceDTO.setInvoiceList(invoiceList);
+        }
 
         return financeInvoiceDTO;
     }
 
     public String saveForm(FinanceInvoiceDTO financeInvoiceDTO) throws Exception{
+        String isOms = "0";
+        if("5".equals(financeInvoiceDTO.getStatus())){
+            isOms = handleDoInvoice(financeInvoiceDTO);
+            //如果需要走开票系统,则设置状态为12
+            if("1".equals(isOms) || "2".equals(isOms)){
+                financeInvoiceDTO.setStatus("12");
+            }
+        }
         FinanceInvoice financeInvoice = FinanceInvoiceWrapper.INSTANCE.toEntity(financeInvoiceDTO);
         if(StringUtils.isNotBlank(financeInvoiceDTO.getRelatedInvoice())){
             financeInvoice.setRelatedInvoice(financeInvoiceDTO.getRelatedInvoice());
@@ -563,9 +608,66 @@ public class FinanceInvoiceService extends ServiceImpl<FinanceInvoiceMapper, Fin
                 financeInvoiceInvalidMapper.insert(financeInvoiceInvalid);
             }
         }
+
+        //蓝票
+        if("1".equals(isOms)){
+            FinanceInvoiceDTO dto = queryById(financeInvoice.getId());
+            new ApprovalThread(dto, "21").start();
+        }
+        //红票
+        if("2".equals(isOms)){
+            FinanceInvoiceDTO dto = queryById(financeInvoice.getId());
+            new RedApprovalThread(dto,financeInvoiceDTO.getInvoiceNumberStr(), "21").start();
+        }
         return financeInvoice.getId();
     }
 
+    /**
+     * 判断是否开票逻辑
+     */
+
+    public String handleDoInvoice(FinanceInvoiceDTO financeInvoiceDTO){
+        boolean OMS_ENABLED = omsEnabled;
+        //保存数据结束,触发开票接口
+        if(OMS_ENABLED){
+            if("5".equals(financeInvoiceDTO.getStatus())){
+                BigDecimal account = new BigDecimal(financeInvoiceDTO.getAccount());
+                if (account.compareTo(BigDecimal.ZERO) > 0) {
+                    //返回开蓝票标记
+                    return "1";
+                }else{
+                    //先获取对应红冲发票的数电票  票号
+                    //查询关联红冲发票的开票号
+                    if("1".equals(financeInvoiceDTO.getRedInvoiceFlag())){
+                        if(StringUtils.isBlank(financeInvoiceDTO.getInvoiceNumberStr())){
+                            if(StringUtils.isNotBlank(financeInvoiceDTO.getRedInvoiceRelevancyId())){
+                                String invoiceNumberStr = this.getInvoiceNumberStr(financeInvoiceDTO.getRedInvoiceRelevancyId());
+                                financeInvoiceDTO.setInvoiceNumberStr(invoiceNumberStr);
+                            }
+                        }
+                        String invoiceNumber = financeInvoiceDTO.getInvoiceNumberStr();
+                        //判断 如果发票小于10位,则表示为老发票红冲,需要按照老方法进行处理
+                        if (invoiceNumber == null || StringUtils.isBlank(invoiceNumber) || invoiceNumber.length() < 10) {
+                            OMS_ENABLED = false;
+                        }
+                        if(OMS_ENABLED && StringUtils.isBlank(invoiceNumber)){
+                            throw new RuntimeException("红冲发票号不存在,无法进行红冲!");
+                        }
+                    }
+                    if(OMS_ENABLED){
+                        //返回开红票标记
+                        return "2";
+                    }
+                }
+            }
+        }
+        return "0";
+    }
+
+    public String getInvoiceNumberStr(String invoiceId) {
+        return financeInvoiceMapper.getInvoiceNumberStr(invoiceId);
+    }
+
     public ResponseEntity saveInvoice(FinanceInvoiceDTO financeInvoiceDTO) throws Exception{
         this.saveForm(financeInvoiceDTO);
         return ResponseEntity.ok("保存成功");
@@ -961,4 +1063,296 @@ public class FinanceInvoiceService extends ServiceImpl<FinanceInvoiceMapper, Fin
         });
         return pageList;
     }
+
+    /**
+     * 文件添加到work_attachment表中
+     * @param uploadResult
+     * @param workInvoice
+     * @param type
+     * @param sort
+     */
+    public void fileUploadOnAttachment(Map<String, Object> uploadResult, FinanceInvoiceDTO workInvoice, String type, String sort){
+        // 解析Map返回结果(核心适配点)
+        boolean isUploadSuccess = (boolean) uploadResult.get("success");
+        String ossFilePath = (String) uploadResult.get("ossFilePath");
+        Long fileSize = (Long) uploadResult.get("fileSize"); // 从Map获取file.size的字节数
+
+        //上传成功判断(替换原StringUtils.isNotBlank(ossFilePath))
+        if (isUploadSuccess && StringUtils.isNotBlank(ossFilePath)) {
+            // 检查是否以"/"开头,若不是则添加
+            if (!ossFilePath.startsWith("/")) {
+                ossFilePath = "/" + ossFilePath;
+            }
+            // 5. 构建附件表实体(保留你的核心业务逻辑,调整fileSize赋值)
+            WorkAttachmentInfo workattachment = new WorkAttachmentInfo();
+            workattachment.setDelFlag(0);
+            workattachment.setUrl(ossFilePath); // 存入OSS路径
+            workattachment.setType(type); // 文件类型
+            workattachment.setName(workInvoice.getNo() + "." + type); // 附件名称
+            workattachment.setAttachmentName(workInvoice.getNo() + "." + type); // 附件名称
+            workattachment.setAttachmentFlag("PG_OMS_invoice_file"); // 附件标识
+            workattachment.setSize(String.valueOf(fileSize)); // 改用Map中的文件大小(上传后本地文件已删除,原f.length()可能失效)
+            workattachment.setAttachmentId(workInvoice.getId()); // 关联发票ID
+            workattachment.setId(UUID.randomUUID().toString().replace("-", "")); // 主键ID
+
+            //校验数据库中是否已存在该附件(避免重复插入)
+
+            List<WorkAttachmentInfo> existAttachments =   SpringUtil.getBean ( IWorkAttachmentApi.class ).getByAttachmentIdAndUrlAndAttachmentFlag(workattachment);
+            if (CollectionUtils.isEmpty(existAttachments)) { // 建议用CollectionUtils.isEmpty判断空集合,更安全
+                Map<String, String> map = new HashMap<>();
+                String workAttachmentDtoInfo = JSON.toJSONString(workattachment);
+                AtomicInteger sortinfo = new AtomicInteger(Integer.parseInt(sort));
+                String userDTOInfo = JSON.toJSONString(workInvoice.getCreateBy());
+                String attachmentId = workInvoice.getId();
+                String attachmentFlag = "PG_OMS_invoice_file";
+                String sortInfo = Integer.toString(sortinfo.get());
+                map.put("workAttachmentDtoInfo", workAttachmentDtoInfo);
+                map.put("userDTOInfo", userDTOInfo);
+                map.put("attachmentId", attachmentId);
+                map.put("attachmentFlag", attachmentFlag);
+                map.put("sortInfo", sortInfo);
+                String fileId = SpringUtil.getBean(IWorkAttachmentApi.class).saveFile(map);
+            }
+        }
+    }
+
+    /**
+     * OMS发票开具失败进行的提示方法操作
+     * @param workInvoice
+     * @param errorMessage
+     * @param informType
+     * @return
+     */
+    @Transactional(readOnly = false)
+    public  Boolean handleInvoiceRetryAllFail(FinanceInvoiceDTO workInvoice, String errorMessage, String informType){
+        Boolean isSuccess = false;
+
+        //需要修改流程完成通知
+        //审核完成提示框
+        List<UserDTO> users = new ArrayList<>();
+        users.add(workInvoice.getCreateBy());
+
+        //项目名称获取
+        List<String> projectNameList = getProjectNameList(workInvoice);
+        String projectNameStr = String.join(",", projectNameList);
+        projectNameStr = projectNameStr.length() > 900
+                ? projectNameStr.substring(0, 900)
+                : projectNameStr;
+
+        String str = "发票申请编号:"+workInvoice.getNo() + "发票金额:" + workInvoice.getAccount() + "(元)。项目【"+ projectNameStr +"】发票申请失败,请核实后重新发起。失败原因:" + errorMessage;
+
+        String title = str;
+        UserDTO user = SpringUtil.getBean ( IUserApi.class ).getById(workInvoice.getCreateBy().getId());
+
+        //通知开票发起人和发票管理员
+        RoleDTO roleDTO = SpringUtil.getBean(IRoleApi.class).getRoleDTOByName2("发票管理员");
+        List<UserDTO> notifiedPartyUsers =SpringUtil.getBean(IUserApi.class).findListByRoleId(roleDTO.getId());
+        //发票管理员
+        notifiedPartyUsers.add(user);
+        if (StringUtils.isNotBlank(workInvoice.getStatus()) && !workInvoice.getStatus().equals("3")){
+            for (UserDTO notifiedPartyUser : notifiedPartyUsers) {
+                sendNotify(notifiedPartyUser,workInvoice.getProcessDefinitionId(),workInvoice.getProcInsId(),title);
+            }
+        }
+
+        //发起短信通知,用于通知开票人
+        //查询开票人员的手机号
+        if (StringUtils.isNotBlank(user.getMobile()) && mobilePattern.matcher(user.getMobile().trim()).matches() ) {
+            //验证手机号是否已经注册
+            if(StringUtils.isNotBlank(user.getMobile())){
+                Map<String,Object> result = null;
+                try{
+                    //调用工具类返回结果
+                    result = SpringUtil.getBean ( IUserApi.class ).sendEntryRandomCodesByInvoice(user.getMobile(),workInvoice.getNo(),errorSmsCode);
+                    Integer statusCode = (Integer) result.get("statusCode");
+                    if (200 == statusCode) {
+                        System.out.println("短信发送成功!");
+                    } else if (10001 == statusCode) {
+                        String message = (String) result.get("message");
+                        System.out.println(message);
+                    }else if(10002 == statusCode){
+                        System.out.println("账户短信量余额不足,请联系管理员进行充值!");
+                    }else if(10003 == statusCode){
+                        System.out.println("手机号获取验证码次数已达每日上限!");
+                    } else {
+                        System.out.println("短信发送失败,错误代码:101,请联系管理员!");
+                    }
+                }catch (Exception e){
+                    System.out.println("阿里云发送短信失败。失败原因为:" + e.getMessage());
+                    e.printStackTrace();
+                }
+            }
+        }
+        isSuccess = true;
+        return isSuccess;
+    }
+
+
+    /**
+     * OMS开票开票成功后对数据进行处理
+     * @param workInvoice
+     * @param invoiceInfo
+     * @param informType
+     * @return
+     */
+    @Transactional(readOnly = false)
+    public  Boolean updateOmsByIdOnDown(FinanceInvoiceDTO workInvoice, OMSInvoiceDetailInfo invoiceInfo, String informType) {
+        Boolean isSuccess = false;
+        workInvoice.setOmsEinVno(invoiceInfo.getAllEinVno());
+        if(StringUtils.isNotBlank(invoiceInfo.getOfdUrl())){
+            //先将文件下载到本地
+            String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getOfdUrl(), workInvoice.getNo() + ".ofd");
+            //如果文件下载下来了,并且返回值正常,则此处需要对文件进行上传到阿里云操作
+            if(StringUtils.isNotBlank(ofdLocalFilePath)){
+                // 调用上传方法,接收Map类型返回值(替换原String类型接收)
+                Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNo());
+                fileUploadOnAttachment(uploadResult,workInvoice, "ofd", "1");
+            }
+        }
+        workInvoice.setOmsOfdUrl(invoiceInfo.getOfdUrl());
+
+        if(StringUtils.isNotBlank(invoiceInfo.getPdfUrl())){
+            //先将文件下载到本地
+            String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getPdfUrl(), workInvoice.getNo() + ".pdf");
+            //如果文件下载下来了,并且返回值正常,则此处需要对文件进行上传到阿里云操作
+            if(StringUtils.isNotBlank(ofdLocalFilePath)){
+                // 调用上传方法,接收Map类型返回值(替换原String类型接收)
+                Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNo());
+                fileUploadOnAttachment(uploadResult,workInvoice, "pdf", "2");
+            }
+        }
+        workInvoice.setOmsPdfUrl(invoiceInfo.getPdfUrl());
+        if(StringUtils.isNotBlank(invoiceInfo.getXmlUrl())){
+            //先将文件下载到本地
+            String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getXmlUrl(), workInvoice.getNo() + ".xml");
+            //如果文件下载下来了,并且返回值正常,则此处需要对文件进行上传到阿里云操作
+            if(StringUtils.isNotBlank(ofdLocalFilePath)){
+                // 调用上传方法,接收Map类型返回值(替换原String类型接收)
+                Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNo());
+                fileUploadOnAttachment(uploadResult,workInvoice, "xml", "3");
+            }
+        }
+        workInvoice.setOmsXmlUrl(invoiceInfo.getXmlUrl());
+        workInvoice.setStatus("5");
+
+        //修改结果
+        updateOmsUrlById(workInvoice);
+        //还需要修改work_invoice_detail表中的对应发票编号(number)值
+        FinanceInvoiceDetail workInvoiceDetail = new FinanceInvoiceDetail();
+        workInvoiceDetail.setInvoiceId(workInvoice.getId());
+        workInvoiceDetail.setNumber(invoiceInfo.getAllEinVno());
+        financeInvoiceDetailMapper.updateNumberById(workInvoiceDetail);
+
+        //需要修改流程完成通知
+        //审核完成提示框
+        List<UserDTO> users = new ArrayList<>();
+        users.add(workInvoice.getCreateBy());
+
+        UserDTO userDTO = SpringUtil.getBean ( IUserApi.class ).getById(workInvoice.getCreateBy().getId());
+        OfficeDTO office = userDTO.getOfficeDTO();
+        String userName = userDTO.getName();
+
+        //项目名称获取
+        List<String> projectNameList = getProjectNameList(workInvoice);
+        String projectNameStr = String.join(",", projectNameList);
+        projectNameStr = projectNameStr.length() > 900
+                ? projectNameStr.substring(0, 900)
+                : projectNameStr;
+
+        String str = "发票金额:" + workInvoice.getAccount() + "(元)。项目【"+ projectNameStr +"】发票申请通过,发票申请编号:"+workInvoice.getNo() + ",创建人:"+userName+",所属部门:"+office.getName();
+        if(null != workInvoice.getBillingWorkplaceRealId()){
+            str = str + ",实际开票单位:"+workInvoice.getBillingWorkplaceReal();
+        }
+
+        String title = str;
+
+        //通知开票发起人和发票管理员
+        RoleDTO roleDTO = SpringUtil.getBean(IRoleApi.class).getRoleDTOByName2("发票管理员");
+        List<UserDTO> notifiedPartyUsers =SpringUtil.getBean(IUserApi.class).findListByRoleId(roleDTO.getId());
+        //发票管理员
+        notifiedPartyUsers.add(userDTO);
+        if (StringUtils.isNotBlank(workInvoice.getStatus()) && !workInvoice.getStatus().equals("3")){
+            workInvoice.setStatus("5");
+            for (UserDTO notifiedPartyUser : notifiedPartyUsers) {
+                sendNotify(notifiedPartyUser,workInvoice.getProcessDefinitionId(),workInvoice.getProcInsId(),title);
+            }
+        }
+
+
+        //发起短信通知,用于通知开票人
+        //查询开票人员的手机号
+        if (StringUtils.isNotBlank(userDTO.getMobile()) && mobilePattern.matcher(userDTO.getMobile().trim()).matches() ) {
+            //验证手机号是否已经注册
+            if(StringUtils.isNotBlank(userDTO.getMobile())){
+                Map<String,Object> result = null;
+                try{
+                    //调用工具类返回结果
+                    result = SpringUtil.getBean ( IUserApi.class ).sendEntryRandomCodesByInvoice(userDTO.getMobile(),workInvoice.getNo(),successSmsCode);
+                    Integer statusCode = (Integer) result.get("statusCode");
+                    if (200 == statusCode) {
+                        System.out.println("短信发送成功!");
+                    } else if (10001 == statusCode) {
+                        String message = (String) result.get("message");
+                        System.out.println(message);
+                    }else if(10002 == statusCode){
+                        System.out.println("账户短信量余额不足,请联系管理员进行充值!");
+                    }else if(10003 == statusCode){
+                        System.out.println("手机号获取验证码次数已达每日上限!");
+                    } else {
+                        System.out.println("短信发送失败,错误代码:101,请联系管理员!");
+                    }
+                }catch (Exception e){
+                    System.out.println("阿里云发送短信失败。失败原因为:" + e.getMessage());
+                    e.printStackTrace();
+                }
+                finally {
+
+                }
+            }
+        }
+        isSuccess = true;
+        return isSuccess;
+    }
+
+    public void sendNotify(UserDTO userDto,String defId,String procInsId,String title ){
+        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        String day = format.format(new Date());
+        Map<String ,String > map = new HashMap<>();
+        map.put("taskId",procInsId);
+        map.put("title",title);
+        map.put("defId",defId);
+        map.put("taskName","开票通知");
+        map.put("createUser",userDto.getName());
+        map.put("createTime",day);
+        map.put("noticeName",userDto.getLoginName());
+        map.put("noticeId",userDto.getId());
+        map.put("createById",userDto.getId());
+        SpringUtil.getBean ( IFlowableApi.class ).add(map);
+    }
+
+    /**
+     * 获取关联项目名称list
+     * @param workInvoice
+     * @return
+     */
+    public List<String> getProjectNameList(FinanceInvoiceDTO workInvoice){
+        //项目名称获取
+        List<String> projectNameList = Lists.newArrayList();
+        if(null != workInvoice.getFinanceInvoiceBaseDTOList()){
+            for (FinanceInvoiceBaseDTO projectRelation : workInvoice.getFinanceInvoiceBaseDTOList()) {
+                if(StringUtils.isNotBlank(projectRelation.getProgramId())){
+                    projectNameList.add(projectRelation.getProgramName());
+                }else{
+                    projectNameList.add("其它:"+projectRelation.getProgramName().replaceAll("\\r|\\n*",""));
+                }
+
+            }
+        }
+        return projectNameList;
+    }
+
+    @Transactional(readOnly = false)
+    public  void updateOmsUrlById(FinanceInvoiceDTO workInvoice){
+        financeInvoiceMapper.updateOmsUrlById(workInvoice);
+    }
 }

+ 221 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/OMS/InvoiceDownloadService.java

@@ -0,0 +1,221 @@
+package com.jeeplus.assess.invoice.service.OMS;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.jeeplus.assess.invoice.domain.OMS.InvoiceDown.OMSInvoiceDetailInfo;
+import com.jeeplus.assess.invoice.domain.OMS.OMSAccessTokenInfo;
+import com.jeeplus.assess.invoice.domain.OMS.OMSInvoiceResultDownloadData;
+import com.jeeplus.assess.invoice.mapper.FinanceInvoiceMapper;
+import com.jeeplus.assess.invoice.service.FinanceInvoiceService;
+import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
+import com.jeeplus.assess.invoice.utils.OMS.HttpPostJsonUtil;
+import com.jeeplus.assess.invoice.utils.OMS.OMSNationUtil;
+import com.jeeplus.common.redis.RedisUtils;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static com.jeeplus.assess.invoice.config.FinanceOmsConfig.*;
+
+
+/**
+ * OMS发票下载
+ */
+@Service
+@Transactional(readOnly = true)
+@Lazy
+public class InvoiceDownloadService {
+
+
+    @Autowired
+    private OMSDisposeService omsDisposeService;
+
+    @Autowired
+    private FinanceInvoiceService workInvoiceService;
+    @Autowired
+    private FinanceInvoiceMapper workInvoiceDetailDao;
+
+
+
+    @Transactional(readOnly = false)
+    public void processInvoiceDownloadTasks() {
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            Set<String> downloadTaskKeys = jedis.keys("PG_OMS_invoice_download:*");
+            if (downloadTaskKeys.isEmpty()) {
+                System.out.println("[InvoiceDownloadTask] 暂无待解析的发票任务");
+                return;
+            }
+
+            // 遍历所有解析任务
+            for (String taskKey : downloadTaskKeys) {
+                String accessToken =(String) jedis.hGet(taskKey, "accessToken");
+                String workInvoiceId = (String) jedis.hGet(taskKey, "workInvoiceId");
+                String informType = (String) jedis.hGet(taskKey, "informType");
+                //获取当前redis中的accessToken
+                //如果有值,则使用当前redis中的accessToken,否则重新生成一个新的accessToken
+                accessToken = (String) jedis.get("PGOMSAccessToken");
+
+                if(StringUtils.isBlank(accessToken)){
+                    // 获取AccessToken 9998重试5次
+                    accessToken = omsDisposeService.getOmsAccessTokenWithRetry(5, "accessToken", workInvoiceId, informType);
+                    if(StringUtils.isNotBlank(accessToken)){
+                        jedis.setEx("PGOMSAccessToken", accessToken,86400, TimeUnit.SECONDS);
+                        System.out.println("重新获取token成功,存入Redis");
+                    } else {
+                        accessToken = "";
+                        System.out.println("获取token失败");
+                        return;
+                    }
+                }
+
+                String firstExecTimeStr = (String) jedis.hGet(taskKey, "firstExecTime");
+
+                // 参数校验
+                if (StringUtils.isAnyBlank(accessToken, firstExecTimeStr)) {
+                    jedis.delete(taskKey);
+                    continue;
+                }
+
+                long firstExecTime = Long.parseLong(firstExecTimeStr);
+                // ========== 改动1:超时判断改为23小时(82800000毫秒) ==========
+                // 23*60*60*1000 = 82800000 ms(原86400000是24小时)
+                if (System.currentTimeMillis() - firstExecTime > 82800000L) {
+                    System.err.println("[InvoiceDownloadTask] 任务["+workInvoiceId+"]已重试23小时,触发兜底");
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票下载解析失败,请通过税务系统进行查看", informType);
+                    jedis.delete(taskKey); // 兜底后删除Redis,保证数据一致
+                    continue;
+                }
+
+                // ========== 核心:调用原始解析方法(移至此) ==========
+                executeInvoiceDownloadWithRetry(accessToken, workInvoiceId, informType);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+
+    /**
+     * 执行发票下载分析方法,获取发票下载接口以及其他信息
+     * @param accessToken   发票当时的accessToken
+     * @param workInvoiceId 发票的id(数据库中work_invoice中对应的id)
+     * @param informType 用于流程通知的判定参数
+     */
+
+    @Transactional(readOnly = false)
+    public void executeInvoiceDownloadWithRetry(String accessToken, String workInvoiceId, String informType) {
+        try {
+            System.out.println("✅ [InvoiceDownloadTask] 开始解析发票,订单号:" + workInvoiceId);
+
+            // ========== 原始解析逻辑 —— 完全不变 ==========
+            OMSInvoiceResultDownloadData getInvoiceInfo = new OMSInvoiceResultDownloadData();
+            getInvoiceInfo.setDeptCode(deptCode);
+            getInvoiceInfo.setOrderno(workInvoiceId);
+            getInvoiceInfo.setIsDetail("1");
+            String jsonInvoiceInfoStr = JSON.toJSONString(getInvoiceInfo);
+            String base64Str = Base64.getEncoder().encodeToString(jsonInvoiceInfoStr.getBytes(StandardCharsets.UTF_8));
+
+            OMSAccessTokenInfo invoiceDownInfo = new OMSAccessTokenInfo();
+            invoiceDownInfo.setAppId(appId);
+            invoiceDownInfo.setAppKey(appKey);
+            invoiceDownInfo.setExchangeId(UUID.randomUUID().toString());
+            invoiceDownInfo.setAccessToken(accessToken);
+            invoiceDownInfo.setData(base64Str);
+            String jsonInvoiceDownStr = JSON.toJSONString(invoiceDownInfo);
+
+            String invoiceResultStr = HttpPostJsonUtil.doPost(
+                    omsUrl +"/prod-api/output/server/invoice/download",
+                    jsonInvoiceDownStr
+            );
+            System.out.println("[InvoiceDownloadTask] 发票解析结果:" + invoiceResultStr);
+
+            String invoceDownJsonStr = null;
+            boolean isSuccess = false;
+            if (StringUtils.isNotBlank(invoiceResultStr)) {
+                OMSAccessTokenInfo resultDownInfo = JSON.parseObject(invoiceResultStr, OMSAccessTokenInfo.class);
+                if (null != resultDownInfo.getResult() && "0000".equals(resultDownInfo.getResult().getCode())) {
+                    invoceDownJsonStr = OMSNationUtil.getDownFromBase64(resultDownInfo.getData().toString());
+                    if (StringUtils.isNotBlank(invoceDownJsonStr)) {
+                        OMSInvoiceDetailInfo invoiceInfo = JSONObject.parseObject(invoceDownJsonStr, OMSInvoiceDetailInfo.class);
+                        if (invoiceInfo != null) {
+                            //根据开票id查询数据并进行修改
+                            //需要将错误信息保存到对应开票信息表中
+                            FinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+                            if(null != workInvoice){
+                                if(StringUtils.isBlank(invoiceInfo.getOfdUrl())){
+                                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取", informType);
+                                }else{
+                                    //进行处理
+                                    isSuccess = workInvoiceService.updateOmsByIdOnDown(workInvoice, invoiceInfo, informType);
+                                }
+                            }else{
+
+                                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取", informType);
+                                return;
+                            }
+                        }
+                    }
+                }else if(null != resultDownInfo.getResult() && ("1002".equals(resultDownInfo.getResult().getCode())
+                        || "1001".equals(resultDownInfo.getResult().getCode())
+                        || "1005".equals(resultDownInfo.getResult().getCode())
+                        || "1006".equals(resultDownInfo.getResult().getCode())
+                        || "0001".equals(resultDownInfo.getResult().getCode())
+                        || "0002".equals(resultDownInfo.getResult().getCode())
+                        || "0003".equals(resultDownInfo.getResult().getCode())
+                        || "0004".equals(resultDownInfo.getResult().getCode())
+                        || "0005".equals(resultDownInfo.getResult().getCode())
+                        || "0006".equals(resultDownInfo.getResult().getCode())
+                        || "0007".equals(resultDownInfo.getResult().getCode())
+                        || "0008".equals(resultDownInfo.getResult().getCode())
+                )){
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,失败原因:" + resultDownInfo.getResult().getMessage(), informType);
+                }
+            }
+
+            // ========== 重试规则调整:去掉次数限制,改为23小时超时(由定时任务控制) ==========
+            if (isSuccess) {
+                System.out.println("✅ [InvoiceDownloadTask] 任务["+workInvoiceId+"]解析成功,删除Redis任务");
+                deleteInvoiceDownloadTaskFromRedis(workInvoiceId); // 成功删除Redis,保证数据一致
+                return;
+            } else {
+                System.out.println("❌ [InvoiceDownloadTask] 任务["+workInvoiceId+"]解析失败,5分钟后重试");
+                // 无需手动延迟,由定时任务每5分钟扫描即可
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ [InvoiceDownloadTask] 任务["+workInvoiceId+"]解析抛出异常,5分钟后重试");
+            // 异常不删除Redis,由定时任务继续重试
+        }
+    }
+
+    // ========== 辅助方法:删除Redis解析任务 ==========
+    private void deleteInvoiceDownloadTaskFromRedis(String orderno) {
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            jedis.delete("PG_OMS_invoice_download:" + orderno);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+
+}

+ 778 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/OMS/OMSDisposeService.java

@@ -0,0 +1,778 @@
+package com.jeeplus.assess.invoice.service.OMS;
+
+import com.alibaba.fastjson.JSON;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.jeeplus.assess.invoice.domain.OMS.OMSAccessTokenInfo;
+import com.jeeplus.assess.invoice.domain.FinanceInvoiceTaxClassificationCode;
+import com.jeeplus.assess.invoice.mapper.FinanceInvoiceMapper;
+import com.jeeplus.assess.invoice.service.FinanceInvoiceService;
+import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
+import com.jeeplus.assess.invoice.utils.OMS.HttpPostJsonUtil;
+import com.jeeplus.assess.invoice.utils.OMS.OMSNationUtil;
+import com.jeeplus.common.redis.RedisUtils;
+
+import com.jeeplus.utils.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static com.jeeplus.assess.invoice.config.FinanceOmsConfig.*;
+
+@Service
+@Transactional(readOnly = true)
+@Lazy
+public class OMSDisposeService {
+
+    /**
+     * 给accessToken查询的有效时间设置为1天
+     */
+
+    //如果接口访问不正确,可以循环访问的次数
+    private final int remainRetryTimes = 20;
+
+
+    @Autowired
+    private FinanceInvoiceMapper workInvoiceDao;
+
+    @Autowired
+    private FinanceInvoiceService workInvoiceService;
+
+    // 注入SpringBoot内置的JSON序列化工具(无额外依赖)
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    /**
+     * 用于生成开蓝票信息
+     * @param map
+     * @param workInvoiceId 需要开票的开票信息id
+     */
+    @Transactional(readOnly = false)
+    public void doInvoiceBusiness(Map<String,Object> map, String workInvoiceId, String informType) {
+        RedisUtils jedis = null;
+        String accessToken = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            accessToken = (String) jedis.get("PGOMSAccessToken");
+            if(StringUtils.isBlank(accessToken)){
+                // 获取AccessToken 9998重试5次
+                accessToken = getOmsAccessTokenWithRetry(10, "accessToken", workInvoiceId, informType);
+                if(StringUtils.isNotBlank(accessToken)){
+                    jedis.setEx("PGOMSAccessToken", accessToken,86400, TimeUnit.SECONDS);
+                    map.put("token状态", "重新获取token成功,存入Redis");
+                } else {
+                    accessToken = "";
+                    map.put("token状态", "获取token失败");
+                    return;
+                }
+            } else {
+                map.put("token状态", "从Redis获取token成功");
+            }
+
+
+
+            OMSNationUtil util = new OMSNationUtil();
+            //获取需要开票的发票信息
+            FinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+
+            if(null != workInvoice){
+                if( StringUtils.isBlank(workInvoice.getBillingPeopleReal())){
+                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                    return;
+                }
+
+                if(StringUtils.isBlank(workInvoice.getBankAccount())){
+                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                    return;
+                }
+
+
+                //判定开票内容是否关联税收编码
+                if(StringUtils.isNotBlank(workInvoice.getBillingContent())){
+                    FinanceInvoiceTaxClassificationCode billingContentDetail = workInvoiceDao.getBillingContentDetail(workInvoice);
+                    if(null != billingContentDetail){
+                        workInvoice.setBillingContent(billingContentDetail.getGoodName());
+                        workInvoice.setGoodsTaxno(billingContentDetail.getGoodsTaxno());
+                    }else{
+                        handleInvoiceRetryAllFail("",workInvoiceId, "未找到对应税收编码,请联系信息部确认后重新发起", informType); // 解析失败直接兜底
+                    }
+                    //生成开票基础信息
+                    String string = util.neatenData(workInvoiceId,workInvoice,billingContentDetail);
+
+                    OMSAccessTokenInfo InvoiceTokenInfo = new OMSAccessTokenInfo();
+                    InvoiceTokenInfo.setAppId(appId);
+                    InvoiceTokenInfo.setAppKey(appKey);
+                    InvoiceTokenInfo.setExchangeId(UUID.randomUUID().toString());
+                    InvoiceTokenInfo.setAccessToken(accessToken);
+                    InvoiceTokenInfo.setData(string);
+                    String jsonInvoiceStr = JSON.toJSONString(InvoiceTokenInfo);
+
+                    String jsonInvoicResultStr = HttpPostJsonUtil.doPost(omsUrl +"/prod-api/output/server/order/upload", jsonInvoiceStr);
+                    System.out.println("✅ 订单提交接口返回值:" + jsonInvoicResultStr);
+                    map.put("订单接口信息", jsonInvoicResultStr);
+
+                    // 调用订单上传重试方法(包含所有码值规则)
+                    if(StringUtils.isNotBlank(jsonInvoicResultStr)){
+                        //将上个节点获取的值存到数据库,留作redis崩溃后进行重新获取处理
+                        workInvoice.setOrderForGoodsResultStr(jsonInvoicResultStr);
+                        workInvoiceDao.updateOrderForGoodsResultStrById(workInvoice);
+
+
+                        String finalAccessToken = accessToken;
+                        String finalJsonInvoiceStr = jsonInvoiceStr;
+                        executeOrderUploadRetry(workInvoiceId, remainRetryTimes, jsonInvoicResultStr, finalJsonInvoiceStr, finalAccessToken, map,"accessToken", "blueTicket", informType);
+                    }else{
+                        handleInvoiceRetryAllFail(jsonInvoiceStr,workInvoiceId, "蓝票发送订单生成接口返回值为空,开票失败。节点访问参数为:" + jsonInvoicResultStr, informType); // 解析失败直接兜底
+
+                    }
+                }else{
+                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                }
+
+
+
+            }else{
+                handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+            }
+
+
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            map.put("errorMsg", "系统异常:" + e.getMessage());
+        } finally {
+            if(jedis != null){
+                //jedis.close();
+            }
+        }
+    }
+
+    /**
+     * 用于调用accessToken
+     * @param remainRetryTimes
+     * @param getKey
+     * @return
+     */
+    @Transactional(readOnly = false)
+    public String getOmsAccessTokenWithRetry(int remainRetryTimes, String getKey, String workInvoiceId,  String informType) {
+        try {
+            OMSAccessTokenInfo tokenInfo = new OMSAccessTokenInfo();
+            tokenInfo.setAppId(appId);
+            tokenInfo.setAppKey(appKey);
+            tokenInfo.setExchangeId(UUID.randomUUID().toString());
+            String jsonStr = JSON.toJSONString(tokenInfo);
+            String accessTokenStr = HttpPostJsonUtil.doPost(omsUrl +"/prod-api/server/accessToken", jsonStr);
+
+            if(StringUtils.isBlank(accessTokenStr)){
+                System.err.println("获取AccessToken失败:接口返回空,剩余重试次数:"+remainRetryTimes);
+                if (remainRetryTimes > 1) {
+                    int nextRetry = remainRetryTimes - 1;
+                    System.err.println("⚠️ 获取AccessToken失败:接口返回空,剩余重试次数:"+nextRetry);
+                    Thread.sleep(30 * 1000);
+                    return getOmsAccessTokenWithRetry(nextRetry, getKey, workInvoiceId, informType);
+                } else {
+                    System.err.println("❌ 获取AccessToken失败:接口返回空,重试次数耗尽!");
+                    //需要将错误信息保存到对应开票信息表中
+                    FinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+                    if(null != workInvoice){
+                        workInvoice.setOmsAccessTokenError("获取AccessToken失败:接口返回空");
+                        //修改结果
+                        workInvoiceDao.updateAccessTokenErrorById(workInvoice);
+                    }
+                    //如果需要 可以将执行失败信息通过短信通知业务发起人
+                    handleInvoiceRetryAllFail("", workInvoiceId, "获取AccessToken失败:接口返回空", informType); // 解析失败也执行兜底方法
+
+                    return "";
+                }
+            }
+
+            OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(accessTokenStr, OMSAccessTokenInfo.class);
+            if(null == resultTokenInfo || null == resultTokenInfo.getResult()){
+                System.err.println("获取AccessToken失败:返回结果解析异常,剩余重试次数:"+remainRetryTimes);
+                if (remainRetryTimes > 1) {
+                    int nextRetry = remainRetryTimes - 1;
+                    System.err.println("⚠️ 获取AccessToken失败:返回结果解析异常,剩余重试次数:"+nextRetry);
+                    Thread.sleep(30 * 1000);
+                    return getOmsAccessTokenWithRetry(nextRetry, getKey, workInvoiceId, informType);
+                } else {
+                    System.err.println("❌ 获取AccessToken失败:返回结果解析异常,重试次数耗尽!");
+                    //需要将错误信息保存到对应开票信息表中
+                    FinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+                    if(null != workInvoice){
+                        workInvoice.setOmsAccessTokenError("获取AccessToken失败:返回结果解析异常");
+                        //修改结果
+                        workInvoiceDao.updateAccessTokenErrorById(workInvoice);
+                    }
+                    //如果需要 可以将执行失败信息通过短信通知业务发起人
+                    handleInvoiceRetryAllFail("", workInvoiceId, "获取AccessToken失败:返回结果解析异常", informType); // 解析失败也执行兜底方法
+
+                    return "";
+                }
+            }
+            String code = resultTokenInfo.getResult().getCode();
+            String message = resultTokenInfo.getResult().getMessage();
+            if ("0000".equals(code)) {
+                String token = OMSNationUtil.extractAccessTokenFromBase64(getKey, resultTokenInfo.getData().toString());
+                System.out.println("✅ 获取AccessToken成功,重试次数剩余:"+remainRetryTimes);
+                return token;
+            } else if ("9998".equals(code)) {
+                if (remainRetryTimes > 1) {
+                    int nextRetry = remainRetryTimes - 1;
+                    System.err.println("⚠️ 获取AccessToken返回9998接口波动,30秒后重试,剩余次数:"+nextRetry);
+                    Thread.sleep(30 * 1000);
+                    return getOmsAccessTokenWithRetry(nextRetry, getKey, workInvoiceId, informType);
+                } else {
+                    System.err.println("❌ 获取AccessToken失败:连续5次返回9998,重试次数耗尽!");
+                    //需要将错误信息保存到对应开票信息表中
+                    FinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+                    if(null != workInvoice){
+                        workInvoice.setOmsAccessTokenError("获取AccessToken失败,需要业务人员重新发起");
+                        //修改结果
+                        workInvoiceDao.updateAccessTokenErrorById(workInvoice);
+                    }
+                    //如果需要 可以将执行失败信息通过短信通知业务发起人
+                    handleInvoiceRetryAllFail("", workInvoiceId, "获取AccessToken失败,需要业务人员重新发起", informType); // 解析失败也执行兜底方法
+
+                    return "";
+                }
+            } else {
+                System.err.println("❌ 获取AccessToken失败:返回业务错误码,code:"+code + "。 错误信息为:"+message);
+                //需要将错误信息保存到对应开票信息表中
+                FinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+                if(null != workInvoice){
+                    workInvoice.setOmsAccessTokenError("获取AccessToken失败:返回业务错误码,code:"+code + "; 错误信息为:"+message + "。需要业务人员重新发起");
+                    //修改结果
+                    workInvoiceDao.updateAccessTokenErrorById(workInvoice);
+                }
+                return "";
+            }
+        } catch (InterruptedException e) {
+            System.err.println("❌ 获取AccessToken失败:重试延迟被中断");
+            Thread.currentThread().interrupt();
+            return "";
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ 获取AccessToken失败:调用接口异常,剩余重试次数:"+remainRetryTimes);
+            return "";
+        }
+    }
+
+
+    /**
+     * 调用订单上传方法,用于执行开票处理
+     * @param workInvoiceId     发票id
+     * @param remainRetryTimes  允许接口重调次数
+     * @param jsonInvoicResultStr      上一个接口返回值
+     * @param jsonInvoiceStr    上一个接口参数值
+     * @param accessToken   接口访问accessToken
+     * @param map   作用不大,仅作为保留
+     * @param getKey    用来判定的参数值
+     * @param initiationType    用来判定是什么类型的,比如蓝票、全类型红票、还是快捷红票
+     * @param informType    流程用来通知的类型
+     */
+
+    @Transactional(readOnly = false)
+    public void executeOrderUploadRetry(String workInvoiceId, int remainRetryTimes, String jsonInvoicResultStr, String jsonInvoiceStr, String accessToken, Map<String,Object> map, String getKey, String initiationType, String informType) {
+        String jsonInvoicResult = "";
+        try {
+            OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(jsonInvoicResultStr, OMSAccessTokenInfo.class);
+            if(null == resultTokenInfo || null == resultTokenInfo.getResult()){
+                System.err.println("❌ 订单上传解析失败,剩余重试次数:"+remainRetryTimes);
+                handleInvoiceRetryAllFail(accessToken, workInvoiceId, "", informType); // 解析失败也执行兜底方法
+                return;
+            }
+            String code = resultTokenInfo.getResult().getCode();
+            String message = resultTokenInfo.getResult().getMessage();
+
+            // ======================== 所有码值规则 全部在这里【最终定稿】=========================
+            if ("0000".equals(code)) {
+                // ✅ 0000 成功:解析数据+存入map+触发30秒后异步下载发票
+                jsonInvoicResult = OMSNationUtil.extractAccessTokenFromBase64(getKey, resultTokenInfo.getData().toString());
+                map.put("订单接口返回值",jsonInvoicResult);
+                System.out.println("✅ 订单上传返回0000成功,触发发票下载接口");
+                if(StringUtils.isNotBlank(jsonInvoicResult)){
+                    if(initiationType.equals("fastRed")){
+                        workInvoiceId = jsonInvoicResult;
+                    }
+                }
+                String finalWorkInvoiceId = workInvoiceId;
+                // 存入Redis,由InvoiceDownloadTask接管
+                saveInvoiceDownloadTaskToRedis(accessToken, finalWorkInvoiceId, informType);
+                System.out.println("✅ 解析开票数据任务["+finalWorkInvoiceId+"]已存入Redis,由InvoiceDownloadTask接管重试");
+
+            } else if ("9998".equals(code)) {
+                // 通过redis调用 方式系统崩溃导致的数据丢失
+                // ✅ 9998 接口波动,放入redis中 用于定时任务进行处理
+                saveInvoiceRetryScheduledTaskToRedis(jsonInvoiceStr, accessToken, workInvoiceId, getKey, initiationType);
+
+            } else if ("0003".equals(code)) {
+                // ✅ 0003 token失效:清除旧token → 重新获取token → 从头完整执行所有流程
+                System.err.println("⚠️ 订单上传返回0003(token失效),开始重新获取token并从头执行流程");
+                RedisUtils jedis = null;
+                try {
+                    jedis = RedisUtils.getInstance();
+                    jedis.delete("PGOMSAccessToken");
+                    map.put("0003处理", "已清除旧token,准备重新获取");
+                } catch (Exception e) {
+                    e.printStackTrace();
+                } finally {
+                    if(jedis != null) {
+                        //jedis.close();
+                    }
+                }
+                doInvoiceBusiness(map, workInvoiceId, informType);
+            } else {
+                // ✅ ✅ ✅ 核心修正:0001/0002/其他任意错误码 → 直接调用handleInvoiceRetryAllFail执行修改系统信息逻辑 ✅ ✅ ✅
+                System.err.println("❌ 订单上传返回业务错误码:"+code+",立即执行失败兜底逻辑修改系统信息!");
+                System.err.println("❌ 订单上传返回业务错误原因:"+message);
+                map.put("订单状态", "失败,错误码:"+code);
+                handleInvoiceRetryAllFail(accessToken, workInvoiceId, "订单上传返回业务错误码:"+code+",错误原因:"+message, informType); // 解析失败也执行兜底方法
+                remainRetryTimes = 0;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ 订单上传重试异常,剩余次数:"+remainRetryTimes);
+            handleInvoiceRetryAllFail(accessToken, workInvoiceId, "订单上传重试异常,请重新发起", informType); // 解析失败也执行兜底方法
+        }
+    }
+
+    /**
+     * 用于访问接口返回9998时使用,存入Redis供定时任务处理
+     * 硬编码配置:初始重试次数100次,Redis过期时间1天(86400秒)
+     * @param jsonInvoiceStr 订单上传的JSON参数
+     * @param accessToken    AccessToken
+     * @param workInvoiceId        订单号
+     * @param getKey         获取token的key
+     * @param initiationType 开票类型(蓝票/红票)
+     */
+    @Transactional(readOnly = false)
+    public void saveInvoiceRetryScheduledTaskToRedis(String jsonInvoiceStr ,String accessToken, String workInvoiceId, String getKey, String initiationType) {
+        RedisUtils jedis = null;
+        // 硬编码:初始重试次数100次
+        final int INIT_RETRY_COUNT = 100;
+        // 硬编码:Redis任务过期时间1天(86400秒)
+        final int REDIS_EXPIRE_SECONDS = 86400;
+
+        try {
+            jedis = RedisUtils.getInstance();
+            String redisKey = "PG_OMS_invoice_retry_scheduled:" + workInvoiceId;
+
+            // 先检查是否已存在,存在则复用原有重试次数,不存在则初始化20次
+            String retryTimes = (String) jedis.hGet(redisKey, "retryTimes");
+            if (StringUtils.isBlank(retryTimes)) {
+                retryTimes = String.valueOf(INIT_RETRY_COUNT); // 初始20次
+            }
+
+            // 存储所有需要的字段
+            jedis.hPut(redisKey, "jsonInvoiceStr", jsonInvoiceStr);
+            jedis.hPut(redisKey, "accessToken", accessToken);
+            jedis.hPut(redisKey, "workInvoiceId", workInvoiceId);
+            jedis.hPut(redisKey, "getKey", getKey);
+            jedis.hPut(redisKey, "initiationType", initiationType);
+            jedis.hPut(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
+            jedis.hPut(redisKey, "retryTimes", retryTimes); // 重试次数
+
+            jedis.expire(redisKey, REDIS_EXPIRE_SECONDS); // 1天过期
+            System.out.println("✅ 9998重试任务已存入Redis,订单号:" + workInvoiceId + ",剩余重试次数:" + retryTimes);
+
+
+            // ========== 追加:Redis数据转JSON,持久化到发票表(兜底逻辑) ==========
+            long redisStoreTime = System.currentTimeMillis() / 1000; // 秒级存储时间,用于后期回滚
+            // 封装Map:包含Redis所有哈希字段 + redisKey/过期时间/存储时间,保留redisKey(按你要求)
+            Map<String, Object> retryTaskMap = new HashMap<>(12);
+            // Redis哈希中所有原字段(和Redis完全一致)
+            retryTaskMap.put("jsonInvoiceStr", jsonInvoiceStr);
+            retryTaskMap.put("accessToken", accessToken);
+            retryTaskMap.put("workInvoiceId", workInvoiceId);
+            retryTaskMap.put("getKey", getKey);
+            retryTaskMap.put("initiationType", initiationType);
+            retryTaskMap.put("firstExecTime", String.valueOf(System.currentTimeMillis()));
+            retryTaskMap.put("retryTimes", retryTimes);
+            // 额外兜底字段(保留redisKey)
+            retryTaskMap.put("redisKey", redisKey);
+            retryTaskMap.put("redisExpireSeconds", REDIS_EXPIRE_SECONDS);
+            retryTaskMap.put("redisStoreTime", redisStoreTime);
+            retryTaskMap.put("initRetryCount", INIT_RETRY_COUNT); // 追加初始重试次数,后期回滚可参考
+
+            // Map转JSON字符串(单独捕获序列化异常,不影响Redis核心逻辑)
+            String retryTaskJson = objectMapper.writeValueAsString(retryTaskMap);
+
+            // 更新发票表(和之前红冲/下载任务用相同的DAO和实体,风格统一)
+            FinanceInvoiceDTO workInvoice = new FinanceInvoiceDTO();
+            workInvoice.setId(workInvoiceId);
+            workInvoice.setRetryInvoiceJson(retryTaskJson);
+            // 处理DAO更新结果,按你的风格打印控制台信息
+            workInvoiceDao.updateRedInvoiceJsonByWorkInvoiceId(workInvoice);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ 存入9998重试任务到Redis失败,订单号:" + workInvoiceId + ",原因:" + e.getMessage());
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+
+
+    /**
+     * 新增开蓝票生成的下载用的redis
+     * @param accessToken
+     * @param workInvoiceId 开票的id
+     */
+    public void saveInvoiceDownloadTaskToRedis(String accessToken, String workInvoiceId, String informType) {
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            String redisKey = "PG_OMS_invoice_download:" + workInvoiceId;
+            jedis.hPut(redisKey, "accessToken", accessToken);
+            jedis.hPut(redisKey, "workInvoiceId", workInvoiceId);
+            jedis.hPut(redisKey, "informType", informType);
+            jedis.hPut(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
+            jedis.expire(redisKey, 7200); // 1天过期
+
+            // ========== 追加:封装Redis数据→转JSON→更新发票表(和红冲方法逻辑一致) ==========
+            long redisStoreTime = System.currentTimeMillis() / 1000; // 秒级存储时间,用于后期回滚
+            // 封装Map:和Redis中存储的键值完全一致,方便后期回滚解析
+            Map<String, Object> downloadTaskMap = new HashMap<>(10);
+            downloadTaskMap.put("accessToken", accessToken);
+            downloadTaskMap.put("workInvoiceId", workInvoiceId);
+            downloadTaskMap.put("informType", informType);
+            downloadTaskMap.put("firstExecTime", String.valueOf(System.currentTimeMillis())); // 和Redis保持一致的毫秒数字符串
+            downloadTaskMap.put("redisKey", redisKey);
+            downloadTaskMap.put("redisExpireSeconds", 7200);
+            downloadTaskMap.put("redisStoreTime", redisStoreTime); // 新增秒级存储时间
+
+            // Map转JSON字符串(单独捕获序列化异常,不影响核心逻辑)
+            String downloadTaskJson = objectMapper.writeValueAsString(downloadTaskMap);
+
+            // 更新发票表(和红冲方法用相同的DAO/实体,保持风格统一)
+            FinanceInvoiceDTO workInvoice = new FinanceInvoiceDTO();
+            workInvoice.setId(workInvoiceId);
+            workInvoice.setBlueDownloadInvoiceJson(downloadTaskJson);
+            workInvoiceDao.updateRedInvoiceJsonByWorkInvoiceId(workInvoice);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+
+    /**
+     * 兜底方法
+     * @param accessToken
+     */
+    @Transactional(readOnly = false)
+    public void handleInvoiceRetryAllFail(String accessToken, String workInvoiceId, String errorMessage, String informType) {
+        // ============ 你的所有【修改系统信息/更新订单状态/写失败日志】逻辑 全部写在这里!!! ============
+        try {
+            System.err.println("============ 开始执行【失败兜底-系统信息修改逻辑】 ============");
+            FinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+            if(null != workInvoice){
+                //将错误信息保存到数据库
+                workInvoice.setOmsErrorMessage(errorMessage);
+                workInvoice.setOmsAccessToken(accessToken);
+                workInvoice.setStatus("3");
+                //修改结果
+                workInvoiceDao.updateAccessTokenErrorById(workInvoice);
+
+                //通知发起人或者开票管理员
+                workInvoiceService.handleInvoiceRetryAllFail(workInvoice, errorMessage, informType);
+
+            }
+
+
+            System.err.println("============ 【失败兜底-系统信息修改逻辑】执行完成 ============");
+
+        } catch (Exception e) {
+            // 必加:捕获这个方法的异常,避免兜底方法报错导致线程异常
+            e.printStackTrace();
+            System.err.println("❌ 执行失败兜底方法时抛出异常:" + e.getMessage());
+        }
+    }
+
+
+    /**
+     * 快速开红票的调用方法
+     * @param map
+     * @param allEinvno
+     */
+    @Transactional(readOnly = false)
+    public void doFastRedInvoiceBusiness(Map<String,Object> map, String allEinvno, String workInvoiceId, String informType) {
+        RedisUtils jedis = null;
+        String accessToken = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            accessToken = (String) jedis.get("PGOMSAccessToken");
+            if(StringUtils.isBlank(accessToken)){
+                // 获取AccessToken 9998重试5次
+                accessToken = getOmsAccessTokenWithRetry(10, "accessToken", workInvoiceId, informType);
+                if(StringUtils.isNotBlank(accessToken)){
+                    jedis.setEx("PGOMSAccessToken", accessToken,86400,TimeUnit.SECONDS);
+                    map.put("token状态", "重新获取token成功,存入Redis");
+                } else {
+                    accessToken = "";
+                    map.put("token状态", "获取token失败");
+                    return;
+                }
+            } else {
+                map.put("token状态", "从Redis获取token成功");
+            }
+
+            OMSNationUtil util = new OMSNationUtil();
+            String string = util.neatenFastRedInvoiceData(allEinvno);
+
+            OMSAccessTokenInfo InvoiceTokenInfo = new OMSAccessTokenInfo();
+            InvoiceTokenInfo.setAppId(appId);
+            InvoiceTokenInfo.setAppKey(appKey);
+            InvoiceTokenInfo.setExchangeId(UUID.randomUUID().toString());
+            InvoiceTokenInfo.setAccessToken(accessToken);
+            InvoiceTokenInfo.setData(string);
+            String jsonInvoiceStr = JSON.toJSONString(InvoiceTokenInfo);
+
+            String jsonInvoicResultStr = HttpPostJsonUtil.doPost(omsUrl +"/prod-api/output/server/invoice/makeredinv", jsonInvoiceStr);
+            System.out.println("✅ 快速红冲订单提交接口返回值:" + jsonInvoicResultStr);
+            map.put("快速红冲订单接口信息", jsonInvoicResultStr);
+
+            // 调用订单上传重试方法(包含所有码值规则)
+            if(StringUtils.isNotBlank(jsonInvoicResultStr)){
+                String finalAccessToken = accessToken;
+                String finalJsonInvoiceStr = jsonInvoiceStr;
+                executeOrderUploadRetry(workInvoiceId, 5, jsonInvoicResultStr, finalJsonInvoiceStr, finalAccessToken, map, "orderno", "fastRed", informType);
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            map.put("errorMsg", "系统异常:" + e.getMessage());
+        } finally {
+            if(jedis != null){
+                //jedis.close();
+            }
+        }
+    }
+
+
+    /**
+     * 调用生成红字确认申请单
+     * @param map
+     * @param workInvoiceId 需要开红字票的开票id信息
+     * @param redInvoiceRelevancyId 开红票对应蓝票的invoiceid
+     * @param originalInvno 原蓝票发票号码(数电票号码)
+     * @param informType 用于流程通知的判定条件
+     */
+    @Transactional(readOnly = false)
+    public void doAllScenarioRedInvoiceBusiness(Map<String,Object> map, String workInvoiceId, String redInvoiceRelevancyId, String originalInvno, String informType) {
+        RedisUtils jedis = null;
+        String accessToken = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            accessToken = (String)jedis.get("PGOMSAccessToken");
+            if(StringUtils.isBlank(accessToken)){
+                // 获取AccessToken 9998重试5次
+                accessToken = getOmsAccessTokenWithRetry(10, "accessToken", workInvoiceId, informType);
+                if(StringUtils.isNotBlank(accessToken)){
+                    jedis.setEx("PGOMSAccessToken", accessToken,86400,TimeUnit.SECONDS);
+                    map.put("token状态", "重新获取token成功,存入Redis");
+                } else {
+                    accessToken = "";
+                    map.put("token状态", "获取token失败");
+                    return;
+                }
+            } else {
+                map.put("token状态", "从Redis获取token成功");
+            }
+
+            OMSNationUtil util = new OMSNationUtil();
+            //获取需要开票的发票信息
+            FinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+
+            if(null != workInvoice){
+
+                if(StringUtils.isBlank(workInvoice.getBillingPeopleReal()) ){
+                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                    return;
+                }
+                //获取开票银行信息
+                if(StringUtils.isBlank(workInvoice.getBankAccount())){
+                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                    return;
+                }
+
+                //获取被红冲发票的基本信息
+
+
+                if(StringUtils.isNotBlank(workInvoice.getBillingContent())){
+                    FinanceInvoiceTaxClassificationCode billingContentDetail = workInvoiceDao.getBillingContentDetail(workInvoice);
+                    if(null != billingContentDetail){
+                        workInvoice.setBillingContent(billingContentDetail.getGoodName());
+                        workInvoice.setGoodsTaxno(billingContentDetail.getGoodsTaxno());
+                    }else{
+                        handleInvoiceRetryAllFail("",workInvoiceId, "未找到对应税收编码,请联系信息部确认后重新发起", informType); // 解析失败直接兜底
+                        return;
+                    }
+
+                    //获取本次红冲对应蓝票的基本信息
+                    FinanceInvoiceDTO workInvoiceRelevancy = workInvoiceService.queryById(redInvoiceRelevancyId);
+                    if(null == workInvoiceRelevancy || null == workInvoiceRelevancy.getBillingPeopleReal()){
+                        handleInvoiceRetryAllFail("",workInvoiceId, "未找到需要红冲票的信息,请确认后重新发起", informType); // 解析失败直接兜底
+                        return;
+                    }
+                    //获取被红冲蓝票的税率信息
+                    FinanceInvoiceTaxClassificationCode relevancyBillingContentDetail = workInvoiceDao.getBillingContentDetail(workInvoiceRelevancy);
+                    if(null == relevancyBillingContentDetail){
+                        handleInvoiceRetryAllFail("",workInvoiceId, "未找到被红冲蓝票的对应税收编码,请联系信息部确认后重新发起", informType); // 解析失败直接兜底
+                        return;
+                    }
+
+                    //生成红冲的数据
+                    String string = util.neatenAllScenarioRedInvoiceData(workInvoiceId, originalInvno,workInvoice, workInvoiceRelevancy, billingContentDetail,relevancyBillingContentDetail);
+
+                    OMSAccessTokenInfo InvoiceTokenInfo = new OMSAccessTokenInfo();
+                    InvoiceTokenInfo.setAppId(appId);
+                    InvoiceTokenInfo.setAppKey(appKey);
+                    InvoiceTokenInfo.setExchangeId(UUID.randomUUID().toString());
+                    InvoiceTokenInfo.setAccessToken(accessToken);
+                    InvoiceTokenInfo.setData(string);
+                    String jsonInvoiceStr = JSON.toJSONString(InvoiceTokenInfo);
+
+                    String jsonInvoicResultStr = HttpPostJsonUtil.doPost(omsUrl +"/prod-api/output/server/redApply/apply", jsonInvoiceStr);
+                    System.out.println("✅ 全场景红冲订单提交接口返回值:" + jsonInvoicResultStr);
+                    map.put("全场景红冲订单接口信息", jsonInvoicResultStr);
+
+                    // 调用订单上传重试方法(包含所有码值规则)
+                    if(StringUtils.isNotBlank(jsonInvoicResultStr)){
+
+                        //将上个节点获取的值存到数据库,留作redis崩溃后进行重新获取处理
+                        workInvoice.setOrderForGoodsResultStr(jsonInvoicResultStr);
+                        workInvoiceDao.updateOrderForGoodsResultStrById(workInvoice);
+
+
+                        String finalAccessToken = accessToken;
+                        queryRedInvoiceConfirm (workInvoiceId, 5, jsonInvoicResultStr, finalAccessToken,informType);
+                    } else {
+                        handleInvoiceRetryAllFail(accessToken,workInvoiceId, "全场景红冲订单接口返回值为空,开票失败。节点申请参数为:"+ jsonInvoiceStr, informType); // 解析失败直接兜底
+                        return;
+                    }
+                }else{
+                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                    return;
+                }
+
+            }else{
+                handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                return;
+            }
+
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            map.put("errorMsg", "系统异常:" + e.getMessage());
+        } finally {
+            if(jedis != null){
+                //jedis.close();
+            }
+        }
+    }
+
+    /**
+     * 全类型红冲--红字确认单查询接口(仅存Redis,不执行业务)
+     * @param remainRetryTimes  剩余重试次数
+     * @param jsonInvoicResultStr   需要解析的返回值密文
+     * @param accessToken   accessToken
+     * @param workInvoiceId 开票的id
+     */
+    @Transactional(readOnly = false)
+    public void queryRedInvoiceConfirm (String workInvoiceId, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String informType) {
+        try {
+            OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(jsonInvoicResultStr, OMSAccessTokenInfo.class);
+            if(null == resultTokenInfo || null == resultTokenInfo.getResult()){
+                System.err.println("❌ 全场景红冲订单提交接口返回值解析失败,剩余重试次数:"+remainRetryTimes);
+                handleInvoiceRetryAllFail(accessToken, workInvoiceId, "", informType); // 解析失败也执行兜底方法
+                return;
+            }
+            String code = resultTokenInfo.getResult().getCode();
+
+            if ("0000".equals(code)) {
+                // 仅存入Redis,删除所有业务执行逻辑
+                saveRedInvoiceTaskToRedis(workInvoiceId, remainRetryTimes, jsonInvoicResultStr, accessToken, workInvoiceId, System.currentTimeMillis(),informType);
+                System.out.println("✅ 红冲任务["+workInvoiceId+"]已存入Redis,由定时任务接管执行");
+            } else {
+                String message = resultTokenInfo.getResult().getMessage();
+                System.err.println("❌ 全类型红冲--红字确认单查询接口返回业务错误码:"+code+",错误原因为:"+message);
+                handleInvoiceRetryAllFail(accessToken, workInvoiceId, "全类型红冲--红字确认单查询接口返回业务错误码:"+code+",错误原因为:"+message, informType); // 解析失败也执行兜底方法
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            handleInvoiceRetryAllFail(accessToken, workInvoiceId, "全类型红冲--红字确认单查询接口发起失败,请重新发起", informType); // 解析失败也执行兜底方法
+        }
+    }
+
+    /**
+     * 存储红冲任务到Redis(供原有业务代码调用,参数完整)
+     * @param workInvoiceId 发票id
+     * @param remainRetryTimes  允许回调的次数,这里暂时未用到,仅作为保留参数
+     * @param jsonInvoicResultStr   全场景红冲订单提交接口返回值
+     * @param accessToken
+     * @param applyNo   发票id
+     * @param startTime 这个任务开始时间。限制:开始3天内如果双方没有全部确认,则本次红冲失败
+     */
+    @Transactional(readOnly = false)
+    public void saveRedInvoiceTaskToRedis(String workInvoiceId, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime, String informType) {
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            String redisKey = "PG_OMS_red_invoice_task:" + workInvoiceId;
+            // 存储所有执行所需的参数(确保定时任务能独立执行)
+            jedis.hPut(redisKey, "remainRetryTimes", String.valueOf(remainRetryTimes));
+            jedis.hPut(redisKey, "jsonInvoicResultStr", jsonInvoicResultStr);
+            jedis.hPut(redisKey, "accessToken", accessToken);
+            jedis.hPut(redisKey, "workInvoiceId", workInvoiceId);
+            jedis.hPut(redisKey, "informType", informType);
+            jedis.hPut(redisKey, "startTime", String.valueOf(startTime));
+            jedis.expire(redisKey, 259200 + 3600); // 3天+1小时过期
+
+
+
+            //将对应的redis中数据进行存储到数据库中,放置redis数据崩溃丢失
+            // 1. 封装Redis中所有存储的参数到Map(和Redis键值完全一致,方便后期回滚解析)
+            long redisStoreTime = System.currentTimeMillis() / 1000; // 核心:精确到秒,long类型
+            Map<String, Object> redInvoiceMap = new HashMap<>(); // 容量从8改为10,适配新字段
+            redInvoiceMap.put("remainRetryTimes", remainRetryTimes);
+            redInvoiceMap.put("jsonInvoicResultStr", jsonInvoicResultStr);
+            redInvoiceMap.put("accessToken", accessToken);
+            redInvoiceMap.put("workInvoiceId", workInvoiceId);
+            redInvoiceMap.put("informType", informType);
+            redInvoiceMap.put("startTime", startTime);
+            redInvoiceMap.put("redisKey", redisKey);
+            redInvoiceMap.put("redisExpireSeconds", 259200 + 3600);
+            redInvoiceMap.put("redisStoreTime", redisStoreTime); // 新增:Redis存储时间(秒级时间戳)
+
+            // 2. Map转JSON字符串(Spring内置Jackson,无额外依赖)
+            String redInvoiceJson = objectMapper.writeValueAsString(redInvoiceMap);
+
+            FinanceInvoiceDTO workInvoice = new FinanceInvoiceDTO();
+            workInvoice.setId(workInvoiceId);
+            workInvoice.setRedInvoiceJson(redInvoiceJson);
+            // 3. 更新现有发票表的JSON字段(根据发票ID精准更新)
+            workInvoiceDao.updateRedInvoiceJsonByWorkInvoiceId(workInvoice);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            handleInvoiceRetryAllFail(accessToken, workInvoiceId, "全类型红冲--红字确认单查询接口发起失败,请重新发起", informType); // 解析失败也执行兜底方法
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+
+
+}

+ 222 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/OMS/RedInvoiceDownloadService.java

@@ -0,0 +1,222 @@
+package com.jeeplus.assess.invoice.service.OMS;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.jeeplus.assess.invoice.domain.OMS.InvoiceDown.OMSInvoiceDetailInfo;
+import com.jeeplus.assess.invoice.domain.OMS.OMSAccessTokenInfo;
+import com.jeeplus.assess.invoice.domain.OMS.OMSInvoiceResultDownloadData;
+import com.jeeplus.assess.invoice.mapper.FinanceInvoiceMapper;
+import com.jeeplus.assess.invoice.service.FinanceInvoiceService;
+import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
+import com.jeeplus.assess.invoice.utils.OMS.HttpPostJsonUtil;
+import com.jeeplus.assess.invoice.utils.OMS.OMSNationUtil;
+import com.jeeplus.common.redis.RedisUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static com.jeeplus.assess.invoice.config.FinanceOmsConfig.*;
+
+/**
+ * OMS发票下载
+ */
+@Service
+@Transactional(readOnly = true)
+@Lazy
+public class RedInvoiceDownloadService {
+
+    @Autowired
+    private OMSDisposeService omsDisposeService;
+
+    @Autowired
+    private FinanceInvoiceService workInvoiceService;
+    @Autowired
+    private FinanceInvoiceMapper workInvoiceDetailDao;
+
+
+    @Transactional(readOnly = false)
+    public void processRedInvoiceDownloadTasks() {
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            Set<String> downloadTaskKeys = jedis.keys("PG_OMS_red_invoice_download:*");
+            if (downloadTaskKeys.isEmpty()) {
+                System.out.println("[RedInvoiceDownloadTask] 暂无待解析的发票任务");
+                return;
+            }
+
+            // 遍历所有解析任务
+            for (String taskKey : downloadTaskKeys) {
+                String accessToken =(String) jedis.hGet(taskKey, "accessToken");
+                String redInvOrderNo = (String)jedis.hGet(taskKey, "redInvOrderNo");    //红票对应的编号
+                String workInvoiceId = (String)jedis.hGet(taskKey, "workInvoiceId");
+                String informType =(String) jedis.hGet(taskKey, "informType");
+                //获取当前redis中的accessToken
+                //如果有值,则使用当前redis中的accessToken,否则重新生成一个新的accessToken
+                accessToken = (String)jedis.get("PGOMSAccessToken");
+
+                if(StringUtils.isBlank(accessToken)){
+                    // 获取AccessToken 9998重试5次
+                    accessToken = omsDisposeService.getOmsAccessTokenWithRetry(5, "accessToken", workInvoiceId, informType);
+                    if(StringUtils.isNotBlank(accessToken)){
+                        jedis.setEx("PGOMSAccessToken", accessToken,86400, TimeUnit.SECONDS);
+                        System.out.println("重新获取token成功,存入Redis");
+                    } else {
+                        accessToken = "";
+                        System.out.println("获取token失败");
+                        return;
+                    }
+                }
+
+                String firstExecTimeStr = (String) jedis.hGet(taskKey, "firstExecTime");
+
+                // 参数校验
+                if (StringUtils.isAnyBlank(accessToken, firstExecTimeStr)) {
+                    jedis.delete(taskKey);
+                    continue;
+                }
+
+                long firstExecTime = Long.parseLong(firstExecTimeStr);
+                // ========== 改动1:超时判断改为23小时(82800000毫秒) ==========
+                // 23*60*60*1000 = 82800000 ms(原86400000是24小时)
+                if (System.currentTimeMillis() - firstExecTime > 82800000L) {
+                    System.err.println("[InvoiceDownloadTask] 任务["+workInvoiceId+"]已重试23小时,触发兜底");
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票下载解析失败,请通过税务系统进行查看", informType);
+                    //需要修改项目相关信息
+
+
+                    jedis.delete(taskKey); // 兜底后删除Redis,保证数据一致
+                    continue;
+                }
+
+                // ========== 核心:调用原始解析方法(移至此) ==========
+                executeInvoiceDownloadWithRetry(accessToken, redInvOrderNo, workInvoiceId, informType);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+
+    /**
+     * 执行发票下载分析方法,获取发票下载接口以及其他信息
+     * @param accessToken   发票当时的accessToken
+     * @param redInvOrderNo 红字发票编号
+     * @param workInvoiceId 发票的id(数据库中work_invoice中对应的id)
+     */
+
+    @Transactional(readOnly = false)
+    public void executeInvoiceDownloadWithRetry(String accessToken, String redInvOrderNo, String workInvoiceId, String informType) {
+        try {
+            System.out.println("✅ [InvoiceDownloadTask] 开始解析发票,订单号:" + workInvoiceId);
+
+            // ========== 原始解析逻辑 —— 完全不变 ==========
+            OMSInvoiceResultDownloadData getInvoiceInfo = new OMSInvoiceResultDownloadData();
+            getInvoiceInfo.setDeptCode(deptCode);
+            getInvoiceInfo.setOrderno(redInvOrderNo);
+            getInvoiceInfo.setIsDetail("1");
+            String jsonInvoiceInfoStr = JSON.toJSONString(getInvoiceInfo);
+            String base64Str = Base64.getEncoder().encodeToString(jsonInvoiceInfoStr.getBytes(StandardCharsets.UTF_8));
+
+            OMSAccessTokenInfo invoiceDownInfo = new OMSAccessTokenInfo();
+            invoiceDownInfo.setAppId(appId);
+            invoiceDownInfo.setAppKey(appKey);
+            invoiceDownInfo.setExchangeId(UUID.randomUUID().toString());
+            invoiceDownInfo.setAccessToken(accessToken);
+            invoiceDownInfo.setData(base64Str);
+            String jsonInvoiceDownStr = JSON.toJSONString(invoiceDownInfo);
+
+            String invoiceResultStr = HttpPostJsonUtil.doPost(
+                    omsUrl +"/prod-api/output/server/invoice/download",
+                    jsonInvoiceDownStr
+            );
+            System.out.println("[InvoiceDownloadTask] 发票解析结果:" + invoiceResultStr);
+
+            String invoceDownJsonStr = null;
+            boolean isSuccess = false;
+            if (StringUtils.isNotBlank(invoiceResultStr)) {
+                OMSAccessTokenInfo resultDownInfo = JSON.parseObject(invoiceResultStr, OMSAccessTokenInfo.class);
+                if (null != resultDownInfo.getResult() && "0000".equals(resultDownInfo.getResult().getCode())) {
+                    invoceDownJsonStr = OMSNationUtil.getDownFromBase64(resultDownInfo.getData().toString());
+                    if (StringUtils.isNotBlank(invoceDownJsonStr)) {
+                        OMSInvoiceDetailInfo invoiceInfo = JSONObject.parseObject(invoceDownJsonStr, OMSInvoiceDetailInfo.class);
+                        if (invoiceInfo != null) {
+                            //根据开票id查询数据并进行修改
+                            //需要将错误信息保存到对应开票信息表中
+                            FinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+                            if(null != workInvoice){
+                                if(StringUtils.isBlank(invoiceInfo.getOfdUrl())){
+                                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取", informType);
+                                }else{
+                                    //进行处理
+                                    isSuccess = workInvoiceService.updateOmsByIdOnDown(workInvoice, invoiceInfo, informType);
+                                }
+
+                            }else{
+
+                                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取", informType);
+                                return;
+                            }
+                        }
+                    }
+                }else if(null != resultDownInfo.getResult() && ("1002".equals(resultDownInfo.getResult().getCode())
+                        || "1001".equals(resultDownInfo.getResult().getCode())
+                        || "1005".equals(resultDownInfo.getResult().getCode())
+                        || "1006".equals(resultDownInfo.getResult().getCode())
+                        || "0001".equals(resultDownInfo.getResult().getCode())
+                        || "0002".equals(resultDownInfo.getResult().getCode())
+                        || "0003".equals(resultDownInfo.getResult().getCode())
+                        || "0004".equals(resultDownInfo.getResult().getCode())
+                        || "0005".equals(resultDownInfo.getResult().getCode())
+                        || "0006".equals(resultDownInfo.getResult().getCode())
+                        || "0007".equals(resultDownInfo.getResult().getCode())
+                        || "0008".equals(resultDownInfo.getResult().getCode())
+                )){
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,失败原因:" + resultDownInfo.getResult().getMessage(), informType);
+                }
+            }
+
+            // ========== 重试规则调整:去掉次数限制,改为23小时超时(由定时任务控制) ==========
+            if (isSuccess) {
+                System.out.println("✅ [InvoiceDownloadTask] 任务["+workInvoiceId+"]解析成功,删除Redis任务");
+                deleteInvoiceDownloadTaskFromRedis(workInvoiceId); // 成功删除Redis,保证数据一致
+                return;
+            } else {
+                System.out.println("❌ [InvoiceDownloadTask] 任务["+workInvoiceId+"]解析失败,5分钟后重试");
+                // 无需手动延迟,由定时任务每5分钟扫描即可
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ [InvoiceDownloadTask] 任务["+workInvoiceId+"]解析抛出异常,5分钟后重试");
+            // 异常不删除Redis,由定时任务继续重试
+        }
+    }
+
+    // ========== 辅助方法:删除Redis解析任务 ==========
+    private void deleteInvoiceDownloadTaskFromRedis(String orderno) {
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            jedis.delete("PG_OMS_red_invoice_download:" + orderno);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+
+}

+ 180 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/OMS/RedInvoiceRetryScheduledService.java

@@ -0,0 +1,180 @@
+package com.jeeplus.assess.invoice.service.OMS;
+
+import com.alibaba.fastjson.JSON;
+import com.jeeplus.assess.invoice.domain.OMS.OMSAccessTokenInfo;
+import com.jeeplus.assess.invoice.mapper.FinanceInvoiceMapper;
+import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
+import com.jeeplus.assess.invoice.utils.OMS.HttpPostJsonUtil;
+import com.jeeplus.common.redis.RedisUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static com.jeeplus.assess.invoice.config.FinanceOmsConfig.*;
+
+/**
+ * 用于开发票都用订单接口推送时使用,用来当返回值时9998时进行处理
+ * 核心规则:
+ * 1. 每2分钟执行一次定时任务(无时段限制)
+ * 2. 单个任务最多重试20次(每次间隔2分钟,由定时任务频率控制)
+ * 3. 仅判断Redis中是否有任务:有则执行,无则跳过
+ * 4. 9998错误纳入重试计数,次数用尽后按失败处理+删除Redis任务
+ */
+@Service
+@Transactional(readOnly = true)
+@Lazy(false) // 非懒加载,项目启动即初始化
+public class RedInvoiceRetryScheduledService {
+
+    // 原业务类中的核心方法需要能被定时任务调用,建议将executeOrderUploadRetry、handleInvoiceRetryAllFail等方法改为public,
+    // 或通过@Autowired注入业务类实例(假设你的业务类叫InvoiceOMSService)
+    @Autowired
+    private OMSDisposeService invoiceOMSService; // 替换为你实际的业务类名
+
+    @Autowired
+    private FinanceInvoiceMapper workInvoiceDao;
+
+    /**
+     * 定时任务:每5分钟执行一次(硬编码300000毫秒=5分钟)
+     * fixedRate:固定间隔执行,直接写死5分钟
+     */
+    @Transactional(readOnly = false)
+    public void handleInvoice9998RetryTask() {
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            // 1. 扫描所有9998重试任务的Redis Key
+            Set<String> redisKeys = jedis.keys("PG_OMS_invoice_retry_scheduled:*");
+            if (redisKeys == null || redisKeys.isEmpty()) {
+                System.out.println("ℹ️ 暂无9998重试任务需要处理");
+                return;
+            }
+            System.out.println("ℹ️ 发现" + redisKeys.size() + "个9998重试任务,开始处理");
+
+            // 2. 遍历每个任务Key,逐个处理
+            for (String redisKey : redisKeys) {
+                String orderNo = redisKey.split(":")[1];
+                try {
+                    // 3. 获取Redis中的任务数据
+                    Map<Object, Object> rawMap = jedis.hGetAll(redisKey);
+                    Map<String, String> taskMap = new HashMap<>();
+                    if (rawMap != null) {
+                        for (Map.Entry<Object, Object> entry : rawMap.entrySet()) {
+                            taskMap.put(
+                                    String.valueOf(entry.getKey()),
+                                    String.valueOf(entry.getValue())
+                            );
+                        }
+                    }
+                    String jsonInvoiceStr = taskMap.get("jsonInvoiceStr");
+                    String accessToken = taskMap.get("accessToken");
+                    String getKey = taskMap.get("getKey");
+                    String initiationType = taskMap.get("initiationType");
+                    String retryTimesStr = taskMap.get("retryTimes");
+                    String informType = taskMap.get("informType");
+                    String workInvoiceId = taskMap.get("workInvoiceId");
+
+                    //获取当前redis中的accessToken
+                    //如果有值,则使用当前redis中的accessToken,否则重新生成一个新的accessToken
+                    accessToken =(String) jedis.get("PGOMSAccessToken");
+
+                    if(StringUtils.isBlank(accessToken)){
+                        // 获取AccessToken 9998重试5次
+                        accessToken = invoiceOMSService.getOmsAccessTokenWithRetry(5, "accessToken", workInvoiceId, informType);
+                        if(StringUtils.isNotBlank(accessToken)){
+                            jedis.setEx("PGOMSAccessToken", accessToken,86400, TimeUnit.SECONDS);
+                            System.out.println("重新获取token成功,存入Redis");
+                        } else {
+                            accessToken = "";
+                            System.out.println("获取token失败");
+                            return;
+                        }
+                    }
+
+                    // 校验必要参数
+                    if (StringUtils.isBlank(jsonInvoiceStr) || StringUtils.isBlank(orderNo) || StringUtils.isBlank(retryTimesStr)) {
+                        System.err.println("❌ 订单" + orderNo + "任务数据不完整,删除无效任务");
+                        jedis.delete(redisKey);
+                        continue;
+                    }
+
+                    // 4. 核心:重试次数扣减(先减1,再判断逻辑)
+                    int remainRetryTimes = Integer.parseInt(retryTimesStr);
+                    int newRemainTimes = remainRetryTimes - 1; // 每次处理减1
+
+                    // 5. 判断是否耗尽次数(第20次,newRemainTimes=0)
+                    if (newRemainTimes <= 0) {
+                        System.err.println("❌ 订单" + orderNo + "重试次数已耗尽(累计20次),执行兜底逻辑handleInvoiceRetryAllFail");
+                        invoiceOMSService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "重试次数已耗尽(累计20次),需要重新发起申请", informType);
+                        jedis.delete(redisKey); // 删除Redis任务,不再重试
+                        continue;
+                    }
+
+                    // 6. 次数未耗尽:更新Redis中的重试次数,再调用接口重试
+                    jedis.hPut(redisKey, "retryTimes", String.valueOf(newRemainTimes));
+                    System.out.println("ℹ️ 订单" + orderNo + "剩余重试次数更新为:" + newRemainTimes);
+
+                    FinanceInvoiceDTO workInvoice = workInvoiceDao.queryById(workInvoiceId);
+                    if(null != workInvoice){
+                        // 7. 调用OMS订单上传接口重试
+                        String newResultStr = HttpPostJsonUtil.doPost(
+                                omsUrl +"/prod-api/output/server/order/upload",
+                                jsonInvoiceStr
+                        );
+                        System.out.println("✅ 订单" + orderNo + "9998重试调用接口返回:" + newResultStr);
+
+                        workInvoice.setOrderForGoodsResultStr(newResultStr);
+
+                        workInvoiceDao.updateOrderForGoodsResultStrById(workInvoice);
+
+
+                        // 8. 根据接口返回码处理Redis任务
+                        OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(newResultStr, OMSAccessTokenInfo.class);
+                        if (resultTokenInfo != null && resultTokenInfo.getResult() != null) {
+                            // 9. 调用核心重试逻辑处理返回结果
+                            Map<String, Object> resultMap = new HashMap<>();
+                            invoiceOMSService.executeOrderUploadRetry(
+                                    workInvoiceId,
+                                    newRemainTimes, // 传入扣减后的剩余次数
+                                    newResultStr,
+                                    jsonInvoiceStr,
+                                    accessToken,
+                                    resultMap,
+                                    getKey,
+                                    initiationType
+                                    , informType
+                            );
+                            jedis.delete(redisKey);
+                        }/* else {
+                        // 解析返回结果失败:执行兜底+删除任务
+                        System.err.println("❌ 订单" + orderNo + "9998重试返回结果解析失败,执行兜底逻辑");
+                        invoiceOMSService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "9998重试返回结果解析失败");
+                        jedis.del(redisKey);
+                    }*/
+                    }
+
+
+
+
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    System.err.println("❌ 处理订单" + orderNo + "9998重试任务异常:" + e.getMessage());
+                    // 异常时不删除任务,下次继续重试(避免因临时异常丢失任务)
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ 9998重试定时任务执行异常:" + e.getMessage());
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+}

+ 421 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/OMS/RedInvoiceScheduledService.java

@@ -0,0 +1,421 @@
+package com.jeeplus.assess.invoice.service.OMS;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.jeeplus.assess.invoice.domain.OMS.OMSAccessTokenInfo;
+import com.jeeplus.assess.invoice.domain.OMS.fastRed.OMSRedInvoiceConfirmResponse;
+import com.jeeplus.assess.invoice.mapper.FinanceInvoiceMapper;
+import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
+import com.jeeplus.assess.invoice.utils.OMS.HttpPostJsonUtil;
+import com.jeeplus.assess.invoice.utils.OMS.OMSNationUtil;
+import com.jeeplus.common.redis.RedisUtils;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import static com.jeeplus.assess.invoice.config.FinanceOmsConfig.*;
+
+/**
+ * 红字确认单查询接口回调
+ * 用于发票红冲 或判定双方是否确认
+ */
+@Service
+@Transactional(readOnly = true)
+@Lazy
+public class RedInvoiceScheduledService {
+
+    //private static final String appId = Global.getConfig("omsAppId");
+    //private static final String appKey = Global.getConfig("omsAppKey");
+    //private static final String deptCode = Global.getConfig("omsDeptCode");
+
+    @Autowired
+    private OMSDisposeService omsDisposeService;
+    @Autowired
+    private FinanceInvoiceMapper workInvoiceDao;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Transactional(readOnly = false)
+    public void processAllRedInvoiceTasks() {
+        // 时间窗口校验
+        Calendar now = Calendar.getInstance();
+        int hour = now.get(Calendar.HOUR_OF_DAY);
+        if (hour < 8 || hour >= 20) {
+            System.out.println("[定时任务] 非业务时段,跳过");
+            return;
+        }
+
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            Set<String> taskKeys = jedis.keys("PG_OMS_red_invoice_task:*");
+            if (taskKeys.isEmpty()) {
+                System.out.println("[定时任务] 暂无待处理任务");
+                return;
+            }
+
+            // 遍历处理所有Redis任务
+            for (String taskKey : taskKeys) {
+                String applyNo = taskKey.replace("PG_OMS_red_invoice_task:", "");
+                // 读取Redis参数
+                String remainRetryTimesStr =(String) jedis.hGet(taskKey, "remainRetryTimes");
+                String jsonInvoicResultStr = (String) jedis.hGet(taskKey, "jsonInvoicResultStr");
+                String accessToken =(String)  jedis.hGet(taskKey, "accessToken");
+                String startTimeStr = (String) jedis.hGet(taskKey, "startTime");
+                String informType = (String) jedis.hGet(taskKey, "informType");
+                String workInvoiceId = (String) jedis.hGet(taskKey, "workInvoiceId");
+
+                //获取当前redis中的accessToken
+                //如果有值,则使用当前redis中的accessToken,否则重新生成一个新的accessToken
+                accessToken =(String)  jedis.get("PGOMSAccessToken");
+
+                if(StringUtils.isBlank(accessToken)){
+                    // 获取AccessToken 9998重试5次
+                    accessToken = omsDisposeService.getOmsAccessTokenWithRetry(5, "accessToken", workInvoiceId, informType);
+                    if(StringUtils.isNotBlank(accessToken)){
+                        jedis.setEx("PGOMSAccessToken", accessToken,86400, TimeUnit.SECONDS);
+                        System.out.println("重新获取token成功,存入Redis");
+                    } else {
+                        accessToken = "";
+                        System.out.println("获取token失败");
+                        return;
+                    }
+                }
+
+
+                // 参数校验
+                if (StringUtils.isAnyBlank(remainRetryTimesStr, accessToken, startTimeStr)) {
+                    jedis.delete(taskKey);
+                    continue;
+                }
+
+                // 转换参数并执行任务
+                int remainRetryTimes = Integer.parseInt(remainRetryTimesStr);
+                long startTime = Long.parseLong(startTimeStr);
+                executeRedInvoiceTask(applyNo, remainRetryTimes, jsonInvoicResultStr, accessToken, startTime, workInvoiceId, informType);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+
+
+    // ========== 1. 封装所有需要的业务逻辑(独立执行,不依赖原有业务类) ==========
+
+    /**
+     *
+     * @param applyNo       发票id
+     * @param remainRetryTimes  允许回调的次数,这里暂时未用到,仅作为保留参数
+     * @param jsonInvoicResultStr   全场景红冲订单提交接口返回值
+     * @param accessToken
+     * @param startTime     这个任务开始时间。限制:开始3天内如果双方没有全部确认,则本次红冲失败
+     * @param workInvoiceId 发票id
+     */
+    @Transactional(readOnly = false)
+    public void executeRedInvoiceTask(String applyNo, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, long startTime, String workInvoiceId, String informType) {
+        try {
+            FinanceInvoiceDTO workInvoice = workInvoiceDao.queryById(workInvoiceId);
+
+            if (null != workInvoice) {
+                // 步骤1:3天超时判断
+                if (System.currentTimeMillis() - startTime > 259200000L) {
+                    System.err.println("❌ 任务["+applyNo+"]超时,触发兜底逻辑");
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "", informType);
+                    deleteRedInvoiceTaskFromRedis(applyNo);
+                    return;
+                }
+
+                // 步骤2:首次执行(有jsonInvoicResultStr)→ 解析结果,触发查询
+                if (StringUtils.isNotBlank(jsonInvoicResultStr)) {
+                    OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(jsonInvoicResultStr, OMSAccessTokenInfo.class);
+                    if (null == resultTokenInfo || null == resultTokenInfo.getResult() || !"0000".equals(resultTokenInfo.getResult().getCode())) {
+                        System.err.println("❌ 任务["+applyNo+"]解析失败,触发兜底");
+                        omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "", informType);
+                        deleteRedInvoiceTaskFromRedis(applyNo);
+                        return;
+                    }
+                    // 解析成功,调用查询接口
+                    callRedInvoiceConfirmQuery(remainRetryTimes, accessToken, applyNo, startTime, workInvoiceId, workInvoice, informType);
+                    return;
+                }
+
+                // 步骤3:后续重试(无jsonInvoicResultStr)→ 直接调用查询接口
+                callRedInvoiceConfirmQuery(remainRetryTimes, accessToken, applyNo, startTime, workInvoiceId, workInvoice, informType);
+            }
+
+
+
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            // 异常后重试次数-1,重新存入Redis
+            if (remainRetryTimes > 1) {
+                saveRedInvoiceTaskToRedis(remainRetryTimes - 1, jsonInvoicResultStr, accessToken, applyNo, startTime, informType);
+            } else {
+                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "红字确认单查询接口回调失败,请重新发起", informType);
+                deleteRedInvoiceTaskFromRedis(applyNo);
+            }
+        }
+    }
+
+    /**
+     * 调用红字确认单查询接口(核心业务逻辑)
+     * @param remainRetryTimes  允许回调的次数,这里暂时未用到,仅作为保留参数
+     * @param accessToken
+     * @param applyNo   发票id
+     * @param startTime 这个任务开始时间。限制:开始3天内如果双方没有全部确认,则本次红冲失败
+     * @param workInvoiceId 发票id
+     */
+    @Transactional(readOnly = false)
+    public void callRedInvoiceConfirmQuery(int remainRetryTimes, String accessToken, String applyNo, long startTime, String workInvoiceId, FinanceInvoiceDTO workInvoice, String informType) {
+        try {
+            // 组装查询参数
+            OMSNationUtil util = new OMSNationUtil();
+            String queryData = util.neatenAllScenarioRedInvoiceConfirmQueryData(applyNo);
+            OMSAccessTokenInfo invoiceDownInfo = new OMSAccessTokenInfo();
+            invoiceDownInfo.setAppId(appId);
+            invoiceDownInfo.setAppKey(appKey);
+            invoiceDownInfo.setExchangeId(UUID.randomUUID().toString());
+            invoiceDownInfo.setAccessToken(accessToken);
+            invoiceDownInfo.setData(queryData);
+            String jsonInvoiceDownStr = JSON.toJSONString(invoiceDownInfo);
+
+            // 调用查询接口(红字确认单接口,即红冲的第3步)
+            String invoiceResultStr = HttpPostJsonUtil.doPost(omsUrl +"/prod-api/output/server/redApply/query", jsonInvoiceDownStr);
+            if (StringUtils.isBlank(invoiceResultStr)) {
+                throw new RuntimeException("查询接口返回空");
+            }
+
+            // 解析查询结果
+            OMSAccessTokenInfo resultDownInfo = JSON.parseObject(invoiceResultStr, OMSAccessTokenInfo.class);
+            if (null == resultDownInfo.getResult() || !"0000".equals(resultDownInfo.getResult().getCode())) {
+                throw new RuntimeException("查询接口返回错误码:" + (resultDownInfo.getResult() != null ? resultDownInfo.getResult().getCode() : "NULL"));
+            }
+            //对 invoiceResultStr 这个调用的结果进行保存,方便redis崩溃重新处理
+            workInvoice.setConfirmationSlipResultStr(invoiceResultStr);
+            workInvoiceDao.updateConfirmationSlipResultStr(workInvoice);
+
+
+            String invoceDownJsonStr = OMSNationUtil.extractFromBase64OnClassStr(resultDownInfo.getData().toString());
+            OMSRedInvoiceConfirmResponse invoiceInfo = JSONObject.parseObject(invoceDownJsonStr, OMSRedInvoiceConfirmResponse.class);
+            String confirmStatus = invoiceInfo.getConfirmStatus();
+
+
+            System.out.println("invoiceInfo的值是:"+ invoiceInfo);
+            System.out.println("开票确认单接口查询,返回状态值是:"+ invoiceInfo.getConfirmStatus());
+            System.out.println("开票确认单接口查询,返回失败原因是:"+ invoiceInfo.getFailCause());
+            System.out.println("开票确认单接口查询,返回开票状态是:"+ invoiceInfo.getMakeStatus());
+            System.out.println("开票确认单接口查询,返回红字发票订单号是:"+ invoiceInfo.getRedInvOrderNo());
+            // 你原有注释和代码:判断开票接口查询返回值,
+            // 如果是01、04 则表示可直接通过
+            // 如果是02、03、15 则需要每30秒重新访问一次 查询红字确认单查询接口 。理论上应该持续3天(即双方确认最终时间节点)
+            // 如果是其他的,直接告诉发起人 让其确认后重新发起
+
+            // 你原有注释和代码:01 无需确认
+            // 02 销方录入待购方确认
+            // 03 购方录入待销方确认
+            // 04 购销双方已确认
+            // 05 作废(销方录入购方否认)
+            // 06 作废(购方录入销方否认)
+            // 07 作废(超 72 小时未确认)
+            // 08 作废(发起方已撤销)
+            // 09 作废(确认后撤销)
+            // 10 作废(异常凭证)
+            // 15  申请中
+            // 16  申请失败
+            // 判断状态,处理不同分支
+
+            switch (confirmStatus) {
+
+                case "01":
+                case "04":
+                    // 访问成功后的后续方法
+                    // 整体默认是自动开票
+                    // 判定 开票状态是否是已开票(03)
+                    if ("3".equals(invoiceInfo.getMakeStatus())) {
+
+                        String redInvOrderNo = invoiceInfo.getRedInvOrderNo();
+                        // 存入Redis,由InvoiceDownloadTask接管
+                        saveInvoiceDownloadTaskToRedis(accessToken, redInvOrderNo, workInvoiceId, informType);
+                        System.out.println("✅ 解析开票数据任务["+redInvOrderNo+"]已存入Redis,由InvoiceDownloadTask接管重试");
+
+                        deleteRedInvoiceTaskFromRedis(applyNo); // 成功后删除Redis
+                    } else if ("1".equals(invoiceInfo.getMakeStatus()) || "4".equals(invoiceInfo.getMakeStatus())) {
+                        // 开票中,重新存入Redis等待下次重试
+                        saveRedInvoiceTaskToRedis(remainRetryTimes, "", accessToken, applyNo, startTime, informType);
+                    } else {
+                        //到这的 应该返回值就是2 了  表示 开票失败
+                        omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "红冲开票失败,请确认后重新开票", informType);
+                        deleteRedInvoiceTaskFromRedis(applyNo);
+                    }
+                    break;
+                case "02":
+                case "03":
+                case "15":
+                    // 待确认,重新存入Redis等待下次重试
+                    //到这 说明 对方应该是没有进行发票确认,所以没能进行开票成功,需要对方进行确认
+                    saveRedInvoiceTaskToRedis(remainRetryTimes, "", accessToken, applyNo, startTime, informType);
+                    break;
+
+                case "05":
+                case "06":
+                case "07":
+                case "08":
+                case "09":
+                case "10":
+                case "16":
+                    // 终止本次红冲操作,通知发起人确认后重新处理
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "本次红冲的开票作废或失败,请确认后重新开票", informType);
+                    // 失败后删除Redis任务
+                    deleteRedInvoiceTaskFromRedis(applyNo);
+                    break;
+                default:
+                    System.out.println("开票确认单接口查询,返回状态值是:"+ invoiceInfo.getConfirmStatus() + "。稍后重试");
+                    break;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            // 接口调用失败,重试次数-1后重新存入Redis
+            if (remainRetryTimes > 1) {
+                saveRedInvoiceTaskToRedis(remainRetryTimes - 1, "", accessToken, applyNo, startTime, informType);
+            } else {
+                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "红冲开票失败,请确认后重新开票", informType);
+                deleteRedInvoiceTaskFromRedis(applyNo);
+            }
+        }
+    }
+
+    /**
+     * 存入Redis(定时任务内部使用)
+     */
+    @Transactional(readOnly = false)
+    public void saveRedInvoiceTaskToRedis(int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime, String informType) {
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            String redisKey = "PG_OMS_red_invoice_task:" + applyNo;
+            jedis.hPut(redisKey, "remainRetryTimes", String.valueOf(remainRetryTimes));
+            jedis.hPut(redisKey, "jsonInvoicResultStr", jsonInvoicResultStr);
+            jedis.hPut(redisKey, "accessToken", accessToken);
+            jedis.hPut(redisKey, "applyNo", applyNo);
+            jedis.hPut(redisKey, "informType", informType);
+            jedis.hPut(redisKey, "startTime", String.valueOf(startTime));
+            jedis.expire(redisKey, 259200 + 3600);
+
+            // ========== 追加:Redis数据转JSON,持久化到发票表(兜底逻辑) ==========
+            long redisStoreTime = System.currentTimeMillis() / 1000; // 秒级存储时间,用于后期回滚
+            // 提取Redis中startTime的字符串值,Map和Redis共用,保证数据完全一致
+            String startTimeStr = String.valueOf(startTime);
+            // 封装Map:包含Redis所有字段 + redisKey/过期时间/存储时间(保留redisKey)
+            Map<String, Object> redInvoiceMap = new HashMap<>();
+            // Redis哈希中所有原字段(与Redis完全一致)
+            redInvoiceMap.put("remainRetryTimes", remainRetryTimes);
+            redInvoiceMap.put("jsonInvoicResultStr", jsonInvoicResultStr);
+            redInvoiceMap.put("accessToken", accessToken);
+            redInvoiceMap.put("applyNo", applyNo);
+            redInvoiceMap.put("informType", informType);
+            redInvoiceMap.put("startTime", startTimeStr);
+            // 额外兜底字段(保留redisKey,贴合你的要求)
+            redInvoiceMap.put("redisKey", redisKey);
+            redInvoiceMap.put("redisExpireSeconds", 259200 + 3600);
+            redInvoiceMap.put("redisStoreTime", redisStoreTime);
+
+            // Map转JSON字符串(单独捕获序列化异常,不影响Redis核心逻辑)
+            String redInvoiceJson = objectMapper.writeValueAsString(redInvoiceMap);
+
+            // 更新发票表(唯一标识为applyNo,与Redis一致)
+            FinanceInvoiceDTO workInvoice = new FinanceInvoiceDTO();
+            workInvoice.setId(applyNo); // 关键:用applyNo作为更新ID,与你的唯一标识一致
+            workInvoice.setRedInvoiceJson(redInvoiceJson);
+            // 处理DAO更新,单独捕获异常,不影响核心逻辑
+            workInvoiceDao.updateRedInvoiceJsonByWorkInvoiceId(workInvoice);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+
+    /**
+     * 删除Redis任务
+     */
+    private void deleteRedInvoiceTaskFromRedis(String applyNo) {
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            jedis.delete("PG_OMS_red_invoice_task:" + applyNo);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+
+
+    /**
+     * 开票成功,需要进行下载解析的开票信息存储到redis中,进行下一步处理
+     * @param accessToken
+     * @param workInvoiceId 开票id
+     */
+    @Transactional(readOnly = false)
+    public void saveInvoiceDownloadTaskToRedis(String accessToken, String redInvOrderNo, String workInvoiceId, String informType) {
+        RedisUtils jedis = null;
+        try {
+            jedis = RedisUtils.getInstance();
+            String redisKey = "PG_OMS_red_invoice_download:" + workInvoiceId;
+            jedis.hPut(redisKey, "accessToken", accessToken);
+            jedis.hPut(redisKey, "redInvOrderNo", redInvOrderNo);
+            jedis.hPut(redisKey, "workInvoiceId", workInvoiceId);
+            jedis.hPut(redisKey, "informType", informType);
+            jedis.hPut(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
+            jedis.expire(redisKey, 86400); // 1天过期
+
+            // ========== 追加:Redis数据转JSON → 持久化到发票表(兜底逻辑) ==========
+            long redisStoreTime = System.currentTimeMillis() / 1000; // 秒级存储时间,用于后期回滚
+            // 封装Map:Redis所有原字段 + redisKey/过期时间/存储时间(保留redisKey)
+            Map<String, Object> downloadTaskMap = new HashMap<>(8);
+            // Redis哈希中5个原字段(与Redis键名/值完全一致,含新增的redInvOrderNo)
+            downloadTaskMap.put("accessToken", accessToken);
+            downloadTaskMap.put("redInvOrderNo", redInvOrderNo);
+            downloadTaskMap.put("workInvoiceId", workInvoiceId);
+            downloadTaskMap.put("informType", informType);
+            downloadTaskMap.put("firstExecTime", String.valueOf(System.currentTimeMillis()));
+            // 额外兜底字段(保留redisKey,后期回滚直接取用)
+            downloadTaskMap.put("redisKey", redisKey);
+            downloadTaskMap.put("redisExpireSeconds", 86400);
+            downloadTaskMap.put("redisStoreTime", redisStoreTime);
+
+            // Map转JSON字符串(单独捕获序列化异常,不影响Redis核心逻辑)
+            String downloadTaskJson = objectMapper.writeValueAsString(downloadTaskMap);
+
+            // 更新发票表(以workInvoiceId为唯一标识,与Redis一致)
+            FinanceInvoiceDTO workInvoice = new FinanceInvoiceDTO();
+            workInvoice.setId(workInvoiceId); // 关键:与redisKey的拼接标识一致
+            workInvoice.setRedDownloadInvoiceJson(downloadTaskJson);
+            workInvoiceDao.updateRedInvoiceJsonByWorkInvoiceId(workInvoice);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) {
+                //jedis.close();
+            }
+        }
+    }
+}

+ 23 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/service/dto/FinanceInvoiceDTO.java

@@ -332,6 +332,29 @@ public class FinanceInvoiceDTO extends BaseDTO {
     private String redFlushReason;	//红冲原因
     private String originalAccount;	//原发票金额
 
+    //OMS新增字段
+    private String goodsTaxno;		// 税收分类编码
+
+    private String omsAccessTokenError; //开具发票时,获取token失败报错信息保存
+    private String omsAccessToken; //开具发票时,获取token失败报错信息保存
+    private String omsErrorMessage; //开具发票失败时返回的错误信息
+
+    private String orderForGoodsResultStr; //订单提交接口返回值(蓝票的第2步。红票的第2步)
+    private String confirmationSlipResultStr; //红字确认单(红冲第3步)
+
+    private String omsEinVno; //对应发票的发票号
+    private String omsOfdUrl; //对应发票的ofdUrl
+    private String omsPdfUrl; //对应发票的pdfUrl
+    private String omsXmlUrl; //对应发票的xmlUrl
+    private String OmsAttachmentUrl; //对应发票的xmlUrl
+
+    private String redInvoiceJson;//将对应的红冲redis中数据进行存储到数据库中,放置redis数据崩溃丢失
+    private String blueDownloadInvoiceJson;//将对应的蓝票redis中数据进行存储到数据库中,放置redis数据崩溃丢失
+    private String retryInvoiceJson;//将对应的9998错误redis中数据进行存储到数据库中,放置redis数据崩溃丢失
+    private String redDownloadInvoiceJson;//将对应的红票redis中数据进行存储到数据库中,放置redis数据崩溃丢失
+
+    private List<WorkAttachmentInfo> invoiceList;
+
 
     private static final long serialVersionUID = 1L;
 }

+ 50 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/thread/ApprovalThread.java

@@ -0,0 +1,50 @@
+package com.jeeplus.assess.invoice.thread;
+
+
+
+import com.jeeplus.assess.invoice.service.OMS.OMSDisposeService;
+import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
+import com.jeeplus.assess.invoice.utils.OMS.SpringContextHolderAccess;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+public class ApprovalThread extends Thread {
+
+    private OMSDisposeService omsDisposeService = SpringContextHolderAccess.getBean(OMSDisposeService.class);
+    private FinanceInvoiceDTO workInvoice;
+    private String informType;
+
+
+    public ApprovalThread(FinanceInvoiceDTO workInvoice, String informType)
+    {
+        this.workInvoice = workInvoice;
+        this.informType = informType;
+    }
+
+    public void run(){
+        String disposeResult = "";
+        Map<String, Object> map = new HashMap<String, Object>();
+        try {
+            //进行签章调用
+            //根据项目id查询是否已经提交过了审定单签章
+            if(null == workInvoice){
+                disposeResult = "查询不到该开票信息";
+                omsDisposeService.handleInvoiceRetryAllFail("","",disposeResult, informType);
+            }
+            if(null!= workInvoice.getOmsOfdUrl()){
+                disposeResult = "该发票已经开过票";
+                omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult, informType);
+            }
+            omsDisposeService.doInvoiceBusiness(map,workInvoice.getId(), informType);
+
+        }catch (Exception e){
+            disposeResult = e.getMessage();
+            omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult, informType);
+        }finally {
+
+        }
+
+    }
+}

+ 59 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/thread/RedApprovalThread.java

@@ -0,0 +1,59 @@
+package com.jeeplus.assess.invoice.thread;
+
+import com.jeeplus.assess.invoice.service.OMS.OMSDisposeService;
+import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
+import com.jeeplus.assess.invoice.utils.OMS.SpringContextHolderAccess;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author: 徐滕
+ * @create: 2021-12-07 16:36
+ **/
+public class RedApprovalThread extends Thread {
+
+    private OMSDisposeService omsDisposeService = SpringContextHolderAccess.getBean(OMSDisposeService.class);
+    private FinanceInvoiceDTO workInvoice;
+    String originalInvno;
+    String informType;
+
+
+    public RedApprovalThread(FinanceInvoiceDTO workInvoice, String originalInvno, String informType)
+    {
+        this.workInvoice = workInvoice;
+        this.originalInvno = originalInvno;
+        this.informType = informType;
+    }
+
+    public void run(){
+        String disposeResult = "";
+        Map<String, Object> map = new HashMap<String, Object>();
+        try {
+            //进行签章调用
+            //根据项目id查询是否已经提交过了审定单签章
+            if(null == workInvoice){
+                disposeResult = "查询不到该开票信息";
+                omsDisposeService.handleInvoiceRetryAllFail("","",disposeResult, informType);
+            }
+            if(StringUtils.isNotBlank(workInvoice.getOmsOfdUrl())){
+                disposeResult = "该发票已经开过票";
+                omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult, informType);
+            }
+            if(StringUtils.isBlank(workInvoice.getRedInvoiceRelevancyId())){
+                disposeResult = "未找到该发票关联需要开红字票的发票";
+                omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult, informType);
+            }
+            omsDisposeService.doAllScenarioRedInvoiceBusiness(map,workInvoice.getId(), workInvoice.getRedInvoiceRelevancyId(), originalInvno, informType);
+
+        }catch (Exception e){
+            disposeResult = e.getMessage();
+            omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult, informType);
+
+        }finally {
+
+        }
+
+    }
+}

+ 268 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/FileHandlingUtil.java

@@ -0,0 +1,268 @@
+package com.jeeplus.assess.invoice.utils.OMS;
+
+
+import cn.hutool.extra.spring.SpringUtil;
+import com.jeeplus.sys.feign.IWorkAttachmentApi;
+import com.jeeplus.utils.StringUtils;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static com.jeeplus.assess.invoice.config.FinanceOmsConfig.*;
+
+public class FileHandlingUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(FileHandlingUtil.class);
+
+
+    /**
+     * 将远程URL文件下载到指定系统目录,并支持自定义重命名(内置异常处理,无返回类)
+     * 基础路径规则:
+     * - Windows:D:/attachment-file/
+     * - Linux/Mac:/attachment-file/
+     * @param remoteFileUrl  远程文件的URL地址(必填)
+     * @param newFileName    重命名后的文件名(支持3种传法):
+     *                       1. 传完整文件名(如"invoice_20260127_123456.pdf")→ 直接使用
+     *                       2. 传前缀(如"invoice_123456")→ 自动补原后缀
+     *                       3. 传null/空 → 自动生成UUID+原后缀
+     * @return String 成功返回本地完整路径,失败返回null
+     */
+    public static String downloadFileToSpecifiedPath(String remoteFileUrl, String newFileName) {
+        // ===================== 1. 前置参数校验 =====================
+        if (remoteFileUrl == null || remoteFileUrl.trim().isEmpty()) {
+            log.error("远程文件URL不能为空");
+            return null;
+        }
+
+        // ===================== 2. 内部判定系统基础路径 =====================
+        String basePath;
+        if (System.getProperty("os.name").toLowerCase().contains("win")) {
+            basePath = "D:/attachment-file/";
+        } else {
+            basePath = "/attachment-file/";
+        }
+        if (!basePath.endsWith(File.separator)) {
+            basePath += File.separator;
+        }
+
+        // ===================== 3. 处理重命名逻辑 =====================
+        String originalSuffix = getFileSuffixFromUrl(remoteFileUrl);
+        String finalFileName;
+
+        if (newFileName == null || newFileName.trim().isEmpty()) {
+            finalFileName = UUID.randomUUID().toString() + originalSuffix;
+        } else if (newFileName.contains(".")) {
+            finalFileName = newFileName.trim();
+        } else {
+            finalFileName = newFileName.trim() + originalSuffix;
+        }
+
+        // 拼接完整路径
+        String fullLocalPath = basePath + finalFileName;
+        File localFile = new File(fullLocalPath);
+
+        // ===================== 4. 核心下载逻辑(内置try-catch) =====================
+        OkHttpClient okHttpClient = new OkHttpClient.Builder()
+                .connectTimeout(30, TimeUnit.SECONDS)
+                .readTimeout(60, TimeUnit.SECONDS)
+                .build();
+
+        Request request = new Request.Builder().url(remoteFileUrl).build();
+
+        try {
+            // 自动创建基础目录
+            File parentDir = localFile.getParentFile();
+            if (!parentDir.exists()) {
+                boolean mkdirsSuccess = parentDir.mkdirs();
+                if (!mkdirsSuccess) {
+                    log.error("创建基础目录失败:{}", parentDir.getAbsolutePath());
+                    return null;
+                }
+                log.info("系统基础目录创建成功:{}", parentDir.getAbsolutePath());
+            }
+
+            // 发起请求下载文件
+            try (Response response = okHttpClient.newCall(request).execute();
+                 InputStream inputStream = response.body().byteStream();
+                 FileOutputStream outputStream = new FileOutputStream(localFile)) {
+
+                if (!response.isSuccessful()) {
+                    log.error("下载文件失败,URL:{},响应码:{}", remoteFileUrl, response.code());
+                    // 清理临时文件
+                    if (localFile.exists()) localFile.delete();
+                    return null;
+                }
+
+                // 写入文件
+                byte[] buffer = new byte[1024 * 8];
+                int len;
+                while ((len = inputStream.read(buffer)) != -1) {
+                    outputStream.write(buffer, 0, len);
+                }
+
+                log.info("文件下载并完成重命名!远程URL:{},本地保存路径:{}", remoteFileUrl, fullLocalPath);
+                // 下载成功,返回本地路径
+                return fullLocalPath;
+
+            }
+        } catch (IOException e) {
+            // 捕获所有IO异常,记录日志后返回null
+            log.error("文件下载失败:{},原因:{}", remoteFileUrl, e.getMessage(), e);
+            // 清理临时文件
+            if (localFile.exists()) localFile.delete();
+            return null;
+        }
+    }
+
+    /**
+     * 辅助方法:从URL提取文件后缀
+     */
+    private static String getFileSuffixFromUrl(String url) {
+        if (url == null || !url.contains(".")) {
+            return ".bin";
+        }
+        String suffix = url.substring(url.lastIndexOf("."));
+        if (suffix.contains("?") || suffix.contains("#")) {
+            suffix = suffix.substring(0, suffix.indexOf("?"));
+        }
+        return suffix;
+    }
+
+
+
+
+
+    private static String datePath(){
+
+        Calendar date = Calendar.getInstance();
+        String year = String.valueOf(date.get(Calendar.YEAR));
+        //String month = String.valueOf(date.get(Calendar.MONTH)+1);
+        //String day = String.valueOf(date.get(Calendar.DAY_OF_MONTH));
+        //String path = "/"+year+"/"+month+"/"+day;
+        String path = "/"+year;
+        return path;
+    }
+
+
+    /**
+     * 本地文件上传到阿里云OSS(文件大小用File.length()获取,返回Map包含OSS路径+文件大小)
+     * @param filePath 本地文件的完整路径(必填,如:D:/attachment-file/KP-2026-0054.pdf)
+     * @param fileStorageLocation 文件存储位置(自定义子目录)
+     * @return Map<String, Object> 结果说明:
+     *         - success:boolean,上传是否成功
+     *         - ossFilePath:String,成功时为OSS完整存储路径,失败时为null
+     *         - fileSize:Long,成功时为文件大小(字节,通过File.length()获取),失败时为0
+     *         - errorMsg:String,失败时为错误信息,成功时为null
+     */
+    public static Map<String, Object> fileUpload(String filePath, String fileStorageLocation) {
+        // 初始化返回Map
+        Map<String, Object> resultMap = new HashMap<>(4);
+        resultMap.put("success", false); // 默认失败
+        resultMap.put("ossFilePath", null);
+        resultMap.put("fileSize", 0L);
+        resultMap.put("errorMsg", null);
+
+        // ========== 1. 基础参数校验 ==========
+        if (StringUtils.isBlank(filePath)) {
+            String errorMsg = "文件上传失败:文件路径不能为空";
+            log.error(errorMsg);
+            resultMap.put("errorMsg", errorMsg);
+            return resultMap;
+        }
+        File localFile = new File(filePath);
+        if (!localFile.exists()) {
+            String errorMsg = "文件上传失败:文件不存在,路径:" + filePath;
+            log.error(errorMsg);
+            resultMap.put("errorMsg", errorMsg);
+            return resultMap;
+        }
+        if (!localFile.isFile()) {
+            String errorMsg = "文件上传失败:路径不是有效文件,路径:" + filePath;
+            log.error(errorMsg);
+            resultMap.put("errorMsg", errorMsg);
+            return resultMap;
+        }
+
+        // ========== 核心:文件大小直接用File.length()(即file.size)获取 ==========
+        long fileSize = localFile.length(); // 等价于你说的file.size,返回字节数
+        log.info("获取本地文件信息:文件名={},文件大小(file.size)={} 字节", localFile.getName(), fileSize);
+
+        // 获取本地文件原文件名(用于uploadFileSignatureOSS第三个参数)
+        String originalFileName = localFile.getName();
+        log.info("获取本地文件原文件名:{}", originalFileName);
+
+        InputStream fileInputStream = null;
+        try {
+            // ========== 2. 读取文件流 ==========
+            fileInputStream = new FileInputStream(localFile);
+            MultipartFile multipartFile = new MockMultipartFile(
+                    "file",
+                    originalFileName,
+                    null,
+                    fileInputStream
+            );
+            if (multipartFile.isEmpty()) {
+                String errorMsg = "文件上传失败:文件流为空,本地路径:" + filePath;
+                log.error(errorMsg);
+                resultMap.put("errorMsg", errorMsg);
+                return resultMap;
+            }
+
+            // ========== 3. 拼接OSS存储目录 ==========
+            String baseDir = directory.replaceFirst("/", "");
+            String realPath = baseDir + "/OMS_invoice" + datePath() + "/" + fileStorageLocation + "/";
+            if (!realPath.endsWith("/")) {
+                realPath += "/";
+            }
+
+            // ========== 4. 调用原有uploadFileSignatureOSS方法,第三个参数传原文件名 ==========
+            SpringUtil.getBean ( IWorkAttachmentApi.class ).uploadFileSignatureOSS(localFile.getPath(), realPath, originalFileName);
+
+            // ========== 5. 上传成功后删除本地文件 ==========
+            boolean deleteSuccess = localFile.delete();
+            if (deleteSuccess) {
+                log.info("本地文件删除成功!路径:{}", filePath);
+            } else {
+                log.warn("本地文件删除失败(文件可能已被占用)!路径:{}", filePath);
+                // 删除失败不影响返回结果,仅告警
+            }
+
+            // ========== 6. 上传成功:更新Map结果(包含file.size获取的文件大小) ==========
+            String ossFullFilePath = realPath + originalFileName;
+            resultMap.put("success", true);
+            resultMap.put("ossFilePath", ossFullFilePath);
+            resultMap.put("fileSize", fileSize); // 存入file.size获取的字节数
+            resultMap.put("errorMsg", null);
+            log.info("文件上传OSS完成!本地路径:{},OSS路径:{},文件大小:{} 字节",
+                    filePath, ossFullFilePath, fileSize);
+
+        } catch (IOException e) {
+            String errorMsg = "文件上传OSS失败:本地路径=" + filePath + ",异常原因=" + e.getMessage();
+            log.error(errorMsg, e);
+            resultMap.put("errorMsg", errorMsg);
+        } finally {
+            // ========== 7. 释放资源 ==========
+            if (fileInputStream != null) {
+                try {
+                    fileInputStream.close();
+                } catch (IOException e) {
+                    log.error("关闭本地文件流失败", e);
+                }
+            }
+        }
+
+        return resultMap;
+    }
+
+}

+ 63 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/FileUtil.java

@@ -0,0 +1,63 @@
+package com.jeeplus.assess.invoice.utils.OMS;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * 文件管理
+ * @author: 徐滕
+ * @version: 2023-12-29 09:54
+ */
+public class FileUtil {
+
+    /**
+     * 文件压缩
+     * @param sourceFolder
+     * @param zipFile
+     */
+    public static void zipFolder(String sourceFolder, String zipFile) {
+        try {
+            FileOutputStream fos = new FileOutputStream(zipFile);
+            ZipOutputStream zos = new ZipOutputStream(fos);
+
+            File folder = new File(sourceFolder);
+            addFolderToZip("", folder, zos);
+
+            zos.close();
+            fos.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static void addFolderToZip(String parentFolder, File folder, ZipOutputStream zos) throws IOException {
+        for (String fileName : folder.list()) {
+            if (parentFolder.equals("")) {
+                addFileToZip(folder.getName(), new File(folder, fileName), zos);
+            } else {
+                addFileToZip(parentFolder + "/" + folder.getName(), new File(folder, fileName), zos);
+            }
+        }
+    }
+
+    public static void addFileToZip(String path, File file, ZipOutputStream zos) throws IOException {
+        if (file.isDirectory()) {
+            addFolderToZip(path, file, zos);
+        } else {
+            byte[] buffer = new byte[1024];
+            FileInputStream fis = new FileInputStream(file);
+            zos.putNextEntry(new ZipEntry(path + "/" + file.getName()));
+            int length;
+            while ((length = fis.read(buffer)) > 0) {
+                zos.write(buffer, 0, length);
+            }
+            zos.closeEntry();
+            fis.close();
+        }
+    }
+
+}

+ 149 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/HttpPostJsonUtil.java

@@ -0,0 +1,149 @@
+package com.jeeplus.assess.invoice.utils.OMS;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+
+/**
+ * 纯原生POST请求工具类 - 仅支持JSON格式请求体 + UTF-8编码
+ * 无任何冗余代码、无第三方依赖、支持HTTP/HTTPS、异常兜底返回空JSON
+ */
+public class HttpPostJsonUtil {
+
+    /**
+     * 核心方法:发送POST请求
+     * @param requestUrl 接口请求地址 (HTTP/HTTPS均可)
+     * @param outputStr  JSON格式的请求参数字符串
+     * @return 接口返回的JSON字符串,异常则返回 {}
+     */
+    public static String doPost(String requestUrl, String outputStr) {
+        HttpURLConnection httpUrlConn = null;
+        try {
+            URL url = new URL(requestUrl);
+            httpUrlConn = (HttpURLConnection) url.openConnection();
+
+            // HTTPS 证书信任配置 (必须保留,兼容HTTPS请求)
+            if (httpUrlConn instanceof HttpsURLConnection) {
+                SSLContext sslContext = SSLContext.getInstance("TLS");
+                sslContext.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new SecureRandom());
+                ((HttpsURLConnection) httpUrlConn).setSSLSocketFactory(sslContext.getSocketFactory());
+                ((HttpsURLConnection) httpUrlConn).setHostnameVerifier((hostname, session) -> true);
+            }
+
+            // 核心配置
+            httpUrlConn.setRequestMethod("POST");
+            httpUrlConn.setDoOutput(true);
+            httpUrlConn.setDoInput(true);
+            httpUrlConn.setUseCaches(false);
+            httpUrlConn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
+
+            // 写入JSON参数 并 UTF-8编码
+            if (outputStr != null && !outputStr.trim().isEmpty()) {
+                OutputStream os = httpUrlConn.getOutputStream();
+                os.write(outputStr.getBytes("UTF-8"));
+                os.flush();
+                os.close();
+            }
+
+            // 读取返回结果 并 UTF-8解码
+            BufferedReader br = new BufferedReader(new InputStreamReader(httpUrlConn.getInputStream(), "UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = br.readLine()) != null) {
+                sb.append(line);
+            }
+            br.close();
+            return sb.toString();
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (httpUrlConn != null) {
+                httpUrlConn.disconnect();
+            }
+        }
+        return "{}";
+    }
+
+    /**
+     * 重载方法:带token请求头的POST请求 (你的业务必用,无冗余新增)
+     * @param requestUrl 接口地址
+     * @param outputStr   JSON参数
+     * @param token       令牌(accessToken/Authorization)
+     * @return 接口返回的JSON字符串
+     */
+    public static String doPost(String requestUrl, String outputStr, String token) {
+        HttpURLConnection httpUrlConn = null;
+        try {
+            URL url = new URL(requestUrl);
+            httpUrlConn = (HttpURLConnection) url.openConnection();
+
+            if (httpUrlConn instanceof HttpsURLConnection) {
+                SSLContext sslContext = SSLContext.getInstance("TLS");
+                sslContext.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new SecureRandom());
+                ((HttpsURLConnection) httpUrlConn).setSSLSocketFactory(sslContext.getSocketFactory());
+                ((HttpsURLConnection) httpUrlConn).setHostnameVerifier((hostname, session) -> true);
+            }
+
+            httpUrlConn.setRequestMethod("POST");
+            httpUrlConn.setDoOutput(true);
+            httpUrlConn.setDoInput(true);
+            httpUrlConn.setUseCaches(false);
+            httpUrlConn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
+            // 添加token请求头
+            if (token != null && !token.trim().isEmpty()) {
+                httpUrlConn.setRequestProperty("accessToken", token);
+                // 如果接口要求 Bearer 前缀,放开下面这行注释即可
+                // httpUrlConn.setRequestProperty("Authorization", "Bearer " + token);
+            }
+
+            if (outputStr != null && !outputStr.trim().isEmpty()) {
+                OutputStream os = httpUrlConn.getOutputStream();
+                os.write(outputStr.getBytes("UTF-8"));
+                os.flush();
+                os.close();
+            }
+
+            BufferedReader br = new BufferedReader(new InputStreamReader(httpUrlConn.getInputStream(), "UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = br.readLine()) != null) {
+                sb.append(line);
+            }
+            br.close();
+            return sb.toString();
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (httpUrlConn != null) {
+                httpUrlConn.disconnect();
+            }
+        }
+        return "{}";
+    }
+
+    /**
+     * 内部类:HTTPS证书信任管理器 (极简实现,无冗余代码)
+     */
+    private static class TrustAnyTrustManager implements X509TrustManager {
+        @Override
+        public void checkClientTrusted(X509Certificate[] chain, String authType) {}
+
+        @Override
+        public void checkServerTrusted(X509Certificate[] chain, String authType) {}
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[0];
+        }
+    }
+}

+ 395 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/OMSNationUtil.java

@@ -0,0 +1,395 @@
+package com.jeeplus.assess.invoice.utils.OMS;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.google.common.collect.Lists;
+import com.jeeplus.assess.invoice.domain.OMS.InvoiceOMSImportInfo;
+import com.jeeplus.assess.invoice.domain.OMS.OrderItem;
+import com.jeeplus.assess.invoice.domain.OMS.fastRed.OMSAllScenarioRedInvoiceInfo;
+import com.jeeplus.assess.invoice.domain.OMS.fastRed.OMSApplyItem;
+import com.jeeplus.assess.invoice.domain.OMS.fastRed.OMSFastRedInvoiceInfo;
+import com.jeeplus.assess.invoice.domain.OMS.fastRed.OMSRedInvoiceConfirmQueryRequest;
+import com.jeeplus.assess.invoice.domain.FinanceInvoiceTaxClassificationCode;
+import com.jeeplus.assess.invoice.service.dto.FinanceInvoiceDTO;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+
+import static com.jeeplus.assess.invoice.config.FinanceOmsConfig.*;
+
+
+@Component
+public class OMSNationUtil {
+
+    /**
+     * 从Base64编码的JSON响应中提取accessToken
+     */
+    public static String extractAccessTokenFromBase64(String getKey, String base64Str) {
+        if (base64Str == null || StringUtils.isBlank(base64Str)) {
+            return "";
+        }
+        // 1. 解码Base64得到JSON字符串
+        byte[] decodeBytes = Base64.getDecoder().decode(base64Str);
+        String jsonStr = new String(decodeBytes, StandardCharsets.UTF_8);
+        // 2. 解析JSON提取accessToken
+        JSONObject jsonObject = JSONObject.parseObject(jsonStr);
+        return jsonObject.getString(getKey);
+    }
+
+    /**
+     * 从Base64编码的JSON响应中提取accessToken
+     */
+    public static String extractFromBase64OnClassStr(String base64Str) {
+        if (base64Str == null ||StringUtils.isBlank(base64Str)) {
+            return "";
+        }
+        // 1. 解码Base64得到JSON字符串
+        byte[] decodeBytes = Base64.getDecoder().decode(base64Str);
+        String jsonStr = new String(decodeBytes, StandardCharsets.UTF_8);
+        // 2. 解析JSON提取accessToken
+        JSONObject jsonObject = JSONObject.parseObject(jsonStr);
+        return jsonObject.toJSONString();
+    }
+
+    // ========== 你的原方法(无需修改,保持不变) ==========
+    public static String getDownFromBase64(String base64Str) {
+        if (StringUtils.isBlank(base64Str)) {
+            return "";
+        }
+        byte[] decodeBytes = null;
+        String jsonStr = null;
+        try {
+            decodeBytes = Base64.getDecoder().decode(base64Str);
+            jsonStr = new String(decodeBytes, StandardCharsets.UTF_8);
+        } catch (IllegalArgumentException e) {
+            System.err.println("Base64解码失败,密文格式错误:" + e.getMessage());
+            return "";
+        }
+
+        try {
+            JSONArray jsonArray = JSONArray.parseArray(jsonStr);
+            if (jsonArray == null || jsonArray.size() == 0) {
+                System.err.println("解码后的JSON数组为空");
+                return "";
+            }
+            JSONObject jsonObject = jsonArray.getJSONObject(0);
+            if (jsonObject == null) {
+                System.err.println("JSON数组第一个元素不是JSONObject对象");
+                return "";
+            }
+            return jsonObject.toJSONString();
+        } catch (Exception e) {
+            System.err.println("JSON解析失败:" + e.getMessage());
+            e.printStackTrace();
+            return "";
+        }
+    }
+
+
+    //private static final String appId = Global.getConfig("omsAppId");
+    //private static final String appKey = Global.getConfig("omsAppKey");
+    //private static final String deptCode = Global.getConfig("omsDeptCode");
+    //private static final String sellerName = Global.getConfig("omsSellerName");
+    //private static final String sellerTaxno = Global.getConfig("omsSellerTaxno");
+    //private static final String bankName = Global.getConfig("omsBankName");
+    //private static final String bankAccount = Global.getConfig("omsBankAccount");
+
+
+
+    /**
+     * 开蓝票
+     * 整理数据明文
+     * @param workInvoiceId 开票信息id,最好是将数据整体传过来
+     * @Param workInvoice   需要开票的信息
+     * @return
+     */
+    public String neatenData(String workInvoiceId, FinanceInvoiceDTO workInvoice, FinanceInvoiceTaxClassificationCode financeInvoiceTaxClassificationCode) {
+        List<OrderItem> orderItems = Lists.newArrayList();
+
+        InvoiceOMSImportInfo omsImportInfo = new InvoiceOMSImportInfo();
+        omsImportInfo.setDeptCode(deptCode);
+        omsImportInfo.setOrderno(workInvoiceId);
+        omsImportInfo.setSellerName(sellerName);
+        omsImportInfo.setSellerTaxno(sellerTaxno); //销方纳税人识别号,必填
+        //生产环境
+        omsImportInfo.setSellerBank(bankName); //销售方开户行名称
+        omsImportInfo.setSellerBankaccount(bankAccount); //销售方开户行账号
+        //* 开票类型(此处固定是蓝字票)
+        //* 01:红字发票 02:蓝字发票,必填
+        omsImportInfo.setInvKind("02");
+
+
+        //* 发票类型
+        //* 01:数电票(增值税专用发票) 02:数电票(普通发票)等
+        if(workInvoice.getType().equals("1")){
+            omsImportInfo.setInvType("01");
+        }else{
+            omsImportInfo.setInvType("02");
+        }
+
+        //添加购买方信息
+        //测试环境
+        //omsImportInfo.setBuyerName("深圳市爱人人餐饮服务有限公司");
+        //omsImportInfo.setBuyerTaxno("500102203117204029");
+        //生产环境
+        omsImportInfo.setBuyerName(workInvoice.getBillingWorkplaceReal());    //购买方名称
+        omsImportInfo.setBuyerTaxno(workInvoice.getTaxpayerIdentificationNo());  //购买方信用代码
+        omsImportInfo.setBuyerAddr(workInvoice.getAddress()); //购买方地址
+        omsImportInfo.setBuyerPhone(workInvoice.getTelPhone()); //购买方电话
+        omsImportInfo.setBuyerBank(workInvoice.getOpenBank()); //购买方开户行名称
+        omsImportInfo.setBuyerBankaccount(workInvoice.getBankAccount()); //购买方开户银行账号
+
+        //omsImportInfo.setBuyerNationality(""); //购买方国籍/地区代码
+        //omsImportInfo.setBuyerIdcardno(""); //购买方证件号码
+        //omsImportInfo.setBuyerIdcartype(""); //购买方证件类型
+        //omsImportInfo.setBuyerCellphone(""); //购买方手机号
+        //omsImportInfo.setBuyerEmail(""); //购买方邮箱
+
+
+        //判定是个人开票 还是企业开票
+        if(workInvoice.getBillingType().equals("1")){
+            omsImportInfo.setBuyerType("0"); //购买方类型(0:企业;1:自然人) 默认企业
+        }else if(workInvoice.getBillingType().equals("2")){
+            omsImportInfo.setBuyerType("1"); //购买方类型(0:企业;1:自然人) 默认企业
+        }else{
+            omsImportInfo.setBuyerType("0"); //购买方类型(0:企业;1:自然人) 默认企业
+        }
+
+        // 如果开票信息存在备注,那么将备注信息进行添加
+        if (StringUtils.isNotBlank(workInvoice.getRemarks())) {
+            omsImportInfo.setRemarks(workInvoice.getRemarks());
+        }
+
+        //omsImportInfo.setRemarks("zzsytdm01,xfsytdm01");
+
+
+        OrderItem orderItem = new OrderItem();
+        //发票行编号
+        orderItem.setLineCode("1");
+        //发票行性质 00(正常行)、01(折扣行)、02(被折扣行)
+        orderItem.setLineType("00");
+        //添加商品名称
+        orderItem.setGoodsName(financeInvoiceTaxClassificationCode.getGoodName());
+        //商品数量
+        orderItem.setQty(BigDecimal.valueOf(1));
+        //orderItem.setPrice(BigDecimal.valueOf(100.00));
+        //默认含税
+        orderItem.setPriceTaxFlag(financeInvoiceTaxClassificationCode.getPriceTax()); //含税状态 0不含税;1含税
+        orderItem.setTaxrate(BigDecimal.valueOf(financeInvoiceTaxClassificationCode.getTaxRate())); //税率
+        //orderItem.setAmount(BigDecimal.valueOf(100.00)); //金额
+        //orderItem.setTax(BigDecimal.valueOf(1.00));//税额
+        orderItem.setTaxamount(new BigDecimal(workInvoice.getAccount()));//含税金额
+        //orderItem.setTaxamount(BigDecimal.valueOf(workInvoice.getMoney()));//含税金额
+        //orderItem.setGoodstaxno("109050901");//税收分类编码(电脑)
+        orderItem.setGoodstaxno(financeInvoiceTaxClassificationCode.getGoodsTaxno());//税收分类编码(餐饮)
+
+        orderItems.add(orderItem);
+
+        omsImportInfo.setOrderItems(orderItems);
+        // 核心一行代码:Java对象 转 JSON格式字符串
+        String jsonStr = JSON.toJSONString(omsImportInfo);
+
+        // 2. 核心新增:JSON字符串 转 标准Base64编码字符串【解决你的核心问题】
+        String base64Str = Base64.getEncoder().encodeToString(jsonStr.getBytes(StandardCharsets.UTF_8));
+
+        return base64Str;
+    }
+
+
+
+    /**
+     * 开红票
+     * 整理数据明文
+     */
+    public String neatenFastRedInvoiceData(String allEinvno) {
+
+        OMSFastRedInvoiceInfo omsImportInfo = new OMSFastRedInvoiceInfo();
+        omsImportInfo.setDeptCode(deptCode);
+        omsImportInfo.setAllEinvno(allEinvno);
+        omsImportInfo.setRedReason("02");
+
+        // 核心一行代码:Java对象 转 JSON格式字符串
+        String jsonStr = JSON.toJSONString(omsImportInfo);
+
+        // 2. 核心新增:JSON字符串 转 标准Base64编码字符串【解决你的核心问题】
+        String base64Str = Base64.getEncoder().encodeToString(jsonStr.getBytes(StandardCharsets.UTF_8));
+
+        return base64Str;
+    }
+
+
+    /**
+     * 开红票(全类型)
+     * 整理数据明文
+     * @param workInvoiceId 开票的id
+     * @param originalInvno 原蓝票发票号码(数电票号码)
+     * @param workInvoice 红冲发票信息
+     * @param workInvoiceRelevancy 原蓝票被红冲发票信息
+     * @param financeInvoiceTaxClassificationCode 红冲数据的税收分类编码
+     * @param relevancyBillingContentDetail 被红冲蓝票数据的税收分类编码
+     * @return
+     */
+    public String neatenAllScenarioRedInvoiceData(String workInvoiceId, String originalInvno, FinanceInvoiceDTO workInvoice, FinanceInvoiceDTO workInvoiceRelevancy, FinanceInvoiceTaxClassificationCode financeInvoiceTaxClassificationCode, FinanceInvoiceTaxClassificationCode relevancyBillingContentDetail) {
+        List<OMSApplyItem> orderItems = Lists.newArrayList();
+
+        OMSAllScenarioRedInvoiceInfo omsAllScenarioRedInvoiceInfo = new OMSAllScenarioRedInvoiceInfo();
+        omsAllScenarioRedInvoiceInfo.setDeptCode(deptCode);
+        omsAllScenarioRedInvoiceInfo.setApplyNo(workInvoiceId);   //红字发票申请单号 即 开票的id
+        omsAllScenarioRedInvoiceInfo.setApplyIdentity("0");
+        omsAllScenarioRedInvoiceInfo.setSellerTaxno(sellerTaxno);
+        omsAllScenarioRedInvoiceInfo.setSellerName(sellerName);
+        //添加购买方信息
+        //测试环境
+        //omsAllScenarioRedInvoiceInfo.setBuyerName("深圳市爱人人餐饮服务有限公司");
+        //omsAllScenarioRedInvoiceInfo.setBuyerTaxno("500102203117204029");
+        //生产环境
+        omsAllScenarioRedInvoiceInfo.setBuyerName(workInvoice.getBillingWorkplaceReal());    //购买方名称
+        omsAllScenarioRedInvoiceInfo.setBuyerTaxno(workInvoice.getTaxpayerIdentificationNo());  //购买方信用代码
+
+        //原蓝票发票号码
+        omsAllScenarioRedInvoiceInfo.setOriginalInvno(originalInvno);
+        //原蓝票发票类型
+        //* 01:数电票(增值税专用发票) 02:数电票(普通发票)等
+        if(workInvoiceRelevancy.getType().equals("1")){
+            omsAllScenarioRedInvoiceInfo.setOriginalInvType("01");
+        }else{
+            omsAllScenarioRedInvoiceInfo.setOriginalInvType("02");
+        }
+
+        //此处用来计算红冲的蓝票的税额
+
+        // 1. 获取含税金额(价税合计)- 从workInvoice中取值
+        BigDecimal blueTaxIncludedAmount = new BigDecimal(workInvoiceRelevancy.getAccount());
+        //获取税率
+        BigDecimal blueTaxRate;
+        if (relevancyBillingContentDetail.getTaxRate() != null) {
+            blueTaxRate = BigDecimal.valueOf(relevancyBillingContentDetail.getTaxRate());
+            // 税率格式兼容:6(整数)转0.06,0.06直接用(保留4位小数,四舍五入)
+            blueTaxRate = blueTaxRate.compareTo(BigDecimal.ONE) > 0
+                    ? blueTaxRate.divide(new BigDecimal("100"), 4, BigDecimal.ROUND_HALF_UP)
+                    : blueTaxRate;
+        } else {
+            // 默认税率6%(用字符串构造避免浮点精度丢失)
+            blueTaxRate = new BigDecimal("0.06");
+        }
+
+        // 3. 计算不含税金额:含税金额 ÷ (1+税率),保留2位小数(四舍五入)
+        BigDecimal blueTaxExcludedAmount = blueTaxIncludedAmount
+                .divide(BigDecimal.ONE.add(blueTaxRate), 2, BigDecimal.ROUND_HALF_UP);
+
+        // 4. 计算税额:含税金额 - 不含税金额(保证无尾差,适配正负值)
+        BigDecimal blueTaxAmount = blueTaxIncludedAmount.subtract(blueTaxExcludedAmount);
+
+        //原蓝票金额
+        omsAllScenarioRedInvoiceInfo.setOriginalInvAmount(blueTaxExcludedAmount.toString());
+
+        //原蓝票税额
+        omsAllScenarioRedInvoiceInfo.setOriginalInvTax(blueTaxAmount.toString());
+
+
+        //此处用来处理红冲对应的金额汇总
+        // 1. 获取含税金额(价税合计)- 从workInvoice中取值
+        BigDecimal taxIncludedAmount =  new BigDecimal(workInvoice.getAccount());
+
+        // 2. 处理税率:优先用动态值,为空则默认6%(兼容6/0.06两种格式)
+        BigDecimal taxRate;
+        if (financeInvoiceTaxClassificationCode.getTaxRate() != null) {
+            taxRate = BigDecimal.valueOf(financeInvoiceTaxClassificationCode.getTaxRate());
+            // 税率格式兼容:6(整数)转0.06,0.06直接用(保留4位小数,四舍五入)
+            taxRate = taxRate.compareTo(BigDecimal.ONE) > 0
+                    ? taxRate.divide(new BigDecimal("100"), 4, BigDecimal.ROUND_HALF_UP)
+                    : taxRate;
+        } else {
+            // 默认税率6%(用字符串构造避免浮点精度丢失)
+            taxRate = new BigDecimal("0.06");
+        }
+
+        // 3. 计算不含税金额:含税金额 ÷ (1+税率),保留2位小数(四舍五入)
+        BigDecimal taxExcludedAmount = taxIncludedAmount
+                .divide(BigDecimal.ONE.add(taxRate), 2, BigDecimal.ROUND_HALF_UP);
+
+        // 4. 计算税额:含税金额 - 不含税金额(保证无尾差,适配正负值)
+        BigDecimal taxAmount = taxIncludedAmount.subtract(taxExcludedAmount);
+
+
+        //合计金额(不含税)
+        omsAllScenarioRedInvoiceInfo.setTotalAmount(taxExcludedAmount.toString());
+        //合计税额
+        omsAllScenarioRedInvoiceInfo.setTotalTax(taxAmount.toString());
+        //价税合计
+        omsAllScenarioRedInvoiceInfo.setTotalTaxamount(taxIncludedAmount.setScale(2, BigDecimal.ROUND_HALF_UP).toString());
+
+        //* 冲红原因(必填)
+        //* 01:开票有误 02:销货退回 03:服务中止 04:销售折让
+        if(StringUtils.isBlank(workInvoice.getRedFlushReason())){
+            //如果蓝票开票额 + 红票开票额 大于零,则表示此处是部分红冲,那么红冲原因就是04
+            if (blueTaxIncludedAmount.add(taxIncludedAmount).compareTo(BigDecimal.ZERO) > 0) {
+                omsAllScenarioRedInvoiceInfo.setRedReason("04");
+            }else{
+                omsAllScenarioRedInvoiceInfo.setRedReason("01");
+            }
+        }else{
+            omsAllScenarioRedInvoiceInfo.setRedReason(workInvoice.getRedFlushReason());
+        }
+        //是否自动开票
+        omsAllScenarioRedInvoiceInfo.setAutoMakeInv("Y");
+
+
+        OMSApplyItem orderItem = new OMSApplyItem();
+        orderItem.setLineCode("1");
+        //添加商品名称
+        orderItem.setGoodsName(financeInvoiceTaxClassificationCode.getGoodName());
+
+        //orderItem.setQty(BigDecimal.valueOf(1));
+        //orderItem.setPrice(BigDecimal.valueOf(100.00));
+
+
+
+        orderItem.setAmount(taxExcludedAmount); //金额
+        orderItem.setTax(taxAmount);//税额
+        orderItem.setTaxrate(BigDecimal.valueOf(financeInvoiceTaxClassificationCode.getTaxRate())); //税率
+        orderItem.setTaxamount( new BigDecimal(workInvoice.getAccount()));//含税金额
+        orderItem.setGoodstaxno(financeInvoiceTaxClassificationCode.getGoodsTaxno());//税收分类编码
+        orderItem.setOriLineCode("1");//对应蓝票明细序号
+
+        orderItems.add(orderItem);
+        omsAllScenarioRedInvoiceInfo.setApplyItems(orderItems);
+
+        // 核心一行代码:Java对象 转 JSON格式字符串
+        String jsonStr = JSON.toJSONString(omsAllScenarioRedInvoiceInfo);
+
+        // 2. 核心新增:JSON字符串 转 标准Base64编码字符串【解决你的核心问题】
+        String base64Str = Base64.getEncoder().encodeToString(jsonStr.getBytes(StandardCharsets.UTF_8));
+
+        return base64Str;
+    }
+
+
+    /**
+     * 查询红字确认单查询接口结果的参数
+     * @param applyNo
+     * @return
+     */
+    public String neatenAllScenarioRedInvoiceConfirmQueryData(String applyNo) {
+
+        OMSRedInvoiceConfirmQueryRequest omsImportInfo = new OMSRedInvoiceConfirmQueryRequest();
+        omsImportInfo.setDeptCode(deptCode);
+        omsImportInfo.setApplyNo(applyNo);
+        omsImportInfo.setApplyIdentity("0");
+
+
+        // 核心一行代码:Java对象 转 JSON格式字符串
+        String jsonStr = JSON.toJSONString(omsImportInfo);
+
+        // 2. 核心新增:JSON字符串 转 标准Base64编码字符串【解决你的核心问题】
+        String base64Str = Base64.getEncoder().encodeToString(jsonStr.getBytes(StandardCharsets.UTF_8));
+
+        return base64Str;
+    }
+
+}

+ 86 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/SpringContextHolderAccess.java

@@ -0,0 +1,86 @@
+/**
+ * Copyright &copy; 2015-2020 <a href="http://www.jeeplus.org/">JeePlus</a> All rights reserved.
+ */
+package com.jeeplus.assess.invoice.utils.OMS;
+
+import org.apache.commons.lang3.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+/**
+ * 以静态变量保存Spring ApplicationContext, 可在任何代码任何地方任何时候取出ApplicaitonContext.
+ *
+ * @author Zaric
+ * @date 2013-5-29 下午1:25:40
+ */
+@Service
+@Lazy(false)
+public class SpringContextHolderAccess implements ApplicationContextAware, DisposableBean {
+
+	private static ApplicationContext applicationContext = null;
+
+	private static Logger logger = LoggerFactory.getLogger(SpringContextHolderAccess.class);
+
+	/**
+	 * 取得存储在静态变量中的ApplicationContext.
+	 */
+	public static ApplicationContext getApplicationContext() {
+		assertContextInjected();
+		return applicationContext;
+	}
+
+	/**
+	 * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> T getBean(String name) {
+		assertContextInjected();
+		return (T) applicationContext.getBean(name);
+	}
+
+	/**
+	 * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
+	 */
+	public static <T> T getBean(Class<T> requiredType) {
+		assertContextInjected();
+		return applicationContext.getBean(requiredType);
+	}
+
+	/**
+	 * 清除SpringContextHolder中的ApplicationContext为Null.
+	 */
+	public static void clearHolder() {
+		if (logger.isDebugEnabled()){
+			logger.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
+		}
+		applicationContext = null;
+	}
+
+	/**
+	 * 实现ApplicationContextAware接口, 注入Context到静态变量中.
+	 */
+	@Override
+	public void setApplicationContext(ApplicationContext applicationContext) {
+		SpringContextHolderAccess.applicationContext = applicationContext;
+	}
+
+	/**
+	 * 实现DisposableBean接口, 在Context关闭时清理静态变量.
+	 */
+	@Override
+	public void destroy() throws Exception {
+		SpringContextHolderAccess.clearHolder();
+	}
+
+	/**
+	 * 检查ApplicationContext不为空.
+	 */
+	private static void assertContextInjected() {
+		Validate.validState(applicationContext != null, "applicaitonContext属性未注入, 请在applicationContext.xml中定义SpringContextHolder.");
+	}
+}

+ 109 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/invoice/utils/OMS/ZipUtils.java

@@ -0,0 +1,109 @@
+package com.jeeplus.assess.invoice.utils.OMS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * ZIP打包工具类
+ */
+public class ZipUtils {
+    private static final Logger logger = LoggerFactory.getLogger(ZipUtils.class);
+
+    /**
+     * 将指定文件夹打包为ZIP文件
+     * @param sourceDir 待打包的文件夹路径(如:D:/attachment-file/KP-2026-301)
+     * @param zipFilePath 生成的ZIP文件路径(如:D:/attachment-file/KP-2026-301.zip)
+     * @throws IOException 打包异常
+     */
+    public static void zipFolder(String sourceDir, String zipFilePath) throws IOException {
+        File sourceFile = new File(sourceDir);
+        if (!sourceFile.exists() || !sourceFile.isDirectory()) {
+            throw new IOException("待打包的文件夹不存在或不是目录:" + sourceDir);
+        }
+
+        // 创建ZIP输出流(指定UTF-8编码,避免文件名乱码)
+        try (FileOutputStream fos = new FileOutputStream(zipFilePath);
+             ZipOutputStream zos = new ZipOutputStream(fos, StandardCharsets.UTF_8)) {
+
+            // 递归打包文件夹内所有文件
+            File[] files = sourceFile.listFiles();
+            if (files != null) {
+                for (File file : files) {
+                    zipFile(file, sourceFile.getName(), zos);
+                }
+            }
+            logger.info("文件夹打包ZIP成功!源目录:{} → ZIP文件:{}", sourceDir, zipFilePath);
+        } catch (IOException e) {
+            logger.error("文件夹打包ZIP失败!", e);
+            throw e;
+        }
+    }
+
+    /**
+     * 递归打包文件/子文件夹到ZIP
+     * @param file 待打包的文件/文件夹
+     * @param parentPath ZIP内的父路径(保持目录结构)
+     * @param zos ZIP输出流
+     * @throws IOException 打包异常
+     */
+    private static void zipFile(File file, String parentPath, ZipOutputStream zos) throws IOException {
+        String entryName = parentPath + File.separator + file.getName();
+        if (file.isDirectory()) {
+            // 处理子文件夹:添加目录Entry,递归打包子文件
+            zos.putNextEntry(new ZipEntry(entryName + File.separator));
+            zos.closeEntry();
+
+            File[] children = file.listFiles();
+            if (children != null) {
+                for (File child : children) {
+                    zipFile(child, entryName, zos);
+                }
+            }
+        } else {
+            // 处理文件:写入ZIP
+            try (FileInputStream fis = new FileInputStream(file);
+                 BufferedInputStream bis = new BufferedInputStream(fis)) {
+
+                zos.putNextEntry(new ZipEntry(entryName));
+                byte[] buffer = new byte[4096];
+                int len;
+                while ((len = bis.read(buffer)) != -1) {
+                    zos.write(buffer, 0, len);
+                }
+                zos.closeEntry();
+            }
+        }
+    }
+
+    /**
+     * 删除文件夹(递归删除所有文件/子文件夹)
+     * @param dir 待删除的文件夹
+     * @return 是否删除成功
+     */
+    public static boolean deleteFolder(File dir) {
+        if (!dir.exists()) {
+            return true;
+        }
+        File[] files = dir.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                if (file.isDirectory()) {
+                    deleteFolder(file);
+                } else {
+                    file.delete();
+                }
+            }
+        }
+        boolean deleted = dir.delete();
+        if (deleted) {
+            logger.info("临时文件夹删除成功:{}", dir.getAbsolutePath());
+        } else {
+            logger.warn("临时文件夹删除失败:{}", dir.getAbsolutePath());
+        }
+        return deleted;
+    }
+}

+ 3 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/workContract/mapper/WorkContractInfoMapper.java

@@ -44,6 +44,9 @@ public interface WorkContractInfoMapper extends BaseMapper<WorkContractInfo> {
     List<WorkAttachmentInfo> findDtos(@Param("id") String id);
 
     @InterceptorIgnore(tenantLine = "true")
+    List<WorkAttachmentInfo> findInvoiceList(@Param("id") String id);
+
+    @InterceptorIgnore(tenantLine = "true")
     List<WorkAttachmentInfoDTO> findByAttachmentIdAndFlag(@Param("attachmentId") String id,@Param("attachmentFlag") String attachmentFlag);
 
     void updateStatusById(@Param("id") String id, @Param("status")String status);

+ 16 - 0
jeeplus-modules/jeeplus-assess/src/main/java/com/jeeplus/assess/workContract/mapper/xml/WorkContractInfoMapper.xml

@@ -135,6 +135,22 @@
 		WHERE
 			del_flag = 0
 			AND attachment_id = #{id}
+			AND attachment_flag != 'PG_OMS_invoice_file'
+	</select>
+
+	<select id="findInvoiceList" resultType="com.jeeplus.sys.domain.WorkAttachmentInfo">
+		SELECT
+			id,
+			url,
+			attachment_name AS `name`,
+			create_by_id AS `by`,
+			create_time
+		FROM
+			work_attachment
+		WHERE
+			del_flag = 0
+		  AND attachment_id = #{id}
+		  AND attachment_flag = 'PG_OMS_invoice_file'
 	</select>
 
 	<select id="findByAttachmentIdAndFlag" resultType="com.jeeplus.sys.service.dto.WorkAttachmentInfoDTO">

+ 45 - 0
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/config/OmsConfig.java

@@ -0,0 +1,45 @@
+package com.jeeplus.finance.invoice.config;
+
+import lombok.Getter;
+import org.springframework.stereotype.Component;
+
+@Getter
+@Component
+public class OmsConfig {
+
+    /** OMS AppId */
+    public static  final String appId = "hyc1";
+
+    /** OMS AppKey */
+    public static  final String appKey = "hyc1";
+
+    /** 组织编码 */
+    public static  final String deptCode = "500102204228315131";
+
+    /** 销方名称 */
+    public static  final String sellerName = "深圳市松胜电子有限公司";
+
+    /** 销售方纳税人识别号 */
+    public static  final String sellerTaxno = "500102204228315131";
+
+    /** 销售方银行名称 */
+    public static  final String bankName = "中信银行南京龙江支行";
+
+    /** 销售方银行账号 */
+    public static  final String bankAccount = "7329010182600006811";
+
+    /** 访问接口地址前缀 */
+    public static  final String omsUrl = "https://oms-sandbox.einvoice.js.cn:7079";
+    //public static  final String omsUrl = "https://www.oms.ejinshui-cloud.com:8899";
+
+    /** 用于判定是否开启oms开票流程事件 */
+    public static  final boolean omsEnabled = true;
+
+    public static final String directory = "/attachment-file";
+
+    /** 成功短信模板编号 */
+    public static  final String successSmsCode = "SMS_501960223";
+    /** 失败短信模板编号 */
+    public static  final String errorSmsCode = "SMS_501645266";
+
+}

+ 16 - 43
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/controller/CwFinanceInvoiceController.java

@@ -12,7 +12,6 @@ import com.jeeplus.common.excel.annotation.ExportMode;
 import com.jeeplus.common.redis.RedisUtils;
 import com.jeeplus.common.utils.ResponseUtil;
 import com.jeeplus.finance.invoice.domain.CwFinanceInvoice;
-import com.jeeplus.finance.invoice.domain.CwFinanceInvoiceBase;
 import com.jeeplus.finance.invoice.service.CwFinanceInvoiceService;
 import com.jeeplus.finance.invoice.service.OMS.*;
 import com.jeeplus.finance.invoice.service.dto.CwFinanceDTO;
@@ -20,16 +19,10 @@ import com.jeeplus.finance.invoice.service.dto.CwFinanceImportDTO;
 import com.jeeplus.finance.invoice.service.dto.CwFinanceInvoiceDTO;
 import com.jeeplus.finance.invoice.service.dto.CwFinanceInvoiceDetailDTO;
 import com.jeeplus.finance.invoice.util.EasyPoiUtil;
-import com.jeeplus.finance.invoice.util.ZipUtils;
-import com.jeeplus.finance.projectReport.service.dto.ProjectReportWorkAttachmentDTO;
 import com.jeeplus.finance.utils.FileUtil;
-import com.jeeplus.finance.utils.Global;
 import com.jeeplus.flowable.feign.IFlowableApi;
 import com.jeeplus.logging.annotation.ApiLog;
 import com.jeeplus.logging.constant.enums.LogTypeEnum;
-//import com.jeeplus.sys.utils.DictUtils;
-//import com.jeeplus.sys.utils.SpringContextHolder;
-import com.jeeplus.sys.domain.WorkAttachmentInfo;
 import com.jeeplus.sys.feign.IDictApi;
 import com.jeeplus.sys.feign.IUserApi;
 import com.jeeplus.sys.feign.IWorkAttachmentApi;
@@ -41,13 +34,14 @@ import org.apache.ibatis.annotations.Param;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.ResponseEntity;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
+import javax.annotation.PostConstruct;
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -723,14 +717,7 @@ public class CwFinanceInvoiceController {
     //private static final String appId = Global.getConfig("omsAppId");
     //private static final String appKey = Global.getConfig("omsAppKey");
     //private static final String deptCode = Global.getConfig("omsDeptCode");
-    @Value("${omsAppId}")
-    private String appId;
-    @Value("${omsAppKey}")
-    private String appKey;
-    @Value("${omsDeptCode}")
-    private String deptCode;
-    @Value("${omsUrl}")
-    private String omsUrl;
+
     /**
      * 开具蓝票
      * OMS发票测试 完整最终版【最终最终定稿,完全匹配你的所有要求】
@@ -741,7 +728,7 @@ public class CwFinanceInvoiceController {
      */
     @GetMapping(value = "/invoiceOMSView")
     @Transactional(readOnly = false)
-    public Map<String,Object> invoiceOMSView(@Param("invoiceId") String workInvoiceId){
+    public Map<String,Object> invoiceOMSView(@RequestParam("invoiceId") String workInvoiceId){
         Map<String,Object> map = new HashMap<>();
         // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
         omsDisposeService.doInvoiceBusiness(map, workInvoiceId, "21");
@@ -774,10 +761,9 @@ public class CwFinanceInvoiceController {
      * @param originalInvno   原蓝票发票号码(数电票号码)
      * @return
      */
-    @RequestMapping(value = "/invoiceAllScenarioRedOMSView")
-    @ResponseBody
+    @GetMapping(value = "/invoiceAllScenarioRedOMSView")
     @Transactional(readOnly = false)
-    public Map<String,Object> invoiceAllScenarioRedOMSView(String workInvoiceId, String redInvoiceRelevancyId, String originalInvno) {
+    public Map<String,Object> invoiceAllScenarioRedOMSView(@RequestParam("workInvoiceId")String workInvoiceId,@RequestParam("redInvoiceRelevancyId") String redInvoiceRelevancyId,@RequestParam("originalInvno") String originalInvno) {
         Map<String,Object> map = new HashMap<>();
         // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
         omsDisposeService.doAllScenarioRedInvoiceBusiness(map, workInvoiceId, redInvoiceRelevancyId, originalInvno,"21");
@@ -793,8 +779,7 @@ public class CwFinanceInvoiceController {
      * @param redisKeyPrefix 传入Redis key前缀(如OMS_invoice_download)
      * @return 包含操作状态和key集合的结果
      */
-    @RequestMapping(value = "/deleteRedisByKey")
-    @ResponseBody
+    @GetMapping(value = "/deleteRedisByKey")
     public Map<String, Object> deleteRedisByKey(String redisKeyPrefix) {
         Map<String, Object> resultMap = new HashMap<>();
         // 初始化返回结果:默认操作失败
@@ -874,7 +859,8 @@ public class CwFinanceInvoiceController {
      * 用于发票开票获取数电票信息处理
      * 和开票系统相关的定时任务
      */
-    @RequestMapping(value = "/processInvoiceDownloadTasks")
+    //@Scheduled(cron = "0 */1 * * * ?")
+    @GetMapping(value = "/processInvoiceDownloadTasks")
     @Transactional(readOnly = false)
     public void processInvoiceDownloadTasks() {
         invoiceDownloadService.processInvoiceDownloadTasks();
@@ -885,7 +871,8 @@ public class CwFinanceInvoiceController {
      * 用于发票开票红冲定时任务信息处理
      * 和开票系统相关的定时任务
      */
-    @RequestMapping(value = "/processRedInvoiceScheduledTask")
+    //@Scheduled(cron = "0 */1 * * * ?")
+    @GetMapping(value = "/processRedInvoiceScheduledTask")
     @Transactional(readOnly = false)
     public void processRedInvoiceScheduledTask() {
         redInvoiceScheduledService.processAllRedInvoiceTasks();
@@ -895,7 +882,8 @@ public class CwFinanceInvoiceController {
      * 用于发票开票时报9998错误时进行重新处理的方法
      * 和开票系统相关的定时任务
      */
-    @RequestMapping(value = "/redInvoiceRetryScheduledTask")
+    //@Scheduled(cron = "0 */1 * * * ?")
+    @GetMapping(value = "/redInvoiceRetryScheduledTask")
     @Transactional(readOnly = false)
     public void redInvoiceRetryScheduledTask() {
         redInvoiceRetryScheduledService.handleInvoice9998RetryTask();
@@ -905,8 +893,8 @@ public class CwFinanceInvoiceController {
     /**
      * 下载附件(文件没有时间戳前缀)
      */
-    @RequestMapping("/downLoadOMSInvoiceAttachzip")
-    public void downLoadOMSInvoiceAttachzip(String fileUrl, HttpServletResponse response) {
+    @GetMapping("/downLoadOMSInvoiceAttachzip")
+    public void downLoadOMSInvoiceAttachzip(@RequestParam("fileUrl")String fileUrl, HttpServletResponse response) {
         long timestamp = System.currentTimeMillis();
         UserDTO userDTO = SpringUtil.getBean(IUserApi.class).getByToken(TokenProvider.getCurrentToken());
         String filePath = null;
@@ -931,25 +919,10 @@ public class CwFinanceInvoiceController {
         try {
             //文件下载到指定文件夹
             String aliyunUrl = "http://cdn.gangwaninfo.com";
-            String aliDownloadUrl = "http://oss.gangwaninfo.com";
-
             String file = aliyunUrl + fileUrl;
-            file = file.replace("amp;", "");
-            String fileName = file.substring(file.lastIndexOf("/") + 1, file.length());
-            String cons = "";
-            if (file.contains(aliyunUrl)) {
-                cons = aliyunUrl;
-            } else if (file.contains("http://gangwan-app.oss-cn-hangzhou.aliyuncs.com")) {
-                cons = "http://gangwan-app.oss-cn-hangzhou.aliyuncs.com";
-            } else {
-                cons = aliDownloadUrl;
-            }
-            String ossKey = file.split(cons + "/")[1];
-            SpringUtil.getBean(IWorkAttachmentApi.class).downByStreamSaveLocal(ossKey, fileName, path + fileName);
-
+            SpringUtil.getBean(IWorkAttachmentApi.class).downloadFolderFromOss(fileUrl, path );
             //文件打包压缩成zip
             FileUtil.zipFolder(filePath, filePath + ".zip");
-
             String downUrl = filePath + ".zip";
 
             OutputStream outputStream = null;

+ 8 - 0
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/mapper/CwFinanceInvoiceMapper.java

@@ -15,6 +15,7 @@ import com.jeeplus.sys.service.dto.AreaDTO;
 import com.jeeplus.sys.service.dto.UserDTO;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -127,6 +128,13 @@ public interface CwFinanceInvoiceMapper extends BaseMapper<CwFinanceInvoice> {
      * @return
      */
     WorkInvoiceTaxClassificationCode getBillingContentDetail(CwFinanceInvoiceDTO workInvoice);
+
+    /**
+     * 修改开票信息对应个节点redis保存的数据
+     * @param workInvoice
+     * @return
+     */
+    Integer updateRedInvoiceJsonByWorkInvoiceId(CwFinanceInvoiceDTO workInvoice);
 }
 
 

+ 33 - 3
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/mapper/xml/CwFinanceInvoiceMapper.xml

@@ -54,6 +54,7 @@
             <collection property="financeInvoiceReceivablesDTOList" column="id" select="getFirList" ofType="com.jeeplus.finance.invoice.service.dto.CwFinanceInvoiceReceivablesDTO"></collection>
             <collection property="financeInvoiceBaseDTOList" column="id" select="getBaseList" ofType="com.jeeplus.finance.invoice.service.dto.CwFinanceInvoiceBaseDTO"></collection>
             <collection property="financeInvoiceDetailDTOList" column="id" select="getFidList" ofType="com.jeeplus.finance.invoice.service.dto.CwFinanceInvoiceDetailDTO"></collection>
+        <collection property="invoiceList" ofType="com.jeeplus.sys.domain.WorkAttachmentInfo" column="id" select="getInvoiceFileList"></collection>
         <collection property="workAttachmentDtoList" ofType="com.jeeplus.sys.domain.WorkAttachmentInfo" column="id" select="getFileList"></collection>
         <collection property="workAttachmenSignContract" ofType="com.jeeplus.sys.domain.WorkAttachmentInfo" column="id" select="getFileListSignContarct"></collection>
     </resultMap>
@@ -245,6 +246,15 @@
         fib.account
     </sql>
 
+    <select id="getInvoiceFileList" resultMap="AttachmentMap">
+        select
+        <include refid="File_Column_List"></include>,
+        su.name as create_name
+        from work_attachment wa
+        left join sys_user su on su.id = wa.create_by_id and su.del_flag = '0'
+        where wa.del_flag = '0' and wa.attachment_id = #{id} and wa.attachment_flag = 'CW_OMS_invoice_file'
+    </select>
+
     <select id="getFileList" resultMap="AttachmentMap">
         select
         <include refid="File_Column_List"></include>,
@@ -806,7 +816,7 @@
                                 oms_access_token_error = #{omsAccessTokenError},
                                 oms_access_token = #{omsAccessToken},
                                 oms_error_message = #{omsErrorMessage},
-                                invoice_state = #{invoiceState}
+                                status = #{status}
         WHERE id = #{id}
     </update>
 
@@ -817,7 +827,7 @@
                                 oms_ofd_url = #{omsOfdUrl},
                                 oms_pdf_url = #{omsPdfUrl},
                                 oms_xml_url = #{omsXmlUrl},
-                                invoice_state = #{invoiceState}
+                                status = #{status}
         WHERE id = #{id}
     </update>
 
@@ -836,8 +846,28 @@
     </update>
 
     <select id="getBillingContentDetail" resultType="com.jeeplus.finance.invoice.domain.WorkInvoiceTaxClassificationCode">
-        select * from finance_invoice_tax_classification_code where del_flag = 0 and billing_content_id = #{billingContent}
+        select * from finance_invoice_tax_classification_code where del_flag = 0 and billing_content_id = #{billingType}
     </select>
 
 
+    <update id="updateRedInvoiceJsonByWorkInvoiceId">
+        UPDATE cw_finance_invoice
+        <!-- 用MyBatis的<set>标签替换原生SET,自动处理首字段逗号问题 -->
+        <set>
+            <if test="null != blueDownloadInvoiceJson and '' != blueDownloadInvoiceJson">
+                blue_download_invoice_json = #{blueDownloadInvoiceJson}
+            </if>
+            <if test="null != retryInvoiceJson and '' != retryInvoiceJson">
+                retry_invoice_json = #{retryInvoiceJson}
+            </if>
+            <if test="null != redInvoiceJson and '' != redInvoiceJson">
+                red_invoice_json = #{redInvoiceJson}
+            </if>
+            <if test="null != redDownloadInvoiceJson and '' != redDownloadInvoiceJson">
+                red_download_invoice_json = #{redDownloadInvoiceJson}
+            </if>
+        </set>
+        WHERE id = #{id}
+    </update>
+
 </mapper>

+ 114 - 71
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/CwFinanceInvoiceService.java

@@ -20,31 +20,24 @@ import com.jeeplus.finance.invoice.domain.OMS.InvoiceDown.OMSInvoiceDetailInfo;
 import com.jeeplus.finance.invoice.mapper.*;
 import com.jeeplus.finance.invoice.service.dto.*;
 import com.jeeplus.finance.invoice.service.mapstruct.*;
+import com.jeeplus.finance.invoice.thread.ApprovalThread;
+import com.jeeplus.finance.invoice.thread.RedApprovalThread;
 import com.jeeplus.finance.invoice.util.OMS.FileHandlingUtil;
 import com.jeeplus.finance.projectRecords.mapper.CwProjectRecordsMapper;
 import com.jeeplus.finance.projectRecords.service.dto.CwProjectRecordsDTO;
 import com.jeeplus.finance.projectReport.domain.CwProjectReportData;
-import com.jeeplus.finance.projectReport.domain.CwProjectReportNumberApply;
 import com.jeeplus.finance.projectReport.mapper.CwProjectReportMapper;
-import com.jeeplus.finance.projectReport.service.dto.CwProjectReportImportDTO;
 import com.jeeplus.finance.workClientInfo.domain.CwWorkClientBase;
 import com.jeeplus.finance.workClientInfo.domain.CwWorkClientBilling;
 import com.jeeplus.finance.workClientInfo.mapper.CwWorkClientBaseMapper;
-import com.jeeplus.finance.workClientInfo.mapper.CwWorkClientBillingMapper;
 import com.jeeplus.finance.workClientInfo.service.CwWorkClientBillingService;
 import com.jeeplus.finance.workClientInfo.service.CwWorkClientService;
-//import com.jeeplus.pubmodules.oss.mapper.OssServiceMapper;
-//import com.jeeplus.pubmodules.oss.service.OssService;
-//import com.jeeplus.pubmodules.serialNumTpl.service.SerialnumTplService;
 import com.jeeplus.finance.workClientInfo.service.dto.CwWorkClientBaseDTO;
 import com.jeeplus.finance.workClientInfo.service.dto.CwWorkClientBillingDTO;
 import com.jeeplus.flowable.feign.IFlowableApi;
-import com.jeeplus.pubmodules.oss.domain.WorkAttachment;
-import com.jeeplus.sys.domain.Office;
 import com.jeeplus.sys.domain.User;
 import com.jeeplus.sys.domain.WorkAttachmentInfo;
 import com.jeeplus.sys.feign.*;
-//import com.jeeplus.sys.service.UserService;
 import com.jeeplus.sys.service.dto.AreaDTO;
 import com.jeeplus.sys.service.dto.OfficeDTO;
 import com.jeeplus.sys.service.dto.RoleDTO;
@@ -65,6 +58,8 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
+import static com.jeeplus.finance.invoice.config.OmsConfig.*;
+
 @Service
 @Transactional(rollbackFor = Exception.class)
 public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper, CwFinanceInvoice> {
@@ -631,7 +626,6 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
 
 
     public CwFinanceInvoiceDTO queryById(String id) {
-
         CwFinanceInvoiceDTO cwFinanceInvoiceDTO = cwFinanceInvoiceMapper.queryById(id);
         if (ObjectUtil.isNotEmpty(cwFinanceInvoiceDTO) && ObjectUtil.isNotEmpty(cwFinanceInvoiceDTO.getRedInvoiceFlag()) && 1 == cwFinanceInvoiceDTO.getRedInvoiceFlag()) {
             //此处为红冲数据 需要获取发票号
@@ -694,12 +688,19 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
                 cwFinanceInvoiceDTO.setBillingWorkplaceReal(cwWorkClientBase.getName());
             }
         }
-
-
         return cwFinanceInvoiceDTO;
     }
 
     public CwFinanceInvoice saveForm(CwFinanceInvoiceDTO cwFinanceInvoiceDTO) throws Exception {
+        String isOms = "0";
+        if("5".equals(cwFinanceInvoiceDTO.getStatus())){
+            isOms = handleDoInvoice(cwFinanceInvoiceDTO);
+            //如果需要走开票系统,则设置状态为12
+            if("1".equals(isOms) || "2".equals(isOms)){
+                cwFinanceInvoiceDTO.setStatus("12");
+            }
+        }
+
         CwFinanceInvoice cwFinanceInvoice = CwFinanceInvoiceWrapper.INSTANCE.toEntity(cwFinanceInvoiceDTO);
         //获取当前登录人信息
         UserDTO userDTO = SpringUtil.getBean(IUserApi.class).getByToken(TokenProvider.getCurrentToken());
@@ -901,9 +902,62 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
                 cwFinanceInvoiceInvalidMapper.insert(cwFinanceInvoiceInvalid);
             }
         }
+
+        //蓝票
+        if("1".equals(isOms)){
+            CwFinanceInvoiceDTO dto = queryById(cwFinanceInvoice.getId());
+            new ApprovalThread(dto, "21").start();
+        }
+        //红票
+        if("2".equals(isOms)){
+            CwFinanceInvoiceDTO dto = queryById(cwFinanceInvoice.getId());
+            new RedApprovalThread(dto,cwFinanceInvoiceDTO.getInvoiceNumberStr(), "21").start();
+        }
         return cwFinanceInvoice;
     }
 
+    public String handleDoInvoice(CwFinanceInvoiceDTO financeInvoiceDTO){
+        boolean OMS_ENABLED = omsEnabled;
+        //保存数据结束,触发开票接口
+        if(OMS_ENABLED){
+            if("5".equals(financeInvoiceDTO.getStatus())){
+                BigDecimal account = new BigDecimal(financeInvoiceDTO.getAccount());
+                if (account.compareTo(BigDecimal.ZERO) > 0) {
+                    //返回开蓝票标记
+                     return "1";
+                }else{
+                    //先获取对应红冲发票的数电票  票号
+                    //查询关联红冲发票的开票号
+                    if(1 == financeInvoiceDTO.getRedInvoiceFlag()){
+                        if(StringUtils.isBlank(financeInvoiceDTO.getInvoiceNumberStr())){
+                            if(StringUtils.isNotBlank(financeInvoiceDTO.getRedInvoiceRelevancyId())){
+                                String invoiceNumberStr = this.getInvoiceNumberStr(financeInvoiceDTO.getRedInvoiceRelevancyId());
+                                financeInvoiceDTO.setInvoiceNumberStr(invoiceNumberStr);
+                            }
+                        }
+                        String invoiceNumber = financeInvoiceDTO.getInvoiceNumberStr();
+                        //判断 如果发票小于10位,则表示为老发票红冲,需要按照老方法进行处理
+                        if (invoiceNumber == null || StringUtils.isBlank(invoiceNumber) || invoiceNumber.length() < 10) {
+                            OMS_ENABLED = false;
+                        }
+                        if(OMS_ENABLED && StringUtils.isBlank(invoiceNumber)){
+                            throw new RuntimeException("红冲发票号不存在,无法进行红冲!");
+                        }
+                    }
+                    if(OMS_ENABLED){
+                        //返回开红票标记
+                        return "2";
+                    }
+                }
+            }
+        }
+        return "0";
+    }
+
+    public String getInvoiceNumberStr(String invoiceId) {
+        return cwFinanceInvoiceMapper.getInvoiceNumberStr(invoiceId);
+    }
+
     public ResponseEntity saveInvoice(CwFinanceInvoiceDTO cwFinanceInvoiceDTO) throws Exception {
         this.saveForm(cwFinanceInvoiceDTO);
         return ResponseEntity.ok("保存成功");
@@ -2012,9 +2066,10 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
             workattachment.setDelFlag(0);
             workattachment.setUrl(ossFilePath); // 存入OSS路径
             workattachment.setType(type); // 文件类型
-            workattachment.setAttachmentName(workInvoice.getNumber() + "." + type); // 附件名称
-            workattachment.setAttachmentFlag("OMS_invoice_file"); // 附件标识
-            workattachment.setFileSize(String.valueOf(fileSize)); // 改用Map中的文件大小(上传后本地文件已删除,原f.length()可能失效)
+            workattachment.setName(workInvoice.getNo() + "." + type); // 附件名称
+            workattachment.setAttachmentName(workInvoice.getNo() + "." + type); // 附件名称
+            workattachment.setAttachmentFlag("CW_OMS_invoice_file"); // 附件标识
+            workattachment.setSize(String.valueOf(fileSize)); // 改用Map中的文件大小(上传后本地文件已删除,原f.length()可能失效)
             workattachment.setAttachmentId(workInvoice.getId()); // 关联发票ID
             workattachment.setId(UUID.randomUUID().toString().replace("-", "")); // 主键ID
 
@@ -2022,10 +2077,19 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
 
             List<WorkAttachmentInfo> existAttachments =   SpringUtil.getBean ( IWorkAttachmentApi.class ).getByAttachmentIdAndUrlAndAttachmentFlag(workattachment);
             if (CollectionUtils.isEmpty(existAttachments)) { // 建议用CollectionUtils.isEmpty判断空集合,更安全
-                Map<String,String> map = new HashMap<>();
-                map.put("workAttachments",JSON.toJSONString(existAttachments));
-                map.put("currentToken", TokenProvider.getCurrentToken());
-                SpringUtil.getBean ( IWorkAttachmentApi.class ).saveMsg2(map);
+                Map<String, String> map = new HashMap<>();
+                String workAttachmentDtoInfo = JSON.toJSONString(workattachment);
+                AtomicInteger sortinfo = new AtomicInteger(Integer.parseInt(sort));
+                String userDTOInfo = JSON.toJSONString(workInvoice.getCreateBy());
+                String attachmentId = workInvoice.getId();
+                String attachmentFlag = "CW_OMS_invoice_file";
+                String sortInfo = Integer.toString(sortinfo.get());
+                map.put("workAttachmentDtoInfo", workAttachmentDtoInfo);
+                map.put("userDTOInfo", userDTOInfo);
+                map.put("attachmentId", attachmentId);
+                map.put("attachmentFlag", attachmentFlag);
+                map.put("sortInfo", sortInfo);
+                String fileId = SpringUtil.getBean(IWorkAttachmentApi.class).saveFile(map);
             }
         }
     }
@@ -2045,9 +2109,6 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
         //审核完成提示框
         List<UserDTO> users = new ArrayList<>();
         users.add(workInvoice.getCreateBy());
-        List<UserDTO> userList = new ArrayList<>();
-
-        List<CwFinanceInvoiceBaseDTO> projectRelationList = workInvoice.getFinanceInvoiceBaseDTOList();
 
         //项目名称获取
         List<String> projectNameList = getProjectNameList(workInvoice);
@@ -2056,34 +2117,31 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
                 ? projectNameStr.substring(0, 900)
                 : projectNameStr;
 
-        String str = "发票申请编号:"+workInvoice.getNumber() + "发票金额:" + workInvoice.getAccount() + "(元)。项目【"+ projectNameStr +"】发票申请失败,请核实后重新发起。失败原因:" + errorMessage;
+        String str = "发票申请编号:"+workInvoice.getNo() + "发票金额:" + workInvoice.getAccount() + "(元)。项目【"+ projectNameStr +"】发票申请失败,请核实后重新发起。失败原因:" + errorMessage;
 
         String title = str;
+        UserDTO user = SpringUtil.getBean ( IUserApi.class ).getById(workInvoice.getCreateBy().getId());
 
-        //通知开票发起人和财务审核人
-        //财务员工
+        //通知开票发起人和发票管理员
         RoleDTO roleDTO = SpringUtil.getBean(IRoleApi.class).getRoleDTOByName2("发票管理员");
         List<UserDTO> notifiedPartyUsers =SpringUtil.getBean(IUserApi.class).findListByRoleId(roleDTO.getId());
-        notifiedPartyUsers.add(workInvoice.getCreateBy());
-
-
+        //发票管理员
+        notifiedPartyUsers.add(user);
         if (StringUtils.isNotBlank(workInvoice.getStatus()) && !workInvoice.getStatus().equals("3")){
             for (UserDTO notifiedPartyUser : notifiedPartyUsers) {
-               sendNotify(notifiedPartyUser,workInvoice.getProcessDefinitionId(),title);
+               sendNotify(notifiedPartyUser,workInvoice.getProcessDefinitionId(),workInvoice.getProcInsId(),title);
             }
         }
 
         //发起短信通知,用于通知开票人
         //查询开票人员的手机号
-        UserDTO user = SpringUtil.getBean ( IUserApi.class ).getById(workInvoice.getCreateBy().getId());
-
         if (StringUtils.isNotBlank(user.getMobile()) && mobilePattern.matcher(user.getMobile().trim()).matches() ) {
             //验证手机号是否已经注册
-            if(SpringUtil.getBean ( IUserApi.class ).getByMobile(user.getMobile()) == null){
+            if(StringUtils.isNotBlank(user.getMobile())){
                 Map<String,Object> result = null;
                 try{
                     //调用工具类返回结果
-                    result = SpringUtil.getBean ( IUserApi.class ).sendEntryRandomCodesByInvoice(user.getMobile(),user.getName(),user.getOfficeDTO().getName(),user.getTenantDTO().getId());
+                    result = SpringUtil.getBean ( IUserApi.class ).sendEntryRandomCodesByInvoice(user.getMobile(),workInvoice.getNo(),errorSmsCode);
                     Integer statusCode = (Integer) result.get("statusCode");
                     if (200 == statusCode) {
                         System.out.println("短信发送成功!");
@@ -2101,9 +2159,6 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
                     System.out.println("阿里云发送短信失败。失败原因为:" + e.getMessage());
                     e.printStackTrace();
                 }
-                finally {
-
-                }
             }
         }
         isSuccess = true;
@@ -2124,11 +2179,11 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
         workInvoice.setOmsEinVno(invoiceInfo.getAllEinVno());
         if(StringUtils.isNotBlank(invoiceInfo.getOfdUrl())){
             //先将文件下载到本地
-            String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getOfdUrl(), workInvoice.getNumber() + ".ofd");
+            String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getOfdUrl(), workInvoice.getNo() + ".ofd");
             //如果文件下载下来了,并且返回值正常,则此处需要对文件进行上传到阿里云操作
             if(StringUtils.isNotBlank(ofdLocalFilePath)){
                 // 调用上传方法,接收Map类型返回值(替换原String类型接收)
-                Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNumber());
+                Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNo());
                 fileUploadOnAttachment(uploadResult,workInvoice, "ofd", "1");
             }
         }
@@ -2136,22 +2191,22 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
 
         if(StringUtils.isNotBlank(invoiceInfo.getPdfUrl())){
             //先将文件下载到本地
-            String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getPdfUrl(), workInvoice.getNumber() + ".pdf");
+            String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getPdfUrl(), workInvoice.getNo() + ".pdf");
             //如果文件下载下来了,并且返回值正常,则此处需要对文件进行上传到阿里云操作
             if(StringUtils.isNotBlank(ofdLocalFilePath)){
                 // 调用上传方法,接收Map类型返回值(替换原String类型接收)
-                Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNumber());
+                Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNo());
                 fileUploadOnAttachment(uploadResult,workInvoice, "pdf", "2");
             }
         }
         workInvoice.setOmsPdfUrl(invoiceInfo.getPdfUrl());
         if(StringUtils.isNotBlank(invoiceInfo.getXmlUrl())){
             //先将文件下载到本地
-            String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getXmlUrl(), workInvoice.getNumber() + ".xml");
+            String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getXmlUrl(), workInvoice.getNo() + ".xml");
             //如果文件下载下来了,并且返回值正常,则此处需要对文件进行上传到阿里云操作
             if(StringUtils.isNotBlank(ofdLocalFilePath)){
                 // 调用上传方法,接收Map类型返回值(替换原String类型接收)
-                Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNumber());
+                Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNo());
                 fileUploadOnAttachment(uploadResult,workInvoice, "xml", "3");
             }
         }
@@ -2159,10 +2214,10 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
         workInvoice.setStatus("5");
 
         //修改结果
-        cwFinanceInvoiceMapper.updateOmsUrlById(workInvoice);
+        updateOmsUrlById(workInvoice);
         //还需要修改work_invoice_detail表中的对应发票编号(number)值
         CwFinanceInvoiceDetail workInvoiceDetail = new CwFinanceInvoiceDetail();
-        workInvoiceDetail.setId(workInvoice.getId());
+        workInvoiceDetail.setInvoiceId(workInvoice.getId());
         workInvoiceDetail.setNumber(invoiceInfo.getAllEinVno());
         cwFinanceInvoiceDetailMapper.updateNumberById(workInvoiceDetail);
 
@@ -2170,12 +2225,8 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
         //审核完成提示框
         List<UserDTO> users = new ArrayList<>();
         users.add(workInvoice.getCreateBy());
-        List<User> userList = new ArrayList<>();
-
-        List<CwFinanceInvoiceBaseDTO> projectRelationList = workInvoice.getFinanceInvoiceBaseDTOList();
-
 
-        UserDTO userDTO = SpringUtil.getBean ( IUserApi.class ).getByToken(TokenProvider.getCurrentToken ( ));
+        UserDTO userDTO = SpringUtil.getBean ( IUserApi.class ).getById(workInvoice.getCreateBy().getId());
         OfficeDTO office = userDTO.getOfficeDTO();
         String userName = userDTO.getName();
 
@@ -2186,37 +2237,35 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
                 ? projectNameStr.substring(0, 900)
                 : projectNameStr;
 
-        String str = "发票金额:" + workInvoice.getAccount() + "(元)。项目【"+ projectNameStr +"】发票申请通过,发票申请编号:"+workInvoice.getNumber() + ",创建人:"+userName+",所属部门:"+office.getName();
-        if(null != workInvoice.getBillingId()){
+        String str = "发票金额:" + workInvoice.getAccount() + "(元)。项目【"+ projectNameStr +"】发票申请通过,发票申请编号:"+workInvoice.getNo() + ",创建人:"+userName+",所属部门:"+office.getName();
+        if(null != workInvoice.getBillingWorkplaceRealId()){
             str = str + ",实际开票单位:"+workInvoice.getBillingWorkplaceReal();
         }
 
         String title = str;
 
-        //财务员工
-        List<UserDTO> notifiedPartyUsers = Lists.newArrayList();
-        notifiedPartyUsers.add(workInvoice.getCreateBy());
-
-        //通知开票人
+        //通知开票发起人和发票管理员
+        RoleDTO roleDTO = SpringUtil.getBean(IRoleApi.class).getRoleDTOByName2("发票管理员");
+        List<UserDTO> notifiedPartyUsers =SpringUtil.getBean(IUserApi.class).findListByRoleId(roleDTO.getId());
+        //发票管理员
+        notifiedPartyUsers.add(userDTO);
         if (StringUtils.isNotBlank(workInvoice.getStatus()) && !workInvoice.getStatus().equals("3")){
             workInvoice.setStatus("5");
             for (UserDTO notifiedPartyUser : notifiedPartyUsers) {
-                sendNotify(notifiedPartyUser,workInvoice.getProcessDefinitionId(),title);
+                sendNotify(notifiedPartyUser,workInvoice.getProcessDefinitionId(),workInvoice.getProcInsId(),title);
             }
         }
 
 
         //发起短信通知,用于通知开票人
         //查询开票人员的手机号
-        UserDTO user = SpringUtil.getBean ( IUserApi.class ).getById(workInvoice.getCreateBy().getId());
-
-        if (StringUtils.isNotBlank(user.getMobile()) && mobilePattern.matcher(user.getMobile().trim()).matches() ) {
+        if (StringUtils.isNotBlank(userDTO.getMobile()) && mobilePattern.matcher(userDTO.getMobile().trim()).matches() ) {
             //验证手机号是否已经注册
-            if(SpringUtil.getBean ( IUserApi.class ).getByMobile(user.getMobile()) == null){
+            if(StringUtils.isNotBlank(userDTO.getMobile())){
                 Map<String,Object> result = null;
                 try{
                     //调用工具类返回结果
-                    result = SpringUtil.getBean ( IUserApi.class ).sendEntryRandomCodesByInvoice(user.getMobile(),user.getName(),user.getOfficeDTO().getName(),user.getTenantDTO().getId());
+                    result = SpringUtil.getBean ( IUserApi.class ).sendEntryRandomCodesByInvoice(userDTO.getMobile(),workInvoice.getNo(),successSmsCode);
                     Integer statusCode = (Integer) result.get("statusCode");
                     if (200 == statusCode) {
                         System.out.println("短信发送成功!");
@@ -2243,12 +2292,11 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
         return isSuccess;
     }
 
-    public void sendNotify(UserDTO userDto,String defId,String title ){
-        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
+    public void sendNotify(UserDTO userDto,String defId,String procInsId,String title ){
+        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         String day = format.format(new Date());
         Map<String ,String > map = new HashMap<>();
-        String uuid = UUID.randomUUID().toString();
-        map.put("taskId",uuid);
+        map.put("taskId",procInsId);
         map.put("title",title);
         map.put("defId",defId);
         map.put("taskName","开票通知");
@@ -2270,13 +2318,9 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
         List<String> projectNameList = Lists.newArrayList();
         if(null != workInvoice.getFinanceInvoiceBaseDTOList()){
             for (CwFinanceInvoiceBaseDTO projectRelation : workInvoice.getFinanceInvoiceBaseDTOList()) {
-                //处理开票是非项目还是项目开票
                 if(StringUtils.isNotBlank(projectRelation.getProgramId())){
                     projectNameList.add(projectRelation.getProgramName());
                 }else{
-                    //if(projectNameList.contains("非项目发票")){
-                    //    continue;
-                    //}
                     projectNameList.add("其它:"+projectRelation.getProgramName().replaceAll("\\r|\\n*",""));
                 }
 
@@ -2290,5 +2334,4 @@ public class CwFinanceInvoiceService extends ServiceImpl<CwFinanceInvoiceMapper,
         cwFinanceInvoiceMapper.updateOmsUrlById(workInvoice);
     }
 
-
 }

+ 26 - 9
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/OMS/InvoiceDownloadService.java

@@ -25,6 +25,8 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
+import static com.jeeplus.finance.invoice.config.OmsConfig.*;
+
 /**
  * OMS发票下载
  */
@@ -38,14 +40,14 @@ public class InvoiceDownloadService {
     //private static final String appKey = Global.getConfig("omsAppKey");
     //private static final String deptCode = Global.getConfig("omsDeptCode");
 
-    @Value("${omsAppId}")
-    private String appId;
-    @Value("${omsAppKey}")
-    private String appKey;
-    @Value("${omsDeptCode}")
-    private String deptCode;
-    @Value("${omsUrl}")
-    private String omsUrl;
+    //@Value("${omsAppId}")
+    //private String appId;
+    //@Value("${omsAppKey}")
+    //private String appKey;
+    //@Value("${omsDeptCode}")
+    //private String deptCode;
+    //@Value("${omsUrl}")
+    //private String omsUrl;
 
 
     @Autowired
@@ -58,6 +60,7 @@ public class InvoiceDownloadService {
 
 
 
+    @Transactional(readOnly = false)
     public void processInvoiceDownloadTasks() {
         RedisUtils jedis = null;
         try {
@@ -149,7 +152,7 @@ public class InvoiceDownloadService {
             String jsonInvoiceDownStr = JSON.toJSONString(invoiceDownInfo);
 
             String invoiceResultStr = HttpPostJsonUtil.doPost(
-                    "https://oms-sandbox.einvoice.js.cn:7079/prod-api/output/server/invoice/download",
+                    omsUrl +"/prod-api/output/server/invoice/download",
                     jsonInvoiceDownStr
             );
             System.out.println("[InvoiceDownloadTask] 发票解析结果:" + invoiceResultStr);
@@ -180,6 +183,20 @@ public class InvoiceDownloadService {
                             }
                         }
                     }
+                }else if(null != resultDownInfo.getResult() && ("1002".equals(resultDownInfo.getResult().getCode())
+                        || "1001".equals(resultDownInfo.getResult().getCode())
+                        || "1005".equals(resultDownInfo.getResult().getCode())
+                        || "1006".equals(resultDownInfo.getResult().getCode())
+                        || "0001".equals(resultDownInfo.getResult().getCode())
+                        || "0002".equals(resultDownInfo.getResult().getCode())
+                        || "0003".equals(resultDownInfo.getResult().getCode())
+                        || "0004".equals(resultDownInfo.getResult().getCode())
+                        || "0005".equals(resultDownInfo.getResult().getCode())
+                        || "0006".equals(resultDownInfo.getResult().getCode())
+                        || "0007".equals(resultDownInfo.getResult().getCode())
+                        || "0008".equals(resultDownInfo.getResult().getCode())
+                )){
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,失败原因:" + resultDownInfo.getResult().getMessage(), informType);
                 }
             }
 

+ 147 - 26
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/OMS/OMSDisposeService.java

@@ -1,6 +1,7 @@
 package com.jeeplus.finance.invoice.service.OMS;
 
 import com.alibaba.fastjson.JSON;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.jeeplus.common.redis.RedisUtils;
 import com.jeeplus.finance.invoice.domain.CwFinanceInvoice;
 import com.jeeplus.finance.invoice.domain.OMS.OMSAccessTokenInfo;
@@ -18,10 +19,13 @@ import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
+import static com.jeeplus.finance.invoice.config.OmsConfig.*;
+
 @Service
 @Transactional(readOnly = true)
 @Lazy
@@ -30,18 +34,18 @@ public class OMSDisposeService {
     /**
      * 给accessToken查询的有效时间设置为1天
      */
-    private final int seconds = 86400;
+    //private final int seconds = 86400;
     //private static final String appId = Global.getConfig("omsAppId");
     //private static final String appKey = Global.getConfig("omsAppKey");
     //private static final String deptCode = Global.getConfig("omsDeptCode");
-    @Value("${omsAppId}")
-    private String appId;
-    @Value("${omsAppKey}")
-    private String appKey;
-    @Value("${omsDeptCode}")
-    private String deptCode;
-    @Value("${omsUrl}")
-    private String omsUrl;
+    //@Value("${omsAppId}")
+    //private String appId;
+    //@Value("${omsAppKey}")
+    //private String appKey;
+    //@Value("${omsDeptCode}")
+    //private String deptCode;
+    //@Value("${omsUrl}")
+    //private String omsUrl;
     //如果接口访问不正确,可以循环访问的次数
     private final int remainRetryTimes = 20;
 
@@ -52,6 +56,10 @@ public class OMSDisposeService {
     @Autowired
     private CwFinanceInvoiceService workInvoiceService;
 
+    // 注入SpringBoot内置的JSON序列化工具(无额外依赖)
+    @Autowired
+    private ObjectMapper objectMapper;
+
     /**
      * 用于生成开蓝票信息
      * @param map
@@ -68,7 +76,7 @@ public class OMSDisposeService {
                 // 获取AccessToken 9998重试5次
                 accessToken = getOmsAccessTokenWithRetry(10, "accessToken", workInvoiceId, informType);
                 if(StringUtils.isNotBlank(accessToken)){
-                    jedis.setEx("OMSAccessToken", accessToken,seconds, TimeUnit.SECONDS);
+                    jedis.setEx("OMSAccessToken", accessToken,86400, TimeUnit.SECONDS);
                     map.put("token状态", "重新获取token成功,存入Redis");
                 } else {
                     accessToken = "";
@@ -83,12 +91,10 @@ public class OMSDisposeService {
 
             OMSNationUtil util = new OMSNationUtil();
             //获取需要开票的发票信息
-            CwFinanceInvoiceDTO workInvoice = workInvoiceDao.queryById(workInvoiceId);
+            CwFinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
 
             if(null != workInvoice){
-                if( StringUtils.isNotBlank(workInvoice.getBillingId())){
-
-                }else{
+                if( StringUtils.isBlank(workInvoice.getBillingPeopleReal())){
                     handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
                     return;
                 }
@@ -177,15 +183,50 @@ public class OMSDisposeService {
 
             if(StringUtils.isBlank(accessTokenStr)){
                 System.err.println("获取AccessToken失败:接口返回空,剩余重试次数:"+remainRetryTimes);
-                return "";
+                if (remainRetryTimes > 1) {
+                    int nextRetry = remainRetryTimes - 1;
+                    System.err.println("⚠️ 获取AccessToken失败:接口返回空,剩余重试次数:"+nextRetry);
+                    Thread.sleep(30 * 1000);
+                    return getOmsAccessTokenWithRetry(nextRetry, getKey, workInvoiceId, informType);
+                } else {
+                    System.err.println("❌ 获取AccessToken失败:接口返回空,重试次数耗尽!");
+                    //需要将错误信息保存到对应开票信息表中
+                    CwFinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+                    if(null != workInvoice){
+                        workInvoice.setOmsAccessTokenError("获取AccessToken失败:接口返回空");
+                        //修改结果
+                        workInvoiceDao.updateAccessTokenErrorById(workInvoice);
+                    }
+                    //如果需要 可以将执行失败信息通过短信通知业务发起人
+                    handleInvoiceRetryAllFail("", workInvoiceId, "获取AccessToken失败:接口返回空", informType); // 解析失败也执行兜底方法
+
+                    return "";
+                }
             }
 
             OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(accessTokenStr, OMSAccessTokenInfo.class);
             if(null == resultTokenInfo || null == resultTokenInfo.getResult()){
                 System.err.println("获取AccessToken失败:返回结果解析异常,剩余重试次数:"+remainRetryTimes);
-                return "";
-            }
+                if (remainRetryTimes > 1) {
+                    int nextRetry = remainRetryTimes - 1;
+                    System.err.println("⚠️ 获取AccessToken失败:返回结果解析异常,剩余重试次数:"+nextRetry);
+                    Thread.sleep(30 * 1000);
+                    return getOmsAccessTokenWithRetry(nextRetry, getKey, workInvoiceId, informType);
+                } else {
+                    System.err.println("❌ 获取AccessToken失败:返回结果解析异常,重试次数耗尽!");
+                    //需要将错误信息保存到对应开票信息表中
+                    CwFinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
+                    if(null != workInvoice){
+                        workInvoice.setOmsAccessTokenError("获取AccessToken失败:返回结果解析异常");
+                        //修改结果
+                        workInvoiceDao.updateAccessTokenErrorById(workInvoice);
+                    }
+                    //如果需要 可以将执行失败信息通过短信通知业务发起人
+                    handleInvoiceRetryAllFail("", workInvoiceId, "获取AccessToken失败:返回结果解析异常", informType); // 解析失败也执行兜底方法
 
+                    return "";
+                }
+            }
             String code = resultTokenInfo.getResult().getCode();
             String message = resultTokenInfo.getResult().getMessage();
             if ("0000".equals(code)) {
@@ -201,7 +242,7 @@ public class OMSDisposeService {
                 } else {
                     System.err.println("❌ 获取AccessToken失败:连续5次返回9998,重试次数耗尽!");
                     //需要将错误信息保存到对应开票信息表中
-                    CwFinanceInvoiceDTO workInvoice = workInvoiceDao.queryById(workInvoiceId);
+                    CwFinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
                     if(null != workInvoice){
                         workInvoice.setOmsAccessTokenError("获取AccessToken失败,需要业务人员重新发起");
                         //修改结果
@@ -215,7 +256,7 @@ public class OMSDisposeService {
             } else {
                 System.err.println("❌ 获取AccessToken失败:返回业务错误码,code:"+code + "。 错误信息为:"+message);
                 //需要将错误信息保存到对应开票信息表中
-                CwFinanceInvoiceDTO workInvoice = workInvoiceDao.queryById(workInvoiceId);
+                CwFinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
                 if(null != workInvoice){
                     workInvoice.setOmsAccessTokenError("获取AccessToken失败:返回业务错误码,code:"+code + "; 错误信息为:"+message + "。需要业务人员重新发起");
                     //修改结果
@@ -322,6 +363,7 @@ public class OMSDisposeService {
      * @param getKey         获取token的key
      * @param initiationType 开票类型(蓝票/红票)
      */
+    @Transactional(readOnly = false)
     public void saveInvoiceRetryScheduledTaskToRedis(String jsonInvoiceStr ,String accessToken, String workInvoiceId, String getKey, String initiationType) {
         RedisUtils jedis = null;
         // 硬编码:初始重试次数100次
@@ -350,6 +392,36 @@ public class OMSDisposeService {
 
             jedis.expire(redisKey, REDIS_EXPIRE_SECONDS); // 1天过期
             System.out.println("✅ 9998重试任务已存入Redis,订单号:" + workInvoiceId + ",剩余重试次数:" + retryTimes);
+
+
+            // ========== 追加:Redis数据转JSON,持久化到发票表(兜底逻辑) ==========
+            long redisStoreTime = System.currentTimeMillis() / 1000; // 秒级存储时间,用于后期回滚
+            // 封装Map:包含Redis所有哈希字段 + redisKey/过期时间/存储时间,保留redisKey(按你要求)
+            Map<String, Object> retryTaskMap = new HashMap<>(12);
+            // Redis哈希中所有原字段(和Redis完全一致)
+            retryTaskMap.put("jsonInvoiceStr", jsonInvoiceStr);
+            retryTaskMap.put("accessToken", accessToken);
+            retryTaskMap.put("workInvoiceId", workInvoiceId);
+            retryTaskMap.put("getKey", getKey);
+            retryTaskMap.put("initiationType", initiationType);
+            retryTaskMap.put("firstExecTime", String.valueOf(System.currentTimeMillis()));
+            retryTaskMap.put("retryTimes", retryTimes);
+            // 额外兜底字段(保留redisKey)
+            retryTaskMap.put("redisKey", redisKey);
+            retryTaskMap.put("redisExpireSeconds", REDIS_EXPIRE_SECONDS);
+            retryTaskMap.put("redisStoreTime", redisStoreTime);
+            retryTaskMap.put("initRetryCount", INIT_RETRY_COUNT); // 追加初始重试次数,后期回滚可参考
+
+            // Map转JSON字符串(单独捕获序列化异常,不影响Redis核心逻辑)
+            String retryTaskJson = objectMapper.writeValueAsString(retryTaskMap);
+
+            // 更新发票表(和之前红冲/下载任务用相同的DAO和实体,风格统一)
+            CwFinanceInvoiceDTO workInvoice = new CwFinanceInvoiceDTO();
+            workInvoice.setId(workInvoiceId);
+            workInvoice.setRetryInvoiceJson(retryTaskJson);
+            // 处理DAO更新结果,按你的风格打印控制台信息
+            workInvoiceDao.updateRedInvoiceJsonByWorkInvoiceId(workInvoice);
+
         } catch (Exception e) {
             e.printStackTrace();
             System.err.println("❌ 存入9998重试任务到Redis失败,订单号:" + workInvoiceId + ",原因:" + e.getMessage());
@@ -375,7 +447,29 @@ public class OMSDisposeService {
             jedis.hPut(redisKey, "workInvoiceId", workInvoiceId);
             jedis.hPut(redisKey, "informType", informType);
             jedis.hPut(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
-            jedis.expire(redisKey, seconds); // 1天过期
+            jedis.expire(redisKey, 7200); // 1天过期
+
+            // ========== 追加:封装Redis数据→转JSON→更新发票表(和红冲方法逻辑一致) ==========
+            long redisStoreTime = System.currentTimeMillis() / 1000; // 秒级存储时间,用于后期回滚
+            // 封装Map:和Redis中存储的键值完全一致,方便后期回滚解析
+            Map<String, Object> downloadTaskMap = new HashMap<>(10);
+            downloadTaskMap.put("accessToken", accessToken);
+            downloadTaskMap.put("workInvoiceId", workInvoiceId);
+            downloadTaskMap.put("informType", informType);
+            downloadTaskMap.put("firstExecTime", String.valueOf(System.currentTimeMillis())); // 和Redis保持一致的毫秒数字符串
+            downloadTaskMap.put("redisKey", redisKey);
+            downloadTaskMap.put("redisExpireSeconds", 7200);
+            downloadTaskMap.put("redisStoreTime", redisStoreTime); // 新增秒级存储时间
+
+            // Map转JSON字符串(单独捕获序列化异常,不影响核心逻辑)
+            String downloadTaskJson = objectMapper.writeValueAsString(downloadTaskMap);
+
+            // 更新发票表(和红冲方法用相同的DAO/实体,保持风格统一)
+            CwFinanceInvoiceDTO workInvoice = new CwFinanceInvoiceDTO();
+            workInvoice.setId(workInvoiceId);
+            workInvoice.setBlueDownloadInvoiceJson(downloadTaskJson);
+            workInvoiceDao.updateRedInvoiceJsonByWorkInvoiceId(workInvoice);
+
         } catch (Exception e) {
             e.printStackTrace();
         } finally {
@@ -435,7 +529,7 @@ public class OMSDisposeService {
                 // 获取AccessToken 9998重试5次
                 accessToken = getOmsAccessTokenWithRetry(10, "accessToken", workInvoiceId, informType);
                 if(StringUtils.isNotBlank(accessToken)){
-                    jedis.setEx("OMSAccessToken", accessToken,seconds,TimeUnit.SECONDS);
+                    jedis.setEx("OMSAccessToken", accessToken,86400,TimeUnit.SECONDS);
                     map.put("token状态", "重新获取token成功,存入Redis");
                 } else {
                     accessToken = "";
@@ -498,7 +592,7 @@ public class OMSDisposeService {
                 // 获取AccessToken 9998重试5次
                 accessToken = getOmsAccessTokenWithRetry(10, "accessToken", workInvoiceId, informType);
                 if(StringUtils.isNotBlank(accessToken)){
-                    jedis.setEx("OMSAccessToken", accessToken,seconds,TimeUnit.SECONDS);
+                    jedis.setEx("OMSAccessToken", accessToken,86400,TimeUnit.SECONDS);
                     map.put("token状态", "重新获取token成功,存入Redis");
                 } else {
                     accessToken = "";
@@ -511,16 +605,16 @@ public class OMSDisposeService {
 
             OMSNationUtil util = new OMSNationUtil();
             //获取需要开票的发票信息
-            CwFinanceInvoiceDTO workInvoice = workInvoiceDao.queryById(workInvoiceId);
+            CwFinanceInvoiceDTO workInvoice = workInvoiceService.queryById(workInvoiceId);
 
             if(null != workInvoice){
 
-                if(StringUtils.isBlank(workInvoice.getBillingId()) ){
+                if(StringUtils.isBlank(workInvoice.getBillingPeopleReal()) ){
                     handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
                     return;
                 }
                 //获取开票银行信息
-                if(StringUtils.isNotBlank(workInvoice.getBankAccount())){
+                if(StringUtils.isBlank(workInvoice.getBankAccount())){
                     handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
                     return;
                 }
@@ -540,7 +634,7 @@ public class OMSDisposeService {
 
                     //获取本次红冲对应蓝票的基本信息
                     CwFinanceInvoiceDTO workInvoiceRelevancy = workInvoiceService.queryById(redInvoiceRelevancyId);
-                    if(null == workInvoiceRelevancy || null == workInvoiceRelevancy.getBillingId()){
+                    if(null == workInvoiceRelevancy || null == workInvoiceRelevancy.getBillingPeopleReal()){
                         handleInvoiceRetryAllFail("",workInvoiceId, "未找到需要红冲票的信息,请确认后重新发起", informType); // 解析失败直接兜底
                         return;
                     }
@@ -643,6 +737,7 @@ public class OMSDisposeService {
      * @param applyNo   发票id
      * @param startTime 这个任务开始时间。限制:开始3天内如果双方没有全部确认,则本次红冲失败
      */
+    @Transactional(readOnly = false)
     public void saveRedInvoiceTaskToRedis(String workInvoiceId, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime, String informType) {
         RedisUtils jedis = null;
         try {
@@ -656,6 +751,32 @@ public class OMSDisposeService {
             jedis.hPut(redisKey, "informType", informType);
             jedis.hPut(redisKey, "startTime", String.valueOf(startTime));
             jedis.expire(redisKey, 259200 + 3600); // 3天+1小时过期
+
+
+
+            //将对应的redis中数据进行存储到数据库中,放置redis数据崩溃丢失
+            // 1. 封装Redis中所有存储的参数到Map(和Redis键值完全一致,方便后期回滚解析)
+            long redisStoreTime = System.currentTimeMillis() / 1000; // 核心:精确到秒,long类型
+            Map<String, Object> redInvoiceMap = new HashMap<>(); // 容量从8改为10,适配新字段
+            redInvoiceMap.put("remainRetryTimes", remainRetryTimes);
+            redInvoiceMap.put("jsonInvoicResultStr", jsonInvoicResultStr);
+            redInvoiceMap.put("accessToken", accessToken);
+            redInvoiceMap.put("workInvoiceId", workInvoiceId);
+            redInvoiceMap.put("informType", informType);
+            redInvoiceMap.put("startTime", startTime);
+            redInvoiceMap.put("redisKey", redisKey);
+            redInvoiceMap.put("redisExpireSeconds", 259200 + 3600);
+            redInvoiceMap.put("redisStoreTime", redisStoreTime); // 新增:Redis存储时间(秒级时间戳)
+
+            // 2. Map转JSON字符串(Spring内置Jackson,无额外依赖)
+            String redInvoiceJson = objectMapper.writeValueAsString(redInvoiceMap);
+
+            CwFinanceInvoiceDTO workInvoice = new CwFinanceInvoiceDTO();
+            workInvoice.setId(workInvoiceId);
+            workInvoice.setRedInvoiceJson(redInvoiceJson);
+            // 3. 更新现有发票表的JSON字段(根据发票ID精准更新)
+            workInvoiceDao.updateRedInvoiceJsonByWorkInvoiceId(workInvoice);
+
         } catch (Exception e) {
             e.printStackTrace();
             handleInvoiceRetryAllFail(accessToken, workInvoiceId, "全类型红冲--红字确认单查询接口发起失败,请重新发起", informType); // 解析失败也执行兜底方法

+ 24 - 9
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/OMS/RedInvoiceDownloadService.java

@@ -24,6 +24,7 @@ import java.util.Base64;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
+import static com.jeeplus.finance.invoice.config.OmsConfig.*;
 
 /**
  * OMS发票下载
@@ -37,14 +38,14 @@ public class RedInvoiceDownloadService {
     //private static final String appId = Global.getConfig("omsAppId");
     //private static final String appKey = Global.getConfig("omsAppKey");
     //private static final String deptCode = Global.getConfig("omsDeptCode");
-    @Value("${omsAppId}")
-    private String appId;
-    @Value("${omsAppKey}")
-    private String appKey;
-    @Value("${omsDeptCode}")
-    private String deptCode;
-    @Value("${omsUrl}")
-    private String omsUrl;
+    //@Value("${omsAppId}")
+    //private String appId;
+    //@Value("${omsAppKey}")
+    //private String appKey;
+    //@Value("${omsDeptCode}")
+    //private String deptCode;
+    //@Value("${omsUrl}")
+    //private String omsUrl;
     @Autowired
     private OMSDisposeService omsDisposeService;
 
@@ -54,6 +55,7 @@ public class RedInvoiceDownloadService {
     private CwFinanceInvoiceMapper workInvoiceDetailDao;
 
 
+    @Transactional(readOnly = false)
     public void processRedInvoiceDownloadTasks() {
         RedisUtils jedis = null;
         try {
@@ -104,7 +106,6 @@ public class RedInvoiceDownloadService {
                     //需要修改项目相关信息
 
 
-
                     jedis.delete(taskKey); // 兜底后删除Redis,保证数据一致
                     continue;
                 }
@@ -182,6 +183,20 @@ public class RedInvoiceDownloadService {
                             }
                         }
                     }
+                }else if(null != resultDownInfo.getResult() && ("1002".equals(resultDownInfo.getResult().getCode())
+                        || "1001".equals(resultDownInfo.getResult().getCode())
+                        || "1005".equals(resultDownInfo.getResult().getCode())
+                        || "1006".equals(resultDownInfo.getResult().getCode())
+                        || "0001".equals(resultDownInfo.getResult().getCode())
+                        || "0002".equals(resultDownInfo.getResult().getCode())
+                        || "0003".equals(resultDownInfo.getResult().getCode())
+                        || "0004".equals(resultDownInfo.getResult().getCode())
+                        || "0005".equals(resultDownInfo.getResult().getCode())
+                        || "0006".equals(resultDownInfo.getResult().getCode())
+                        || "0007".equals(resultDownInfo.getResult().getCode())
+                        || "0008".equals(resultDownInfo.getResult().getCode())
+                )){
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,失败原因:" + resultDownInfo.getResult().getMessage(), informType);
                 }
             }
 

+ 1 - 3
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/OMS/RedInvoiceRetryScheduledService.java

@@ -17,6 +17,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import static com.jeeplus.finance.invoice.config.OmsConfig.*;
 
 /**
  * 用于开发票都用订单接口推送时使用,用来当返回值时9998时进行处理
@@ -39,9 +40,6 @@ public class RedInvoiceRetryScheduledService {
     @Autowired
     private CwFinanceInvoiceMapper workInvoiceDao;
 
-    @Value("${omsUrl}")
-    private String omsUrl;
-
     /**
      * 定时任务:每5分钟执行一次(硬编码300000毫秒=5分钟)
      * fixedRate:固定间隔执行,直接写死5分钟

+ 63 - 13
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/OMS/RedInvoiceScheduledService.java

@@ -2,6 +2,7 @@ package com.jeeplus.finance.invoice.service.OMS;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.jeeplus.common.redis.RedisUtils;
 import com.jeeplus.finance.invoice.domain.OMS.OMSAccessTokenInfo;
 import com.jeeplus.finance.invoice.domain.OMS.fastRed.OMSRedInvoiceConfirmResponse;
@@ -17,10 +18,9 @@ import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.Calendar;
-import java.util.Set;
-import java.util.UUID;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
+import static com.jeeplus.finance.invoice.config.OmsConfig.*;
 
 /**
  * 红字确认单查询接口回调
@@ -34,26 +34,21 @@ public class RedInvoiceScheduledService {
     //private static final String appId = Global.getConfig("omsAppId");
     //private static final String appKey = Global.getConfig("omsAppKey");
     //private static final String deptCode = Global.getConfig("omsDeptCode");
-    @Value("${omsAppId}")
-    private String appId;
-    @Value("${omsAppKey}")
-    private String appKey;
-    @Value("${omsDeptCode}")
-    private String deptCode;
-    @Value("${omsUrl}")
-    private String omsUrl;
 
     @Autowired
     private OMSDisposeService omsDisposeService;
     @Autowired
     private CwFinanceInvoiceMapper workInvoiceDao;
 
+    @Autowired
+    private ObjectMapper objectMapper;
+
     @Transactional(readOnly = false)
     public void processAllRedInvoiceTasks() {
         // 时间窗口校验
         Calendar now = Calendar.getInstance();
         int hour = now.get(Calendar.HOUR_OF_DAY);
-        if (hour < 8 || hour >= 18) {
+        if (hour < 8 || hour >= 20) {
             System.out.println("[定时任务] 非业务时段,跳过");
             return;
         }
@@ -305,7 +300,8 @@ public class RedInvoiceScheduledService {
     /**
      * 存入Redis(定时任务内部使用)
      */
-    private void saveRedInvoiceTaskToRedis(int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime, String informType) {
+    @Transactional(readOnly = false)
+    public void saveRedInvoiceTaskToRedis(int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime, String informType) {
         RedisUtils jedis = null;
         try {
             jedis = RedisUtils.getInstance();
@@ -317,6 +313,35 @@ public class RedInvoiceScheduledService {
             jedis.hPut(redisKey, "informType", informType);
             jedis.hPut(redisKey, "startTime", String.valueOf(startTime));
             jedis.expire(redisKey, 259200 + 3600);
+
+            // ========== 追加:Redis数据转JSON,持久化到发票表(兜底逻辑) ==========
+            long redisStoreTime = System.currentTimeMillis() / 1000; // 秒级存储时间,用于后期回滚
+            // 提取Redis中startTime的字符串值,Map和Redis共用,保证数据完全一致
+            String startTimeStr = String.valueOf(startTime);
+            // 封装Map:包含Redis所有字段 + redisKey/过期时间/存储时间(保留redisKey)
+            Map<String, Object> redInvoiceMap = new HashMap<>();
+            // Redis哈希中所有原字段(与Redis完全一致)
+            redInvoiceMap.put("remainRetryTimes", remainRetryTimes);
+            redInvoiceMap.put("jsonInvoicResultStr", jsonInvoicResultStr);
+            redInvoiceMap.put("accessToken", accessToken);
+            redInvoiceMap.put("applyNo", applyNo);
+            redInvoiceMap.put("informType", informType);
+            redInvoiceMap.put("startTime", startTimeStr);
+            // 额外兜底字段(保留redisKey,贴合你的要求)
+            redInvoiceMap.put("redisKey", redisKey);
+            redInvoiceMap.put("redisExpireSeconds", 259200 + 3600);
+            redInvoiceMap.put("redisStoreTime", redisStoreTime);
+
+            // Map转JSON字符串(单独捕获序列化异常,不影响Redis核心逻辑)
+            String redInvoiceJson = objectMapper.writeValueAsString(redInvoiceMap);
+
+            // 更新发票表(唯一标识为applyNo,与Redis一致)
+            CwFinanceInvoiceDTO workInvoice = new CwFinanceInvoiceDTO();
+            workInvoice.setId(applyNo); // 关键:用applyNo作为更新ID,与你的唯一标识一致
+            workInvoice.setRedInvoiceJson(redInvoiceJson);
+            // 处理DAO更新,单独捕获异常,不影响核心逻辑
+            workInvoiceDao.updateRedInvoiceJsonByWorkInvoiceId(workInvoice);
+
         } catch (Exception e) {
             e.printStackTrace();
         } finally {
@@ -349,6 +374,7 @@ public class RedInvoiceScheduledService {
      * @param accessToken
      * @param workInvoiceId 开票id
      */
+    @Transactional(readOnly = false)
     private void saveInvoiceDownloadTaskToRedis(String accessToken, String redInvOrderNo, String workInvoiceId,  String informType) {
         RedisUtils jedis = null;
         try {
@@ -360,6 +386,30 @@ public class RedInvoiceScheduledService {
             jedis.hPut(redisKey, "informType", informType);
             jedis.hPut(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
             jedis.expire(redisKey, 86400); // 1天过期
+
+            // ========== 追加:Redis数据转JSON → 持久化到发票表(兜底逻辑) ==========
+            long redisStoreTime = System.currentTimeMillis() / 1000; // 秒级存储时间,用于后期回滚
+            // 封装Map:Redis所有原字段 + redisKey/过期时间/存储时间(保留redisKey)
+            Map<String, Object> downloadTaskMap = new HashMap<>(8);
+            // Redis哈希中5个原字段(与Redis键名/值完全一致,含新增的redInvOrderNo)
+            downloadTaskMap.put("accessToken", accessToken);
+            downloadTaskMap.put("redInvOrderNo", redInvOrderNo);
+            downloadTaskMap.put("workInvoiceId", workInvoiceId);
+            downloadTaskMap.put("informType", informType);
+            downloadTaskMap.put("firstExecTime", String.valueOf(System.currentTimeMillis()));
+            // 额外兜底字段(保留redisKey,后期回滚直接取用)
+            downloadTaskMap.put("redisKey", redisKey);
+            downloadTaskMap.put("redisExpireSeconds", 86400);
+            downloadTaskMap.put("redisStoreTime", redisStoreTime);
+
+            // Map转JSON字符串(单独捕获序列化异常,不影响Redis核心逻辑)
+            String downloadTaskJson = objectMapper.writeValueAsString(downloadTaskMap);
+
+            // 更新发票表(以workInvoiceId为唯一标识,与Redis一致)
+            CwFinanceInvoiceDTO workInvoice = new CwFinanceInvoiceDTO();
+            workInvoice.setId(workInvoiceId); // 关键:与redisKey的拼接标识一致
+            workInvoice.setRedDownloadInvoiceJson(downloadTaskJson);
+            workInvoiceDao.updateRedInvoiceJsonByWorkInvoiceId(workInvoice);
         } catch (Exception e) {
             e.printStackTrace();
         } finally {

+ 7 - 0
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/service/dto/CwFinanceInvoiceDTO.java

@@ -410,5 +410,12 @@ public class CwFinanceInvoiceDTO extends BaseDTO {
     private String omsXmlUrl; //对应发票的xmlUrl
     private String OmsAttachmentUrl; //对应发票的xmlUrl
 
+    private String redInvoiceJson;//将对应的红冲redis中数据进行存储到数据库中,放置redis数据崩溃丢失
+    private String blueDownloadInvoiceJson;//将对应的蓝票redis中数据进行存储到数据库中,放置redis数据崩溃丢失
+    private String retryInvoiceJson;//将对应的9998错误redis中数据进行存储到数据库中,放置redis数据崩溃丢失
+    private String redDownloadInvoiceJson;//将对应的红票redis中数据进行存储到数据库中,放置redis数据崩溃丢失
+
+    private List<WorkAttachmentInfo> invoiceList;
+
     private static final long serialVersionUID = 1L;
 }

+ 3 - 2
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/thread/ApprovalThread.java

@@ -3,7 +3,8 @@ package com.jeeplus.finance.invoice.thread;
 
 import com.jeeplus.finance.invoice.service.OMS.OMSDisposeService;
 import com.jeeplus.finance.invoice.service.dto.CwFinanceInvoiceDTO;
-import com.jeeplus.sys.utils.SpringContextHolder;
+import com.jeeplus.finance.invoice.util.OMS.SpringContextHolderFinance;
+
 
 import java.util.HashMap;
 import java.util.Map;
@@ -14,7 +15,7 @@ import java.util.Map;
  **/
 public class ApprovalThread extends Thread {
 
-    private OMSDisposeService omsDisposeService = SpringContextHolder.getBean(OMSDisposeService.class);
+    private OMSDisposeService omsDisposeService = SpringContextHolderFinance.getBean(OMSDisposeService.class);
     private CwFinanceInvoiceDTO workInvoice;
     private String informType;
 

+ 2 - 2
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/thread/RedApprovalThread.java

@@ -2,7 +2,7 @@ package com.jeeplus.finance.invoice.thread;
 
 import com.jeeplus.finance.invoice.service.OMS.OMSDisposeService;
 import com.jeeplus.finance.invoice.service.dto.CwFinanceInvoiceDTO;
-import com.jeeplus.sys.utils.SpringContextHolder;
+import com.jeeplus.finance.invoice.util.OMS.SpringContextHolderFinance;
 import org.apache.commons.lang3.StringUtils;
 
 import java.util.HashMap;
@@ -14,7 +14,7 @@ import java.util.Map;
  **/
 public class RedApprovalThread extends Thread {
 
-    private OMSDisposeService omsDisposeService = SpringContextHolder.getBean(OMSDisposeService.class);
+    private OMSDisposeService omsDisposeService = SpringContextHolderFinance.getBean(OMSDisposeService.class);
     private CwFinanceInvoiceDTO workInvoice;
     String originalInvno;
     String informType;

+ 6 - 13
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/util/OMS/FileHandlingUtil.java

@@ -1,8 +1,8 @@
 package com.jeeplus.finance.invoice.util.OMS;
 
 
-import com.jeeplus.finance.utils.Global;
-import com.jeeplus.pubmodules.oss.service.OSSClientService;
+import cn.hutool.extra.spring.SpringUtil;
+import com.jeeplus.sys.feign.IWorkAttachmentApi;
 import com.jeeplus.utils.StringUtils;
 import okhttp3.OkHttpClient;
 import okhttp3.Request;
@@ -19,6 +19,8 @@ import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
+import static com.jeeplus.finance.invoice.config.OmsConfig.directory;
+
 public class FileHandlingUtil {
 
     private static final Logger log = LoggerFactory.getLogger(FileHandlingUtil.class);
@@ -138,7 +140,7 @@ public class FileHandlingUtil {
     }
 
 
-    private final static String directory = Global.getConfig("remoteServer.directory");
+
 
 
     private static String datePath(){
@@ -200,7 +202,6 @@ public class FileHandlingUtil {
         String originalFileName = localFile.getName();
         log.info("获取本地文件原文件名:{}", originalFileName);
 
-        OSSClientService ossClientUtil = null;
         InputStream fileInputStream = null;
         try {
             // ========== 2. 读取文件流 ==========
@@ -226,8 +227,7 @@ public class FileHandlingUtil {
             }
 
             // ========== 4. 调用原有uploadFileSignatureOSS方法,第三个参数传原文件名 ==========
-            ossClientUtil = new OSSClientService();
-            ossClientUtil.uploadFileSignatureOSS(localFile.getPath(), realPath, originalFileName);
+            SpringUtil.getBean ( IWorkAttachmentApi.class ).uploadFileSignatureOSS(localFile.getPath(), realPath, originalFileName);
 
             // ========== 5. 上传成功后删除本地文件 ==========
             boolean deleteSuccess = localFile.delete();
@@ -260,13 +260,6 @@ public class FileHandlingUtil {
                     log.error("关闭本地文件流失败", e);
                 }
             }
-            if (ossClientUtil != null) {
-                try {
-                    //ossClientUtil.();
-                } catch (Exception e) {
-                    log.error("关闭OSS客户端失败", e);
-                }
-            }
         }
 
         return resultMap;

+ 26 - 37
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/util/OMS/OMSNationUtil.java

@@ -16,12 +16,16 @@ import com.jeeplus.finance.invoice.service.dto.CwFinanceInvoiceDTO;
 import com.jeeplus.finance.utils.Global;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
 import java.util.Base64;
 import java.util.List;
 
+import static com.jeeplus.finance.invoice.config.OmsConfig.*;
+
+@Component
 public class OMSNationUtil {
 
     /**
@@ -97,22 +101,6 @@ public class OMSNationUtil {
     //private static final String bankName = Global.getConfig("omsBankName");
     //private static final String bankAccount = Global.getConfig("omsBankAccount");
 
-    @Value("${omsAppId}")
-    private String appId;
-    @Value("${omsAppKey}")
-    private String appKey;
-    @Value("${omsDeptCode}")
-    private String deptCode;
-    @Value("${omsSellerName}")
-    private String sellerName;
-    @Value("${omsSellerTaxno}")
-    private String sellerTaxno;
-    @Value("${omsBankName}")
-    private String bankName;
-    @Value("${omsBankAccount}")
-    private String bankAccount;
-    @Value("${omsUrl}")
-    private String omsUrl;
 
 
     /**
@@ -130,8 +118,9 @@ public class OMSNationUtil {
         omsImportInfo.setOrderno(workInvoiceId);
         omsImportInfo.setSellerName(sellerName);
         omsImportInfo.setSellerTaxno(sellerTaxno); //销方纳税人识别号,必填
-        //omsImportInfo.setSellerBank(bankName); //销售方开户行名称
-        //omsImportInfo.setSellerBankaccount(bankAccount); //销售方开户行账号
+        //生产环境
+        omsImportInfo.setSellerBank(bankName); //销售方开户行名称
+        omsImportInfo.setSellerBankaccount(bankAccount); //销售方开户行账号
         //* 开票类型(此处固定是蓝字票)
         //* 01:红字发票 02:蓝字发票,必填
         omsImportInfo.setInvKind("02");
@@ -146,16 +135,16 @@ public class OMSNationUtil {
         }
 
         //添加购买方信息
-        //测试
-        omsImportInfo.setBuyerName("深圳市爱人人餐饮服务有限公司");
-        omsImportInfo.setBuyerTaxno("500102203117204029");
-        //生产
-        //omsImportInfo.setBuyerName(workInvoice.getBillingWorkplaceReal());    //购买方名称
-        //omsImportInfo.setBuyerTaxno(workInvoice.getTaxpayerIdentificationNo());  //购买方信用代码
-        //omsImportInfo.setBuyerAddr(workInvoice.getAddress()); //购买方地址
-        //omsImportInfo.setBuyerPhone(workInvoice.getTelPhone()); //购买方电话
-        //omsImportInfo.setBuyerBank(workInvoice.getOpenBank()); //购买方开户行名称
-        //omsImportInfo.setBuyerBankaccount(workInvoice.getBaseAccount()); //购买方开户银行账号
+        //测试环境
+        //omsImportInfo.setBuyerName("深圳市爱人人餐饮服务有限公司");
+        //omsImportInfo.setBuyerTaxno("500102203117204029");
+        //生产环境
+        omsImportInfo.setBuyerName(workInvoice.getBillingWorkplaceReal());    //购买方名称
+        omsImportInfo.setBuyerTaxno(workInvoice.getTaxpayerIdentificationNo());  //购买方信用代码
+        omsImportInfo.setBuyerAddr(workInvoice.getAddress()); //购买方地址
+        omsImportInfo.setBuyerPhone(workInvoice.getTelPhone()); //购买方电话
+        omsImportInfo.setBuyerBank(workInvoice.getOpenBank()); //购买方开户行名称
+        omsImportInfo.setBuyerBankaccount(workInvoice.getBaseAccount()); //购买方开户银行账号
 
         //omsImportInfo.setBuyerNationality(""); //购买方国籍/地区代码
         //omsImportInfo.setBuyerIdcardno(""); //购买方证件号码
@@ -257,12 +246,12 @@ public class OMSNationUtil {
         omsAllScenarioRedInvoiceInfo.setSellerTaxno(sellerTaxno);
         omsAllScenarioRedInvoiceInfo.setSellerName(sellerName);
         //添加购买方信息
-        //测试
-        omsAllScenarioRedInvoiceInfo.setBuyerName("深圳市爱人人餐饮服务有限公司");
-        omsAllScenarioRedInvoiceInfo.setBuyerTaxno("500102203117204029");
-        //生产
-        //omsAllScenarioRedInvoiceInfo.setBuyerName(workInvoice.getBillingWorkplaceReal());    //购买方名称
-        //omsAllScenarioRedInvoiceInfo.setBuyerTaxno(workInvoice.getTaxpayerIdentificationNo());  //购买方信用代码
+        //测试环境
+        //omsAllScenarioRedInvoiceInfo.setBuyerName("深圳市爱人人餐饮服务有限公司");
+        //omsAllScenarioRedInvoiceInfo.setBuyerTaxno("500102203117204029");
+        //生产环境
+        omsAllScenarioRedInvoiceInfo.setBuyerName(workInvoice.getBillingWorkplaceReal());    //购买方名称
+        omsAllScenarioRedInvoiceInfo.setBuyerTaxno(workInvoice.getTaxpayerIdentificationNo());  //购买方信用代码
 
         //原蓝票发票号码
         omsAllScenarioRedInvoiceInfo.setOriginalInvno(originalInvno);
@@ -299,7 +288,7 @@ public class OMSNationUtil {
         BigDecimal blueTaxAmount = blueTaxIncludedAmount.subtract(blueTaxExcludedAmount);
 
         //原蓝票金额
-        omsAllScenarioRedInvoiceInfo.setOriginalInvAmount(blueTaxIncludedAmount.toString());
+        omsAllScenarioRedInvoiceInfo.setOriginalInvAmount(blueTaxExcludedAmount.toString());
 
         //原蓝票税额
         omsAllScenarioRedInvoiceInfo.setOriginalInvTax(blueTaxAmount.toString());
@@ -307,7 +296,7 @@ public class OMSNationUtil {
 
         //此处用来处理红冲对应的金额汇总
         // 1. 获取含税金额(价税合计)- 从workInvoice中取值
-        BigDecimal taxIncludedAmount =  new BigDecimal(workInvoiceRelevancy.getAccount());
+        BigDecimal taxIncludedAmount =  new BigDecimal(workInvoice.getAccount());
 
         // 2. 处理税率:优先用动态值,为空则默认6%(兼容6/0.06两种格式)
         BigDecimal taxRate;
@@ -366,7 +355,7 @@ public class OMSNationUtil {
         orderItem.setAmount(taxExcludedAmount); //金额
         orderItem.setTax(taxAmount);//税额
         orderItem.setTaxrate(BigDecimal.valueOf(workInvoiceTaxClassificationCode.getTaxRate())); //税率
-        orderItem.setTaxamount( new BigDecimal(workInvoiceRelevancy.getAccount()));//含税金额
+        orderItem.setTaxamount( new BigDecimal(workInvoice.getAccount()));//含税金额
         orderItem.setGoodstaxno(workInvoiceTaxClassificationCode.getGoodsTaxno());//税收分类编码
         orderItem.setOriLineCode("1");//对应蓝票明细序号
 

+ 86 - 0
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/invoice/util/OMS/SpringContextHolderFinance.java

@@ -0,0 +1,86 @@
+/**
+ * Copyright &copy; 2015-2020 <a href="http://www.jeeplus.org/">JeePlus</a> All rights reserved.
+ */
+package com.jeeplus.finance.invoice.util.OMS;
+
+import org.apache.commons.lang3.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+/**
+ * 以静态变量保存Spring ApplicationContext, 可在任何代码任何地方任何时候取出ApplicaitonContext.
+ *
+ * @author Zaric
+ * @date 2013-5-29 下午1:25:40
+ */
+@Service
+@Lazy(false)
+public class SpringContextHolderFinance implements ApplicationContextAware, DisposableBean {
+
+	private static ApplicationContext applicationContext = null;
+
+	private static Logger logger = LoggerFactory.getLogger(SpringContextHolderFinance.class);
+
+	/**
+	 * 取得存储在静态变量中的ApplicationContext.
+	 */
+	public static ApplicationContext getApplicationContext() {
+		assertContextInjected();
+		return applicationContext;
+	}
+
+	/**
+	 * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> T getBean(String name) {
+		assertContextInjected();
+		return (T) applicationContext.getBean(name);
+	}
+
+	/**
+	 * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
+	 */
+	public static <T> T getBean(Class<T> requiredType) {
+		assertContextInjected();
+		return applicationContext.getBean(requiredType);
+	}
+
+	/**
+	 * 清除SpringContextHolder中的ApplicationContext为Null.
+	 */
+	public static void clearHolder() {
+		if (logger.isDebugEnabled()){
+			logger.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
+		}
+		applicationContext = null;
+	}
+
+	/**
+	 * 实现ApplicationContextAware接口, 注入Context到静态变量中.
+	 */
+	@Override
+	public void setApplicationContext(ApplicationContext applicationContext) {
+		SpringContextHolderFinance.applicationContext = applicationContext;
+	}
+
+	/**
+	 * 实现DisposableBean接口, 在Context关闭时清理静态变量.
+	 */
+	@Override
+	public void destroy() throws Exception {
+		SpringContextHolderFinance.clearHolder();
+	}
+
+	/**
+	 * 检查ApplicationContext不为空.
+	 */
+	private static void assertContextInjected() {
+		Validate.validState(applicationContext != null, "applicaitonContext属性未注入, 请在applicationContext.xml中定义SpringContextHolder.");
+	}
+}

+ 9 - 0
jeeplus-modules/jeeplus-public-modules/src/main/java/com/jeeplus/pubmodules/oss/controller/OssFileController.java

@@ -386,6 +386,15 @@ public class OssFileController {
         ossClientService.downByStreamSaveLocal(key, fileName, downFileStr);
     }
 
+
+    /**
+     * 根据路径下载所有文件到本地指定文件夹
+     */
+    @RequestMapping(value = "/downloadFolderFromOss", method = RequestMethod.POST)
+    public void downloadFolderFromOss(String prefix, String localDir) {
+        ossClientService.downloadFolderFromOss(prefix, localDir);
+    }
+
     /**
      * 获取编号模板编号
      * @param companyId

+ 6 - 0
jeeplus-modules/jeeplus-public-modules/src/main/java/com/jeeplus/pubmodules/oss/feign/WorkAttachmentApiImpl.java

@@ -72,6 +72,12 @@ public class WorkAttachmentApiImpl implements IWorkAttachmentApi {
     public void downByStreamSaveLocal(String key, String fileName, String downFileStr) {
 
     }
+
+    @Override
+    public void downloadFolderFromOss(String prefix, String localDir) {
+
+    }
+
     @Override
     public byte[] downBytesByStream(String key, String fileName) {
         return null;

+ 63 - 3
jeeplus-modules/jeeplus-public-modules/src/main/java/com/jeeplus/pubmodules/oss/service/OSSClientService.java

@@ -4,9 +4,7 @@ import cn.hutool.extra.spring.SpringUtil;
 import com.aliyun.oss.OSS;
 import com.aliyun.oss.OSSClient;
 import com.aliyun.oss.OSSClientBuilder;
-import com.aliyun.oss.model.OSSObject;
-import com.aliyun.oss.model.PutObjectResult;
-import com.aliyun.oss.model.SimplifiedObjectMeta;
+import com.aliyun.oss.model.*;
 //import com.jeeplus.sys.utils.Global;
 import com.jeeplus.sys.feign.IWorkAttachmentApi;
 import lombok.extern.slf4j.Slf4j;
@@ -254,6 +252,68 @@ public class OSSClientService {
         }
     }
 
+    /**
+     * 根据路径下载所有文件到本地指定文件夹
+     */
+    public void downloadFolderFromOss(String prefix, String localDir) {
+        OSSClient ossClient = null;
+        try {
+            ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
+
+            ListObjectsRequest request = new ListObjectsRequest(bucketName);
+            request.setPrefix(prefix); // OSS 文件夹前缀
+            request.setMaxKeys(1000);
+
+            ObjectListing listing;
+
+            do {
+                listing = ossClient.listObjects(request);
+
+
+                for (OSSObjectSummary summary : listing.getObjectSummaries()) {
+                    String objectKey = summary.getKey();
+
+                    // 跳过“文件夹”
+                    if (objectKey.endsWith("/")) {
+                        continue;
+                    }
+
+                    // 本地文件路径
+                    String localFilePath = localDir + File.separator
+                            + objectKey.substring(prefix.length());
+
+                    File localFile = new File(localFilePath);
+
+                    // 创建父目录
+                    if (!localFile.getParentFile().exists()) {
+                        localFile.getParentFile().mkdirs();
+                    }
+
+                    // 下载文件
+                    OSSObject ossObject = ossClient.getObject(bucketName, objectKey);
+                    try (
+                            InputStream in = new BufferedInputStream(ossObject.getObjectContent());
+                            OutputStream out = new BufferedOutputStream(new FileOutputStream(localFile))
+                    ) {
+                        byte[] buffer = new byte[4096];
+                        int len;
+                        while ((len = in.read(buffer)) != -1) {
+                            out.write(buffer, 0, len);
+                        }
+                    }
+                }
+
+                request.setMarker(listing.getNextMarker());
+            } while (listing.isTruncated());
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+    }
 
     /**
      * 文件下载(签章系统)

+ 2 - 2
jeeplus-modules/jeeplus-system/src/main/java/com/jeeplus/sys/feign/UserApiImpl.java

@@ -67,8 +67,8 @@ public class UserApiImpl implements IUserApi {
     }
 
     @Override
-    public Map<String, Object> sendEntryRandomCodesByInvoice(String mobile, String name, String officeName, String tenantId) throws Exception {
-        return ALiYunSmsUtil.sendEntryRandomCodesByInvoice(mobile, name, officeName, tenantId);
+    public Map<String, Object> sendEntryRandomCodesByInvoice(String mobile, String number,String smsCode) throws Exception {
+        return ALiYunSmsUtil.sendEntryRandomCodesByInvoice(mobile, number,smsCode);
     }
 
 

+ 5 - 37
jeeplus-modules/jeeplus-system/src/main/java/com/jeeplus/sys/utils/ALiYunSmsUtil.java

@@ -325,13 +325,12 @@ public class ALiYunSmsUtil {
     /**
      * 开票短信通知(正式员工)
      * @param phoneNumbers  被通知人员手机号
-     * @param name  被通知人员名称
-     * @param officeName    被通知人员部门名称
-     * @param tenantId  隶属公司的tenantId
+     * @param number  发票申请编号
+     * @param smsCode  短信模板编号
      * @return
      * @throws Exception
      */
-    public static HashMap<String,Object> sendEntryRandomCodesByInvoice(String phoneNumbers, String name, String officeName, String tenantId) throws Exception {
+    public static HashMap<String,Object> sendEntryRandomCodesByInvoice(String phoneNumbers, String number,String smsCode) throws Exception {
         HashMap<String,Object> map = new HashMap<>();
         Config config = new Config()
                 .setAccessKeyId(ACCESS_KEY_ID)
@@ -339,39 +338,8 @@ public class ALiYunSmsUtil {
                 .setEndpoint(ENDPOINT);
 
         // 构造包含多个参数的JSON字符串
-        String templateParams = String.format("{\"name\":\"%s\",\"officeName\":\"%s\"}", name, officeName);
-
-        String templateCode = "";
-        switch (tenantId){
-            case "10001":
-                //评估公司
-                templateCode = "SMS_491485468";
-                break;
-            case "10002":
-                //综合管理公司
-                templateCode = "SMS_491460422";
-                break;
-            case "10003":
-                //会计业务管理公司
-                templateCode = "SMS_491515430";
-                break;
-            case "10004":
-                //中审管理公司
-                templateCode = "SMS_491400442";
-                break;
-            case "10005":
-                //兴光项目公司
-                templateCode = "SMS_491460423";
-                break;
-            case "10006":
-                //苏州分公司
-                templateCode = "SMS_491420460";
-                break;
-            default:
-                templateCode = "SMS_491450356";
-                break;
-        }
-
+        String templateParams = String.format("{\"number\":\"%s\"}", number);
+        String templateCode = smsCode;
         Client client = new Client(config);
         SendSmsRequest sendSmsRequest = new SendSmsRequest()
                 .setPhoneNumbers(phoneNumbers)

+ 30 - 4
jeeplus-modules/jeeplus-xxl-job-executor-sample/src/main/java/com/xxl/job/executor/service/jobhandler/SampleXxlJob.java

@@ -1142,7 +1142,16 @@ public class SampleXxlJob {
      */
     @XxlJob("processInvoiceDownloadTasks")
     public void processInvoiceDownloadTasks() {
-        SpringUtil.getBean(IFinanceApi.class).processInvoiceDownloadTasks();
+        try {
+            SpringUtil.getBean(IFinanceApi.class).processInvoiceDownloadTasks();
+        } catch (Exception e) {
+            logger.warn("调用 finance 失败,可能服务不存在", e);
+        }
+        try {
+            SpringUtil.getBean(IAssessApi.class).processInvoiceDownloadTasks();
+        } catch (Exception e) {
+            logger.warn("调用 assess 失败,可能服务不存在", e);
+        }
     }
 
     /**
@@ -1151,8 +1160,16 @@ public class SampleXxlJob {
      */
     @XxlJob("processRedInvoiceScheduledTask")
     public void processRedInvoiceScheduledTask() {
-        SpringUtil.getBean(IFinanceApi.class).processRedInvoiceScheduledTask();
-
+        try {
+            SpringUtil.getBean(IFinanceApi.class).processRedInvoiceScheduledTask();
+        } catch (Exception e) {
+            logger.warn("调用 finance 失败,可能服务不存在", e);
+        }
+        try {
+            SpringUtil.getBean(IAssessApi.class).processRedInvoiceScheduledTask();
+        } catch (Exception e) {
+            logger.warn("调用 assess 失败,可能服务不存在", e);
+        }
     }
 
 
@@ -1162,7 +1179,16 @@ public class SampleXxlJob {
      */
     @XxlJob("redInvoiceRetryScheduledTask")
     public void redInvoiceRetryScheduledTask() {
-        SpringUtil.getBean(IFinanceApi.class).redInvoiceRetryScheduledTask();
+        try {
+            SpringUtil.getBean(IFinanceApi.class).redInvoiceRetryScheduledTask();
+        } catch (Exception e) {
+            logger.warn("调用 finance 失败,可能服务不存在", e);
+        }
+        try {
+            SpringUtil.getBean(IAssessApi.class).redInvoiceRetryScheduledTask();
+        } catch (Exception e) {
+            logger.warn("调用 assess 失败,可能服务不存在", e);
+        }
     }
 
 }