【微信小程序开发】用户一键登录 + 获取手机号 + 微信支付V3
一、技术点摘要
- 基于Springboot搭建微信小程序后端框架
- 获取并缓存微信api调用凭证AccessToken
- 封装微信一键登录、获取手机号API
- 封装微信支付客户端
- 封装函数式接口,统一捕获微信支付异常,减少重复代码
二、微信支付pom依赖
<!-- 微信支付 -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.11</version>
</dependency>
三、微信相关配置
3.1 配置文件
ai-wxapp:
wechat:
# 应用id(小程序id)
appid: xxxxx
# 应用秘钥(小程序秘钥)
secret: xxxxxx
wechat-pay:
# 商户号
merchantId: xxxxxx
# 商户API私钥路径
privateKeyPath: D:\\Develop\\Wechat\\apiclient_key.pem
# 商户证书序列号
merchantSerialNumber: xxxxxx
# 商户APIv3密钥
apiV3Key: xxxxxx
# 支付成功回调url,可公网https访问的
payNotifyUrl: https://xxxxxx/wx/pay/payCallback
# 退款成功回调url,可公网https访问的
refundNotifyUrl: xxx
3.2 配置类
@Component
@ConfigurationProperties(prefix = "ai-wxapp.wechat")
@Data
public class WechatAppConfig {
private String appid;
private String secret;
}
@Data
@Component
@ConfigurationProperties(prefix = "ai-wxapp.wechat-pay")
public class WechatPayConfig {
/**
* 商户id
*/
private String merchantId;
/**
* 商户API私钥
*/
private String privateKeyPath;
/**
* 商户证书序列号
*/
private String merchantSerialNumber;
/**
* 商户APIv3密钥
*/
private String apiV3Key;
/**
* 支付成功回调url,可公网https访问的
*/
private String payNotifyUrl;
/**
* 退款成功回调url,可公网https访问的
*/
private String refundNotifyUrl;
}
四、微信一键登录+获取手机号
4.1 缓存微信api调用凭证:AccessToken
微信开放文档:获取接口调用凭据
@Slf4j
@Component
public class WechatAccessTokenService {
@Resource
private RedisUtils redisUtils;
@Resource
private WechatAppConfig wechatConfig;
private final String getAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token";
private final String access_token = "access_token";
private final String expires_in = "expires_in";
/**
* 获取微信api调用凭证,可用于后续获取手机号等
*
* @return AccessToken
* @author shn 2024/05/24 12:33
*/
@Nullable
public String getAccessToken() {
String key = RedisKeys.WX_ACCESS_TOKEN;
// 先查缓存
if (redisUtils.hasKey(key) && redisUtils.getExpire(key) > 0L) {
return redisUtils.get(key).toString();
}
// 缓存中没有,则调用微信接口获取
Map<String, String> params = new HashMap<>();
params.put("appid", wechatConfig.getAppid());
params.put("secret", wechatConfig.getSecret());
params.put("grant_type", "client_credential");
String response = OkHttpUtil.httpGet(getAccessTokenUrl, null, params);
if (StringUtils.isBlank(response)) {
log.error("调用微信API获取AccessToken失败");
return null;
}
JSONObject jsonObject = JSON.parseObject(response);
if (StringUtils.isBlank(jsonObject.getString(access_token))) {
return null;
}
// 凭证有效时间,单位:秒。目前是7200秒之内的值。
int expiresIn = jsonObject.getIntValue(expires_in);
// 缓存并设置过期时间,设置为提前30秒过期
redisUtils.set(key, jsonObject.getString(access_token), expiresIn - 30, TimeUnit.SECONDS);
return jsonObject.getString(access_token);
}
}
4.2 微信一键登录 + 获取手机号
@Slf4j
@Component
public class WechatAuthApiService {
@Resource
private WechatAppConfig wechatConfig;
@Resource
private WechatAccessTokenService accessTokenService;
private final String code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session";
private final String session_key = "session_key";
private final String unionid = "unionid";
private final String openid = "openid";
private final String getPhoneNumberUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s";
private final String error_code = "errcode";
private final String phone_info = "phone_info";
private final String purePhoneNumber = "purePhoneNumber";
/**
* 微信登录
*
* @param code 前端调用wx.login获取到的code
* @return com.shunxi.ai.common.sdk.weixin.WechatSessionEntity
* @author shn 2024/05/24 12:32
*/
@Nullable
public WechatAuthEntity code2Session(String code) {
Map<String, String> params = new HashMap<>();
params.put("appid", wechatConfig.getAppid());
params.put("secret", wechatConfig.getSecret());
params.put("js_code", code);
params.put("grant_type", "authorization_code");
String response = OkHttpUtil.httpGet(code2SessionUrl, null, params);
if (StringUtils.isBlank(response)) {
return null;
}
JSONObject jsonObject = JSON.parseObject(response);
if (StringUtils.isBlank(jsonObject.getString(session_key))) {
return null;
}
return WechatAuthEntity.builder()
.sessionKey(jsonObject.getString(session_key))
.unionId(jsonObject.getString(unionid))
.openId(jsonObject.getString(openid))
.build();
}
/**
* 获取用户手机号
*
* @param code 手机号获取凭证,这个code和wx.login获取到的code作用不一样,不能混用!!!
* @return 用户手机号
* @author shn 2024/05/24 12:34
*/
@Nullable
public String getPhoneNumber(String code) {
String accessToken = accessTokenService.getAccessToken();
if (StringUtils.isBlank(accessToken)) {
return null;
}
String url = String.format(getPhoneNumberUrl, accessToken);
String response = OkHttpUtil.postJson(url, null, Collections.singletonMap("code", code));
if (StringUtils.isBlank(response)) {
return null;
}
JSONObject jsonObject = JSON.parseObject(response);
if (jsonObject.getIntValue(error_code) != 0) {
return null;
}
return jsonObject.getJSONObject(phone_info).getString(purePhoneNumber);
}
}
五、微信支付
微信支付开放文档:微信支付
微信支付V3版本sdk:wechatpay-apiv3
5.1 微信支付Service封装
PaySuccessParamEntity:
/**
* @ClassName PaySuccessParamEntity
* @Description 支付成功回调参数实体
* @Author shn
* @Date 2024/6/21 18:36
* @Version 1.0
*/
@Data
public class PaySuccessParamEntity {
private String orderId;
private LocalDateTime paidTime;
private String wxTransactionId;
}
WechatPayService:
- 封装微信支付客户端
- 封装函数式接口,对调用微信支付相关异常,统一捕获处理
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.shunxi.ai.wxapp.common.config.WechatPayConfig;
import com.shunxi.ai.wxapp.common.exception.ServiceException;
import com.shunxi.ai.wxapp.domain.wechat.model.entity.PaySuccessParamEntity;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* 微信支付服务
*
* @author shn 2024/05/28 14:05
*/
@Slf4j
@Service
public class WechatPayService {
private static final int KEY_LENGTH_BYTE = 32;
private static final int TAG_LENGTH_BIT = 128;
@Resource
private WechatPayConfig wechatPayConfig;
private volatile JsapiServiceExtension jsapiServiceExtension;
private volatile Config config;
/**
* 初始化微信Config
*
* @return com.wechat.pay.java.core.Config
* @author shn 2024/05/28 14:06
*/
private Config getConfig() {
// 懒加载创建单例的微信支付Config对象
if (config == null) {
synchronized (this) {
if (config == null) {
config = new RSAAutoCertificateConfig.Builder()
.merchantId(Objects.requireNonNull(
wechatPayConfig.getMerchantId(), "【商户号】不能为空"))
.privateKeyFromPath(Objects.requireNonNull(
wechatPayConfig.getPrivateKeyPath(), "【商户API私钥文件存放路径】不能为空"))
.merchantSerialNumber(Objects.requireNonNull(
wechatPayConfig.getMerchantSerialNumber(), "【商户证书序列号】不能为空"))
.apiV3Key(Objects.requireNonNull(
wechatPayConfig.getApiV3Key(), "【商户API V3秘钥】不能为空"))
.build();
}
}
}
return config;
}
/**
* 微信支付-小程序客户端
*
* @return com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension
* @author shn 2024/05/28 14:06
*/
public JsapiServiceExtension getJsapiServiceExtension() {
// 懒加载创建单例的小程序支付客户端
if (jsapiServiceExtension == null) {
synchronized (this) {
if (jsapiServiceExtension == null) {
jsapiServiceExtension = new JsapiServiceExtension.Builder()
.config(this.getConfig())
.signType("RSA")
.build();
}
}
}
return jsapiServiceExtension;
}
/**
* 封装函数式接口,对调用微信支付相关异常,统一捕获处理
*/
public <T> T payExecute(Supplier<T> supplier) {
try {
T t = supplier.get();
log.info("【调用微信接口】返回结果: {}", JSON.toJSONString(t));
return t;
} catch (HttpException e) {
// 发送HTTP请求失败
log.error("HttpException:{}", e.getMessage());
throw new ServiceException(e.getMessage());
} catch (com.wechat.pay.java.core.exception.ServiceException e) {
// 服务返回状态小于200或大于等于300,例如500
log.error("ServiceException:{}", e.getResponseBody());
throw new ServiceException(e.getResponseBody());
} catch (MalformedMessageException e) {
// 服务返回成功,返回体类型不合法,或者解析返回体失败
log.error("MalformedMessageException:{}", e.getMessage());
throw new ServiceException(e.getMessage());
}
}
/**
* 根据支付回调结果,执行相应业务逻辑
*/
public Boolean payCallbackExecute(JSONObject jsonObject,
Consumer<PaySuccessParamEntity> consumer) {
log.info("【微信支付回调】回调参数:{}", jsonObject.toJSONString());
String orderId = "";
try {
// 解密
String decryptData = this.decrypt2String(jsonObject);
if (StringUtils.isBlank(decryptData)) {
return Boolean.FALSE;
}
// 处理后续业务逻辑
JSONObject dataJsonObj = JSONObject.parseObject(decryptData, JSONObject.class);
if ("SUCCESS".equalsIgnoreCase(dataJsonObj.getString("trade_state"))) {
// out_trade_no:商户订单号,是我们自己系统生成的订单号,下单时传给微信的。
orderId = dataJsonObj.getString("out_trade_no");
log.info("【微信支付回调】商户订单号: {},支付成功,开始执行业务逻辑", orderId);
try {
PaySuccessParamEntity entity = new PaySuccessParamEntity();
entity.setOrderId(orderId);
// 支付时间
String successTime = dataJsonObj.getString("success_time");
entity.setPaidTime(OffsetDateTime
.parse(successTime, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
.toLocalDateTime());
// 微信交易流水号
entity.setWxTransactionId(dataJsonObj.getString("transaction_id"));
consumer.accept(entity);
} catch (Exception e) {
log.error("【微信支付回调】商户订单号: {},支付成功,执行业务逻辑异常", orderId, e);
return Boolean.FALSE;
}
}
} catch (Exception e) {
log.error("【微信支付回调】解析回调参数异常", e);
return Boolean.FALSE;
}
log.info("【微信支付回调】商户订单号:{},回调成功,业务逻辑执行完成! ", orderId);
return Boolean.TRUE;
}
/**
* 支付通知API文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml">...</a>
* <p>
* 1. 使用AEAD_AES_256_GCM算法,取得对应的参数nonce和associated_data
* <p>
* 2. 使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象
*/
private String decrypt2String(JSONObject jsonObject) {
try {
cn.hutool.json.JSON json = JSONUtil.parse(jsonObject.toString());
String ciphertext = (String) JSONUtil.getByPath(json, "resource.ciphertext");
String associatedData = (String) JSONUtil.getByPath(json, "resource.associated_data");
String nonce = (String) JSONUtil.getByPath(json, "resource.nonce");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(
wechatPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8), "AES");
cipher.init(Cipher.DECRYPT_MODE, key,
new GCMParameterSpec(TAG_LENGTH_BIT, nonce.getBytes(StandardCharsets.UTF_8)));
cipher.updateAAD(
associatedData.getBytes(StandardCharsets.UTF_8));
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("【微信支付回调】参数解密失败", e);
}
return null;
}
}
5.2 创建微信预支付订单 + 支付回调
@Slf4j
@RestController
@RequestMapping("/wx/pay")
@Api(value = "微信支付相关接口", tags = "微信支付相关接口")
public class WxPayController {
@Resource
private WechatAppConfig wechatAppConfig;
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayService wechatPayService;
@Resource
private IOrderService orderService;
@WebLog
@ApiOperation("创建微信预支付订单,返回预支付信息")
@PostMapping("/createWxPrepareOrder")
public Result<PrepayWithRequestPaymentResponse>
createWxPrepareOrder(@Valid @RequestBody CreateWxPrepareOrderRequestDTO param) {
// 查询订单信息
OrdersEntity entity = orderService.getByOrderId(param.getOrderId());
// 调用微信预下单接口
JsapiServiceExtension service = wechatPayService.getJsapiServiceExtension();
PrepayRequest prepayRequest = new PrepayRequest();
// 商品描述
prepayRequest.setDescription("订单支付");
// 小程序id
prepayRequest.setAppid(wechatAppConfig.getAppid());
// 商户id
prepayRequest.setMchid(wechatPayConfig.getMerchantId());
// 商户订单号
prepayRequest.setOutTradeNo(entity.getOrderId());
// 订单失效时间
String timeExpire = buildTimeExpire(entity.getOrderId(), entity.getOrderTime());
prepayRequest.setTimeExpire(timeExpire);
// 支付回调接口
prepayRequest.setNotifyUrl(wechatPayConfig.getPayNotifyUrl());
// 支付金额,单位为分
Amount amount = new Amount();
amount.setCurrency("CNY");
amount.setTotal(entity.getTotalAmount().multiply(new BigDecimal(100)).intValue());
prepayRequest.setAmount(amount);
// 支付人
Payer payer = new Payer();
payer.setOpenid(entity.getOpenId());
prepayRequest.setPayer(payer);
PrepayWithRequestPaymentResponse response = wechatPayService.payExecute(() ->
service.prepayWithRequestPayment(prepayRequest));
return Result.ok(response, "操作成功");
}
@WebLog
@ApiOperation("支付回调接口")
@PostMapping("/payCallback")
@ApiIgnore
public Map<String, String> payCallback(@RequestBody JSONObject jsonObject) {
// 执行回调逻辑
Boolean success = wechatPayService
.payCallbackExecute(jsonObject, entity -> orderService.paySuccess(entity));
// 给微信返回结果
Map<String, String> result = new HashMap<>();
result.put("code", success ? "SUCCESS" : "FAIL");
result.put("message", success ? "成功" : "失败");
return result;
}
/**
* 构建订单失效时间(下单2小时内有效)
* 示例值:2018-06-08T10:34:56+08:00
*/
private static String buildTimeExpire(String orderId, LocalDateTime orderTime) {
String timeExpire = orderTime.plusHours(2)
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.split("\\.")[0] + ZoneOffset.ofHours(8).getId();
log.info("创建微信预支付订单,订单id: {} 订单失效时间: {}", orderId, timeExpire);
return timeExpire;
}
}