前言:
M-pesa:肯尼亚移动运营商Safaricom推出的手机银行业务。是依托于手机SIM卡进行支付的。
官网:https://www.safaricom.co.ke/dealers/login.php
开发者网站:https://developer.safaricom.co.ke/docs#going-live
正文:
业务背景:公司需要在官网上加一个注册为经销商的功能,其中有一个环节就是需要用户支付一定的金钱。业务范围是在肯尼亚。
流程:
需要先在肯尼亚那边注册一个商户账号,那边审批也要一定的时间,所以要提早去申请。通过之后,我们可以拿到商户编号,密钥等信息,它是有正式接口和测试接口的,可以先用测试接口调试。
后台需要写三个接口,一个是给前端的信息,一个是发送支付请求给Mpesa,一个是获取Mpesa的支付状态。
前端需要在用户点击支付之后调用后台的支付接口,然后不断地请求后台的支付状态接口,直到获取到状态为止。
用户那边的表现形式为,手机上会收到一个支付的弹窗,会有金额数和商户名称,用户输入密码即可完成支付。用户支付超时,弹窗还在,此时输入支付密码是不会支付成功的。在弹窗关闭后,用户在手机上是无法主动再次调出弹窗的,也没有相关的短信。
代码:
//支付相关 @Service public class PaymentService { public static Logger logger = Logger.getLogger(PaymentService.class.getName()); @Autowired private IDistributorDao distributorDao; //更新经销商信息 @Autowired private IPaymentBillDao paymentBillDao; //记录订单流水 /** 给前端显示的商户信息 * paymentVo:用户名,支付手机号等(传过来的手机号格式为 区号手机号,前面不要加+号,中间不要加横杠。254777777777) * PaybillVo:返回给前端的信息,订单号,金额等 */ public ResultComplete<PaybillVo> getPaybill(PaymentVo paymentVo) { logger.info("getPaybill begin; info:%s" + paymentVo.toString()); PaybillVo info = new PaybillVo(); info.setPaybill(MpesaConfig.COMPANY_NAME); //商户名称(与用户弹窗内的显示一致) info.setPayAmount(BaseView.PAY_AMOUNT); //金额(货币类型为肯尼亚先令) info.setOrderNo(this.getOrderNo()); //获取订单号 //处理支付流水。。。记录相关信息到支付流水表 return new ResultComplete<>(info); } //支付接口 public ResultComplete<String> payment(String payPhone, String orderNo) throws IOException { String amount = BaseView.PAY_AMOUNT; logger.info(String.format("payment begin; params: payPhone:%s, orderNo:%s", amount, payPhone, orderNo)); //调用M-pesa接口 String payInfo = Mpesa.STKPushSimulation(amount, payPhone, orderNo); //TODO test 测试的时候,因为我在国内,没有办法支付,所以把数据写死了。 // 由于是改造旧项目,工期比较紧,所以没有进行配置,直接是注释掉了,大家有时间可以优化一下,这里仅做参考 // String payInfo = "{" // + "\"MerchantRequestID\":\"6809-2590977-1\"," // + "\"CheckoutRequestID\":\"test\"," // + "\"ResponseCode\": \"0\"" // + "}"; Gson gson = new Gson(); Map map = gson.fromJson(payInfo, Map.class); String responseCode = (String) map.get("ResponseCode"); String checkoutRequestId = (String) map.get("CheckoutRequestID"); if (responseCode == null || Integer.parseInt(responseCode) != 0) { logger.error("payment request fail"); String payResultCode = responseCode; String payResultDesc = (String) map.get("CustomerMessage"); if (StringUtils.isBlank(responseCode)) { payResultCode = (String) map.get("errorCode"); payResultDesc = (String) map.get("errorMessage"); } //支付请求失败时更新支付流水表。。。 return new ResultComplete<>(false, 101, "支付请求失败", null); } //由于是旧项目的缘故,我用的session进行校验。其实不是很方便,可改为存到Redis HttpSession session = HttpHelper.getSession(); session.setAttribute(BaseView.SESSION_PAY_KEY + orderNo, payInfo); session.setMaxInactiveInterval(0); //支付请求成功时更新支付流水表。。。 logger.info("payment end"); return new ResultComplete<>(null); } //前端调用支付状态 public ResultComplete<String> getPaymentStatus(String orderNo) throws IOException { logger.info(String.format("getPaymentStatus begin; params: orderNo:%s", orderNo)); //TODO test String checkoutRequestId = getCheckoutRequestId(orderNo); //获取支付请求流水号 if (StringUtils.isBlank(checkoutRequestId)) { return new ResultComplete<>(false, 101, "未获取支付请求信息", null); } String statusInfo = Mpesa.STKPushTransactionStatus(checkoutRequestId); // String statusInfo = "{" // + "\"MerchantRequestID\":\"6809-2590977-1\"," // + "\"CheckoutRequestID\":\"test\"," // + "\"ResponseCode\": \"0\"," // + "\"ResultCode\": \"0\"," // + "\"ResultDesc\": \"Success\"" // + "}"; Gson gson = new Gson(); Map map = gson.fromJson(statusInfo, Map.class); String responseCode = (String) map.get("ResponseCode"); String resultCode = (String) map.get("ResultCode"); String resultDesc = (String) map.get("ResultDesc"); if (responseCode == null || Integer.parseInt(responseCode) != 0 || resultCode == null || Integer.parseInt(resultCode) != 0) { logger.error("payment status fail"); String errorCode = (String) map.get("errorCode"); String errorMessage = (String) map.get("errorMessage"); if (errorCode != null && errorCode.equals("500.001.1001")) { return new ResultComplete<>(true, 201, "支付正在处理中", null); } String payResultCode = StringUtils.isNotBlank(resultCode) ? resultCode : errorCode; String payResultDesc = StringUtils.isNotBlank(resultDesc) ? resultDesc : errorMessage; //更新流水 return new ResultComplete<>(false, 102, "支付失败", null); } HttpSession session = HttpHelper.getSession(); session.setAttribute(BaseView.SESSION_PAY_STATUS_KEY + orderNo, statusInfo); session.setMaxInactiveInterval(0); //更新流水 logger.info("getPaymentStatus end"); return new ResultComplete<>(null); } //获取订单号(我定义的是PAY-20190817-000001这样,可自己视情况而定) private String getOrderNo() { String nowDate = CalendarUtil.DtoSYmd(new Date()); String