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());
}
}