PayerMax旨在向客户提供安全便捷的海外本地化支付服务和国际信用卡支付服务,目前覆盖地区有东南亚,中东和拉美等地区的多个国家和地区,如印度,印尼,菲律宾,泰国,阿联酋,沙特阿拉伯,埃及,土耳其,巴西,阿根廷等。
可支持的服务包括:收款、退款、付款以及交易查询,另外我们也提供配套商户自助平台(Merchant Dashboard)。客户只需要在自己的网站或APP内集成需要的支付产品,即可拥有完整的在线收、付款能力。目前PayerMax支持的支付方式包括:钱包、借记卡、信用卡、网银、UPI、线下支付以及运营商支付等多种支付方式,充分满足客户的需求。
官方文档在这里,可以结合一起看
商户管理后台:https://mmc.payermax.com/#/login
技术对接文档:https://docs.payermax.com/#30?page_id=580&lang=zh-cn
1.先商务联系商家申请商户号
2.先获取RSA加密的公钥私钥
秘钥获取点这里通过随机生成,通过该链接获取后将公钥填写到商户后台,同时自己保存好私钥信息,后续开发需要用来验签。
这里分为正式环境跟测试环境根据自己需要选择填写
注意:测试环境的话后续接口都用如图的前缀路径!!
在后台如图填写好自己生成的公钥
3.选择支付方式
点这里查看所有的支付步骤,本文选择的收银台支付方式。
1.)收银台支付的话客户端只需要调用返回的请求地址即可,PayerMax有提供支付的页面,仅需在请求中携带回调,后面会介绍。
2.)如选择纯api支付的话则需要先根据申请token接口获取token,然后生成订单获取支付页面,客户端需要自己选择支付页面,并获取用户支付信息。(比较麻烦,建议收银台支付)
4.收银台支付方式
4.1生成支付订单(下单接口:/orderAndPay)
请求定义参数详解这里就不说了根据自己定义,参考收银台下单官方文档
这里说一下自己踩的坑,请求订单时间必须为当前时间两分钟以内,且按照文档要求格式传输,格式转换看我下面的代码。
请求头需要带上加密串信息,加密方法放在最后。
/**
* 支付
*
* @return
*/
@ApiOperation("生成支付信息")
@PostMapping("/pay")
public JSONObject pay(@RequestBody BoxPayVo boxPayVo) {
JSONObject paramMap = new JSONObject();
paramMap.put("version", "1.4");
paramMap.put("keyVersion", "1");
Date date = new Date();
String strDate = DateUtil.formatDateTime(date);
try {
date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(strDate);//先按照原格式转换为时间
} catch (ParseException e) {
e.printStackTrace();
}
String str = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").format(date);
paramMap.put("requestTime", str);
paramMap.put("appId", "你申请下来的appid");
paramMap.put("merchantNo", "你申请下来的商户id");
JSONObject dataJson = new JSONObject();
dataJson.put("outTradeNo", boxPayVo.getOrderId());
dataJson.put("integrate", "Hosted_Checkout");
dataJson.put("subject", boxPayVo.getGoodName());
dataJson.put("totalAmount", boxPayVo.getTotalAmount());
dataJson.put("currency", "KRW");
dataJson.put("userId", boxPayVo.getUserId());
dataJson.put("reference", "回调时候的参数,自己可以定义为String格式");
dataJson.put("frontCallbackURL","支付后跳转的页面自己定义");
dataJson.put("notifyUrl", "回调地址");
JSONArray jsonArray = new JSONArray();
JSONObject goods = new JSONObject();
goods.put("goodsId", boxPayVo.getGoodId());
goods.put("goodsName", boxPayVo.getGoodName());
goods.put("quantity", boxPayVo.getNum());
//商品原价
goods.put("price", boxPayVo.getVoucher());
//商品描述
goods.put("goodsCategory", boxPayVo.getVoucherType());
jsonArray.add(goods);
dataJson.put("goodsDetails", jsonArray);
paramMap.put("data", dataJson);
String body = paramMap.toJSONString();
//rsa签名 这里是自己定义了工具类加密解密文档内容放在结尾
String sign = PayRsaUtils.signForRSA(body);
//请求post 带上签名放入请求头
String result2 = HttpRequest.post("请求的地址")
.body(body).header("sign", sign)
.execute().body();
return JSONObject.parseObject(result2);
}
请求返回信息如下:客户端访问redirectUrl的地址,跳转到支付界面,用户完成付款后,会根据上面生成订单时填写的回调通知
{
"code": "APPLY_SUCCESS",
"msg": "",
"data": {
"redirectUrl": "https://cashier-n.payermax.com/index.html#/cashier/home?merchantId=020213827212251&merchantAppId=3b242b56a8b64274bcc37dac281120e3&country=ID&tradeToken=TOKEN20220117091121294138752&language=en&token=IHjqkZ8%2F%2FFcnfDPxWTvJFOrulUAKfXFUkxHJSiTdlnjnX1G6AOuTiSl6%2BN05EzxTaJkcSsSyGh5a1q%2FACwWN0sDD%2FgwY5YdWu3ghDcH2wqm%2BJIcEh0qZqo%2BQFnXp65bvkLZnY7VO7HwZGzyrpMBlPhfRCQxwBbc6lJcSYuPf%2Fe8%3D&amount=10000¤cy=IDR&frontCallbackUrl=https%3A%2F%2Fwww.payermax.com",
"outTradeNo": "P1642410680681",
"tradeToken": "TOKEN20220117091121294138752",
"status": "PENDING"
}
}
4.2回调通知接收
接收回调验证签名,如果没有接收到回调也可以主动请求获取订单状态,主动查询看文档收银台支付-主动查询,这里就不介绍了
/**
* 支付
*
* @return
*/
@ApiOperation("支付回调")
@PostMapping("/callback")
public JSONObject callback(HttpServletRequest request) throws Exception {
//定义请求体 验签需要用
StringBuilder requestBody = new StringBuilder();
BufferedReader reader = null;
try {
try {
reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
requestBody.append(line);
}
} finally {
if (reader != null) {
reader.close();
}
}
// 将JSON字符串转换为LinkedHashMap 防止转json顺序变化
LinkedHashMap<String, Object> linkedHashMap = JSON.parseObject(requestBody.toString(), LinkedHashMap.class);
// 将LinkedHashMap转换为JSONObject
JSONObject payerMaxCallback = new JSONObject(linkedHashMap);
String sign = request.getHeader("sign");
//验签
boolean flag = checkSign(sign, requestBody.toString());
if (flag) {
log.info("===回调成功验签:");
if (payerMaxCallback.getString("code").equals("APPLY_SUCCESS")) {
//回调成功处理自己的逻辑 如发货
} else {
log.info("===code回调失败" + payerMaxCallback.getString("code"));
}
//响应给第三方
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg", "Success");
jsonObject.put("code", "SUCCESS");
return jsonObject;
}
return null;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
private boolean checkSign(String sign, String body) {
//验签工具类在文章结尾
return PayRsaUtils.verify(body, sign);
}
5.RSA加密解密工具类
package com.qpyx.common.utils.rsa;
import com.qpyx.common.utils.exception.UtilException;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* @author zhu.q
*/
public class PayRsaUtils {
public static final String CHAR_SET = "UTF-8";
public static final String PUBLIC_KEY_NAME = "自己的公钥";
public static final String PRIVATE_KEY_NAME = "自己的私钥";
/**
* key生成算法
*/
private static final String KEY_ALGORITHM = "RSA";
/**
* 签名算法
*/
private static final String SIGN_ALGORITHMS = "SHA256WithRSA";
/**
* 获取签名
*
* @param body 参数
* @return 返回加签结果
*/
public static String signForRSA(String body) {
try {
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIVATE_KEY_NAME));
KeyFactory keyf = KeyFactory.getInstance(KEY_ALGORITHM);
PrivateKey priKey = keyf.generatePrivate(priPKCS8);
Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
signature.initSign(priKey);
signature.update(body.getBytes(CHAR_SET));
byte[] signed = signature.sign();
String sign = Base64.getEncoder().encodeToString(signed);
System.out.println(sign);
return sign;
} catch (Exception e) {
throw new UtilException("签名错误");
}
}
/**
* 公钥验证签名
*
* @param body 参数
* @param sign 签名
* @return 返回验证结果
*/
public static boolean verify(String body, String sign) {
try {
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
byte[] encodeKey = Base64.getDecoder().decode(PUBLIC_KEY_NAME);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodeKey));
Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
signature.initVerify(pubKey);
signature.update(body.getBytes(CHAR_SET));
return signature.verify(Base64.getDecoder().decode(sign));
} catch (Exception e) {
throw new UtilException("签名错误");
}
}
/**
* 生成密钥对
*
* @return Map
*/
public static Map<String, String> createKeyPair() {
//使用RSA算法获得密钥对生成器对象keyPairGenerator
KeyPairGenerator keyPairGenerator = null;
try {
keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
//设置密钥长度为2048
keyPairGenerator.initialize(2048);
//生成密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
//获取公钥
Key publicKey = keyPair.getPublic();
//获取私钥
Key privateKey = keyPair.getPrivate();
Map<String, String> keyMap = new HashMap<>(2);
keyMap.put(PRIVATE_KEY_NAME, Base64.getEncoder().encodeToString(privateKey.getEncoded()));
keyMap.put(PUBLIC_KEY_NAME, Base64.getEncoder().encodeToString(publicKey.getEncoded()));
return keyMap;
}
}
退款这些目前没有做,需要的话上面的文档链接有,过程中有问题的可以一起讨论交流。