银商大华捷通平台对接代收款接口规范
银商大华捷通平台对接代收款接口规范
最近业务用到与大华捷通代收款(POS支付),要与其对接获取订单数据和支付通知两个接口。主要流程如下:
1. 中心系统产生订单,生成包含订单号的二维码,并显示在店铺的APP页面上;
2. 大华POS通过扫描店铺APP页面二维码获取到订单号,中心接收到请求要核对POS的机具***是否正确;
3. 大华POS根据此订单号向中心系统发送请求,获取到订单金额等数据;
4. 大华POS根据订单号、订单金额等数据跳转生成支付页面,由客户支付;
5. 支付成功,进行支付回调通知,中心系统收到通知相应改变订单状态。
支付流程图
相关文档:
银商大华捷通平台与第三方物流ERP系统接口规范-完整版_V2.7.3.pdf
主要问题: MAC加密验证主要代码:
import org.apache.commons.lang3.StringUtils; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.io.OutputFormat; import org.dom4j.io.XMLWriter; import org.jsoup.Jsoup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.StringWriter; import java.math.BigDecimal; import java.security.MessageDigest; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * Created by think on 2017/4/12. */ @Service public class DahuaPaymentServiceImpl extends BaseJpaServiceImpl<PayPaymentEntity,Long,PayPaymentDaoImpl> implements DahuaPaymentService { private Logger log = LoggerFactory.getLogger(DahuaPaymentServiceImpl.class); //双方预定约好的MAC——32位 @Value("${dahua.mac.appointed}") private String MAC_APPOINTED; @Autowired private ShopOrderService shopOrderService; @Autowired private PayPaymentService paymentService; @Autowired private WkOrderService wkOrderService; /** * 通过订单SN号获取订单信息 * @param request * @param response * @throws IOException */ @RequestMapping(value = "/getOrderData", method = RequestMethod.POST) public void getOrderData(HttpServletRequest request, HttpServletResponse response) throws IOException { log.debug("大华获取订单数据开始"); String result = request.getParameter("context"); log.debug("大华获取订单数据请求报文: " + result); String mac = Jsoup.parse(result).select("mac").html(); if(!checkSignMac(result)){ response.getWriter().write(errorXml(DahuaResponseCodeEnum.MAC_ERROR.getCode(), DahuaResponseCodeEnum.MAC_ERROR.getName())); return; } String orderSn = Jsoup.parse(result).select("orderno").html(); String version = Jsoup.parse(result).select("version").html(); String employno = Jsoup.parse(result).select("employno").html(); String termid = Jsoup.parse(result).select("termid").html(); if(null == termid || termid.length() < 1){ response.getWriter().write(errorXml("05", "缺少设备ID信息(termid)")); return; } QueryFilter filter = QueryFilterBuilder.of() .joinFetch("t.mbTenant") .eq("t.sn", orderSn) .build(); ShopOrderEntity entity = shopOrderService.get(filter); //判断是否存在这个订单 if(null == entity || null == entity.getId()){ response.getWriter().write(errorXml(DahuaResponseCodeEnum.NON_ORDER.getCode(), DahuaResponseCodeEnum.NON_ORDER.getName())); return; } //判断工单的店铺信息是否正常 if(null == entity.getMbTenant() || null == entity.getMbTenant().getId()){ response.getWriter().write(errorXml(DahuaResponseCodeEnum.NOT_THIS_SHOP.getCode(), DahuaResponseCodeEnum.NOT_THIS_SHOP.getName())); return; } //判断设备号是否相符 if(!termid.equals(entity.getMbTenant().getPosSn())){ response.getWriter().write(errorXml(DahuaResponseCodeEnum.NOT_THIS_SHOP.getCode(), DahuaResponseCodeEnum.NOT_THIS_SHOP.getName())); return; } // 更改交易发起时间 Date sendPaymentDate = new Date(); //发起交易时间,与响应报文的响应时间相同 PayPaymentEntity paymentEntity = paymentService.getByOrderSn(orderSn); if(null != paymentEntity){ paymentEntity.setSendPaymentDate(sendPaymentDate); paymentService.updateSelect(paymentEntity); } //response.getWriter().write(createHtml(map, "UTF-8")); Document document = DocumentHelper.createDocument(); Element rootElement = document.addElement("transaction"); Element headerElement = rootElement.addElement("transaction_header"); headerElement.addElement("version").setText(version); headerElement.addElement("transtype").setText("P004"); headerElement.addElement("employno").setText(employno); headerElement.addElement("termid").setText(termid); headerElement.addElement("response_time").setText(formatTime(sendPaymentDate)); headerElement.addElement("response_code").setText("00"); headerElement.addElement("response_msg").setText("获取订单数据成功"); headerElement.addElement("mac").setText(mac); Element bodyElement = rootElement.addElement("transaction_body"); bodyElement.addElement("netcode").setText(""); bodyElement.addElement("netname").setText(""); bodyElement.addElement("weight").setText(""); //金额 bodyElement.addElement("cod").setText(this.formatDecimal(entity.getOffsetAmount()).toString()); //运费 bodyElement.addElement("fee").setText(""); bodyElement.addElement("goodscount").setText(""); bodyElement.addElement("address").setText(""); bodyElement.addElement("people").setText(""); bodyElement.addElement("peopletel").setText(""); bodyElement.addElement("status").setText("02"); bodyElement.addElement("memo").setText(""); bodyElement.addElement("dssn").setText(""); bodyElement.addElement("dsname").setText(""); bodyElement.addElement("dsorderno").setText(""); bodyElement.addElement("dlvryno").setText(""); //bodyElement.addElement("buzitype").setText("1"); response.getWriter().write(signature(asXml(document))); return; } /** * 支付成功的回调通知 * @param request * @param response * @throws IOException */ @RequestMapping(value = "/notify", method = RequestMethod.POST) public void notify(HttpServletRequest request, HttpServletResponse response) throws IOException { log.debug("大华支付后台通知开始"); String result = request.getParameter("context"); log.debug("大华支付后台通知请求报文: " + result); String mac = Jsoup.parse(result).select("mac").html(); if(!checkSignMac(result)){ response.getWriter().write(errorXml(DahuaResponseCodeEnum.MAC_ERROR.getCode(), DahuaResponseCodeEnum.MAC_ERROR.getName())); return; } String orderSn = Jsoup.parse(result).select("orderno").html(); String payway = Jsoup.parse(result).select("payway").html(); String traceTime = Jsoup.parse(result).select("traceTime").html(); String cardid = Jsoup.parse(result).select("cardid").html(); String cod = Jsoup.parse(result).select("cod").html(); String postrace = Jsoup.parse(result).select("postrace").html(); //POS机的流水号 String memo = Jsoup.parse(result).select("memo").html(); //memo内为json格式数据 String version = Jsoup.parse(result).select("version").html(); String employno = Jsoup.parse(result).select("employno").html(); String termid = Jsoup.parse(result).select("termid").html(); if(null == termid || termid.length() < 1){ response.getWriter().write(errorXml("05", "缺少设备ID信息(termid)")); return; } QueryFilter filter = QueryFilterBuilder.of() .joinFetch("t.mbTenant") .eq("t.sn", orderSn) .build(); ShopOrderEntity entity = shopOrderService.get(filter); //判断是否存在这个订单 if(null == entity || null == entity.getId()){ response.getWriter().write(errorXml(DahuaResponseCodeEnum.NON_ORDER.getCode(), DahuaResponseCodeEnum.NON_ORDER.getName())); return; } //判断工单的店铺信息是否正常 if(null == entity.getMbTenant() || null == entity.getMbTenant().getId()){ response.getWriter().write(errorXml(DahuaResponseCodeEnum.NOT_THIS_SHOP.getCode(), DahuaResponseCodeEnum.NOT_THIS_SHOP.getName())); return; } //判断设备号是否相符 if(!termid.equals(entity.getMbTenant().getPosSn())){ response.getWriter().write(errorXml(DahuaResponseCodeEnum.NOT_THIS_SHOP.getCode(), DahuaResponseCodeEnum.NOT_THIS_SHOP.getName())); return; } //收到回调后,对涉及资金类的交易,请再发起查询接口查询,确定交易成功后更新数据库。 { // 交易成功 PayPaymentEntity paymentEntity = paymentService.getByOrderSn(orderSn); if(PaymentStatusEnum.SUCCESS.getCode().equals(paymentEntity.getStatusCode().getCode())){ response.getWriter().write(errorXml(DahuaResponseCodeEnum.PAID.getCode(), DahuaResponseCodeEnum.PAID.getName())); return; } paymentEntity.setStatusCode(new ComDataDictionaryEntity(PaymentStatusEnum.SUCCESS.getCode())); if(!StringUtils.isEmpty(cod)){ paymentEntity.setAmount(formatDecimal(new BigDecimal(cod))); }else{ paymentEntity.setAmount(formatDecimal(new BigDecimal(0))); } paymentEntity.setPaymentSn(postrace); //设置为POS的流水号 paymentEntity.setPaymentDate(formatTime(traceTime)); paymentEntity.setMethod(payway); paymentEntity.setPayCardNo(cardid); paymentEntity.setMemo(memo); String jsonResult = ""; paymentEntity.setPaymengResult(jsonResult); paymentService.updateSelect(paymentEntity); //改变工单状态 wkOrderService.changeWkOrderStatusToPay(orderSn); } //response.getWriter().write(createHtml(map, "UTF-8")); Document document = DocumentHelper.createDocument(); Element rootElement = document.addElement("transaction"); Element headerElement = rootElement.addElement("transaction_header"); headerElement.addElement("version").setText(version); headerElement.addElement("transtype").setText("P003"); headerElement.addElement("employno").setText(employno); headerElement.addElement("termid").setText(termid); headerElement.addElement("response_time").setText(formatTime(new Date())); headerElement.addElement("response_code").setText("00"); headerElement.addElement("response_msg").setText("交易成功"); headerElement.addElement("mac").setText(mac); Element bodyElement = rootElement.addElement("transaction_body"); response.getWriter().write(signature(asXml(document))); return; } /** * 时间格式化 * @param date * @return */ private String formatTime(Date date){ return new SimpleDateFormat("yyyyMMddHHmmss").format(date); } private Date formatTime(String dateStr){ try { return new SimpleDateFormat("yyyyMMddHHmmss").parse(dateStr); } catch (ParseException e) { e.printStackTrace(); return null; } } /** * 签名加密 * @param xmlStr * @return */ private String signature(String xmlStr){ //第一步:约定32字节签名密文 String macAppointed = MAC_APPOINTED; Document document = null; try { document = DocumentHelper.parseText(xmlStr); Element rootElement = document.getRootElement(); Element headerElement = rootElement.element("transaction_header"); Element macElement = headerElement.element("mac"); //第二步:去除“mac“节点 headerElement.remove(macElement); //第三步:约定密文附加在xml后面,组成待签名密文 String signXml = asXml(document); signXml = signXml.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", ""); //去掉表头 signXml = signXml.replace("<?xml version=\"1.0\" encoding=\"utf-8\"?>", ""); //去掉表头 signXml = signXml.replace("context=", ""); //去掉context= signXml = signXml.replace("\n", ""); //去掉回车 signXml = signXml.replace(" ", ""); //去掉空格 signXml = signXml + macAppointed; //加约定密文 //第四叔:待签名密文进行MD5签名 String mac = this.MD5(signXml); //第五步:MD5值放到mac节点中 headerElement.addElement("mac").setText(mac); } catch (DocumentException e) { e.printStackTrace(); } return asXml(document); } /** * 验证MAC * @param xmlStr * @return */ private boolean checkSignMac(String xmlStr){ //第一步:约定32字节签名密文 String macAppointed = MAC_APPOINTED; String macReceive; Document document = null; try { document = DocumentHelper.parseText(xmlStr); Element rootElement = document.getRootElement(); Element headerElement = rootElement.element("transaction_header"); Element macElement = headerElement.element("mac"); macReceive = macElement.getText(); log.debug("大华请求报文MAC校验,接收到的MAC: " + macReceive); //第二步:去除“mac“节点 headerElement.remove(macElement); //第三步:约定密文附加在xml后面,组成待签名密文 String signXml = asXml(document); signXml = signXml.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", ""); //去掉表头 signXml = signXml.replace("<?xml version=\"1.0\" encoding=\"utf-8\"?>", ""); //去掉表头 signXml = signXml.replace("context=", ""); //去掉context= signXml = signXml.replace("\n", ""); //去掉回车 signXml = signXml.replace(" ", ""); //去掉空格 signXml = signXml + macAppointed; //加约定密文 log.debug("大华请求报文MAC校验,待加密XML: " + signXml); //第四步:待签名密文进行MD5签名 String mac = this.MD5(signXml); log.debug("大华请求报文MAC校验,加密的MAC: " + mac); if(null != macReceive && macReceive.length() > 0 && macReceive.equals(mac)){ log.debug("大华请求报文MAC校验成功"); return true; }else{ log.error("大华请求报文MAC校验失败"); return false; } } catch (DocumentException e) { e.printStackTrace(); log.error("大华请求报文MAC校验失败: " + e.getMessage()); return false; } } private String MD5(String s) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(s.getBytes("utf-8")); return toHex(bytes); } catch (Exception e) { throw new RuntimeException(e); } } private static String toHex(byte[] bytes) { final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); StringBuilder ret = new StringBuilder(bytes.length * 2); for (int i=0; i<bytes.length; i++) { ret.append(HEX_DIGITS[(bytes[i] >> 4) & 0x0f]); ret.append(HEX_DIGITS[bytes[i] & 0x0f]); } return ret.toString(); } /** * 返回错误信息 * @param errorCode * @param errorStr * @return */ private String errorXml(String errorCode, String errorStr){ log.error("ERROR: " + "errorCode " + errorCode + " errorStr " + errorStr); Document document = DocumentHelper.createDocument(); Element rootElement = document.addElement("transaction"); Element headerElement = rootElement.addElement("transaction_header"); headerElement.addElement("response_code").setText(errorCode); headerElement.addElement("response_msg").setText(errorStr); Element bodyElement = rootElement.addElement("transaction_body"); return asXml(document); } //转换为标准格式(避免自闭合的问题) private String asXml(Document document){ OutputFormat format = new OutputFormat(); format.setEncoding("UTF-8"); //format.setExpandEmptyElements(true); StringWriter out = new StringWriter(); XMLWriter writer = new XMLWriter(out, format); try { writer.write(document); writer.flush(); } catch (IOException e) { e.printStackTrace(); } return out.toString(); } //保留小数据点后两位 private BigDecimal formatDecimal(BigDecimal amount){ if(null != amount){ amount = amount.setScale(2, BigDecimal.ROUND_HALF_UP); } return amount; } }
ENUM也贴上
public enum DahuaResponseCodeEnum { SUCCESS("成功", "00"), NON_ORDER("订单不存在", "H3"), MAC_ERROR("数据校验错误", "A0"), NOT_THIS_SHOP("不属于本店订单", "H1"), PAID("已支付", "35"), CANCELED("已取消", "36"); private String name; private String code; DahuaResponseCodeEnum(String name, String code) { this.name = name; this.code = code; } public String getCode() { return this.code; } public String getName() { return this.name; } public static String getName(String code) { for (DahuaResponseCodeEnum c : DahuaResponseCodeEnum.values()) { if (code.equals(c.getCode())) { return c.name; } } return null; } }