Răsfoiți Sursa

微信发票

skyline 2 ani în urmă
părinte
comite
1e6beab556

+ 9 - 0
admin/src/main/resources/application-dev.yml

@@ -36,6 +36,15 @@ wechat:
     aesKey: #微信小程序消息服务器配置的EncodingAESKey
     msgDataFormat: JSON
 
+  fapiao:
+    baseInformation: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/merchant/base-information
+    taxCodes: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/merchant/tax-codes
+    fapiaoApplications: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications
+    notifyUrl: https://dev-cloud.kuaiyuman.cn/admin/finance/fapiaoNotify
+    devConfig: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/merchant/development-config
+    fapiaoFiles: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/%s/fapiao-files
+    queryFapiao: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/%s
+
 spring:
   datasource:
     druid: #以下是全局默认值,可以全局更改

+ 3 - 0
common/src/main/java/com/kym/common/config/WxFapiaoConfig.java

@@ -18,5 +18,8 @@ public class WxFapiaoConfig {
     private String taxCodes;
     private String fapiaoApplications;
     private String notifyUrl;
+    private String devConfig;
+    private String fapiaoFiles;
+    private String queryFapiao;
 
 }

+ 16 - 0
entity/src/main/java/com/kym/entity/miniapp/Invoice.java

@@ -26,7 +26,18 @@ import java.util.List;
 public class Invoice extends BaseEntity {
     public static final String TYPE_个人 = "INDIVIDUAL";
     public static final String TYPE_企业 = "ORGANIZATION";
+
+    public static final int STATUS_待开票 = 0;
+    public static final int STATUS_已开票 = 1;
+
+
     private static final long serialVersionUID = 1L;
+
+    /**
+     * 微信发票申请id
+     */
+    private String applyId;
+
     private Long userId;
 
     /**
@@ -110,6 +121,11 @@ public class Invoice extends BaseEntity {
      */
     private String biller;
 
+    /**
+     * 发票状态
+     */
+    private Integer status;
+
     /**
      * 备注
      */

+ 162 - 0
entity/src/main/java/com/kym/entity/wechat/FapiaoApplications.java

@@ -0,0 +1,162 @@
+package com.kym.entity.wechat;
+
+import java.util.List;
+
+/**
+ * @author skyline
+ * @description 查询电子发票
+ * @date 2023-10-07 16:02
+ */
+public class FapiaoApplications {
+    private int total_count;
+    private List<FapiaoEntity> fapiao_information;
+
+    class FapiaoEntity {
+        private String fapiao_id;
+        /**
+         * ISSUE_ACCEPTED: 开票申请已受理
+         * ISSUED: 发票已开具
+         * REVERSE_ACCEPTED: 冲红申请已受理
+         * REVERSED: 发票已冲红
+         */
+        private String status;
+
+        /**
+         * 蓝字发票信息,当发票状态不为ISSUE_ACCEPTED时存在
+         */
+        private FapiaoInfo blue_fapiao;
+        /**
+         * 红字发票信息,当发票状态为REVERSED时存在
+         */
+        private FapiaoInfo red_fapiao;
+
+        /**
+         * 电子发票卡券信息
+         */
+        private CardInfo card_information;
+
+        /**
+         * 总价税合计,所有发票行单行金额合计的累加,单位:分
+         */
+        private int total_amount;
+        /**
+         * 总税额,所有发票行单行税额的累加,单位:分
+         */
+        private int tax_amount;
+        /**
+         * 总金额,所有发票行单行金额的累加,单位:分
+         */
+        private int amount;
+        /**
+         * 销售方信息
+         */
+        private InvoiceBaseInfo.SellerInfo seller_information;
+        /**
+         * 购买方信息
+         */
+        private FaPiao.BuyerInformation buyer_information;
+        /**
+         * 附加信息
+         */
+        private InvoiceBaseInfo.ExtraInfo extra_information;
+
+        /**
+         * 发票行信息
+         */
+        private List<FapiaoItem> items;
+        /**
+         * 备注
+         */
+        private String remark;
+    }
+
+    class FapiaoInfo {
+        /**
+         * 发票代码
+         */
+        private String fapiao_code;
+        /**
+         * 发票号码
+         */
+        private String fapiao_number;
+        /**
+         * 校验码
+         */
+        private String check_code;
+        /**
+         * 发票票面密码区内容
+         */
+        private String password;
+        /**
+         * 开票时间,遵循RFC3339标准格式
+         * 2020-07-01T12:00:00+08:00
+         */
+        private String fapiao_time;
+    }
+
+    class CardInfo {
+        private String card_appid;
+        private String card_openid;
+        private String card_id;
+        private String card_code;
+        /**
+         * INSERT_ACCEPTED: 插卡申请已受理
+         * INSERTED: 已插入用户卡包
+         * DISCARD_ACCEPTED: 作废申请已受理
+         * DISCARDED: 发票卡券已作废
+         */
+        private String card_status;
+    }
+
+    class FapiaoItem {
+        /**
+         * 税局侧规定的货物或应税劳务、服务税收分类编码
+         */
+        private String tax_code;
+        /**
+         * 由商户自定义的货物或应税劳务、服务名称
+         */
+        private String goods_name;
+        /**
+         * 规格型号
+         */
+        private String specification;
+        /**
+         * 单位
+         */
+        private String unit;
+        /**
+         * 数量,单位为10^-8^,100000000表示数量为1
+         */
+        private int quantity;
+        /**
+         * 单价,单位为10^-6^分,100000000表示1元
+         */
+        private int unit_price;
+        /**
+         * 单行金额,单位:分
+         */
+        private int amount;
+        /**
+         * 单行金额和税费的和,单位:分
+         */
+        private int tax_amount;
+        /**
+         * 税率,单位为万分之一,如1300代表13%
+         */
+        private int tax_rate;
+        /**
+         * 税收优惠政策标识可选取值:
+         * NO_FAVORABLE: 无优惠
+         * OUTSIDE_VAT: 不征税
+         * VAT_EXEMPT: 免税
+         * NORMAL_ZERO_RATED: 普通零税率
+         * EXPORT_ZERO_RATED: 出口零税率
+         */
+        private int tax_prefer_mark;
+        /**
+         * 指定该发票行是否折扣行,折扣行一定是被折扣行的下一行
+         */
+        private boolean discount = false;
+    }
+}

+ 24 - 0
entity/src/main/java/com/kym/entity/wechat/FapiaoDownload.java

@@ -0,0 +1,24 @@
+package com.kym.entity.wechat;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author skyline
+ * @description 发票下载
+ * @date 2023-10-07 15:42
+ */
+@Data
+public class FapiaoDownload {
+
+    private List<FapiaoDownloadInfo> fapiao_download_info_list;
+
+    @Data
+    public class FapiaoDownloadInfo {
+        private String fapiao_id;
+        private String download_url;
+        private String status;
+    }
+
+}

+ 3 - 3
entity/src/main/java/com/kym/entity/wechat/InvoiceNotification.java

@@ -32,7 +32,7 @@ public class InvoiceNotification {
     /**
      * 发票状态
      */
-    enum FapiaoStatus {
+    public enum FapiaoStatus {
         ISSUE_ACCEPTED, ISSUED, REVERSE_ACCEPTED, REVERSED
     }
 
@@ -40,12 +40,12 @@ public class InvoiceNotification {
     /**
      * 发票状态-卡包状态
      */
-    enum CardStatus {
+    public enum CardStatus {
         INSERT_ACCEPTED, INSERTED, DISCARD_ACCEPTED, DISCARDED
     }
 
     @Data
-    class FapiaoInformation {
+    public class FapiaoInformation {
 
         /**
          * 商户发票单号,唯一标识一张发票

+ 6 - 1
mapper/src/main/resources/mappers/miniapp/InvoiceMapper.xml

@@ -4,6 +4,8 @@
 
     <!-- 通用查询映射结果 -->
     <resultMap id="BaseResultMap" type="com.kym.entity.miniapp.Invoice">
+        <result column="id" property="id" />
+        <result column="applyId" property="applyId" />
         <result column="user_id" property="userId" />
         <result column="order_details" property="orderDetails" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler" />
         <result column="total_power" property="totalPower" />
@@ -20,12 +22,15 @@
         <result column="invoice_amount" property="invoiceAmount" />
         <result column="tax_info" property="taxInfo" />
         <result column="biller" property="biller" />
+        <result column="status" property="status" />
         <result column="remark" property="remark" />
+        <result column="create_time" property="createTime" />
+        <result column="update_time" property="updateTime" />
     </resultMap>
 
     <!-- 通用查询结果列 -->
     <sql id="Base_Column_List">
-        user_id, order_details, total_power, total_money, elec_money, service_money, email, invoice_type, invoice_title, tax_id, address, bank_name, bank_account, invoice_amount, tax_info, biller, remark
+        id,apply_id,user_id, order_details, total_power, total_money, elec_money, service_money, email, invoice_type, invoice_title, tax_id, address, bank_name, bank_account, invoice_amount, tax_info, biller, status,remark,create_time,update_time
     </sql>
 
 </mapper>

+ 4 - 0
service/src/main/java/com/kym/service/wechat/WxPayService.java

@@ -29,4 +29,8 @@ public interface WxPayService {
     void wxNotify(HttpServletRequest request) throws IOException;
 
     InvoiceBaseInfo baseInformation();
+
+    void applyInvoice(String invoiceId);
+
+    void wxInvoiceNotify(HttpServletRequest request);
 }

+ 74 - 10
service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

@@ -13,11 +13,10 @@ import com.kym.common.config.WxPayConfig;
 import com.kym.common.constant.ResponseEnum;
 import com.kym.common.exception.BusinessException;
 import com.kym.common.utils.CommUtil;
-import com.kym.common.utils.HttpUtil;
 import com.kym.common.utils.LambadaTools;
 import com.kym.common.utils.OrderUtils;
-import com.kym.entity.miniapp.*;
 import com.kym.entity.miniapp.Account;
+import com.kym.entity.miniapp.*;
 import com.kym.entity.wechat.*;
 import com.kym.service.miniapp.*;
 import com.kym.service.wechat.WxPayService;
@@ -54,10 +53,8 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Stream;
 
 
 /**
@@ -557,9 +554,9 @@ public class WxPayServiceImpl implements WxPayService {
     /**
      * 申请发票
      */
-
+    @Override
     public void applyInvoice(String invoiceId) {
-        var invoice = invoiceService.lambdaQuery().eq(Invoice::getId,invoiceId).one();
+        var invoice = invoiceService.lambdaQuery().eq(Invoice::getId, invoiceId).one();
         // 电费 税务商品编码:1100101020000000000
         var elecMoney = invoice.getOrderDetails().stream().mapToInt(InvoiceOrderDetail::getElecMoney).sum();
         var elecMoneyItem = new FaPiao.FaPiaoInfomation.IssueItem().setTotalAmount(elecMoney).setTaxCode("1100101020000000000");
@@ -574,6 +571,7 @@ public class WxPayServiceImpl implements WxPayService {
 
         // 发票申请单号
         var fapiaoApplyId = OrderUtils.getOrderNo();
+        invoiceService.lambdaUpdate().set(Invoice::getApplyId, fapiaoApplyId).eq(Invoice::getId, invoiceId).update();
 
         // 客户开票信息
         var buyerInformation = new FaPiao.BuyerInformation()
@@ -587,9 +585,10 @@ public class WxPayServiceImpl implements WxPayService {
 
         // 发票信息
         var fapiaoInformation = new FaPiao.FaPiaoInfomation();
+        // fapiao_id为invoice.id
         fapiaoInformation.setFapiao_id(String.valueOf(invoice.getId()))
                 .setTotal_amount(invoice.getInvoiceAmount())
-                .setItems(List.of(elecMoneyItem,serviceMoneyItem));
+                .setItems(List.of(elecMoneyItem, serviceMoneyItem));
 
         // 请求参数
         var fapiao = new FaPiao()
@@ -599,7 +598,7 @@ public class WxPayServiceImpl implements WxPayService {
                 .setFapiao_information(List.of(fapiaoInformation));
 
         var requestBody = new JsonRequestBody.Builder().body(JSONObject.toJSONString(fapiao)).build();
-        wxHttpClient.post(headers, fapiaoConfig.getFapiaoApplications(), requestBody,null);
+        wxHttpClient.post(headers, fapiaoConfig.getFapiaoApplications(), requestBody, null);
     }
 
     /**
@@ -608,13 +607,20 @@ public class WxPayServiceImpl implements WxPayService {
      * @param request
      * @return
      */
-    void wxInvoiceNotify(HttpServletRequest request) {
+    @Override
+    public void wxInvoiceNotify(HttpServletRequest request) {
         var notifyRes = handleWxNotify(request);
         try {
             InvoiceNotification invoiceNotification = ((NotificationParser) notifyRes[1]).parse((RequestParam) notifyRes[0], InvoiceNotification.class);
             LOGGER.info("微信开具发票结果通知回调{}:验签解密完毕,数据:\n{}", notifyRes[2], invoiceNotification);
-            // TODO: 2023-09-17 业务逻辑
+            // 业务逻辑
+            if (invoiceNotification.getFapiaoInformation().get(0).getFapiaoStatus().equals(InvoiceNotification.FapiaoStatus.ISSUED)) {
+                // 更新发票状态
+                invoiceService.lambdaUpdate().set(Invoice::getStatus, Invoice.STATUS_已开票).eq(Invoice::getApplyId, invoiceNotification.getFapiaoApplyId()).update();
 
+            } else {
+                LOGGER.error("微信开具发票失败:{}", invoiceNotification);
+            }
         } catch (ValidationException e) {
             // 签名验证失败,返回 401 UNAUTHORIZED 状态码
             LOGGER.error("微信开具发票结果通知验签失败", e);
@@ -622,5 +628,63 @@ public class WxPayServiceImpl implements WxPayService {
         }
     }
 
+    /**
+     * 微信电子发票配置开发选项,例如回调地址、全部账单展示开发票入口开关
+     */
+    void devConfig() {
+        var headers = new HttpHeaders();
+        headers.addHeader("Accept", "application/json");
+        headers.addHeader("Content-Type", "application/json");
+
+        var params = """
+                {
+                  "callback_url" : %s,
+                  "show_fapiao_cell" : false
+                }
+                """.stripIndent().formatted(fapiaoConfig.getNotifyUrl());
+        var requestBody = new JsonRequestBody.Builder().body(params).build();
+        var res = wxHttpClient.patch(headers, fapiaoConfig.getDevConfig(), requestBody, JSONObject.class);
+        LOGGER.info("微信电子发票开发设置:{}", res.toString());
+        // TODO: 2023-10-07 业务逻辑
+    }
+
+    // TODO: 2023-10-07 下载发票
+    void downloadInvoice(String invoiceId) {
+        var invoice = invoiceService.getById(invoiceId);
+        // TODO: 2023-10-07 查询发票下载信息,下载链接30s有效
+        var download = getFapiaoDownloadInfo(invoice.getApplyId());
+        var downloadInfo = download.getFapiao_download_info_list().get(0);
+        var params = "?token=%s&mchid=%s&openid=%s&invoice_code=%s&invoice_no=%s&fapiao_id=%s";
+        var inputStream = wxHttpClient.download(downloadInfo.getDownload_url().concat(params.formatted("", conf.getMchid(), "", "", "", invoice.getId())));
+
+    }
+
+    /**
+     * 获取发票下载信息
+     *
+     * @param applyId
+     * @return
+     */
+    private FapiaoDownload getFapiaoDownloadInfo(String applyId) {
+        var downloadUrl = fapiaoConfig.getFapiaoFiles().formatted(applyId);
+        var headers = new HttpHeaders();
+        headers.addHeader("Accept", "application/json");
+        var res = wxHttpClient.get(headers, downloadUrl, FapiaoDownload.class);
+        return res.getServiceResponse();
+    }
+
+    /**
+     * 查询电子发票
+     *
+     * @param applyId
+     */
+    private FapiaoApplications queryFapiao(String applyId) {
+        var headers = new HttpHeaders();
+        headers.addHeader("Accept", "application/json");
+        headers.addHeader("Content-Type", "application/json");
+        var res = wxHttpClient.get(headers, fapiaoConfig.getQueryFapiao().formatted(applyId), FapiaoApplications.class);
+        return res.getServiceResponse();
+    }
+
 
 }