赞
踩
电子小票与纸质小票都是线下购物的有效凭证,是纸质小票的电子化形态。电子小票易于存储、查看,便于消费追溯、获取售后服务等。
目前微信官方推出电子小票功能方案分为两种
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/png
或image/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格式的图片进行上传
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。