简介
JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。
应用场景
JSAPI支付适用于线下场所、公众号场景和PC网站场景。
商户已有H5商城网站,用户通过消息或扫描二维码在微信内打开网页时,可以调用微信支付完成下单购买的流程。具体操作流程如下:
1.商户下发图文消息或者通过自定义菜单吸引用户点击进入商户网页
2.进入商户网页,用户选择购买,完成选购流程。
3.调起微信支付控件,用户开始输入支付密码
4.密码验证通过,支付成功。商户后台得到支付成功的通知
5.返回商户页面,显示购买成功。该页面由商户自定义
6.微信支付公众号下发支付凭证
接入前准备
直接跳转微信支付商户平台 https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml
生成密钥文件:
配置文件:
wx:
appId: appId
keyPath: apiclient_key.pem
certPath: apiclient_cert.pem
certP12Path: 暂不用
platformCertPath: platform_cert.pem
mchId: mchId
apiKey3: 暂不用
apiKey: apiKey
domain: https://hous.exchang.cn
serialNo: 序列号
pom文件:
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.2</version>
</dependency>
代码:
配置类:
//获取yml中微信配置
@Data
@ConfigurationProperties(prefix = "wx")
public class WechatProperties {
private String appId;
private String keyPath;
private String certPath;
private String platformCertPath;
private String mchId;
private String apiKey;
private String domain;
private String serialNo;
}
//配置类
@Configuration
@EnableConfigurationProperties(WechatProperties.class)
public class WechatJsapiConfig {
private final WechatProperties wechatProperties;
public WechatJsapiConfig(WechatProperties wechatProperties) {
this.wechatProperties = wechatProperties;
}
@Bean
public RSAConfig rsaConfig() throws IOException {
ClassPathResource keyResource = new ClassPathResource(wechatProperties.getKeyPath());
String apiClientKey = keyResource.getFile().getPath();
ClassPathResource certResource = new ClassPathResource(wechatProperties.getPlatformCertPath());
String platformCertResourceKey = certResource.getFile().getPath();
/*String apiClientKey = wechatProperties.getKeyPath();
// ClassPathResource certResource = new ClassPathResource(wechatProperties.getPlatformCertPath());
String platformCertResourceKey = wechatProperties.getPlatformCertPath();*/
return new RSAConfig.Builder()
.merchantId(wechatProperties.getMchId())
.privateKeyFromPath(apiClientKey)
.merchantSerialNumber(wechatProperties.getSerialNo())
//平台证书
.wechatPayCertificatesFromPath(platformCertResourceKey)
.build();
}
@Bean
public JsapiServiceExtension jsapiService() throws IOException {
return new JsapiServiceExtension.Builder()
.config(this.rsaConfig())
.build();
}
@Bean
public NotificationParser notificationParser() throws IOException{
ClassPathResource certResource = new ClassPathResource(wechatProperties.getPlatformCertPath());
String platformCertResourceKey = certResource.getFile().getPath();
//String platformCertResourceKey = wechatProperties.getPlatformCertPath();
NotificationConfig config = new RSANotificationConfig.Builder()
.apiV3Key(wechatProperties.getApiKey())
.certificatesFromPath(platformCertResourceKey)
.build();
// 初始化 NotificationParser
return new NotificationParser(config);
}
@Bean(name = "payRefundService")
public RefundService refundService() throws IOException {
return new RefundService.Builder().config(this.rsaConfig()).build();
}
}
创建支付单:
@Slf4j
@Service
@EnableConfigurationProperties(WechatProperties.class)
public class WechatJsapiPayServiceImpl implements BasePayService {
@Resource
private WechatProperties wechatProperties;
@Resource
private JsapiServiceExtension jsapiService;
@Value("${spring.application.name}")
private String serviceName;
@Resource
private MemberDao memberDao;
@Resource
private RedisCache redisCache;
@Resource
private ShopDao shopDao;
@Override
public R<?> paymentOrder(List<Order> orderList) {
BigDecimal total = orderList.stream().map(Order::getPayAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
Order order = orderList.get(0);
Long mId = order.getMId();
Member member = memberDao.queryById(mId);
Integer payTotal = total.multiply(BigDecimal.valueOf(100)).intValue();
PrepayRequest prepayRequest = new PrepayRequest();
prepayRequest.setAppid(wechatProperties.getAppId());
prepayRequest.setMchid(wechatProperties.getMchId());
prepayRequest.setDescription("供应链下单");
prepayRequest.setOutTradeNo(order.getBatchSn());
prepayRequest.setNotifyUrl(wechatProperties.getDomain() + "/callback/wechat/pay");
prepayRequest.setAttach("1");
prepayRequest.setTimeExpire(this.getTimeExpire());
Amount amount = new Amount();
amount.setTotal(payTotal);
Payer payer = new Payer();
payer.setOpenid(member.getOpenId());
prepayRequest.setAmount(amount);
prepayRequest.setPayer(payer);
log.info("创建预支付单入参:【{}】", JSONObject.toJSONString(prepayRequest));
try {
PrepayWithRequestPaymentResponse response = jsapiService.prepayWithRequestPayment(prepayRequest);
log.info("创建预支付单并生成二维码成功,出参:【{}】", JSONObject.toJSONString(response));
//未支付监听
redisCache.setCacheObject(RedisConstants.ORDER_PAY_RESULT.concat(order.getBatchSn()),
JSONObject.toJSONString(response), 10, TimeUnit.MINUTES);
redisCache.setCacheObject(RedisConstants.ORDER_NOT_PAY_LISTENER.concat(order.getBatchSn()),
NumberUtils.INTEGER_ZERO, 10, TimeUnit.MINUTES);
//发送未支付消息
redisCache.setCacheObject(RedisConstants.ORDER_MSG_LISTENER.concat(order.getBatchSn()),
NumberUtils.INTEGER_ZERO, 2, TimeUnit.MINUTES);
return R.ok(response);
} catch (HttpException e) { // 发送HTTP请求失败
log.error("发送HTTP请求失败:{}", e.getHttpRequest());
return R.fail("发送HTTP请求失败");
} catch (ServiceException e) { // 服务返回状态小于200或大于等于300,例如500
log.error("微信支付返回状态异常,{}", e.getResponseBody());
return R.fail("微信支付返回状态异常");
} catch (MalformedMessageException e) { // 服务返回成功,返回体类型不合法,或者解析返回体失败
log.error("返回类型不合法:{}", e.getMessage());
return R.fail("返回类型不合法");
}
}
@Override
public void closeOrder(String outTradeNo) {
CloseOrderRequest closeOrderRequest = new CloseOrderRequest();
closeOrderRequest.setMchid(wechatProperties.getMchId());
closeOrderRequest.setOutTradeNo(outTradeNo);
jsapiService.closeOrder(closeOrderRequest);
log.info("订单关闭成功,入参:【{}】", JSONObject.toJSONString(closeOrderRequest));
}
@Override
public Optional<Transaction> getOrderInfo(String outTradeNo) {
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
request.setOutTradeNo(outTradeNo);
request.setMchid(wechatProperties.getMchId());
Transaction transaction = jsapiService.queryOrderByOutTradeNo(request);
if (Objects.isNull(transaction)) {
return Optional.empty();
}
return Optional.of(transaction);
}
private String getTimeExpire(){
//过期时间 RFC 3339格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
//支付订单过期时间
return sdf.format(new Date(System.currentTimeMillis() + 1000 * 60 * 5));
}
}
创建退款单:
@Slf4j
@Service
@EnableConfigurationProperties(WechatProperties.class)
public class WechatRefundServiceImpl implements BaseRefundService {
@Resource
private RefundService refundService;
@Resource
private WechatProperties wechatProperties;
@Value("${spring.application.name:scm-ofc-system}")
private String serviceName;
@Resource
private OrderDao orderDao;
@Resource
private OrderItemDao orderItemDao;
@Resource
private ShopDao shopDao;
@Override
public R<?> refundOrder(OrderRefund orderRefund) {
try {
BigDecimal refundAmount = orderRefund.getRefundAmount();
Long orderId = orderRefund.getOrderId();
Order order = orderDao.queryById(orderId);
//退款金额
long refundTotal = refundAmount.multiply(BigDecimal.valueOf(100)).longValue();
//原支付金额
String batchSn = order.getBatchSn();
OrderDTO params = new OrderDTO();
params.setBatchSn(batchSn);
List<Order> orders = orderDao.selectOrderList(params);
BigDecimal officialReceipts = orders.stream().map(Order::getPayAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
long payTotal = officialReceipts.multiply(BigDecimal.valueOf(100)).longValue();
//创建微信退款单
CreateRequest createRequest = new CreateRequest();
AmountReq amountReq = new AmountReq();
amountReq.setCurrency("CNY");
amountReq.setTotal(payTotal);
amountReq.setRefund(refundTotal);
createRequest.setOutTradeNo(batchSn);
createRequest.setAmount(amountReq);
createRequest.setOutRefundNo(orderRefund.getRefundSn());
createRequest.setNotifyUrl(wechatProperties.getDomain() + "/" + serviceName + "/callback/wechat/refund");
log.info("退款单入参:{}", JSONObject.toJSONString(createRequest));
Refund refund = refundService.create(createRequest);
log.info("创建退款单成功:{}", JSONObject.toJSONString(refund));
if (Objects.isNull(refund)) {
log.error("退款异常,参数:{}", JSONObject.toJSONString(createRequest));
return R.fail(500, "退款异常,请求微信返回值为null:参数" + JSONObject.toJSONString(createRequest));
}
if (Objects.equals(refund.getStatus(), Status.SUCCESS)) {
return R.ok(refund);
}
} catch (Exception e) {
log.error("退款异常:{}", e.getMessage());
return R.fail(500, "退款异常,请求微信报错" + e.getMessage());
}
return R.ok();
}
}
支付与退款回调
@Slf4j
@Service
public class CallbackServiceImpl implements CallbackService {
@Resource
private NotificationParser notificationParser;
@Resource
private RedisCache redisCache;
@Resource
private OrderService orderService;
@Resource
private BillDao billDao;
@Resource
private ShopAccountDao shopAccountDao;
@Resource
private OrderScmService orderScmService;
@Resource
private OrderRefundDao orderRefundDao;
@Resource
private ShopSkuService shopSkuService;
@Resource
private OrderItemDao orderItemDao;
@Resource
private MessageService messageService;
@Override
@Transactional(rollbackFor = Exception.class)
public void wechatPayCallback(HttpServletRequest request, HttpServletResponse response) {
try {
log.info("=========================微信native支付回调通知============================");
Transaction transaction = this.verifyAndDecrypt(request, Transaction.class);
log.info("验证签名成功:{}", JSONObject.toJSONString(transaction));
Transaction.TradeStateEnum tradeState = transaction.getTradeState();
if (!Objects.equals(tradeState, Transaction.TradeStateEnum.SUCCESS)) {
return;
}
String outTradeNo = transaction.getOutTradeNo();
log.info("支付回调执行成功,待支付->待发货");
} catch (Exception e) {
log.error("支付回调异常:{}", e.getMessage());
}
}
@Override
public void wechatRefundCallback(HttpServletRequest request, HttpServletResponse response) {
log.info("=========================微信native退款回调通知============================");
RefundNotification refundNotification = this.verifyAndDecrypt(request, RefundNotification.class);
Status refundStatus = refundNotification.getRefundStatus();
if (!Objects.equals(refundStatus, Status.SUCCESS)) {
return;
}
//执行退款业务
}
/**
* 获取供应链订单id
* @param data
* @param orderList
* @return
*/
private Map<Long, String> getOrderIdMap(OrderByThirdIdVO data,List<Order> orderList){
List<OrderSkuDetailVO> skus = data.getSkus();
Map<String, String> skuOrderIdMap = skus.stream()
.collect(Collectors.toMap(OrderSkuDetailVO::getSku, OrderSkuDetailVO::getOrderId));
List<OrderItem> orderItems = orderList.stream().map(Order::getOrderItem).collect(Collectors.toList());
return orderItems.stream().map(item -> {
String originId = item.getOriginId();
String orderId = skuOrderIdMap.get(originId);
return new DefaultKeyValue<>(item.getOrderId(), orderId);
}).collect(Collectors.toMap(DefaultKeyValue::getKey, DefaultKeyValue::getValue));
}
/**
* 验证并解密报文
*
* @param request
*/
private <T> T verifyAndDecrypt(HttpServletRequest request, Class<T> clazz) {
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
String serialNo = request.getHeader("Wechatpay-Serial");
String signature = request.getHeader("Wechatpay-Signature");
String signType = request.getHeader("Wechatpay-Signature-Type");
String body = this.getBody(request);
log.info("\n请求头信息:\n" +
"Wechatpay-Timestamp:{},\n" +
"Wechatpay-Nonce:{},\n" +
"Wechatpay-Serial:{},\n" +
"Wechatpay-Signature:{},\n" +
"Wechatpay-Signature-Type:{},\n" +
"body: {}",
timestamp, nonce, serialNo, signature, signType, body);
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(serialNo)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
.signType(signType)
.body(body)
.build();
return notificationParser.parse(requestParam, clazz);
}
/**
* 获取请求体内容
*
* @param request
* @return
*/
private String getBody(HttpServletRequest request) {
BufferedReader reader;
String body = "";
try {
reader = request.getReader();
String line;
StringBuilder inputString = new StringBuilder();
while ((line = reader.readLine()) != null) {
inputString.append(line);
}
body = inputString.toString();
} catch (IOException e) {
e.printStackTrace();
}
return body;
}
}
以上就是微信jsapi支付的对接,仅供参考