银商大华捷通平台对接代收款接口规范

银商大华捷通平台对接代收款接口规范

最近业务用到与大华捷通代收款(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;
    }
}