Просмотр исходного кода

OMS发票信息部分代码上传

徐滕 1 месяц назад
Родитель
Сommit
fb23fc37f0
14 измененных файлов с 3240 добавлено и 69 удалено
  1. 297 56
      src/main/java/com/jeeplus/modules/ruralprojectrecords/web/RuralProjectSignatureOldMessageDisposeController.java
  2. 29 0
      src/main/java/com/jeeplus/modules/workcalendar/service/WorkCalendarTaskService.java
  3. 443 0
      src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSAllScenarioRedInvoiceInfo.java
  4. 298 0
      src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSApplyItem.java
  5. 131 0
      src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSFastRedInvoiceInfo.java
  6. 254 0
      src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSRedInvoiceConfirmApplyItem.java
  7. 52 0
      src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSRedInvoiceConfirmInfo.java
  8. 86 0
      src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSRedInvoiceConfirmQueryRequest.java
  9. 355 0
      src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSRedInvoiceConfirmResponse.java
  10. 163 0
      src/main/java/com/jeeplus/modules/workinvoice/service/OMS/InvoiceDownloadService.java
  11. 408 0
      src/main/java/com/jeeplus/modules/workinvoice/service/OMS/RedInvoiceRetryScheduledService.java
  12. 295 0
      src/main/java/com/jeeplus/modules/workinvoice/service/OMS/RedInvoiceScheduledService.java
  13. 155 13
      src/main/java/com/jeeplus/modules/workinvoice/utils/OMSNationUtil.java
  14. 274 0
      src/main/java/com/jeeplus/modules/workinvoice/utils/RedInvoiceScheduledTask.java

+ 297 - 56
src/main/java/com/jeeplus/modules/ruralprojectrecords/web/RuralProjectSignatureOldMessageDisposeController.java

@@ -40,16 +40,20 @@ import com.jeeplus.modules.workinvoice.entity.OMS.InvoiceDown.OMSInvoiceDetailIn
 import com.jeeplus.modules.workinvoice.entity.OMS.InvoiceOMSImportInfo;
 import com.jeeplus.modules.workinvoice.entity.OMS.OMSAccessTokenInfo;
 import com.jeeplus.modules.workinvoice.entity.OMS.OMSInvoiceResultDownloadData;
+import com.jeeplus.modules.workinvoice.entity.OMS.fastRed.OMSRedInvoiceConfirmInfo;
+import com.jeeplus.modules.workinvoice.entity.OMS.fastRed.OMSRedInvoiceConfirmResponse;
 import com.jeeplus.modules.workinvoice.entity.TemporaryInvoiceInfo;
 import com.jeeplus.modules.workinvoice.service.WorkInvoiceService;
 import com.jeeplus.modules.workinvoice.utils.HttpPostJsonUtil;
 import com.jeeplus.modules.workinvoice.utils.OMSNationUtil;
+import com.jeeplus.modules.workinvoice.utils.RedInvoiceScheduledTask;
 import com.jeeplus.modules.workinvoice.utils.ThreadPoolUtil;
 import com.jeeplus.modules.workreimbursement.service.WorkReimbursementService;
 import com.jeeplus.modules.workstaff.service.WorkStaffBasicInfoService;
 import freemarker.template.Configuration;
 import freemarker.template.Template;
 import org.activiti.engine.HistoryService;
+import org.apache.ibatis.annotations.Param;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.transaction.annotation.Transactional;
@@ -942,23 +946,33 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
 
 
     /**
+     * 给accessToken查询的有效时间设置为1天
+     */
+    private final int seconds = 86400;
+    private final String appId = "sscs";
+    private final String appKey = "sscs";
+    //如果接口访问不正确,可以循环访问的次数
+    private final int remainRetryTimes = 10;
+    /**
+     * 开具蓝票
      * OMS发票测试 完整最终版【最终最终定稿,完全匹配你的所有要求】
      * 精准码值规则:
      * 0000=成功执行下载 | 9998=5次重试/30秒 | 0003=清token从头执行 | 0001/0002/其他码=直接执行兜底方法修改系统信息
+     * @param workInvoiceId   对应的应该是发票管理中的 id  即 work_invoice表中需要开票的id(这个id应该被传到redis中进行记录,方便后期回调的时候进行处理)
+     * @return
      */
     @RequestMapping(value = "/invoiceOMSView")
     @ResponseBody
     @Transactional(readOnly = false)
-    public Map<String,Object> invoiceOMSView(){
+    public Map<String,Object> invoiceOMSView(@Param("orderno") String workInvoiceId){
         Map<String,Object> map = new HashMap<>();
         // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
-        doInvoiceBusiness(map);
+        doInvoiceBusiness(map, workInvoiceId);
         return map;
     }
 
     // ======================== 抽离核心业务流程:方便0003时从头重新调用 =========================
-    private void doInvoiceBusiness(Map<String,Object> map) {
-        int seconds = 86400;
+    private void doInvoiceBusiness(Map<String,Object> map, String workInvoiceId) {
         Jedis jedis = null;
         String accessToken = null;
         try {
@@ -966,7 +980,7 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
             accessToken = jedis.get("OMSAccessToken");
             if(StringUtils.isBlank(accessToken)){
                 // 获取AccessToken 9998重试5次
-                accessToken = getOmsAccessTokenWithRetry(5);
+                accessToken = getOmsAccessTokenWithRetry(5, "accessToken");
                 if(StringUtils.isNotBlank(accessToken)){
                     jedis.setex("OMSAccessToken", seconds, accessToken);
                     map.put("token状态", "重新获取token成功,存入Redis");
@@ -980,11 +994,12 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
             }
 
             OMSNationUtil util = new OMSNationUtil();
-            String string = util.neatenData();
+            //生成开票基础信息
+            String string = util.neatenData(workInvoiceId);
 
             OMSAccessTokenInfo InvoiceTokenInfo = new OMSAccessTokenInfo();
-            InvoiceTokenInfo.setAppId("sscs");
-            InvoiceTokenInfo.setAppKey("sscs");
+            InvoiceTokenInfo.setAppId(appId);
+            InvoiceTokenInfo.setAppKey(appKey);
             InvoiceTokenInfo.setExchangeId(UUID.randomUUID().toString());
             InvoiceTokenInfo.setAccessToken(accessToken);
             InvoiceTokenInfo.setData(string);
@@ -998,7 +1013,7 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
             if(StringUtils.isNotBlank(jsonInvoicResultStr)){
                 String finalAccessToken = accessToken;
                 String finalJsonInvoiceStr = jsonInvoiceStr;
-                executeOrderUploadRetry(5, jsonInvoicResultStr, finalJsonInvoiceStr, finalAccessToken, map);
+                executeOrderUploadRetry(remainRetryTimes, jsonInvoicResultStr, finalJsonInvoiceStr, finalAccessToken, workInvoiceId, map,"accessToken", "blueTicket");
             }
 
         } catch (Exception e) {
@@ -1012,11 +1027,11 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
     }
 
     // ======================== 原有方法1:获取AccessToken 9998重试5次【无修改】 =========================
-    private String getOmsAccessTokenWithRetry(int remainRetryTimes) {
+    private String getOmsAccessTokenWithRetry(int remainRetryTimes, String getKey) {
         try {
             OMSAccessTokenInfo tokenInfo = new OMSAccessTokenInfo();
-            tokenInfo.setAppId("sscs");
-            tokenInfo.setAppKey("sscs");
+            tokenInfo.setAppId(appId);
+            tokenInfo.setAppKey(appKey);
             tokenInfo.setExchangeId(UUID.randomUUID().toString());
             String jsonStr = JSON.toJSONString(tokenInfo);
             String accessTokenStr = HttpPostJsonUtil.doPost("https://oms-sandbox.einvoice.js.cn:7079/prod-api/server/accessToken", jsonStr);
@@ -1034,7 +1049,7 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
 
             String code = resultTokenInfo.getResult().getCode();
             if ("0000".equals(code)) {
-                String token = OMSNationUtil.extractAccessTokenFromBase64(resultTokenInfo.getData().toString());
+                String token = OMSNationUtil.extractAccessTokenFromBase64(getKey, resultTokenInfo.getData().toString());
                 System.out.println("✅ 获取AccessToken成功,重试次数剩余:"+remainRetryTimes);
                 return token;
             } else if ("9998".equals(code)) {
@@ -1042,7 +1057,7 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
                     int nextRetry = remainRetryTimes - 1;
                     System.err.println("⚠️ 获取AccessToken返回9998接口波动,30秒后重试,剩余次数:"+nextRetry);
                     Thread.sleep(30 * 1000);
-                    return getOmsAccessTokenWithRetry(nextRetry);
+                    return getOmsAccessTokenWithRetry(nextRetry,getKey);
                 } else {
                     System.err.println("❌ 获取AccessToken失败:连续5次返回9998,重试次数耗尽!");
                     return "";
@@ -1063,7 +1078,19 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
     }
 
     // ======================== 核心修改【仅这一处,完美匹配你的最新要求】 =========================
-    private void executeOrderUploadRetry(int remainRetryTimes, String jsonInvoicResultStr, String jsonInvoiceStr, String accessToken, Map<String,Object> map) {
+
+    /**
+     * 对方法进行发起并进行处理
+     * @param remainRetryTimes
+     * @param jsonInvoicResultStr
+     * @param jsonInvoiceStr
+     * @param accessToken
+     * @param orderno
+     * @param map
+     * @param getKey
+     * @param initiationType    用来判定是什么类型的,比如蓝票、全类型红票、还是快捷红票
+     */
+    private void executeOrderUploadRetry(int remainRetryTimes, String jsonInvoicResultStr, String jsonInvoiceStr, String accessToken, String orderno, Map<String,Object> map, String getKey, String initiationType) {
         String jsonInvoicResult = "";
         try {
             OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(jsonInvoicResultStr, OMSAccessTokenInfo.class);
@@ -1073,16 +1100,25 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
                 return;
             }
             String code = resultTokenInfo.getResult().getCode();
+            String message = resultTokenInfo.getResult().getMessage();
 
             // ======================== 所有码值规则 全部在这里【最终定稿】=========================
             if ("0000".equals(code)) {
                 // ✅ 0000 成功:解析数据+存入map+触发30秒后异步下载发票
-                jsonInvoicResult = OMSNationUtil.extractAccessTokenFromBase64(resultTokenInfo.getData().toString());
+                jsonInvoicResult = OMSNationUtil.extractAccessTokenFromBase64(getKey, resultTokenInfo.getData().toString());
                 map.put("订单接口返回值",jsonInvoicResult);
                 System.out.println("✅ 订单上传返回0000成功,触发发票下载接口");
-                ThreadPoolUtil.executeDelay(30, () -> {
-                    executeInvoiceDownloadWithRetry(5, accessToken);
-                });
+                if(StringUtils.isNotBlank(jsonInvoicResult)){
+                    if(initiationType.equals("fastRed")){
+                        orderno = jsonInvoicResult;
+                    }
+                }
+                String finalOrderno = orderno;
+                // 存入Redis,由InvoiceDownloadTask接管
+                saveInvoiceDownloadTaskToRedis(accessToken, finalOrderno);
+                System.out.println("✅ 解析开票数据任务["+finalOrderno+"]已存入Redis,由InvoiceDownloadTask接管重试");
+
+
             } else if ("9998".equals(code)) {
                 // ✅ 9998 接口波动:延迟30秒+重新调用接口+重试次数-1,最多5次
                 if (remainRetryTimes > 1) {
@@ -1091,7 +1127,7 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
                     Thread.sleep(30 * 1000);
                     String newResultStr = HttpPostJsonUtil.doPost("https://oms-sandbox.einvoice.js.cn:7079/prod-api/output/server/order/upload", jsonInvoiceStr);
                     map.put("订单接口信息-重试"+(6-nextRetry), newResultStr);
-                    executeOrderUploadRetry(nextRetry, newResultStr, jsonInvoiceStr, accessToken, map);
+                    executeOrderUploadRetry(nextRetry, newResultStr, jsonInvoiceStr, accessToken, orderno, map,getKey, initiationType);
                 } else {
                     System.err.println("❌ 订单上传失败:连续5次9998,重试次数耗尽!");
                     handleInvoiceRetryAllFail(accessToken); // 5次9998耗尽也执行兜底方法
@@ -1110,10 +1146,11 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
                 } finally {
                     if(jedis != null) jedis.close();
                 }
-                doInvoiceBusiness(map);
+                doInvoiceBusiness(map, orderno);
             } else {
                 // ✅ ✅ ✅ 核心修正:0001/0002/其他任意错误码 → 直接调用handleInvoiceRetryAllFail执行修改系统信息逻辑 ✅ ✅ ✅
                 System.err.println("❌ 订单上传返回业务错误码:"+code+"(0001/0002等),立即执行失败兜底逻辑修改系统信息!");
+                System.err.println("❌ 订单上传返回业务错误原因:"+message);
                 map.put("订单状态", "失败,错误码:"+code);
                 handleInvoiceRetryAllFail(accessToken); // 关键:直接执行你的修改逻辑,不抛异常、不重试
             }
@@ -1128,14 +1165,21 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
         }
     }
 
-    // ======================== 原有方法2:发票下载接口重试5次【无任何修改,一行未动】 =========================
-    private void executeInvoiceDownloadWithRetry(int remainingRetryTimes, String accessToken) {
+
+    /**
+     * 临时访问接口
+     * @param accessToken
+     * @param orderno
+     */
+    @RequestMapping(value = "/executeInvoiceDownload")
+    @ResponseBody
+    @Transactional(readOnly = false)
+    public void executeInvoiceDownload(String accessToken, String orderno) {
         try {
-            System.out.println("✅ 延迟执行成功,剩余重试次数:" + remainingRetryTimes + ",开始调用发票下载接口");
 
             OMSInvoiceResultDownloadData getInvoiceInfo = new OMSInvoiceResultDownloadData();
             getInvoiceInfo.setDeptCode("7777");
-            getInvoiceInfo.setOrderno("fff79669f3774c2a8be557e909a746d5");
+            getInvoiceInfo.setOrderno(orderno);
             getInvoiceInfo.setIsDetail("1");
             String jsonInvoiceInfoStr = JSON.toJSONString(getInvoiceInfo);
             String base64Str = Base64.getEncoder().encodeToString(jsonInvoiceInfoStr.getBytes(StandardCharsets.UTF_8));
@@ -1152,7 +1196,6 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
             System.out.println("发票开票结果invoiceResultStr:" + 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())){
@@ -1160,44 +1203,39 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
                     if(StringUtils.isNotBlank(invoceDownJsonStr)){
                         OMSInvoiceDetailInfo invoiceInfo = JSONObject.parseObject(invoceDownJsonStr, OMSInvoiceDetailInfo.class);
                         if (invoiceInfo != null) {
-                            System.out.println("✅ 第"+(6-remainingRetryTimes)+"次调用发票接口成功,拿到有效数据!");
-                            System.out.println("getOfdUrl" + invoiceInfo.getOfdUrl());
-                            System.out.println("getPdfUrl" + invoiceInfo.getPdfUrl());
-                            System.out.println("getXmlUrl" + invoiceInfo.getXmlUrl());
-                            isSuccess = true;
+                            System.out.println("✅ 第1次调用发票接口成功,拿到有效数据!");
+                            System.out.println("allEinVno:" + invoiceInfo.getAllEinVno());  //数电票号码
+                            System.out.println("getOfdUrl:" + invoiceInfo.getOfdUrl());
+                            System.out.println("getPdfUrl:" + invoiceInfo.getPdfUrl());
+                            System.out.println("getXmlUrl:" + invoiceInfo.getXmlUrl());
                         }
                     }
                 }
             }
 
-            if (isSuccess) {
-                System.out.println("✅ 发票接口调用成功,重试流程结束,执行后续业务");
-                return;
-            } else {
-                if (remainingRetryTimes > 1) {
-                    int nextRetryTimes = remainingRetryTimes - 1;
-                    System.out.println("❌ 发票接口返回结果异常/数据未就绪,准备重试!剩余重试次数:" + nextRetryTimes);
-                    ThreadPoolUtil.executeDelay(30, () -> {
-                        executeInvoiceDownloadWithRetry(nextRetryTimes, accessToken);
-                    });
-                } else {
-                    System.err.println("❌ 发票接口调用失败,已重试5次全部失败,触发系统信息修改逻辑!");
-                    handleInvoiceRetryAllFail(accessToken);
-                }
-            }
+
 
         } catch (Exception e) {
             e.printStackTrace();
-            System.err.println("❌ 调用发票接口抛出异常,剩余重试次数:" + remainingRetryTimes);
-            if (remainingRetryTimes > 1) {
-                int nextRetryTimes = remainingRetryTimes - 1;
-                ThreadPoolUtil.executeDelay(30, () -> {
-                    executeInvoiceDownloadWithRetry(nextRetryTimes, accessToken);
-                });
-            } else {
-                System.err.println("❌ 发票接口调用抛出异常,已重试5次全部失败,触发系统信息修改逻辑!");
-                handleInvoiceRetryAllFail(accessToken);
-            }
+        }
+    }
+
+
+
+    // ========== 新增:解析开票数据Redis操作(仅新增,不修改原有) ==========
+    private void saveInvoiceDownloadTaskToRedis(String accessToken, String orderno) {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            String redisKey = "OMS_invoice_download:" + orderno;
+            jedis.hset(redisKey, "accessToken", accessToken);
+            jedis.hset(redisKey, "orderno", orderno);
+            jedis.hset(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
+            jedis.expire(redisKey, 86400); // 1天过期
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
         }
     }
 
@@ -1228,4 +1266,207 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
         }
     }
 
+
+    /**
+     * 开具红票(快速红冲)
+     * OMS发票测试 完整最终版【最终最终定稿,完全匹配你的所有要求】
+     * 精准码值规则:
+     * 0000=成功执行下载 | 9998=5次重试/30秒 | 0003=清token从头执行 | 0001/0002/其他码=直接执行兜底方法修改系统信息
+     */
+    @RequestMapping(value = "/invoiceFastRedOMSView")
+    @ResponseBody
+    @Transactional(readOnly = false)
+    public Map<String,Object> invoiceFastRedOMSView(String allEinvno){
+        Map<String,Object> map = new HashMap<>();
+        // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
+        doFastRedInvoiceBusiness(map, allEinvno);
+        return map;
+    }
+
+    // ======================== 抽离核心业务流程:方便0003时从头重新调用 =========================
+    private void doFastRedInvoiceBusiness(Map<String,Object> map, String allEinvno) {
+        int seconds = 86400;
+        Jedis jedis = null;
+        String accessToken = null;
+        try {
+            jedis = JedisUtils.getResource();
+            accessToken = jedis.get("OMSAccessToken");
+            if(StringUtils.isBlank(accessToken)){
+                // 获取AccessToken 9998重试5次
+                accessToken = getOmsAccessTokenWithRetry(5, "accessToken");
+                if(StringUtils.isNotBlank(accessToken)){
+                    jedis.setex("OMSAccessToken", seconds, accessToken);
+                    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("sscs");
+            InvoiceTokenInfo.setAppKey("sscs");
+            InvoiceTokenInfo.setExchangeId(UUID.randomUUID().toString());
+            InvoiceTokenInfo.setAccessToken(accessToken);
+            InvoiceTokenInfo.setData(string);
+            String jsonInvoiceStr = JSON.toJSONString(InvoiceTokenInfo);
+
+            String jsonInvoicResultStr = HttpPostJsonUtil.doPost("https://oms-sandbox.einvoice.js.cn:7079/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(5, jsonInvoicResultStr, finalJsonInvoiceStr, finalAccessToken, allEinvno, map, "orderno", "fastRed");
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            map.put("errorMsg", "系统异常:" + e.getMessage());
+        } finally {
+            if(jedis != null){
+                jedis.close();
+            }
+        }
+    }
+
+
+    /**
+     * 开具红票(全场景红冲,包含已入账红冲处理。我方发起)
+     * @param applyNo   申请单号
+     * @return
+     */
+    @RequestMapping(value = "/invoiceAllScenarioRedOMSView")
+    @ResponseBody
+    @Transactional(readOnly = false)
+    public Map<String,Object> invoiceAllScenarioRedOMSView(String applyNo, String originalInvno){
+        Map<String,Object> map = new HashMap<>();
+        // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
+        doAllScenarioRedInvoiceBusiness(map, applyNo, originalInvno);
+        return map;
+    }
+
+    /**
+     * 调用生成红字确认申请单
+     * @param map
+     * @param applyNo
+     * @param originalInvno
+     */
+    private void doAllScenarioRedInvoiceBusiness(Map<String,Object> map, String applyNo, String originalInvno) {
+        int seconds = 86400;
+        Jedis jedis = null;
+        String accessToken = null;
+        try {
+            jedis = JedisUtils.getResource();
+            accessToken = jedis.get("OMSAccessToken");
+            if(StringUtils.isBlank(accessToken)){
+                // 获取AccessToken 9998重试5次
+                accessToken = getOmsAccessTokenWithRetry(5, "accessToken");
+                if(StringUtils.isNotBlank(accessToken)){
+                    jedis.setex("OMSAccessToken", seconds, accessToken);
+                    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.neatenAllScenarioRedInvoiceData(applyNo, originalInvno);
+
+            OMSAccessTokenInfo InvoiceTokenInfo = new OMSAccessTokenInfo();
+            InvoiceTokenInfo.setAppId("sscs");
+            InvoiceTokenInfo.setAppKey("sscs");
+            InvoiceTokenInfo.setExchangeId(UUID.randomUUID().toString());
+            InvoiceTokenInfo.setAccessToken(accessToken);
+            InvoiceTokenInfo.setData(string);
+            String jsonInvoiceStr = JSON.toJSONString(InvoiceTokenInfo);
+
+            String jsonInvoicResultStr = HttpPostJsonUtil.doPost("https://oms-sandbox.einvoice.js.cn:7079/prod-api/output/server/redApply/apply", jsonInvoiceStr);
+            System.out.println("✅ 全场景红冲订单提交接口返回值:" + jsonInvoicResultStr);
+            map.put("全场景红冲订单接口信息", jsonInvoicResultStr);
+
+            // 调用订单上传重试方法(包含所有码值规则)
+            if(StringUtils.isNotBlank(jsonInvoicResultStr)){
+                String finalAccessToken = accessToken;
+                queryRedInvoiceConfirm (5, jsonInvoicResultStr, finalAccessToken, applyNo);
+            }
+
+        } 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 applyNo
+     */
+    private void queryRedInvoiceConfirm (int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo) {
+        try {
+            OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(jsonInvoicResultStr, OMSAccessTokenInfo.class);
+            if(null == resultTokenInfo || null == resultTokenInfo.getResult()){
+                System.err.println("❌ 全场景红冲订单提交接口返回值解析失败,剩余重试次数:"+remainRetryTimes);
+                handleInvoiceRetryAllFail(accessToken); // 解析失败直接兜底
+                return;
+            }
+            String code = resultTokenInfo.getResult().getCode();
+
+            if ("0000".equals(code)) {
+                // 仅存入Redis,删除所有业务执行逻辑
+                saveRedInvoiceTaskToRedis(remainRetryTimes, jsonInvoicResultStr, accessToken, applyNo, System.currentTimeMillis());
+                System.out.println("✅ 红冲任务["+applyNo+"]已存入Redis,由定时任务接管执行");
+            } else {
+                String message = resultTokenInfo.getResult().getMessage();
+                System.err.println("❌ 订单上传返回业务错误码:"+code+",触发兜底逻辑!");
+                handleInvoiceRetryAllFail(accessToken);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            handleInvoiceRetryAllFail(accessToken);
+        }
+    }
+
+    /**
+     * 存储红冲任务到Redis(供原有业务代码调用,参数完整)
+     */
+    private void saveRedInvoiceTaskToRedis(int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime) {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            String redisKey = "red_invoice_task:" + applyNo;
+            // 存储所有执行所需的参数(确保定时任务能独立执行)
+            jedis.hset(redisKey, "remainRetryTimes", String.valueOf(remainRetryTimes));
+            jedis.hset(redisKey, "jsonInvoicResultStr", jsonInvoicResultStr);
+            jedis.hset(redisKey, "accessToken", accessToken);
+            jedis.hset(redisKey, "applyNo", applyNo);
+            jedis.hset(redisKey, "startTime", String.valueOf(startTime));
+            jedis.expire(redisKey, 259200 + 3600); // 3天+1小时过期
+        } catch (Exception e) {
+            e.printStackTrace();
+            handleInvoiceRetryAllFail(accessToken);
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
 }

+ 29 - 0
src/main/java/com/jeeplus/modules/workcalendar/service/WorkCalendarTaskService.java

@@ -13,6 +13,8 @@ import com.jeeplus.modules.sys.service.UserService;
 import com.jeeplus.modules.sys.utils.UserUtils;
 import com.jeeplus.modules.workcalendar.entity.WorkCalendar;
 import com.jeeplus.modules.workinvoice.entity.WorkInvoice;
+import com.jeeplus.modules.workinvoice.service.OMS.InvoiceDownloadService;
+import com.jeeplus.modules.workinvoice.service.OMS.RedInvoiceScheduledService;
 import com.jeeplus.modules.workinvoice.service.WorkInvoiceService;
 import com.jeeplus.modules.workstaff.service.WorkStaffBasicInfoService;
 import org.slf4j.Logger;
@@ -62,6 +64,12 @@ public class WorkCalendarTaskService  {
     @Autowired
     private MilitaryIndustryConfidentialityService militaryIndustryConfidentialityService;
 
+    @Autowired
+    private InvoiceDownloadService invoiceDownloadService;
+
+    @Autowired
+    private RedInvoiceScheduledService redInvoiceScheduledService;
+
     //@Scheduled(cron= "0 0/1 * * * ?")
     public void notifyTask() {
         logger.info("-----------定时任务开始------------------");
@@ -482,4 +490,25 @@ public class WorkCalendarTaskService  {
         System.out.println("文件清理完成,保留 handSignature 文件夹");
     }
 
+
+    // ========== 定时扫描Redis:每5分钟执行一次解析任务 ==========
+
+    /**
+     * 用于发票开票获取数电票信息处理
+     * 和开票系统相关的定时任务
+     */
+    @Scheduled(cron = "0 */2 * * * ?")
+    public void processInvoiceDownloadTasks() {
+        invoiceDownloadService.processInvoiceDownloadTasks();
+    }
+
+    /**
+     * 用于发票开票获取数电票信息处理
+     * 和开票系统相关的定时任务
+     */
+    @Scheduled(cron = "0 */3 * * * ?")
+    public void processRedInvoiceScheduledTask() {
+        redInvoiceScheduledService.processAllRedInvoiceTasks();
+    }
+
 }

+ 443 - 0
src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSAllScenarioRedInvoiceInfo.java

@@ -0,0 +1,443 @@
+package com.jeeplus.modules.workinvoice.entity.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
src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSApplyItem.java

@@ -0,0 +1,298 @@
+package com.jeeplus.modules.workinvoice.entity.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
src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSFastRedInvoiceInfo.java

@@ -0,0 +1,131 @@
+package com.jeeplus.modules.workinvoice.entity.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
src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSRedInvoiceConfirmApplyItem.java

@@ -0,0 +1,254 @@
+package com.jeeplus.modules.workinvoice.entity.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
src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSRedInvoiceConfirmInfo.java

@@ -0,0 +1,52 @@
+package com.jeeplus.modules.workinvoice.entity.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
src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSRedInvoiceConfirmQueryRequest.java

@@ -0,0 +1,86 @@
+package com.jeeplus.modules.workinvoice.entity.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;
+    }
+}

+ 355 - 0
src/main/java/com/jeeplus/modules/workinvoice/entity/OMS/fastRed/OMSRedInvoiceConfirmResponse.java

@@ -0,0 +1,355 @@
+package com.jeeplus.modules.workinvoice.entity.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;
+    }
+}

+ 163 - 0
src/main/java/com/jeeplus/modules/workinvoice/service/OMS/InvoiceDownloadService.java

@@ -0,0 +1,163 @@
+package com.jeeplus.modules.workinvoice.service.OMS;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.jeeplus.common.utils.JedisUtils;
+import com.jeeplus.modules.workinvoice.entity.OMS.InvoiceDown.OMSInvoiceDetailInfo;
+import com.jeeplus.modules.workinvoice.entity.OMS.OMSAccessTokenInfo;
+import com.jeeplus.modules.workinvoice.entity.OMS.OMSInvoiceResultDownloadData;
+import com.jeeplus.modules.workinvoice.utils.HttpPostJsonUtil;
+import com.jeeplus.modules.workinvoice.utils.OMSNationUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import redis.clients.jedis.Jedis;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Set;
+import java.util.UUID;
+
+@Service
+@Transactional(readOnly = true)
+@Lazy
+public class InvoiceDownloadService {
+
+    public void processInvoiceDownloadTasks() {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            Set<String> downloadTaskKeys = jedis.keys("OMS_invoice_download:*");
+            if (downloadTaskKeys.isEmpty()) {
+                System.out.println("[InvoiceDownloadTask] 暂无待解析的发票任务");
+                return;
+            }
+
+            // 遍历所有解析任务
+            for (String taskKey : downloadTaskKeys) {
+                String orderno = taskKey.replace("OMS_invoice_download:", "");
+                String accessToken = jedis.hget(taskKey, "accessToken");
+                //获取当前redis中的accessToken
+                //如果有值,则使用当前redis中的accessToken,否则重新生成一个新的accessToken
+
+
+                String firstExecTimeStr = jedis.hget(taskKey, "firstExecTime");
+
+                // 参数校验
+                if (StringUtils.isAnyBlank(accessToken, firstExecTimeStr)) {
+                    jedis.del(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] 任务["+orderno+"]已重试23小时,触发兜底");
+                    handleInvoiceRetryAllFail(accessToken);
+                    //需要修改项目相关信息
+
+
+
+                    jedis.del(taskKey); // 兜底后删除Redis,保证数据一致
+                    continue;
+                }
+
+                // ========== 核心:调用原始解析方法(移至此) ==========
+                executeInvoiceDownloadWithRetry(accessToken, orderno);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
+    // ========== 原始解析方法(完全保留核心逻辑,仅调整重试规则) ==========
+    private void executeInvoiceDownloadWithRetry(String accessToken, String orderno) {
+        try {
+            System.out.println("✅ [InvoiceDownloadTask] 开始解析发票,订单号:" + orderno);
+
+            // ========== 原始解析逻辑 —— 完全不变 ==========
+            OMSInvoiceResultDownloadData getInvoiceInfo = new OMSInvoiceResultDownloadData();
+            getInvoiceInfo.setDeptCode("7777");
+            getInvoiceInfo.setOrderno(orderno);
+            getInvoiceInfo.setIsDetail("1");
+            String jsonInvoiceInfoStr = JSON.toJSONString(getInvoiceInfo);
+            String base64Str = Base64.getEncoder().encodeToString(jsonInvoiceInfoStr.getBytes(StandardCharsets.UTF_8));
+
+            OMSAccessTokenInfo invoiceDownInfo = new OMSAccessTokenInfo();
+            invoiceDownInfo.setAppId("sscs");
+            invoiceDownInfo.setAppKey("sscs");
+            invoiceDownInfo.setExchangeId(UUID.randomUUID().toString());
+            invoiceDownInfo.setAccessToken(accessToken);
+            invoiceDownInfo.setData(base64Str);
+            String jsonInvoiceDownStr = JSON.toJSONString(invoiceDownInfo);
+
+            String invoiceResultStr = HttpPostJsonUtil.doPost(
+                    "https://oms-sandbox.einvoice.js.cn:7079/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) {
+                            System.out.println("✅ [InvoiceDownloadTask] 解析成功!");
+                            System.out.println("allEinVno:" + invoiceInfo.getAllEinVno());
+                            System.out.println("ofdUrl:" + invoiceInfo.getOfdUrl());
+                            System.out.println("pdfUrl:" + invoiceInfo.getPdfUrl());
+                            System.out.println("xmlUrl:" + invoiceInfo.getXmlUrl());
+                            isSuccess = true;
+                        }
+                    }
+                }
+            }
+
+            // ========== 重试规则调整:去掉次数限制,改为23小时超时(由定时任务控制) ==========
+            if (isSuccess) {
+                System.out.println("✅ [InvoiceDownloadTask] 任务["+orderno+"]解析成功,删除Redis任务");
+                deleteInvoiceDownloadTaskFromRedis(orderno); // 成功删除Redis,保证数据一致
+                return;
+            } else {
+                System.out.println("❌ [InvoiceDownloadTask] 任务["+orderno+"]解析失败,5分钟后重试");
+                // 无需手动延迟,由定时任务每5分钟扫描即可
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ [InvoiceDownloadTask] 任务["+orderno+"]解析抛出异常,5分钟后重试");
+            // 异常不删除Redis,由定时任务继续重试
+        }
+    }
+
+    // ========== 辅助方法:删除Redis解析任务 ==========
+    private void deleteInvoiceDownloadTaskFromRedis(String orderno) {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            jedis.del("OMS_invoice_download:" + orderno);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
+
+
+    /**
+     * 兜底逻辑
+     */
+    public void handleInvoiceRetryAllFail(String accessToken) {
+        System.err.println("📢 执行失败兜底逻辑:更新系统状态 + 通知发起人");
+        // 此处替换为你的实际兜底逻辑(如更新数据库、发送通知等)
+    }
+}

+ 408 - 0
src/main/java/com/jeeplus/modules/workinvoice/service/OMS/RedInvoiceRetryScheduledService.java

@@ -0,0 +1,408 @@
+package com.jeeplus.modules.workinvoice.service.OMS;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.jeeplus.common.utils.JedisUtils;
+import com.jeeplus.modules.workinvoice.entity.OMS.OMSAccessTokenInfo;
+import com.jeeplus.modules.workinvoice.entity.OMS.fastRed.OMSRedInvoiceConfirmResponse;
+import com.jeeplus.modules.workinvoice.utils.HttpPostJsonUtil;
+import com.jeeplus.modules.workinvoice.utils.OMSNationUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import redis.clients.jedis.Jedis;
+
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * 红冲发票定时重试任务处理类(无时段/超时限制版)
+ * 核心规则:
+ * 1. 每2分钟执行一次定时任务(无时段限制)
+ * 2. 单个任务最多重试20次(每次间隔2分钟,由定时任务频率控制)
+ * 3. 仅判断Redis中是否有任务:有则执行,无则跳过
+ * 4. 9998错误纳入重试计数,次数用尽后按失败处理+删除Redis任务
+ */
+@Service
+@Transactional(readOnly = true)
+@Lazy(false) // 非懒加载,项目启动即初始化
+public class RedInvoiceRetryScheduledService {
+
+    // ========== 核心配置常量(仅保留重试次数限制) ==========
+    /** 最大重试次数:20次 */
+    private static final int MAX_RETRY_TIMES = 20;
+    /** Redis任务Key前缀 */
+    private static final String REDIS_TASK_PREFIX = "red_invoice_task:";
+
+    // ========== 定时任务入口(每2分钟执行一次,无时段限制) ==========
+    @Scheduled(cron = "0 */2 * * * ?") // cron表达式:每2分钟执行一次
+    public void processRedInvoiceRetryTask() {
+        Jedis jedis = null;
+        try {
+            // 1. 获取Redis连接,扫描所有待处理任务
+            jedis = JedisUtils.getResource();
+            Set<String> taskKeys = jedis.keys(REDIS_TASK_PREFIX + "*");
+            if (taskKeys.isEmpty()) {
+                System.out.println("[红冲发票定时任务] 暂无待处理的红冲发票任务");
+                return;
+            }
+            System.out.println("[红冲发票定时任务] 发现待处理任务数:" + taskKeys.size());
+
+            // 2. 遍历处理每个任务(有值就执行,无则跳过)
+            for (String taskKey : taskKeys) {
+                String applyNo = taskKey.replace(REDIS_TASK_PREFIX, "");
+                handleSingleRedInvoiceTask(jedis, applyNo, taskKey);
+            }
+        } catch (Exception e) {
+            System.err.println("[红冲发票定时任务] 批量处理任务异常:" + e.getMessage());
+            e.printStackTrace();
+        } finally {
+            // 3. 释放Redis连接
+            if (jedis != null) {
+                try {
+                    jedis.close();
+                } catch (Exception e) {
+                    System.err.println("[红冲发票定时任务] 关闭Redis连接异常:" + e.getMessage());
+                }
+            }
+        }
+    }
+
+    // ========== 处理单个红冲发票任务(无超时限制) ==========
+    private void handleSingleRedInvoiceTask(Jedis jedis, String applyNo, String taskKey) {
+        try {
+            // 1. 读取Redis中任务参数
+            String remainRetryTimesStr = jedis.hget(taskKey, "remainRetryTimes");
+            String jsonInvoicResultStr = jedis.hget(taskKey, "jsonInvoicResultStr");
+            String accessToken = jedis.hget(taskKey, "accessToken");
+            String getKey = jedis.hget(taskKey, "getKey"); // 解析AccessToken的key
+
+            // 2. 重试次数初始化&校验(仅保留20次限制)
+            int remainRetryTimes = StringUtils.isBlank(remainRetryTimesStr) ? MAX_RETRY_TIMES : Integer.parseInt(remainRetryTimesStr);
+
+            // 2.1 重试次数用尽 → 触发失败逻辑+删除Redis
+            if (remainRetryTimes <= 0) {
+                System.err.println("[红冲发票任务] 任务[" + applyNo + "]重试次数用尽(20次),触发失败逻辑");
+                triggerTaskFailLogic(jedis, taskKey, accessToken, "重试次数用尽(20次)");
+                return;
+            }
+
+            // 3. 优先获取/刷新AccessToken(适配9998重试逻辑)
+            AccessTokenResult tokenResult = getOmsAccessTokenWithRetry(remainRetryTimes, getKey);
+            if (tokenResult.isSuccess()) {
+                // 3.1 AccessToken获取成功 → 更新Redis中的token
+                accessToken = tokenResult.getToken();
+                jedis.hset(taskKey, "accessToken", accessToken);
+            } else if (tokenResult.isNeedRetry()) {
+                // 3.2 AccessToken需要重试 → 更新重试次数,等待下次定时任务
+                updateTaskRetryTimes(jedis, taskKey, tokenResult.getRemainRetryTimes(), jsonInvoicResultStr, accessToken);
+                System.out.println("[红冲发票任务] 任务[" + applyNo + "]AccessToken需重试,剩余次数:" + tokenResult.getRemainRetryTimes());
+                return;
+            } else if (tokenResult.getRemainRetryTimes() <= 0) {
+                // 3.3 AccessToken重试次数用尽 → 触发失败逻辑
+                triggerTaskFailLogic(jedis, taskKey, accessToken, "AccessToken重试次数用尽");
+                return;
+            }
+
+            // 4. 执行核心业务逻辑(红冲发票查询)
+            boolean taskSuccess = executeRedInvoiceBusinessLogic(applyNo, accessToken, jsonInvoicResultStr);
+            if (taskSuccess) {
+                // 5. 任务成功 → 删除Redis任务
+                jedis.del(taskKey);
+                System.out.println("[红冲发票任务] 任务[" + applyNo + "]处理成功,已删除Redis任务");
+            } else {
+                // 6. 任务未成功 → 重试次数-1,更新Redis
+                int newRetryTimes = remainRetryTimes - 1;
+                updateTaskRetryTimes(jedis, taskKey, newRetryTimes, jsonInvoicResultStr, accessToken);
+                System.out.println("[红冲发票任务] 任务[" + applyNo + "]处理未成功,剩余重试次数:" + newRetryTimes);
+            }
+        } catch (Exception e) {
+            System.err.println("[红冲发票任务] 处理单个任务[" + applyNo + "]异常:" + e.getMessage());
+            e.printStackTrace();
+            // 异常时重试次数-1,更新Redis
+            try {
+                String remainRetryTimesStr = jedis.hget(taskKey, "remainRetryTimes");
+                int remainRetryTimes = StringUtils.isBlank(remainRetryTimesStr) ? MAX_RETRY_TIMES : Integer.parseInt(remainRetryTimesStr);
+                int newRetryTimes = remainRetryTimes - 1;
+                updateTaskRetryTimes(jedis, taskKey, newRetryTimes, jedis.hget(taskKey, "jsonInvoicResultStr"), jedis.hget(taskKey, "accessToken"));
+            } catch (Exception ex) {
+                System.err.println("[红冲发票任务] 更新重试次数异常:" + ex.getMessage());
+            }
+        }
+    }
+
+    // ========== 执行核心业务逻辑(红冲发票查询) ==========
+    private boolean executeRedInvoiceBusinessLogic(String applyNo, String accessToken, String jsonInvoicResultStr) {
+        try {
+            // 1. 首次执行:解析初始化结果(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 + "]初始化结果解析失败");
+                    return false;
+                }
+            }
+
+            // 2. 组装查询参数
+            OMSNationUtil util = new OMSNationUtil();
+            String queryData = util.neatenAllScenarioRedInvoiceConfirmQueryData(applyNo);
+            OMSAccessTokenInfo invoiceDownInfo = new OMSAccessTokenInfo();
+            invoiceDownInfo.setAppId("sscs");
+            invoiceDownInfo.setAppKey("sscs");
+            invoiceDownInfo.setExchangeId(UUID.randomUUID().toString());
+            invoiceDownInfo.setAccessToken(accessToken);
+            invoiceDownInfo.setData(queryData);
+            String jsonInvoiceDownStr = JSON.toJSONString(invoiceDownInfo);
+
+            // 3. 调用OMS红字确认单查询接口
+            String invoiceResultStr = HttpPostJsonUtil.doPost(
+                    "https://oms-sandbox.einvoice.js.cn:7079/prod-api/output/server/redApply/query",
+                    jsonInvoiceDownStr
+            );
+            if (StringUtils.isBlank(invoiceResultStr)) {
+                System.err.println("[红冲发票业务] 任务[" + applyNo + "]查询接口返回空");
+                return false;
+            }
+
+            // 4. 解析查询结果
+            OMSAccessTokenInfo resultDownInfo = JSON.parseObject(invoiceResultStr, OMSAccessTokenInfo.class);
+            if (null == resultDownInfo.getResult()) {
+                System.err.println("[红冲发票业务] 任务[" + applyNo + "]查询结果无返回体");
+                return false;
+            }
+
+            String resultCode = resultDownInfo.getResult().getCode();
+            // 4.1 接口返回成功(0000)→ 解析发票状态
+            if ("0000".equals(resultCode)) {
+                String invoceDownJsonStr = OMSNationUtil.extractFromBase64OnClassStr(resultDownInfo.getData().toString());
+                OMSRedInvoiceConfirmResponse invoiceInfo = JSONObject.parseObject(invoceDownJsonStr, OMSRedInvoiceConfirmResponse.class);
+
+                // 打印调试信息
+                System.out.println("[红冲发票业务] 任务[" + applyNo + "]查询结果:");
+                System.out.println("  - 确认状态:" + invoiceInfo.getConfirmStatus());
+                System.out.println("  - 开票状态:" + invoiceInfo.getMakeStatus());
+                System.out.println("  - 失败原因:" + invoiceInfo.getFailCause());
+
+                // 5. 判断发票状态是否处理完成
+                return judgeInvoiceStatusCompleted(invoiceInfo, applyNo, accessToken);
+            }
+            // 4.2 接口返回9998(接口波动)→ 纳入重试
+            else if ("9998".equals(resultCode)) {
+                System.err.println("[红冲发票业务] 任务[" + applyNo + "]查询接口返回9998(接口波动),需重试");
+                return false;
+            }
+            // 4.3 其他错误码 → 任务失败
+            else {
+                System.err.println("[红冲发票业务] 任务[" + applyNo + "]查询接口返回错误码:" + resultCode);
+                return false;
+            }
+        } catch (Exception e) {
+            System.err.println("[红冲发票业务] 任务[" + applyNo + "]执行异常:" + e.getMessage());
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    // ========== 判断发票状态是否处理完成 ==========
+    private boolean judgeInvoiceStatusCompleted(OMSRedInvoiceConfirmResponse invoiceInfo, String applyNo, String accessToken) {
+        String confirmStatus = invoiceInfo.getConfirmStatus();
+        String makeStatus = invoiceInfo.getMakeStatus();
+
+        // 1. 确认状态为无需确认/已确认(01/04)
+        if ("01".equals(confirmStatus) || "04".equals(confirmStatus)) {
+            // 1.1 开票状态为已开票(03)→ 任务完成
+            if ("3".equals(makeStatus)) {
+                String redInvOrderNo = invoiceInfo.getRedInvOrderNo();
+                // 存入下载任务Redis,由下载任务接管(保留你的原有逻辑)
+                saveInvoiceDownloadTaskToRedis(accessToken, redInvOrderNo);
+                System.out.println("[红冲发票业务] 任务[" + applyNo + "]开票完成,订单号:" + redInvOrderNo);
+                return true;
+            }
+            // 1.2 开票状态为开票中(01/04)→ 需重试
+            else if ("1".equals(makeStatus) || "4".equals(makeStatus)) {
+                System.err.println("[红冲发票业务] 任务[" + applyNo + "]开票中,需继续重试");
+                return false;
+            }
+            // 1.3 其他开票状态 → 任务失败
+            else {
+                System.err.println("[红冲发票业务] 任务[" + applyNo + "]开票状态异常:" + makeStatus);
+                return false;
+            }
+        }
+        // 2. 确认状态为待确认/申请中(02/03/15)→ 需重试
+        else if ("02".equals(confirmStatus) || "03".equals(confirmStatus) || "15".equals(confirmStatus)) {
+            System.err.println("[红冲发票业务] 任务[" + applyNo + "]待确认/申请中,需继续重试");
+            return false;
+        }
+        // 3. 确认状态为作废/失败(05/06/07/08/09/10/16)→ 任务失败
+        else if ("05".equals(confirmStatus) || "06".equals(confirmStatus) || "07".equals(confirmStatus) ||
+                "08".equals(confirmStatus) || "09".equals(confirmStatus) || "10".equals(confirmStatus) || "16".equals(confirmStatus)) {
+            System.err.println("[红冲发票业务] 任务[" + applyNo + "]作废/申请失败,状态:" + confirmStatus);
+            return false;
+        }
+        // 4. 未知状态 → 任务失败
+        else {
+            System.err.println("[红冲发票业务] 任务[" + applyNo + "]未知确认状态:" + confirmStatus);
+            return false;
+        }
+    }
+
+    // ========== AccessToken获取(适配9998重试,20次限制) ==========
+    private AccessTokenResult getOmsAccessTokenWithRetry(int remainRetryTimes, String getKey) {
+        // 兼容getKey为空的场景
+        if (StringUtils.isBlank(getKey)) {
+            System.err.println("[AccessToken] getKey为空,无需重试");
+            return new AccessTokenResult(false, "", false, remainRetryTimes);
+        }
+
+        try {
+            // 1. 组装请求参数
+            OMSAccessTokenInfo tokenInfo = new OMSAccessTokenInfo();
+            tokenInfo.setAppId("sscs"); // 替换为你的实际appId
+            tokenInfo.setAppKey("sscs"); // 替换为你的实际appKey
+            tokenInfo.setExchangeId(UUID.randomUUID().toString());
+            String jsonStr = JSON.toJSONString(tokenInfo);
+
+            // 2. 调用AccessToken接口
+            String accessTokenStr = HttpPostJsonUtil.doPost(
+                    "https://oms-sandbox.einvoice.js.cn:7079/prod-api/server/accessToken",
+                    jsonStr
+            );
+
+            // 3. 空值校验
+            if (StringUtils.isBlank(accessTokenStr)) {
+                System.err.println("获取AccessToken失败:接口返回空,剩余重试次数:"+remainRetryTimes);
+                return new AccessTokenResult(false, "", false, remainRetryTimes);
+            }
+
+            // 4. 解析结果
+            OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(accessTokenStr, OMSAccessTokenInfo.class);
+            if (null == resultTokenInfo || null == resultTokenInfo.getResult()) {
+                System.err.println("获取AccessToken失败:返回结果解析异常,剩余重试次数:"+remainRetryTimes);
+                return new AccessTokenResult(false, "", false, remainRetryTimes);
+            }
+
+            String code = resultTokenInfo.getResult().getCode();
+            // 5. 0000成功:返回token
+            if ("0000".equals(code)) {
+                String token = OMSNationUtil.extractAccessTokenFromBase64(getKey, resultTokenInfo.getData().toString());
+                System.out.println("✅ 获取AccessToken成功,重试次数剩余:"+remainRetryTimes);
+                return new AccessTokenResult(true, token, false, remainRetryTimes);
+            }
+            // 6. 9998错误:判断是否需要重试
+            else if ("9998".equals(code)) {
+                if (remainRetryTimes > 1) {
+                    int nextRetry = remainRetryTimes - 1;
+                    System.err.println("⚠️ 获取AccessToken返回9998接口波动,剩余重试次数:"+nextRetry);
+                    return new AccessTokenResult(false, "", true, nextRetry);
+                } else {
+                    System.err.println("❌ 获取AccessToken失败:连续20次返回9998,重试次数耗尽!");
+                    return new AccessTokenResult(false, "", false, 0);
+                }
+            }
+            // 7. 其他错误码:返回失败
+            else {
+                System.err.println("❌ 获取AccessToken失败:返回业务错误码,code="+code);
+                return new AccessTokenResult(false, "", false, remainRetryTimes);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ 获取AccessToken失败:调用接口异常,剩余重试次数:"+remainRetryTimes);
+            // 异常场景:判断是否还有重试次数
+            if (remainRetryTimes > 1) {
+                return new AccessTokenResult(false, "", true, remainRetryTimes - 1);
+            } else {
+                return new AccessTokenResult(false, "", false, 0);
+            }
+        }
+    }
+
+    // ========== 辅助方法(保留你的原有逻辑) ==========
+    /**
+     * 任务失败兜底逻辑
+     */
+    private void triggerTaskFailLogic(Jedis jedis, String taskKey, String accessToken, String failReason) {
+        try {
+            // 1. 执行失败兜底(可扩展:更新数据库/发送通知)
+            System.err.println("[红冲发票失败兜底] 任务失败原因:" + failReason + ",执行兜底逻辑");
+            handleInvoiceRetryAllFail(accessToken);
+
+            // 2. 删除Redis任务
+            jedis.del(taskKey);
+            System.out.println("[红冲发票失败兜底] 已删除Redis任务:" + taskKey);
+        } catch (Exception e) {
+            System.err.println("[红冲发票失败兜底] 执行异常:" + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 更新Redis任务重试次数(移除startTime,无超时限制)
+     */
+    private void updateTaskRetryTimes(Jedis jedis, String taskKey, int newRetryTimes, String jsonInvoicResultStr, String accessToken) {
+        try {
+            jedis.hset(taskKey, "remainRetryTimes", String.valueOf(newRetryTimes));
+            jedis.hset(taskKey, "jsonInvoicResultStr", StringUtils.defaultString(jsonInvoicResultStr));
+            jedis.hset(taskKey, "accessToken", StringUtils.defaultString(accessToken));
+            // 仅保留Redis过期时间(避免任务永久残留)
+            jedis.expire(taskKey, 86400 * 7); // 7天过期(可自定义)
+        } catch (Exception e) {
+            System.err.println("[红冲发票任务] 更新Redis重试次数异常:" + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 兜底逻辑(可扩展)
+     */
+    public void handleInvoiceRetryAllFail(String accessToken) {
+        // 此处实现失败后的具体逻辑:更新数据库/发送通知等
+        System.err.println("[兜底逻辑] 执行失败处理:accessToken=" + accessToken);
+    }
+
+    /**
+     * 保存发票下载任务到Redis(完全保留你的原有方法)
+     */
+    private void saveInvoiceDownloadTaskToRedis(String accessToken, String orderno) {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            String redisKey = "OMS_invoice_download:" + orderno;
+            jedis.hset(redisKey, "accessToken", accessToken);
+            jedis.hset(redisKey, "orderno", orderno);
+            jedis.hset(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
+            jedis.expire(redisKey, 86400); // 1天过期
+            System.out.println("[下载任务] 已存入Redis:" + redisKey);
+        } catch (Exception e) {
+            System.err.println("[下载任务] 存入Redis异常:" + e.getMessage());
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
+    /**
+     * AccessToken获取结果封装类
+     */
+    private static class AccessTokenResult {
+        private boolean success; // 是否获取成功
+        private String token;    // 成功时返回token
+        private boolean needRetry; // 是否需要重试
+        private int remainRetryTimes; // 剩余重试次数
+
+        public AccessTokenResult(boolean success, String token, boolean needRetry, int remainRetryTimes) {
+            this.success = success;
+            this.token = token;
+            this.needRetry = needRetry;
+            this.remainRetryTimes = remainRetryTimes;
+        }
+
+        // getter
+        public boolean isSuccess() { return success; }
+        public String getToken() { return token; }
+        public boolean isNeedRetry() { return needRetry; }
+        public int getRemainRetryTimes() { return remainRetryTimes; }
+    }
+}

+ 295 - 0
src/main/java/com/jeeplus/modules/workinvoice/service/OMS/RedInvoiceScheduledService.java

@@ -0,0 +1,295 @@
+package com.jeeplus.modules.workinvoice.service.OMS;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.jeeplus.common.utils.JedisUtils;
+import com.jeeplus.modules.workinvoice.entity.OMS.OMSAccessTokenInfo;
+import com.jeeplus.modules.workinvoice.entity.OMS.fastRed.OMSRedInvoiceConfirmResponse;
+import com.jeeplus.modules.workinvoice.utils.HttpPostJsonUtil;
+import com.jeeplus.modules.workinvoice.utils.OMSNationUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import redis.clients.jedis.Jedis;
+
+import java.util.Calendar;
+import java.util.Set;
+import java.util.UUID;
+
+@Service
+@Transactional(readOnly = true)
+@Lazy
+public class RedInvoiceScheduledService {
+
+    public void processAllRedInvoiceTasks() {
+        // 时间窗口校验
+        Calendar now = Calendar.getInstance();
+        int hour = now.get(Calendar.HOUR_OF_DAY);
+        if (hour < 8 || hour >= 18) {
+            System.out.println("[定时任务] 非业务时段,跳过");
+            return;
+        }
+
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            Set<String> taskKeys = jedis.keys("red_invoice_task:*");
+            if (taskKeys.isEmpty()) {
+                System.out.println("[定时任务] 暂无待处理任务");
+                return;
+            }
+
+            // 遍历处理所有Redis任务
+            for (String taskKey : taskKeys) {
+                String applyNo = taskKey.replace("red_invoice_task:", "");
+                // 读取Redis参数
+                String remainRetryTimesStr = jedis.hget(taskKey, "remainRetryTimes");
+                String jsonInvoicResultStr = jedis.hget(taskKey, "jsonInvoicResultStr");
+                String accessToken = jedis.hget(taskKey, "accessToken");
+                String startTimeStr = jedis.hget(taskKey, "startTime");
+
+                // 参数校验
+                if (StringUtils.isAnyBlank(remainRetryTimesStr, accessToken, startTimeStr)) {
+                    jedis.del(taskKey);
+                    continue;
+                }
+
+                // 转换参数并执行任务
+                int remainRetryTimes = Integer.parseInt(remainRetryTimesStr);
+                long startTime = Long.parseLong(startTimeStr);
+                executeRedInvoiceTask(applyNo, remainRetryTimes, jsonInvoicResultStr, accessToken, startTime);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
+
+    // ========== 1. 封装所有需要的业务逻辑(独立执行,不依赖原有业务类) ==========
+    /**
+     * 核心:执行单个红冲任务的全流程逻辑
+     */
+    private void executeRedInvoiceTask(String applyNo, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, long startTime) {
+        try {
+            // 步骤1:3天超时判断
+            if (System.currentTimeMillis() - startTime > 259200000L) {
+                System.err.println("❌ 任务["+applyNo+"]超时,触发兜底逻辑");
+                handleInvoiceRetryAllFail(accessToken);
+                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+"]解析失败,触发兜底");
+                    handleInvoiceRetryAllFail(accessToken);
+                    deleteRedInvoiceTaskFromRedis(applyNo);
+                    return;
+                }
+                // 解析成功,调用查询接口
+                callRedInvoiceConfirmQuery(remainRetryTimes, accessToken, applyNo, startTime);
+                return;
+            }
+
+            // 步骤3:后续重试(无jsonInvoicResultStr)→ 直接调用查询接口
+            callRedInvoiceConfirmQuery(remainRetryTimes, accessToken, applyNo, startTime);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            // 异常后重试次数-1,重新存入Redis
+            if (remainRetryTimes > 1) {
+                saveRedInvoiceTaskToRedis(remainRetryTimes - 1, jsonInvoicResultStr, accessToken, applyNo, startTime);
+            } else {
+                handleInvoiceRetryAllFail(accessToken);
+                deleteRedInvoiceTaskFromRedis(applyNo);
+            }
+        }
+    }
+
+    /**
+     * 调用红字确认单查询接口(核心业务逻辑)
+     */
+    private void callRedInvoiceConfirmQuery(int remainRetryTimes, String accessToken, String applyNo, long startTime) {
+        try {
+            // 组装查询参数
+            OMSNationUtil util = new OMSNationUtil();
+            String queryData = util.neatenAllScenarioRedInvoiceConfirmQueryData(applyNo);
+            OMSAccessTokenInfo invoiceDownInfo = new OMSAccessTokenInfo();
+            invoiceDownInfo.setAppId("sscs");
+            invoiceDownInfo.setAppKey("sscs");
+            invoiceDownInfo.setExchangeId(UUID.randomUUID().toString());
+            invoiceDownInfo.setAccessToken(accessToken);
+            invoiceDownInfo.setData(queryData);
+            String jsonInvoiceDownStr = JSON.toJSONString(invoiceDownInfo);
+
+            // 调用查询接口
+            String invoiceResultStr = HttpPostJsonUtil.doPost("https://oms-sandbox.einvoice.js.cn:7079/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"));
+            }
+
+            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);
+                        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);
+                    } else {
+                        handleInvoiceRetryAllFail(accessToken);
+                        deleteRedInvoiceTaskFromRedis(applyNo);
+                    }
+                    break;
+                case "02":
+                case "03":
+                case "15":
+                    // 待确认,重新存入Redis等待下次重试
+                    saveRedInvoiceTaskToRedis(remainRetryTimes, "", accessToken, applyNo, startTime);
+                    break;
+
+                case "05":
+                case "06":
+                case "07":
+                case "08":
+                case "09":
+                case "10":
+                case "16":
+                    // 终止本次红冲操作,通知发起人确认后重新处理
+                    handleInvoiceRetryAllFail(accessToken);
+                    // 失败后删除Redis任务
+                    deleteRedInvoiceTaskFromRedis(applyNo);
+                    break;
+                default:
+                    //终止本次红冲操作,通知发起人确认后重新处理
+                    // 失败状态,兜底+删除Redis
+                    handleInvoiceRetryAllFail(accessToken);
+                    deleteRedInvoiceTaskFromRedis(applyNo);
+                    break;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            // 接口调用失败,重试次数-1后重新存入Redis
+            if (remainRetryTimes > 1) {
+                saveRedInvoiceTaskToRedis(remainRetryTimes - 1, "", accessToken, applyNo, startTime);
+            } else {
+                handleInvoiceRetryAllFail(accessToken);
+                deleteRedInvoiceTaskFromRedis(applyNo);
+            }
+        }
+    }
+
+    /**
+     * 兜底逻辑
+     */
+    public void handleInvoiceRetryAllFail(String accessToken) {
+        System.err.println("📢 执行失败兜底逻辑:更新系统状态 + 通知发起人");
+        // 此处替换为你的实际兜底逻辑(如更新数据库、发送通知等)
+    }
+
+    /**
+     * 存入Redis(定时任务内部使用)
+     */
+    private void saveRedInvoiceTaskToRedis(int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime) {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            String redisKey = "red_invoice_task:" + applyNo;
+            jedis.hset(redisKey, "remainRetryTimes", String.valueOf(remainRetryTimes));
+            jedis.hset(redisKey, "jsonInvoicResultStr", jsonInvoicResultStr);
+            jedis.hset(redisKey, "accessToken", accessToken);
+            jedis.hset(redisKey, "applyNo", applyNo);
+            jedis.hset(redisKey, "startTime", String.valueOf(startTime));
+            jedis.expire(redisKey, 259200 + 3600);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
+    /**
+     * 删除Redis任务
+     */
+    private void deleteRedInvoiceTaskFromRedis(String applyNo) {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            jedis.del("red_invoice_task:" + applyNo);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
+
+
+    // ========== 新增:解析开票数据Redis操作(仅新增,不修改原有) ==========
+    private void saveInvoiceDownloadTaskToRedis(String accessToken, String orderno) {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            String redisKey = "OMS_invoice_download:" + orderno;
+            jedis.hset(redisKey, "accessToken", accessToken);
+            jedis.hset(redisKey, "orderno", orderno);
+            jedis.hset(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
+            jedis.expire(redisKey, 86400); // 1天过期
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+}

+ 155 - 13
src/main/java/com/jeeplus/modules/workinvoice/utils/OMSNationUtil.java

@@ -6,6 +6,10 @@ import com.alibaba.fastjson.JSONObject;
 import com.google.common.collect.Lists;
 import com.jeeplus.modules.workinvoice.entity.OMS.InvoiceOMSImportInfo;
 import com.jeeplus.modules.workinvoice.entity.OMS.OrderItem;
+import com.jeeplus.modules.workinvoice.entity.OMS.fastRed.OMSAllScenarioRedInvoiceInfo;
+import com.jeeplus.modules.workinvoice.entity.OMS.fastRed.OMSApplyItem;
+import com.jeeplus.modules.workinvoice.entity.OMS.fastRed.OMSFastRedInvoiceInfo;
+import com.jeeplus.modules.workinvoice.entity.OMS.fastRed.OMSRedInvoiceConfirmQueryRequest;
 import org.apache.commons.lang3.StringUtils;
 
 import java.math.BigDecimal;
@@ -17,8 +21,8 @@ public class OMSNationUtil {
 
     public static void main(String[] args) {
         OMSNationUtil util = new OMSNationUtil();
-        String string = util.neatenData();
-        String accessToken = extractAccessTokenFromBase64(string);
+        String string = util.neatenData("");
+        String accessToken = extractAccessTokenFromBase64("accessToken", string);
         String string1 = HttpPostJsonUtil.doPost("https://oms-sandbox.einvoice.js.cn:7079/prod-api/output/server/order/upload", accessToken);
         System.out.println("✅ 提取到的accessToken:" + accessToken);
         System.out.println("✅ 提取到的string1:" + string1);
@@ -27,7 +31,7 @@ public class OMSNationUtil {
     /**
      * 从Base64编码的JSON响应中提取accessToken
      */
-    public static String extractAccessTokenFromBase64(String base64Str) {
+    public static String extractAccessTokenFromBase64(String getKey, String base64Str) {
         if (base64Str == null || com.jeeplus.common.utils.StringUtils.isBlank(base64Str)) {
             return "";
         }
@@ -36,7 +40,22 @@ public class OMSNationUtil {
         String jsonStr = new String(decodeBytes, StandardCharsets.UTF_8);
         // 2. 解析JSON提取accessToken
         JSONObject jsonObject = JSONObject.parseObject(jsonStr);
-        return jsonObject.getString("accessToken");
+        return jsonObject.getString(getKey);
+    }
+
+    /**
+     * 从Base64编码的JSON响应中提取accessToken
+     */
+    public static String extractFromBase64OnClassStr(String base64Str) {
+        if (base64Str == null || com.jeeplus.common.utils.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 InvoiceOMSImportInfo getInvoiceInfo(String base64Str) {
@@ -111,14 +130,15 @@ public class OMSNationUtil {
 
 
     /**
+     * 开蓝票
      * 整理数据明文
      */
-    public String neatenData() {
+    public String neatenData(String orderno) {
         List<OrderItem> orderItems = Lists.newArrayList();
 
         InvoiceOMSImportInfo omsImportInfo = new InvoiceOMSImportInfo();
         omsImportInfo.setDeptCode("7777");
-        omsImportInfo.setOrderno("fff79669f3774c2a8be557e909a746d5");
+        omsImportInfo.setOrderno(orderno);
         omsImportInfo.setSellerName("深圳市松胜电子有限公司");
         omsImportInfo.setSellerTaxno("500102204228315131");
         omsImportInfo.setInvKind("02");
@@ -132,21 +152,37 @@ public class OMSNationUtil {
         OrderItem orderItem = new OrderItem();
         orderItem.setLineCode("1");
         orderItem.setLineType("00");
-        orderItem.setLineType("00");
         //添加商品名称
         //orderItem.setGoodsCode("1001");
-        orderItem.setGoodsName("电脑");
+        orderItem.setGoodsName("电脑配件");
         orderItem.setQty(BigDecimal.valueOf(1));
-        orderItem.setPrice(BigDecimal.valueOf(100000.00));
+        orderItem.setPrice(BigDecimal.valueOf(100.00));
         orderItem.setPriceTaxFlag("0"); //含税状态
         orderItem.setTaxrate(BigDecimal.valueOf(0.01)); //税率
-        orderItem.setAmount(BigDecimal.valueOf(100000.00)); //金额
-        orderItem.setTax(BigDecimal.valueOf(1000.00));//税额
-        orderItem.setTaxamount(BigDecimal.valueOf(101000.00));//含税金额
+        orderItem.setAmount(BigDecimal.valueOf(100.00)); //金额
+        orderItem.setTax(BigDecimal.valueOf(1.00));//税额
+        orderItem.setTaxamount(BigDecimal.valueOf(101.00));//含税金额
         orderItem.setGoodstaxno("109050901");//税收分类编码
 
-
         orderItems.add(orderItem);
+
+
+        /*OrderItem orderItem2 = new OrderItem();
+        orderItem2.setLineCode("2");
+        orderItem2.setLineType("00");
+        //添加商品名称
+        orderItem2.setGoodsName("电脑固态硬盘256G");
+        orderItem2.setQty(BigDecimal.valueOf(1));
+        orderItem2.setPrice(BigDecimal.valueOf(50.00));
+        orderItem2.setPriceTaxFlag("0"); //含税状态
+        orderItem2.setTaxrate(BigDecimal.valueOf(0.01)); //税率
+        orderItem2.setAmount(BigDecimal.valueOf(50.00)); //金额
+        orderItem2.setTax(BigDecimal.valueOf(0.5));//税额
+        orderItem2.setTaxamount(BigDecimal.valueOf(50.5));//含税金额
+        orderItem2.setGoodstaxno("109050901");//税收分类编码
+
+
+        orderItems.add(orderItem2);*/
         omsImportInfo.setOrderItems(orderItems);
         // 核心一行代码:Java对象 转 JSON格式字符串
         String jsonStr = JSON.toJSONString(omsImportInfo);
@@ -158,4 +194,110 @@ public class OMSNationUtil {
     }
 
 
+
+    /**
+     * 开红票
+     * 整理数据明文
+     */
+    public String neatenFastRedInvoiceData(String allEinvno) {
+
+        OMSFastRedInvoiceInfo omsImportInfo = new OMSFastRedInvoiceInfo();
+        omsImportInfo.setDeptCode("7777");
+        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;
+    }
+
+
+    /**
+     * 开红票(全类型)
+     * 整理数据明文
+     */
+    public String neatenAllScenarioRedInvoiceData(String applyNo, String originalInvno) {
+        List<OMSApplyItem> orderItems = Lists.newArrayList();
+
+        OMSAllScenarioRedInvoiceInfo omsAllScenarioRedInvoiceInfo = new OMSAllScenarioRedInvoiceInfo();
+        omsAllScenarioRedInvoiceInfo.setDeptCode("7777");
+        omsAllScenarioRedInvoiceInfo.setApplyNo(applyNo);
+        omsAllScenarioRedInvoiceInfo.setApplyIdentity("0");
+        omsAllScenarioRedInvoiceInfo.setSellerTaxno("500102204228315131");
+        omsAllScenarioRedInvoiceInfo.setSellerName("深圳市松胜电子有限公司");
+        //添加购买方信息
+        omsAllScenarioRedInvoiceInfo.setBuyerName("江苏兴光项目管理有限公司");
+        //原蓝票发票号码
+        omsAllScenarioRedInvoiceInfo.setOriginalInvno(originalInvno);
+        //原蓝票发票类型
+        omsAllScenarioRedInvoiceInfo.setOriginalInvType("01");
+        //原蓝票金额
+        omsAllScenarioRedInvoiceInfo.setOriginalInvAmount("100");
+        //原蓝票税额
+        omsAllScenarioRedInvoiceInfo.setOriginalInvTax("1");
+        //合计金额(不含税)
+        omsAllScenarioRedInvoiceInfo.setTotalAmount("-100");
+        //合计税额
+        omsAllScenarioRedInvoiceInfo.setTotalTax("-1");
+        //价税合计
+        omsAllScenarioRedInvoiceInfo.setTotalTaxamount("-101");
+        //redReason
+        omsAllScenarioRedInvoiceInfo.setRedReason("04");
+        //是否自动开票
+        omsAllScenarioRedInvoiceInfo.setAutoMakeInv("Y");
+
+
+        OMSApplyItem orderItem = new OMSApplyItem();
+        orderItem.setLineCode("1");
+        //添加商品名称
+        orderItem.setGoodsName("电脑配件");
+
+        //orderItem.setQty(BigDecimal.valueOf(1));
+        //orderItem.setPrice(BigDecimal.valueOf(100.00));
+
+        orderItem.setAmount(BigDecimal.valueOf(-100)); //金额
+        orderItem.setTax(BigDecimal.valueOf(-1.00));//税额
+        orderItem.setTaxrate(BigDecimal.valueOf(0.01)); //税率
+        orderItem.setTaxamount(BigDecimal.valueOf(-101.00));//含税金额
+        orderItem.setGoodstaxno("109050901");//税收分类编码
+        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("7777");
+        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;
+    }
+
 }

+ 274 - 0
src/main/java/com/jeeplus/modules/workinvoice/utils/RedInvoiceScheduledTask.java

@@ -0,0 +1,274 @@
+package com.jeeplus.modules.workinvoice.utils;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.jeeplus.modules.workinvoice.entity.OMS.OMSAccessTokenInfo;
+import com.jeeplus.modules.workinvoice.entity.OMS.fastRed.OMSRedInvoiceConfirmResponse;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.apache.commons.lang3.StringUtils;
+import redis.clients.jedis.Jedis;
+
+import java.util.Calendar;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * 红冲任务独立执行器(仅从Redis取数,全量执行业务逻辑,8:00-18:00每小时执行)
+ */
+@Component
+public class RedInvoiceScheduledTask {
+
+
+
+    // ========== 2. 定时任务入口(8:00-18:00每小时执行) ==========
+    @Scheduled(cron = "0 0 */1 * * ?")
+    public void processAllRedInvoiceTasks() {
+        // 时间窗口校验
+        Calendar now = Calendar.getInstance();
+        int hour = now.get(Calendar.HOUR_OF_DAY);
+        if (hour < 8 || hour >= 18) {
+            System.out.println("[定时任务] 非业务时段,跳过");
+            return;
+        }
+
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            Set<String> taskKeys = jedis.keys("red_invoice_task:*");
+            if (taskKeys.isEmpty()) {
+                System.out.println("[定时任务] 暂无待处理任务");
+                return;
+            }
+
+            // 遍历处理所有Redis任务
+            for (String taskKey : taskKeys) {
+                String applyNo = taskKey.replace("red_invoice_task:", "");
+                // 读取Redis参数
+                String remainRetryTimesStr = jedis.hget(taskKey, "remainRetryTimes");
+                String jsonInvoicResultStr = jedis.hget(taskKey, "jsonInvoicResultStr");
+                String accessToken = jedis.hget(taskKey, "accessToken");
+                String startTimeStr = jedis.hget(taskKey, "startTime");
+
+                // 参数校验
+                if (StringUtils.isAnyBlank(remainRetryTimesStr, accessToken, startTimeStr)) {
+                    jedis.del(taskKey);
+                    continue;
+                }
+
+                // 转换参数并执行任务
+                int remainRetryTimes = Integer.parseInt(remainRetryTimesStr);
+                long startTime = Long.parseLong(startTimeStr);
+                executeRedInvoiceTask(applyNo, remainRetryTimes, jsonInvoicResultStr, accessToken, startTime);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
+
+    // ========== 1. 封装所有需要的业务逻辑(独立执行,不依赖原有业务类) ==========
+    /**
+     * 核心:执行单个红冲任务的全流程逻辑
+     */
+    private void executeRedInvoiceTask(String applyNo, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, long startTime) {
+        try {
+            // 步骤1:3天超时判断
+            if (System.currentTimeMillis() - startTime > 259200000L) {
+                System.err.println("❌ 任务["+applyNo+"]超时,触发兜底逻辑");
+                handleInvoiceRetryAllFail(accessToken);
+                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+"]解析失败,触发兜底");
+                    handleInvoiceRetryAllFail(accessToken);
+                    deleteRedInvoiceTaskFromRedis(applyNo);
+                    return;
+                }
+                // 解析成功,调用查询接口
+                callRedInvoiceConfirmQuery(remainRetryTimes, accessToken, applyNo, startTime);
+                return;
+            }
+
+            // 步骤3:后续重试(无jsonInvoicResultStr)→ 直接调用查询接口
+            callRedInvoiceConfirmQuery(remainRetryTimes, accessToken, applyNo, startTime);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            // 异常后重试次数-1,重新存入Redis
+            if (remainRetryTimes > 1) {
+                saveRedInvoiceTaskToRedis(remainRetryTimes - 1, jsonInvoicResultStr, accessToken, applyNo, startTime);
+            } else {
+                handleInvoiceRetryAllFail(accessToken);
+                deleteRedInvoiceTaskFromRedis(applyNo);
+            }
+        }
+    }
+
+    /**
+     * 调用红字确认单查询接口(核心业务逻辑)
+     */
+    private void callRedInvoiceConfirmQuery(int remainRetryTimes, String accessToken, String applyNo, long startTime) {
+        try {
+            // 组装查询参数
+            OMSNationUtil util = new OMSNationUtil();
+            String queryData = util.neatenAllScenarioRedInvoiceConfirmQueryData(applyNo);
+            OMSAccessTokenInfo invoiceDownInfo = new OMSAccessTokenInfo();
+            invoiceDownInfo.setAppId("sscs");
+            invoiceDownInfo.setAppKey("sscs");
+            invoiceDownInfo.setExchangeId(UUID.randomUUID().toString());
+            invoiceDownInfo.setAccessToken(accessToken);
+            invoiceDownInfo.setData(queryData);
+            String jsonInvoiceDownStr = JSON.toJSONString(invoiceDownInfo);
+
+            // 调用查询接口
+            String invoiceResultStr = HttpPostJsonUtil.doPost("https://oms-sandbox.einvoice.js.cn:7079/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"));
+            }
+
+            String invoceDownJsonStr = OMSNationUtil.extractFromBase64OnClassStr(resultDownInfo.getData().toString());
+            OMSRedInvoiceConfirmResponse invoiceInfo = JSONObject.parseObject(invoceDownJsonStr, OMSRedInvoiceConfirmResponse.class);
+            String confirmStatus = invoiceInfo.getConfirmStatus();
+
+            // 判断状态,处理不同分支
+            switch (confirmStatus) {
+                case "01":
+                case "04":
+                    if ("3".equals(invoiceInfo.getMakeStatus())) {
+                        // ========== 仅此处微调:删除直接调用,改为存入Redis ==========
+                        // 原代码 ↓
+                        // ThreadPoolUtil.executeDelay(30, () -> {
+                        //     executeInvoiceDownloadWithRetry(5, accessToken, invoiceInfo.getRedInvOrderNo());
+                        // });
+                        // 新代码 ↑
+                        String redInvOrderNo = invoiceInfo.getRedInvOrderNo();
+                        // 存入Redis,由InvoiceDownloadTask接管
+                        saveInvoiceDownloadTaskToRedis(accessToken, redInvOrderNo);
+                        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);
+                    } else {
+                        handleInvoiceRetryAllFail(accessToken);
+                        deleteRedInvoiceTaskFromRedis(applyNo);
+                    }
+                    break;
+                case "02":
+                case "03":
+                case "15":
+                    // 待确认,重新存入Redis等待下次重试
+                    saveRedInvoiceTaskToRedis(remainRetryTimes, "", accessToken, applyNo, startTime);
+                    break;
+                default:
+                    // 失败状态,兜底+删除Redis
+                    handleInvoiceRetryAllFail(accessToken);
+                    deleteRedInvoiceTaskFromRedis(applyNo);
+                    break;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            // 接口调用失败,重试次数-1后重新存入Redis
+            if (remainRetryTimes > 1) {
+                saveRedInvoiceTaskToRedis(remainRetryTimes - 1, "", accessToken, applyNo, startTime);
+            } else {
+                handleInvoiceRetryAllFail(accessToken);
+                deleteRedInvoiceTaskFromRedis(applyNo);
+            }
+        }
+    }
+
+    /**
+     * 兜底逻辑
+     */
+    public void handleInvoiceRetryAllFail(String accessToken) {
+        System.err.println("📢 执行失败兜底逻辑:更新系统状态 + 通知发起人");
+        // 此处替换为你的实际兜底逻辑(如更新数据库、发送通知等)
+    }
+
+    /**
+     * 存入Redis(定时任务内部使用)
+     */
+    private void saveRedInvoiceTaskToRedis(int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime) {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            String redisKey = "red_invoice_task:" + applyNo;
+            jedis.hset(redisKey, "remainRetryTimes", String.valueOf(remainRetryTimes));
+            jedis.hset(redisKey, "jsonInvoicResultStr", jsonInvoicResultStr);
+            jedis.hset(redisKey, "accessToken", accessToken);
+            jedis.hset(redisKey, "applyNo", applyNo);
+            jedis.hset(redisKey, "startTime", String.valueOf(startTime));
+            jedis.expire(redisKey, 259200 + 3600);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
+    /**
+     * 删除Redis任务
+     */
+    private void deleteRedInvoiceTaskFromRedis(String applyNo) {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            jedis.del("red_invoice_task:" + applyNo);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
+    // ========== 3. 工具类依赖(替换为你的实际工具类) ==========
+    // 此处仅为占位,需替换为你项目中实际的工具类实例/注入
+    public static class JedisUtils {
+        public static Jedis getResource() {
+            // 替换为你的Redis连接获取逻辑
+            return null;
+        }
+    }
+
+    private static class ThreadPoolUtil {
+        public static void executeDelay(int seconds, Runnable runnable) {
+            // 替换为你的线程池延迟执行逻辑
+            new Thread(runnable).start();
+        }
+    }
+
+    // ========== 新增:解析开票数据Redis操作(仅新增,不修改原有) ==========
+    private void saveInvoiceDownloadTaskToRedis(String accessToken, String orderno) {
+        Jedis jedis = null;
+        try {
+            jedis = JedisUtils.getResource();
+            String redisKey = "red_invoice_download:" + orderno;
+            jedis.hset(redisKey, "accessToken", accessToken);
+            jedis.hset(redisKey, "orderno", orderno);
+            jedis.hset(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
+            jedis.expire(redisKey, 86400); // 1天过期
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (jedis != null) jedis.close();
+        }
+    }
+
+
+    // 其他实体类(OMSNationUtil、OMSAccessTokenInfo等)替换为你项目中的实际类
+}