官网和文档
https://pay.weixin.qq.com/docs/merchant/development/interface-rules/introduction.html
SDK
https://github.com/wechatpay-apiv3
授权登录
获取手机号和openId,便于支付
<!-- 登录 -->
<u-popup :show="login.show" mode="center" :closeOnClickOverlay="false" customStyle="margin: 0 40rpx" :round="4">
<view class="login-box">
<view class="login-title">登录提醒</view>
<view class="login-content">为了给您提供更好的服务,请先进行授权登录,绑定您的个人数据,同时请认真阅读《用户协议》和《隐私协议》</view>
<view class="login-option">
<button @click="login.show = false">取消</button>
<button open-type="getPhoneNumber" @getphonenumber="phonenumber">登录</button>
</view>
</view>
</u-popup>
// 登录
phonenumber(e) {
// 获取登录令牌获取openId
let that = this
wx.login({
success(res) {
console.log(res);
if (res.code) {
httpUtil('/authorization', 'POST', {
code: e.detail.code,
openIdCode: res.code
}, (res) => {
if (res.code == 200) {
uni.setStorageSync('token', res.data.token)
uni.setStorageSync('userId', res.data.userId)
uni.setStorageSync('phone', res.data.phone)
uni.setStorageSync('createTime', res.data.createTime)
uni.setStorageSync('photo', res.data.photo)
that.login.show = false
uni.reLaunch({
url: '/pages/home/home'
})
} else {
that.$refs.uToast.show({
message: res.msg,
})
}
}, (err) => {
that.$refs.uToast.show({
message: '服务异常',
})
})
} else {
console.log('授权失败!' + res.errMsg)
}
}
})
},
后端处理
Override
public Result<AuthorizationRespVo> authorization(AuthorizationReqVo params) {
// 获取手机号
String wxToken = getWxToken();
WxGetPhoneVo phone = getPhone(wxToken, params.getCode());
Criteria criteria = new Criteria();
criteria.and(FieldUtil.get(User::getPhone)).is(phone.getPhone());
criteria.and(FieldUtil.get(User::getStatus)).is(GlobalStatusEnum.STATUS_NORMAL.value());
// TODO 拉黑人员处理
User user = mongoTemplate.findOne(new Query(criteria), User.class);
if (Objects.isNull(user)) {
WxCode2SessionVo wxCode2SessionVo = openId(params.getOpenIdCode());
// 没有则注册
user = new User();
user.setPhone(phone.getPhone());
user.setOpenId(wxCode2SessionVo.getOpenid());
user.setStatus(GlobalStatusEnum.STATUS_NORMAL.value());
user.setCreateTime(new Date());
user.setUpdateTime(new Date());
mongoTemplate.save(user);
}
// 缓存用户信息对象
UserCacheVo userCacheVo = new UserCacheVo();
userCacheVo.setPhone(user.getPhone());
userCacheVo.setId(user.getId().toString());
String json;
try {
json = new ObjectMapper().writeValueAsString(userCacheVo);
} catch (JsonProcessingException e) {
log.info("登录成功后缓存对象转换异常:" + e);
throw new RuntimeException();
}
// 缓存登录信息
String token = new ObjectId().toString();
redisTemplate.opsForValue().set(RedisKeyEnum.KEY_USER_TOKEN.value() + token, json, 30, TimeUnit.DAYS);
// 返回信息
AuthorizationRespVo authorizationRespVo = new AuthorizationRespVo();
authorizationRespVo.setId(user.getId().toString());
authorizationRespVo.setToken(token);
authorizationRespVo.setPhone(user.getPhone());
authorizationRespVo.setCreateTime(user.getCreateTime());
authorizationRespVo.setPhoto(user.getPhoto());
authorizationRespVo.setUserId(user.getId().toString());
return Result.data("登录成功", authorizationRespVo);
}
/**
* 获取微信凭证
*/
private String getWxToken() {
// 优先从缓存读取
String accessToken = redisTemplate.opsForValue().get(RedisKeyEnum.KEY_WEIXIN_ACCESS_TOKEN.value());
if (Objects.isNull(accessToken)) {
String url = "https://api.weixin.qq.com/cgi-bin/stable_token";
JsonObject json = new JsonObject();
json.addProperty("grant_type", "client_credential");
json.addProperty("appid", "xxxx"); // appid
json.addProperty("secret", "xxxx"); // secret 从微信小程序开发管理中生成
log.debug(String.valueOf(json));
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
HttpEntity<String> formEntity = new HttpEntity<>(json.toString(), headers);
String sdResponse = restTemplate.postForEntity(url, formEntity, String.class).getBody();
log.debug(sdResponse);
if (Objects.isNull(sdResponse)) {
throw new RuntimeException("获取微信凭证失败");
}
accessToken = JsonParser.parseString(sdResponse).getAsJsonObject().get("access_token").getAsString();
log.debug(accessToken);
if (Objects.isNull(accessToken)) {
throw new RuntimeException("获取微信凭证无法解析Token");
}
// 缓存
redisTemplate.opsForValue().set(RedisKeyEnum.KEY_WEIXIN_ACCESS_TOKEN.value(), accessToken, 7000, TimeUnit.SECONDS);
}
return accessToken;
}
/**
* 根据动态令牌获取手机号
*/
private WxGetPhoneVo getPhone(String accessToken, String code) {
String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken;
JsonObject json = new JsonObject();
json.addProperty("code", code);
log.debug(String.valueOf(json));
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
HttpEntity<String> formEntity = new HttpEntity<>(json.toString(), headers);
String sdResponse = restTemplate.postForEntity(url, formEntity, String.class).getBody();
log.debug(sdResponse);
if (Objects.isNull(sdResponse)) {
throw new RuntimeException("根据code获取手机号失败");
}
JsonObject phoneInfo = JsonParser.parseString(sdResponse).getAsJsonObject().get("phone_info").getAsJsonObject();
// 手机号
String phoneNumber = phoneInfo.get("purePhoneNumber").getAsString();
// 国外手机号会有区号
// String countryCode = phoneInfo.get("countryCode").getAsString();
log.debug(phoneNumber);
if (Objects.isNull(phoneNumber)) {
throw new RuntimeException("根据code获取手机号无法解析");
}
// appId
JsonObject watermark = phoneInfo.get("watermark").getAsJsonObject();
String appid = watermark.get("appid").getAsString();
WxGetPhoneVo wxGetPhoneVo = new WxGetPhoneVo();
wxGetPhoneVo.setPhone(phoneNumber);
wxGetPhoneVo.setAppid(appid);
log.info("登录获取手机号:{}", wxGetPhoneVo);
return wxGetPhoneVo;
}
/**
* 从微信获取openid
*/
private WxCode2SessionVo openId(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session";
url += "?appid=xxxx";
url += "&secret=xxxx";
url += "&grant_type=authorization_code";
url += "&js_code=" + code;
log.info(url);
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(url, String.class);
log.info(response);
return ObjAndJson.toObj(response, WxCode2SessionVo.class);
}
支付
package com.code.impl;
import com.code.comEnum.GlobalStatusEnum;
import com.code.commonVo.Result;
import com.code.entity.User;
import com.code.service.PayService;
import com.code.utils.UserInfoUtil;
import com.code.vo.weixin.WxPayOrderReqVo;
import com.code.vo.weixin.WxPayOrderRespVo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.JsonParser;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.*;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.Base64;
import java.util.Date;
import java.util.Objects;
@Slf4j
@Service
public class PayServiceImpl implements PayService {
private String privateKey = "-----BEGIN PRIVATE KEY-----\n" +
"MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDT9pVo21NwRkSx\n" +
"9wHAVe80i2yeUZLJD4wI1Np/uCVYHGPXI+MyqxZgLVfbjXsLqCCFAkFS1e4ObA79\n" +
"......" +
"5xhcPP8DVt/HjnzIOsBJYv5Ah0JByGaHcPmPfDJyXIuic8pPGnlgK62uQN+ledei\n" +
"dJGJHxZQInP/6D3GkIIX25yBSQGZYtRuUXZyvVLIcwKBgQCkJwgRBRJZ6ILkh7lg\n" +
"ilIAepTG/bqBq8uIXKaGaSr8u+zZ+Q4pZgQfbDJeIbPtSRDntx8Npt7onE17idmF\n" +
"70+MTk9pfp/LAsWj4N4ToCN3c7aHQYDqSbVfuOiRyk02fl255pkQ4sp4JNMRSj14\n" +
"yK/SLWj7CnD9fLA4HzJuQMP6ZQ==\n" +
"-----END PRIVATE KEY-----";
private String mchId = "xxx";
private String mchSerialNo = "xxxx";
private String apiV3Key = "xxx";
private String appid = "xxxx";
@Resource
private MongoTemplate mongoTemplate;
@Override
public Result<WxPayOrderRespVo> wxPay(WxPayOrderReqVo params) {
try {
// 查找用户,获取appId
User user = mongoTemplate.findById(UserInfoUtil.userId(), User.class);
if (Objects.isNull(user) || !Objects.equals(user.getStatus(), GlobalStatusEnum.STATUS_NORMAL.value())) {
throw new RuntimeException("用户不存在");
}
// 加载商户私钥(privateKey:私钥字符串)
PrivateKey merchantPrivateKey = PemUtil
.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8)));
// 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(
mchId,
new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),
apiV3Key.getBytes(StandardCharsets.UTF_8)
);
// 初始化httpClient
HttpClient httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier)).build();
// 下单
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
// 请求参数
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("appid", appid)
.put("mchid", mchId)
.put("description", "会员购买")
.put("out_trade_no", new ObjectId().toString()) // 订单号
.put("notify_url", "https://fanmr.cn/test"); // 回调地址
rootNode.putObject("amount")
.put("total", 1);
rootNode.putObject("payer")
.put("openid", "oGeWs64ho4UzSGogobEoWfEySHaA");
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
HttpResponse execute = httpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(execute.getEntity());
log.info(bodyAsString);
String prepay_id = JsonParser.parseString(bodyAsString).getAsJsonObject().get("prepay_id").getAsString();
WxPayOrderRespVo wxPayOrderRespVo = new WxPayOrderRespVo();
wxPayOrderRespVo.setAppId(appid);
wxPayOrderRespVo.setPartnerid(mchId);
wxPayOrderRespVo.setPrepayId(prepay_id);
wxPayOrderRespVo.setNonceStr("c5sEwbaNPiXAF3iv");
wxPayOrderRespVo.setTimeStamp(String.valueOf(new Date().getTime() / 1000));
// 计算签名
String signStr = appid + "\n" + wxPayOrderRespVo.getTimeStamp()
+ "\n" + wxPayOrderRespVo.getNonceStr() + "\nprepay_id="
+ wxPayOrderRespVo.getPrepayId() + "\n";
wxPayOrderRespVo.setSign(sign(signStr.getBytes()));
return Result.data(wxPayOrderRespVo);
} catch (RuntimeException | IOException e) {
throw new RuntimeException(e);
}
}
/**
* 加载证书
*/
public PrivateKey privateKey() {
// 加载证书
byte[] bytes = privateKey.getBytes(StandardCharsets.UTF_8);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
return PemUtil.loadPrivateKey(byteArrayInputStream);
}
/**
* 小程序签名计算
*/
private String sign(byte[] message) {
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey());
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 签名验证器
*/
public Verifier verifier() throws NotFoundException, IOException, GeneralSecurityException, HttpCodeException {
CertificatesManager certificatesManager = CertificatesManager.getInstance();
certificatesManager.putMerchant(mchId, new WechatPay2Credentials(mchId,
new PrivateKeySigner(mchSerialNo, privateKey())), apiV3Key.getBytes(StandardCharsets.UTF_8));
return certificatesManager.getVerifier(mchId);
}
/**
* 定时更新平台证书功能
*/
public CloseableHttpClient getWxPayClient() throws GeneralSecurityException, NotFoundException, IOException, HttpCodeException {
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey())
.withValidator(new WechatPay2Validator(verifier()));
return builder.build();
}
}
前端uniapp支付
payOrder() {
httpUtil('/pay/order', 'POST', {
buy: 1
}, (res) => {
console.log(res);
// 调用支付
uni.getProvider({
service: 'payment',
success: function(payRes) {
if (~payRes.provider.indexOf('wxpay')) {
console.log('调用支付');
console.log(payRes);
uni.requestPayment({
"provider": "wxpay", //固定值为"wxpay"
"timeStamp": res.data.timeStamp,
"nonceStr": res.data.nonceStr,
"package": "prepay_id=" + res.data.prepayId,
"signType": "RSA",
"paySign": res.data.sign,
success: function(res2) {
console.log('支付成功', res2);
},
fail: function(err2) {
console.error('支付失败', err2);
}
});
}
}
});
})
},
