微信支付商家券 领券事件回调通知API

1.  商家券 领券事件回调通知API 接口编写

微信支付接口文档:微信支付-开发者文档

1.1 需要创建的 model 

1)先创建一个model 来接收微信传过来的参数

这里各个字段的顺序不能错乱!!和微信支付给的“通知参数”顺序保持一致!因为后面验签的时候会用到body里面的这些参数,顺序错了验签就会失败!

import lombok.Data;


@Data
public class TicketGiveWxDto {
    /**
     * 通知ID
     */
    private String id;

    /**
     * 通知创建时间
     */
    private String create_time;

    /**
     * 通知数据类型
     */
    private String resource_type;

    /**
     * 通知类型
     */
    private String event_type;

    /**
     * 回调摘要
     */
    private String summary;

    /**
     * 通知数据
     */
    private Object resource;

}

2)创建一个model来接收解密后的数据

@Data
public class TicketGiveInfo {
    /**
     * 事件类型
     * 枚举值:EVENT_TYPE_BUSICOUPON_SEND:商家券用户领券通知
     */
    private String event_type;
    /**
     * 券code 券的唯一标识
     */
    private String coupon_code;
    /**
     * 批次号 微信为每个商家券批次分配的唯一ID
     */
    private String stock_id;
    /**
     * 发放时间 为yyyy-MM-DDTHH:mm:ss+TIMEZONE
     */
    private String send_time;
    /**
     * 用户标识 微信用户在appid下的唯一标识。
     */
    private String openid;
    /**
     * 用户统一标识 微信用户在同一个微信开放平台账号下的唯一用户标识
     */
    private String unionid;
    /**
     * 发放渠道,枚举值:
     * BUSICOUPON_SEND_CHANNEL_MINIAPP:小程序
     * BUSICOUPON_SEND_CHANNEL_API:API
     * BUSICOUPON_SEND_CHANNEL_PAYGIFT:支付有礼
     * BUSICOUPON_SEND_CHANNEL_H5:H5
     * BUSICOUPON_SEND_CHANNEL_FTOF:面对面
     * BUSICOUPON_SEND_CHANNEL_MEMBERCARD_ACT:会员卡活动
     * BUSICOUPON_SEND_CHANNEL_HALL:扫码领券(营销馆)
     * BUSICOUPON_SEND_CHANNEL_JSAPI:JSAPI
     * BUSICOUPON_SEND_CHANNEL_MINI_APP_LIVE:微信小程序直播
     * BUSICOUPON_SEND_CHANNEL_WECHAT_SEARCH:搜一搜
     * BUSICOUPON_SEND_CHANNEL_PAY_HAS_DISCOUNT:微信支付有优惠
     * BUSICOUPON_SEND_CHANNEL_WECHAT_AD:微信广告
     * BUSICOUPON_SEND_CHANNEL_RIGHTS_PLATFORM:权益平台
     * BUSICOUPON_SEND_CHANNEL_RECEIVE_MONEY_GIFT:收款有礼
     * BUSICOUPON_SEND_CHANNEL_MEMBER_PAY_RIGHT:会员付费权益
     * BUSICOUPON_SEND_CHANNEL_BUSI_SMART_RETAIL:智慧零售活动
     * BUSICOUPON_SEND_CHANNEL_FINDER_LIVEROOM:视频号直播
     */
    private String send_channel;
    /**
     * 发券商户号
     */
    private String send_merchant;
    /**
     * 发券附加信息 仅在支付有礼、扫码领券(营销馆)、会员有礼发放渠道,才有该信息
     */
    private Object attach_info;
}

1.2 controller 中接口编写

import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import retail.mps.gateway.dto.TicketGiveWxDto;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import org.springframework.stereotype.Service;
import retail.mps.gateway.common.wxverify.WxPayUtil;
import retail.mps.gateway.dto.TicketGiveInfo;
import com.alibaba.fastjson.JSON;

@Slf4j
@RestController
public class Controller {


    @RequestMapping("/notify")
    public void notify(@RequestBody TicketGiveWxDto body, HttpServletRequest request, HttpServletResponse response) throws IOException {
        log.info("领券事件回调通知API,传参:{}", body);

        response.setContentType("text/html; charset=UTF-8");
        HashMap<String, String> resMap = new HashMap<>();

        //接收请求头中各个参数
        String serial = request.getHeader("Wechatpay-Serial"); //平台证书序列号
        String signature = request.getHeader("Wechatpay-Signature"); //签名
        String timestamp = request.getHeader("Wechatpay-Timestamp"); //应答时间戳
        String nonce = request.getHeader("Wechatpay-Nonce"); //应答随机串
        log.info("领券事件回调通知API,请求头参数, serial:{}, signature:{}, timestamp:{}, nonce:{}", serial, signature, timestamp, nonce);

        try {
            ticketNotify1(JSONUtil.toJsonStr(body), serial, signature, timestamp, nonce);
            //微信那边要求 接收成功时HTTP应答状态码需返回200或204,无需返回应答报文。
            response.setStatus(HttpServletResponse.SC_NO_CONTENT);
        } catch (Exception e) {
            //接收失败:HTTP应答状态码需返回5XX或4XX,同时需返回应答报文
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            log.info("领券事件回调通知API,操作失败:", e);
            resMap.put("code", "FAIL");
            resMap.put("message", e.getMessage());
            response.getWriter().write(JSONUtil.toJsonStr(resMap));
        }
    }


    /**
     * @param body      业务传参
     * @param serial    微信平台证书序列号
     * @param signature 签名
     * @param timestamp 应答时间戳
     * @param nonce     应答随机串
     * @return
     */
    public void notify1(String body, String serial, String signature, String timestamp, String nonce) throws Exception {
        //用商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,记为key。
        String apiV3Key = "hghhaksdgjkaeugiqaernbgvjdsfnbvjf";
        //微信平台证书
        String platformCertPem = "-----BEGIN CERTIFICATE-----\n" +
                "MIID3DDSCDSFBgAwIGAgIUGs/IfJAH2eOZifHEZk9BZswVdgfkjkaDQYJKoZIhvcNAQEL\n" +
                "vakjdhgkajdfjBgkajrdg+PCUQcgfvuabVgnvagjaierjg8ueDFSKGKKSJGhjhgjh\n" +
                "tbPOucphW4zn99aO15Yn63+rdgiuqaingfvPbU0GOEbnJddY1m/zrH6qHwcOcSH\n" +
                "g9DizqnZ9IhvDcNAQJhk2+0Z4NL8yCZa5W01eJB4rdsO5bEDSCSDCRFFASDFGru+g=\n" +
                "-----END CERTIFICATE-----";

        //1. 验证签名及解密数据
        String data = WxPayUtil.verifyNotify(body, serial, signature, nonce, timestamp, apiV3Key, platformCertPem);
        log.info("领券事件回调通知API,解密后数据:{}", data);

        TicketGiveInfo ticketGiveInfo = JSON.parseObject(data, TicketGiveInfo.class);

        //2. 处理业务逻辑
        updateStatus(ticketGiveInfo);
    }
}
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.*;
import java.util.Base64;


@Slf4j
public class WxPayUtil {
    /**
     * 支付异步通知验证签名
     *
     * @param body            异步通知密文
     * @param serialNo        证书序列号
     * @param signature       签名
     * @param nonce           随机字符串
     * @param timestamp       时间戳
     * @param key             api 密钥
     * @param platformCertPem 平台证书
     * @return 异步通知明文
     * @throws Exception 异常信息
     */
    public static String verifyNotify(String body, String serialNo, String signature, String nonce, String timestamp,
                                      String key, String platformCertPem) throws Exception {
        // 获取平台证书
        X509Certificate certificate = getCertificate(platformCertPem);

        String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
        // 验证证书序列号是否一致
        if (serialNumber.equals(serialNo)) {
            //验证签名
            boolean verifySignature = verifySignature(signature, body, nonce, timestamp, certificate);
            if (verifySignature) {
                //签名正确之后解密传参数据
                JSONObject resultObject = JSONUtil.parseObj(body);
                JSONObject resource = resultObject.getJSONObject("resource");

                String cipherText = resource.getStr("ciphertext");
                String nonceStr = resource.getStr("nonce");
                String associatedData = resource.getStr("associated_data");

                AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8));
                // 密文解密
                return aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonceStr.getBytes(StandardCharsets.UTF_8), cipherText);
            } else {
                throw new Exception("签名错误");
            }
        } else {
            throw new Exception("证书序列号错误");
        }
    }


    /**
     * @param signature   待验证的签名
     * @param body        应答主体
     * @param nonce       随机串
     * @param timestamp   时间戳
     * @param certificate 平台证书
     * @return 签名验证结果
     */
    public static boolean verifySignature(String signature, String body, String nonce, String timestamp, X509Certificate certificate) throws Exception {
        String message = timestamp + "\n" + nonce + "\n" + (body == null ? "" : body) + "\n";
        return verify(certificate, message, signature);
    }

    private static boolean verify(X509Certificate certificate, String message, String signature) throws Exception {
        try {
            Signature sign = Signature.getInstance("SHA256WithRSA");
            sign.initVerify(certificate);
            sign.update(message.getBytes(StandardCharsets.UTF_8));
            return sign.verify(Base64.getDecoder().decode(signature));
        } catch (SignatureException e) {
            return false;
        } catch (InvalidKeyException e) {
            throw new Exception("验证使用非法证书", e);
        } catch (NoSuchAlgorithmException e) {
            throw new UnsupportedOperationException("The current Java environment does not support SHA256WithRSA", e);
        }
    }

    /**
     * 获取证书
     *
     * @param platformCertPem 证书文件
     * @return {@link X509Certificate} 获取证书
     */
    private static X509Certificate getCertificate(String platformCertPem) throws Exception {
        try {
            Security.addProvider(new BouncyCastleProvider());
            CertificateFactory cf = CertificateFactory.getInstance("X.509", new BouncyCastleProvider());
            InputStream inputStream = new ByteArrayInputStream(platformCertPem.getBytes());
            X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
            cert.checkValidity();
            return cert;
        } catch (CertificateExpiredException e) {
            throw new Exception("证书已过期", e);
        } catch (CertificateNotYetValidException e) {
            throw new Exception("证书尚未生效", e);
        } catch (CertificateException e) {
            throw new Exception("无效的证书", e);
        }
    }
    
}
import cn.hutool.core.codec.Base64;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class AesUtil {
    static final int KEY_LENGTH_BYTE = 32;
    static final int TAG_LENGTH_BIT = 128;
    private final byte[] aesKey;
    /**
     * @param key APIv3 密钥
     */
    public AesUtil(byte[] key) {
        if (key.length != KEY_LENGTH_BYTE) {
            throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
        }
        this.aesKey = key;
    }

    /**
     * 证书和回调报文解密
     *
     * @param associatedData associated_data
     * @param nonce          nonce
     * @param cipherText     ciphertext
     * @return {String} 平台证书明文
     * @throws GeneralSecurityException 异常
     */
    public String decryptToString(byte[] associatedData, byte[] nonce, String cipherText) throws GeneralSecurityException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);

            return new String(cipher.doFinal(Base64.decode(cipherText)), StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

2. H5发券API - 测试接口编写

微信支付接口文档:微信支付-开发者文档

import cn.wonhigh.retail.mps.common.utils.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletResponse;
import java.util.*;


/**
 * @Description 测试微信领券
 */
@Slf4j
@RestController
public class Test {
    /**
     *
     * @param stock_id 批次号
     * @param openId 微信openId
     * @param response
     * @throws Exception
     */
    @GetMapping("/giveTicketTest")
    public void giveTicketTest(String stock_id, String openId, HttpServletResponse response) throws Exception {
        //发券凭证
        String out_request_no = "HH" + DateUtil.getCurrentDateTime2Str2();
        //发券商户号
        String send_coupon_merchant = "6548163121564";
        //自己小程序的appKey
        String key = "adghuiqyaeuigbnkasgjohgl";

        Map<String, String> params = new HashMap<>();
        params.put("stock_id", stock_id);
        params.put("out_request_no", out_request_no);
        params.put("send_coupon_merchant", send_coupon_merchant);
        params.put("open_id", openId);

        String sign = getHMACSHA256Sign(params, key);
        log.info("HMAC-SHA256签名结果:{}", sign);

        //重定向微信接口
        String url = "https://action.weixin.qq.com/busifavor/getcouponinfo?" + "stock_id=" + stock_id + "&out_request_no="
                + out_request_no + "&sign=" + sign + "&send_coupon_merchant=" + send_coupon_merchant + "&open_id=" + openId + "#wechat_redirect";
        log.info("重定向微信接口:{}", url);

        response.setHeader("Location", url);
        response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);

    }

    /**
     * 生成 HMAC-SHA256 签名
     *
     * @param params 待签名参数集合
     * @param key    密钥
     * @return 签名字符串
     */
    public static String getHMACSHA256Sign(Map<String, String> params, String key) throws Exception {
        // 将集合内的非空参数值按照键名的字典顺序排序
        List<String> keys = new ArrayList<String>(params.keySet());
        Collections.sort(keys);
        StringBuilder sb = new StringBuilder();
        for (String k : keys) {
            // 对于空值或者 sign 参数,不进行签名
            if (params.get(k) != null && !"".equals(params.get(k)) && !"sign".equals(k)) {
                sb.append(k).append("=").append(params.get(k)).append("&");
            }
        }
        sb.append("key=").append(key);
        // 使用 HMAC-SHA256 加密算法对最终字符串进行签名
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "HmacSHA256");
        mac.init(secretKeySpec);
        byte[] signBytes = mac.doFinal(sb.toString().getBytes());
        // 将字节数组转为16进制字符串
        StringBuilder hexStrBuilder = new StringBuilder();
        for (byte b : signBytes) {
            String hexStr = Integer.toHexString(0xFF & b);
            if (hexStr.length() == 1) {
                hexStrBuilder.append("0");
            }
            hexStrBuilder.append(hexStr);
        }
        return hexStrBuilder.toString().toUpperCase();
    }
}

3. 商家券 根据过滤条件查询用户券API 接口编写

微信支付接口文档:微信支付-开发者文档

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import java.util.Base64;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;


@Slf4j
@RestController
public class Test {

    //API 证书中的 key.pem
    private final String API_KEY_PEM = "-----BEGIN PRIVATE KEY-----\n" +
            "MIIEvAIBADANBgkqsdgfafyjnAQEFAASCBKYwggSiAgCj5t87QSdsvz4oz60\n" +
            "D3fSGEvshyjvadfVAsAxRWkiJSaX4cdybcOHexfhgngdhmZgKPhFfcPvfsdhaccNWZ\n" +
            "LupysXWZvdfJdthdbss+N0M9gm4tANihqPcSDGvethbSDWqwwahytyi79iibnWqqeceO4\n" +
            "KGKGSfYcWxjxfheH540/lYrEAfAoIBAQIwkYNd618ascsdhlslnhkSe0/Trg5ssp1b\n" +
            "oodgW/neN2dsgYQtCjacIBMxfaweD4Yw==\n" +
            "-----END PRIVATE KEY-----\n";

    /**
     * @param openId
     * @param stock_id 批次号
     * @return
     * @throws Exception
     */
    @GetMapping("/getGiveTicketRes")
    public ArrayList getGiveTicketRes(String openId, String stock_id) throws Exception {
        String appid = "wxadgjahgag7ag";
        long currentTimeMillis = System.currentTimeMillis() / 1000;
        String serial_no = "5FASGARGMKAGB4G98ARGAEEG4A5R7H98E"; //API证书序列号
        String mchid = "6548163121564"; //商户号

        String url = "/v3/marketing/busifavor/users/" + openId + "/coupons?appid=" + appid + "&stock_id=" + stock_id + "&creator_merchant=" + mchid;
        log.info("url:{}", url);

        String nonce = "593BEC0C930BF1AFEB40B4A08C8FB242";

        String data = "GET" + "\n"
                + url + "\n"
                + currentTimeMillis + "\n"
                + nonce + "\n"
                + "\n";

        String signature = getSignString(data, API_KEY_PEM);
        log.info("签名结果:{}", signature);

        String token = "mchid=\"" + mchid + "\","
                + "nonce_str=\"" + nonce + "\","
                + "timestamp=\"" + currentTimeMillis + "\","
                + "serial_no=\"" + serial_no + "\","
                + "signature=\"" + signature + "\"";

        String authorization = "WECHATPAY2-SHA256-RSA2048 " + token;

        String httpurl = "https://api.mch.weixin.qq.com" + url;
        ArrayList res = sendHttp(httpurl, authorization);
        return res;
    }

    private ArrayList sendHttp(String url, String authorization) {
        OkHttpClient client = new OkHttpClient();

        // 创建请求对象,并设置请求头
        Request request = new Request.Builder()
                .url(url)
                .header("Accept", "application/json")
                .addHeader("Authorization", authorization)
                .build();

        try {
            // 发送请求并获取响应
            Response response = client.newCall(request).execute();
            ResponseBody responseBody = response.body();

            if (responseBody != null) {
                String responseData = responseBody.string();
                log.info("Response: {}", responseData);

                // 解析返回值中的数组数据
                JSONObject jsonObject = new JSONObject(responseData);
                JSONArray dataArray = jsonObject.getJSONArray("data");

                ArrayList dataObj = JSON.parseObject(String.valueOf(dataArray), ArrayList.class);

                log.info("结果返回data:" + dataObj);
                return dataObj;

            }
            // 关闭响应
            response.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static String getSignString(String data, String privateKeyString) throws Exception {
        String strippedPrivateKeyString = privateKeyString.replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", "");
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = kf.generatePrivate(
                new PKCS8EncodedKeySpec(Base64.getDecoder().decode(strippedPrivateKeyString)));

        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(signature.sign());
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值