对于后端来说,百度小程序最重要的无非也就登录,支付退款等接口最为重要,所以此文就记录一下自己的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即可

结语

支付回调,退款审核回调,退款回调,都是在申请支付的时候设置好的,这个不需要多说。
还有一点就是,百度退款慢的很,我测的时候都是等了十几二十分钟才给我退,而且我写在退款回调里的代码也是等很久才执行

其实写完发现挺简单的,就是写的时候要命了,哈哈哈