java对接微信电子小票功能

电子小票与纸质小票都是线下购物的有效凭证,是纸质小票的电子化形态。电子小票易于存储、查看,便于消费追溯、获取售后服务等。

目前微信官方推出电子小票功能方案分为两种

1.小程序接入方案:微信电子小票通过API接口与商户收银/ERP系统对接,商户将自己小程序中的线下订单详情页path路径,通过接口随每笔支付回传微信,微信将通过支付消息凭证为用户下发该笔小票。用户点击后,直接跳转进入商户小程序的订单详情查看小票信息。

但是该方案依赖商户具备小程序线下订单详情

2.图片接入方案:微信电子小票通过API接口与商户收银/ERP系统对接,商户将小票数据转化为图片格式,通过接口随每笔支付回传微信,微信将通过支付消息凭证为用户下发该笔小票图片。

笔者使用的是第二种方案,

因公司为saas服务商,服务的各个商家不一定有自己的小程序,所以采用图片回传的方式去进行微信电子小票的实现

最终的实现效果是这样的:

 一般的支付详情中是没有【浏览商家小票】这个按钮的,这个是实现之后的效果,点击之后能看到我上传的商家小票信息

3.申请开通

不管是哪种方案接入,都需要联系微信对应DB进行方案接入,申请邮件中填入需要申请的微信商户号等信息,服务商模式则只需要填写服务商商户号,其子商户会一并开通不需要额外申请

4.对接流程

官方流程:

接口:/v3/marketing/shopping-receipt/shoppingreceipts

域名:https://api.mch.weixin.qq.com

对接前需要引入微信支付的相关jar包

pom可导入dependency

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.9</version>
</dependency>
String filePath = "/your/home/test.png";
URI uri = new URI("https://api.mch.weixin.qq.com/v3/marketing/shopping-receipt/shoppingreceipts");
File file = new File(filePath);
try (FileInputStream fileIs = new FileInputStream(file)) {
    String transaction_id = "420000153220220···158964";
    String transaction_mchid = "1900006#";
    String transaction_sub_mchid = "";
    String out_trade_no = "sdk123456789202205#809";
    String openid = "oK7fFt8zzEZ909XH-LE2#";
    String upload_time = "2022-05-07T15:39:35.000+08:00";
    String meta = "";
    String sha256 = DigestUtils.sha256Hex(fileIs);
    if (transaction_sub_mchid == "") {
        meta = String.format("{\"transaction_id\":\"%s\",\"transaction_mchid\":\"%s\",\"out_trade_no\":\"%s\",\"openid\":\"%s\",\"sha256\":\"%s\",\"upload_time\":\"%s\"}}", transaction_id, transaction_mchid, out_trade_no, openid, sha256, upload_time);
    } else {
        meta = String.format("{\"transaction_id\":\"%s\",\"transaction_mchid\":\"%s\",\"transaction_sub_mchid\":\"%s\",\"out_trade_no\":\"%s\",\"openid\":\"%s\",\"sha256\":\"%s\",\"upload_time\":\"%s\"}}", transaction_id, transaction_mchid, transaction_sub_mchid, out_trade_no, openid, sha256, upload_time);
    }
    try (InputStream is = new FileInputStream(file)) {
        WechatPayUploadHttpPost request = new WechatPayUploadHttpPost.Builder(uri)
                .withFile(file.getName(), meta, is)
                .build();
        try (CloseableHttpResponse response = httpClient.execute(request)) {
            // do something useful with the response body
            // and ensure it is fully consumed
            String s = EntityUtils.toString(response.getEntity());
            System.out.println("result: "+s);
        }
    }
}

参数释义:

  • transaction_id 必填【微信支付订单号】 微信支付订单的交易单号,上传的电子小票会关联到该订单。用户可以在该笔微信支付订单的账单详情页,看到上传的电子小票。

  • transaction_mchid 选填【商户号】 微信支付订单的下单商户号。若该笔微信支付订单存在下单子商户号,则该字段可不填。订单中无下单子商户该字段必填。

  • transaction_sub_mchid 选填【子商户号】 微信支付订单的下单子商户号。若该笔微信支付订单不存在下单子商户号,则该字段不填。订单中有下单商户号该字段必填。

  • out_trade_no 选填【商户订单号】 微信支付订单的商户订单号。

  • openid 必填【电子小票归属的OpenID】 微信支付订单中OpenID。若微信支付订单中有sub_OpenID,则填写sub_OpenID内容。

  • sha256 必填 【电子小票图片文件摘要】 图片文件的文件摘要,即对图片文件的二进制内容进行sha256计算得到的值。

  • merchant_contact_information 选填【商户与商家的联系渠道】 商户与商家的联系渠道

  • upload_time 选填【上传时间】 用于标识请求的先后顺序。遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss.SSS+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,SSS表示毫秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35.100+08:00表示,北京时间2015年5月20日13点29分35秒100毫秒。

  • file 必填【图片文件】 将图片文件以二进制方式读取后,原样设置到该HTTP文件表单对象的value中。电子小票图片只支持PNG、JPG格式,需在HTTP表单参数content-type中,设置图片类型为image/pngimage/jpg。文件大小不能超过200KB。

应答示例

{
  "receipt": {
    "brand_id": 1142,
    "create_time": "2015-05-20T13:29:35+08:00",
    "image_type": "PNG",
    "merchant_contact_information": {
      "consultation_phone_number": "pVd1HJ6v/69bDnuC4EL5Kz4jBHLiCa8MRtelw/wDa4SzfeespQO/0kjiwfqdfg=="
    },
    "modify_time": "2015-05-20T13:29:35+08:00",
    "openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o",
    "receipt_id": "121630001",
    "sha256": "2969f98ef4763da62670d3aee5d456b56a8f3447c0178da21445206aa400a464",
    "state": "WAIT_REVIEW",
    "transaction_id": "1217752501201407033233368018",
    "transaction_mchid": "1230000109",
    "transaction_sub_mchid": "1230000109",
    "upload_time": "2021-05-20T13:29:35.120+08:00"
  }
}

以上是官方给的文档,其实该文档不完整,调用该接口需要有证书

本人是服务商,普通微信商家是一样的,需要在微信后台下载相关证书

 将证书下载到对应的本地地址进行解压得到以下三个文件

 需要用到的是apiclient_cert.pem,将该文件放在本地,linux需要放在指定位置便于访问

我代码是这么处理的

 @RequestMapping("/upload")
    public ResultVo upload(HttpServletRequest req)  {
        File file = null;
        try{
            String sid = req.getHeader("sid");
            String spid = req.getHeader("spid");
            String transaction_id = req.getHeader("transactionId");
            String transaction_mchid = WXPayUtils.merchantId;
            String transaction_sub_mchid = req.getHeader("transactionSubMchId");
            String out_trade_no = req.getHeader("outTradeNo");
            String openid = req.getHeader("openid");
            //String upload_time = req.getHeader("upload_time");


            ThreadVar var = VarHolder.getVar();
            InputStream is = req.getInputStream();
            byte[] bb = ByteUtils.readStream(is);
            File ff = new File(importpath);
            judeDirExists(ff);
            String filename = spid + "-" +sid+ "-"+ out_trade_no + ".png";
            ByteUtils.saveFile(importpath + "/" + filename, bb);

            file = new File(importpath + "/" + filename);



            URI uri = new URI(WXPayUtils.shoppingreceiptsUrl);

            try (FileInputStream fileIs = new FileInputStream(file)) {
        
                String meta = "";
                String sha256 = DigestUtils.sha256Hex(fileIs);
                if (transaction_sub_mchid == "") {
                    meta = String.format("{\"transaction_id\":\"%s\",\"transaction_mchid\":\"%s\",\"out_trade_no\":\"%s\",\"openid\":\"%s\",\"sha256\":\"%s\"}}", transaction_id, transaction_mchid, out_trade_no, openid, sha256);
                } else {
                    meta = String.format("{\"transaction_id\":\"%s\",\"transaction_mchid\":\"%s\",\"transaction_sub_mchid\":\"%s\",\"out_trade_no\":\"%s\",\"openid\":\"%s\",\"sha256\":\"%s\"}}", transaction_id, transaction_mchid, transaction_sub_mchid, out_trade_no, openid, sha256);
                }
                try (InputStream ins = new FileInputStream(file)) {
                    WechatPayUploadHttpPost request = new WechatPayUploadHttpPost.Builder(uri)
                            .withFile(file.getName(), meta, ins)
                            .build();
                    //.withWechatPay(wechatPayCertificates);
                    // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
                    //merchantId商户号。
                    //merchantSerialNumber商户API证书的证书序列号。
                    //merchantPrivateKey商户API私钥,如何加载商户API私钥请看常见问题。
                    //wechatPayCertificates微信支付平台证书列表。你也可以使用后面章节提到的“定时更新平台证书功能”,而不需要关心平台证书的来龙去脉。
                    // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
                    CloseableHttpClient wxHttpClient = WXPayUtils.getWxHttpClient(transaction_mchid);

                    try (CloseableHttpResponse response = wxHttpClient.execute(request)) {
                        // do something useful with the response body
                        // and ensure it is fully consumed
                        String s = EntityUtils.toString(response.getEntity());
                        JSONObject jsonObject = JSONObject.parseObject(s);
                        LogUtils.log("result: "+s);
                        if(jsonObject.getString("code").contains("ERROR")){
                            return failReturn(jsonObject.getString("message"));
                        }
                        //{"code":"PARAM_ERROR","message":"交易信息不合法,请检查transaction_id、transaction_mchid、transaction_sub_mchid、openid后重试"}
                        //if(){
                        //
                        //}
                        //System.out.println("result: "+s);
                    }
                }
            }
            file.delete();
            return sucessReturn("上传成功",null);

        }catch (Exception e){
            if(e.getCause().getMessage().contains("应答的微信支付签名验证失败")){
                file.delete();
                return sucessReturn("上传成功",null);
            }
            e.printStackTrace();
            return failReturn(e.getMessage());
        }
    }

public class WXPayUtils {

    public static final String merchantId = "服务商微信编号";

    public static final String serialNo = "序列号,在后台获取";

    public static final String privateKey = "-----BEGIN PRIVATE KEY-----\n"//私钥

    public static String shoppingreceiptsUrl="https://api.mch.weixin.qq.com/v3/marketing/shopping-receipt/shoppingreceipts";

    public static CloseableHttpClient getWxHttpClient(String merchantId) throws IOException {
        //证书序列号
        String merchantSerialNumber = serialNo;
        //String apiFileName = wxPay.getApiFileName();
        //私钥
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
                new ByteArrayInputStream(privateKey.getBytes("utf-8")));
        List certificates1 = new ArrayList<>();
        //下载的微信支付平台证书列表,我是提前下载好的。
        //String wxFileName = wxPay.getWxFileName();
        X509Certificate certificate = getCertificate("证书地址");
        certificates1.add(certificate);
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
                .withValidator(closeableHttpResponse -> true)
                .withWechatPay(certificates1);

        CloseableHttpClient httpClient = builder.build();
        return httpClient;
    }

    public static X509Certificate getCertificate(String filename) throws IOException {
        InputStream fis = new FileInputStream(filename);
        BufferedInputStream bis = new BufferedInputStream(fis);

        try {
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            X509Certificate cert = (X509Certificate) cf.generateCertificate(bis);
            cert.checkValidity();
            return cert;
        } catch (CertificateExpiredException e) {
            throw new RuntimeException("证书已过期", e);
        } catch (CertificateNotYetValidException e) {
            throw new RuntimeException("证书尚未生效", e);
        } catch (CertificateException e) {
            throw new RuntimeException("无效的证书文件", e);
        } finally {
            bis.close();
        }
    }

}

其实接口调用之后有个应答机制,但是此处接口调用不涉及到微信支付,可以捕获异常之后返回成功,电子小票可以正常生成,不需要做应答,当然做好应答机制肯定是更好的

-------------------------------------------------------------分割线-----------------------------------------------------------

在做文件上传的时候有小伙伴遇到这个问题

提示:图片不是png格式,请检查后重试

应该是文件类型错误,传的图片不规范,不是png格式的,有可能是jpg格式的你给改了文件类型,导致文件判断出错,建议取一个本身就是png格式的图片进行上传

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值