Bladeren bron

fix: 恢复三步流程,补全创建申请单的必填字段

- Step 1: 创建发票申请单,补上 fapiao_bill_type、billing_person_id、billing_person
- Step 2: 下载PDF并上传获取 media_id
- Step 3: 调用 insert-cards 插入卡包

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 dagen geleden
bovenliggende
commit
73e403d9d0
1 gewijzigde bestanden met toevoegingen van 104 en 90 verwijderingen
  1. 104 90
      service/src/main/java/com/kym/service/wechat/impl/WechatPayFapiaoService.java

+ 104 - 90
service/src/main/java/com/kym/service/wechat/impl/WechatPayFapiaoService.java

@@ -20,14 +20,14 @@ import java.util.concurrent.TimeUnit;
 
 /**
  * 通过微信支付 v3 电子发票 API 将航信发票插入用户微信卡包。
- *
- * @see <a href="https://pay.wechatpay.cn/doc/v3/merchant/4012538365">插入卡包 API</a>
+ * 三步流程:创建申请单 → 上传PDF → 插入卡包
  */
 @Service
 @Slf4j
 public class WechatPayFapiaoService {
 
     private static final String API_BASE = "https://api.mch.weixin.qq.com";
+    private static final String CREATE_APP = "/v3/new-tax-control-fapiao/fapiao-applications";
     private static final String UPLOAD_FILE = "/v3/new-tax-control-fapiao/fapiao-applications/%s/fapiao-files";
     private static final String INSERT_CARDS = "/v3/new-tax-control-fapiao/fapiao-applications/%s/insert-cards";
     private static final String MCH_SERIAL = "6A45EEB068369430B2FFD45EA29F641A8E18165F";
@@ -41,54 +41,37 @@ public class WechatPayFapiaoService {
     }
 
     private final OkHttpClient okHttpClient = new OkHttpClient.Builder()
-            .connectTimeout(15, TimeUnit.SECONDS)
-            .readTimeout(30, TimeUnit.SECONDS)
-            .build();
-
-    /**
-     * 将已开具的航信电子发票插入用户微信卡包。
-     * 流程:下载航信 PDF → 上传微信支付获取 media_id → 调用 insert-cards。
-     *
-     * @return true 表示调用成功
-     */
+            .connectTimeout(15, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).build();
+
     public boolean insertToCard(Invoice invoice, InvoiceInfo invoiceInfo) {
         if (invoice.getOpenid() == null || invoice.getOpenid().isBlank()) {
             log.info("用户无 openid,跳过卡包插入, applyId:{}", invoice.getApplyId());
             return false;
         }
-
         try {
-            // Step 1: 下载航信 PDF
+            // Step 1: 创建发票申请单
+            var appReq = buildCreateAppRequest(invoice, invoiceInfo);
+            log.info("创建发票申请单, applyId:{}, body:{}", invoice.getApplyId(), JSON.toJSONString(appReq));
+
+            HttpResponse<JSONObject> appResp = WxPayServiceImpl.wxHttpClient.post(
+                    JSON_HEADERS, API_BASE + CREATE_APP,
+                    new JsonRequestBody.Builder().body(JSON.toJSONString(appReq)).build(),
+                    JSONObject.class);
+            log.info("创建申请单响应, applyId:{}, resp:{}",
+                    invoice.getApplyId(), JSON.toJSONString(appResp.getServiceResponse()));
+
+            // Step 2: 下载航信PDF并上传到微信支付
             byte[] pdfBytes = downloadPdf(invoiceInfo.getInvoiceurl());
             if (pdfBytes == null || pdfBytes.length == 0) {
                 log.warn("下载航信 PDF 失败, applyId:{}", invoice.getApplyId());
                 return false;
             }
-
-            // Step 2: 上传 PDF 到微信支付
-            String meta = JSON.toJSONString(Map.of(
-                    "file_type", "FAPIAO_PDF",
-                    "fapiao_apply_id", invoice.getApplyId()
-            ));
-
-            String uploadUrl = API_BASE + String.format(UPLOAD_FILE, invoice.getApplyId());
-            log.info("上传发票文件, applyId:{}, meta:{}", invoice.getApplyId(), meta);
-
-            String uploadRespBody = uploadFile(uploadUrl, meta, pdfBytes);
-            if (uploadRespBody == null) {
+            String mediaId = uploadFile(invoice.getApplyId(), pdfBytes);
+            if (mediaId == null) {
                 log.warn("上传发票文件失败, applyId:{}", invoice.getApplyId());
                 return false;
             }
 
-            JSONObject uploadJson = JSONObject.parseObject(uploadRespBody);
-            String mediaId = uploadJson.getString("fapiao_media_id");
-            log.info("上传发票文件成功, applyId:{}, mediaId:{}", invoice.getApplyId(), mediaId);
-
-            if (mediaId == null || mediaId.isBlank()) {
-                log.warn("未获取到 fapiao_media_id, applyId:{}", invoice.getApplyId());
-                return false;
-            }
-
             // Step 3: 插入卡包
             var cardReq = buildInsertCardsRequest(invoice, invoiceInfo, mediaId);
             String cardUrl = API_BASE + String.format(INSERT_CARDS, invoice.getApplyId());
@@ -98,10 +81,8 @@ public class WechatPayFapiaoService {
                     JSON_HEADERS, cardUrl,
                     new JsonRequestBody.Builder().body(JSON.toJSONString(cardReq)).build(),
                     JSONObject.class);
-
-            log.info("插入发票卡包响应, applyId:{}, resp:{}",
+            log.info("插入卡包响应, applyId:{}, resp:{}",
                     invoice.getApplyId(), JSON.toJSONString(cardResp.getServiceResponse()));
-
             return true;
         } catch (Exception e) {
             log.error("微信卡包插入异常, applyId:{}, msg:{}", invoice.getApplyId(), e.getMessage());
@@ -109,26 +90,78 @@ public class WechatPayFapiaoService {
         }
     }
 
+    // ========== Step 1: 创建申请单 ==========
+
+    private HashMap<String, Object> buildCreateAppRequest(Invoice invoice, InvoiceInfo info) {
+        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 items = new ArrayList<HashMap<String, Object>>();
+        int totalAmount = 0;
+        if (elecMoney > 0) { items.add(buildAppItem("充电电费", elecMoney, "1100101020200000000", 1300)); totalAmount += elecMoney; }
+        if (netServiceMoney > 0) { items.add(buildAppItem("充电服务费", netServiceMoney, "1100101020200000000", 1300)); totalAmount += netServiceMoney; }
+
+        var fapiaoInfo = new HashMap<String, Object>();
+        fapiaoInfo.put("fapiao_id", invoice.getApplyId());
+        fapiaoInfo.put("total_amount", totalAmount);
+        fapiaoInfo.put("fapiao_bill_type", "COMM_FAPIAO");
+        fapiaoInfo.put("billing_person_id", "2061377495014797313");
+        fapiaoInfo.put("billing_person", "*磊 2417");
+        fapiaoInfo.put("items", items);
+        if (invoice.getRemark() != null && !invoice.getRemark().isBlank()) {
+            fapiaoInfo.put("remark", invoice.getRemark());
+        }
+
+        var buyerInfo = new HashMap<String, Object>();
+        buyerInfo.put("type", invoice.getInvoiceType());
+        buyerInfo.put("name", invoice.getInvoiceTitle());
+        if (invoice.getTaxId() != null && !invoice.getTaxId().isBlank()) {
+            buyerInfo.put("taxpayer_id", invoice.getTaxId());
+        }
+
+        var req = new HashMap<String, Object>();
+        req.put("fapiao_apply_id", invoice.getApplyId());
+        req.put("scene", "WITHOUT_WECHATPAY");
+        req.put("buyer_information", buyerInfo);
+        req.put("fapiao_information", List.of(fapiaoInfo));
+
+        return req;
+    }
+
+    private HashMap<String, Object> buildAppItem(String name, int totalAmount, String taxCode, int taxRate) {
+        var item = new HashMap<String, Object>();
+        item.put("tax_code", taxCode);
+        item.put("goods_name", name);
+        item.put("quantity", 100000000);
+        item.put("total_amount", totalAmount);
+        item.put("tax_rate", taxRate);
+        item.put("discount", false);
+        item.put("preferential_policy_code", 1);
+        return item;
+    }
+
+    // ========== Step 2: 下载PDF & 上传文件 ==========
+
     private byte[] downloadPdf(String url) {
         try {
             var req = new Request.Builder().url(url).get().build();
             try (var resp = okHttpClient.newCall(req).execute()) {
-                if (resp.isSuccessful() && resp.body() != null) {
-                    return resp.body().bytes();
-                }
+                if (resp.isSuccessful() && resp.body() != null) return resp.body().bytes();
             }
-        } catch (Exception e) {
-            log.error("下载 PDF 异常, url:{}", url, e);
-        }
+        } catch (Exception e) { log.error("下载 PDF 异常, url:{}", url, e); }
         return null;
     }
 
-    /**
-     * 上传文件到微信支付,使用 SDK credential 签名 + OkHttp3 发送 multipart。
-     */
-    private String uploadFile(String url, String meta, byte[] pdfBytes) throws Exception {
-        // 构建 multipart body
-        String boundary = "WxPayUpload" + System.currentTimeMillis();
+    private String uploadFile(String applyId, byte[] pdfBytes) throws Exception {
+        String meta = JSON.toJSONString(Map.of(
+                "file_type", "FAPIAO_PDF",
+                "fapiao_apply_id", applyId
+        ));
+
+        String boundary = "WxPayUp" + System.currentTimeMillis();
         okhttp3.RequestBody multipartBody = new MultipartBody.Builder(boundary)
                 .setType(MultipartBody.FORM)
                 .addFormDataPart("meta", null,
@@ -137,29 +170,31 @@ public class WechatPayFapiaoService {
                         okhttp3.RequestBody.create(pdfBytes, okhttp3.MediaType.parse("application/pdf")))
                 .build();
 
-        // 用 SDK 生成签名 — 签名的 body 是 meta 字符串
+        String url = API_BASE + String.format(UPLOAD_FILE, applyId);
         String auth = WxPayServiceImpl.config.createCredential()
                 .getAuthorization(new java.net.URI(url), "POST", meta);
 
-        // 发送请求
         okhttp3.Request request = new Request.Builder()
-                .url(url)
-                .header("Authorization", auth)
+                .url(url).header("Authorization", auth)
                 .header("Accept", "application/json")
                 .header("Wechatpay-Serial", MCH_SERIAL)
-                .post(multipartBody)
-                .build();
-
-        try (okhttp3.Response response = okHttpClient.newCall(request).execute()) {
-            if (response.isSuccessful() && response.body() != null) {
-                return response.body().string();
+                .post(multipartBody).build();
+
+        try (okhttp3.Response resp = okHttpClient.newCall(request).execute()) {
+            if (resp.isSuccessful() && resp.body() != null) {
+                JSONObject json = JSONObject.parseObject(resp.body().string());
+                String mediaId = json.getString("fapiao_media_id");
+                log.info("上传发票文件成功, applyId:{}, mediaId:{}", applyId, mediaId);
+                return mediaId;
             }
-            String errBody = response.body() != null ? response.body().string() : "";
-            log.error("上传文件 HTTP {}, body:{}", response.code(), errBody);
-            return null;
+            String err = resp.body() != null ? resp.body().string() : "";
+            log.error("上传文件失败, applyId:{}, HTTP:{}, body:{}", applyId, resp.code(), err);
         }
+        return null;
     }
 
+    // ========== Step 3: 插入卡包 ==========
+
     private HashMap<String, Object> buildInsertCardsRequest(Invoice invoice, InvoiceInfo info, String mediaId) {
         var orderDetails = invoice.getOrderDetails();
         int elecMoney = orderDetails.stream().mapToInt(InvoiceOrderDetail::getElecMoney).sum();
@@ -170,25 +205,11 @@ public class WechatPayFapiaoService {
         var items = new ArrayList<HashMap<String, Object>>();
         int totalAmount = 0;
         int totalTax = 0;
+        if (elecMoney > 0) { int tax = elecMoney - (int)Math.round(elecMoney/1.13); items.add(buildItem("充电电费", elecMoney, "1100101020200000000", 1300)); totalAmount += elecMoney; totalTax += tax; }
+        if (netServiceMoney > 0) { int tax = netServiceMoney - (int)Math.round(netServiceMoney/1.13); items.add(buildItem("充电服务费", netServiceMoney, "1100101020200000000", 1300)); totalAmount += netServiceMoney; totalTax += tax; }
 
-        if (elecMoney > 0) {
-            int tax = elecMoney - (int) Math.round(elecMoney / 1.13);
-            totalAmount += elecMoney;
-            totalTax += tax;
-            items.add(buildItem("充电电费", elecMoney, "1100101020200000000", 1300));
-        }
-        if (netServiceMoney > 0) {
-            int tax = netServiceMoney - (int) Math.round(netServiceMoney / 1.13);
-            totalAmount += netServiceMoney;
-            totalTax += tax;
-            items.add(buildItem("充电服务费", netServiceMoney, "1100101020200000000", 1300));
-        }
-
-        // 票面时间 → RFC 3339
         String ticketDate = info.getTicketDate();
-        if (ticketDate != null && ticketDate.length() == 16) {
-            ticketDate += ":00";
-        }
+        if (ticketDate != null && ticketDate.length() == 16) ticketDate += ":00";
         String fapiaoTime = ticketDate != null
                 ? LocalDateTime.parse(ticketDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
                         .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss+08:00"))
@@ -198,8 +219,6 @@ public class WechatPayFapiaoService {
         sellerInfo.put("name", "深圳市快与慢科技有限公司");
         sellerInfo.put("taxpayer_id", "91440300MA5HJNDG14");
 
-        var extraInfo = new HashMap<String, Object>();
-
         var cardInfo = new HashMap<String, Object>();
         cardInfo.put("fapiao_media_id", mediaId);
         cardInfo.put("fapiao_number", info.getInvoicenumber());
@@ -209,24 +228,19 @@ public class WechatPayFapiaoService {
         cardInfo.put("tax_amount", totalTax);
         cardInfo.put("amount", totalAmount - totalTax);
         cardInfo.put("seller_information", sellerInfo);
-        cardInfo.put("extra_information", extraInfo);
+        cardInfo.put("extra_information", new HashMap<>());
         cardInfo.put("items", items);
-        if (invoice.getRemark() != null && !invoice.getRemark().isBlank()) {
-            cardInfo.put("remark", invoice.getRemark());
-        }
+        if (invoice.getRemark() != null && !invoice.getRemark().isBlank()) cardInfo.put("remark", invoice.getRemark());
 
         var buyerInfo = new HashMap<String, Object>();
         buyerInfo.put("type", invoice.getInvoiceType());
         buyerInfo.put("name", invoice.getInvoiceTitle());
-        if (invoice.getTaxId() != null && !invoice.getTaxId().isBlank()) {
-            buyerInfo.put("taxpayer_id", invoice.getTaxId());
-        }
+        if (invoice.getTaxId() != null && !invoice.getTaxId().isBlank()) buyerInfo.put("taxpayer_id", invoice.getTaxId());
 
         var req = new HashMap<String, Object>();
         req.put("scene", "WITHOUT_WECHATPAY");
         req.put("buyer_information", buyerInfo);
         req.put("fapiao_card_information", List.of(cardInfo));
-
         return req;
     }