构造发起支付的请求对象,该对象是根据微信支付文档来的
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JacksonXmlRootElement(localName = "xml")
public class WeChatPayRequest {
@JacksonXmlProperty(localName = "appid")
private String appId;
@JacksonXmlProperty(localName = "mch_id")
private String mchId;
@JacksonXmlProperty(localName = "device_info")
private String deviceInfo; // 非必填
@JacksonXmlProperty(localName = "nonce_str")
private String nonceStr;
@JacksonXmlProperty(localName = "sign")
private String sign;
@JacksonXmlProperty(localName = "sign_type")
private String signType; // 非必填
@JacksonXmlProperty(localName = "body")
private String body;
@JacksonXmlProperty(localName = "details")
private String details; // 非必填
@JacksonXmlProperty(localName = "attach")
private String attach; // 非必填
@JacksonXmlProperty(localName = "out_trade_no")
private String outTradeNo;
@JacksonXmlProperty(localName = "fee_type")
private String feeType; // 非必填
@JacksonXmlProperty(localName = "total_fee")
private String totalFee;
@JacksonXmlProperty(localName = "spbill_create_ip")
private String spbillCreateIp;
@JacksonXmlProperty(localName = "time_start")
private String timeStart; // 非必填
@JacksonXmlProperty(localName = "time_expire")
private String timeExpire; // 非必填
@JacksonXmlProperty(localName = "goods_tag")
private String goodsTag; // 非必填
@JacksonXmlProperty(localName = "notify_url")
private String notifyUrl;
@JacksonXmlProperty(localName = "trade_type")
private String tradeType;
@JacksonXmlProperty(localName = "limit_pay")
private String limitPay; // 非必填
@JacksonXmlProperty(localName = "scene_info")
private String sceneInfo;
}
调用微信支付统一下单接口,本例使用Spring Cloud Feign(申明式RESTful API),具体使用方法不是本例的重点,请参考相关文档。
package com.hfcsbc.pay.service.feign;
import com.hfcsbc.pay.config.WeChatCodecConfig;
import com.hfcsbc.pay.dto.wechat.miniApp.WeChatMiniAppPayRequest;
import com.hfcsbc.pay.dto.WeChatPayResponse;
import com.hfcsbc.pay.dto.WeChatPayRequest;
import com.hfcsbc.pay.dto.wechat.openApp.WeChatOpenAppPayRequest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* Created by lijian on 2017/9/27
*/
@FeignClient(value = "weChatClient", url = "https://api.mch.weixin.qq.com", configuration = WeChatCodecConfig.class)
public interface WeChatClient {
@PostMapping(value = "/pay/unifiedorder", consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
WeChatPayResponse unifiedOrder(@RequestBody WeChatPayRequest request);
}
package com.hfcsbc.pay.config;
import feign.codec.Decoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder;
import org.springframework.cloud.netflix.feign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import java.util.ArrayList;
import java.util.List;
/**
* Created by lijian on 2017/9/28
*/
public class WeChatCodecConfig {
@Bean
public Decoder feignDecoder() {
MappingJackson2XmlHttpMessageConverter xmlConverter = new MappingJackson2XmlHttpMessageConverter();
List<MediaType> types = xmlConverter.getSupportedMediaTypes();
List<MediaType> newTypes = new ArrayList<MediaType>();
newTypes.add(MediaType.TEXT_PLAIN);
for(MediaType t: types) {
newTypes.add(t);
}
xmlConverter.setSupportedMediaTypes(newTypes);
ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(xmlConverter);
return new ResponseEntityDecoder(new SpringDecoder(objectFactory));
}
}
统一下单接口返回的对象
package com.hfcsbc.pay.dto;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Created by lijian on 2017/10/25
*/
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@JacksonXmlRootElement(localName = "xml")
public class WeChatPayResponse {
@JacksonXmlProperty(localName = "return_code")
private String returnCode;
@JacksonXmlProperty(localName = "return_msg")
private String returnMsg;
@JacksonXmlProperty(localName = "appid")
private String appId;
@JacksonXmlProperty(localName = "mch_id")
private String mchId;
@JacksonXmlProperty(localName = "device_info")
private String deviceInfo;
@JacksonXmlProperty(localName = "nonce_str")
private String nonceStr;
private String sign;
@JacksonXmlProperty(localName = "result_code")
private String resultCode;
@JacksonXmlProperty(localName = "err_code")
private String errCode;
@JacksonXmlProperty(localName = "err_code_des")
private String errCodeDes;
@JacksonXmlProperty(localName = "trade_type")
private String tradeType;
@JacksonXmlProperty(localName = "prepay_id")
private String prepayId;
@JacksonXmlProperty(localName = "code_url")
private String codeUrl;
private String tradeId; // 此处是自定义的参数,不是微信服务器返回参数
public Boolean isSuccess(){
return "SUCCESS".equals(this.returnCode) && "SUCCESS".equals(this.resultCode);
}
public Boolean returnCodeIsSuccess(){
return "SUCCESS".equals(this.returnCode);
}
public void populateOutTradeId(String tradeId){
this.tradeId = tradeId;
}
}
支付完成回调的对象
package com.hfcsbc.orderservice.command.wechat;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.hfcsbc.orderservice.util.WeChatUtils;
import lombok.*;
/**
* Created by lijian on 2017/9/29
*/
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JacksonXmlRootElement(localName = "xml")
public class WeChatNotifyCmd {
@JacksonXmlProperty(localName = "appid")
private String appId;
@JacksonXmlProperty(localName = "mch_id")
private String mchId;
@JacksonXmlProperty(localName = "device_info")
private String deviceInfo; // 非必填
@JacksonXmlProperty(localName = "nonce_str")
private String nonceStr;
@JacksonXmlProperty(localName = "sign")
private String sign;
@JacksonXmlProperty(localName = "sign_type")
private String signType;
@JacksonXmlProperty(localName = "result_code")
private String resultCode;
@JacksonXmlProperty(localName = "return_code")
private String returnCode;
@JacksonXmlProperty(localName = "return_msg")
private String returnMsg;
@JacksonXmlProperty(localName = "err_code")
private String errCode;
@JacksonXmlProperty(localName = "err_code_des")
private String errCodeDes;
@JacksonXmlProperty(localName = "openid")
private String openId;
@JacksonXmlProperty(localName = "is_subscribe")
private String isSubscribe;
@JacksonXmlProperty(localName = "trade_type")
private String tradeType;
@JacksonXmlProperty(localName = "bank_type")
private String bankType;
@JacksonXmlProperty(localName = "total_fee")
private String totalFee;
@JacksonXmlProperty(localName = "settlement_total_fee")
private String settlementTotalFee;
@JacksonXmlProperty(localName = "fee_type")
private String feeType;
@JacksonXmlProperty(localName = "cash_fee")
private String cashFee;
@JacksonXmlProperty(localName = "cash_fee_type")
private String cashFeeType;
@JacksonXmlProperty(localName = "coupon_fee")
private String couponFee;
@JacksonXmlProperty(localName = "coupon_count")
private String couponCount;
@JacksonXmlProperty(localName = "coupon_type_0")
private String couponType0;
@JacksonXmlProperty(localName = "coupon_type_1")
private String couponType1;
@JacksonXmlProperty(localName = "coupon_type_2")
private String couponType2;
@JacksonXmlProperty(localName = "coupon_type_3")
private String couponType3;
@JacksonXmlProperty(localName = "coupon_type_4")
private String couponType4;
@JacksonXmlProperty(localName = "coupon_id_0")
private String couponId0;
@JacksonXmlProperty(localName = "coupon_id_1")
private String couponId1;
@JacksonXmlProperty(localName = "coupon_id_2")
private String couponId2;
@JacksonXmlProperty(localName = "coupon_id_3")
private String couponId3;
@JacksonXmlProperty(localName = "coupon_id_4")
private String couponId4;
@JacksonXmlProperty(localName = "coupon_fee_0")
private String couponFee0;
@JacksonXmlProperty(localName = "coupon_fee_1")
private String couponFee1;
@JacksonXmlProperty(localName = "coupon_fee_2")
private String couponFee2;
@JacksonXmlProperty(localName = "coupon_fee_3")
private String couponFee3;
@JacksonXmlProperty(localName = "coupon_fee_4")
private String couponFee4;
@JacksonXmlProperty(localName = "transaction_id")
private String transactionId;
@JacksonXmlProperty(localName = "out_trade_no")
private String outTradeNo;
@JacksonXmlProperty(localName = "attach")
private String attach;
@JacksonXmlProperty(localName = "time_end")
private String timeEnd;
public Boolean ifValid(){
return ifResultSuccess() && ifSignValid();
}
public Boolean ifOpenAppValid(){
return ifResultSuccess() && ifOpenAppSignValid();
}
private Boolean ifResultSuccess(){
return "SUCCESS".equals(this.returnCode) && "SUCCESS".equals(this.resultCode);
}
private Boolean ifSignValid(){
return WeChatUtils.generateSign(this).equals(this.sign);
}
private Boolean ifOpenAppSignValid(){
return WeChatUtils.generateOpenAppSign(this).equals(this.sign);
}
}
生成签名的工具类
package com.hfcsbc.orderservice.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.hfcsbc.orderservice.constant.WeChatProperty;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import java.util.Map;
import java.util.TreeMap;
import static com.google.common.collect.Maps.newTreeMap;
/**
* Created by lijian on 2017/9/27
*/
@Slf4j
public class WeChatUtils {
/**
* 微信app支付、扫码支付 根据请求对象生成签名,具体步骤:
* 1.将对象转换成按字典排序的map
* 2.将map拼装成排序字符串,并拼接key
* 3.md5编码,并转成大写
* @param request
* @return
*/
public static String generateSign(Object request){
Map<String, String> sortedMap = convertToSortedMap(request);
String sortRequest = convertToStr(sortedMap) + "key=" + WeChatProperty.KEY;
return DigestUtils.md5Hex(sortRequest).toUpperCase();
}
public static String generateOpenAppSign(Object request){
Map<String, String> sortedMap = convertToSortedMap(request);
String sortRequest = convertToStr(sortedMap) + "key=" + WeChatProperty.OPEN_KEY;
return DigestUtils.md5Hex(sortRequest).toUpperCase();
}
/**
* 将对象转换成排序的map
* @param object
* @return
*/
private static Map<String, String> convertToSortedMap(Object object){
ObjectMapper objectMapper = new ObjectMapper();
Map<String, String> sortedMap = objectMapper.convertValue(object, TreeMap.class);
Map formatedMap = newTreeMap();
sortedMap.forEach((key, value) -> {
// sign属性不参与签名
if(!key.equals("sign")){
JacksonXmlProperty jacksonXmlProperty = obtainJacksonXmlPropertyAnnotationByKey(object, key);
formatedMap.put(jacksonXmlProperty.localName(), value);
}
});
return formatedMap;
}
/**
* 获取注解上的属性名称
* 根据微信支付文档,有些属性不遵循驼峰命名规范,但是我们项目中的属性是驼峰的,
* 不可能为了接入微信支付而打破代码的命名规范,因此在属性上加了注解,注解中的属性名称与微信支付中一致
* @param object
* @param key
* @return
*/
private static JacksonXmlProperty obtainJacksonXmlPropertyAnnotationByKey(Object object, String key){
JacksonXmlProperty jacksonXmlProperty = null;
try {
jacksonXmlProperty = object.getClass().getDeclaredField(key).getAnnotation(JacksonXmlProperty.class);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return jacksonXmlProperty;
}
/**
* 将排序的map转成有序字符串
* @param sortedMap
* @return
*/
private static String convertToStr(Map<String, String> sortedMap){
final String[] temp = {""};
sortedMap.forEach((key, value) -> {
if(StringUtils.isNotBlank(value)){
temp[0] += key + "=" + value + "&";
}
});
return temp[0];
}
}