对于后端来说,百度小程序最重要的无非也就登录,支付退款等接口最为重要,所以此文就记录一下自己的Demo代码,其实写完之后就会发现,没有什么难度,都很简单
请求百度服务器获取sessionKey
先准备一个配置类,用来存放百度小程序的各项参数
/**
* @author 萧一旬
* @date Create in 14:37 2019/11/29
*/
public class BaiDuConfig {
/**
* 智能小程序的AppKey
*/
public static final String APP_KEY = "";
/**
* 智能小程序的AppSecret
*/
public static final String APP_SECRET = "";
/**
* 跳转百度收银台支付必带参数之一,是百度收银台的财务结算凭证,与账号绑定的结算协议一一对应,每笔交易将结算到dealId对应的协议主体。
*/
public static final String DEAL_ID = "";
/**
* 平台公钥
*/
public static final String PLATFORM_PUBLIC_KEY = "";
/**
* 开发者公钥
*/
public static final String DEVELOPMENT_PUBLIC_KEY = "";
/**
* 支付AppKey
*/
public static final String APP_KEY_PAY = "";
/**
* 用户私钥
*/
public static final String PRIVATE_KEY = "";
}
这个是登录时候获取用户信息,需要传给前端的一个重要值,而且该注意的官方文档也都写了,所以不多赘述。
/**
* 请求百度服务器获取sessionKey
*
* @param code Authorization Code
* @return
*/
@GetMapping("/getSessionKey")
public JsonResult getSessionKey(String code) {
String url = "https://spapi.baidu.com/oauth/jscode2sessionkey";
try {
Map<String, String> data = new HashMap<>();
//code是前端会传的
data.put("code", code);
//这两个参数,申请百度小程序后就有的
data.put("client_id", BaiDuConfig.APP_KEY);
data.put("sk", BaiDuConfig.APP_SECRET);
String resultJson = HttpRequest.doGet(url, data);
//这里其实也可以使用JSONObject来转化获取,但是我直接规定了一个JavaBean,效果都一样
BaiDuUser user = JsonUtils.parse(resultJson, BaiDuUser.class);
if (user != null && StringUtils.isNotBlank(user.getOpenid())) {
//当获取到百度小程序的user openID后记得保存user
}
}
jsonResult.setCode(SysConst.JSON_RESULT_CODE_SUCCESS);
jsonResult.setMsg("成功");
//然后把请求到的数据返回给前端即可
jsonResult.setResult(user);
} catch (Exception e) {
e.printStackTrace();
jsonResult.setCode(SysConst.JSON_RESULT_CODE_ERROR);
jsonResult.setMsg("失败");
}
return jsonResult;
}
自此登录任务我们就做完了。
支付
官方文档链接:https://smartprogram.baidu.com/docs/introduction/pay/
阅读官方文档可知,调起百度小程序需要的参数就是一个orderInfo
对象
BaiDuOrderInfo.class
import com.abroad.utils.JsonUtils;
import lombok.Data;
import lombok.ToString;
/**
* @author 萧一旬
* @date Create in 15:52 2019/11/29
*/
@Data
@ToString
public class BaiDuOrderInfo {
private String dealId;
private String appKey;
private String totalAmount;
private String tpOrderId;
private String dealTitle;
private String signFieldsRange;
private String rsaSign;
private String bizInfo;
//这里写一个方法,提供传入bizInfo对象来给this.bizInfo的值,当然也可以直接使用set方法赋值
public void setBaiDuBizInfo(BaiDuBizInfo bizInfo) {
String json = JsonUtils.serialize(bizInfo);
StringBuffer buffer = new StringBuffer("{\"tpData\":");
buffer.append(json);
buffer.append("}");
this.setBizInfo(buffer.toString());
}
}
BaiDuBizInfo.class
import lombok.Data;
/**
* @author 萧一旬
* @date Create in 8:52 2019/12/2
*/
@Data
public class BaiDuBizInfo {
private String appKey;
private String dealld;
private String tpOrderId;
private String totalAmount;
}
准备妥当后,就可以开始码重要代码了。
//百度收银台创建
BaiDuOrderInfo orderInfo = new BaiDuOrderInfo();
orderInfo.setAppKey(BaiDuConfig.APP_KEY_PAY);
orderInfo.setDealId(BaiDuConfig.DEAL_ID);
orderInfo.setDealTitle("订单标题");
orderInfo.setTotalAmount("订单金额,单位为分");
orderInfo.setTpOrderId("商户订单号");
orderInfo.setSignFieldsRange("1");
//百度支付签名
Map<String, String> data = new HashMap<>();
data.put("appKey", orderInfo.getAppKey());
data.put("dealId", BaiDuConfig.DEAL_ID);
data.put("totalAmount", orderInfo.getTotalAmount());
data.put("tpOrderId", orderInfo.getTpOrderId());
//签名使用的类是百度签名Demo里的,自行下载即可
String sign = NuomiSignature.genSignWithRsa(data, BaiDuConfig.PRIVATE_KEY);
if (StringUtils.isBlank(sign)) {
jsonResult.setCode(SysConst.JSON_RESULT_CODE_ERROR);
jsonResult.setMsg("签名错误");
return jsonResult;
}
orderInfo.setRsaSign(sign);
BaiDuBizInfo bizInfo = new BaiDuBizInfo();
bizInfo.setAppKey(BaiDuConfig.APP_KEY);
bizInfo.setDealld(BaiDuConfig.DEAL_ID);
bizInfo.setTotalAmount(money);
bizInfo.setTpOrderId(out_trade_no);
orderInfo.setBaiDuBizInfo(bizInfo);
//最后把orderInfo对象返回给前端即可
支付回调
/**
* 支付回调
*
* @param request
* @param response
* @param tpOrderId 商户订单ID
* @param status 支付状态
* @param orderId 百度订单ID
* @return
*/
@PostMapping("/payCallback")
public String payCallback(HttpServletRequest request, HttpServletResponse response,
@RequestParam("tpOrderId") String tpOrderId,
@RequestParam("status") String status,
@RequestParam("orderId") String orderId,
@RequestParam("userId") String userId) {
/**
代码主体根据自己项目逻辑编写,毕竟每个项目都不一样,这里值得注意的是:
百度小程序不同于微信和支付宝,微信支付宝退款啥的,只需要传入一个商户订单号或者平台订单号即可,百度我看文档说是需要两个,两个都是必须,所以百度平台订单号如果没有保存逻辑的需要保存,百度传回的userId亦是,这都是后面退款需要用到的
*/
/**
返回值也必须按照官方文档规定来。
百度小程序订单状态也比其他平台要多一种:
已付款:用户付款成功,成功调用成功回调,但支付金额不会进入公司账户
已消费:使已付款的订单金额,进入到公司账户
下面的示例返回就是核销当前订单,使之付款金额进入公司账户,其余的自己看文档
*/
return "{\"errno\":0,\"msg\":\"success\",\"data\":{\"isConsumed\":2}}";
}
取消核销
已经支付核销过的订单不允许直接退款,想要发起退款时,需要先调用此接口进行取消核销
这个接口没什么好说的,唯一需要注意的就是参数不能以json
方式传参,需要以Map
方式,也就是Content-Type application/x-www-form-urlencoded
/**
* 取消核销接口,已经支付核销过的订单不允许直接退款,想要发起退款时,需要先调用此接口进行取消核销
*
* @param orderId
* @param userId
* @return
*/
private static String syncOrderStatus(String orderId, String userId) {
try {
BaiDuSyncOrderStatus rest = new BaiDuSyncOrderStatus();
rest.setAppKey(BaiDuConfig.APP_KEY_PAY);
rest.setOrderId(orderId);
rest.setUserId(userId);
rest.setType("3");
rest.setMethod("nuomi.cashier.syncorderstatus");
Map<String, String> data = new HashMap<>();
data.put("orderId", rest.getOrderId());
data.put("method", rest.getMethod());
data.put("userId", rest.getUserId());
data.put("type", rest.getType());
data.put("appKey", rest.getAppKey());
rest.setRsaSign(NuomiSignature.genSignWithRsa(data, BaiDuConfig.PRIVATE_KEY));
System.out.println(JsonUtils.serialize(rest));
String postJson = HttpRequest.doPost("https://nop.nuomi.com/nop/server/rest", JsonUtils.parseMap(JsonUtils.serialize(rest), String.class, Object.class));
System.out.println("postJson------------>" + postJson);
BaiDuResultInfo resultInfo = JsonUtils.parse(postJson, BaiDuResultInfo.class);
if ("success".equals(resultInfo.getMsg())) {
return "success";
}
} catch (NuomiApiException e) {
e.printStackTrace();
}
return null;
}
申请退款
/**
* 百度小程序申请退款
*
* @param orderId
* @param userId
* @param tpOrderId
* @return
*/
public static BaiDuResultInfo applyOrderRefund(String orderId, String userId, String tpOrderId) {
try {
//核销的订单需要先取消核销 调用上面的核销接口,成功核销才可以申请退款
String result = syncOrderStatus(orderId, userId);
if (StringUtils.isBlank(result)) {
return null;
}
BaiDuRefund refund = new BaiDuRefund();
refund.setMethod("nuomi.cashier.applyorderrefund");
refund.setOrderId(orderId);
refund.setUserId(userId);
refund.setRefundType(1);
refund.setRefundReason(URLEncoder.encode("退款理由", "GBK"));
refund.setTpOrderId(tpOrderId);
refund.setAppKey(BaiDuConfig.APP_KEY_PAY);
Map<String, String> data = new HashMap<>();
data.put("method", refund.getMethod());
data.put("orderId", refund.getOrderId());
data.put("userId", refund.getUserId());
data.put("refundType", refund.getRefundType().toString());
data.put("refundReason", refund.getRefundReason());
data.put("tpOrderId", refund.getTpOrderId());
data.put("appKey", refund.getAppKey());
refund.setRsaSign(NuomiSignature.genSignWithRsa(data, BaiDuConfig.PRIVATE_KEY));
//参数也是得以map形式传递,这里当你发送请求,然后判断他的返回值是否正确即可,具体怎么用具体情况具体对待
String postJson = HttpRequest.doPost("https://nop.nuomi.com/nop/server/rest", JsonUtils.parseMap(JsonUtils.serialize(refund), String.class, Object.class));
BaiDuResultInfo resultInfo = JsonUtils.parse(postJson, BaiDuResultInfo.class);
if (resultInfo != null && "success".equals(resultInfo.getMsg())) {
return resultInfo;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
退款审核
当用户申请了退款之后,百度就会调用这个接口,来询问是否可以退款
/**
* 退款审核回调
*
* @param request
* @param response
* @param tpOrderId 商户订单号
* @param refundBatchId 退款批次号
* @param orderId 百度订单号
* @return
*/
@PostMapping("/refundCheckCallback")
public String refundCheckCallback(HttpServletRequest request, HttpServletResponse response,
@RequestParam("tpOrderId") String tpOrderId,
@RequestParam("refundBatchId") String refundBatchId,
@RequestParam("orderId") String orderId) {
//这里也没什么好说的,是否可以退款也是看逻辑,只要返回值按规定返回即可
//auditStatus 代表着是否可退,具体含义阅读文档
if (true) {
//可退款
return "{\"errno\":0,\"msg\":\"success\",\"data\":{\"auditStatus\":1, \"calculateRes\":{\"refundPayMoney\":" + 实际退款金额 + "}}}";
} else {
//不可退款
return "{\"errno\":0,\"msg\":\"success\",\"data\":{\"auditStatus\":2, \"calculateRes\":{\"refundPayMoney\":" + 实际退款金额 + "}}}";
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
退款回调
/**
* 退款回调
*
* @return
*/
@PostMapping("/refundCallback")
public String refundCallback(HttpServletRequest request, HttpServletResponse response,
@RequestParam("tpOrderId") String tpOrderId,
@RequestParam("refundBatchId") String refundBatchId,
@RequestParam("orderId") String orderId,
@RequestParam("refundStatus") String refundStatus) {
response.setHeader("Content-type", "text/html;charset=UTF-8");
try {
String reqParams = StreamUtil.read(request.getInputStream());
System.out.println("百度小程序退款回调结果:" + reqParams);
System.out.println("tpOrderId = " + tpOrderId);
System.out.println("refundBatchId = " + refundBatchId);
System.out.println("orderId = " + orderId);
//退款状态:1:退款成功,2:退款失败
System.out.println("refundStatus = " + refundStatus);
//代码主体略
return "{\"errno\":0,\"msg\":\"success\",\"data\":{}}";
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
获取手机号
官方虽然给了Java版本的示例代码,但是不知道为什么我这不能直接用,所以贴上一套自己的代码
public String getBaiDuUserPhone(String encrypdata,String code)
throws Exception {
byte[] original;
try {
//先获取sessionKey
String url = "https://spapi.baidu.com/oauth/jscode2sessionkey";
Map<String, String> data = new HashMap<>();
data.put("code", code);
data.put("client_id", BaiDuConfig.APP_ABROAD_KEY);
data.put("sk", BaiDuConfig.APP_ABROAD_SECRET);
String resultJson = HttpRequest.doGet(url, data);
JSONObject jsonObject = JSONObject.parseObject(resultJson);
byte[] aesKey = Base64.decodeBase64(jsonObject.get("session_key") + "=");
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
byte[] encrypted = Base64.decodeBase64(encrypdata);
original = cipher.doFinal(encrypted);
} catch (Exception e) {
throw new Exception(e);
}
String xmlContent;
String fromClientId;
try {
// 去除补位字符
byte[] bytes = pkcs7_unpad(original);
// 分离16位随机字符串,网络字节序和ClientId
byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
int xmlLength = recoverNetworkBytesOrder(networkOrder);
xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
fromClientId = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
} catch (Exception e) {
throw new Exception(e);
}
return xmlContent;
}
/**
* 还原4个字节的网络字节序
*
* @param orderBytes 字节码
* @return sourceNumber
*/
private int recoverNetworkBytesOrder(byte[] orderBytes) {
int sourceNumber = 0;
int length = 4;
int number = 8;
for (int i = 0; i < length; i++) {
sourceNumber <<= number;
sourceNumber |= orderBytes[i] & 0xff;
}
return sourceNumber;
}
private static byte[] pkcs7_unpad(byte[] data) throws Exception {
int lastValue = data[data.length - 1];
byte[] unpad = new byte[data.length - lastValue];
System.arraycopy(data, 0, unpad, 0, unpad.length);
return unpad;
}
可能出现的异常
java.security.InvalidKeyException: Illegal key size
异常原因:如果密钥大于128, 会抛出java.security.InvalidKeyException: Illegal key size 异常. 因为密钥长度是受限制的, java运行时环境读到的是受限的policy文件. 文件位于${java_home}/jre/lib/security, 这种限制是因为美国对软件出口的控制.
这一点官方给的示例代码上面也有相应的解释
下载链接:https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
下载好后,把解压后文件中的jar包放进${java_home}/jre/lib/security
即可
结语
支付回调,退款审核回调,退款回调,都是在申请支付的时候设置好的,这个不需要多说。
还有一点就是,百度退款慢的很,我测的时候都是等了十几二十分钟才给我退,而且我写在退款回调里的代码也是等很久才执行
其实写完发现挺简单的,就是写的时候要命了,哈哈哈