一、参考文章
java微信小程序调用微信支付apiv3接口(最简洁代码)_微信小程序调用支付接口-优快云博客
新人小白第一次做的支付功能,基于大佬的文档做的开发,涉及一些个人在处理过程中遇到问题。
二、开发前准备
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "wx")
public class WechatConfigProperties {
private String appId;
/*private String appSecret;*/
//# 证书序列号
private String mchSerialNo;
//#平台证书地址,平台证书需要手动下载或者使用v3接口进行下载
private String platformCertPath;
//客户证书地址,也就是apiclient_cert.pem的路径
private String apiClientCertPath;
//#证书密钥地址,也就是apiclient_key.pem的路径
private String apiClientKeyPath;
//商户号
private String mchId;
//apiv3秘钥w
private String apiKey;
//微信服务器地址
private String domain;
//回调,自己的回调地址,暂时内网穿透
private String notifyUrl;
}
上文涉及到的几个参数,可以在微信平台申请,也可以用接口访问下载。相关文件在申请后可以配置到服务器上,笔者是将文件上传到服务器固定存放静态文件的文件夹下。在本地打包前可存放到resources根目录下。
三、具体代码
1.下单请求参数
这里的请求是按个人需求定制化处理的,因为笔者这里涉及一些后续逻辑处理,所以传递这三个参数。
@Data
@ApiModel("微信支付请求参数")
public class IWxPayParamVO {
//操作人账号id
private Long accountId;
//下单金额
private Integer amount;
//下单类型
private Integer payType;
}
2.controller层
import com.dltech.edu.account.module.wxPay.domain.vo.IWxPayParamVO;
import com.dltech.edu.account.module.wxPay.service.impl.WeChatPayServiceImpl;
import com.eop.common.ssm.domain.vo.response.BaseResponse;
import com.eop.common.ssm.web.controller.BaseController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Api(tags = "微信支付接口")
@RestController
@RequestMapping("/payment")
public class WeChatPayController extends BaseController {
@Autowired
private WeChatPayServiceImpl wxPayNewService;
/**
* 微信统一下单接口
*
* @param iWxPayParamVO 业务需要的参数
* @return
*/
@ApiOperation("微信统一下单接口")
@PostMapping("/doUnifiedOrder")
public BaseResponse<Map<String, String>> doUnifiedOrder(@RequestBody IWxPayParamVO iWxPayParamVO) throws Exception {
Map<String, String> stringStringMap = wxPayNewService.doUnifiedOrder(iWxPayParamVO);
return success(stringStringMap);
}
/**
* 微信支付的回调接收接口
*
* @param request
* @param response
*/
@ApiOperation("微信支付的回调接收接口")
@RequestMapping(value = "/payNotify", method = {org.springframework.web.bind.annotation.RequestMethod.POST, org.springframework.web.bind.annotation.RequestMethod.GET})
public void callBack(HttpServletRequest request, HttpServletResponse response) {
wxPayNewService.callBack(request, response);
}
/**
* 生成平台证书
*/
@ApiOperation("生成v3证书")
@RequestMapping("/createPlatformCert")
@ResponseBody
public String createPlatformCert() throws IOException {
return wxPayNewService.createPlatformCert();
}
/**
* 通过订单号查询支付情况
*
* @param outTradeNo 订单号
* @return String
*/
@ApiOperation("通过订单号查询支付情况")
@RequestMapping("/query")
@ResponseBody
public String query(@RequestParam String outTradeNo) {
return wxPayNewService.query(outTradeNo);
}
/**
* 微信退款
* outTradeNo: 原支付交易对应的商户订单号
*/
@ApiOperation("申请退款接口")
@GetMapping("/payRefund")
public String payRefund( @RequestParam String outTradeNo) {
return wxPayNewService.payRefund(outTradeNo);
}
}
这里的退款接口是可以传递transactionId 微信是支持两个订单号申请退款的。
3.serviceImpl层
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dltech.edu.account.config.WechatConfigProperties;
import com.dltech.edu.account.module.ac.domain.entity.AcAccountWx;
import com.dltech.edu.account.module.ac.service.impl.AcAccountWxServiceImpl;
import com.dltech.edu.account.module.wxPay.domain.entity.WeChatPay;
import com.dltech.edu.account.module.wxPay.domain.vo.IWxPayParamVO;
import com.dltech.edu.account.module.wxPay.mapper.WeChatPayMapper;
import com.ijpay.core.IJPayHttpResponse;
import com.ijpay.core.enums.AuthTypeEnum;
import com.ijpay.core.enums.RequestMethodEnum;
import com.ijpay.core.kit.AesUtil;
import com.ijpay.core.kit.HttpKit;
import com.ijpay.core.kit.PayKit;
import com.ijpay.core.kit.WxPayKit;
import com.ijpay.core.utils.DateTimeZoneUtil;
import com.ijpay.wxpay.WxPayApi;
import com.ijpay.wxpay.enums.WxDomainEnum;
import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
import com.ijpay.wxpay.enums.v3.CertAlgorithmTypeEnum;
import com.ijpay.wxpay.model.v3.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.*;
import static com.eop.common.cache.CacheProvider.logger;
@Slf4j
@Service
public class WeChatPayServiceImpl extends ServiceImpl<WeChatPayMapper, WeChatPay> {
@Autowired
private AcAccountWxServiceImpl acAccountWxService;
@Autowired
private WechatConfigProperties wechatConfigProperties;
public WeChatPayServiceImpl(WechatConfigProperties wechatConfigProperties) {
this.wechatConfigProperties = wechatConfigProperties;
}
/**
* 微信统一下单接口
*
* @return Map
* @throws Exception 异常
*/
public Map<String, String> doUnifiedOrder(IWxPayParamVO iWxPayParamVO) throws Exception {
log.info("当前账户id {}",iWxPayParamVO.getAccountId());
AcAccountWx acAccountWx = acAccountWxService.selectByAccountId(iWxPayParamVO.getAccountId());
//用户登录时候微信返回的openId(注意不是unionId)
String openId = "";
//先写死1, 1就是1分钱,100=1元
String timeExpire = DateTimeZoneUtil.dateToTimeZone(System.currentTimeMillis() + 1000 * 60 * 3);
UnifiedOrderModel unifiedOrderModel = new UnifiedOrderModel()
.setAppid(wechatConfigProperties.getAppId())
.setMchid(wechatConfigProperties.getMchId())
.setDescription("支付说明")
//这是随机数
.setOut_trade_no(PayKit.generateStr())
.setTime_expire(timeExpire)
.setAttach("附加说明")
//回调地址
.setNotify_url(wechatConfigProperties.getNotifyUrl())
.setAmount(new Amount().setTotal(iWxPayParamVO.getAmount()))
.setPayer(new Payer().setOpenid(openId));
log.info("统一下单参数 {}", JSONUtil.toJsonStr(unifiedOrderModel));
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.POST,
//中国境内的微信支付环境
WxDomainEnum.CHINA.toString(),
//微信支付API接口
BasePayApiEnum.JS_API_PAY.toString(),
wechatConfigProperties.getMchId(),
//获取证书序列号
getSerialNumber(),
null,
wechatConfigProperties.getApiClientKeyPath(),
JSONUtil.toJsonStr(unifiedOrderModel)
);
log.info("统一下单响应 {}", response);
Map<String, String> map = new HashMap<>(16);
//这个是证书文件,先写死,后续调整成读取证书文件的服务器存放地址,根据证书序列号查询对应的证书来验证签名结果
boolean verifySignatures = WxPayKit.verifySignature(response, wechatConfigProperties.getApiClientCertPath());
log.info("verifySignature: {}", verifySignatures);
if (response.getStatus() == HttpServletResponse.SC_OK) {
// 根据证书序列号查询对应的证书来验证签名结果
String body = response.getBody();
JSONObject jsonObject = JSONUtil.parseObj(body);
String prepayId = jsonObject.getStr("prepay_id");
// 私钥
map = WxPayKit.jsApiCreateSign(wechatConfigProperties.getAppId(), prepayId, wechatConfigProperties.getApiClientKeyPath());
log.info("唤起支付参数:{}", map);
}
// todo 微信预支付的订单新入库,为了业务查询记录,可以做存入数据库操作
return map;
}
/**
* 微信支付回调接口
*
* @param request 请求
* @param response 详情
*/
public void callBack(HttpServletRequest request, HttpServletResponse response) {
log.info("收到微信支付回调");
Map<String, String> map = new HashMap<>(12);
try {
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
String serialNo = request.getHeader("Wechatpay-Serial");
String signature = request.getHeader("Wechatpay-Signature");
log.info("timestamp:{} nonce:{} serialNo:{} signature:{}", timestamp, nonce, serialNo, signature);
String result = HttpKit.readData(request);
log.info("支付通知密文 {}", result);
// 根据证书序列号查询对应的证书来验证签名结果
String platformCertPath = this.wechatConfigProperties.getPlatformCertPath();
log.info("平台证书地址 {}", platformCertPath);
//这个商户号对应的那个V3秘钥
String mckKey = wechatConfigProperties.getApiKey();
//需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
String plainText = WxPayKit.verifyNotify(serialNo, result, signature, nonce, timestamp, mckKey, platformCertPath);
log.info("支付通知明文 {}", plainText);
//这个就是具体的业务情况
savePayPlainText(plainText);
//回复微信
if (StrUtil.isNotEmpty(plainText)) {
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
} else {
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "签名错误");
}
response.setHeader("Content-type", ContentType.JSON.toString());
response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 查询账单
*
* @param outTradeNo
* @return
*/
public String query(String outTradeNo) {
try {
Map<String, String> params = new HashMap<>(16);
params.put("mchid", wechatConfigProperties.getMchId());
log.info("统一下单参数 {}", JSONUtil.toJsonStr(params));
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.GET,
WxDomainEnum.CHINA.toString(),
String.format(BasePayApiEnum.ORDER_QUERY_BY_OUT_TRADE_NO.toString(), outTradeNo),
wechatConfigProperties.getMchId(),
getSerialNumber(),
null,
wechatConfigProperties.getApiClientKeyPath(),
params
);
log.info("查询响应 {}", response);
if (response.getStatus() == HttpServletResponse.SC_OK) {
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, wechatConfigProperties.getPlatformCertPath());
log.info("verifySignature: {}", verifySignature);
return response.getBody();
}
return JSONUtil.toJsonStr(response);
} catch (Exception e) {
log.error("系统异常", e);
return e.getMessage();
}
}
/**
* 获取v3的证书
*
* @return String类型
*/
public String createPlatformCert() {
//这个商户号对应的那个V3秘钥
String mckKey = wechatConfigProperties.getApiKey();
// 获取平台证书列表
try {
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.GET,
WxDomainEnum.CHINA.toString(),
CertAlgorithmTypeEnum.getCertSuffixUrl(CertAlgorithmTypeEnum.RSA.getCode()),
wechatConfigProperties.getMchId(),
getSerialNumber(),
null,
wechatConfigProperties.getApiClientKeyPath(),
"",
AuthTypeEnum.RSA.getCode()
);
String serialNumber = response.getHeader("Wechatpay-Serial");
String body = response.getBody();
int status = response.getStatus();
log.info("serialNumber: {}", serialNumber);
log.info("status: {}", status);
// log.info("body: {}", body);
if (status == HttpServletResponse.SC_OK) {
JSONObject jsonObject = JSONUtil.parseObj(body);
JSONArray dataArray = jsonObject.getJSONArray("data");
// 默认认为只有一个平台证书
JSONObject encryptObject = dataArray.getJSONObject(0);
JSONObject encryptCertificate = encryptObject.getJSONObject("encrypt_certificate");
String associatedData = encryptCertificate.getStr("associated_data");
String cipherText = encryptCertificate.getStr("ciphertext");
String nonce = encryptCertificate.getStr("nonce");
String serialNo = encryptObject.getStr("serial_no");
//生成一个证书文件,根据秘钥对应的证书
final String platSerialNo = savePlatformCert(associatedData, mckKey, nonce, cipherText, wechatConfigProperties.getPlatformCertPath());
log.info("平台证书序列号: {} serialNo: {}", platSerialNo, serialNo);
}
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, wechatConfigProperties.getPlatformCertPath());
if (verifySignature) {
return body;
} else {
return "平台证书不正确";
}
} catch (Exception e) {
e.printStackTrace();
return "不能获取平台证书";
}
}
private String savePlatformCert(String associatedData, String apiKey3, String nonce, String cipherText, String certPath) {
try {
AesUtil aesUtil = new AesUtil(apiKey3.getBytes(StandardCharsets.UTF_8));
// 平台证书密文解密
// encrypt_certificate 中的 associated_data nonce ciphertext
String publicKey = aesUtil.decryptToString(
associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
cipherText
);
// log.info("获取证书key:{},保存路径platformCert:{}", publicKey, certPath);
log.info("保存路径platformCert:{}", certPath);
//将生成的证书写入指定路径,文件名为:cert.pem
FileOutputStream fos = new FileOutputStream(certPath);
fos.write(publicKey.getBytes());
fos.close();
// 获取平台证书序列号
X509Certificate certificate = PayKit.getCertificate(new ByteArrayInputStream(publicKey.getBytes()));
return certificate.getSerialNumber().toString(16).toUpperCase();
} catch (Exception e) {
log.error("写入证书错误:{}", e);
return e.getMessage();
}
}
/**
* 保存订单的支付通知明文
*
* @param plainText 纯文本
*/
private void savePayPlainText(String plainText) {
JSONObject jsonObject = JSONUtil.parseObj(plainText);
//这个就是发起订单时的那个订单号
String outTradeNo = jsonObject.getStr("out_trade_no");
//todo 把微信支付回调的明文消息存进数据库,方便后续校验查看
// 保存到数据库
//this.baseMapper.insert(weChatPay);
log.info("业务订单号,outTradeNo:{}", outTradeNo);
//todo 把微信支付后需要处理的具体业务处理了
}
/**
* 获取证书序列号
*
* @return String
*/
private String getSerialNumber() {
log.info("验证证书路径:{}", wechatConfigProperties.getApiClientCertPath());
// 获取证书序列号
X509Certificate certificate = PayKit.getCertificate(FileUtil.getInputStream(wechatConfigProperties.getApiClientCertPath()));
String serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
log.info("获取证书序列号:{},", serialNo);
return serialNo;
}
/**
* 申请退款
*
* @return String
*/
public String payRefund(String outTradeNo) {
logger.info("进入退款接口------>" );
logger.info("执行操作的 原支付交易对应的商户订单号:{}", outTradeNo );
try {
String outRefundNo = PayKit.generateStr();
logger.info("商户退款单号: {}", outRefundNo);
List<RefundGoodsDetail> list = new ArrayList<>();
RefundGoodsDetail refundGoodsDetail = new RefundGoodsDetail()
.setMerchant_goods_id( outRefundNo )
.setGoods_name( "取消订单" )
.setUnit_price( 1 ) //金额,单位为分
.setRefund_amount( 1 )
.setRefund_quantity(1);
list.add(refundGoodsDetail);
RefundModel refundModel = new RefundModel()
.setOut_refund_no(outRefundNo)
.setReason("取消订单")
//.setNotify_url(wechatPayV3Bean.getDomain().concat("/wechat/operation/pay/refundNotify")) //退款异步回调通知
.setAmount(new RefundAmount().setRefund(1).setTotal(1).setCurrency("CNY"))
.setGoods_detail(list);
if (outTradeNo!=null && !outTradeNo.equals("")) {
refundModel.setOut_trade_no( outTradeNo );
}
logger.info("退款参数: {}", JSONUtil.toJsonStr(refundModel));
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.POST,
//中国境内的微信支付环境
WxDomainEnum.CHINA.toString(),
//微信退款API接口
BasePayApiEnum.DOMESTIC_REFUND.toString(),
wechatConfigProperties.getMchId(),
//获取证书序列号
getSerialNumber(),
null,
wechatConfigProperties.getApiClientKeyPath(),
JSONUtil.toJsonStr(refundModel)
);
logger.info("退款响应 {}", response);
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, wechatConfigProperties.getPlatformCertPath());
logger.info("verifySignature: {}", verifySignature);
String body = response.getBody();
JSONObject jsonObject = JSONUtil.parseObj(body); //转换为JSON
if(response.getStatus()==200){ //退款成功,处理业务逻辑
// todo 微信退款成功,处理业务逻辑
//return response.getBody();
}
if (verifySignature) {
return response.getBody();
}
} catch (Exception e) {
e.printStackTrace();
return e.getMessage();
}
return null;
}
}
注意openId 是微信登录时候返回的标识,当前登录账户与付款账户需要为同一人。
关于微信登录换取openId 可以参考笔者下文的方法,也可直接参考微信开发文档。
public static Map<String, String> getUnionId2(String code, String appid, String secret) {
String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code";
String res = HttpUtils.sendGet(url, null);
JSONObject jsonObject = JSON.parseObject(res);
HashMap<String, String> map = new HashMap<>();
System.out.println("========" + jsonObject);
String unionid = jsonObject.get("unionid").toString();
String openid = jsonObject.get("openid").toString();
map.put("unionid",unionid);
map.put("openid",openid);
return map;
}
四、总结
笔者实现的小程序支付是一个简单的支付流程(下单,回调通知,退款,查询账单)。
实际的扣款操作是前端调用微信的SDK或API进行支付,后台处理的是下单的订单信息,通过提供给微信的回调接口获取支付通知的过程。
关于接口传参和返回值的具体信息可以参考: