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

发票快据模块

zxf 1 napja
szülő
commit
489784fe17

+ 22 - 0
jd-logistics-modules/jd-logistics-system/src/main/java/com/ruoyi/logistics/config/InvoiceConfig.java

@@ -0,0 +1,22 @@
+package com.ruoyi.logistics.config;
+
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@RefreshScope
+@ConfigurationProperties(prefix = "invoice.config")
+public class InvoiceConfig {
+
+    private String appId;
+    private String appSecret;
+    private String serviceId;
+    private String signType;
+    private String baseUrl;
+    private String kpZddm;
+    private String serviceSelect;
+}

+ 58 - 0
jd-logistics-modules/jd-logistics-system/src/main/java/com/ruoyi/logistics/controller/BizInvoiceController.java

@@ -0,0 +1,58 @@
+package com.ruoyi.logistics.controller;
+import com.ruoyi.common.core.web.domain.AjaxResult;
+import com.ruoyi.logistics.callback.SfRoutingPushCallback;
+import com.ruoyi.logistics.domain.InvoiceRequest;
+import com.ruoyi.logistics.service.InvoiceService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+
+/**
+ * 百旺开票相关
+ * @author RuiJing
+ * @date 2026-02-06
+ */
+@RestController
+@RequestMapping("/invoice")
+public class BizInvoiceController {
+    private static final Logger logger = LoggerFactory.getLogger(BizInvoiceController.class);
+
+     @Autowired
+     InvoiceService invoiceService;
+
+    /**
+     * 发票开具
+     */
+    @PostMapping("/invoiceIssuance")
+    public AjaxResult invoiceIssuance(@RequestBody InvoiceRequest invoiceRequest) {
+        try {
+            Map result = invoiceService.invoiceIssuance(invoiceRequest);
+            if("0".equals(result.get("code").toString())){
+                AjaxResult.success("开票成功!");
+            }
+            return AjaxResult.error("开票失败!  原因:"+result.get("renson").toString());
+        }
+        catch(Exception e){
+            return AjaxResult.error("系统发生错误!"+e.getMessage());
+
+        }
+
+    }
+
+
+    /**
+     * 发票查询
+     */
+    @PostMapping("/selectInvoice")
+    public AjaxResult selectInvoice(@RequestBody Map param) {
+        Map result=invoiceService.selectInvoice(param);
+        return  null;
+    }
+}

+ 29 - 0
jd-logistics-modules/jd-logistics-system/src/main/java/com/ruoyi/logistics/domain/InvoiceRequest.java

@@ -0,0 +1,29 @@
+package com.ruoyi.logistics.domain;
+
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+
+/**
+ * 开票请求实体
+ * @author zxf
+ * @date 2026-02-03
+ */
+@Data
+public class InvoiceRequest {
+    private String khmc;   //购货方名称 这里要传供应商的相关信息
+    private String khsh;   //购货方税号 专票必须
+    private BigDecimal hsje;// 这个就是月度账单上面的总金额
+    private  String khdzdh;// 购货方单位地址  电话  中间空格隔开
+    private  String khyhzh; //购货方银行 账号  中间空格隔开
+    private  String gmfMobile;  //电话
+    private  String gmfEmail;   //邮箱
+    private  String bz;         //备注
+
+
+
+
+
+}

+ 20 - 0
jd-logistics-modules/jd-logistics-system/src/main/java/com/ruoyi/logistics/service/InvoiceService.java

@@ -0,0 +1,20 @@
+package com.ruoyi.logistics.service;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.ruoyi.logistics.domain.InvoiceRequest;
+
+import java.util.Map;
+
+/**
+ * 百旺开票Service接口
+ * @author RuiJing
+ * @date 2026-02-06
+ */
+public interface InvoiceService {
+
+
+    Map invoiceIssuance(InvoiceRequest invoiceRequest) throws JsonProcessingException;
+
+    Map selectInvoice(Map param);
+}

+ 138 - 0
jd-logistics-modules/jd-logistics-system/src/main/java/com/ruoyi/logistics/service/impl/InvoiceServiceImpl.java

@@ -0,0 +1,138 @@
+package com.ruoyi.logistics.service.impl;
+import com.alibaba.fastjson2.JSONObject;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.common.redis.service.RedisIdGenerator;
+import com.ruoyi.logistics.config.InvoiceConfig;
+import com.ruoyi.logistics.domain.InvoiceRequest;
+import com.ruoyi.logistics.service.InvoiceService;
+import com.ruoyi.logistics.util.Base64Util;
+import com.ruoyi.logistics.util.HttpUtil;
+import com.ruoyi.logistics.util.SignUtil;
+import com.ruoyi.logistics.util.TaxAmountCalculator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import java.math.BigDecimal;
+import com.fasterxml.jackson.core.type.TypeReference;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * 开票相关接口
+ *
+ * @author zxf
+ * @date 2026-02-11
+ */
+
+@Service
+public class InvoiceServiceImpl implements InvoiceService {
+    private static final Logger logger = LoggerFactory.getLogger(InvoiceServiceImpl.class);
+    @Autowired
+    private RedisIdGenerator redisIdGenerator;
+    @Autowired
+    private InvoiceConfig invoiceConfig;
+
+
+
+    /**
+     * 电子发票专票开据
+     * @param InvoiceRequest 开票相入参
+     * @return 运单明细
+     */
+
+    @Override
+    public Map invoiceIssuance(InvoiceRequest invoiceRequest) throws JsonProcessingException {
+        logger.info("收到开票请求参数:参数{}",invoiceRequest);
+        // 1. 组装正票申请的业务报文(content)
+        JSONObject content = new JSONObject();
+        // 必选参数
+        content.put("djbh", redisIdGenerator.generateUniqueId("BWKP")); // 全局唯一单据编号
+        content.put("kpzddm", invoiceConfig.getKpZddm());      // 开票终端代码
+        content.put("fplxdm", "01");                           // 发票类型:01=专票,02=普票
+        content.put("kplx", "0");                              // 开票类型:0=蓝字发票
+        content.put("zsfs", "0");                              // 征收方式:0=普通征税
+        content.put("khmc", invoiceRequest.getKhmc());         // 购货单位名称
+        content.put("hsje", invoiceRequest.getHsje());                           // 含税金额(价税合计)
+        content.put("gsdm", "7600");                           // 公司代码
+        content.put("yhdm", "ec014266e63344698642d6e5c5b8b556"); // 用户代码
+        content.put("sjlx", "1"); // 数据类型(批发系统,由平台提供)
+        content.put("sfwzzfp", "N"); // 非纸质发票(默认N)
+        content.put("hsbz", "1"); // 含税标志(1=含税,默认)
+        //可选参数
+        content.put("khsh", invoiceRequest.getKhsh()); // 购货单位税号(专票必填)
+        content.put("khdzdh", invoiceRequest.getKhdzdh()); // 购货单位地址电话
+        content.put("khyhzh", invoiceRequest.getKhyhzh()); // 购货单位银行账号
+        content.put("gmfMobile", invoiceRequest.getGmfMobile()); // 电子发票手机号
+        content.put("gmfEmail", invoiceRequest.getGmfEmail()); // 电子发票邮箱
+        content.put("bz", invoiceRequest.getBz()); // 备注
+        // 商品明细(mxxx,必选)
+        JSONObject goodsItem = new JSONObject();
+        goodsItem.put("djhh", "1"); // 单据行号
+        goodsItem.put("fphxz", "0"); // 发票行性质:0=正常行
+        goodsItem.put("spmc", "测试商品"); // 商品名称
+        goodsItem.put("jldw", "月"); //计量单位
+        goodsItem.put("spsl", 1.00); // 商品数量
+        goodsItem.put("ssbm", "3040802010100000000"); // 19位税收编码
+        goodsItem.put("hsdj", invoiceRequest.getHsje()); // 含税单价
+        goodsItem.put("hsje", invoiceRequest.getHsje()); // 商品含税金额
+        BigDecimal[] result =TaxAmountCalculator.calculateFromTaxIncluded(invoiceRequest.getHsje(), BigDecimal.valueOf(0.06));
+        goodsItem.put("tax", 0.06); // 税率13%
+        goodsItem.put("se", result[1]); // 商品税额
+        content.put("mxxx", new JSONObject[]{goodsItem});
+       logger.info("开票参数组装完成  参数{}",content);
+        String contentBase64 = Base64Util.encode(content.toJSONString());
+        // 3. 构建签名参数,生成签名值
+        Map<String, String> signParams = new HashMap<>();
+        signParams.put("appid", invoiceConfig.getAppId());
+        signParams.put("serviceid", invoiceConfig.getServiceId());
+        signParams.put("content", contentBase64);
+        String signature = SignUtil.sign(invoiceConfig.getSignType(), invoiceConfig.getAppSecret(), signParams);
+        // 4. 组装接口请求的公共参数
+        JSONObject requestParam = new JSONObject();
+        requestParam.put("appid", invoiceConfig.getAppId());
+        requestParam.put("serviceid", invoiceConfig.getServiceId());
+        requestParam.put("content", contentBase64);
+        requestParam.put("signature", signature);
+        requestParam.put("signType", invoiceConfig.getSignType());
+        String responseJson = HttpUtil.doPostJson(invoiceConfig.getBaseUrl(), requestParam.toJSONString());
+        ObjectMapper objectMapper = new ObjectMapper();
+       // JsonNode jsonNode = objectMapper.readTree(responseJson);
+        Map<String, Object> invoiceMap = objectMapper.readValue(
+                responseJson,
+                new TypeReference<Map<String, Object>>() {}
+        );
+        return invoiceMap;
+    }
+
+
+
+    /**
+     * 电子发票正票擦汗寻
+     * @param InvoiceRequest 开票相入参
+     * @return 运单明细
+     */
+
+    @Override
+    public Map selectInvoice(Map param) {
+        // 1. 组装正票申请的业务报文(content)
+        JSONObject content = new JSONObject();
+        content.put("djbh",param.get("djbh"));
+        String contentBase64 = Base64Util.encode(content.toJSONString());
+        Map<String, String> signParams = new HashMap<>();
+        signParams.put("appid", invoiceConfig.getAppId());
+        signParams.put("serviceid", invoiceConfig.getServiceSelect());
+        signParams.put("content", contentBase64);
+        String signature = SignUtil.sign(invoiceConfig.getSignType(), invoiceConfig.getAppSecret(), signParams);
+        JSONObject requestParam = new JSONObject();
+        requestParam.put("appid", invoiceConfig.getAppId());
+        requestParam.put("serviceid", invoiceConfig.getServiceSelect());
+        requestParam.put("content", contentBase64);
+        requestParam.put("signature", signature);
+        requestParam.put("signType", invoiceConfig.getSignType());
+        String responseJson = HttpUtil.doPostJson(invoiceConfig.getBaseUrl(), requestParam.toJSONString());
+        return null;
+    }
+}

+ 39 - 0
jd-logistics-modules/jd-logistics-system/src/main/java/com/ruoyi/logistics/util/HttpUtil.java

@@ -0,0 +1,39 @@
+package com.ruoyi.logistics.util;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import java.nio.charset.StandardCharsets;
+
+public class HttpUtil {
+    // 发送JSON格式POST请求
+    public static String doPostJson(String url, String jsonBody) {
+        CloseableHttpClient httpClient = HttpClients.createDefault();
+        HttpPost httpPost = new HttpPost(url);
+        try {
+            // 设置请求头
+            httpPost.setHeader("Content-Type", "application/json;charset=UTF-8");
+            // 设置请求体
+            StringEntity entity = new StringEntity(jsonBody, StandardCharsets.UTF_8);
+            httpPost.setEntity(entity);
+            // 执行请求
+            CloseableHttpResponse response = httpClient.execute(httpPost);
+            HttpEntity responseEntity = response.getEntity();
+            if (responseEntity != null) {
+                return EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                httpClient.close();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+}

+ 10 - 8
jd-logistics-modules/jd-logistics-system/src/main/java/com/ruoyi/logistics/util/SignUtil.java

@@ -1,16 +1,14 @@
-/*
 package com.ruoyi.logistics.util;
-
-import java.util.Map;
-import java.util.TreeMap;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
+import java.util.Map;
+import java.util.TreeMap;
 import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.lang3.StringUtils;
-*/
+
 /**
  * 签名工具
- *//*
+ */
 
 public class SignUtil {
 
@@ -28,7 +26,9 @@ public class SignUtil {
     public static String sign(String signType, String appsecret, Map<String, String> querys) {
         String signature = "";
         try {
-            signType = StringUtils.validateStr(signType, SIGN_TYPE_0);//默认 HmacSHA256
+            if (StringUtils.isBlank(signType) || (!SIGN_TYPE_0.equals(signType) && !SIGN_TYPE_1.equals(signType))) {
+                signType = SIGN_TYPE_0; // 默认HmacSHA256
+            }
             if(SIGN_TYPE_0.equals(signType)){
                 Mac hmacSha256 = Mac.getInstance(HMAC_SHA256);
                 byte[] keyBytes = appsecret.getBytes(ENCODING);
@@ -106,5 +106,7 @@ public class SignUtil {
         System.out.println(">>>>>>>>>>>>>>>>>>>>");
         return sb.toString();
     }
+
+
 }
-*/
+

+ 135 - 0
jd-logistics-modules/jd-logistics-system/src/main/java/com/ruoyi/logistics/util/TaxAmountCalculator.java

@@ -0,0 +1,135 @@
+package com.ruoyi.logistics.util;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 税务金额计算工具类(保证精度,适配财务/税务规则)
+ */
+public class TaxAmountCalculator {
+    // 小数位数:中间计算保留6位,最终展示保留2位(符合税务要求)
+    private static final int SCALE_DETAIL = 6;
+    private static final int SCALE_FINAL = 2;
+    // 舍入模式:四舍五入(财务/税务标准)
+    private static final RoundingMode ROUND_MODE = RoundingMode.HALF_UP;
+
+    /**
+     * 1. 已知含税金额 + 税率 → 计算不含税金额 + 税额
+     * @param taxIncludedAmount 含税金额(如113.00元)
+     * @param taxRate 税率(如13%则传0.13)
+     * @return 结果数组:[0]不含税金额 [1]税额
+     */
+    public static BigDecimal[] calculateFromTaxIncluded(BigDecimal taxIncludedAmount, BigDecimal taxRate) {
+        // 校验参数(非空、非负)
+        validateAmount(taxIncludedAmount);
+        validateRate(taxRate);
+
+        // 计算不含税金额:含税金额 / (1 + 税率)
+        BigDecimal taxExcludedAmount = taxIncludedAmount
+                .divide(BigDecimal.ONE.add(taxRate), SCALE_DETAIL, ROUND_MODE)
+                .setScale(SCALE_FINAL, ROUND_MODE); // 最终保留2位
+
+        // 计算税额:含税金额 - 不含税金额(避免乘法误差)
+        BigDecimal taxAmount = taxIncludedAmount.subtract(taxExcludedAmount)
+                .setScale(SCALE_FINAL, ROUND_MODE);
+
+        return new BigDecimal[]{taxExcludedAmount, taxAmount};
+    }
+
+    /**
+     * 2. 已知不含税金额 + 税率 → 计算含税金额 + 税额
+     * @param taxExcludedAmount 不含税金额(如100.00元)
+     * @param taxRate 税率(如13%则传0.13)
+     * @return 结果数组:[0]含税金额 [1]税额
+     */
+    public static BigDecimal[] calculateFromTaxExcluded(BigDecimal taxExcludedAmount, BigDecimal taxRate) {
+        validateAmount(taxExcludedAmount);
+        validateRate(taxRate);
+
+        // 计算税额:不含税金额 × 税率
+        BigDecimal taxAmount = taxExcludedAmount.multiply(taxRate)
+                .setScale(SCALE_FINAL, ROUND_MODE);
+
+        // 计算含税金额:不含税金额 + 税额
+        BigDecimal taxIncludedAmount = taxExcludedAmount.add(taxAmount);
+
+        return new BigDecimal[]{taxIncludedAmount, taxAmount};
+    }
+
+    /**
+     * 3. 批量金额汇总计算(多笔金额合计,避免逐笔舍入误差)
+     * @param taxIncludedAmounts 多笔含税金额数组
+     * @param taxRate 税率
+     * @return 汇总结果:[0]汇总不含税金额 [1]汇总税额 [2]汇总含税金额
+     */
+    public static BigDecimal[] calculateBatchTotal(BigDecimal[] taxIncludedAmounts, BigDecimal taxRate) {
+        validateRate(taxRate);
+        if (taxIncludedAmounts == null || taxIncludedAmounts.length == 0) {
+            return new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO};
+        }
+
+        // 先汇总含税金额(避免逐笔计算后舍入导致总额误差)
+        BigDecimal totalTaxIncluded = BigDecimal.ZERO;
+        for (BigDecimal amount : taxIncludedAmounts) {
+            validateAmount(amount);
+            totalTaxIncluded = totalTaxIncluded.add(amount);
+        }
+
+        // 基于汇总含税金额计算总额
+        BigDecimal totalTaxExcluded = totalTaxIncluded
+                .divide(BigDecimal.ONE.add(taxRate), SCALE_DETAIL, ROUND_MODE)
+                .setScale(SCALE_FINAL, ROUND_MODE);
+        BigDecimal totalTax = totalTaxIncluded.subtract(totalTaxExcluded)
+                .setScale(SCALE_FINAL, ROUND_MODE);
+
+        return new BigDecimal[]{totalTaxExcluded, totalTax, totalTaxIncluded};
+    }
+
+    // 校验金额:非空、非负
+    private static void validateAmount(BigDecimal amount) {
+        if (amount == null) {
+            throw new IllegalArgumentException("金额不能为null");
+        }
+        if (amount.compareTo(BigDecimal.ZERO) < 0) {
+            throw new IllegalArgumentException("金额不能为负数:" + amount);
+        }
+    }
+
+    // 校验税率:非空、0-1之间
+    private static void validateRate(BigDecimal rate) {
+        if (rate == null) {
+            throw new IllegalArgumentException("税率不能为null");
+        }
+        if (rate.compareTo(BigDecimal.ZERO) < 0 || rate.compareTo(BigDecimal.ONE) > 0) {
+            throw new IllegalArgumentException("税率需在0-1之间(如13%传0.13):" + rate);
+        }
+    }
+
+    // 测试示例
+    public static void main(String[] args) {
+        // 测试场景1:已知含税金额113元,税率13%
+        BigDecimal taxIncluded = new BigDecimal("128900.56");
+        BigDecimal taxRate = new BigDecimal("0.06");
+        BigDecimal[] result1 = calculateFromTaxIncluded(taxIncluded, taxRate);
+        System.out.println("场景1:含税金额113元,税率13%");
+        System.out.println("不含税金额:" + result1[0] + " 元"); // 输出100.00
+        System.out.println("税额:" + result1[1] + " 元"); // 输出13.00
+        System.out.println("------------------------");
+
+        // 测试场景2:已知不含税金额100元,税率13%
+        BigDecimal taxExcluded = new BigDecimal("100.00");
+        BigDecimal[] result2 = calculateFromTaxExcluded(taxExcluded, taxRate);
+        System.out.println("场景2:不含税金额100元,税率13%");
+        System.out.println("含税金额:" + result2[0] + " 元"); // 输出113.00
+        System.out.println("税额:" + result2[1] + " 元"); // 输出13.00
+        System.out.println("------------------------");
+
+        // 测试场景3:批量金额汇总(113元 + 226元)
+        BigDecimal[] batchAmounts = {new BigDecimal("113.00"), new BigDecimal("226.00")};
+        BigDecimal[] result3 = calculateBatchTotal(batchAmounts, taxRate);
+        System.out.println("场景3:批量金额汇总(113+226),税率13%");
+        System.out.println("汇总不含税金额:" + result3[0] + " 元"); // 输出300.00
+        System.out.println("汇总税额:" + result3[1] + " 元"); // 输出39.00
+        System.out.println("汇总含税金额:" + result3[2] + " 元"); // 输出339.00
+    }
+}