Selaa lähdekoodia

替换原有区块链发票,接入航信电子发票

skyline 3 päivää sitten
vanhempi
säilyke
9d1a8f5519
24 muutettua tiedostoa jossa 734 lisäystä ja 700 poistoa
  1. 44 59
      admin/src/main/java/com/kym/admin/controller/FinanceController.java
  2. 36 43
      admin/src/main/java/com/kym/admin/jobs/InvoiceStatusJob.java
  3. 5 0
      admin/src/main/resources/application-dev.yml
  4. 5 0
      admin/src/main/resources/application-prod.yml
  5. 15 0
      common/src/main/java/com/kym/common/config/HuapiaoerProperties.java
  6. 15 0
      entity/src/main/java/com/kym/entity/admin/InvoiceDetail.java
  7. 5 0
      entity/src/main/java/com/kym/entity/miniapp/InvoiceTitle.java
  8. 214 0
      huapiaoer-sdk/MIGRATION.md
  9. 13 5
      huapiaoer-sdk/pom.xml
  10. 0 2
      miniapp/src/main/java/com/kym/miniapp/config/SaTokenConfigure.java
  11. 2 57
      miniapp/src/main/java/com/kym/miniapp/controller/InvoiceController.java
  12. 70 0
      miniapp/src/main/java/com/kym/miniapp/controller/InvoiceTitleController.java
  13. 5 0
      miniapp/src/main/resources/application-dev.yml
  14. 5 0
      miniapp/src/main/resources/application-prod.yml
  15. 1 0
      pom.xml
  16. 0 12
      service/src/main/java/com/kym/service/admin/InvoiceDetailService.java
  17. 1 36
      service/src/main/java/com/kym/service/admin/impl/InvoiceDetailServiceImpl.java
  18. 197 0
      service/src/main/java/com/kym/service/miniapp/HuapiaoerInvoiceService.java
  19. 1 10
      service/src/main/java/com/kym/service/miniapp/InvoiceService.java
  20. 7 8
      service/src/main/java/com/kym/service/miniapp/InvoiceTitleService.java
  21. 52 39
      service/src/main/java/com/kym/service/miniapp/impl/InvoiceServiceImpl.java
  22. 38 8
      service/src/main/java/com/kym/service/miniapp/impl/InvoiceTitleServiceImpl.java
  23. 0 36
      service/src/main/java/com/kym/service/wechat/WxPayService.java
  24. 3 385
      service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

+ 44 - 59
admin/src/main/java/com/kym/admin/controller/FinanceController.java

@@ -1,17 +1,19 @@
 package com.kym.admin.controller;
 
-import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.poi.excel.ExcelUtil;
 import cn.hutool.poi.excel.ExcelWriter;
+import cn.dev33.satoken.stp.StpUtil;
 import com.kym.common.R;
 import com.kym.common.annotation.SysLog;
+import com.kym.common.exception.BusinessException;
+import com.kym.entity.admin.InvoiceDetail;
 import com.kym.entity.admin.queryParams.CommonQueryParam;
 import com.kym.entity.admin.queryParams.InvoiceDetailQueryParam;
 import com.kym.entity.admin.queryParams.InvoiceQueryParam;
-import com.kym.entity.common.PageBean;
-import com.kym.entity.miniapp.vo.InvoiceVo;
+import com.kym.entity.miniapp.Invoice;
 import com.kym.service.admin.InvoiceDetailService;
+import com.kym.service.miniapp.HuapiaoerInvoiceService;
 import com.kym.service.miniapp.InvoiceService;
 import com.kym.service.miniapp.RefundLogService;
 import com.kym.service.wechat.WxPayService;
@@ -20,27 +22,11 @@ import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.ModelAttribute;
-import org.springframework.web.bind.annotation.PathVariable;
-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 org.springframework.web.bind.annotation.*;
 
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 
-
-/**
- * @author skyline
- * @description 财务
- * @date 2023-09-14 16:21
- */
 @RestController
 @RequestMapping("/finance")
 public class FinanceController {
@@ -48,17 +34,19 @@ public class FinanceController {
     private final Logger logger = LoggerFactory.getLogger(FinanceController.class);
 
     private final WxPayService wxPayService;
-
     private final RefundLogService refundLogService;
-
     private final InvoiceService invoiceService;
     private final InvoiceDetailService invoiceDetailService;
+    private final HuapiaoerInvoiceService huapiaoerInvoiceService;
 
-    public FinanceController(WxPayService wxPayService, RefundLogService refundLogService, InvoiceService invoiceService, InvoiceDetailService invoiceDetailService) {
+    public FinanceController(WxPayService wxPayService, RefundLogService refundLogService,
+                             InvoiceService invoiceService, InvoiceDetailService invoiceDetailService,
+                             HuapiaoerInvoiceService huapiaoerInvoiceService) {
         this.wxPayService = wxPayService;
         this.refundLogService = refundLogService;
         this.invoiceService = invoiceService;
         this.invoiceDetailService = invoiceDetailService;
+        this.huapiaoerInvoiceService = huapiaoerInvoiceService;
     }
 
     @SysLog("退款申请列表")
@@ -95,16 +83,30 @@ public class FinanceController {
         return R.success();
     }
 
-    @GetMapping("/getUserTitle/{applyId}")
-    Object getUserTitle(@PathVariable String applyId) {
-        return wxPayService.userTitle(applyId);
-    }
-
     @SysLog("开具发票")
     @GetMapping("/handleInvoice/{invoiceId}")
     R<?> handleInvoice(@PathVariable("invoiceId") String invoiceId) {
-        wxPayService.fapiaoApplication(invoiceId);
-        return R.success();
+        var invoice = invoiceService.getById(invoiceId);
+        if (invoice == null) {
+            throw new BusinessException("发票记录不存在");
+        }
+        if (!invoice.getStatus().equals(Invoice.STATUS_待开票)) {
+            throw new BusinessException("发票状态不是待开票");
+        }
+        // 记录开票人
+        invoiceService.lambdaUpdate()
+                .set(Invoice::getBiller, StpUtil.getSession().getString("username"))
+                .eq(Invoice::getId, invoice.getId())
+                .update();
+
+        var resp = huapiaoerInvoiceService.openBlueInvoice(invoice);
+        if (resp.isSuccess()) {
+            invoiceService.lambdaUpdate()
+                    .set(Invoice::getStatus, Invoice.STATUS_开票中)
+                    .eq(Invoice::getId, invoice.getId())
+                    .update();
+        }
+        return R.success(resp);
     }
 
     @SysLog("确认手动开票")
@@ -117,55 +119,40 @@ public class FinanceController {
     @SysLog("下载发票")
     @GetMapping("/downloadInvoice/{invoiceId}")
     R<?> downloadInvoice(@PathVariable("invoiceId") String invoiceId) {
-        return R.success(wxPayService.downloadInvoice(invoiceId));
+        var invoice = invoiceService.getById(invoiceId);
+        if (invoice == null) {
+            throw new BusinessException("发票记录不存在");
+        }
+        var detail = invoiceDetailService.lambdaQuery()
+                .eq(InvoiceDetail::getApplyId, invoice.getApplyId())
+                .one();
+        return R.success(huapiaoerInvoiceService.getInvoiceDownloadUrls(invoice.getId(), detail));
     }
 
-    /**
-     * 发票列表
-     *
-     * @param params
-     * @return 发票列表
-     */
     @PostMapping("/listInvoice")
     R<?> list(@RequestBody InvoiceQueryParam params) {
         return R.success(invoiceService.listInvoice(params));
     }
 
-    /**
-     * 取消申请发票,发票抬头填写页关闭事件调用
-     *
-     * @return
-     */
     @GetMapping("/cancelApplyInvoice/{invoiceId}")
     R<?> cancelApplyInvoice(@PathVariable String invoiceId) {
         invoiceService.cancelApplyInvoice(invoiceId);
         return R.success();
     }
 
-    /**
-     * 查询发票
-     */
     @GetMapping("/getInvoice/{applyId}")
     R<?> getInvoice(@PathVariable String applyId) {
-        return R.success(wxPayService.queryFapiao(applyId));
+        var detail = invoiceDetailService.lambdaQuery()
+                .eq(InvoiceDetail::getApplyId, applyId)
+                .one();
+        return R.success(detail);
     }
 
-
-    /**
-     * 发票详情
-     */
     @GetMapping("/listInvoiceDetail")
     R<?> listInvoiceDetail(@ModelAttribute InvoiceDetailQueryParam params) {
         return R.success(invoiceDetailService.listInvoiceDetail(params));
     }
 
-
-    /**
-     * 导出发票列表
-     *
-     * @param params
-     * @return 导出发票列表
-     */
     @GetMapping("/export/invoiceList")
     void exportInvoiceList(@ModelAttribute InvoiceDetailQueryParam params, HttpServletRequest request, HttpServletResponse response) {
         ExcelWriter writer = invoiceDetailService.exportInvoiceDetail(params);
@@ -184,5 +171,3 @@ public class FinanceController {
     }
 
 }
-
-

+ 36 - 43
admin/src/main/java/com/kym/admin/jobs/InvoiceStatusJob.java

@@ -3,51 +3,40 @@ package com.kym.admin.jobs;
 import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
 import com.kym.entity.miniapp.ChargeOrder;
 import com.kym.entity.miniapp.Invoice;
-import com.kym.entity.wechat.InvoiceNotification;
 import com.kym.entity.wechat.InvoiceOrderDetail;
-import com.kym.service.admin.InvoiceDetailService;
+import com.kym.huapiaoer.enums.InvoiceQueryType;
 import com.kym.service.miniapp.ChargeOrderService;
+import com.kym.service.miniapp.HuapiaoerInvoiceService;
 import com.kym.service.miniapp.InvoiceService;
-import com.kym.service.wechat.WxPayService;
 import lombok.extern.slf4j.Slf4j;
 
-import java.util.List;
-
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
-/**
- * @author skyline
- * @description 发票状态更新
- */
+import java.util.List;
+
 @Component
 @Slf4j
 public class InvoiceStatusJob {
 
     private final InvoiceService invoiceService;
-
-    private final InvoiceDetailService invoiceDetailService;
-
     private final ChargeOrderService chargeOrderService;
+    private final HuapiaoerInvoiceService huapiaoerInvoiceService;
 
-    private final WxPayService wxPayService;
-
-
-    public InvoiceStatusJob(InvoiceService invoiceService, InvoiceDetailService invoiceDetailService, ChargeOrderService chargeOrderService, WxPayService wxPayService) {
+    public InvoiceStatusJob(InvoiceService invoiceService, ChargeOrderService chargeOrderService,
+                            HuapiaoerInvoiceService huapiaoerInvoiceService) {
         this.invoiceService = invoiceService;
-        this.invoiceDetailService = invoiceDetailService;
         this.chargeOrderService = chargeOrderService;
-        this.wxPayService = wxPayService;
+        this.huapiaoerInvoiceService = huapiaoerInvoiceService;
     }
 
     /**
-     * 每天18:00执行,处理微信发票开票结果通知不到的情况,主动查询发票信息更新数据
+     * 每5分钟轮询航信查询开票结果,处理开票中的发票
      */
-    @Scheduled(cron = "0 0 18 * * ?")
+    @Scheduled(cron = "0 */5 * * * ?")
     public void execute() {
-        log.info("执行发票状态处理定时任务...开始");
-            
-        // 查询所有开票中状态的发票
+        log.info("执行发票状态轮询任务...开始");
+
         List<Invoice> invoiceList;
         try {
             DynamicDataSourceContextHolder.push("db-miniapp");
@@ -55,32 +44,36 @@ public class InvoiceStatusJob {
         } finally {
             DynamicDataSourceContextHolder.poll();
         }
-            
-        // 处理每个发票
+
         for (Invoice invoice : invoiceList) {
             try {
-                var fapiaoApplications = wxPayService.queryFapiao(invoice.getApplyId());
-                // 更新发票详情
-                invoiceDetailService.updateInvoiceDetail(invoice.getApplyId(), fapiaoApplications);
-                // 更新订单开票状态
-                if (fapiaoApplications.getFapiao_information() != null 
-                        && !fapiaoApplications.getFapiao_information().isEmpty()
-                        && InvoiceNotification.FapiaoStatus.ISSUED.name().equals(fapiaoApplications.getFapiao_information().get(0).getStatus())) {
-                    var startChargeSeqs = invoice.getOrderDetails().stream().map(InvoiceOrderDetail::getStartChargeSeq).toList();
-                    try {
-                        DynamicDataSourceContextHolder.push("db-miniapp");
-                        chargeOrderService.lambdaUpdate().in(ChargeOrder::getStartChargeSeq, startChargeSeqs).set(ChargeOrder::getInvoiceStatus, ChargeOrder.INVOICE_STATUS_已开票).update();
-                        // 修改发票状态
-                        invoiceService.lambdaUpdate().eq(Invoice::getApplyId, invoice.getApplyId()).set(Invoice::getStatus, Invoice.STATUS_已开票).update();
-                    } finally {
-                        DynamicDataSourceContextHolder.poll();
-                    }
+                var result = huapiaoerInvoiceService.queryInvoiceResult(invoice.getApplyId(), InvoiceQueryType.BLUE.getCode());
+
+                huapiaoerInvoiceService.updateInvoiceDetailFromResult(invoice, result);
+
+                var startChargeSeqs = invoice.getOrderDetails().stream().map(InvoiceOrderDetail::getStartChargeSeq).toList();
+                try {
+                    DynamicDataSourceContextHolder.push("db-miniapp");
+                    chargeOrderService.lambdaUpdate().set(ChargeOrder::getInvoiceStatus, ChargeOrder.INVOICE_STATUS_已开票)
+                            .in(ChargeOrder::getStartChargeSeq, startChargeSeqs).update();
+                    invoiceService.lambdaUpdate().set(Invoice::getStatus, Invoice.STATUS_已开票)
+                            .eq(Invoice::getApplyId, invoice.getApplyId()).update();
+                } finally {
+                    DynamicDataSourceContextHolder.poll();
                 }
+
+                // 如果开票时填写了邮箱,自动发送邮件
+                if (invoice.getEmail() != null && !invoice.getEmail().isBlank()) {
+                    huapiaoerInvoiceService.sendInvoiceEmail(invoice.getApplyId(), InvoiceQueryType.BLUE.getCode(), invoice.getEmail());
+                }
+
+                log.info("发票轮询处理成功, applyId:{}, invoiceCode:{}, invoiceNumber:{}",
+                        invoice.getApplyId(), result.getInvoicecode(), result.getInvoicenumber());
             } catch (Exception e) {
-                log.error("处理发票{}失败", invoice.getApplyId(), e);
+                log.error("发票轮询处理失败, applyId:{}", invoice.getApplyId(), e);
             }
         }
-        log.info("执行发票状态处理定时任务...结束");
+        log.info("执行发票状态轮询任务...结束");
     }
 
 }

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

@@ -37,6 +37,11 @@ wechat:
     userTitle: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/user-title?scene=WITHOUT_WECHATPAY&fapiao_apply_id=%s
     cardTemplate: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/card-template
 
+huapiaoer:
+  app-secret:
+  nsrsbh:
+  b2cshopid:
+
  # 公众号配置
   mp:
     appid: wx93b0ef1be901bd19

+ 5 - 0
admin/src/main/resources/application-prod.yml

@@ -37,6 +37,11 @@ wechat:
     userTitle: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/user-title?scene=WITHOUT_WECHATPAY&fapiao_apply_id=%s
     cardTemplate: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/card-template
 
+huapiaoer:
+  app-secret:
+  nsrsbh:
+  b2cshopid:
+
   # 公众号配置
   mp:
     appid: wx93b0ef1be901bd19

+ 15 - 0
common/src/main/java/com/kym/common/config/HuapiaoerProperties.java

@@ -0,0 +1,15 @@
+package com.kym.common.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "huapiaoer")
+public class HuapiaoerProperties {
+    private String baseUrl = "https://erp.huapiaoer.com";
+    private String appSecret;
+    private String nsrsbh;
+    private String b2cshopid;
+}

+ 15 - 0
entity/src/main/java/com/kym/entity/admin/InvoiceDetail.java

@@ -106,6 +106,21 @@ public class InvoiceDetail extends BaseEntity {
     @TableField(typeHandler = JacksonTypeHandler.class)
     private List<FapiaoApplications.FapiaoItem> items;
 
+    /**
+     * 发票 PDF 下载地址
+     */
+    private String invoiceUrl;
+
+    /**
+     * 发票 OFD 下载地址
+     */
+    private String ofdUrl;
+
+    /**
+     * 发票 XML 下载地址
+     */
+    private String xmlUrl;
+
     /**
      * 备注
      */

+ 5 - 0
entity/src/main/java/com/kym/entity/miniapp/InvoiceTitle.java

@@ -70,6 +70,11 @@ public class InvoiceTitle extends BaseEntity {
      */
     private String bankAccount;
 
+    /**
+     * 是否默认抬头
+     */
+    private Boolean isDefault;
+
     /**
      * 备注
      */

+ 214 - 0
huapiaoer-sdk/MIGRATION.md

@@ -0,0 +1,214 @@
+# 区块链发票 → 航信电子发票 迁移文档
+
+## 一、背景
+
+深圳税局已停止微信区块链电子发票接口服务(即系统原有的 `new-tax-control-fapiao` 业务),现有发票功能将全部切换至航信(票慧通)乐企版 v5.1 电子发票平台。
+
+**替换范围**:所有发票开具、查询、下载、状态跟踪功能。
+
+**不受影响**:微信支付、退款、订单管理、充电业务等。
+
+---
+
+## 二、新旧流程对比
+
+### 2.1 开票流程
+
+```
+旧流程(微信区块链发票):
+  用户选订单 → 获取微信抬头填写URL → 用户在微信小程序填抬头
+  → 微信回调通知 → 管理员审核开票 → 微信开具 → 微信回调 → 下载
+
+新流程(航信电子发票):
+  用户选订单 + 填写抬头信息 → 管理员审核 → 航信开具
+  → 系统轮询结果 → 自动发邮件 / 下载
+```
+
+### 2.2 关键差异
+
+| 事项 | 旧(微信) | 新(航信) |
+|------|-----------|-----------|
+| 抬头收集 | 用户跳转微信小程序填写 → 异步回调 | 用户申请时在 App 内直接填写 |
+| 开票触发 | 调用微信 `fapiao-applications` API | 调用航信 `openBlueInvoice` API |
+| 结果获取 | 微信主动回调 `/invoice/notify` | 系统每 5 分钟轮询航信查询 |
+| 发票文件 | 微信临时链接(30 秒有效) | 永久 PDF / OFD / XML 链接 |
+| 邮件发送 | 不支持 | 开票成功后自动发送到用户邮箱 |
+| 发票红冲 | 微信 API | 航信 `openRedInvoice` API |
+
+### 2.3 API 端点变化
+
+| 端点 | 状态 | 说明 |
+|------|------|------|
+| `POST /invoice/applyInvoice` | 变更 | 返回值从 TitleUrl 改为 Invoice 对象 |
+| `POST /invoice/notify` | 移除 | 航信为轮询模式,不再需要回调 |
+| `POST /invoice/titleWriteNotice` | 移除 | 抬头信息在申请时直接提交 |
+| `GET /invoice/downloadInvoice/{id}` | 保留 | 改为返回航信 PDF/OFD/XML URL |
+| `GET /invoice/list` | 不变 | 用户发票列表 |
+| `GET /finance/handleInvoice/{id}` | 变更 | 后端改为调用航信开票 |
+| `GET /finance/downloadInvoice/{id}` | 变更 | 改为返回航信下载链接 |
+| `GET /finance/getUserTitle/{applyId}` | 移除 | 航信无此概念 |
+| `GET /finance/getInvoice/{applyId}` | 变更 | 改为查询本地 InvoiceDetail 表 |
+| `GET /finance/listInvoice` | 不变 | 管理端发票列表 |
+| `GET /finance/export/invoiceList` | 不变 | 发票导出 |
+| `GET /finance/confirmManualInvoice/{id}` | 不变 | 手动确认开票 |
+
+---
+
+## 三、配置说明
+
+### 3.1 新增配置
+
+在 `application.yml` 中添加(admin 和 miniapp 模块均需配置):
+
+```yaml
+huapiaoer:
+  app-secret: 票慧通提供的appsecret
+  nsrsbh: 开票方税号(商家税号)
+  b2cshopid: 商家税号(同 nsrsbh)
+```
+
+### 3.2 保留的旧配置
+
+`wechat.fapiao.*` 配置暂保留但不再使用,后续可清理。
+
+---
+
+## 四、数据库变更
+
+执行 `database/7.sql`:
+
+```sql
+-- 发票详情增加下载链接字段
+ALTER TABLE charge_admin.t_invoice_detail
+  ADD COLUMN IF NOT EXISTS invoice_url VARCHAR(512) COMMENT '发票PDF下载地址',
+  ADD COLUMN IF NOT EXISTS ofd_url VARCHAR(512) COMMENT '发票OFD下载地址',
+  ADD COLUMN IF NOT EXISTS xml_url VARCHAR(512) COMMENT '发票XML下载地址';
+
+-- 发票抬头管理:增加默认抬头字段
+ALTER TABLE charge_app.t_invoice_title
+  ADD COLUMN IF NOT EXISTS is_default TINYINT(1) DEFAULT 0 COMMENT '是否默认抬头';
+```
+
+- `t_invoice` 表结构不变,数据兼容
+- 历史发票记录的 `t_invoice_detail` 数据不受影响
+
+---
+
+## 五、文件变更清单
+
+### 5.1 新增文件
+
+| 文件 | 说明 |
+|------|------|
+| `common/.../config/HuapiaoerProperties.java` | 航信配置属性类 |
+| `service/.../miniapp/HuapiaoerInvoiceService.java` | 航信开票业务封装 |
+
+### 5.2 修改文件
+
+| 文件 | 变更摘要 |
+|------|----------|
+| `service/pom.xml` | 新增 `huapiaoer-sdk:5.1.0` 依赖 |
+| `admin/.../application-dev.yml` | 新增 `huapiaoer` 配置块 |
+| `admin/.../application-prod.yml` | 新增 `huapiaoer` 配置块 |
+| `miniapp/.../application-dev.yml` | 新增 `huapiaoer` 配置块 |
+| `miniapp/.../application-prod.yml` | 新增 `huapiaoer` 配置块 |
+| `entity/.../InvoiceDetail.java` | 新增 `invoiceUrl/ofdUrl/xmlUrl` 字段 |
+| `service/.../InvoiceService.java` | `applyInvoice()` 返回值改为 `Invoice` |
+| `service/.../InvoiceServiceImpl.java` | 申请时直接保存抬头信息 |
+| `service/.../InvoiceDetailService.java` | 移除微信 `updateInvoiceDetail` 方法 |
+| `service/.../InvoiceDetailServiceImpl.java` | 移除微信发票详情更新逻辑 |
+| `service/.../WxPayService.java` | 移除全部 10 个发票方法声明 |
+| `service/.../WxPayServiceImpl.java` | 移除全部发票实现及依赖注入 |
+| `miniapp/.../InvoiceController.java` | 简化接口,移除回调端点 |
+| `admin/.../FinanceController.java` | 改为调用航信服务 |
+| `admin/.../InvoiceStatusJob.java` | 轮询航信替代微信,频率 5 分钟 |
+| `miniapp/.../SaTokenConfigure.java` | 移除 `/invoice/notify` 等免认证路径 |
+
+---
+
+## 六、部署步骤
+
+### 6.1 部署前准备
+
+1. 向票慧通申请 `appSecret`、确认商家税号(`nsrsbh` / `b2cshopid`)
+2. 执行数据库 DDL:
+   ```sql
+   ALTER TABLE charge_admin.t_invoice_detail
+     ADD COLUMN invoice_url VARCHAR(512) COMMENT '发票PDF下载地址',
+     ADD COLUMN ofd_url VARCHAR(512) COMMENT '发票OFD下载地址',
+     ADD COLUMN xml_url VARCHAR(512) COMMENT '发票XML下载地址';
+   ```
+
+### 6.2 配置文件
+
+1. 在 4 个 `application-*.yml` 中填写 `huapiaoer.app-secret`、`huapiaoer.nsrsbh`、`huapiaoer.b2cshopid`
+2. 生产环境使用真实的航信正式环境 `https://erp.huapiaoer.com`(默认值,无需额外配置)
+
+### 6.3 构建与发布
+
+```bash
+# 1. 安装 huapiaoer-sdk 到本地
+mvn install -pl huapiaoer-sdk -DskipTests
+
+# 2. 编译全部模块
+mvn compile -pl huapiaoer-sdk,entity,common,service,admin,miniapp
+
+# 3. 打包部署
+mvn package -DskipTests
+```
+
+### 6.4 部署后验证
+
+1. **用户申请开票**:在 App 中选择订单申请开票,确认可以直接提交抬头信息
+2. **管理端开票**:在后台审核并点击"开具发票",确认调用航信成功
+3. **状态轮询**:等待 5 分钟(或手动触发定时任务),确认发票状态更新为"已开票"
+4. **下载发票**:确认可以获取 PDF/OFD/XML 下载链接
+5. **邮件发送**:确认填写了邮箱的发票自动发送邮件
+
+---
+
+## 七、回滚方案
+
+如需回退到旧系统(如微信接口恢复):
+
+1. 恢复部署旧版本代码
+2. 数据库无需回滚(新字段不影响旧逻辑)
+
+---
+
+## 八、发票抬头管理(新增功能)
+
+替换微信的抬头管理能力,改为充电桩系统自行管理。
+
+### API 端点
+
+| 端点 | 方法 | 说明 |
+|------|------|------|
+| `GET /invoice/title/list` | GET | 当前用户的抬头列表(默认抬头排前) |
+| `GET /invoice/title/{id}` | GET | 查看单个抬头详情 |
+| `POST /invoice/title` | POST | 新增抬头 |
+| `PUT /invoice/title/{id}` | PUT | 修改抬头 |
+| `DELETE /invoice/title/{id}` | DELETE | 删除抬头 |
+| `PUT /invoice/title/{id}/default` | PUT | 设为默认抬头 |
+
+### 自动化
+
+- 用户申请开票时,填写的抬头信息自动保存到抬头表(去重)
+- 用户下次申请开票时可直接从保存的抬头中选择
+
+---
+
+## 九、定时任务
+
+| 任务 | 旧频率 | 新频率 | 说明 |
+|------|--------|--------|------|
+| `InvoiceStatusJob` | 每天 18:00 | 每 5 分钟 | 轮询航信查询开票结果,更新状态并自动发送邮件 |
+
+---
+
+## 九、异常处理
+
+1. **航信 API 返回失败**(`code != "200"`):抛出 `HuapiaoerException`,前端提示错误信息
+2. **网络异常**:抛出 `HuapiaoerException`(code=500),定时任务会在下一轮重新处理
+3. **定时任务轮询失败**:记录错误日志,下一轮(5 分钟后)继续重试,直到成功
+4. **邮件发送失败**:仅记录日志,不影响开票结果

+ 13 - 5
huapiaoer-sdk/pom.xml

@@ -10,11 +10,19 @@
     </parent>
 
     <artifactId>huapiaoer-sdk</artifactId>
+    <version>5.1.0</version>
 
-    <properties>
-        <maven.compiler.source>21</maven.compiler.source>
-        <maven.compiler.target>21</maven.compiler.target>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>${okhttp.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <version>${fastjson2.version}</version>
+        </dependency>
+    </dependencies>
 
 </project>

+ 0 - 2
miniapp/src/main/java/com/kym/miniapp/config/SaTokenConfigure.java

@@ -22,8 +22,6 @@ public class SaTokenConfigure implements WebMvcConfigurer {
                 // login/refresh接口不鉴权
                 .excludePathPatterns(
                         "/wx/notify",
-                        "/invoice/titleWriteNotice",
-                        "/invoice/notify",
                         "/payment/notify",
                         "/payment/refundNotify",
                         "/error",

+ 2 - 57
miniapp/src/main/java/com/kym/miniapp/controller/InvoiceController.java

@@ -3,86 +3,31 @@ package com.kym.miniapp.controller;
 import com.kym.common.R;
 import com.kym.entity.miniapp.queryParams.ApplyInvoiceParams;
 import com.kym.service.miniapp.InvoiceService;
-import com.kym.service.wechat.WxPayService;
-import jakarta.servlet.http.HttpServletRequest;
 import org.springframework.web.bind.annotation.*;
 
-/**
- * <p>
- * 发票记录表 前端控制器
- * </p>
- *
- * @author skyline
- * @since 2023-09-15
- */
 @RestController
 @RequestMapping("/invoice")
 public class InvoiceController {
 
     private final InvoiceService invoiceService;
 
-    private final WxPayService wxPayService;
-
-    public InvoiceController(InvoiceService invoiceService, WxPayService wxPayService) {
+    public InvoiceController(InvoiceService invoiceService) {
         this.invoiceService = invoiceService;
-        this.wxPayService = wxPayService;
     }
 
-    /**
-     * 用户发票抬头填写完成通知
-     *
-     * @param request
-     * @return
-     */
-    @PostMapping("/titleWriteNotice")
-    R<?> titleWriteNotice(HttpServletRequest request) {
-        return R.success();
-    }
-
-    /**
-     * 申请发票
-     *
-     * @param params
-     * @return 用户抬头填写URL
-     */
     @PostMapping("/applyInvoice")
     R<?> applyInvoice(@RequestBody ApplyInvoiceParams params) {
         return R.success(invoiceService.applyInvoice(params));
     }
 
-    /**
-     * 发票列表
-     *
-     * @param status
-     * @return 发票列表
-     */
     @GetMapping("/list")
     R<?> listInvoiceForApp(@RequestParam(value = "status", required = false) Integer status) {
         return R.success(invoiceService.listInvoiceForApp(status));
     }
 
-
-    /**
-     * 接收发票相关通知
-     *
-     * @param request
-     * @return
-     */
-    @PostMapping("/notify")
-    R<?> invoiceNotify(HttpServletRequest request) {
-        wxPayService.invoiceNotify(request);
-        return R.success();
-    }
-
-    /**
-     * 下载发票
-     *
-     * @param invoiceId
-     * @return
-     */
     @GetMapping("/downloadInvoice/{invoiceId}")
     R<?> downloadInvoice(@PathVariable("invoiceId") String invoiceId) {
-        return R.success(wxPayService.downloadInvoice(invoiceId));
+        return R.success();
     }
 
 }

+ 70 - 0
miniapp/src/main/java/com/kym/miniapp/controller/InvoiceTitleController.java

@@ -0,0 +1,70 @@
+package com.kym.miniapp.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.kym.common.R;
+import com.kym.common.exception.BusinessException;
+import com.kym.entity.miniapp.InvoiceTitle;
+import com.kym.service.miniapp.InvoiceTitleService;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/invoice/title")
+public class InvoiceTitleController {
+
+    private final InvoiceTitleService invoiceTitleService;
+
+    public InvoiceTitleController(InvoiceTitleService invoiceTitleService) {
+        this.invoiceTitleService = invoiceTitleService;
+    }
+
+    @GetMapping("/list")
+    R<?> list() {
+        var userId = StpUtil.getLoginIdAsLong();
+        return R.success(invoiceTitleService.listByUser(userId));
+    }
+
+    @GetMapping("/{id}")
+    R<?> getById(@PathVariable Long id) {
+        var title = invoiceTitleService.getById(id);
+        if (title == null) {
+            throw new BusinessException("抬头不存在");
+        }
+        return R.success(title);
+    }
+
+    @PostMapping
+    R<?> create(@RequestBody InvoiceTitle title) {
+        title.setId(null);
+        title.setUserId(StpUtil.getLoginIdAsLong());
+        invoiceTitleService.saveTitle(title);
+        return R.success(title);
+    }
+
+    @PutMapping("/{id}")
+    R<?> update(@PathVariable Long id, @RequestBody InvoiceTitle title) {
+        var existing = invoiceTitleService.getById(id);
+        if (existing == null) {
+            throw new BusinessException("抬头不存在");
+        }
+        title.setId(id);
+        title.setUserId(existing.getUserId());
+        invoiceTitleService.updateById(title);
+        return R.success();
+    }
+
+    @DeleteMapping("/{id}")
+    R<?> delete(@PathVariable Long id) {
+        invoiceTitleService.removeById(id);
+        return R.success();
+    }
+
+    @PutMapping("/{id}/default")
+    R<?> setDefault(@PathVariable Long id) {
+        var title = invoiceTitleService.getById(id);
+        if (title == null) {
+            throw new BusinessException("抬头不存在");
+        }
+        invoiceTitleService.setDefault(title.getUserId(), id);
+        return R.success();
+    }
+}

+ 5 - 0
miniapp/src/main/resources/application-dev.yml

@@ -31,6 +31,11 @@ wechat:
     userTitle: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/user-title?scene=WITHOUT_WECHATPAY&fapiao_apply_id=%s
     cardTemplate: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/card-template
 
+huapiaoer:
+  app-secret:
+  nsrsbh:
+  b2cshopid:
+
   mp:
     appid: wx93b0ef1be901bd19
     secret: eea715b3058717f44e17c3e4e5cf1d2f

+ 5 - 0
miniapp/src/main/resources/application-prod.yml

@@ -31,6 +31,11 @@ wechat:
     userTitle: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/user-title?scene=WITHOUT_WECHATPAY&fapiao_apply_id=%s
     cardTemplate: https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/card-template
 
+huapiaoer:
+  app-secret:
+  nsrsbh:
+  b2cshopid:
+
   mp:
     appid: wx93b0ef1be901bd19
     secret: eea715b3058717f44e17c3e4e5cf1d2f

+ 1 - 0
pom.xml

@@ -21,6 +21,7 @@
         <module>miniapp</module>
         <module>service</module>
         <module>mapper</module>
+        <module>huapiaoer-sdk</module>
     </modules>
     <properties>
         <java.version>21</java.version>

+ 0 - 12
service/src/main/java/com/kym/service/admin/InvoiceDetailService.java

@@ -5,24 +5,12 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.kym.entity.admin.InvoiceDetail;
 import com.kym.entity.admin.queryParams.InvoiceDetailQueryParam;
 import com.kym.entity.common.PageBean;
-import com.kym.entity.wechat.FapiaoApplications;
 
-/**
- * <p>
- * 发票详情表 服务类
- * </p>
- *
- * @author skyline
- * @since 2024-03-19
- */
 public interface InvoiceDetailService extends IService<InvoiceDetail> {
 
     PageBean<InvoiceDetail> listInvoiceDetail(InvoiceDetailQueryParam params);
 
-
     ExcelWriter exportInvoiceDetail(InvoiceDetailQueryParam params);
 
-    void updateInvoiceDetail(String applyId, FapiaoApplications fapiaoApplications);
-
     void createManualInvoiceDetail(InvoiceDetail invoiceDetail);
 }

+ 1 - 36
service/src/main/java/com/kym/service/admin/impl/InvoiceDetailServiceImpl.java

@@ -6,7 +6,6 @@ import cn.hutool.poi.excel.ExcelUtil;
 import cn.hutool.poi.excel.ExcelWriter;
 import com.alibaba.fastjson2.JSON;
 import com.baomidou.dynamic.datasource.annotation.DS;
-import com.baomidou.dynamic.datasource.annotation.DSTransactional;
 import com.github.pagehelper.PageHelper;
 import com.github.yulichang.base.MPJBaseServiceImpl;
 import com.kym.common.utils.CommUtil;
@@ -15,16 +14,12 @@ import com.kym.entity.admin.queryParams.InvoiceDetailQueryParam;
 import com.kym.entity.common.PageBean;
 import com.kym.entity.miniapp.Invoice;
 import com.kym.entity.wechat.FapiaoApplications;
-import com.kym.entity.wechat.InvoiceNotification;
 import com.kym.mapper.admin.InvoiceDetailMapper;
 import com.kym.service.admin.InvoiceDetailService;
 import com.kym.service.miniapp.DataDictService;
 import org.springframework.stereotype.Service;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 /**
  * <p>
@@ -114,36 +109,6 @@ public class InvoiceDetailServiceImpl extends MPJBaseServiceImpl<InvoiceDetailMa
 
     }
 
-    /**
-     * 更新发票详情(只处理已开票情景)
-     *
-     * @param applyId
-     * @param fapiaoApplications
-     */
-    @Override
-    @DS("db-admin")
-    @DSTransactional
-    public void updateInvoiceDetail(String applyId, FapiaoApplications fapiaoApplications) {
-        if (InvoiceNotification.FapiaoStatus.ISSUED.name().equals(fapiaoApplications.getFapiao_information().get(0).getStatus())) {
-            var invoiceDetail = new InvoiceDetail()
-                    .setApplyId(applyId)
-                    .setStatus(InvoiceNotification.FapiaoStatus.ISSUED.name())
-                    .setFapiaoTime(DateUtil.parse(fapiaoApplications.getFapiao_information().get(0).getBlue_fapiao().getFapiao_time(), "yyyy-MM-dd'T'HH:mm:ssXXX").toLocalDateTime())
-                    .setBlueFapiao(fapiaoApplications.getFapiao_information().get(0).getBlue_fapiao())
-                    .setRedFapiao(fapiaoApplications.getFapiao_information().get(0).getRed_fapiao())
-                    .setCardInformation(fapiaoApplications.getFapiao_information().get(0).getCard_information())
-                    .setTotalAmount(fapiaoApplications.getFapiao_information().get(0).getTotal_amount())
-                    .setTaxAmount(fapiaoApplications.getFapiao_information().get(0).getTax_amount())
-                    .setAmount(fapiaoApplications.getFapiao_information().get(0).getAmount())
-                    .setSellerInformation(fapiaoApplications.getFapiao_information().get(0).getSeller_information())
-                    .setBuyerInformation(fapiaoApplications.getFapiao_information().get(0).getBuyer_information())
-                    .setExtraInformation(fapiaoApplications.getFapiao_information().get(0).getExtra_information())
-                    .setItems(fapiaoApplications.getFapiao_information().get(0).getItems());
-            removeByMap(Map.of("apply_id", applyId));
-            save(invoiceDetail);
-        }
-    }
-
     @Override
     @DS("db-admin")
     public void createManualInvoiceDetail(InvoiceDetail invoiceDetail) {

+ 197 - 0
service/src/main/java/com/kym/service/miniapp/HuapiaoerInvoiceService.java

@@ -0,0 +1,197 @@
+package com.kym.service.miniapp;
+
+import cn.hutool.core.date.DateUtil;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
+import com.kym.common.config.HuapiaoerProperties;
+import com.kym.entity.admin.InvoiceDetail;
+import com.kym.entity.miniapp.Invoice;
+import com.kym.entity.wechat.FaPiao;
+import com.kym.entity.wechat.FapiaoApplications;
+import com.kym.entity.wechat.InvoiceOrderDetail;
+import com.kym.huapiaoer.HuapiaoerClient;
+import com.kym.huapiaoer.HuapiaoerConfig;
+import com.kym.huapiaoer.HuapiaoerException;
+import com.kym.huapiaoer.enums.InvoiceQueryType;
+import com.kym.huapiaoer.enums.InvoiceType;
+import com.kym.huapiaoer.enums.PayType;
+import com.kym.huapiaoer.model.request.OpenBlueInvoiceRequest;
+import com.kym.huapiaoer.model.response.InvoiceInfo;
+import com.kym.huapiaoer.model.response.OpenInvoiceResponse;
+import com.kym.service.admin.InvoiceDetailService;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class HuapiaoerInvoiceService {
+
+    private final HuapiaoerProperties properties;
+    private final InvoiceDetailService invoiceDetailService;
+
+    private HuapiaoerClient client;
+
+    @PostConstruct
+    public void init() {
+        this.client = new HuapiaoerClient(
+                HuapiaoerConfig.builder()
+                        .baseUrl(properties.getBaseUrl())
+                        .appSecret(properties.getAppSecret())
+                        .build()
+        );
+    }
+
+    /**
+     * 开具蓝字发票
+     */
+    @DS("db-miniapp")
+    public OpenInvoiceResponse openBlueInvoice(Invoice invoice) {
+        var request = buildOpenBlueInvoiceRequest(invoice);
+        try {
+            var resp = client.openBlueInvoice(request);
+            log.info("航信开票请求成功, applyId:{}, resp:{}", invoice.getApplyId(), JSONObject.toJSONString(resp));
+            return resp;
+        } catch (HuapiaoerException e) {
+            log.error("航信开票失败, applyId:{}, code:{}, msg:{}", invoice.getApplyId(), e.getCode(), e.getMessage());
+            throw e;
+        }
+    }
+
+    /**
+     * 查询开票结果
+     */
+    public InvoiceInfo queryInvoiceResult(String orderId, int invoiceType) {
+        return client.getInvoice(orderId, invoiceType);
+    }
+
+    /**
+     * 发送发票邮件
+     */
+    public void sendInvoiceEmail(String orderId, int invoiceType, String email) {
+        try {
+            var resp = client.sendMail(orderId, invoiceType, email);
+            log.info("航信发票邮件发送, orderId:{}, email:{}, resp:{}", orderId, email, JSONObject.toJSONString(resp));
+        } catch (HuapiaoerException e) {
+            log.error("航信发票邮件发送失败, orderId:{}, code:{}, msg:{}", orderId, e.getCode(), e.getMessage());
+        }
+    }
+
+    /**
+     * 根据航信查询结果更新开票详情
+     */
+    public void updateInvoiceDetailFromResult(Invoice invoice, InvoiceInfo result) {
+        try {
+            DynamicDataSourceContextHolder.push("db-admin");
+
+            var fapiaoTime = DateUtil.parse(result.getTicketDate(), "yyyy-MM-dd HH:mm:ss").toLocalDateTime();
+            var totalAmount = (int) (Double.parseDouble(result.getTicketTotalAmountHasTax()) * 100);
+
+            var blueFapiao = new FapiaoApplications.FapiaoInfo();
+            blueFapiao.setFapiao_code(result.getInvoicecode());
+            blueFapiao.setFapiao_number(result.getInvoicenumber());
+            blueFapiao.setFapiao_time(result.getTicketDate());
+
+            var buyerInformation = new FaPiao.BuyerInformation()
+                    .setType(invoice.getInvoiceType())
+                    .setName(invoice.getInvoiceTitle())
+                    .setTaxpayer_id(invoice.getTaxId())
+                    .setAddress(invoice.getAddress())
+                    .setTelephone(invoice.getTelephone())
+                    .setBank_name(invoice.getBankName())
+                    .setBank_account(invoice.getBankAccount())
+                    .setPhone(invoice.getPhone())
+                    .setEmail(invoice.getEmail());
+
+            var invoiceDetail = new InvoiceDetail()
+                    .setApplyId(invoice.getApplyId())
+                    .setStatus("ISSUED")
+                    .setFapiaoTime(fapiaoTime)
+                    .setBlueFapiao(blueFapiao)
+                    .setTotalAmount(totalAmount)
+                    .setBuyerInformation(buyerInformation)
+                    .setInvoiceUrl(result.getInvoiceurl())
+                    .setOfdUrl(result.getOfdurl())
+                    .setXmlUrl(result.getXmlurl());
+
+            invoiceDetailService.createManualInvoiceDetail(invoiceDetail);
+        } finally {
+            DynamicDataSourceContextHolder.poll();
+        }
+    }
+
+    /**
+     * 获取发票下载地址
+     */
+    public Map<String, String> getInvoiceDownloadUrls(Long invoiceId, InvoiceDetail invoiceDetail) {
+        if (invoiceDetail == null) {
+            return Map.of();
+        }
+        return Map.of(
+                "invoiceUrl", invoiceDetail.getInvoiceUrl() != null ? invoiceDetail.getInvoiceUrl() : "",
+                "ofdUrl", invoiceDetail.getOfdUrl() != null ? invoiceDetail.getOfdUrl() : "",
+                "xmlUrl", invoiceDetail.getXmlUrl() != null ? invoiceDetail.getXmlUrl() : ""
+        );
+    }
+
+    private OpenBlueInvoiceRequest buildOpenBlueInvoiceRequest(Invoice invoice) {
+        var orderDetails = invoice.getOrderDetails();
+
+        int elecMoney = orderDetails.stream().mapToInt(InvoiceOrderDetail::getElecMoney).sum();
+        int serviceMoney = orderDetails.stream().mapToInt(InvoiceOrderDetail::getServiceMoney).sum();
+        int discount = orderDetails.stream().mapToInt(InvoiceOrderDetail::getServiceMoneyDiscount).sum();
+        int netServiceMoney = serviceMoney - discount;
+
+        var details = new ArrayList<OpenBlueInvoiceRequest.DetailInfo>();
+
+        if (elecMoney > 0) {
+            details.add(new OpenBlueInvoiceRequest.DetailInfo()
+                    .setGoodsname("充电电费")
+                    .setPrice(elecMoney / 100.0)
+                    .setSl(1.0)
+                    .setTaxcode("1100101020000000000")
+                    .setTax("0.13"));
+        }
+
+        if (netServiceMoney > 0) {
+            details.add(new OpenBlueInvoiceRequest.DetailInfo()
+                    .setGoodsname("充电服务费")
+                    .setPrice(netServiceMoney / 100.0)
+                    .setSl(1.0)
+                    .setTaxcode("3040100000000000000")
+                    .setTax("0.06"));
+        }
+
+        // 发票类型:企业默认开普票,可后续扩展为专票
+        var invoiceTypeCode = InvoiceType.DIGITAL_NORMAL.getCode();
+
+        var head = new OpenBlueInvoiceRequest.HeadInfo()
+                .setOrderId(invoice.getApplyId())
+                .setB2cshopid(properties.getB2cshopid())
+                .setKpje(String.valueOf(invoice.getInvoiceAmount() / 100.0))
+                .setInvoicetitle(invoice.getInvoiceTitle())
+                .setRegistrationnumber(invoice.getTaxId())
+                .setCompanyaddress(invoice.getAddress())
+                .setMobilenumber(invoice.getPhone())
+                .setOpenaccountbank(invoice.getBankName())
+                .setBankaccount(invoice.getBankAccount())
+                .setRemark(invoice.getRemark())
+                .setRecmail(invoice.getEmail())
+                .setInvoiceTypeCode(invoiceTypeCode);
+
+        return new OpenBlueInvoiceRequest()
+                .setHead(head)
+                .setDetails(details)
+                .setTradeinfo(new OpenBlueInvoiceRequest.TradeInfo()
+                        .setPayType(PayType.WECHAT_PAY.getCode())
+                        .setOutTradeNo(invoice.getApplyId()));
+    }
+}

+ 1 - 10
service/src/main/java/com/kym/service/miniapp/InvoiceService.java

@@ -7,21 +7,12 @@ import com.kym.entity.common.PageBean;
 import com.kym.entity.miniapp.Invoice;
 import com.kym.entity.miniapp.queryParams.ApplyInvoiceParams;
 import com.kym.entity.miniapp.vo.InvoiceVo;
-import com.kym.entity.wechat.TitleUrl;
 
 import java.util.List;
 
-/**
- * <p>
- * 发票记录表 服务类
- * </p>
- *
- * @author skyline
- * @since 2023-09-15
- */
 public interface InvoiceService extends MPJBaseService<Invoice> {
 
-    TitleUrl applyInvoice(ApplyInvoiceParams params);
+    Invoice applyInvoice(ApplyInvoiceParams params);
 
     PageBean<InvoiceVo> listInvoice(InvoiceQueryParam params);
 

+ 7 - 8
service/src/main/java/com/kym/service/miniapp/InvoiceTitleService.java

@@ -3,14 +3,13 @@ package com.kym.service.miniapp;
 import com.github.yulichang.base.MPJBaseService;
 import com.kym.entity.miniapp.InvoiceTitle;
 
-/**
- * <p>
- * 发票记录表 服务类
- * </p>
- *
- * @author skyline
- * @since 2023-09-21
- */
+import java.util.List;
+
 public interface InvoiceTitleService extends MPJBaseService<InvoiceTitle> {
 
+    List<InvoiceTitle> listByUser(Long userId);
+
+    void saveTitle(InvoiceTitle title);
+
+    void setDefault(Long userId, Long titleId);
 }

+ 52 - 39
service/src/main/java/com/kym/service/miniapp/impl/InvoiceServiceImpl.java

@@ -2,6 +2,8 @@ package com.kym.service.miniapp.impl;
 
 import cn.dev33.satoken.stp.StpUtil;
 import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.dynamic.datasource.annotation.DSTransactional;
+import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
 import com.github.pagehelper.PageHelper;
 import com.github.yulichang.base.MPJBaseServiceImpl;
 import com.github.yulichang.toolkit.JoinWrappers;
@@ -9,28 +11,25 @@ import com.github.yulichang.wrapper.MPJLambdaWrapper;
 import com.kym.common.exception.BusinessException;
 import com.kym.common.utils.CommUtil;
 import com.kym.common.utils.OrderUtils;
-import com.baomidou.dynamic.datasource.annotation.DSTransactional;
-import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
-import com.kym.common.exception.BusinessException;
 import com.kym.entity.admin.InvoiceDetail;
 import com.kym.entity.admin.queryParams.InvoiceQueryParam;
 import com.kym.entity.common.PageBean;
 import com.kym.entity.miniapp.ChargeOrder;
 import com.kym.entity.miniapp.Invoice;
+import com.kym.entity.miniapp.InvoiceTitle;
 import com.kym.entity.miniapp.User;
 import com.kym.entity.miniapp.queryParams.ApplyInvoiceParams;
 import com.kym.entity.miniapp.vo.InvoiceVo;
 import com.kym.entity.wechat.FaPiao;
 import com.kym.entity.wechat.InvoiceOrderDetail;
-import com.kym.entity.wechat.TitleUrl;
 import com.kym.mapper.miniapp.InvoiceMapper;
 import com.kym.service.admin.InvoiceDetailService;
 import com.kym.service.miniapp.ChargeOrderService;
 import com.kym.service.miniapp.DataDictService;
+import com.kym.service.miniapp.HuapiaoerInvoiceService;
 import com.kym.service.miniapp.InvoiceService;
-import com.kym.service.wechat.WxPayService;
+import com.kym.service.miniapp.InvoiceTitleService;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -52,48 +51,34 @@ import java.util.List;
 public class InvoiceServiceImpl extends MPJBaseServiceImpl<InvoiceMapper, Invoice> implements InvoiceService {
 
     private final ChargeOrderService chargeOrderService;
-    private final WxPayService wxPayService;
+    private final HuapiaoerInvoiceService huapiaoerInvoiceService;
     private final DataDictService dataDictService;
     private final InvoiceDetailService invoiceDetailService;
+    private final InvoiceTitleService invoiceTitleService;
 
-    public InvoiceServiceImpl(ChargeOrderService chargeOrderService, @Lazy WxPayService wxPayService, @Lazy DataDictService dataDictService, InvoiceDetailService invoiceDetailService) {
+    public InvoiceServiceImpl(ChargeOrderService chargeOrderService, HuapiaoerInvoiceService huapiaoerInvoiceService, DataDictService dataDictService, InvoiceDetailService invoiceDetailService, InvoiceTitleService invoiceTitleService) {
         this.dataDictService = dataDictService;
         this.chargeOrderService = chargeOrderService;
-        this.wxPayService = wxPayService;
+        this.huapiaoerInvoiceService = huapiaoerInvoiceService;
         this.invoiceDetailService = invoiceDetailService;
+        this.invoiceTitleService = invoiceTitleService;
     }
 
 
-    /**
-     * 申请发票
-     * 业务流程:
-     * 1.校验订单
-     * 2.数据库写入invoice对象
-     * 3.api获取填写抬头的链接给前端
-     * 4.前端打开上一步返回的填写链接,客户填写抬头信息
-     * 5.接收抬头填写完成的通知,将抬头信息更新至第2步写入的invoice数据中
-     * 6.运营系统财务人员审核之后确认开票,请求api进行电子发票开票操作
-     *
-     * @param params
-     * @return
-     */
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public TitleUrl applyInvoice(ApplyInvoiceParams params) {
+    public Invoice applyInvoice(ApplyInvoiceParams params) {
         if (CommUtil.isEmptyOrNull(params.getStartChargeSeqs())) {
             throw new BusinessException("请选择需要开票的订单");
         }
-        // orderDetails 获取申请开票的订单
         var userId = StpUtil.getLoginIdAsLong();
         var orders = chargeOrderService.getChargeOrdersBySeqs(params.getStartChargeSeqs());
 
-        // 校验未完结的订单 orderStatus = 0
         if (orders.stream().anyMatch(o -> o.getOrderStatus().equals(ChargeOrder.ORDER_STATUS_未知))) {
             log.error("存在未知状态的订单:{}", orders.stream().filter(o -> o.getOrderStatus().equals(ChargeOrder.ORDER_STATUS_未知)).map(ChargeOrder::getStartChargeSeq).toArray());
             throw new BusinessException("请勿勾选未完结的订单");
         }
 
-        // 校验订单是已开票或者开票中
         var invoiced = orders.stream().filter(o -> o.getInvoiceStatus().equals(ChargeOrder.INVOICE_STATUS_开票中) || o.getInvoiceStatus().equals(ChargeOrder.INVOICE_STATUS_已开票)).toList();
         if (!invoiced.isEmpty()) {
             log.error("存在已开票或者开票中的订单:{}", invoiced.stream().map(ChargeOrder::getStartChargeSeq).toArray());
@@ -107,35 +92,63 @@ public class InvoiceServiceImpl extends MPJBaseServiceImpl<InvoiceMapper, Invoic
                         .setServiceMoneyDiscount(item.getServiceMoneyDiscount()))
                 .toList();
 
-        // 校验开票金额大于0
         var totalPayAmount = orders.stream().mapToInt(ChargeOrder::getPayAmount).sum();
         var elecMoney = orders.stream().mapToInt(ChargeOrder::getElecMoney).sum();
         var serviceMoney = orders.stream().mapToInt(ChargeOrder::getServiceMoney).sum();
         if (!(totalPayAmount > 0 && elecMoney > 0 && serviceMoney > 0)) {
             throw new BusinessException("订单总金额或总电费或总服务费金额异常");
         }
-        // 组装invoice
 
-        // todo 先放入缓存,等待抬头填写完成之后再写入数据库,设置超期时间,过期主动查询抬头填写情况
         var invoice = new Invoice()
                 .setUserId(userId)
                 .setOpenid(StpUtil.getSession().getString("openid"))
                 .setApplyId(OrderUtils.getOrderNo())
-                .setOrderDetails(orderDetails) // 订单信息
-                .setInvoiceAmount(totalPayAmount)   // 发票金额
-                .setTotalMoney(elecMoney + serviceMoney) // 订单总金额
-                .setElecMoney(elecMoney)    // 电费
-                .setServiceMoney(serviceMoney)    // 服务费
-                .setTotalPower(orders.stream().mapToDouble(ChargeOrder::getTotalPower).sum())   // 总电量
-                .setServiceMoneyDiscount(orders.stream().mapToInt(ChargeOrder::getServiceMoneyDiscount).sum()); //总优惠服务费
+                .setOrderDetails(orderDetails)
+                .setInvoiceAmount(totalPayAmount)
+                .setTotalMoney(elecMoney + serviceMoney)
+                .setElecMoney(elecMoney)
+                .setServiceMoney(serviceMoney)
+                .setTotalPower(orders.stream().mapToDouble(ChargeOrder::getTotalPower).sum())
+                .setServiceMoneyDiscount(orders.stream().mapToInt(ChargeOrder::getServiceMoneyDiscount).sum())
+                // 用户申请时直接保存抬头信息
+                .setEmail(params.getEmail())
+                .setPhone(params.getPhone())
+                .setInvoiceType(params.getInvoiceType())
+                .setInvoiceTitle(params.getInvoiceTitle())
+                .setTaxId(params.getTaxId())
+                .setAddress(params.getAddress())
+                .setBankName(params.getBankName())
+                .setBankAccount(params.getBankAccount())
+                .setRemark(params.getRemark());
         baseMapper.insert(invoice);
 
-        // 更新订单开票状态为开票中
+        // 自动保存抬头信息
+        if (params.getInvoiceTitle() != null && !params.getInvoiceTitle().isBlank()) {
+            var exists = invoiceTitleService.lambdaQuery()
+                    .eq(InvoiceTitle::getUserId, userId)
+                    .eq(params.getInvoiceType() != null, InvoiceTitle::getInvoiceType, params.getInvoiceType())
+                    .eq(Invoice.TYPE_企业.equals(params.getInvoiceType()), InvoiceTitle::getTaxId, params.getTaxId())
+                    .eq(Invoice.TYPE_个人.equals(params.getInvoiceType()), InvoiceTitle::getInvoiceTitle, params.getInvoiceTitle())
+                    .exists();
+            if (!exists) {
+                var title = new InvoiceTitle()
+                        .setUserId(userId)
+                        .setEmail(params.getEmail())
+                        .setPhone(params.getPhone())
+                        .setInvoiceType(params.getInvoiceType())
+                        .setInvoiceTitle(params.getInvoiceTitle())
+                        .setTaxId(params.getTaxId())
+                        .setAddress(params.getAddress())
+                        .setBankName(params.getBankName())
+                        .setBankAccount(params.getBankAccount());
+                invoiceTitleService.saveTitle(title);
+            }
+        }
+
         chargeOrderService.lambdaUpdate().set(ChargeOrder::getInvoiceStatus, ChargeOrder.INVOICE_STATUS_开票中)
                 .in(ChargeOrder::getStartChargeSeq, Arrays.stream(params.getStartChargeSeqs()).toList()).update();
 
-        return wxPayService.titleUrl(invoice);
-
+        return invoice;
     }
 
     @Override

+ 38 - 8
service/src/main/java/com/kym/service/miniapp/impl/InvoiceTitleServiceImpl.java

@@ -1,20 +1,50 @@
 package com.kym.service.miniapp.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.github.yulichang.base.MPJBaseServiceImpl;
 import com.kym.entity.miniapp.InvoiceTitle;
 import com.kym.mapper.miniapp.InvoiceTitleMapper;
 import com.kym.service.miniapp.InvoiceTitleService;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
 
-/**
- * <p>
- * 发票记录表 服务实现类
- * </p>
- *
- * @author skyline
- * @since 2023-09-21
- */
 @Service
+@DS("db-miniapp")
 public class InvoiceTitleServiceImpl extends MPJBaseServiceImpl<InvoiceTitleMapper, InvoiceTitle> implements InvoiceTitleService {
 
+    @Override
+    public List<InvoiceTitle> listByUser(Long userId) {
+        return lambdaQuery()
+                .eq(InvoiceTitle::getUserId, userId)
+                .orderByDesc(InvoiceTitle::getIsDefault)
+                .orderByDesc(InvoiceTitle::getUpdateTime)
+                .list();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void saveTitle(InvoiceTitle title) {
+        if (Boolean.TRUE.equals(title.getIsDefault())) {
+            lambdaUpdate()
+                    .set(InvoiceTitle::getIsDefault, false)
+                    .eq(InvoiceTitle::getUserId, title.getUserId())
+                    .update();
+        }
+        save(title);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void setDefault(Long userId, Long titleId) {
+        lambdaUpdate()
+                .set(InvoiceTitle::getIsDefault, false)
+                .eq(InvoiceTitle::getUserId, userId)
+                .update();
+        lambdaUpdate()
+                .set(InvoiceTitle::getIsDefault, true)
+                .eq(InvoiceTitle::getId, titleId)
+                .update();
+    }
 }

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

@@ -1,21 +1,13 @@
 package com.kym.service.wechat;
 
 import com.alibaba.fastjson2.JSONObject;
-import com.kym.entity.miniapp.Invoice;
-import com.kym.entity.wechat.*;
 import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
 import com.wechat.pay.java.service.refund.model.Refund;
 import jakarta.servlet.http.HttpServletRequest;
 import org.springframework.http.ResponseEntity;
 
 import java.io.IOException;
-import java.util.Map;
 
-/**
- * @author skyline
- * @description
- * @date 2023-08-11 19:05
- */
 public interface WxPayService {
     void applyWxRefund(String reason);
 
@@ -34,32 +26,4 @@ public interface WxPayService {
     PrepayWithRequestPaymentResponse wxPay(JSONObject rechargeAmount);
 
     ResponseEntity<Object> wxNotify(HttpServletRequest request) throws IOException;
-
-
-    Object devConfig();
-
-    void cardTemplate();
-
-    Object getDevConfig();
-
-    //================================================================发票=====================================================================
-    TitleUrl titleUrl(Invoice invoice);
-
-    void titleWriteNotice(Object[] notifyRes);
-
-    FaPiao.BuyerInformation userTitle(String applyId);
-
-    InvoiceBaseInfo baseInformation();
-
-    void fapiaoApplication(String invoiceId);
-
-    void wxInvoiceNotify(Object[] notifyRes);
-
-    void invoiceNotify(HttpServletRequest request);
-
-    Map<String, String> downloadInvoice(String invoiceId);
-
-    FapiaoDownload getFapiaoDownloadInfo(String applyId);
-
-    FapiaoApplications queryFapiao(String applyId);
 }

+ 3 - 385
service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

@@ -6,12 +6,8 @@ import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.util.RandomUtil;
-import cn.hutool.core.util.URLUtil;
 import com.alibaba.fastjson2.JSONObject;
 import com.baomidou.dynamic.datasource.annotation.DS;
-import com.baomidou.dynamic.datasource.annotation.DSTransactional;
-import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
-import com.kym.common.config.WxFapiaoConfig;
 import com.kym.common.config.WxPayConfig;
 import com.kym.common.constant.ResponseEnum;
 import com.kym.common.exception.BusinessException;
@@ -20,9 +16,7 @@ import com.kym.common.utils.LambadaTools;
 import com.kym.common.utils.OrderUtils;
 import com.kym.entity.miniapp.Account;
 import com.kym.entity.miniapp.*;
-import com.kym.entity.wechat.*;
 import com.kym.service.admin.ActivityService;
-import com.kym.service.admin.InvoiceDetailService;
 import com.kym.common.cache.PlatformCache;
 import com.kym.service.miniapp.*;
 import com.kym.service.platform.PlatformApiService;
@@ -89,8 +83,6 @@ public class WxPayServiceImpl implements WxPayService {
 
     private final WxPayConfig conf;
 
-    private final WxFapiaoConfig fapiaoConfig;
-
     private final WalletDetailService walletDetailService;
 
     private final PayLogService payLogService;
@@ -101,42 +93,29 @@ public class WxPayServiceImpl implements WxPayService {
 
     private final RefundLogService refundLogService;
 
-    private final InvoiceService invoiceService;
-
-    private final InvoiceTitleService invoiceTitleService;
-
     private final PlatformApiService platformApiService;
 
     private final ActivityService activityService;
 
     private final UserRechargeRightsService userRechargeRightsService;
 
-    private final InvoiceDetailService invoiceDetailService;
-
 
-    /**
-     * 微信支付专用,支持自动签名验签解密等
-     */
     private OkHttpClientAdapter wxHttpClient;
 
 
-    public WxPayServiceImpl(WxPayConfig conf, WxFapiaoConfig fapiaoConfig, WalletDetailService walletDetailService,
+    public WxPayServiceImpl(WxPayConfig conf, WalletDetailService walletDetailService,
                             PayLogService payLogService, AccountService accountService, ChargeOrderService chargeOrderService,
-                            RefundLogService refundLogService, InvoiceService invoiceService, InvoiceTitleService invoiceTitleService,
-                            PlatformApiService platformApiService, ActivityService activityService, UserRechargeRightsService userRechargeRightsService, InvoiceDetailService invoiceDetailService) {
+                            RefundLogService refundLogService,
+                            PlatformApiService platformApiService, ActivityService activityService, UserRechargeRightsService userRechargeRightsService) {
         this.conf = conf;
-        this.fapiaoConfig = fapiaoConfig;
         this.walletDetailService = walletDetailService;
         this.payLogService = payLogService;
         this.accountService = accountService;
         this.chargeOrderService = chargeOrderService;
         this.refundLogService = refundLogService;
-        this.invoiceService = invoiceService;
-        this.invoiceTitleService = invoiceTitleService;
         this.platformApiService = platformApiService;
         this.activityService = activityService;
         this.userRechargeRightsService = userRechargeRightsService;
-        this.invoiceDetailService = invoiceDetailService;
     }
 
     /**
@@ -783,365 +762,4 @@ public class WxPayServiceImpl implements WxPayService {
         }
     }
 
-    //================================================================发票=====================================================================
-
-    /**
-     * 微信电子发票配置开发选项,例如回调地址、全部账单展示开发票入口开关
-     */
-    @Override
-    public Object devConfig() {
-        // 创建卡券模版
-        cardTemplate();
-        var headers = new HttpHeaders();
-        headers.addHeader("Accept", "application/json");
-        headers.addHeader("Content-Type", "application/json");
-        var params = JSONObject.of("callback_url", "https://www.kuaiyuman.cn/api/invoice/notify", "show_fapiao_cell", false);
-        var requestBody = new JsonRequestBody.Builder().body(params.toJSONString()).build();
-        var res = wxHttpClient.patch(headers, fapiaoConfig.getDevConfig(), requestBody, JSONObject.class);
-        LOGGER.info("微信电子发票开发设置:{}", res.toString());
-        return res.getServiceResponse();
-    }
-
-    /**
-     * 创建电子发票卡券模版
-     */
-    @Override
-    public void cardTemplate() {
-        var headers = new HttpHeaders();
-        headers.addHeader("Accept", "application/json");
-        headers.addHeader("Content-Type", "application/json");
-        var params = JSONObject.of(
-                "card_appid", "wx93b0ef1be901bd19", // 公众号appid,不是小程序appid
-                "card_template_information", JSONObject.of(
-                        "logo_url", "https://kym-static.oss-cn-shenzhen.aliyuncs.com/logo/K7.png"
-                )
-        );
-        var requestBody = new JsonRequestBody.Builder().body(params.toJSONString()).build();
-        var res = wxHttpClient.post(headers, fapiaoConfig.getCardTemplate(), requestBody, JSONObject.class);
-        LOGGER.info("创建电子发票卡券模版:{}", res);
-    }
-
-    /**
-     * 查询商户配置的开发选项
-     */
-    @Override
-    public Object getDevConfig() {
-        var headers = new HttpHeaders();
-        headers.addHeader("Accept", "application/json");
-        headers.addHeader("Content-Type", "application/json");
-        var res = wxHttpClient.get(headers, fapiaoConfig.getDevConfig(), JSONObject.class);
-        LOGGER.info("查询微信电子发票开发设置:{}", res.toString());
-        return res;
-    }
-
-    /**
-     * 获取用户抬头信息填写URL
-     *
-     * @param invoice
-     * @return
-     */
-    @Override
-    public TitleUrl titleUrl(Invoice invoice) {
-        var headers = new HttpHeaders();
-        var params = Map.of(
-                "fapiao_apply_id", invoice.getApplyId(),
-                "appid", conf.getAppid(),
-                "openid", invoice.getOpenid(),
-                "total_amount", invoice.getInvoiceAmount(),
-                "source", "MINIPROGRAM"
-        );
-        headers.addHeader("Accept", "application/json");
-        var res = wxHttpClient.get(headers, fapiaoConfig.getTitleUrl().concat("?").concat(URLUtil.buildQuery(params, StandardCharsets.UTF_8)), TitleUrl.class);
-        return res.getServiceResponse();
-    }
-
-
-    /**
-     * 处理发票抬头填写完成通知
-     * event_type为FAPIAO.USER_APPLIED
-     *
-     * @param notifyRes
-     */
-    @Override
-    public void titleWriteNotice(Object[] notifyRes) {
-        try {
-            TitleWriteNotification titleWriteNotification = ((NotificationParser) notifyRes[1]).parse((RequestParam) notifyRes[0], TitleWriteNotification.class);
-
-            // 查询用户填写的抬头信息
-            var applyId = titleWriteNotification.getFapiaoApplyId();
-            LOGGER.info("回调成功,applyId:{}", applyId);
-            var buyerInformation = userTitle(applyId);
-
-            // 更新invoice表数据
-            var invoice = invoiceService.lambdaQuery().eq(Invoice::getApplyId, applyId).one();
-            invoice.setInvoiceType(buyerInformation.getType())
-                    .setInvoiceTitle(buyerInformation.getName())
-                    .setTaxId(buyerInformation.getTaxpayer_id())
-                    .setAddress(buyerInformation.getAddress())
-                    .setTelephone(buyerInformation.getTelephone())
-                    .setBankName(buyerInformation.getBank_name())
-                    .setBankAccount(buyerInformation.getBank_account())
-                    .setEmail(buyerInformation.getEmail())
-                    .setPhone(buyerInformation.getPhone());
-            invoiceService.updateById(invoice);
-
-            // 将发票抬头写入用户抬头表
-            var invoiceTitle = new InvoiceTitle()
-                    .setUserId(invoice.getUserId())
-                    .setInvoiceType(buyerInformation.getType())
-                    .setInvoiceTitle(buyerInformation.getName())
-                    .setTaxId(buyerInformation.getTaxpayer_id())
-                    .setTelephone(buyerInformation.getTelephone())
-                    .setAddress(buyerInformation.getAddress())
-                    .setBankName(buyerInformation.getBank_name())
-                    .setBankAccount(buyerInformation.getBank_account())
-                    .setEmail(buyerInformation.getEmail())
-                    .setPhone(buyerInformation.getPhone());
-            boolean exist = invoiceTitleService.lambdaQuery()
-                    .eq(InvoiceTitle::getUserId, invoiceTitle.getUserId())
-                    .eq(InvoiceTitle::getTaxId, invoiceTitle.getTaxId())
-                    .exists();
-            if (!exist) {
-                invoiceTitleService.save(invoiceTitle);
-            }
-            // 运营后台财务操作开具发票
-        } catch (ValidationException e) {
-            // 签名验证失败,返回 401 UNAUTHORIZED 状态码
-            LOGGER.error("微信处理发票抬头填写完成通知验签失败", e);
-            throw new BusinessException("验签失败");
-        }
-    }
-
-    /**
-     * 获取用户填写的抬头信息
-     *
-     * @param applyId
-     * @return
-     */
-    @Override
-    public FaPiao.BuyerInformation userTitle(String applyId) {
-        var headers = new HttpHeaders();
-        headers.addHeader("Accept", "application/json");
-        var res = wxHttpClient.get(headers, fapiaoConfig.getUserTitle().formatted(applyId), FaPiao.BuyerInformation.class);
-        LOGGER.info("用户发票申请applyId:{},抬头信息:{}", applyId, res);
-        return res.getServiceResponse();
-    }
-
-    /**
-     * 获取商户开票基础信息
-     */
-    @Override
-    public InvoiceBaseInfo baseInformation() {
-        var headers = new HttpHeaders();
-        headers.addHeader("Accept", "application/json");
-        var res = wxHttpClient.get(headers, fapiaoConfig.getBaseInformation(), InvoiceBaseInfo.class);
-        return res.getServiceResponse();
-    }
-
-
-    /**
-     * 获取商户可开具的商品和服务税收分类编码对照表
-     */
-    private TaxCodes getTaxCodes() {
-        var headers = new HttpHeaders();
-        headers.addHeader("Accept", "application/json");
-        var url = fapiaoConfig.getTaxCodes().concat("?offset=%d&limit=%d").formatted(0, 5);
-        var res = wxHttpClient.get(headers, url, TaxCodes.class);
-        return res.getServiceResponse();
-    }
-
-
-    /**
-     * 开具电子发票
-     */
-    @Override
-    @DS("db-miniapp")
-    public void fapiaoApplication(String invoiceId) {
-        var invoice = invoiceService.lambdaQuery().eq(Invoice::getId, invoiceId).one();
-        // 开票状态校验
-        if (!invoice.getStatus().equals(Invoice.STATUS_待开票)) {
-            throw new BusinessException("发票状态为开票中或已开票");
-        }
-        // 记录开票人
-        invoiceService.lambdaUpdate().set(Invoice::getBiller, StpUtil.getSession().getString("username")).eq(Invoice::getId, invoice.getId()).update();
-
-        // 订单金额+ ,服务费优惠金额- 服务费优惠金额单独一个item,金额为负数
-        var itemList = new ArrayList<FaPiao.FaPiaoInfomation.IssueItem>();
-        // 电费 税务商品编码:1100101020000000000
-        var elecMoney = invoice.getOrderDetails().stream().mapToInt(InvoiceOrderDetail::getElecMoney).sum();
-        // 必须为正数
-        if (elecMoney > 0) {
-            var elecMoneyItem = new FaPiao.FaPiaoInfomation.IssueItem().setTotal_amount(elecMoney).setTax_code("1100101020000000000");
-            itemList.add(elecMoneyItem);
-        }
-        // 服务费 税务商品编码:3040100000000000000
-        var serviceMoney = invoice.getOrderDetails().stream().mapToInt(InvoiceOrderDetail::getServiceMoney).sum();
-        // 必须为正数
-        if (serviceMoney > 0) {
-            var serviceMoneyItem = new FaPiao.FaPiaoInfomation.IssueItem().setTotal_amount(serviceMoney).setTax_code("3040100000000000000");
-            itemList.add(serviceMoneyItem);
-        }
-
-        // 服务费优惠 税务商品编码:3040100000000000000  注意:折扣行必须是被折扣行的下一行
-        var serviceMoneyDiscount = -invoice.getOrderDetails().stream().mapToInt(InvoiceOrderDetail::getServiceMoneyDiscount).sum();
-        // 必须为负数
-        if (serviceMoneyDiscount < 0) {
-            var serviceMoneyDiscountItem = new FaPiao.FaPiaoInfomation.IssueItem().setTotal_amount(serviceMoneyDiscount).setTax_code("3040100000000000000").setDiscount(true);
-            itemList.add(serviceMoneyDiscountItem);
-        }
-
-        var headers = new HttpHeaders();
-        headers.addHeader("Accept", "application/json");
-        headers.addHeader("Content-Type", "application/json");
-        headers.addHeader("Wechatpay-Serial", conf.getMchsn());
-
-        // 客户开票信息
-        var buyerInformation = new FaPiao.BuyerInformation()
-                .setType(invoice.getInvoiceType())
-                .setName(invoice.getInvoiceTitle())
-                .setTaxpayer_id(invoice.getTaxId())
-                .setTelephone(invoice.getTelephone())
-                .setAddress(invoice.getAddress())
-                .setBank_name(invoice.getBankName())
-                .setBank_account(invoice.getBankAccount());
-
-        // 发票信息
-        var fapiaoInformation = new FaPiao.FaPiaoInfomation();
-        // fapiao_id为invoice.id
-        fapiaoInformation.setFapiao_id(String.valueOf(invoice.getId()))
-                .setTotal_amount(invoice.getInvoiceAmount())
-                .setItems(itemList);
-
-        // 请求参数
-        var fapiao = new FaPiao()
-                .setScene("WITHOUT_WECHATPAY")
-                .setFapiao_apply_id(invoice.getApplyId())
-                .setBuyer_information(buyerInformation)
-                .setFapiao_information(List.of(fapiaoInformation));
-
-        var requestBody = new JsonRequestBody.Builder().body(JSONObject.toJSONString(fapiao)).build();
-        wxHttpClient.post(headers, fapiaoConfig.getFapiaoApplications(), requestBody, null);
-        // 修改发票状态为开票中
-        invoiceService.lambdaUpdate().set(Invoice::getStatus, Invoice.STATUS_开票中).eq(Invoice::getId, invoiceId).update();
-    }
-
-    /**
-     * 微信开票结果通知
-     * event_type为FAPIAO.ISSUED
-     *
-     * @param notifyRes
-     * @return
-     */
-    @Override
-    @DSTransactional
-    public void wxInvoiceNotify(Object[] notifyRes) {
-        try {
-            InvoiceNotification invoiceNotification = ((NotificationParser) notifyRes[1]).parse((RequestParam) notifyRes[0], InvoiceNotification.class);
-            LOGGER.info("微信开具发票结果通知回调{}:验签解密完毕,数据:\n{}", notifyRes[2], invoiceNotification);
-            // 业务逻辑
-            if (invoiceNotification.getFapiaoInformation().get(0).getFapiaoStatus().equals(InvoiceNotification.FapiaoStatus.ISSUED.name())) {
-                // 更新发票状态
-                invoiceService.lambdaUpdate().set(Invoice::getStatus, Invoice.STATUS_已开票).eq(Invoice::getApplyId, invoiceNotification.getFapiaoApplyId()).update();
-                var invoice = invoiceService.lambdaQuery().eq(Invoice::getApplyId, invoiceNotification.getFapiaoApplyId()).one();
-                // 更新订单开票状态为已开票
-                var chargeOrderSeqs = invoice.getOrderDetails().stream().map(InvoiceOrderDetail::getStartChargeSeq).toList();
-                if (!CommUtil.isEmptyOrNull(chargeOrderSeqs)) {
-                    chargeOrderService.lambdaUpdate().set(ChargeOrder::getInvoiceStatus, ChargeOrder.INVOICE_STATUS_已开票).in(ChargeOrder::getStartChargeSeq, chargeOrderSeqs).update();
-                }
-                /*
-                 * 销项发票excel数据中需包含:
-                 * 1.开票日期
-                 * 2.购买方名称及税号
-                 * 3.开票明细、金额、税额
-                 * 4.开票优惠明细、金额、税额
-                 * 5.发票状态是否作废或者红冲
-                 */
-                var fapiaoApplications = queryFapiao(invoice.getApplyId());
-                // 手动切换数据源
-                DynamicDataSourceContextHolder.push("db-admin");
-                invoiceDetailService.updateInvoiceDetail(invoice.getApplyId(), fapiaoApplications);
-                DynamicDataSourceContextHolder.poll();
-            } else {
-                LOGGER.error("微信开具发票失败:{}", invoiceNotification);
-            }
-        } catch (ValidationException e) {
-            // 签名验证失败,返回 401 UNAUTHORIZED 状态码
-            LOGGER.error("微信开具发票结果通知验签失败", e);
-            throw new BusinessException("验签失败");
-        }
-    }
-
-
-    /**
-     * 接收电子发票相关回调通知
-     *
-     * @param request
-     */
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public void invoiceNotify(HttpServletRequest request) {
-        var notifyRes = handleWxNotify(request);
-        var requestParam = (RequestParam) notifyRes[0];
-        Notification notification = JSONObject.parseObject(requestParam.getBody(), Notification.class);
-        switch (notification.getEventType()) {
-            // 发票抬头填写完成
-            case "FAPIAO.USER_APPLIED" -> titleWriteNotice(notifyRes);
-            // 发票开具结果
-            case "FAPIAO.ISSUED" -> wxInvoiceNotify(notifyRes);
-        }
-    }
-
-
-    /**
-     * 下载发票,下载链接30s有效
-     *
-     * @param invoiceId
-     * @return
-     */
-    @Override
-    public Map<String, String> downloadInvoice(String invoiceId) {
-        var invoice = invoiceService.getById(invoiceId);
-        var download = getFapiaoDownloadInfo(invoice.getApplyId());
-        var downloadInfo = download.getFapiao_download_info_list().get(0);
-        // 查询电子发票
-        var fapiaoApplications = queryFapiao(invoice.getApplyId());
-
-        var params = "&mchid=%s&openid=%s&invoice_code=%s&invoice_no=%s&fapiao_id=%s";
-        // openid 通过查询电子发票接口获取的card_openid
-        var fapiao = fapiaoApplications.getFapiao_information().get(0);
-        var downloadUrl = downloadInfo.getDownload_url().concat(params.formatted(conf.getMchid(),
-                fapiao.getCard_information().getCard_openid(), fapiao.getBlue_fapiao().getFapiao_code(),
-                fapiao.getBlue_fapiao().getFapiao_number(), invoice.getId()));
-        return Map.of("downloadUrl", downloadUrl);
-    }
-
-    /**
-     * 获取发票下载信息
-     *
-     * @param applyId
-     * @return
-     */
-    @Override
-    public 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
-     */
-    @Override
-    public 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();
-    }
-
 }