|
@@ -1,257 +0,0 @@
|
|
|
-package com.kym.service.wechat.impl;
|
|
|
|
|
-
|
|
|
|
|
-import com.alibaba.fastjson2.JSON;
|
|
|
|
|
-import com.alibaba.fastjson2.JSONObject;
|
|
|
|
|
-import com.kym.entity.miniapp.Invoice;
|
|
|
|
|
-import com.kym.entity.wechat.InvoiceOrderDetail;
|
|
|
|
|
-import com.kym.huapiaoer.model.response.InvoiceInfo;
|
|
|
|
|
-import com.wechat.pay.java.core.http.*;
|
|
|
|
|
-import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
-import okhttp3.*;
|
|
|
|
|
-import org.springframework.stereotype.Service;
|
|
|
|
|
-
|
|
|
|
|
-import java.time.LocalDateTime;
|
|
|
|
|
-import java.time.format.DateTimeFormatter;
|
|
|
|
|
-import java.util.ArrayList;
|
|
|
|
|
-import java.util.HashMap;
|
|
|
|
|
-import java.util.List;
|
|
|
|
|
-import java.util.Map;
|
|
|
|
|
-import java.util.concurrent.TimeUnit;
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 通过微信支付 v3 电子发票 API 将航信发票插入用户微信卡包。
|
|
|
|
|
- * 三步流程:创建申请单 → 上传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";
|
|
|
|
|
-
|
|
|
|
|
- private static final HttpHeaders JSON_HEADERS;
|
|
|
|
|
-
|
|
|
|
|
- static {
|
|
|
|
|
- JSON_HEADERS = new HttpHeaders();
|
|
|
|
|
- JSON_HEADERS.addHeader("Accept", "application/json");
|
|
|
|
|
- JSON_HEADERS.addHeader("Content-Type", "application/json");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private final OkHttpClient okHttpClient = new OkHttpClient.Builder()
|
|
|
|
|
- .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: 创建发票申请单
|
|
|
|
|
- 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;
|
|
|
|
|
- }
|
|
|
|
|
- String mediaId = uploadFile(invoice.getApplyId(), pdfBytes);
|
|
|
|
|
- if (mediaId == null) {
|
|
|
|
|
- log.warn("上传发票文件失败, applyId:{}", invoice.getApplyId());
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Step 3: 插入卡包
|
|
|
|
|
- var cardReq = buildInsertCardsRequest(invoice, invoiceInfo, mediaId);
|
|
|
|
|
- String cardUrl = API_BASE + String.format(INSERT_CARDS, invoice.getApplyId());
|
|
|
|
|
- log.info("插入发票卡包, applyId:{}, body:{}", invoice.getApplyId(), JSON.toJSONString(cardReq));
|
|
|
|
|
-
|
|
|
|
|
- HttpResponse<JSONObject> cardResp = WxPayServiceImpl.wxHttpClient.post(
|
|
|
|
|
- JSON_HEADERS, cardUrl,
|
|
|
|
|
- new JsonRequestBody.Builder().body(JSON.toJSONString(cardReq)).build(),
|
|
|
|
|
- JSONObject.class);
|
|
|
|
|
- log.info("插入卡包响应, applyId:{}, resp:{}",
|
|
|
|
|
- invoice.getApplyId(), JSON.toJSONString(cardResp.getServiceResponse()));
|
|
|
|
|
- return true;
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.error("微信卡包插入异常, applyId:{}, msg:{}", invoice.getApplyId(), e.getMessage());
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ========== 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();
|
|
|
|
|
- }
|
|
|
|
|
- } catch (Exception e) { log.error("下载 PDF 异常, url:{}", url, e); }
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- 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,
|
|
|
|
|
- okhttp3.RequestBody.create(meta, okhttp3.MediaType.parse("application/json")))
|
|
|
|
|
- .addFormDataPart("file", "invoice.pdf",
|
|
|
|
|
- okhttp3.RequestBody.create(pdfBytes, okhttp3.MediaType.parse("application/pdf")))
|
|
|
|
|
- .build();
|
|
|
|
|
-
|
|
|
|
|
- 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)
|
|
|
|
|
- .header("Accept", "application/json")
|
|
|
|
|
- .header("Wechatpay-Serial", MCH_SERIAL)
|
|
|
|
|
- .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 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();
|
|
|
|
|
- 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;
|
|
|
|
|
- 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; }
|
|
|
|
|
-
|
|
|
|
|
- String ticketDate = info.getTicketDate();
|
|
|
|
|
- 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"))
|
|
|
|
|
- : LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss+08:00"));
|
|
|
|
|
-
|
|
|
|
|
- var sellerInfo = new HashMap<String, Object>();
|
|
|
|
|
- sellerInfo.put("name", "深圳市快与慢科技有限公司");
|
|
|
|
|
- sellerInfo.put("taxpayer_id", "91440300MA5HJNDG14");
|
|
|
|
|
-
|
|
|
|
|
- var cardInfo = new HashMap<String, Object>();
|
|
|
|
|
- cardInfo.put("fapiao_media_id", mediaId);
|
|
|
|
|
- cardInfo.put("fapiao_number", info.getInvoicenumber());
|
|
|
|
|
- cardInfo.put("fapiao_code", info.getInvoicecode() != null ? info.getInvoicecode() : "");
|
|
|
|
|
- cardInfo.put("fapiao_time", fapiaoTime);
|
|
|
|
|
- cardInfo.put("total_amount", totalAmount);
|
|
|
|
|
- cardInfo.put("tax_amount", totalTax);
|
|
|
|
|
- cardInfo.put("amount", totalAmount - totalTax);
|
|
|
|
|
- cardInfo.put("seller_information", sellerInfo);
|
|
|
|
|
- cardInfo.put("extra_information", new HashMap<>());
|
|
|
|
|
- cardInfo.put("items", items);
|
|
|
|
|
- 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());
|
|
|
|
|
-
|
|
|
|
|
- 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;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private HashMap<String, Object> buildItem(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);
|
|
|
|
|
- return item;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|