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

OMS发票信息部分代码上传

徐滕 1 месяц назад
Родитель
Сommit
2b2f996515
23 измененных файлов с 1738 добавлено и 163 удалено
  1. 137 0
      src/main/java/com/jeeplus/common/oss/OSSClientUtil.java
  2. 5 4
      src/main/java/com/jeeplus/modules/ruralprojectrecords/web/RuralProjectSignatureOldMessageDisposeController.java
  3. 106 0
      src/main/java/com/jeeplus/modules/sys/utils/ALiYunSmsUtil.java
  4. 80 0
      src/main/java/com/jeeplus/modules/sys/utils/UserUtils.java
  5. 109 0
      src/main/java/com/jeeplus/modules/workfullmanage/utils/ZipUtils.java
  6. 229 3
      src/main/java/com/jeeplus/modules/workfullmanage/web/WorkFullManageController.java
  7. 12 19
      src/main/java/com/jeeplus/modules/workinvoice/service/OMS/InvoiceDownloadService.java
  8. 128 56
      src/main/java/com/jeeplus/modules/workinvoice/service/OMS/OMSDisposeService.java
  9. 12 15
      src/main/java/com/jeeplus/modules/workinvoice/service/OMS/RedInvoiceDownloadService.java
  10. 3 1
      src/main/java/com/jeeplus/modules/workinvoice/service/OMS/RedInvoiceRetryScheduledService.java
  11. 22 18
      src/main/java/com/jeeplus/modules/workinvoice/service/OMS/RedInvoiceScheduledService.java
  12. 27 5
      src/main/java/com/jeeplus/modules/workinvoice/service/WorkInvoiceAllService.java
  13. 417 6
      src/main/java/com/jeeplus/modules/workinvoice/service/WorkInvoiceService.java
  14. 7 5
      src/main/java/com/jeeplus/modules/workinvoice/thread/ApprovalThread.java
  15. 13 7
      src/main/java/com/jeeplus/modules/workinvoice/thread/RedApprovalThread.java
  16. 273 0
      src/main/java/com/jeeplus/modules/workinvoice/utils/FileHandlingUtil.java
  17. 88 16
      src/main/java/com/jeeplus/modules/workinvoice/utils/OMSNationUtil.java
  18. 6 0
      src/main/java/com/jeeplus/modules/workinvoicedetail/dao/WorkInvoiceDetailDao.java
  19. 4 2
      src/main/resources/mappings/modules/sys/WorkattachmentDao.xml
  20. 10 1
      src/main/resources/mappings/modules/workinvoice/WorkInvoiceDao.xml
  21. 7 0
      src/main/resources/mappings/modules/workinvoicedetail/WorkInvoiceDetailDao.xml
  22. 2 2
      src/main/webapp/webpage/modules/workinvoice/workInvoiceNotProjectAuditEnd.jsp
  23. 41 3
      src/main/webapp/webpage/modules/workinvoice/workInvoiceView.jsp

+ 137 - 0
src/main/java/com/jeeplus/common/oss/OSSClientUtil.java

@@ -28,6 +28,10 @@ import java.util.*;
  * Created by Meng on 2017/6/8.
  */
 public class OSSClientUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(OSSClientUtil.class);
+
+
     private static Logger logger = LoggerFactory.getLogger(OSSClientUtil.class);
     private String appData = Global.getAppData();
     private String endpoint = Global.getEndpoint();
@@ -636,6 +640,16 @@ public class OSSClientUtil {
      * @param fileName
      * @param response
      */
+    public void downLoadAttachNoprefix(String key, String fileName, HttpServletResponse response,String agent){
+        downLoadAttachNoprefix(key,fileName,response,agent,null);
+    }
+
+    /**
+     * 附件下载
+     * @param key
+     * @param fileName
+     * @param response
+     */
     public void downByStreamOnFileName(String key, String fileName, HttpServletResponse response,String agent){
         downByStreamOnFileName(key,fileName,response,agent,null);
     }
@@ -691,6 +705,49 @@ public class OSSClientUtil {
      * @param agent
      * @param bucketName
      */
+    public void downLoadAttachNoprefix(String key, String fileName, HttpServletResponse response,String agent,String bucketName){
+        try {
+            if (StringUtils.isEmpty(bucketName)) {
+                bucketName = this.bucketName;
+            }
+            String newName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20").replaceAll("%28", "\\(").replaceAll("%29", "\\)").replaceAll("%3B", ";").replaceAll("%40", "@").replaceAll("%23", "\\#").replaceAll("%26", "\\&").replaceAll("%2C", "\\,");
+            // 创建OSSClient实例
+            OSSObject ossObject = ossClient.getObject(bucketName, key);
+            BufferedInputStream in = new BufferedInputStream(ossObject.getObjectContent());
+            BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
+            newName = fileName;
+            response.setHeader("Content-Disposition","attachment;filename*=UTF-8''"+ newName);
+
+            /*if(agent != null && agent.toLowerCase().indexOf("firefox") > 0){
+                response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''"+ URLEncoder.encode(fileName,"utf-8"));
+            }else {
+                response.setHeader("Content-Disposition","attachment;filename="+ URLEncoder.encode(fileName,"utf-8"));
+            }*/
+            byte[] car=new byte[1024];
+            int L=0;
+            while((L=in.read(car))!=-1){
+                out.write(car, 0,L);
+            }
+            if(out!=null){
+                out.flush();
+                out.close();
+            }
+            if(in!=null){
+                in.close();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 附件下载
+     * @param key
+     * @param fileName
+     * @param response
+     * @param agent
+     * @param bucketName
+     */
     public void downByStreamOnFileName(String key, String fileName, HttpServletResponse response, String agent, String bucketName) {
         try {
             if (StringUtils.isEmpty(bucketName)) {
@@ -729,6 +786,86 @@ public class OSSClientUtil {
             e.printStackTrace();
         }
     }
+
+    // ========== 新增:文件夹批量下载方法(基于你的单个下载方法扩展) ==========
+    /**
+     * 下载OSS指定文件夹下的所有文件(适配2.6.0版本SDK)
+     * @param ossFolderPrefix OSS文件夹前缀(如:attachment-file/OMS_invoice/2026/KP-2026-301/,2.6.0版本无需处理开头/)
+     * @param localRootDir 本地根目录(如:D:/attachment-file/ 或 /attachment-file/)
+     * @param localSubFolder 本地子文件夹名称(如:KP-2026-301)
+     */
+    public void downFolderByStreamSaveLocal(String ossFolderPrefix, String localRootDir, String localSubFolder, HttpServletResponse response) {
+        // 1. 参数校验
+        if (ossFolderPrefix == null) {
+            log.error("OSS文件夹路径为空:{}", ossFolderPrefix);
+            return;
+        }
+        // 检查是否以"/"结尾,若不是则追加
+        if (!ossFolderPrefix.endsWith("/")) {
+            ossFolderPrefix = ossFolderPrefix + "/";
+        }
+        if (localRootDir == null || localSubFolder == null) {
+            log.error("本地根目录/子文件夹名称不能为空!");
+            return;
+        }
+
+        // 2. 构建本地目标目录并自动创建
+        String localTargetDir = localRootDir + File.separator + localSubFolder + File.separator;
+        File localTargetDirFile = new File(localTargetDir);
+        if (!localTargetDirFile.exists()) {
+            boolean mkdirsSuccess = localTargetDirFile.mkdirs();
+            if (!mkdirsSuccess) {
+                log.error("创建本地目标文件夹失败!路径:{}", localTargetDir);
+                return;
+            }
+            log.info("本地目标文件夹创建成功:{}", localTargetDir);
+        }
+
+        try {
+            // ========== 核心适配:2.6.0版本通过ListObjectsRequest设置前缀 ==========
+            ListObjectsRequest listRequest = new ListObjectsRequest(bucketName);
+            listRequest.setPrefix(ossFolderPrefix); // 设置文件夹前缀(过滤该文件夹下的所有文件)
+            listRequest.setMaxKeys(100); // 每页最多100个文件(2.6.0版本分页参数)
+            listRequest.setDelimiter(""); // 不返回公共前缀(只返回文件,不返回子文件夹)
+
+            String nextMarker = null;
+            do {
+                if (nextMarker != null) {
+                    listRequest.setMarker(nextMarker); // 设置分页标记(2.6.0版本分页)
+                }
+
+                // 2.6.0版本:通过ListObjectsRequest获取文件列表
+                ObjectListing objectListing = ossClient.listObjects(listRequest);
+
+                // 3. 遍历文件并调用单个下载方法
+                List<OSSObjectSummary> objectSummaries = objectListing.getObjectSummaries();
+                if (objectSummaries.isEmpty()) {
+                    log.warn("OSS文件夹{}下无任何文件!", ossFolderPrefix);
+                    break;
+                }
+
+                for (OSSObjectSummary summary : objectSummaries) {
+                    String ossFileKey = summary.getKey(); // OSS文件完整Key
+                    String fileName = ossFileKey.substring(ossFileKey.lastIndexOf("/") + 1); // 截取文件名
+                    String localFilePath = localTargetDir + fileName; // 本地文件路径
+
+                    // 调用你原有单个下载方法
+                    this.downByStreamSaveLocal(ossFileKey, fileName, localFilePath);
+                }
+
+                // 更新分页标记(2.6.0版本获取下一页标记)
+                nextMarker = objectListing.getNextMarker();
+            } while (nextMarker != null); // 分页遍历所有文件
+
+            log.info("OSS文件夹{}下载完成!所有文件已保存到本地{}", ossFolderPrefix, localTargetDir);
+
+        } catch (Exception e) {
+            log.error("OSS文件夹下载失败!前缀:{},原因:{}", ossFolderPrefix, e.getMessage(), e);
+            e.printStackTrace();
+        }
+    }
+
+
     /**
      * 附件下载到本地指定文件夹
      * @param key

+ 5 - 4
src/main/java/com/jeeplus/modules/ruralprojectrecords/web/RuralProjectSignatureOldMessageDisposeController.java

@@ -970,7 +970,7 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
     public Map<String,Object> invoiceOMSView(@Param("workInvoiceId") String workInvoiceId){
         Map<String,Object> map = new HashMap<>();
         // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
-        omsDisposeService.doInvoiceBusiness(map, workInvoiceId);
+        omsDisposeService.doInvoiceBusiness(map, workInvoiceId, "21");
         return map;
     }
 
@@ -1048,7 +1048,7 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
     public Map<String,Object> invoiceFastRedOMSView(String allEinvno, String workInvoiceId) {
         Map<String,Object> map = new HashMap<>();
         // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
-        omsDisposeService.doFastRedInvoiceBusiness(map, allEinvno, workInvoiceId);
+        omsDisposeService.doFastRedInvoiceBusiness(map, allEinvno, workInvoiceId,"21");
         return map;
     }
 
@@ -1058,16 +1058,17 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
     /**
      * 开具红票(全场景红冲,包含已入账红冲处理。我方发起)
      * @param workInvoiceId   申请单号
+     * @param redInvoiceRelevancyId 开红票对应蓝票的invoiceid
      * @param originalInvno   原蓝票发票号码(数电票号码)
      * @return
      */
     @RequestMapping(value = "/invoiceAllScenarioRedOMSView")
     @ResponseBody
     @Transactional(readOnly = false)
-    public Map<String,Object> invoiceAllScenarioRedOMSView(String workInvoiceId, String originalInvno) {
+    public Map<String,Object> invoiceAllScenarioRedOMSView(String workInvoiceId, String redInvoiceRelevancyId, String originalInvno) {
         Map<String,Object> map = new HashMap<>();
         // 调用抽离后的核心业务方法,实现流程复用(0003时可重新调用)
-        omsDisposeService.doAllScenarioRedInvoiceBusiness(map, workInvoiceId, originalInvno);
+        omsDisposeService.doAllScenarioRedInvoiceBusiness(map, workInvoiceId, redInvoiceRelevancyId, originalInvno,"21");
         return map;
     }
 

+ 106 - 0
src/main/java/com/jeeplus/modules/sys/utils/ALiYunSmsUtil.java

@@ -175,4 +175,110 @@ public class ALiYunSmsUtil {
         }
     }
 
+    /**
+     * oms开票成功通知短信
+     * @param phoneNumbers  被通知手机号
+     * @param number    被通知的参数(此处为开票编号)
+     * @return
+     * @throws Exception
+     */
+    public static HashMap<String,Object> omsWorkInvoiceSuccessSms(String phoneNumbers, String number) {
+        HashMap<String,Object> map = new HashMap<>();
+        try {
+            Config config = new Config()
+                    .setAccessKeyId(ACCESS_KEY_ID)
+                    .setAccessKeySecret(ACCESS_KEY_SECRET)
+                    .setEndpoint(ENDPOINT);
+
+            Client client = new Client(config);
+            SendSmsRequest sendSmsRequest = new SendSmsRequest()
+                    .setPhoneNumbers(phoneNumbers)
+                    .setSignName(SIGNNAME)
+                    /*.setTemplateCode("SMS_472770050")*/
+                    .setTemplateCode("SMS_501905029")
+                    //此处是设计模版的时候预留的变量${number}
+                    .setTemplateParam(String.format("{\"number\":\"%s\"}", number));
+
+            SendSmsResponse sendSmsResponse = client.sendSms(sendSmsRequest);
+            System.out.println(sendSmsResponse);
+            if(sendSmsResponse.body.code.equals("isv.BUSINESS_LIMIT_CONTROL")){
+                if(sendSmsResponse.body.message.contains("触发分钟级流控")){
+                    map.put("message","手机号获取验证码次数已触发每分钟可发送数量上限,请稍后进行重试!");
+                }else if(sendSmsResponse.body.message.contains("触发小时级流控")){
+                    map.put("message","手机号获取验证码次数已触发每小时可发送数量上限,请稍后进行重试!");
+                }else{
+                    map.put("message","手机号获取验证码次数已触发每小时可发送数量上限,请稍后进行重试!");
+                }
+                //触发云通信流控限制 每小时限量
+                map.put("statusCode",10001);
+            }else if(sendSmsResponse.body.code.contains("isv.AMOUNT_NOT_ENOUGH")){
+                //触发账户余额不足
+                map.put("statusCode",10002);
+            }else if(sendSmsResponse.body.code.contains("isv.DAY_LIMIT_CONTROL")){
+                //触发触发日发送限额
+                map.put("statusCode",10003);
+            }else if(sendSmsResponse.body.code.contains("OK")){
+                map.put("statusCode",sendSmsResponse.getStatusCode());
+            }
+            return map;
+        } catch (Throwable error) {  // 捕获所有异常和错误
+            map.put("statusCode", 500);
+            map.put("message", "发送失败:" + error.getMessage());
+            return map;
+        }
+    }
+
+    /**
+     * oms开票成功通知短信
+     * @param phoneNumbers  被通知手机号
+     * @param number    被通知的参数(此处为开票编号)
+     * @return
+     * @throws Exception
+     */
+    public static HashMap<String,Object> omsWorkInvoiceErrorSms(String phoneNumbers, String number) {
+        HashMap<String,Object> map = new HashMap<>();
+        try {
+            Config config = new Config()
+                    .setAccessKeyId(ACCESS_KEY_ID)
+                    .setAccessKeySecret(ACCESS_KEY_SECRET)
+                    .setEndpoint(ENDPOINT);
+
+            Client client = new Client(config);
+            SendSmsRequest sendSmsRequest = new SendSmsRequest()
+                    .setPhoneNumbers(phoneNumbers)
+                    .setSignName(SIGNNAME)
+                    /*.setTemplateCode("SMS_472770050")*/
+                    .setTemplateCode("SMS_501750017")
+                    //此处是设计模版的时候预留的变量${number}
+                    .setTemplateParam(String.format("{\"number\":\"%s\"}", number));
+
+            SendSmsResponse sendSmsResponse = client.sendSms(sendSmsRequest);
+            System.out.println(sendSmsResponse);
+            if(sendSmsResponse.body.code.equals("isv.BUSINESS_LIMIT_CONTROL")){
+                if(sendSmsResponse.body.message.contains("触发分钟级流控")){
+                    map.put("message","手机号获取验证码次数已触发每分钟可发送数量上限,请稍后进行重试!");
+                }else if(sendSmsResponse.body.message.contains("触发小时级流控")){
+                    map.put("message","手机号获取验证码次数已触发每小时可发送数量上限,请稍后进行重试!");
+                }else{
+                    map.put("message","手机号获取验证码次数已触发每小时可发送数量上限,请稍后进行重试!");
+                }
+                //触发云通信流控限制 每小时限量
+                map.put("statusCode",10001);
+            }else if(sendSmsResponse.body.code.contains("isv.AMOUNT_NOT_ENOUGH")){
+                //触发账户余额不足
+                map.put("statusCode",10002);
+            }else if(sendSmsResponse.body.code.contains("isv.DAY_LIMIT_CONTROL")){
+                //触发触发日发送限额
+                map.put("statusCode",10003);
+            }else if(sendSmsResponse.body.code.contains("OK")){
+                map.put("statusCode",sendSmsResponse.getStatusCode());
+            }
+            return map;
+        } catch (Throwable error) {  // 捕获所有异常和错误
+            map.put("statusCode", 500);
+            map.put("message", "发送失败:" + error.getMessage());
+            return map;
+        }
+    }
+
 }

+ 80 - 0
src/main/java/com/jeeplus/modules/sys/utils/UserUtils.java

@@ -1286,6 +1286,86 @@ public class UserUtils {
 	}
 
 
+	public static List<User> getByRoleActivityEnnameOnNotCompany(String enname,int type,String officeId,String businessType,User userId, String companyId){
+		//businessType 1:全平台   2:合同模块 	3:合同归档 	4:项目登记 	5:开票管理
+		// 6:退票管理 	7:收入管理 	8:报销管理 	9:月度计划管理 10:案例管理 11:行政管理 12 报告管理13 采购合同管理 14采购管理  15报废管理
+		//16借用管理  17 领用管理 18劳动关系管理 19 加班申请 20 出差申请 21外勤申请 22 转正申请 23 职级调整 24 部门调整 25 请假申请
+		//26 补卡申请 27 考勤审批
+		String name = getRoleActivityEnname(companyId,userId);
+		if (!enname.contains(name)){
+			enname = name+enname;
+		}
+		Role role = new Role();
+		role.setEnname(enname);
+		Office office = officeDao.get(officeId);
+		if (StringUtils.isNotBlank(office.getBranchOffice()) || type == 2) {
+			if (type == 2) {
+				role.setOffice(new Office(officeId));
+			} else if (type == 3) {
+				role.setBranchCompany(new Office(office.getBranchOffice()));
+			} else if(type == 1){
+				Office branchOffice =  officeDao.get(office.getBranchOffice());
+				if (StringUtils.isNotBlank(branchOffice.getParentId())&& !"0".equals(branchOffice.getParentId())){
+					Office topCompany = officeDao.get(branchOffice.getParentId());
+					if ("1".equals(topCompany.getType())){
+						Office office1 = new Office();
+						office1.setName("总公司");
+						role.setBranchCompany(office1);
+					}else{
+						role.setBranchCompany(new Office(branchOffice.getParentId()));
+					}
+				}else {
+					Office office1 = new Office();
+					office1.setName("总公司");
+					role.setBranchCompany(office1);
+				}
+			} else {
+				Office office1 = new Office();
+				office1.setName("总公司");
+				role.setBranchCompany(office1);
+			}
+		}else {
+			Office office1 = new Office();
+			office1.setName("总公司");
+			role.setBranchCompany(office1);
+		}
+		String roleIds = "";
+		List<Role> roles = roleDao.getByRoleActivityEnname(role);
+		for (Role r : roles) {
+			roleIds += r.getId() + ",";
+		}
+		User user = new User();
+		user.setRoleIds(roleIds);
+		List<User> list = userDao.findListByRoleList(user);
+		List<User> users = new ArrayList<>();
+		Set<String> set = new HashSet<>();
+		for (User u:list){
+			ActivityAssignment select = new ActivityAssignment();
+			select.setCreateBy(u);
+			select.setStartDate(new Date());
+			select.setType(businessType);
+			select.setCompanyId(companyId);
+			ActivityAssignment activityAssignment = activityAssignmentDao.getByUserIdAndDate(select);
+			if (activityAssignment!=null && !activityAssignment.getIsNewRecord()){
+				User activityAssignmentUser = activityAssignment.getUser();
+				activityAssignmentUser.setRemarks(u.getName()+"的委托任务");
+				if (!set.contains(activityAssignmentUser.getId())) {
+					users.add(activityAssignmentUser);
+				}
+			}else {
+				if (!set.contains(u.getId())) {
+					users.add(u);
+				}
+			}
+			set.add(u.getId());
+		}
+		//暂时删除
+		/*if(users==null || users.size()==0){
+			users = null;
+		}*/
+		return users;
+	}
+
 	/**
 	 * 回调使用的查询角色人员信息接口
 	 * @param enname

+ 109 - 0
src/main/java/com/jeeplus/modules/workfullmanage/utils/ZipUtils.java

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

+ 229 - 3
src/main/java/com/jeeplus/modules/workfullmanage/web/WorkFullManageController.java

@@ -47,6 +47,7 @@ import com.jeeplus.modules.workfullmanage.service.WorkFullConstructService;
 import com.jeeplus.modules.workfullmanage.service.WorkFullDesignService;
 import com.jeeplus.modules.workfullmanage.service.WorkFullManageService;
 import com.jeeplus.modules.workfullmanage.service.WorkFullSurveyService;
+import com.jeeplus.modules.workfullmanage.utils.ZipUtils;
 import com.jeeplus.modules.workproject.entity.WorkProject;
 import com.jeeplus.modules.workproject.service.WorkProjectService;
 import com.jeeplus.modules.workprojectnotify.service.WorkProjectNotifyService;
@@ -56,6 +57,11 @@ import freemarker.template.Configuration;
 import freemarker.template.Template;
 import org.apache.commons.lang.ArrayUtils;
 import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
 import org.apache.shiro.authz.annotation.Logical;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -70,12 +76,11 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.ConstraintViolationException;
-import java.io.File;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.io.*;
 import java.net.MalformedURLException;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
 import java.util.*;
 
@@ -712,6 +717,121 @@ public class WorkFullManageController extends BaseController {
 
 	}
 
+
+	/**
+	 * 下载附件(文件没有时间戳前缀)
+	 */
+	@RequestMapping("/downLoadAttachNoprefix")
+	public String downLoadAttachNoprefix(String file,HttpServletResponse response) throws IOException {
+		file = file.replace("amp;","");
+		String fileName = file.substring(file.lastIndexOf("/") + 1, file.length());
+		String aliyunUrl = Global.getAliyunUrl();
+		String aliDownloadUrl = Global.getAliDownloadUrl();
+		String cons = "";
+		if (file.contains(aliyunUrl)){
+			cons = aliyunUrl;
+		}else if (file.contains("http://gangwan-app.oss-cn-hangzhou.aliyuncs.com")){
+			cons = "http://gangwan-app.oss-cn-hangzhou.aliyuncs.com";
+		}else {
+			cons = aliDownloadUrl;
+		}
+		String key = file.split(cons+"/")[1];
+		logger.info("-----------------------------------------");
+		logger.info("fileName="+fileName);
+		logger.info("key="+key);
+		logger.info("-----------------------------------------");
+		new OSSClientUtil().downLoadAttachNoprefix(key,fileName,response,request.getHeader("USER-AGENT"));
+		return null;
+
+	}
+
+	/**
+	 * 下载外部URL的文件(强制下载,不预览)
+	 * @param fileUrl 外部文件的完整URL(必传)
+	 * @param fileName 下载后的自定义文件名(可选,不传则从URL自动截取)
+	 * @param request 请求对象(用于处理浏览器编码)
+	 * @param response 响应对象(用于输出文件流)
+	 * @throws IOException 异常抛出(可根据业务调整为全局异常处理)
+	 */
+	@RequestMapping("/downloadExternalFile")
+	public void downloadExternalFile(
+			@RequestParam("fileUrl") String fileUrl,  // 外部文件URL(必填)
+			@RequestParam(value = "fileName", required = false) String fileName,  // 自定义文件名(可选)
+			HttpServletRequest request,
+			HttpServletResponse response) throws IOException {
+
+		// 1. 清理URL中的特殊字符(比如转义的&符号)
+		fileUrl = fileUrl.replace("amp;", "");
+
+		// 2. 自动生成文件名(如果前端未传)
+		if (fileName == null || fileName.trim().isEmpty()) {
+			// 从URL截取文件名(处理URL带参数的情况,比如 xxx.pdf?token=123)
+			fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
+			if (fileName.contains("?")) {
+				fileName = fileName.substring(0, fileName.indexOf("?"));
+			}
+		}
+
+		// 3. 设置响应头:核心是强制下载 + 解决中文文件名乱码
+		response.setCharacterEncoding("UTF-8");
+		response.setContentType("application/octet-stream"); // 二进制流,强制下载
+		// 处理不同浏览器的文件名编码
+		String userAgent = request.getHeader("USER-AGENT");
+		if (userAgent != null && (userAgent.contains("MSIE") || userAgent.contains("Trident"))) {
+			// IE浏览器编码
+			fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString());
+		} else {
+			// Chrome/Firefox等非IE浏览器编码
+			fileName = new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
+		}
+		// 关键头:attachment表示附件(强制下载),filename指定下载后的文件名
+		response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
+		// 禁用缓存,避免文件缓存问题
+		response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+		response.setHeader("Pragma", "no-cache");
+		response.setHeader("Expires", "0");
+
+		// 4. 调用外部URL,获取文件流并写入响应
+		CloseableHttpClient httpClient = HttpClients.createDefault();
+		OutputStream outputStream = null;
+		try {
+			HttpGet httpGet = new HttpGet(fileUrl);
+			CloseableHttpResponse httpResponse = httpClient.execute(httpGet);
+
+			// 检查外部URL响应状态(200=成功)
+			if (httpResponse.getStatusLine().getStatusCode() == 200) {
+				// 获取外部文件字节流
+				byte[] fileBytes = EntityUtils.toByteArray(httpResponse.getEntity());
+				// 写入前端响应流(触发下载)
+				outputStream = response.getOutputStream();
+				outputStream.write(fileBytes);
+				outputStream.flush();
+			} else {
+				logger.error("外部文件URL响应失败,状态码:{}", httpResponse.getStatusLine().getStatusCode());
+				throw new IOException("下载失败:外部文件无法访问,状态码=" + httpResponse.getStatusLine().getStatusCode());
+			}
+		} catch (Exception e) {
+			logger.error("下载外部文件异常", e);
+			throw new IOException("下载失败:" + e.getMessage());
+		} finally {
+			// 5. 关闭资源,避免内存泄漏
+			if (outputStream != null) {
+				try {
+					outputStream.close();
+				} catch (IOException e) {
+					logger.error("关闭输出流异常", e);
+				}
+			}
+			if (httpClient != null) {
+				try {
+					httpClient.close();
+				} catch (IOException e) {
+					logger.error("关闭HttpClient异常", e);
+				}
+			}
+		}
+	}
+
 	/**
 	 * 下载附件
 	 */
@@ -759,6 +879,112 @@ public class WorkFullManageController extends BaseController {
 
 
 	/**
+	 * 下载附件(文件没有时间戳前缀)
+	 */
+	@RequestMapping("/downLoadOMSInvoiceAttachzip")
+	public void downLoadOMSInvoiceAttachzip(String file, HttpServletResponse response) {
+		// 声明临时变量,用于finally清理
+		File localTargetDirFile = null;
+		File zipFile = null;
+		InputStream is = null;
+		OutputStream os = null;
+
+		try {
+			// 1. 解析OSS Key
+			file = file.replace("amp;", "");
+			String fileName = file.substring(file.lastIndexOf("/") + 1);
+			String aliyunUrl = Global.getAliyunUrl();
+			String aliDownloadUrl = Global.getAliDownloadUrl();
+			String cons = "";
+			if (file.contains(aliyunUrl)) {
+				cons = aliyunUrl;
+			} else if (file.contains("http://gangwan-app.oss-cn-hangzhou.aliyuncs.com")) {
+				cons = "http://gangwan-app.oss-cn-hangzhou.aliyuncs.com";
+			} else {
+				cons = aliDownloadUrl;
+			}
+			String key = file.split(cons + "/")[1];
+			logger.info("fileName={}, key={}", fileName, key);
+
+			// 2. 截取OSS文件夹路径和本地文件夹名称
+			key = key.trim();
+			int lastSlashIndex = key.lastIndexOf("/");
+			if (lastSlashIndex == key.length() - 1) {
+				lastSlashIndex = key.lastIndexOf("/", lastSlashIndex - 1);
+			}
+			if (lastSlashIndex == -1) {
+				throw new IllegalArgumentException("OSS Key格式错误,无目录分隔符:" + key);
+			}
+			String folderPath = key.substring(0, lastSlashIndex);
+
+			// 3. 确定本地根目录和目标文件夹
+			String path = System.getProperty("os.name").toLowerCase().contains("win")
+					? "D:/attachment-file"
+					: "/attachment-file";
+			int lastFolderSlashIndex = folderPath.lastIndexOf("/");
+			String localFolderName = lastFolderSlashIndex == -1 ? folderPath : folderPath.substring(lastFolderSlashIndex + 1);
+			// 本地目标文件夹(下载OSS文件的目录)
+			String localTargetDir = path + File.separator + localFolderName;
+			localTargetDirFile = new File(localTargetDir);
+			// ZIP文件路径(与目标文件夹同目录,名称为文件夹名.zip)
+			String zipFilePath = path + File.separator + localFolderName + ".zip";
+			zipFile = new File(zipFilePath);
+
+			// 4. 下载OSS文件夹到本地
+			new OSSClientUtil().downFolderByStreamSaveLocal(folderPath + "/", path, localFolderName, response);
+
+			// 5. 将本地文件夹打包为ZIP
+			ZipUtils.zipFolder(localTargetDir, zipFilePath);
+
+			// 6. 设置响应头,返回ZIP文件给前端下载
+			response.setCharacterEncoding("UTF-8");
+			response.setContentType("application/zip");
+			// 处理文件名中文乱码
+			String zipFileName = URLEncoder.encode("编号:" + localFolderName + "所有类型发票.zip", StandardCharsets.UTF_8.name())
+					.replace("+", "%20");
+			response.setHeader("Content-Disposition", "attachment;filename=" + zipFileName);
+
+			// 7. 读取ZIP文件并输出到响应流
+			is = new BufferedInputStream(new FileInputStream(zipFile));
+			os = new BufferedOutputStream(response.getOutputStream());
+			byte[] buffer = new byte[4096];
+			int len;
+			while ((len = is.read(buffer)) != -1) {
+				os.write(buffer, 0, len);
+			}
+			os.flush();
+			logger.info("ZIP文件已返回给前端下载:{}", zipFileName);
+
+		} catch (Exception e) {
+			logger.error("下载并打包OSS文件夹失败!", e);
+			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+			try {
+				response.getWriter().write("下载失败:" + e.getMessage());
+			} catch (IOException ioException) {
+				logger.error("响应错误信息失败!", ioException);
+			}
+		} finally {
+			// 8. 清理临时文件(关闭流→删除ZIP→删除下载的文件夹)
+			try {
+				if (os != null) os.close();
+				if (is != null) is.close();
+			} catch (IOException e) {
+				logger.error("关闭流失败!", e);
+			}
+			// 删除ZIP文件
+			if (zipFile != null && zipFile.exists()) {
+				boolean zipDeleted = zipFile.delete();
+				logger.info("ZIP文件删除{}:{}", zipDeleted ? "成功" : "失败", zipFile.getAbsolutePath());
+			}
+			// 删除下载的临时文件夹
+			if (localTargetDirFile != null) {
+				ZipUtils.deleteFolder(localTargetDirFile);
+			}
+		}
+	}
+
+
+	/**
 	 * 阿里云文件通过文件key获取临时查看url
 	 * @return
 	 */

+ 12 - 19
src/main/java/com/jeeplus/modules/workinvoice/service/OMS/InvoiceDownloadService.java

@@ -11,6 +11,7 @@ import com.jeeplus.modules.workinvoice.entity.WorkInvoice;
 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.workinvoicedetail.dao.WorkInvoiceDetailDao;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
@@ -42,6 +43,8 @@ public class InvoiceDownloadService {
 
     @Autowired
     private WorkInvoiceService workInvoiceService;
+    @Autowired
+    private WorkInvoiceDetailDao workInvoiceDetailDao;
 
 
 
@@ -59,6 +62,7 @@ public class InvoiceDownloadService {
             for (String taskKey : downloadTaskKeys) {
                 String accessToken = jedis.hget(taskKey, "accessToken");
                 String workInvoiceId = jedis.hget(taskKey, "workInvoiceId");
+                String informType = jedis.hget(taskKey, "informType");
                 //获取当前redis中的accessToken
                 //如果有值,则使用当前redis中的accessToken,否则重新生成一个新的accessToken
                 accessToken = jedis.get("OMSAccessToken");
@@ -89,13 +93,13 @@ public class InvoiceDownloadService {
                 // 23*60*60*1000 = 82800000 ms(原86400000是24小时)
                 if (System.currentTimeMillis() - firstExecTime > 82800000L) {
                     System.err.println("[InvoiceDownloadTask] 任务["+workInvoiceId+"]已重试23小时,触发兜底");
-                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票下载解析失败,请通过税务系统进行查看");
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票下载解析失败,请通过税务系统进行查看", informType);
                     jedis.del(taskKey); // 兜底后删除Redis,保证数据一致
                     continue;
                 }
 
                 // ========== 核心:调用原始解析方法(移至此) ==========
-                executeInvoiceDownloadWithRetry(accessToken, workInvoiceId);
+                executeInvoiceDownloadWithRetry(accessToken, workInvoiceId, informType);
             }
         } catch (Exception e) {
             e.printStackTrace();
@@ -108,10 +112,11 @@ public class InvoiceDownloadService {
      * 执行发票下载分析方法,获取发票下载接口以及其他信息
      * @param accessToken   发票当时的accessToken
      * @param workInvoiceId 发票的id(数据库中work_invoice中对应的id)
+     * @param informType 用于流程通知的判定参数
      */
 
     @Transactional(readOnly = false)
-    public void executeInvoiceDownloadWithRetry(String accessToken, String workInvoiceId) {
+    public void executeInvoiceDownloadWithRetry(String accessToken, String workInvoiceId, String informType) {
         try {
             System.out.println("✅ [InvoiceDownloadTask] 开始解析发票,订单号:" + workInvoiceId);
 
@@ -151,26 +156,14 @@ public class InvoiceDownloadService {
                             WorkInvoice workInvoice = workInvoiceService.get(workInvoiceId);
                             if(null != workInvoice){
                                 if(StringUtils.isBlank(invoiceInfo.getOfdUrl())){
-                                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取");
+                                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取", informType);
                                 }else{
-                                    System.out.println("✅ [InvoiceDownloadTask] 解析成功!");
-                                    workInvoice.setOmsEinVno(invoiceInfo.getAllEinVno());
-                                    workInvoice.setOmsOfdUrl(invoiceInfo.getOfdUrl());
-                                    workInvoice.setOmsPdfUrl(invoiceInfo.getPdfUrl());
-                                    workInvoice.setOmsXmlUrl(invoiceInfo.getXmlUrl());
-                                    workInvoice.setInvoiceState("5");
+                                    //进行处理
+                                    isSuccess = workInvoiceService.updateOmsByIdOnDown(workInvoice, invoiceInfo, informType);
                                 }
-                                //修改结果
-                                workInvoiceService.updateOmsUrlById(workInvoice);
-                                //同时 还需要将这个开票申请流程结束,并且向发起人发送通知
-
-
-
-
-                                isSuccess = true;
                             }else{
 
-                                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取");
+                                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取", informType);
                                 return;
                             }
                         }

+ 128 - 56
src/main/java/com/jeeplus/modules/workinvoice/service/OMS/OMSDisposeService.java

@@ -4,8 +4,6 @@ import com.alibaba.fastjson.JSON;
 import com.jeeplus.common.config.Global;
 import com.jeeplus.common.utils.JedisUtils;
 import com.jeeplus.common.utils.StringUtils;
-import com.jeeplus.modules.sys.entity.MainDictDetail;
-import com.jeeplus.modules.sys.utils.DictUtils;
 import com.jeeplus.modules.workclientinfo.dao.WorkClientBankDao;
 import com.jeeplus.modules.workclientinfo.dao.WorkClientInfoDao;
 import com.jeeplus.modules.workclientinfo.entity.WorkClientBank;
@@ -14,6 +12,7 @@ import com.jeeplus.modules.workinvoice.dao.WorkInvoiceDao;
 import com.jeeplus.modules.workinvoice.entity.OMS.OMSAccessTokenInfo;
 import com.jeeplus.modules.workinvoice.entity.WorkInvoice;
 import com.jeeplus.modules.workinvoice.entity.WorkInvoiceTaxClassificationCode;
+import com.jeeplus.modules.workinvoice.service.WorkInvoiceService;
 import com.jeeplus.modules.workinvoice.utils.HttpPostJsonUtil;
 import com.jeeplus.modules.workinvoice.utils.OMSNationUtil;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -49,13 +48,16 @@ public class OMSDisposeService {
     @Autowired
     private WorkClientBankDao workClientBankDao;
 
+    @Autowired
+    private WorkInvoiceService workInvoiceService;
+
     /**
      * 用于生成开蓝票信息
      * @param map
      * @param workInvoiceId 需要开票的开票信息id
      */
     @Transactional(readOnly = false)
-    public void doInvoiceBusiness(Map<String,Object> map, String workInvoiceId) {
+    public void doInvoiceBusiness(Map<String,Object> map, String workInvoiceId, String informType) {
         Jedis jedis = null;
         String accessToken = null;
         try {
@@ -89,20 +91,22 @@ public class OMSDisposeService {
                     if(null != workClientInfo){
                         workInvoice.setClient(workClientInfo);
                     }else{
-                        handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败"); // 解析失败直接兜底
+                        handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
                         return;
                     }
                 }else{
-                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败"); // 解析失败直接兜底
+                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
                     return;
                 }
 
-                WorkClientBank workClientBank = workClientBankDao.get(workInvoice.getBank());
-                if(null != workClientBank){
-                    workInvoice.setBank(workClientBank.getOurBank());
-                }else{
-                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败"); // 解析失败直接兜底
-                    return;
+                if(StringUtils.isNotBlank(workInvoice.getBank())){
+                    WorkClientBank workClientBank = workClientBankDao.get(workInvoice.getBank());
+                    if(null != workClientBank){
+                        workInvoice.setBank(workClientBank.getOurBank());
+                    }else{
+                        handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                        return;
+                    }
                 }
 
 
@@ -112,6 +116,8 @@ public class OMSDisposeService {
                     if(null != billingContentDetail){
                         workInvoice.setBillingContent(billingContentDetail.getGoodName());
                         workInvoice.setGoodsTaxno(billingContentDetail.getGoodsTaxno());
+                    }else{
+                        handleInvoiceRetryAllFail("",workInvoiceId, "未找到对应税收编码,请联系信息部确认后重新发起", informType); // 解析失败直接兜底
                     }
                     //生成开票基础信息
                     String string = util.neatenData(workInvoiceId,workInvoice,billingContentDetail);
@@ -137,19 +143,19 @@ public class OMSDisposeService {
 
                         String finalAccessToken = accessToken;
                         String finalJsonInvoiceStr = jsonInvoiceStr;
-                        executeOrderUploadRetry(workInvoiceId, remainRetryTimes, jsonInvoicResultStr, finalJsonInvoiceStr, finalAccessToken, map,"accessToken", "blueTicket");
+                        executeOrderUploadRetry(workInvoiceId, remainRetryTimes, jsonInvoicResultStr, finalJsonInvoiceStr, finalAccessToken, map,"accessToken", "blueTicket", informType);
                     }else{
-                        handleInvoiceRetryAllFail(jsonInvoiceStr,workInvoiceId, "蓝票发送订单生成接口返回值为空,开票失败。节点访问参数为:" + jsonInvoicResultStr); // 解析失败直接兜底
+                        handleInvoiceRetryAllFail(jsonInvoiceStr,workInvoiceId, "蓝票发送订单生成接口返回值为空,开票失败。节点访问参数为:" + jsonInvoicResultStr, informType); // 解析失败直接兜底
 
                     }
                 }else{
-                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败"); // 解析失败直接兜底
+                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
                 }
 
 
 
             }else{
-                handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败"); // 解析失败直接兜底
+                handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
             }
 
 
@@ -249,16 +255,17 @@ public class OMSDisposeService {
      * @param map   作用不大,仅作为保留
      * @param getKey    用来判定的参数值
      * @param initiationType    用来判定是什么类型的,比如蓝票、全类型红票、还是快捷红票
+     * @param informType    流程用来通知的类型
      */
 
     @Transactional(readOnly = false)
-    public void executeOrderUploadRetry(String workInvoiceId, int remainRetryTimes, String jsonInvoicResultStr, String jsonInvoiceStr, String accessToken, Map<String,Object> map, String getKey, String initiationType) {
+    public void executeOrderUploadRetry(String workInvoiceId, int remainRetryTimes, String jsonInvoicResultStr, String jsonInvoiceStr, String accessToken, Map<String,Object> map, String getKey, String initiationType, String informType) {
         String jsonInvoicResult = "";
         try {
             OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(jsonInvoicResultStr, OMSAccessTokenInfo.class);
             if(null == resultTokenInfo || null == resultTokenInfo.getResult()){
                 System.err.println("❌ 订单上传解析失败,剩余重试次数:"+remainRetryTimes);
-                handleInvoiceRetryAllFail(accessToken, workInvoiceId, ""); // 解析失败也执行兜底方法
+                handleInvoiceRetryAllFail(accessToken, workInvoiceId, "", informType); // 解析失败也执行兜底方法
                 return;
             }
             String code = resultTokenInfo.getResult().getCode();
@@ -277,7 +284,7 @@ public class OMSDisposeService {
                 }
                 String finalWorkInvoiceId = workInvoiceId;
                 // 存入Redis,由InvoiceDownloadTask接管
-                saveInvoiceDownloadTaskToRedis(accessToken, finalWorkInvoiceId);
+                saveInvoiceDownloadTaskToRedis(accessToken, finalWorkInvoiceId, informType);
                 System.out.println("✅ 解析开票数据任务["+finalWorkInvoiceId+"]已存入Redis,由InvoiceDownloadTask接管重试");
 
             } else if ("9998".equals(code)) {
@@ -298,19 +305,19 @@ public class OMSDisposeService {
                 } finally {
                     if(jedis != null) jedis.close();
                 }
-                doInvoiceBusiness(map, workInvoiceId);
+                doInvoiceBusiness(map, workInvoiceId, informType);
             } else {
                 // ✅ ✅ ✅ 核心修正:0001/0002/其他任意错误码 → 直接调用handleInvoiceRetryAllFail执行修改系统信息逻辑 ✅ ✅ ✅
                 System.err.println("❌ 订单上传返回业务错误码:"+code+",立即执行失败兜底逻辑修改系统信息!");
                 System.err.println("❌ 订单上传返回业务错误原因:"+message);
                 map.put("订单状态", "失败,错误码:"+code);
-                handleInvoiceRetryAllFail(accessToken, workInvoiceId, "订单上传返回业务错误码:"+code+",错误原因:"+message); // 解析失败也执行兜底方法
+                handleInvoiceRetryAllFail(accessToken, workInvoiceId, "订单上传返回业务错误码:"+code+",错误原因:"+message, informType); // 解析失败也执行兜底方法
                 remainRetryTimes = 0;
             }
         } catch (Exception e) {
             e.printStackTrace();
             System.err.println("❌ 订单上传重试异常,剩余次数:"+remainRetryTimes);
-            handleInvoiceRetryAllFail(accessToken, workInvoiceId, "订单上传重试异常,请重新发起"); // 解析失败也执行兜底方法
+            handleInvoiceRetryAllFail(accessToken, workInvoiceId, "订单上传重试异常,请重新发起", informType); // 解析失败也执行兜底方法
         }
     }
 
@@ -365,13 +372,14 @@ public class OMSDisposeService {
      * @param accessToken
      * @param workInvoiceId 开票的id
      */
-    public void saveInvoiceDownloadTaskToRedis(String accessToken, String workInvoiceId) {
+    public void saveInvoiceDownloadTaskToRedis(String accessToken, String workInvoiceId, String informType) {
         Jedis jedis = null;
         try {
             jedis = JedisUtils.getResource();
             String redisKey = "OMS_invoice_download:" + workInvoiceId;
             jedis.hset(redisKey, "accessToken", accessToken);
             jedis.hset(redisKey, "workInvoiceId", workInvoiceId);
+            jedis.hset(redisKey, "informType", informType);
             jedis.hset(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
             jedis.expire(redisKey, seconds); // 1天过期
         } catch (Exception e) {
@@ -386,7 +394,7 @@ public class OMSDisposeService {
      * @param accessToken
      */
     @Transactional(readOnly = false)
-    public void handleInvoiceRetryAllFail(String accessToken, String workInvoiceId, String errorMessage) {
+    public void handleInvoiceRetryAllFail(String accessToken, String workInvoiceId, String errorMessage, String informType) {
         // ============ 你的所有【修改系统信息/更新订单状态/写失败日志】逻辑 全部写在这里!!! ============
         try {
             System.err.println("============ 开始执行【失败兜底-系统信息修改逻辑】 ============");
@@ -401,6 +409,8 @@ public class OMSDisposeService {
 
 
                 //通知发起人或者开票管理员
+                workInvoiceService.handleInvoiceRetryAllFail(workInvoice, errorMessage, informType);
+
             }
 
 
@@ -420,7 +430,7 @@ public class OMSDisposeService {
      * @param allEinvno
      */
     @Transactional(readOnly = false)
-    public void doFastRedInvoiceBusiness(Map<String,Object> map, String allEinvno, String workInvoiceId) {
+    public void doFastRedInvoiceBusiness(Map<String,Object> map, String allEinvno, String workInvoiceId, String informType) {
         Jedis jedis = null;
         String accessToken = null;
         try {
@@ -460,7 +470,7 @@ public class OMSDisposeService {
             if(StringUtils.isNotBlank(jsonInvoicResultStr)){
                 String finalAccessToken = accessToken;
                 String finalJsonInvoiceStr = jsonInvoiceStr;
-                executeOrderUploadRetry(workInvoiceId, 5, jsonInvoicResultStr, finalJsonInvoiceStr, finalAccessToken, map, "orderno", "fastRed");
+                executeOrderUploadRetry(workInvoiceId, 5, jsonInvoicResultStr, finalJsonInvoiceStr, finalAccessToken, map, "orderno", "fastRed", informType);
             }
 
         } catch (Exception e) {
@@ -477,11 +487,13 @@ public class OMSDisposeService {
     /**
      * 调用生成红字确认申请单
      * @param map
-     * @param workInvoiceId 需要开票的开票id信息
+     * @param workInvoiceId 需要开红字票的开票id信息
+     * @param redInvoiceRelevancyId 开红票对应蓝票的invoiceid
      * @param originalInvno 原蓝票发票号码(数电票号码)
+     * @param informType 用于流程通知的判定条件
      */
     @Transactional(readOnly = false)
-    public void doAllScenarioRedInvoiceBusiness(Map<String,Object> map, String workInvoiceId, String originalInvno) {
+    public void doAllScenarioRedInvoiceBusiness(Map<String,Object> map, String workInvoiceId, String redInvoiceRelevancyId, String originalInvno, String informType) {
         Jedis jedis = null;
         String accessToken = null;
         try {
@@ -505,37 +517,96 @@ public class OMSDisposeService {
             OMSNationUtil util = new OMSNationUtil();
             //获取需要开票的发票信息
             WorkInvoice workInvoice = workInvoiceDao.get(workInvoiceId);
+
             if(null != workInvoice){
-                //生成红冲的数据
-                String string = util.neatenAllScenarioRedInvoiceData(workInvoiceId, originalInvno,workInvoice);
 
-                OMSAccessTokenInfo InvoiceTokenInfo = new OMSAccessTokenInfo();
-                InvoiceTokenInfo.setAppId(appId);
-                InvoiceTokenInfo.setAppKey(appKey);
-                InvoiceTokenInfo.setExchangeId(UUID.randomUUID().toString());
-                InvoiceTokenInfo.setAccessToken(accessToken);
-                InvoiceTokenInfo.setData(string);
-                String jsonInvoiceStr = JSON.toJSONString(InvoiceTokenInfo);
+                if(null != workInvoice.getClient() && StringUtils.isNotBlank(workInvoice.getClient().getId())){
+                    //查询关联的客户开票信息
+                    WorkClientInfo workClientInfo = workClientInfoDao.get(workInvoice.getClient().getId());
+                    if(null != workClientInfo){
+                        workInvoice.setClient(workClientInfo);
+                    }else{
+                        handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                        return;
+                    }
+                }else{
+                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                    return;
+                }
+                //获取开票银行信息
+                if(StringUtils.isNotBlank(workInvoice.getBank())){
+                    WorkClientBank workClientBank = workClientBankDao.get(workInvoice.getBank());
+                    if(null != workClientBank){
+                        workInvoice.setBank(workClientBank.getOurBank());
+                    }else{
+                        handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                        return;
+                    }
+                }
 
-                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)){
 
-                    //将上个节点获取的值存到数据库,留作redis崩溃后进行重新获取处理
-                    workInvoice.setOrderForGoodsResultStr(jsonInvoicResultStr);
-                    workInvoiceDao.updateOrderForGoodsResultStrById(workInvoice);
+                if(StringUtils.isNotBlank(workInvoice.getBillingContent())){
+                    WorkInvoiceTaxClassificationCode billingContentDetail = workInvoiceDao.getBillingContentDetail(workInvoice);
+                    if(null != billingContentDetail){
+                        workInvoice.setBillingContent(billingContentDetail.getGoodName());
+                        workInvoice.setGoodsTaxno(billingContentDetail.getGoodsTaxno());
+                    }else{
+                        handleInvoiceRetryAllFail("",workInvoiceId, "未找到对应税收编码,请联系信息部确认后重新发起", informType); // 解析失败直接兜底
+                        return;
+                    }
+
+                    //获取本次红冲对应蓝票的基本信息
+                    WorkInvoice workInvoiceRelevancy = workInvoiceDao.get(redInvoiceRelevancyId);
+                    if(null == workInvoiceRelevancy || null == workInvoiceRelevancy.getClient()){
+                        handleInvoiceRetryAllFail("",workInvoiceId, "未找到需要红冲票的信息,请确认后重新发起", informType); // 解析失败直接兜底
+                        return;
+                    }
+                    //获取被红冲蓝票的税率信息
+                    WorkInvoiceTaxClassificationCode relevancyBillingContentDetail = workInvoiceDao.getBillingContentDetail(workInvoiceRelevancy);
+                    if(null == relevancyBillingContentDetail){
+                        handleInvoiceRetryAllFail("",workInvoiceId, "未找到被红冲蓝票的对应税收编码,请联系信息部确认后重新发起", informType); // 解析失败直接兜底
+                        return;
+                    }
 
+                    //生成红冲的数据
+                    String string = util.neatenAllScenarioRedInvoiceData(workInvoiceId, originalInvno,workInvoice, workInvoiceRelevancy, billingContentDetail,relevancyBillingContentDetail);
 
-                    String finalAccessToken = accessToken;
-                    queryRedInvoiceConfirm (workInvoiceId, 5, jsonInvoicResultStr, finalAccessToken);
-                } else {
-                    handleInvoiceRetryAllFail(accessToken,workInvoiceId, "全场景红冲订单接口返回值为空,开票失败。节点申请参数为:"+ jsonInvoiceStr); // 解析失败直接兜底
+                    OMSAccessTokenInfo InvoiceTokenInfo = new OMSAccessTokenInfo();
+                    InvoiceTokenInfo.setAppId(appId);
+                    InvoiceTokenInfo.setAppKey(appKey);
+                    InvoiceTokenInfo.setExchangeId(UUID.randomUUID().toString());
+                    InvoiceTokenInfo.setAccessToken(accessToken);
+                    InvoiceTokenInfo.setData(string);
+                    String jsonInvoiceStr = JSON.toJSONString(InvoiceTokenInfo);
+
+                    String jsonInvoicResultStr = HttpPostJsonUtil.doPost("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)){
+
+                        //将上个节点获取的值存到数据库,留作redis崩溃后进行重新获取处理
+                        workInvoice.setOrderForGoodsResultStr(jsonInvoicResultStr);
+                        workInvoiceDao.updateOrderForGoodsResultStrById(workInvoice);
+
+
+                        String finalAccessToken = accessToken;
+                        queryRedInvoiceConfirm (workInvoiceId, 5, jsonInvoicResultStr, finalAccessToken,informType);
+                    } else {
+                        handleInvoiceRetryAllFail(accessToken,workInvoiceId, "全场景红冲订单接口返回值为空,开票失败。节点申请参数为:"+ jsonInvoiceStr, informType); // 解析失败直接兜底
+                        return;
+                    }
+                }else{
+                    handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                    return;
                 }
+
             }else{
-                handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败"); // 解析失败直接兜底
+                handleInvoiceRetryAllFail("",workInvoiceId, "开票获取发票信息失败", informType); // 解析失败直接兜底
+                return;
             }
 
 
@@ -557,28 +628,28 @@ public class OMSDisposeService {
      * @param workInvoiceId 开票的id
      */
     @Transactional(readOnly = false)
-    public void queryRedInvoiceConfirm (String workInvoiceId, int remainRetryTimes, String jsonInvoicResultStr, String accessToken) {
+    public void queryRedInvoiceConfirm (String workInvoiceId, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String informType) {
         try {
             OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(jsonInvoicResultStr, OMSAccessTokenInfo.class);
             if(null == resultTokenInfo || null == resultTokenInfo.getResult()){
                 System.err.println("❌ 全场景红冲订单提交接口返回值解析失败,剩余重试次数:"+remainRetryTimes);
-                handleInvoiceRetryAllFail(accessToken, workInvoiceId, ""); // 解析失败也执行兜底方法
+                handleInvoiceRetryAllFail(accessToken, workInvoiceId, "", informType); // 解析失败也执行兜底方法
                 return;
             }
             String code = resultTokenInfo.getResult().getCode();
 
             if ("0000".equals(code)) {
                 // 仅存入Redis,删除所有业务执行逻辑
-                saveRedInvoiceTaskToRedis(workInvoiceId, remainRetryTimes, jsonInvoicResultStr, accessToken, workInvoiceId, System.currentTimeMillis());
+                saveRedInvoiceTaskToRedis(workInvoiceId, remainRetryTimes, jsonInvoicResultStr, accessToken, workInvoiceId, System.currentTimeMillis(),informType);
                 System.out.println("✅ 红冲任务["+workInvoiceId+"]已存入Redis,由定时任务接管执行");
             } else {
                 String message = resultTokenInfo.getResult().getMessage();
                 System.err.println("❌ 全类型红冲--红字确认单查询接口返回业务错误码:"+code+",错误原因为:"+message);
-                handleInvoiceRetryAllFail(accessToken, workInvoiceId, "全类型红冲--红字确认单查询接口返回业务错误码:"+code+",错误原因为:"+message); // 解析失败也执行兜底方法
+                handleInvoiceRetryAllFail(accessToken, workInvoiceId, "全类型红冲--红字确认单查询接口返回业务错误码:"+code+",错误原因为:"+message, informType); // 解析失败也执行兜底方法
             }
         } catch (Exception e) {
             e.printStackTrace();
-            handleInvoiceRetryAllFail(accessToken, workInvoiceId, "全类型红冲--红字确认单查询接口发起失败,请重新发起"); // 解析失败也执行兜底方法
+            handleInvoiceRetryAllFail(accessToken, workInvoiceId, "全类型红冲--红字确认单查询接口发起失败,请重新发起", informType); // 解析失败也执行兜底方法
         }
     }
 
@@ -591,7 +662,7 @@ public class OMSDisposeService {
      * @param applyNo   发票id
      * @param startTime 这个任务开始时间。限制:开始3天内如果双方没有全部确认,则本次红冲失败
      */
-    public void saveRedInvoiceTaskToRedis(String workInvoiceId, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime) {
+    public void saveRedInvoiceTaskToRedis(String workInvoiceId, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime, String informType) {
         Jedis jedis = null;
         try {
             jedis = JedisUtils.getResource();
@@ -601,11 +672,12 @@ public class OMSDisposeService {
             jedis.hset(redisKey, "jsonInvoicResultStr", jsonInvoicResultStr);
             jedis.hset(redisKey, "accessToken", accessToken);
             jedis.hset(redisKey, "workInvoiceId", workInvoiceId);
+            jedis.hset(redisKey, "informType", informType);
             jedis.hset(redisKey, "startTime", String.valueOf(startTime));
             jedis.expire(redisKey, 259200 + 3600); // 3天+1小时过期
         } catch (Exception e) {
             e.printStackTrace();
-            handleInvoiceRetryAllFail(accessToken, workInvoiceId, "全类型红冲--红字确认单查询接口发起失败,请重新发起"); // 解析失败也执行兜底方法
+            handleInvoiceRetryAllFail(accessToken, workInvoiceId, "全类型红冲--红字确认单查询接口发起失败,请重新发起", informType); // 解析失败也执行兜底方法
         } finally {
             if (jedis != null) jedis.close();
         }

+ 12 - 15
src/main/java/com/jeeplus/modules/workinvoice/service/OMS/RedInvoiceDownloadService.java

@@ -11,6 +11,8 @@ import com.jeeplus.modules.workinvoice.entity.WorkInvoice;
 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.workinvoicedetail.dao.WorkInvoiceDetailDao;
+import com.jeeplus.modules.workinvoicedetail.entity.WorkInvoiceDetail;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
@@ -42,7 +44,8 @@ public class RedInvoiceDownloadService {
 
     @Autowired
     private WorkInvoiceService workInvoiceService;
-
+    @Autowired
+    private WorkInvoiceDetailDao workInvoiceDetailDao;
 
 
     public void processRedInvoiceDownloadTasks() {
@@ -60,6 +63,7 @@ public class RedInvoiceDownloadService {
                 String accessToken = jedis.hget(taskKey, "accessToken");
                 String redInvOrderNo = jedis.hget(taskKey, "redInvOrderNo");    //红票对应的编号
                 String workInvoiceId = jedis.hget(taskKey, "workInvoiceId");
+                String informType = jedis.hget(taskKey, "informType");
                 //获取当前redis中的accessToken
                 //如果有值,则使用当前redis中的accessToken,否则重新生成一个新的accessToken
                 accessToken = jedis.get("OMSAccessToken");
@@ -90,7 +94,7 @@ public class RedInvoiceDownloadService {
                 // 23*60*60*1000 = 82800000 ms(原86400000是24小时)
                 if (System.currentTimeMillis() - firstExecTime > 82800000L) {
                     System.err.println("[InvoiceDownloadTask] 任务["+workInvoiceId+"]已重试23小时,触发兜底");
-                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票下载解析失败,请通过税务系统进行查看");
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票下载解析失败,请通过税务系统进行查看", informType);
                     //需要修改项目相关信息
 
 
@@ -100,7 +104,7 @@ public class RedInvoiceDownloadService {
                 }
 
                 // ========== 核心:调用原始解析方法(移至此) ==========
-                executeInvoiceDownloadWithRetry(accessToken, redInvOrderNo, workInvoiceId);
+                executeInvoiceDownloadWithRetry(accessToken, redInvOrderNo, workInvoiceId, informType);
             }
         } catch (Exception e) {
             e.printStackTrace();
@@ -117,7 +121,7 @@ public class RedInvoiceDownloadService {
      */
 
     @Transactional(readOnly = false)
-    public void executeInvoiceDownloadWithRetry(String accessToken, String redInvOrderNo, String workInvoiceId) {
+    public void executeInvoiceDownloadWithRetry(String accessToken, String redInvOrderNo, String workInvoiceId, String informType) {
         try {
             System.out.println("✅ [InvoiceDownloadTask] 开始解析发票,订单号:" + workInvoiceId);
 
@@ -157,22 +161,15 @@ public class RedInvoiceDownloadService {
                             WorkInvoice workInvoice = workInvoiceService.get(workInvoiceId);
                             if(null != workInvoice){
                                 if(StringUtils.isBlank(invoiceInfo.getOfdUrl())){
-                                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取");
+                                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取", informType);
                                 }else{
-                                    System.out.println("✅ [InvoiceDownloadTask] 解析成功!");
-                                    workInvoice.setOmsEinVno(invoiceInfo.getAllEinVno());
-                                    workInvoice.setOmsOfdUrl(invoiceInfo.getOfdUrl());
-                                    workInvoice.setOmsPdfUrl(invoiceInfo.getPdfUrl());
-                                    workInvoice.setOmsXmlUrl(invoiceInfo.getXmlUrl());
+                                    //进行处理
+                                    isSuccess = workInvoiceService.updateOmsByIdOnDown(workInvoice, invoiceInfo, informType);
                                 }
-                                //修改结果
-                                workInvoiceService.updateOmsUrlById(workInvoice);
-                                //同时 还需要将这个开票申请流程结束,并且向发起人发送通知
 
-                                isSuccess = true;
                             }else{
 
-                                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取");
+                                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "发票信息获取失败,请到税务系统进行获取", informType);
                                 return;
                             }
                         }

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

@@ -66,6 +66,7 @@ public class RedInvoiceRetryScheduledService {
                     String getKey = taskMap.get("getKey");
                     String initiationType = taskMap.get("initiationType");
                     String retryTimesStr = taskMap.get("retryTimes");
+                    String informType = taskMap.get("informType");
                     String workInvoiceId = taskMap.get("workInvoiceId");
 
                     //获取当前redis中的accessToken
@@ -99,7 +100,7 @@ public class RedInvoiceRetryScheduledService {
                     // 5. 判断是否耗尽次数(第20次,newRemainTimes=0)
                     if (newRemainTimes <= 0) {
                         System.err.println("❌ 订单" + orderNo + "重试次数已耗尽(累计20次),执行兜底逻辑handleInvoiceRetryAllFail");
-                        invoiceOMSService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "重试次数已耗尽(累计20次),需要重新发起申请");
+                        invoiceOMSService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "重试次数已耗尽(累计20次),需要重新发起申请", informType);
                         jedis.del(redisKey); // 删除Redis任务,不再重试
                         continue;
                     }
@@ -136,6 +137,7 @@ public class RedInvoiceRetryScheduledService {
                                     resultMap,
                                     getKey,
                                     initiationType
+                                    , informType
                             );
                             jedis.del(redisKey);
                         }/* else {

+ 22 - 18
src/main/java/com/jeeplus/modules/workinvoice/service/OMS/RedInvoiceScheduledService.java

@@ -66,6 +66,7 @@ public class RedInvoiceScheduledService {
                 String jsonInvoicResultStr = jedis.hget(taskKey, "jsonInvoicResultStr");
                 String accessToken = jedis.hget(taskKey, "accessToken");
                 String startTimeStr = jedis.hget(taskKey, "startTime");
+                String informType = jedis.hget(taskKey, "informType");
                 String workInvoiceId = jedis.hget(taskKey, "workInvoiceId");
 
                 //获取当前redis中的accessToken
@@ -95,7 +96,7 @@ public class RedInvoiceScheduledService {
                 // 转换参数并执行任务
                 int remainRetryTimes = Integer.parseInt(remainRetryTimesStr);
                 long startTime = Long.parseLong(startTimeStr);
-                executeRedInvoiceTask(applyNo, remainRetryTimes, jsonInvoicResultStr, accessToken, startTime, workInvoiceId);
+                executeRedInvoiceTask(applyNo, remainRetryTimes, jsonInvoicResultStr, accessToken, startTime, workInvoiceId, informType);
             }
         } catch (Exception e) {
             e.printStackTrace();
@@ -117,7 +118,7 @@ public class RedInvoiceScheduledService {
      * @param workInvoiceId 发票id
      */
     @Transactional(readOnly = false)
-    public void executeRedInvoiceTask(String applyNo, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, long startTime, String workInvoiceId) {
+    public void executeRedInvoiceTask(String applyNo, int remainRetryTimes, String jsonInvoicResultStr, String accessToken, long startTime, String workInvoiceId, String informType) {
         try {
             WorkInvoice workInvoice = workInvoiceDao.get(workInvoiceId);
 
@@ -125,7 +126,7 @@ public class RedInvoiceScheduledService {
                 // 步骤1:3天超时判断
                 if (System.currentTimeMillis() - startTime > 259200000L) {
                     System.err.println("❌ 任务["+applyNo+"]超时,触发兜底逻辑");
-                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "");
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "", informType);
                     deleteRedInvoiceTaskFromRedis(applyNo);
                     return;
                 }
@@ -135,17 +136,17 @@ public class RedInvoiceScheduledService {
                     OMSAccessTokenInfo resultTokenInfo = JSON.parseObject(jsonInvoicResultStr, OMSAccessTokenInfo.class);
                     if (null == resultTokenInfo || null == resultTokenInfo.getResult() || !"0000".equals(resultTokenInfo.getResult().getCode())) {
                         System.err.println("❌ 任务["+applyNo+"]解析失败,触发兜底");
-                        omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "");
+                        omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "", informType);
                         deleteRedInvoiceTaskFromRedis(applyNo);
                         return;
                     }
                     // 解析成功,调用查询接口
-                    callRedInvoiceConfirmQuery(remainRetryTimes, accessToken, applyNo, startTime, workInvoiceId, workInvoice);
+                    callRedInvoiceConfirmQuery(remainRetryTimes, accessToken, applyNo, startTime, workInvoiceId, workInvoice, informType);
                     return;
                 }
 
                 // 步骤3:后续重试(无jsonInvoicResultStr)→ 直接调用查询接口
-                callRedInvoiceConfirmQuery(remainRetryTimes, accessToken, applyNo, startTime, workInvoiceId, workInvoice);
+                callRedInvoiceConfirmQuery(remainRetryTimes, accessToken, applyNo, startTime, workInvoiceId, workInvoice, informType);
             }
 
 
@@ -155,9 +156,9 @@ public class RedInvoiceScheduledService {
             e.printStackTrace();
             // 异常后重试次数-1,重新存入Redis
             if (remainRetryTimes > 1) {
-                saveRedInvoiceTaskToRedis(remainRetryTimes - 1, jsonInvoicResultStr, accessToken, applyNo, startTime);
+                saveRedInvoiceTaskToRedis(remainRetryTimes - 1, jsonInvoicResultStr, accessToken, applyNo, startTime, informType);
             } else {
-                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "红字确认单查询接口回调失败,请重新发起");
+                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "红字确认单查询接口回调失败,请重新发起", informType);
                 deleteRedInvoiceTaskFromRedis(applyNo);
             }
         }
@@ -172,7 +173,7 @@ public class RedInvoiceScheduledService {
      * @param workInvoiceId 发票id
      */
     @Transactional(readOnly = false)
-    public void callRedInvoiceConfirmQuery(int remainRetryTimes, String accessToken, String applyNo, long startTime, String workInvoiceId, WorkInvoice workInvoice) {
+    public void callRedInvoiceConfirmQuery(int remainRetryTimes, String accessToken, String applyNo, long startTime, String workInvoiceId, WorkInvoice workInvoice, String informType) {
         try {
             // 组装查询参数
             OMSNationUtil util = new OMSNationUtil();
@@ -241,16 +242,16 @@ public class RedInvoiceScheduledService {
 
                         String redInvOrderNo = invoiceInfo.getRedInvOrderNo();
                         // 存入Redis,由InvoiceDownloadTask接管
-                        saveInvoiceDownloadTaskToRedis(accessToken, redInvOrderNo, workInvoiceId);
+                        saveInvoiceDownloadTaskToRedis(accessToken, redInvOrderNo, workInvoiceId, informType);
                         System.out.println("✅ 解析开票数据任务["+redInvOrderNo+"]已存入Redis,由InvoiceDownloadTask接管重试");
 
                         deleteRedInvoiceTaskFromRedis(applyNo); // 成功后删除Redis
                     } else if ("1".equals(invoiceInfo.getMakeStatus()) || "4".equals(invoiceInfo.getMakeStatus())) {
                         // 开票中,重新存入Redis等待下次重试
-                        saveRedInvoiceTaskToRedis(remainRetryTimes, "", accessToken, applyNo, startTime);
+                        saveRedInvoiceTaskToRedis(remainRetryTimes, "", accessToken, applyNo, startTime, informType);
                     } else {
                         //到这的 应该返回值就是2 了  表示 开票失败
-                        omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "红冲开票失败,请确认后重新开票");
+                        omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "红冲开票失败,请确认后重新开票", informType);
                         deleteRedInvoiceTaskFromRedis(applyNo);
                     }
                     break;
@@ -259,7 +260,7 @@ public class RedInvoiceScheduledService {
                 case "15":
                     // 待确认,重新存入Redis等待下次重试
                     //到这 说明 对方应该是没有进行发票确认,所以没能进行开票成功,需要对方进行确认
-                    saveRedInvoiceTaskToRedis(remainRetryTimes, "", accessToken, applyNo, startTime);
+                    saveRedInvoiceTaskToRedis(remainRetryTimes, "", accessToken, applyNo, startTime, informType);
                     break;
 
                 case "05":
@@ -270,20 +271,21 @@ public class RedInvoiceScheduledService {
                 case "10":
                 case "16":
                     // 终止本次红冲操作,通知发起人确认后重新处理
-                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "本次红冲的开票作废或失败,请确认后重新开票");
+                    omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "本次红冲的开票作废或失败,请确认后重新开票", informType);
                     // 失败后删除Redis任务
                     deleteRedInvoiceTaskFromRedis(applyNo);
                     break;
                 default:
+                    System.out.println("开票确认单接口查询,返回状态值是:"+ invoiceInfo.getConfirmStatus() + "。稍后重试");
                     break;
             }
         } catch (Exception e) {
             e.printStackTrace();
             // 接口调用失败,重试次数-1后重新存入Redis
             if (remainRetryTimes > 1) {
-                saveRedInvoiceTaskToRedis(remainRetryTimes - 1, "", accessToken, applyNo, startTime);
+                saveRedInvoiceTaskToRedis(remainRetryTimes - 1, "", accessToken, applyNo, startTime, informType);
             } else {
-                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "红冲开票失败,请确认后重新开票");
+                omsDisposeService.handleInvoiceRetryAllFail(accessToken, workInvoiceId, "红冲开票失败,请确认后重新开票", informType);
                 deleteRedInvoiceTaskFromRedis(applyNo);
             }
         }
@@ -292,7 +294,7 @@ public class RedInvoiceScheduledService {
     /**
      * 存入Redis(定时任务内部使用)
      */
-    private void saveRedInvoiceTaskToRedis(int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime) {
+    private void saveRedInvoiceTaskToRedis(int remainRetryTimes, String jsonInvoicResultStr, String accessToken, String applyNo, long startTime, String informType) {
         Jedis jedis = null;
         try {
             jedis = JedisUtils.getResource();
@@ -301,6 +303,7 @@ public class RedInvoiceScheduledService {
             jedis.hset(redisKey, "jsonInvoicResultStr", jsonInvoicResultStr);
             jedis.hset(redisKey, "accessToken", accessToken);
             jedis.hset(redisKey, "applyNo", applyNo);
+            jedis.hset(redisKey, "informType", informType);
             jedis.hset(redisKey, "startTime", String.valueOf(startTime));
             jedis.expire(redisKey, 259200 + 3600);
         } catch (Exception e) {
@@ -331,7 +334,7 @@ public class RedInvoiceScheduledService {
      * @param accessToken
      * @param workInvoiceId 开票id
      */
-    private void saveInvoiceDownloadTaskToRedis(String accessToken, String redInvOrderNo, String workInvoiceId) {
+    private void saveInvoiceDownloadTaskToRedis(String accessToken, String redInvOrderNo, String workInvoiceId,  String informType) {
         Jedis jedis = null;
         try {
             jedis = JedisUtils.getResource();
@@ -339,6 +342,7 @@ public class RedInvoiceScheduledService {
             jedis.hset(redisKey, "accessToken", accessToken);
             jedis.hset(redisKey, "redInvOrderNo", redInvOrderNo);
             jedis.hset(redisKey, "workInvoiceId", workInvoiceId);
+            jedis.hset(redisKey, "informType", informType);
             jedis.hset(redisKey, "firstExecTime", String.valueOf(System.currentTimeMillis()));
             jedis.expire(redisKey, 86400); // 1天过期
         } catch (Exception e) {

+ 27 - 5
src/main/java/com/jeeplus/modules/workinvoice/service/WorkInvoiceAllService.java

@@ -2368,11 +2368,14 @@ public class WorkInvoiceAllService extends CrudService<WorkInvoiceDao, WorkInvoi
 		List<WorkInvoiceProjectRelation> projectRelation = getProjectRelation(workInvoice);
 		workInvoice.setWorkInvoiceProjectRelationList(projectRelation);
 		if(workInvoice==null)return;
-		WorkClientAttachment attchment = new WorkClientAttachment();
-		attchment.setAttachmentId(workInvoice.getId());
-		attchment.setAttachmentFlag("115");
-		List<WorkClientAttachment> attachments = workattachmentService.getAttachmentList(attchment);
-		for (WorkClientAttachment clientAttachment:attachments){
+
+		List<Workattachment> attachmentList = workattachmentService.getListByAttachmentIdAndFlag(workInvoice.getId(), "115");
+		workInvoice.setWorkAttachments(attachmentList);
+		//处理数据的临时访问地址
+		//数据处理(如果为阿里云文件服务器,则对查看的路径进行处理)
+		workattachmentService.attachmentManageOnUrl(workInvoice.getWorkAttachments());
+
+		for (Workattachment clientAttachment:attachmentList){
 			if (clientAttachment.getCollectFlag().equals("1")){
 				for (Workattachment workattachment:workInvoice.getWorkAttachments()){
 					if (clientAttachment.getId().equals(workattachment.getId())){
@@ -2382,6 +2385,25 @@ public class WorkInvoiceAllService extends CrudService<WorkInvoiceDao, WorkInvoi
 				}
 			}
 		}
+		//如果没有查出来附件的信息,说明有可能是之后的数据,可按照oms的接口进行查询
+		if(attachmentList.isEmpty()){
+			attachmentList = workattachmentService.getListByAttachmentIdAndFlag(workInvoice.getId(), "OMS_invoice_file");
+			workInvoice.setWorkAttachments(attachmentList);
+			//处理数据的临时访问地址
+			//数据处理(如果为阿里云文件服务器,则对查看的路径进行处理)
+			workattachmentService.attachmentManageOnUrl(workInvoice.getWorkAttachments());
+			for (Workattachment clientAttachment:attachmentList){
+
+				if (StringUtils.isNotBlank(clientAttachment.getCollectFlag()) && clientAttachment.getCollectFlag().equals("1")){
+					for (Workattachment workattachment:workInvoice.getWorkAttachments()){
+						if (clientAttachment.getId().equals(workattachment.getId())){
+							workattachment.setCollectFlag("1");
+							break;
+						}
+					}
+				}
+			}
+		}
 	}
 
 	/**

+ 417 - 6
src/main/java/com/jeeplus/modules/workinvoice/service/WorkInvoiceService.java

@@ -5,12 +5,14 @@ package com.jeeplus.modules.workinvoice.service;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.jeeplus.common.json.AjaxJson;
+import com.jeeplus.common.oss.OSSClientUtil;
 import com.jeeplus.common.persistence.Page;
 import com.jeeplus.common.service.CrudService;
 import com.jeeplus.common.utils.DateUtils;
+import com.jeeplus.common.utils.JedisUtils;
 import com.jeeplus.common.utils.MenuStatusEnum;
 import com.jeeplus.common.utils.StringUtils;
-import com.jeeplus.common.utils.excel.utils.ExcelUtilInvoice;
 import com.jeeplus.modules.act.service.ActTaskService;
 import com.jeeplus.modules.act.utils.ActUtils;
 import com.jeeplus.modules.projectrecord.entity.ProjectRecords;
@@ -20,14 +22,18 @@ import com.jeeplus.modules.ruralprojectrecords.service.RuralProjectRecordsServic
 import com.jeeplus.modules.serialnum.service.SerialNumTplService;
 import com.jeeplus.modules.statement.entity.StatementCompanyComprehensiveInfo;
 import com.jeeplus.modules.statement.service.StatementCompanyComprehensiveService;
+import com.jeeplus.modules.sys.dao.UserDao;
 import com.jeeplus.modules.sys.entity.Office;
 import com.jeeplus.modules.sys.entity.Role;
 import com.jeeplus.modules.sys.entity.User;
 import com.jeeplus.modules.sys.entity.Workattachment;
 import com.jeeplus.modules.sys.service.AreaService;
 import com.jeeplus.modules.sys.service.OfficeService;
+import com.jeeplus.modules.sys.service.UserService;
 import com.jeeplus.modules.sys.service.WorkattachmentService;
+import com.jeeplus.modules.sys.utils.ALiYunSmsUtil;
 import com.jeeplus.modules.sys.utils.UserUtils;
+import com.jeeplus.modules.tools.utils.SignaturePostUtil;
 import com.jeeplus.modules.workactivity.dao.WorkActivityProcessDao;
 import com.jeeplus.modules.workactivity.entity.Activity;
 import com.jeeplus.modules.workactivity.entity.WorkActivityProcess;
@@ -45,8 +51,10 @@ import com.jeeplus.modules.workinvoice.dao.WorkInvoiceCloudDao;
 import com.jeeplus.modules.workinvoice.dao.WorkInvoiceDao;
 import com.jeeplus.modules.workinvoice.dao.WorkInvoiceReceiptDao;
 import com.jeeplus.modules.workinvoice.entity.*;
+import com.jeeplus.modules.workinvoice.entity.OMS.InvoiceDown.OMSInvoiceDetailInfo;
 import com.jeeplus.modules.workinvoice.thread.ApprovalThread;
 import com.jeeplus.modules.workinvoice.thread.RedApprovalThread;
+import com.jeeplus.modules.workinvoice.utils.FileHandlingUtil;
 import com.jeeplus.modules.workinvoicedetail.dao.WorkInvoiceDetailDao;
 import com.jeeplus.modules.workinvoicedetail.entity.WorkInvoiceDetail;
 import com.jeeplus.modules.workinvoicedetail.service.WorkInvoiceDetailService;
@@ -60,15 +68,22 @@ import org.activiti.engine.task.Task;
 import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
+import org.springframework.mock.web.MockMultipartFile;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+import redis.clients.jedis.Jedis;
 
+import java.io.File;
+import java.io.FileInputStream;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
+import java.util.regex.Pattern;
+
 
 /**
  * 开票管理Service
@@ -131,8 +146,14 @@ public class WorkInvoiceService extends CrudService<WorkInvoiceDao, WorkInvoice>
 	private WorkContractInfoService contractInfoService;
 	@Autowired
 	private WorkInvoiceCloudDao workInvoiceCloudDao;
+	@Autowired
+	private UserDao userDao;
+
+	Pattern mobilePattern = Pattern.compile("^1[3-9]\\d{9}$");
 
 	private static byte[] SYN_BYTE = new byte[0];
+    @Autowired
+    private UserService userService;
 
 	public WorkInvoice getByProcInsId(String procInsId) {
 		return dao.getByProcInsId(procInsId);
@@ -1821,7 +1842,7 @@ public class WorkInvoiceService extends CrudService<WorkInvoiceDao, WorkInvoice>
 							//需要判定,此处 如果开票金额为正数,则表示是开蓝票,如果是负数,则表示是开红票
 							if(workInvoice.getMoney()>0){
 								//财务审核通过时,需要调用OMS开票接口,用于开票操作。开票完成后,再进行发送成功通知 或者失败通知
-								new ApprovalThread(workInvoice).start();
+								new ApprovalThread(workInvoice, "21").start();
 							}else{
 								//先获取对应红冲发票的数电票  票号
 								//查询关联红冲发票的开票号
@@ -1829,8 +1850,8 @@ public class WorkInvoiceService extends CrudService<WorkInvoiceDao, WorkInvoice>
 									String invoiceNumberStr = this.getInvoiceNumberStr(workInvoice.getRedInvoiceRelevancyId());
 									workInvoice.setInvoiceNumberStr(invoiceNumberStr);
 								}
-
-								new RedApprovalThread(workInvoice,workInvoice.getInvoiceNumberStr()).start();
+								//还需要获取被红冲发票的对应invoice信息
+								new RedApprovalThread(workInvoice,workInvoice.getInvoiceNumberStr(), "21").start();
 							}
 
 							/*enname = "fpgly";
@@ -1896,6 +1917,10 @@ public class WorkInvoiceService extends CrudService<WorkInvoiceDao, WorkInvoice>
 			userList = workProjectNotifyService.readByNotifyId(notify);
 
 
+
+
+
+
 				/*users.add(workInvoice.getCreateBy());
 				if ("yes".equals(workInvoice.getAct().getFlag())) {
 					title = "项目【"+ projectNameStr +"】发票申请通过";
@@ -2597,6 +2622,23 @@ public class WorkInvoiceService extends CrudService<WorkInvoiceDao, WorkInvoice>
 						//notifyRole = "财务主任审批";
 						notifyRole = "审批通过";
 						workActivityProcess.setIsApproval("1");
+
+						//需要判定,此处 如果开票金额为正数,则表示是开蓝票,如果是负数,则表示是开红票
+						if(workInvoice.getMoney()>0){
+							//财务审核通过时,需要调用OMS开票接口,用于开票操作。开票完成后,再进行发送成功通知 或者失败通知
+							new ApprovalThread(workInvoice,"213").start();
+						}else{
+							//先获取对应红冲发票的数电票  票号
+							//查询关联红冲发票的开票号
+							if(1 == workInvoice.getRedInvoiceFlag() && StringUtils.isNotBlank(workInvoice.getRedInvoiceRelevancyId())){
+								String invoiceNumberStr = this.getInvoiceNumberStr(workInvoice.getRedInvoiceRelevancyId());
+								workInvoice.setInvoiceNumberStr(invoiceNumberStr);
+							}
+							//还需要获取被红冲发票的对应invoice信息
+							new RedApprovalThread(workInvoice,workInvoice.getInvoiceNumberStr(),"213").start();
+						}
+
+
 							/*enname = "fpgly";
 							vars.put("fpglyList", auditUsers);
 							vars.put("fpglycount",auditUsers.size());*/
@@ -2645,8 +2687,16 @@ public class WorkInvoiceService extends CrudService<WorkInvoiceDao, WorkInvoice>
 		List<User> userList = new ArrayList<>();
 		//ProjectRecords projectRecords = projectRecordsService.getRuralMasters(workInvoice.getProject().getId());
 		if (!state) {
+			//将数据流程接点重新归属到审批状态不变,等发票开启之后 进行变更为审核通过,若失败,则变更为暂存状态并发送通知
 			users.add(workInvoice.getCreateBy());
-			if ("yes".equals(workInvoice.getAct().getFlag())) {
+			//新设定的值,该值表示开票进行中
+			workInvoice.setInvoiceState("12");
+			WorkProjectNotify notify = new WorkProjectNotify();
+			notify.setNotifyId(workInvoice.getId());
+			userList = workProjectNotifyService.readByNotifyId(notify);
+
+
+			/*if ("yes".equals(workInvoice.getAct().getFlag())) {
 				title = "项目【"+ projectNameStr +"】发票申请通过";
 				str = "项目【"+ projectNameStr +"】发票申请通过,发票申请编号:"+workInvoice.getNumber();
 				workInvoice.setInvoiceState("5");
@@ -2684,7 +2734,7 @@ public class WorkInvoiceService extends CrudService<WorkInvoiceDao, WorkInvoice>
 											notifyRole));
 				}
 			}
-			workActivityProcessService.deleteProcessIdAuditUsers(workInvoice.getProcessInstanceId());
+			workActivityProcessService.deleteProcessIdAuditUsers(workInvoice.getProcessInstanceId());*/
 		} else {
 			if (StringUtils.isNotBlank(workActivityMenu.getProcessType()) && !workActivityMenu.getProcessType().equals("newWorkInvoiceNotProject")) {
 				WorkProjectNotify notify = new WorkProjectNotify();
@@ -4107,4 +4157,365 @@ public class WorkInvoiceService extends CrudService<WorkInvoiceDao, WorkInvoice>
 	public  void updateOmsUrlById(WorkInvoice workInvoice){
 		dao.updateOmsUrlById(workInvoice);
 	}
+
+
+	/**
+	 * OMS开票开票成功后对数据进行处理
+	 * @param workInvoice
+	 * @param invoiceInfo
+	 * @param informType
+	 * @return
+	 */
+	@Transactional(readOnly = false)
+	public  Boolean updateOmsByIdOnDown(WorkInvoice workInvoice, OMSInvoiceDetailInfo invoiceInfo, String informType){
+		Boolean isSuccess = false;
+		workInvoice.setOmsEinVno(invoiceInfo.getAllEinVno());
+		if(StringUtils.isNotBlank(invoiceInfo.getOfdUrl())){
+			//先将文件下载到本地
+			String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getOfdUrl(), workInvoice.getNumber() + ".ofd");
+			//如果文件下载下来了,并且返回值正常,则此处需要对文件进行上传到阿里云操作
+			if(StringUtils.isNotBlank(ofdLocalFilePath)){
+				// 调用上传方法,接收Map类型返回值(替换原String类型接收)
+				Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNumber());
+				fileUploadOnAttachment(uploadResult,workInvoice, "ofd", "1");
+			}
+		}
+		workInvoice.setOmsOfdUrl(invoiceInfo.getOfdUrl());
+
+		if(StringUtils.isNotBlank(invoiceInfo.getPdfUrl())){
+			//先将文件下载到本地
+			String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getPdfUrl(), workInvoice.getNumber() + ".pdf");
+			//如果文件下载下来了,并且返回值正常,则此处需要对文件进行上传到阿里云操作
+			if(StringUtils.isNotBlank(ofdLocalFilePath)){
+				// 调用上传方法,接收Map类型返回值(替换原String类型接收)
+				Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNumber());
+				fileUploadOnAttachment(uploadResult,workInvoice, "pdf", "2");
+			}
+		}
+
+		workInvoice.setOmsPdfUrl(invoiceInfo.getPdfUrl());
+
+		if(StringUtils.isNotBlank(invoiceInfo.getXmlUrl())){
+			//先将文件下载到本地
+			String ofdLocalFilePath = FileHandlingUtil.downloadFileToSpecifiedPath(invoiceInfo.getXmlUrl(), workInvoice.getNumber() + ".xml");
+			//如果文件下载下来了,并且返回值正常,则此处需要对文件进行上传到阿里云操作
+			if(StringUtils.isNotBlank(ofdLocalFilePath)){
+				// 调用上传方法,接收Map类型返回值(替换原String类型接收)
+				Map<String, Object> uploadResult = FileHandlingUtil.fileUpload(ofdLocalFilePath, workInvoice.getNumber());
+				fileUploadOnAttachment(uploadResult,workInvoice, "xml", "3");
+			}
+		}
+		workInvoice.setOmsXmlUrl(invoiceInfo.getXmlUrl());
+		workInvoice.setInvoiceState("5");
+		//修改结果
+		dao.updateOmsUrlById(workInvoice);
+		//还需要修改work_invoice_detail表中的对应发票编号(number)值
+		WorkInvoiceDetail workInvoiceDetail = new WorkInvoiceDetail();
+		workInvoiceDetail.setInvoiceId(workInvoice.getId());
+		workInvoiceDetail.setNumber(invoiceInfo.getAllEinVno());
+		workInvoiceDetailDao.updateNumberById(workInvoiceDetail);
+
+		//需要修改流程完成通知
+		//审核完成提示框
+		List<User> users = new ArrayList<>();
+		users.add(workInvoice.getCreateBy());
+		List<User> userList = new ArrayList<>();
+
+		List<WorkInvoiceProjectRelation> projectRelationList = dao.getProjectRelation(workInvoice.getId());
+		workInvoice.setWorkInvoiceProjectRelationList(projectRelationList);
+
+		Office office = officeService.get(workInvoice.getOffice().getId());
+		String userName = UserUtils.get(workInvoice.getCreateBy().getId()).getName();
+
+		//项目名称获取
+		List<String> projectNameList = getProjectNameList(workInvoice);
+		String projectNameStr = String.join(",", projectNameList);
+		projectNameStr = projectNameStr.length() > 900
+				? projectNameStr.substring(0, 900)
+				: projectNameStr;
+
+		String str = "发票金额:" + workInvoice.getMoney() + "(元)。项目【"+ projectNameStr +"】发票申请通过,发票申请编号:"+workInvoice.getNumber() + ",创建人:"+userName+",所属部门:"+office.getName();
+		if(null != workInvoice.getClient()){
+			str = str + ",实际开票单位:"+workInvoice.getClient().getName();
+		}
+
+		String title = str;
+
+		WorkProjectNotify notify = new WorkProjectNotify();
+		notify.setNotifyId(workInvoice.getId());
+		userList = workProjectNotifyService.readByNotifyId(notify);
+
+		//财务员工
+		List<User> notifiedPartyUsers = Lists.newArrayList();
+		/*Office cwbOffice = officeService.getByName("财务部");
+		List<User> notifiedPartyUsers = UserUtils.getByRoleActivityEnnameOnNotCompany("cwygevod",3,cwbOffice.getId(),"5",workInvoice.getCreateBy(),workInvoice.getCompanyId());*/
+		notifiedPartyUsers.add(workInvoice.getCreateBy());
+
+		//通知开票人
+		if (StringUtils.isNotBlank(workInvoice.getInvoiceState()) && !workInvoice.getInvoiceState().equals("3")){
+			workInvoice.setInvoiceState("5");
+			for (User notifiedPartyUser : notifiedPartyUsers) {
+				workProjectNotifyService
+						.save(UtilNotify
+								.saveNewNotify(workInvoice.getId(),
+										notifiedPartyUser,
+										workInvoice.getCompanyId(),
+										title,
+										str,
+										informType,
+										"0",
+										"待通知",
+										"开票完成",
+										workInvoice.getProcessInstanceId(),new Date()));
+			}
+		}
+		//删除对应的流程节点信息
+		workActivityProcessService.deleteProcessIdAuditUsers(workInvoice.getProcessInstanceId());
+
+		if (userList!=null) {
+			for (User u : userList) {
+				UserUtils.pushMeIm(u.getId());
+			}
+		}
+
+		//发起短信通知,用于通知开票人
+		AjaxJson j = new AjaxJson();
+		//查询开票人员的手机号
+		User user = userDao.get(workInvoice.getCreateBy().getId());
+		if (StringUtils.isNotBlank(user.getMobile()) && mobilePattern.matcher(user.getMobile().trim()).matches() ) {
+			//验证手机号是否已经注册
+			if(userDao.validateMobile("mobile") == null){
+				HashMap<String,Object> result = null;
+				try{
+					//调用工具类返回结果
+					System.out.println("准备调用updatePasswordSendSms,mobile=" + user.getMobile() + ",number=" + workInvoice.getNumber());
+					result = ALiYunSmsUtil.omsWorkInvoiceSuccessSms(user.getMobile(), workInvoice.getNumber());
+					System.out.println("调用updatePasswordSendSms完成,返回结果=" + result);
+					Integer statusCode = (Integer) result.get("statusCode");
+					if (200 == statusCode) {
+						System.out.println("进入获取密码修改短信通知接口2。获取阿里云短信通知成功");
+						j.setSuccess(true);
+						j.setErrorCode("-1");
+						j.setMsg("短信发送成功!");
+
+					}else if(10001 == statusCode){
+
+						j.setSuccess(false);
+						j.setErrorCode("2");
+						String message = (String) result.get("message");
+						//j.setMsg("短信发送失败,错误代码:"+result+",请联系管理员。");
+						j.setMsg(message);
+						j.put("ErrorXml",result);
+					}else if(10002 == statusCode){
+						j.setSuccess(false);
+						j.setErrorCode("2");
+						j.put("message","账户短信量余额不足,请联系管理员进行充值!");
+						j.put("ErrorXml",result);
+					}else if(10003 == statusCode){
+						j.setSuccess(false);
+						j.setErrorCode("2");
+						j.put("message","手机号获取验证码次数已达每日上限!");
+						j.put("ErrorXml",result);
+					}else{
+						j.setSuccess(false);
+						j.setErrorCode("2");
+						j.put("message","短信发送失败,错误代码:101,请联系管理员!");
+						j.put("ErrorXml",result);
+					}
+				}catch (Exception e){
+					System.out.println("阿里云发送短信失败。失败原因为:" + e.getMessage());
+					logger.error("调用短信接口发生错误", e);  // 记录详细日志
+					e.printStackTrace();
+					logger.info("5");
+					j.setSuccess(false);
+					j.setErrorCode("101");
+					j.setMsg("短信发送失败!");
+				}
+				finally {
+
+				}
+			}
+		}
+
+
+		isSuccess = true;
+
+
+		return isSuccess;
+	}
+
+
+	/**
+	 * 文件添加到work_attachment表中
+	 * @param uploadResult
+	 * @param workInvoice
+	 * @param type
+	 * @param sort
+	 */
+	public void fileUploadOnAttachment(Map<String, Object> uploadResult, WorkInvoice workInvoice, String type, String sort){
+		// 解析Map返回结果(核心适配点)
+		boolean isUploadSuccess = (boolean) uploadResult.get("success");
+		String ossFilePath = (String) uploadResult.get("ossFilePath");
+		Long fileSize = (Long) uploadResult.get("fileSize"); // 从Map获取file.size的字节数
+
+		//上传成功判断(替换原StringUtils.isNotBlank(ossFilePath))
+		if (isUploadSuccess && StringUtils.isNotBlank(ossFilePath)) {
+			// 检查是否以"/"开头,若不是则添加
+			if (!ossFilePath.startsWith("/")) {
+				ossFilePath = "/" + ossFilePath;
+			}
+			// 5. 构建附件表实体(保留你的核心业务逻辑,调整fileSize赋值)
+			Workattachment workattachment = new Workattachment();
+			workattachment.setDelFlag("0");
+			workattachment.setUrl(ossFilePath); // 存入OSS路径
+			workattachment.setType(type); // 文件类型
+			workattachment.setAttachmentName(workInvoice.getNumber() + "." + type); // 附件名称
+			workattachment.setAttachmentFlag("OMS_invoice_file"); // 附件标识
+			workattachment.setFileSize(String.valueOf(fileSize)); // 改用Map中的文件大小(上传后本地文件已删除,原f.length()可能失效)
+			workattachment.setAttachmentId(workInvoice.getId()); // 关联发票ID
+			workattachment.setAttachmentUser("1");
+			workattachment.setId(UUID.randomUUID().toString().replace("-", "")); // 主键ID
+			workattachment.setCreateBy(userService.get("1"));
+			workattachment.setSort(sort);
+			workattachment.setCreateDate(new Date()); // 创建时间
+			workattachment.setUpdateBy(userService.get("1"));
+			workattachment.setUpdateDate(new Date()); // 更新时间
+
+			//校验数据库中是否已存在该附件(避免重复插入)
+			List<Workattachment> existAttachments = workattachmentService.getByAttachmentIdAndUrlAndAttachmentFlag(workattachment);
+			if (CollectionUtils.isEmpty(existAttachments)) { // 建议用CollectionUtils.isEmpty判断空集合,更安全
+				workattachmentService.insert(workattachment);
+			}
+		}
+	}
+
+
+	/**
+	 * OMS发票开具失败进行的提示方法操作
+	 * @param workInvoice
+	 * @param errorMessage
+	 * @param informType
+	 * @return
+	 */
+	@Transactional(readOnly = false)
+	public  Boolean handleInvoiceRetryAllFail(WorkInvoice workInvoice, String errorMessage, String informType){
+		Boolean isSuccess = false;
+
+		//需要修改流程完成通知
+		//审核完成提示框
+		List<User> users = new ArrayList<>();
+		users.add(workInvoice.getCreateBy());
+		List<User> userList = new ArrayList<>();
+
+		List<WorkInvoiceProjectRelation> projectRelationList = dao.getProjectRelation(workInvoice.getId());
+		workInvoice.setWorkInvoiceProjectRelationList(projectRelationList);
+
+		//项目名称获取
+		List<String> projectNameList = getProjectNameList(workInvoice);
+		String projectNameStr = String.join(",", projectNameList);
+		projectNameStr = projectNameStr.length() > 900
+				? projectNameStr.substring(0, 900)
+				: projectNameStr;
+
+		String str = "发票申请编号:"+workInvoice.getNumber() + "发票金额:" + workInvoice.getMoney() + "(元)。项目【"+ projectNameStr +"】发票申请失败,请核实后重新发起。失败原因:" + errorMessage;
+
+		String title = str;
+
+		WorkProjectNotify notify = new WorkProjectNotify();
+		notify.setNotifyId(workInvoice.getId());
+		userList = workProjectNotifyService.readByNotifyId(notify);
+		//通知开票发起人和财务审核人
+		//财务员工
+		Office cwbOffice = officeService.getByName("财务部");
+		List<User> notifiedPartyUsers = UserUtils.getByRoleActivityEnnameOnNotCompany("cwygevod",3,cwbOffice.getId(),"5",workInvoice.getCreateBy(),workInvoice.getCompanyId());
+		notifiedPartyUsers.add(workInvoice.getCreateBy());
+
+
+		if (StringUtils.isNotBlank(workInvoice.getInvoiceState()) && !workInvoice.getInvoiceState().equals("3")){
+			for (User notifiedPartyUser : notifiedPartyUsers) {
+				workProjectNotifyService
+						.save(UtilNotify
+								.saveNewNotify(workInvoice.getId(),
+										notifiedPartyUser,
+										workInvoice.getCompanyId(),
+										title,
+										str,
+										informType,
+										"0",
+										"待通知",
+										"开票失败",
+										workInvoice.getProcessInstanceId(),new Date()));
+			}
+		}
+		//删除对应的流程节点信息
+		workActivityProcessService.deleteProcessIdAuditUsers(workInvoice.getProcessInstanceId());
+
+		if (userList!=null) {
+			for (User u : userList) {
+				UserUtils.pushMeIm(u.getId());
+			}
+		}
+
+		//发起短信通知,用于通知开票人
+		AjaxJson j = new AjaxJson();
+		//查询开票人员的手机号
+		User user = userDao.get(workInvoice.getCreateBy().getId());
+		if (StringUtils.isNotBlank(user.getMobile()) && mobilePattern.matcher(user.getMobile().trim()).matches() ) {
+			//验证手机号是否已经注册
+			if(userDao.validateMobile("mobile") == null){
+				HashMap<String,Object> result = null;
+				try{
+					//调用工具类返回结果
+					result = ALiYunSmsUtil.omsWorkInvoiceErrorSms(user.getMobile(), workInvoice.getNumber());
+					Integer statusCode = (Integer) result.get("statusCode");
+					if (200 == statusCode) {
+						System.out.println("进入获取密码修改短信通知接口2。获取阿里云短信通知成功");
+						j.setSuccess(true);
+						j.setErrorCode("-1");
+						j.setMsg("短信发送成功!");
+
+					}else if(10001 == statusCode){
+
+						j.setSuccess(false);
+						j.setErrorCode("2");
+						String message = (String) result.get("message");
+						//j.setMsg("短信发送失败,错误代码:"+result+",请联系管理员。");
+						j.setMsg(message);
+						j.put("ErrorXml",result);
+					}else if(10002 == statusCode){
+						j.setSuccess(false);
+						j.setErrorCode("2");
+						j.put("message","账户短信量余额不足,请联系管理员进行充值!");
+						j.put("ErrorXml",result);
+					}else if(10003 == statusCode){
+						j.setSuccess(false);
+						j.setErrorCode("2");
+						j.put("message","手机号获取验证码次数已达每日上限!");
+						j.put("ErrorXml",result);
+					}else{
+						j.setSuccess(false);
+						j.setErrorCode("2");
+						j.put("message","短信发送失败,错误代码:101,请联系管理员!");
+						j.put("ErrorXml",result);
+					}
+				}catch (Exception e){
+					System.out.println("阿里云发送短信失败。失败原因为:" + e.getMessage());
+					logger.error("调用短信接口发生错误", e);  // 记录详细日志
+					e.printStackTrace();
+					logger.info("5");
+					j.setSuccess(false);
+					j.setErrorCode("101");
+					j.setMsg("短信发送失败!");
+				}
+				finally {
+
+				}
+			}
+		}
+
+		isSuccess = true;
+
+
+		return isSuccess;
+	}
 }

+ 7 - 5
src/main/java/com/jeeplus/modules/workinvoice/thread/ApprovalThread.java

@@ -39,11 +39,13 @@ public class ApprovalThread extends Thread {
 
     private OMSDisposeService omsDisposeService = SpringContextHolder.getBean(OMSDisposeService.class);
     private WorkInvoice workInvoice;
+    private String informType;
 
 
-    public ApprovalThread(WorkInvoice workInvoice)
+    public ApprovalThread(WorkInvoice workInvoice, String informType)
     {
         this.workInvoice = workInvoice;
+        this.informType = informType;
     }
 
     public void run(){
@@ -54,17 +56,17 @@ public class ApprovalThread extends Thread {
             //根据项目id查询是否已经提交过了审定单签章
             if(null == workInvoice){
                 disposeResult = "查询不到该开票信息";
-                omsDisposeService.handleInvoiceRetryAllFail("","",disposeResult);
+                omsDisposeService.handleInvoiceRetryAllFail("","",disposeResult, informType);
             }
             if(null!= workInvoice.getOmsOfdUrl()){
                 disposeResult = "该发票已经开过票";
-                omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult);
+                omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult, informType);
             }
-            omsDisposeService.doInvoiceBusiness(map,workInvoice.getId());
+            omsDisposeService.doInvoiceBusiness(map,workInvoice.getId(), informType);
 
         }catch (Exception e){
             disposeResult = e.getMessage();
-            omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult);
+            omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult, informType);
         }finally {
 
         }

+ 13 - 7
src/main/java/com/jeeplus/modules/workinvoice/thread/RedApprovalThread.java

@@ -16,13 +16,15 @@ public class RedApprovalThread extends Thread {
 
     private OMSDisposeService omsDisposeService = SpringContextHolder.getBean(OMSDisposeService.class);
     private WorkInvoice workInvoice;
-    String originalInvno = "";
+    String originalInvno;
+    String informType;
 
 
-    public RedApprovalThread(WorkInvoice workInvoice, String originalInvno)
+    public RedApprovalThread(WorkInvoice workInvoice, String originalInvno, String informType)
     {
         this.workInvoice = workInvoice;
         this.originalInvno = originalInvno;
+        this.informType = informType;
     }
 
     public void run(){
@@ -33,17 +35,21 @@ public class RedApprovalThread extends Thread {
             //根据项目id查询是否已经提交过了审定单签章
             if(null == workInvoice){
                 disposeResult = "查询不到该开票信息";
-                omsDisposeService.handleInvoiceRetryAllFail("","",disposeResult);
+                omsDisposeService.handleInvoiceRetryAllFail("","",disposeResult, informType);
             }
-            if(null!= workInvoice.getOmsOfdUrl()){
+            if(StringUtils.isNotBlank(workInvoice.getOmsOfdUrl())){
                 disposeResult = "该发票已经开过票";
-                omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult);
+                omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult, informType);
             }
-            omsDisposeService.doAllScenarioRedInvoiceBusiness(map,workInvoice.getId(),originalInvno);
+            if(StringUtils.isBlank(workInvoice.getRedInvoiceRelevancyId())){
+                disposeResult = "未找到该发票关联需要开红字票的发票";
+                omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult, informType);
+            }
+            omsDisposeService.doAllScenarioRedInvoiceBusiness(map,workInvoice.getId(), workInvoice.getRedInvoiceRelevancyId(), originalInvno, informType);
 
         }catch (Exception e){
             disposeResult = e.getMessage();
-            omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult);
+            omsDisposeService.handleInvoiceRetryAllFail("",workInvoice.getId(),disposeResult, informType);
 
         }finally {
 

+ 273 - 0
src/main/java/com/jeeplus/modules/workinvoice/utils/FileHandlingUtil.java

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

+ 88 - 16
src/main/java/com/jeeplus/modules/workinvoice/utils/OMSNationUtil.java

@@ -147,7 +147,7 @@ public class OMSNationUtil {
         }else if(workInvoice.getNewDrawer().equals("2")){
             omsImportInfo.setBuyerType("1"); //购买方类型(0:企业;1:自然人) 默认企业
         }else{
-            omsImportInfo.setBuyerType(""); //购买方类型(0:企业;1:自然人) 默认企业
+            omsImportInfo.setBuyerType("0"); //购买方类型(0:企业;1:自然人) 默认企业
         }
 
 
@@ -214,9 +214,13 @@ public class OMSNationUtil {
      * 整理数据明文
      * @param workInvoiceId 开票的id
      * @param originalInvno 原蓝票发票号码(数电票号码)
+     * @param workInvoice 红冲发票信息
+     * @param workInvoiceRelevancy 原蓝票被红冲发票信息
+     * @param workInvoiceTaxClassificationCode 红冲数据的税收分类编码
+     * @param relevancyBillingContentDetail 被红冲蓝票数据的税收分类编码
      * @return
      */
-    public String neatenAllScenarioRedInvoiceData(String workInvoiceId, String originalInvno, WorkInvoice workInvoice) {
+    public String neatenAllScenarioRedInvoiceData(String workInvoiceId, String originalInvno, WorkInvoice workInvoice, WorkInvoice workInvoiceRelevancy, WorkInvoiceTaxClassificationCode workInvoiceTaxClassificationCode, WorkInvoiceTaxClassificationCode relevancyBillingContentDetail) {
         List<OMSApplyItem> orderItems = Lists.newArrayList();
 
         OMSAllScenarioRedInvoiceInfo omsAllScenarioRedInvoiceInfo = new OMSAllScenarioRedInvoiceInfo();
@@ -231,22 +235,88 @@ public class OMSNationUtil {
         //原蓝票发票号码
         omsAllScenarioRedInvoiceInfo.setOriginalInvno(originalInvno);
         //原蓝票发票类型
-        omsAllScenarioRedInvoiceInfo.setOriginalInvType("01");
+        //* 01:数电票(增值税专用发票) 02:数电票(普通发票)等
+        if(workInvoiceRelevancy.getInvoiceType().equals("1")){
+            omsAllScenarioRedInvoiceInfo.setOriginalInvType("01");
+        }else{
+            omsAllScenarioRedInvoiceInfo.setOriginalInvType("02");
+        }
+
+        //此处用来计算红冲的蓝票的税额
+
+        // 1. 获取含税金额(价税合计)- 从workInvoice中取值
+        BigDecimal blueTaxIncludedAmount = BigDecimal.valueOf(workInvoiceRelevancy.getMoney());
+        //获取税率
+        BigDecimal blueTaxRate;
+        if (relevancyBillingContentDetail.getTaxRate() != null) {
+            blueTaxRate = BigDecimal.valueOf(relevancyBillingContentDetail.getTaxRate());
+            // 税率格式兼容:6(整数)转0.06,0.06直接用(保留4位小数,四舍五入)
+            blueTaxRate = blueTaxRate.compareTo(BigDecimal.ONE) > 0
+                    ? blueTaxRate.divide(new BigDecimal("100"), 4, BigDecimal.ROUND_HALF_UP)
+                    : blueTaxRate;
+        } else {
+            // 默认税率6%(用字符串构造避免浮点精度丢失)
+            blueTaxRate = new BigDecimal("0.06");
+        }
+
+        // 3. 计算不含税金额:含税金额 ÷ (1+税率),保留2位小数(四舍五入)
+        BigDecimal blueTaxExcludedAmount = blueTaxIncludedAmount
+                .divide(BigDecimal.ONE.add(blueTaxRate), 2, BigDecimal.ROUND_HALF_UP);
+
+        // 4. 计算税额:含税金额 - 不含税金额(保证无尾差,适配正负值)
+        BigDecimal blueTaxAmount = blueTaxIncludedAmount.subtract(blueTaxExcludedAmount);
+
         //原蓝票金额
-        omsAllScenarioRedInvoiceInfo.setOriginalInvAmount("100");
+        omsAllScenarioRedInvoiceInfo.setOriginalInvAmount(blueTaxIncludedAmount.toString());
+
         //原蓝票税额
-        omsAllScenarioRedInvoiceInfo.setOriginalInvTax("1");
+        omsAllScenarioRedInvoiceInfo.setOriginalInvTax(blueTaxAmount.toString());
+
+
+        //此处用来处理红冲对应的金额汇总
+        // 1. 获取含税金额(价税合计)- 从workInvoice中取值
+        BigDecimal taxIncludedAmount = BigDecimal.valueOf(workInvoice.getMoney());
+
+        // 2. 处理税率:优先用动态值,为空则默认6%(兼容6/0.06两种格式)
+        BigDecimal taxRate;
+        if (workInvoiceTaxClassificationCode.getTaxRate() != null) {
+            taxRate = BigDecimal.valueOf(workInvoiceTaxClassificationCode.getTaxRate());
+            // 税率格式兼容:6(整数)转0.06,0.06直接用(保留4位小数,四舍五入)
+            taxRate = taxRate.compareTo(BigDecimal.ONE) > 0
+                    ? taxRate.divide(new BigDecimal("100"), 4, BigDecimal.ROUND_HALF_UP)
+                    : taxRate;
+        } else {
+            // 默认税率6%(用字符串构造避免浮点精度丢失)
+            taxRate = new BigDecimal("0.06");
+        }
+
+        // 3. 计算不含税金额:含税金额 ÷ (1+税率),保留2位小数(四舍五入)
+        BigDecimal taxExcludedAmount = taxIncludedAmount
+                .divide(BigDecimal.ONE.add(taxRate), 2, BigDecimal.ROUND_HALF_UP);
+
+        // 4. 计算税额:含税金额 - 不含税金额(保证无尾差,适配正负值)
+        BigDecimal taxAmount = taxIncludedAmount.subtract(taxExcludedAmount);
+
+
         //合计金额(不含税)
-        omsAllScenarioRedInvoiceInfo.setTotalAmount("-100");
+        omsAllScenarioRedInvoiceInfo.setTotalAmount(taxExcludedAmount.toString());
         //合计税额
-        omsAllScenarioRedInvoiceInfo.setTotalTax("-1.00");
+        omsAllScenarioRedInvoiceInfo.setTotalTax(taxAmount.toString());
         //价税合计
-        omsAllScenarioRedInvoiceInfo.setTotalTaxamount("-101.00");
+        omsAllScenarioRedInvoiceInfo.setTotalTaxamount(taxIncludedAmount.setScale(2, BigDecimal.ROUND_HALF_UP).toString());
 
         //* 冲红原因(必填)
         //* 01:开票有误 02:销货退回 03:服务中止 04:销售折让
-
-        omsAllScenarioRedInvoiceInfo.setRedReason("01");
+        if(StringUtils.isBlank(workInvoice.getRedFlushReason())){
+            //如果蓝票开票额 + 红票开票额 大于零,则表示此处是部分红冲,那么红冲原因就是04
+            if (blueTaxIncludedAmount.add(taxIncludedAmount).compareTo(BigDecimal.ZERO) > 0) {
+                omsAllScenarioRedInvoiceInfo.setRedReason("04");
+            }else{
+                omsAllScenarioRedInvoiceInfo.setRedReason("01");
+            }
+        }else{
+            omsAllScenarioRedInvoiceInfo.setRedReason(workInvoice.getRedFlushReason());
+        }
         //是否自动开票
         omsAllScenarioRedInvoiceInfo.setAutoMakeInv("Y");
 
@@ -254,16 +324,18 @@ public class OMSNationUtil {
         OMSApplyItem orderItem = new OMSApplyItem();
         orderItem.setLineCode("1");
         //添加商品名称
-        orderItem.setGoodsName(goodsName);
+        orderItem.setGoodsName(workInvoiceTaxClassificationCode.getGoodName());
 
         //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(goodstaxno);//税收分类编码
+
+
+        orderItem.setAmount(taxExcludedAmount); //金额
+        orderItem.setTax(taxAmount);//税额
+        orderItem.setTaxrate(BigDecimal.valueOf(workInvoiceTaxClassificationCode.getTaxRate())); //税率
+        orderItem.setTaxamount(BigDecimal.valueOf(workInvoice.getMoney()));//含税金额
+        orderItem.setGoodstaxno(workInvoiceTaxClassificationCode.getGoodsTaxno());//税收分类编码
         orderItem.setOriLineCode("1");//对应蓝票明细序号
 
         orderItems.add(orderItem);

+ 6 - 0
src/main/java/com/jeeplus/modules/workinvoicedetail/dao/WorkInvoiceDetailDao.java

@@ -50,4 +50,10 @@ public interface WorkInvoiceDetailDao extends CrudDao<WorkInvoiceDetail> {
     int countByNumAndComp(@Param("invoiceNum") String invoiceNum, @Param("comId") String comId);
 
     void deleteByInvoiceId(@Param("invoiceId")String invoiceId);
+
+    /**
+     * 根据开票id修改number值
+     * @param invoice
+     */
+    void updateNumberById(WorkInvoiceDetail invoice);
 }

+ 4 - 2
src/main/resources/mappings/modules/sys/WorkattachmentDao.xml

@@ -292,7 +292,8 @@
 			attachment_flag,
 			div_id_type,
 			project_id,
-		    file_size
+		    file_size,
+		    sort
 		) VALUES (
 			#{id},
 			#{createBy.id},
@@ -310,7 +311,8 @@
 			#{attachmentFlag},
 			#{divIdType},
 			#{projectId},
-		    #{fileSize}
+		    #{fileSize},
+		    #{sort}
 		)
 	</insert>
 

+ 10 - 1
src/main/resources/mappings/modules/workinvoice/WorkInvoiceDao.xml

@@ -76,7 +76,16 @@
 		a.red_invoice_flag as "redInvoiceFlag",
 		a.red_invoice_relevancy_id as "redInvoiceRelevancyId",
 		a.red_invoice_relevancy_number as "redInvoiceRelevancyNumber",
-		a.id_card as "idCard"
+		a.oms_access_token_error as "omsAccessTokenError",
+		a.oms_access_token as "omsAccessToken",
+		a.oms_error_message as "omsErrorMessage",
+		a.oms_ein_vno as "omsEinVno",
+		a.oms_ofd_url as "omsOfdUrl",
+		a.oms_pdf_url as "omsPdfUrl",
+		a.oms_xml_url as "omsXmlUrl",
+		a.order_for_goods_result_str as "orderForGoodsResultStr",
+		a.confirmation_slip_result_str as "confirmationSlipResultStr",
+		a.red_flush_reason as "redFlushReason"
 	</sql>
 
 	<sql id="newWorkInvoiceColumns">

+ 7 - 0
src/main/resources/mappings/modules/workinvoicedetail/WorkInvoiceDetailDao.xml

@@ -619,4 +619,11 @@
 		DELETE FROM work_invoice_detail
 		WHERE invoice_id = #{invoiceId}
 	</update>
+
+
+	<update id="updateNumberById">
+		UPDATE work_invoice_detail SET
+			number = #{number}
+		WHERE invoice_id = #{invoiceId} and del_flag = 0
+	</update>
 </mapper>

+ 2 - 2
src/main/webapp/webpage/modules/workinvoice/workInvoiceNotProjectAuditEnd.jsp

@@ -896,7 +896,7 @@
 					<thead>
 					<tr>
 						<th>发票代码</th>
-						<th class="judgment-item"><span class="require-item">*</span>发票号</th>
+						<th class="judgment-item">发票号</th>
 						<th class="judgment-item"><span class="require-item">*</span>开票金额</th>
 						<th width="80px">税率</th>
 						<th width="100px">金额</th>
@@ -923,7 +923,7 @@
 									<input id="workAccountList{{idx}}_code" name="workAccountList[{{idx}}].code" type="text" value="{{row.code}}"    class="form-control number"/>
 								 </td>
 								 <td>
-									<input id="workAccountList{{idx}}_number" name="workAccountList[{{idx}}].number" onchange="checkSame(this)" type="text" value="{{row.number}}"  minlength="8" maxlength="8"  class="form-control number judgment"/>
+									<input id="workAccountList{{idx}}_number" name="workAccountList[{{idx}}].number" onchange="checkSame(this)" type="text" value="{{row.number}}"  minlength="8" maxlength="8"  class="form-control number"/>
 									 <input type="hidden" id="workAccountList{{idx}}_oldNumber" value="{{row.number}}"/>
 								 </td>
 								<td>

+ 41 - 3
src/main/webapp/webpage/modules/workinvoice/workInvoiceView.jsp

@@ -1,5 +1,6 @@
 <%@ page contentType="text/html;charset=UTF-8" %>
 <%@ include file="/webpage/include/taglib.jsp"%>
+
 <html>
 <head>
 	<title>发票管理</title>
@@ -11,6 +12,20 @@
 			padding-left: 0px;
 			padding-right: 0px;
 		}
+		.download-btn {
+			font-size: 12px; /* 字体偏小 */
+			padding: 2px 8px;
+			background-color: #e9ecef;
+			border: 1px solid #ced4da;
+			border-radius: 2px;
+			color: #495057;
+			text-decoration: none;
+		}
+
+		.download-btn:hover {
+			background-color: #dee2e6;
+			color: #212529;
+		}
 	</style>
 	<script type="text/javascript">
 		var validateForm;
@@ -350,7 +365,18 @@
 
 			</div>
 			<div class="form-group layui-row">
-				<div class="form-group-label"><h2>电子发票附件信息</h2></div>
+				<div class="form-group-label" style="display: flex; align-items: center; gap: 12px;">
+					<h2 style="margin: 0;">电子发票附件信息</h2>
+						<%-- 核心修改:判断workInvoice.workAttachments是否有值(非空且不是空集合) --%>
+					<c:if test="${not empty workInvoice.workAttachments}">
+						<%-- 提取集合中第一个元素的url(任意一个,取第一个最直接) --%>
+						<c:set var="downloadUrl" value="${workInvoice.workAttachments[0].url}" />
+
+						<%-- 拼接基础URL并编码,downloadUrl替换为提取的attachment的url --%>
+						<c:set var="baseUrl" value="${ctx}/workfullmanage/workFullManage/downLoadOMSInvoiceAttachzip?file=${downloadUrl}&fileName=发票-${workInvoice.number}所有发票格式.zip" />
+						<a href="<%= response.encodeURL(pageContext.getAttribute("baseUrl").toString()) %>" class="btn btn-xs btn-primary">下载所有格式发票</a>
+					</c:if>
+				</div>
 				<span id="attachment_title"></span>
 				<div class="layui-item layui-col-xs12" style="padding:0 16px;">
 					<table id="upTable" class="table table-bordered table-condensed details">
@@ -440,12 +466,24 @@
 											<c:when test="${workInvoice.uploadMode == 2}">
 												<c:choose>
 													<c:when test="${fn:containsIgnoreCase(workClientAttachment.attachmentName,'pdf')}">
-														<a href="${workClientAttachment.temporaryUrl}" target="_blank" class="op-btn op-btn-download"><i class="fa fa-download"></i>&nbsp;下载</a>
+														<a href="javascript:location.href='${ctx}/workfullmanage/workFullManage/downLoadAttachNoprefix?file='+encodeURIComponent('${workClientAttachment.url}');"
+														   class="op-btn op-btn-download">
+															<i class="fa fa-download"></i>&nbsp;下载
+														</a>
+													</c:when>
+													<c:when test="${fn:containsIgnoreCase(workClientAttachment.attachmentName,'xml')}">
+														<a href="javascript:location.href='${ctx}/workfullmanage/workFullManage/downLoadAttachNoprefix?file='+encodeURIComponent('${workClientAttachment.url}');"
+														   class="op-btn op-btn-download">
+															<i class="fa fa-download"></i>&nbsp;下载
+														</a>
 													</c:when>
 													<c:otherwise>
-														<a href="${workClientAttachment.temporaryUrl}" class="op-btn op-btn-download"><i class="fa fa-download"></i>&nbsp;下载</a>
+														<a href="${workClientAttachment.temporaryUrl}" class="op-btn op-btn-download">
+															<i class="fa fa-download"></i>&nbsp;下载
+														</a>
 													</c:otherwise>
 												</c:choose>
+
 											</c:when>
 											<c:otherwise>
 												<a href="javascript:location.href='${ctx}/workfullmanage/workFullManage/downLoadAttach?file='+encodeURIComponent('${workClientAttachment.url}');" class="op-btn op-btn-download"><i class="fa fa-download"></i>&nbsp;下载</a>