Java - 百度收银台
目前,网上关于百度收银台的博客寥寥无几,我几乎没看到过,从头到尾都是跟着官方文档来的,中间遇到过一些小挫折,在百度客服小姐姐和百度技术小哥的指导下,最终还是调通了,并成功上线,好了不扯了,下面开始进入正题 !
本文默认各位读者的公司都注册过百度智能小程序账号,并上线应用,只是单纯来寻求百度收银台支付的Blog ~
【简介】
百度收银台支付是百度面向有开发能力的智能小程序合作者提供的支付能力,聚合了度小满、微信、支付宝等多种支付方式,方便开发者一站式快速接入多种支付渠道,让百度用户在智能小程序场景下,直接完成支付实现交易闭环,提升用户支付体验,提高订单转化率。
开通指引请参考官方文档:https://smartprogram.baidu.com/docs/introduction/pay/
注:若您已入驻百度电商平台,可以绑定已有电商平台账号,也可以开通新的支付账号
也就是说,无需借助百度电商平台,可以直接在百度智能小程序平台进行配置即可
【开发准备】
◆ 在平台(本文平台均指百度智能小程序平台)的支付管理,配置开发者公钥,私钥保存在服务端代码。
RSA公钥私钥生成工具,推荐:支付宝RAS**生成器(如下代码纯属测试数据)
生成的公钥直接配置在平台的开发者公钥选项,私钥会保存在rsa_private_key_pkcs8.pem文件(Java专属)
◆ 配置支付回调地址,退款审核地址,退款回调地址(如果回调接口部署在阿里云或有网关准入限制,请参考文档阿里云安全组设置中的IP地址设置白名单,网址为http://dianshang.baidu.com/platform/doclist/index.html#!/doc/nuomiplus_1_guide/aliyun_v2.md)
下面开始Java接入百度收银台,官方文档:http://dianshang.baidu.com/platform/doclist/index.html#!/doc/nuomiplus_1_guide/mini_program_cashier/standard_interface/push_notice.md
【签名工具类】
package com.maiji.cloud.utils;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
import com.nuomi.common.NuomiApiException;
import com.nuomi.common.NuomiConstants;
import com.nuomi.util.StreamUtil;
import com.nuomi.util.codec.Base64;
import org.apache.commons.lang.StringUtils;
/**
* 签名工具类
* 目前只支持rsa方式 不支持rsa2
*/
public class NuomiSignature {
public static final String appKey = "";
public static final String dealId = "";
public static final String PUB_KEY = "";
public static final String PRIVATE_KEY = "";
public static String genSignWithRsa(String totalAmount, String tpOrderId) throws NuomiApiException {
HashMap apiParams = new HashMap();
apiParams.put("appKey", appKey);
apiParams.put("dealId", dealId);
apiParams.put("totalAmount", totalAmount);
apiParams.put("tpOrderId", tpOrderId);
return NuomiSignature.genSignWithRsa(apiParams, PRIVATE_KEY);
}
public static Boolean checkSignWithRsa(String totalAmount, String tpOrderId, String rsaSign) throws NuomiApiException {
HashMap apiParams = new HashMap();
apiParams.put("appKey", appKey);
apiParams.put("dealId", dealId);
apiParams.put("totalAmount", totalAmount);
apiParams.put("tpOrderId", tpOrderId);
return NuomiSignature.checkSignWithRsa(apiParams, PUB_KEY, rsaSign);
}
/**
* 获取签名
*
* @param sortedParams 排序后的参数
* @param privateKey 私钥
* @return 返回签名后的字符串
* @throws NuomiApiException
*/
public static String genSignWithRsa(Map<String, String> sortedParams, String privateKey) throws NuomiApiException {
String sortedParamsContent = getSignContent(sortedParams);
return rsaSign(sortedParamsContent, privateKey, NuomiConstants.CHARSET_UTF8);
}
/**
* 签名验证
*
* @param sortedParams
* @param pubKey
* @param sign
* @return
* @throws NuomiApiException
*/
public static boolean checkSignWithRsa(Map<String, String> sortedParams, String pubKey, String sign) throws NuomiApiException {
String sortedParamsContent = getSignContent(sortedParams);
return doCheck(sortedParamsContent, sign, pubKey, NuomiConstants.CHARSET_UTF8);
}
/**
* @param sortedParams 已经排序的字符串
* @return 返回签名后的字符串
*/
public static String getSignContent(Map<String, String> sortedParams) {
StringBuffer content = new StringBuffer();
List<String> keys = new ArrayList<String>(sortedParams.keySet());
Collections.sort(keys);
int index = 0;
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = sortedParams.get(key);
content.append((index == 0 ? "" : "&") + key + "=" + value);
index++;
}
return content.toString();
}
/**
* sha1WithRsa 加签
*
* @param content 需要加密的字符串
* @param privateKey 私钥
* @param charset 字符编码类型 如:utf8
* @return
* @throws NuomiApiException
*/
public static String rsaSign(String content, String privateKey, String charset) throws NuomiApiException {
try {
PrivateKey priKey = getPrivateKeyFromPKCS8(NuomiConstants.SIGN_TYPE_RSA,
new ByteArrayInputStream(privateKey.getBytes()));
java.security.Signature signature = java.security.Signature
.getInstance(NuomiConstants.SIGN_ALGORITHMS);
signature.initSign(priKey);
if (StringUtils.isEmpty(charset)) {
signature.update(content.getBytes());
} else {
signature.update(content.getBytes(charset));
}
byte[] signed = signature.sign();
return new String(Base64.encodeBase64(signed));
} catch (InvalidKeySpecException ie) {
throw new NuomiApiException("RSA私钥格式不正确,请检查是否正确配置了PKCS8格式的私钥", ie);
} catch (Exception e) {
throw new NuomiApiException("RSAcontent = " + content + "; charset = " + charset, e);
}
}
public static PrivateKey getPrivateKeyFromPKCS8(String algorithm, InputStream ins) throws Exception {
if (ins == null || StringUtils.isEmpty(algorithm)) {
return null;
}
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
byte[] encodedKey = StreamUtil.readText(ins).getBytes();
encodedKey = Base64.decodeBase64(encodedKey);
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
}
/**
* RSA验签名检查
*
* @param content 待签名数据
* @param sign 签名值
* @param publicKey 分配给开发商公钥
* @param encode 字符集编码
* @return 布尔值
* @throws NuomiApiException
*/
private static boolean doCheck(String content, String sign, String publicKey, String encode) throws NuomiApiException {
try {
KeyFactory keyFactory = KeyFactory.getInstance(NuomiConstants.SIGN_TYPE_RSA);
byte[] bytes = publicKey.getBytes();
byte[] encodedKey = Base64.decodeBase64(bytes);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
java.security.Signature signature = java.security.Signature.getInstance(NuomiConstants.SIGN_ALGORITHMS);
signature.initVerify(pubKey);
signature.update(content.getBytes(encode));
boolean bverify = signature.verify(Base64.decodeBase64(sign.getBytes()));
return bverify;
} catch (Exception e) {
throw new NuomiApiException("RSA私钥格式不正确,请检查是否正确配置了PKCS8格式的私钥", e);
}
}
}
【支付&核销&退款工具类】
package com.maiji.cloud.utils;
import cn.hutool.core.util.NumberUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import com.maiji.cloud.response.BdXCXResDto;
import com.nuomi.common.NuomiApiException;
import org.springframework.web.client.RestTemplate;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 百度收银台工具类
* @Author: zhaobin
* @Date: 2020/6/10
*/
public class BaiduUtil {
public static final String appKey = "";
public static final String dealId = "";
public static final String PUB_KEY = "";
public static final String PRIVATE_KEY = "";
private static String url = "https://nop.nuomi.com/nop/server/rest";
/**
* 百度收银台 - 退款接口
*
* @param orderId
* @param userId
* @param orderNo
* @param amount
* @return
* @throws NuomiApiException
*/
public static JSONObject bdRefund(String orderId, String userId, String orderNo, Double amount, String bizRefundBatchId, String refundReason) throws Exception {
String totalAmountStr = NumberUtil.toStr(amount * 100);
Map<String, String> param = new HashMap<>();
param.put("method", "nuomi.cashier.applyorderrefund");
param.put("orderId", orderId);
param.put("userId", userId);
param.put("refundType", "1");
param.put("refundReason", refundReason);
param.put("tpOrderId", orderNo);
param.put("appKey", appKey);
param.put("applyRefundMoney", totalAmountStr); //退款金额,单位:分
param.put("bizRefundBatchId", bizRefundBatchId); //业务方退款批次id,退款业务流水唯一编号
String sign = NuomiSignature.genSignWithRsa(param, NuomiSignature.PRIVATE_KEY);
String refundApplyUrl = url + "?method=nuomi.cashier.applyorderrefund&orderId=" + orderId + "&userId=" + userId + "&refundType=1&refundReason=" + refundReason +
"&tpOrderId=" + orderNo + "&appKey=" + appKey + "&applyRefundMoney=" + totalAmountStr + "&bizRefundBatchId=" + bizRefundBatchId + "&rsaSign=" + sign;
JSONObject forObject = new RestTemplate().getForObject(refundApplyUrl, JSONObject.class);
return forObject;
}
public static JSONObject cancelHx(String orderId, String userId) throws Exception {
Map<String, String> param = new HashMap<>();
param.put("method", "nuomi.cashier.syncorderstatus");
param.put("orderId", orderId);
param.put("userId", userId);
param.put("type", "3");
param.put("appKey", appKey);
String sign = NuomiSignature.genSignWithRsa(param, NuomiSignature.PRIVATE_KEY);
String finalUrl = url + "?method=nuomi.cashier.syncorderstatus&orderId=" + orderId + "&userId=" + userId + "&type=3&appKey=MMUBBH&rsaSign=" + sign;
JSONObject forObject = new RestTemplate().getForObject(finalUrl, JSONObject.class);
return forObject;
}
}
【接口通用响应实体】
@NoArgsConstructor
@Data
@Accessors(chain=true)
public class BdXCXResDto<T> {
private T data;
private Integer errno;
private String msg;
public BdXCXResDto(Integer errno, String msg) {
this.errno = errno;
this.msg = msg;
}
}
【调起百度收银台】
百度收银台没有预支付,直接由小程序端调起百度收银台发起支付,不过他们需要一个参数 “rsaSign” 需要调后台接口返回:
@PostMapping("/rsaSign")
public BaseDataResDto<String> rsaSign (@RequestBody BaseDataReqDto<BaiduXCXReqData> baseDataReqDto) {
Double totalAmount = baseDataReqDto.getData().getTotalAmount();
String tpOrderId = baseDataReqDto.getData().getTpOrderId();
String totalAmountStr = NumberUtil.toStr(totalAmount * 100);
try {
String sign = NuomiSignature.genSignWithRsa(totalAmountStr, tpOrderId);
} catch (NuomiApiException e) {
e.printStackTrace();
return new BaseDataResDto<>(Status.ERROR);
}
return new BaseDataResDto<String>(Status.SUCCESS).setData(sign);
}
【通知支付状态】
◆ 百度收银台主动发起通知,该方式才会被启用
业务方智能小程序跳转至百度收银台,输入正确的交易密码之后,即订单支付成功后,百度收银台会主动调用业务方的的支付回调地址(开发者平台注册的支付回调地址)通知业务方该订单支付成功。
/**
* 实际开发可以直接用 @ResponseBody Map<String, String> param 接收
* 因为我在SpringCloud做了一层拦截,所以该博客直接用@RequestParam一个个接收
*/
@PostMapping("bdXCXCallBack")
public BdXCXResDto<Map<String, Object>> bdXCXCallBack(@RequestParam Integer totalMoney, @RequestParam String tpOrderId,
@RequestParam String rsaSign, @RequestParam Long orderId, @RequestParam Long userId) {
return capitalMainLogService.bdXCXCallBack(totalMoney,tpOrderId,rsaSign,orderId,userId);
}
@Override
public BdXCXResDto<Map<String, Object>> bdXCXCallBack(Integer totalMoney, String tpOrderId, String rsaSign, Long orderId, Long userId) {
BdXCXResDto<Map<String, Object>> bdXCXResDto = new BdXCXResDto<>(0, "success");
Map<String, Object> map = Maps.newHashMap();
map.put("isConsumed", 2);
try {
// 根据订单号查看订单信息
ShopingOrder shopingOrder = shopingOrderService.selectOne(new EntityWrapper<ShopingOrder>().eq("order_no", tpOrderId));
if (BooleanUtils.isFalse(NuomiSignature.checkSignWithRsa(totalMoney.toString(), tpOrderId, NuomiSignature.genSignWithRsa( NumberUtil.toStr(shopingOrder.getAmount() * 100), tpOrderId)))) {
logger.info("bdXCXCallBack ===>> 签名验证失败");
map.put("isErrorOrder", 1);
return bdXCXResDto.setData(map);
}
// 已经付款
if (Arrays.asList(1, 2, 3, 5, 6).contains(shopingOrder.getStatus())) return bdXCXResDto.setData(map);
shopingOrder.setStatus(1).setPayType(5).setPayId(userId + "").setPayDate(new Date()).setPrepayId(orderId + "");
// 修改支付状态为成功
shopingOrderMapper.updateById(shopingOrder);
logger.info("百度收银台已支付");
// 其他业务逻辑...
} catch (Exception e) {
e.printStackTrace();
map.put("isErrorOrder", 1);
return bdXCXResDto.setData(map);
}
return bdXCXResDto.setData(map);
}
返回参数说明
名称 | 类型 | 是否必须 | 示例值 | 描述 |
---|---|---|---|---|
errno | Integer | 是 | 0 | 返回码 |
msg | String | 是 | success | 返回信息 |
data | Object | 是 | {"isConsumed":0} | 返回数据 |
data字段为JSON格式,参数如下:
名称 | 类型 | 是否必须 | 示例值 | 描述 |
---|---|---|---|---|
isConsumed | Integer | 是 | 2 | 是否标记核销 |
isErrorOrder | Integer | 否 | 1 | 是否异常订单(如需主动发起异常退款,需将此字段设置为1) |
isConsumed字段参数枚举值如下:
取值 | 描述 |
---|---|
1 | 未消费 |
2 | 已消费 |
注意: isConsumed重要性:为必传参数(不传会触发异常退款),用来标记该订单是否已消费。 小程序接入为支付成功即消费场景,该字段需设置为2。(字段不设置为2订单同样会变更为“已消费”)如isConsumed值不返回2,“已消费”状态的订单金额不能顺利进入企业余额。
返回(RESPONSE) DEMO:
{"errno":0,"msg":"success","data":{"isConsumed":2}}
如处理支付回调的过程中开发者端参数异常、其他异常,返回以下参数进行异常退款:
{"errno": 0,"msg": "success","data": {"isErrorOrder": 1,"isConsumed": 2}
小程序场景isConsumed返回值一定要为2,(字段不设置为2订单不会变更为“已消费”)不按照要求值返回参数,用户已付款金额不能顺利进入企业余额。
【申请退款】
◆ 业务方可以通过该接口申请订单退款,仅限在百度电商开放平台支付的订单。
◆ 特别说明:
防止资金池金额小于退款金额时退款失败的情况,建议根据业务退款情况,在“管理中心——支付服务设置——我的服务——服务——财务设置”设置“每日退款上限(元)”和“打款预留(元)”。
每日退款上限(元) :设置每日退款上限。当日退款达到设置金额时,当日新发起的退款都会失败。
打款预留(元):结款日资金池预留的不打款的金额,保证资金池有金额退款。
发起部分退款时,订单必须是核销状态。
@PostMapping("executeRefund")
public BaseResDto executeRefund(@RequestBody BaseDataReqDto<String> baseDataReqDto) {
String orderRefundId = baseDataReqDto.getData();
if (StringUtil.isBlank(orderRefundId))
return new BaseResDto(Status.PARAMETERERROR);
return capitalMainLogService.executeRefund(orderRefundId);
}
@Override
public BaseResDto executeRefund(String orderRefundId) {
ShoppingOrderRefundEntity shoppingOrderRefund = shopingOrderRefundService
.selectById(orderRefundId).setRefundMiddleTime(new Date()).setStatus(3);//退款中
String orderId = shoppingOrderRefund.getOrderId();
ShopingOrder shopingOrder = shopingOrderMapper.selectById(orderId) .setRefundStatus(3);//退款中
Double refundMoney = shoppingOrderRefund.getRefundMoney();
Double amount = shopingOrder.getAmount();
if (refundManey > amount)
return BaseResDto.baseResDto(Status.ERROR, "退款金额错误!");
try{
// 先取消核销 (官方客服回复:新服务不需要关注取消核销接口,请直接调用申请退款接口。)
JSONObject hx = BaiduUtil.cancelHx(shopingOrder.getPrepayId(), shopingOrder.getPayId());
if((Integer)hx.get("errno") != 0)
BaseResDto.baseResDto(Status.ERROR, "百度收银台退款失败,错误码:" + hx.get("errno"));
// 调用百度API申请退款
JSONObject refundApply = BaiduUtil.bdRefund(shopingOrder.getPrepayId(), shopingOrder.getPayId(),
shopingOrder.getOrderNo(), refundManey, shoppingOrderRefund.getUuId(), shoppingOrderRefund.getRefundReason());
if ((Integer)refundApply.get("errno") != 0)
return BaseResDto.baseResDto(Status.ERROR, "百度收银台退款失败");
}
} catch (Exception e) {
e.printStackTrace();
return BaseResDto.baseResDto(Status.ERROR, "百度收银台退款失败异常!");
}
if (!shopingOrderService.updateById(shopingOrder))
return BaseResDto.baseResDto(Status.ERROR, "修改订单退款状态失败!");
if (!shopingOrderRefundService.updateById(shoppingOrderRefund))
return BaseResDto.baseResDto(Status.ERROR, "修改退款记录退款状态失败!");
return new BaseResDto(Status.SUCCESS);
}
返回说明
名称 | 类型 | 是否必须 | 示例值 | 描述 |
---|---|---|---|---|
errno | Integer | 是 | 0 | 返回码 |
msg | String | 是 | success | 返回信息 |
data | Object | 是 | [] | 返回数据 |
返回示例
{
"errno": 0,
"msg": "success",
"data": {
"refundBatchId": "152713835",//平台退款批次号
"refundPayMoney": "9800" //平台可退退款金额【分为单位】
}
}
【请求业务方退款审核】
使用场景:
◆ 当用户的某个订单申请了退款后,百度收银台会主动调用业务方的退款审核地址(开发者平台注册的退款审核地址)询问订单是否可退
◆ 用户支付成功后,百度收银台通过通知业务方支付成功接口通知业务方,业务方反馈给百度收银台的字符不是合法json或解析出来的errno不为0时,系统会自动发起退款,此时百度收银台也会调用业务方的退款审核接口询问业务方订单是否可以退款
/** 百度小程序退款审核地址 */
@PostMapping("bdRefund")
public BdXCXResDto<Map<String, Object>> bdRefund(@RequestParam String tpOrderId) {
return capitalMainLogService.bdRefund(tpOrderId);
}
@Override
public BdXCXResDto<Map<String, Object>> bdRefund(String tpOrderId) {
BdXCXResDto<Map<String, Object>> bdXCXResDto = new BdXCXResDto<>(0, "success");
Map<String, Object> map = Maps.newHashMap();
try {
ShopingOrder shopingOrder = shopingOrderService.selectOne(new EntityWrapper<ShopingOrder>().eq("order_no", tpOrderId));
if (shopingOrder == null) {
// 订单不存在
map.put("auditStatus", 2); // 审核不通过,不能退款
map.put("calculateRes", new JSONObject().put("refundPayMoney", 0));
return bdXCXResDto.setData(map);
}
// if (BooleanUtils.isFalse(NuomiSignature.checkSignWithRsa(shopingOrder.getAmount() + "", tpOrderId, rsaSign))) {
// logger.info("CapitalMainLogServiceImpl.bdRefund ===>> 签名验证失败"); //如果金额不对,则会导致验签失败,所以后续不用判断金额是否匹配
// map.put("auditStatus", 2); // 审核不通过,不能退款
// map.put("calculateRes", new JSONObject().put("refundPayMoney", 0));
// return bdXCXResDto.setData(map);
// }
map.put("auditStatus", 1); // 审核通过可退款
map.put("calculateRes", new JSONObject().put("refundPayMoney",
shopingOrderMapper.getRefundMoneyByOrderId(shopingOrder.getUuId())));
} catch (Exception e) {
e.printStackTrace();
map.put("auditStatus", 2); // 审核不通过,不能退款
map.put("calculateRes", new JSONObject().put("refundPayMoney", 0));
return bdXCXResDto.setData(map);
}
return bdXCXResDto.setData(map);
}
返回(响应)DEMO:
{"errno":0,"msg":"success","data":{"auditStatus":1, "calculateRes":{"refundPayMoney":100}}}
refundPayMoney的值是以分为单位的整数,如不严格按照文档提示操作,会导致退款审核失败。
【通知退款状态】
百度收银台调用业务方的退款审核接口成功,且业务方返回允许退款后,平台会去做退款操作,当订单退款成功后,百度收银台会主动调用业务方的退款回调地址(开发者平台注册的退款回调地址)通知业务方该订单退款成功。
通知触发条件:退款成功后,平台会调用该接口,将退款成功消息通知到业务方。
/** 百度小程序退款回调地址 */
@PostMapping("bdRefundCallBack")
public BdXCXResDto<Map<String, Object>> bdRefundCallBack(@RequestParam String tpOrderId) {
return capitalMainLogService.bdRefundCallBack(tpOrderId);
}
@Override
public BdXCXResDto<Map<String, Object>> bdRefundCallBack(String tpOrderId) {
BdXCXResDto<Map<String, Object>> bdXCXResDto = new BdXCXResDto<>(0, "success");
try {
// String tpOrderId = param.get("tpOrderId"); //订单号
// String rsaSign = param.get("rsaSign");
ShopingOrder shopingOrder = shopingOrderService.selectOne(new EntityWrapper<ShopingOrder>().eq("order_no", tpOrderId));
if (shopingOrder == null) {
// 订单不存在
return bdXCXResDto.setData(new JSONObject());
}
// if (BooleanUtils.isFalse(NuomiSignature.checkSignWithRsa(shopingOrder.getAmount() + "", tpOrderId, rsaSign))) {
// return bdXCXResDto.setData(new JSONObject()); //验签失败
// }
// 已经退款
if (shopingOrder.getStatus() == 4)
return bdXCXResDto.setData(new JSONObject());
// 修改订单退款状态
shopingOrder.setRefundStatus(4);
shopingOrderService.updateById(shopingOrder)
// 修改退款状态
EntityWrapper<ShoppingOrderRefundEntity> entityWrapper = new EntityWrapper<>();
entityWrapper.eq("order_id", shopingOrder.getUuId());
ShoppingOrderRefundEntity shoppingOrderRefund = shopingOrderRefundService.selectOne(entityWrapper);
shoppingOrderRefund.setRefundFinishTime(new Date()).setStatus(4);
shopingOrderRefundService.updateById(shoppingOrderRefund)
// 其他业务逻辑...
} catch (Exception e) {
e.printStackTrace();
return bdXCXResDto.setData(new JSONObject()); //失败(官网也没说失败如何返回,干脆来个空json)
}
return bdXCXResDto.setData(new JSONObject()); //成功返回
}
返回(响应)DEMO:
{"errno":0,"msg":"success","data":{}}
至此,百度收银台付款,退款接口都整合完毕了,最后,我想吐槽一下百度收银台的文档真的太差劲了,很多参数和概念都模棱两可,而且客服每天上午十点才上班,下午五点就下班了,找客服问问题都是以邮件的形式转接给百度的技术人员,交流起来时间成本还是挺大的,不得不说,还是支付宝和微信香啊 ~
上一篇: 收银台项目详解
下一篇: JavaWeb_MVC模式的简单demo