需求要求:用户点开链接直接进行付款,原来打算使用微信H5进行支付,但是审核太麻烦,审核和多次都没通过,并且H5支付只能通过浏览器打开链接,然后唤醒微信进行支付,如果在微信中点开链接是无法支付的。然后采用微信jsapi方案进行支付。支付前需要申请公众号,支付商户号,商户号和公众号进行绑定。支付整体逻辑,用户点开链接,静默获取用户openId(生成支付订单的时候会用到),生成预支付订单,获取到预支付订单的参数后,前端换起微信支付。
准备阶段,我就直接使用另外一名博主的截图,自己难得取图了,具体代码和这位博主有些区别,因为我是直接使用微信官方的sdk。在实际开发中也遇到一些坑,后面会描述。微信公众号-JSAPI支付(保姆级教程)_微信jsapi支付-CSDN博客
微信公众号-JSAPI支付
1、微信公众号配置
https://mp.weixin.qq.com/cgi-bin/home
① 开通网页授权域名(前置条件:需开通网页授权接口权限)

②暴露网络授权域名

③获取基本配置中的AppID、AppSecret

④微信支付关联商户号

2、微信支付配置
https://pay.weixin.qq.com/
① 配置API证书 获取证书序列号

②获取正式序列号


③设置APIV3秘钥

④ 产品中心—开通JSAPI支付

⑤添加支付域名配置

⑥暴露域名

⑦AppId账号设置-商户关联公众号

程序配置:
①配置yml文件
wx:
# 公众号appid
appId:xxxxxd5381a6
# 公众号 appSecret
appSecret: ccccccaff013ebe0eff1
# 商户号
mchId: 123456
# 商户APIV3密钥
apiV3Key: ccccccccJCcMw466spcSAyV
# 商户证书序列号
merchantSerialNumber: cccccccF237C29CC
# 微信回调地址
v3PayNotifyUrl: https://xxxx.cn/orderPay/payNotify
② 微信支付平台下载的秘钥证书放入项目resources目录下

pom文件需要引入微信sdk
<!--微信支付-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.12</version>
</dependency>
在生产预支付订单的解密密钥时候我遇到一个很奇怪的报错

经过各方资料查询需要引入这个版本的包确实解决了
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>1.3.50</version>
</dependency>
主要流程及代码实现
①前端通过url获取code
(url参数配置)
Appid:公众号的appid
Redirect_uri:重定向至微信支付页面,携带用户openId
Scope:snsapi_base(授权方式 无需弹窗静默授权)
https://open.weixin.qq.com/connect/oauth2/authorize?appid=xx&redirect_uri=xxx&response_type=code&scope=snsapi_base&state=STATE&connect_redirect=1#wechat_redirect
②通过前端获取的code,传入指定支付接口(Redirect_uri),获取openId
1)通过appID、appSecret、code构建请求URL,解析返回结果获取openId
可参考微信官方文档:网页授权 | 微信开放文档
————————————————
/** 前端域名地址*/
@Value("${ccsMobileUrl}")
private String ccsMobileUrl;
/**
* 通过code获取用户openId,并重定向到支付页面
* @param request
* @return
* @throws Exception
*/
@GetMapping("/tpPayOrderPage")
public ResponseEntity<String> tpPayOrderPage(HttpServletRequest request) throws Exception {
String cardBusinessNo = request.getParameter("cardBusinessNo");
String code = request.getParameter("code");
String openId = getOpenId(code);
String redirectUrl = ccsMobileUrl+"#/payOrder?cardBusinessNo="+cardBusinessNo+"&openId="+openId;
return ResponseEntity.status(HttpStatus.FOUND).header("Location", redirectUrl).build();
}
重定向到前端的地址为:https://ccc.cn/mobile/#/payOrder?cardBusinessNo=CBABCX2407220002&openId=12345t
跳转到前端页面后,准备进行支付
整个前端代码 先贴出来
<template>
<div class="root-layout">
<div class="title-1">尊敬的客户:您的订单已生成,请确认订单信息</div>
<div class="title-line"></div>
<div class="item-title">商品信息</div>
<div class="item-line" v-if="form.lineList && form.lineList.length>0" :key="key" v-for="(item, key) in form.lineList">
<van-image class="item_img" :src="item.picUrl" />
<div class="item-content">
<span class="item-name">{{item.itemName}}</span>
<span>数量 {{item.quantity}}张</span>
<span>单价 ¥{{item.unitPrice}}</span>
</div>
</div>
<div v-else>
<van-empty description="没有行信息" />
</div>
<div class="title-line"></div>
<div class="title-2">
<div class="price">合计:2839元</div>
<template>
<button class="submit" :class="{ disabled: isPaid }" @click="onClickVersion" :disabled="isPaid">
{{ isPaid ? '已支付' : '确认付款' }}
</button>
</template>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { CellGroup, Field, Image as VanImage } from 'vant'
import { Toast } from 'vant'
declare global {
interface Window {
WeixinJSBridge: any;
}
}
@Component({
name: 'AboutUs',
components: {
[Field.name]: Field,
[CellGroup.name]: CellGroup,
[VanImage.name]: VanImage,
},
})
export default class PayOrder extends Vue {
form: any = {};
isPaid = true; // 用于跟踪支付状态
mounted() {
document.title = '订单支付';
this.queryCardBusinessInfoHandler();
}
private onClickVersion() {
const weixinJSBridge = window.WeixinJSBridge;
if(typeof weixinJSBridge == "undefined") {
Toast('请使用微信浏览器打开页面')
}else{
// 调用接口生成预支付订单
this.producePrepayOrder();
}
}
//查询订单详情
queryCardBusinessInfoHandler() {
// {
// //cardBusinessNo: this.$route.query.cardBusinessNo,
// cardBusinessNo: 'CBABCX2407220002',
// }
Http.queryCardBusinessInfo<any, any>(this.$route.query.cardBusinessNo).then((res) => {
if(res.code=== 500) {
this.$toast(res.chnDesc);
return
}
this.form = res || {};
if(res.paymentStatus == "1"){
this.isPaid = false
}else{
this.isPaid = true
}
console.log('this.form', this.form)
});
}
//查询订单详情
producePrepayOrder() {
const weixinJSBridge = window.WeixinJSBridge;
Http.producePrepayOrder({
cardBusinessNo: this.$route.query.cardBusinessNo,
openId: this.$route.query.openId
}).then((res: any) => {
if(res.code=== 500) {
this.$toast(res.chnDesc);
return
}
console.log('this.res', res)
weixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId": res.appId, // 公众号名称,由商户传入
"timeStamp": res.timeStamp, // 时间戳,自1970年以来的秒数
"nonceStr": res.nonceStr, // 随机串
"package": res.packageVal,
"signType": res.signType, // 微信签名方式
"paySign": res.paySign // 微信签名
},
(res: { err_msg: string }) => { // 指定 res 的类型
// 支付成功
if (res.err_msg === "get_brand_wcpay_request:ok") {
// 支付成功逻辑
Toast('支付成功')
}
// 支付过程中用户取消
else if (res.err_msg === "get_brand_wcpay_request:cancel") {
// 取消支付逻辑
Toast('取消支付')
}
// 支付失败
else if (res.err_msg === "get_brand_wcpay_request:fail") {
// 失败逻辑
Toast('支付失败')
}
// 其它错误处理
else if (res.err_msg === "调用支付JSAPI缺少参数:total_fee") {
// 错误处理逻辑
Toast('支付错误')
}
location.reload();
}
);
});
}
}
</script>
<style lang="less" scoped>
.item-line{
margin-top: 5px;
padding: 0,10px;
display: flex;
justify-content: space-between;
.item_img{
width: 120px;
}
.item-content{
margin-left: 10px;
padding: 5px;
display: flex;
justify-content: space-evenly;
flex-direction: column;
line-height: 30px;
flex: 1;
.item-name{
white-space: nowrap; /* 确保文本在一行内显示 */
overflow: hidden; /* 隐藏超出容器的部分文本 */
text-overflow: ellipsis; /* 超出部分显示为省略号 */
width: 200px;
}
}
}
.root-layout{
padding: 20px 10px
}
.title-1{
border: 2px solid #CECECE;
width: 99%;
height: 100px;
text-align: center;
line-height: 6;
}
.title-line{
border: 2px solid #CECECE;
height: 22px;
background: #CECECE;
}
.item-title{
margin-top: 6%;
margin-left: 6%;
}
.title-2 {
height: 60px;
display: flex; /* 启用Flexbox布局 */
justify-content: space-between; /* 子元素分布在容器的两端 */
align-items: center; /* 子元素在交叉轴上的对齐方式,这里为居中 */
margin-top: 12%;
}
.price {
/* 移除高度和行高设置,因为Flexbox会自动处理这些 */
margin-left: 5%; /* 如果需要的话,可以保留 */
/* 如果你想让价格文本垂直居中,可以不需要设置line-height,因为align-items已经处理了 */
}
.submit {
text-align: right; /* 文本靠右对齐 */
padding: 0 2.66667vw; /* 使用视口宽度单位设置内边距 */
background-color: #E96C2D; /* 背景色 */
border: 1px solid #ccc; /* 边框色,但通常你会想要与背景色形成对比的边框色 */
cursor: pointer; /* 鼠标悬停时显示指针样式 */
height: 100%; /* 占据父容器的全部高度 */
width: 23%; /* 占据父容器的23%宽度 */
color: white; /* 文本颜色为白色 */
display: flex; /* 启用Flexbox布局 */
align-items: center; /* 使子项(文本)在垂直方向上居中 */
justify-content: flex-end; /* 如果你还想要文本在水平方向上靠右(尽管text-align: right;已经做了这件事,但在这里是冗余的) */
border-radius: 10px;
}
.submit.disabled {
background-color: #ccc;
cursor: not-allowed;
}
</style>
1 先queryCardBusinessInfoHandler()查询订单详情,展示订单基本信息
2 点击确认付款按钮进入onClickVersion()准备支付,jsapi只能在微信浏览器中进行支付,所以这里判断了是否是在微信浏览器中操作。
3 方法producePrepayOrder() 生成预支付订单,调用了后端orderPay/prepay 的接口,后端方法后面会贴出来
4 生成预支付订单后,前端weixinJSBridge.invoke换起 微信支付。这里前端的操作基本上就到此结束。
前端换起微信支付可参考微信官方文档:开发指引 - JSAPI支付 | 微信支付服务商文档中心

后端准备开始生成预支付订单
controller
@PostMapping("/prepay")
public BaseResponse<OrderWechatPayResponseDTO> wehchatPay(@RequestBody WechatPayReqDTO request) {
return orderWechatPayFacade.wehchatPay(request);
}
/** 商户号*/
@Value("${wx.mchId}")
private String mchId;
/** 公众号appid*/
@Value("${wx.appId}")
private String appId;
/** 公众号appSecret*/
@Value("${wx.appSecret}")
private String appSecret;
/** 商户APIV3密钥*/
@Value("${wx.apiV3Key}")
private String apiV3Key;
/** 商户证书序列号 */
@Value("${wx.merchantSerialNumber}")
private String merchantSerialNumber;
/**微信回调地址*/
@Value("${wx.v3PayNotifyUrl}")
private String v3PayNotifyUrl;
/**
* 微信支付生成预支付订单
* @param reqDTO
* @return
*/
public OrderWechatPayResponseDTO wehchatPay(WechatPayReqDTO reqDTO) {
OrderWechatPayResponseDTO result = new OrderWechatPayResponseDTO();
OrderCardBusinessHeadResponseDTO orderWechatPayRespDTO = new OrderCardBusinessHeadResponseDTO();
log.info("wehchatPay-reqDTO===={}",JSON.toJSONString(reqDTO));
// 支付前校验参数
checkParms(reqDTO,orderWechatPayRespDTO);
// 获取apiclient_key.pem文件
File keyFile = getKeyFile();
// 订单号
//元转换为分
// 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
if (config == null) {
config =new RSAAutoCertificateConfig.Builder()
.merchantId(mchId)
.privateKeyFromPath(keyFile.getPath())
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
}
// 构建service
if (service == null) {
service = new JsapiServiceExtension.Builder().config(config).build();
}
//组装预约支付的实体
// request.setXxx(val)设置所需参数,具体参数可见Request定义
PrepayRequest request = new PrepayRequest();
//计算金额
Amount amount = new Amount();
// 金额单位为分 需要乘以100
amount.setTotal(orderWechatPayRespDTO.getOrderTotalPrice().multiply(new BigDecimal("100")).intValue());
amount.setCurrency("CNY");
request.setAmount(amount);
//公众号appId
request.setAppid(appId);
//商户号
request.setMchid(mchId);
//支付者信息
Payer payer = new Payer();
payer.setOpenid(reqDTO.getOpenId());
request.setPayer(payer);
//描述
request.setDescription("支付测试");
//微信回调地址,需要是https://开头的,必须外网可以正常访问
//本地测试可以使用内网穿透工具,网上很多的
request.setNotifyUrl(v3PayNotifyUrl);
//订单号
request.setOutTradeNo(orderWechatPayRespDTO.getCardBusinessNo());
PrepayWithRequestPaymentResponse payment = null;
String errorMsg = "";
// 加密
try {
payment = service.prepayWithRequestPayment(request);
log.info("支付成功:{}", JSON.toJSONString(payment));
}catch (Exception e){
log.error("支付失败:{}",e.getMessage());
errorMsg = e.getMessage();
}finally {
// 支付前将记录保存到支付日志表中
saveWechatPayLog(payment,request,errorMsg);
}
//默认加密类型为RSA
payment.setSignType("MD5");
//返回数据,前端调起支付
BeanUtils.copyProperties(payment,result);
return result;
}
使用官方sdk方法构建config的时候 这里在获取apiclient_key.pem文件 一直遇到一个问题,本地调试可以读取到密钥内容,但是打包后发版到服务器就获取不到了,通过资料查询,打包后的密钥文件打包到class文件下使用FileInputStream读取不到文件。

所以这里单独写了一个读取密钥文件的方法,如果没有读取到就创建文件并保存到服务器指定目录
/**
* 读取密钥文件
* @return
*/
public File getKeyFile() {
File keyFile = new File(TEMP_DIR, KEY_FILE_NAME);
// 检查文件是否已存在
if (keyFile.exists()) {
return keyFile;
}
// 如果不存在,创建并保存文件
ClassPathResource keyClassPath = new ClassPathResource("key/apiclient_key.pem");
try (InputStream inputStream = keyClassPath.getInputStream();
FileOutputStream outputStream = new FileOutputStream(keyFile)) {
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
// 返回新生成的文件
return keyFile;
} catch (IOException e) {
throw new RuntimeException("Unable to load key file", e);
}
}
在我的代码中,有一些对订单是否能够完成支付的校验,这个方法看自己是否需要checkParms(reqDTO,orderWechatPayRespDTO);
生产预支付订单方法wehchatPay(),我这里返回的实体类是OrderWechatPayResponseDTO,你可以直接返回微信的实体类 PrepayWithRequestPaymentResponse 是一样的
前端获取到预支付订单,换起微信支付,支付成功后,微信会调用我们回调接口,通知支付成功,这里把后端回调的方法贴出来
controller
@ApiOperation(value = "支付回调")
@PostMapping("/payNotify")
public synchronized void payNotify(HttpServletRequest request){
WechatPayNotifyReqDTO reqDTO = new WechatPayNotifyReqDTO();
log.info("------收到支付通知controller------");
try (ServletInputStream inputStream = request.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"))) {
// 读取请求体
StringBuilder sb = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
String notify = sb.toString();
log.info("支付通知内容:" + notify);
// 请求头处理...
reqDTO.setSignature(request.getHeader("Wechatpay-Signature"));
reqDTO.setNonce(request.getHeader("Wechatpay-Nonce"));
reqDTO.setTimestamp(request.getHeader("Wechatpay-Timestamp"));
reqDTO.setSerial(request.getHeader("Wechatpay-Serial"));
reqDTO.setSignType(request.getHeader("Wechatpay-Signature-Type"));
reqDTO.setNotify(notify);
log.info("reqDTO====={}", JSON.toJSONString(reqDTO));
orderWechatPayFacade.payNotify(reqDTO);
} catch (Exception e) {
log.error("解析付款通知出错:{}", e.getMessage(), e);
}
}
service
/**
* 支付回调
* @param request
*/
public void payNotify(WechatPayNotifyReqDTO request) {
log.info("------收到支付通知------");
File keyFile = getKeyFile();
// 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
if (config == null) {
config =new RSAAutoCertificateConfig.Builder()
.merchantId(mchId)
.privateKeyFromPath(keyFile.getPath())
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
}
// 请求头Wechatpay-Signature
String signature = request.getSignature();
// 请求头Wechatpay-nonce
String nonce = request.getNonce();
// 请求头Wechatpay-Timestamp
String timestamp = request.getTimestamp();
// 微信支付证书序列号
String serial = request.getSerial();
// 签名方式
String signType = request.getSignType();
// 构造 RequestParam
com.wechat.pay.java.core.notification.RequestParam requestParam = new com.wechat.pay.java.core.notification.RequestParam.Builder()
.serialNumber(serial)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
.signType(signType)
.body(request.getNotify())
.build();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
// 以支付通知回调为例,验签、解密并转换成 Transaction
log.info("验签参数:{}", requestParam);
Transaction transaction = parser.parse(requestParam, Transaction.class);
log.info("验签成功!-支付回调结果:{}", transaction.toString());
Map<String, String> returnMap = new HashMap<>(2);
returnMap.put("code", "FAIL");
returnMap.put("message", "失败");
if (Transaction.TradeStateEnum.SUCCESS != transaction.getTradeState()) {
log.info("内部订单号【{}】,微信支付订单号【{}】支付未成功",
}
}

2698

被折叠的 条评论
为什么被折叠?



