#微信支付服务商接入指引

本文主要针对服务商下特约商户的小程序支付进行讲解。(扫码支付, h5支付大致流程都差不多,了解了小程序支付能够很快接入其他支付类型)

说明:本文中的支付都是指在服务商模式下

##支付主体

  • 服务商:拥有支付开发能力的第三方提供商
  • 普通商户: 拥有开发能力的商户
  • 特约商户:服务商下的商户

一个商家主体可以在不同服务商下申请特约商户,每个服务商都会给商家主体在此服务商下一个特约商户号。

普通商户申请需要花费大约300RMB,服务商申请特约商户不需要费用。

一个商家主体可以申请 普通商户,特约商户。同一个商户主体申请的普通商户与在服务商下申请的特约商户号是独立的。

##服务商

服务商下的特约商户的资金流转不会直接经过服务商的支付账户,最终消费者的资金直接和服务商下的特约商户进行来往,但是服务商可以查看自己下的特约商户资金流水。

服务商小程序开发文档

##开发支付
###开发之前
申请注册服务商,通过之后登录微信商户平台,进入菜单: 服务商功能 --> 特约商户管理 -->新增商户(也就是申请服务商下的特约商户)
申请如果没有问题会在三到五天通过,之后可以在特约商户管理
下看到服务商自己的特约商户,我们在开发中需要 服务商商户号及这里的商户号(特约商户号)
###支付需要接口:微信统一下单,及提供给微信的回调接口

微信官方给的业务流程图:

特约商户进件java 特约商户入账_xml

可以很清晰的理解业务流程走向。
###统一下单接口

微信统一下单请求参数

统一下单请求参数封装为我们可以处理的对象:

此处我的命名是: WechatUnifiedorderRequest

以下是我开发中遇到一些坑,主要是由于微信官方的文档给的参数很模糊,特别是小程序支付。

名称

描述

appid

公众号appid

mch_id

服务商号

sub_appid

小程序的appId

notify-url

微信回调地址

trade_type

交易类型 JSAPI:小程序 NATIVE:扫码支付

nonce_str

32位随机字符

sub_mch_id

特约商户号

totalPrice

支付金额单位分

spbill_create_ip

发起支付者的IP

sub_openid

发起支付的微信统一标识

####对我们填充的值按照字典排序,连接key进行签名,以xml格式字符向微信发起请求

在填充好了WechatUnifiedorderRequest对象后

  1. 我们需要对对象按照字典序排序

第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。

  1. 字典排序后的字符连接key(需要在微信商户平台进行配置建议使用UUID生成32位)
  2. MD5加密签名,得到sign填充WechatUnifiedorderRequest对象
  3. WechatUnifiedorderRequest转换为微信需要的xml类型
  4. 发起请求
  5. 得到微信统一下单的响应(是xml字符格式),解析为对象(对返回的响应封装对象进行处理WechatUnifiedorderResponse),
  6. 对返回的对象进行验证,通过验证返回给小程序 需要的参数及签名 小程序调起支付API
  7. 小程序支付成功,微信开始回调在统一下单传给微信的回调地址

#####获取下单用户的真实IP

/**
     * 获取用户真实IP
     * 如果有代理,获取真实客户端IP
     * @param request
     * @return
     */
    public  static  String getRealId(HttpServletRequest request){

        String xForwardedForHeader= request.getHeader("X-Forwarded-For");
        if(xForwardedForHeader == null){
            return  request.getRemoteAddr();

        }else {
            return  new StringTokenizer(xForwardedForHeader, ",").nextToken().trim();
        }

    }

#####按照字典序排序

/**
     * 使用java反射机制,动态获取对象的属性和参数值,排除值为null的情况,并按字典序排序
     * @param object
     * @return
     */
    public static   String getSortMap(Object object) throws  Exception{
        //1.得到属性的名称及值 如果为null不存入map
        Field [] fields = object.getClass().getDeclaredFields();
        Map<String,String> map = new HashMap<>();
        for(Field field : fields){
            String name = field.getName();
            /*String methodName = "get"+name.replaceFirst(name.substring(0, 1), name.substring(0, 1)
                    .toUpperCase());*/
            //通过get方法直接获取属性值
            field.setAccessible(true);
            Object value = field.get(object);
            if (value != null){
                map.put(name, value.toString());
            }


        }
        //排序
        Map<String, String> sortMap = new TreeMap<String,String>(
                new Comparator<String>() {

                    @Override
                    public int compare(String arg0, String arg1) {

                        return arg0.compareTo(arg1);
                    }
                });
        sortMap.putAll(map);


        StringBuilder sortFeil = new StringBuilder();
        //得到键值对的格式(即key1=value1&key2=value2…
        sortMap.forEach((k,v)-> {
            sortFeil.append(k+"="+v+"&");
        });
        //移除最后一个 &
        sortFeil.deleteCharAt(sortFeil.length()-1);
        return sortFeil.toString();

    }

使用字典序返回的字符连接key,使用MD5进行加密,得到sign

#####WechatUnifiedorderRequest转换为微信需要的xml类型

在WechatUnifiedorderRequest对象上使用注解

  • @xmlAccessorType @xmlAccessorType(XmlAccessType.FIELD)
  • @xmlRootElement @xmlRootElement(name =“xml”) ( name = "xml : "WechatUnifiedorderReques对象转换为xml的根名称)
/**
 * 微信统一下单请求对象
 *
 * @Author xuelongjiang
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "xml")//xml的根元素
public class WechatUnifiedorderRequest  implements Serializable{
}

对象转换为xml字符
引入包:import javax.xml.bind.JAXBContext

/**
     * 对象转换为xml
     * @param object
     * @return
     */
    public static  String objectToXml(Object object){

        StringWriter sw = new StringWriter();
        try {

            JAXBContext context = JAXBContext.newInstance(object.getClass());
            Marshaller marshaller =  context.createMarshaller();
            marshaller.marshal(object,sw);

        }catch (Exception e){
            e.printStackTrace();
            logger.error("对象解析xml出现异常,对象为"+object.toString());
        }

        return sw.toString();
    }
得到微信统一下单的响应(是xml字符格式),解析为对象

封装对象:WechatUnifiedorderResponse 表示微信统一下单响应的对象。

请求微信统一下单返回示例:

<xml>
   <return_code><![CDATA[SUCCESS]]></return_code>
   <return_msg><![CDATA[OK]]></return_msg>
   <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
   <sub_appid><![CDATA[wx2421b1c4370ec11b]]></sub_appid>
   <mch_id><![CDATA[10000100]]></mch_id>
   <sub_mch_id>![CDATA[10000101]]></appid>
   <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
   <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
   <result_code><![CDATA[SUCCESS]]></result_code>
   <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
   <trade_type><![CDATA[JSAPI]]></trade_type>
</xml>

参数值用XML转义即可,CDATA标签用于说明数据不被XML解析器解析,在转为对象的时候我们需要解析
<![CDATA[]]>

WechatUnifiedorderResponse对象使用注解

  • @XmlAccessorType(XmlAccessType.FIELD)
  • @XmlRootElement(name = “xml”)//解析xml的根元素
    以上的和WechatUnifiedorderRequest是一样,但是由于需要解析**<![CDATA[]]>,我们创建CDataAdapter继承XmlAdapter ,使用注解@XmlJavaTypeAdapter来处理,在WechatUnifiedorderResponse需要处理<![CDATA[]]>**的域上使用注解

如下:

@XmlJavaTypeAdapter(CDataAdapter.class)// 解析<![CDATA[]]>
    private String return_code; //返回状态码

######CDataAdapter解析<![CDATA[]]>

/**
 *
 * 注解使用, 对象与xml转换的字段需要有 <![CDATA[]]>
 *
 * @Author xuelongjiang
 */
public class CDataAdapter extends XmlAdapter<String,String> {

    private static Logger logger = LoggerFactory.getLogger(CDataAdapter.class);

    /**
     * Do-nothing constructor for the derived classes.
     */
    protected CDataAdapter() {
        super();
    }

    /**
     * Convert a value type to a bound type.
     *
     * @param v The value to be converted. Can be null.
     * @throws Exception if there's an error during the conversion. The caller is responsible for
     *                   reporting the error to the user through {@link ValidationEventHandler}.
     */
    @Override
    public String unmarshal(String v) throws Exception {

      if("<![CDATA[]]>".equals(v)){
          return "";
      }
      String v1 = null;
      String v2 = null;

      String subStart = "<![CDATA[";
      int a = v.indexOf(subStart);
      if(a>= 0){
          v1 = v.substring(subStart.length(),v.length());

      }else {
          return v;
      }
      String subEnd = "]]>";
      int b = v1.indexOf(subEnd);
      if(b>= 0){
          v2 = v1.substring(0,b);
      }
      return v2;

    }

    /**
     * Convert a bound type to a value type.
     *
     * @param v The value to be convereted. Can be null.
     * @throws Exception if there's an error during the conversion. The caller is responsible for
     *                   reporting the error to the user through {@link ValidationEventHandler}.
     */
    @Override
    public String marshal(String v) throws Exception {

        logger.info("对象转换xml:"+"<![CDATA["+ v +"]]>");
        return "<![CDATA["+ v +"]]>";
    }
}

到此为止,我们已经得到微信统一下单的响应值了,后续的处理不是很复杂。按照文档不会有很大的坑。

在做微信支付的时候,难点是以上的:请求参数说明模糊,在经历几次的传参试验及百度谷歌之后,才明白了参数的具体的使用,其实后续在做扫码支付的时候,发现扫码支付解释的比较清楚,小程序的文档确实比较坑。