微信APP支付和退款(JAVA)
2018-12-05 本文已影响320人
Ludwigvan
微信APP支付和退款
- 微信支付流程说明
- Java demo实例
- 退款
- 转账
1、微信支付
1 微信支付流程说明
本文支付流程说明均参考微信支付开发文档,微信支付开发文档,用于商户在移动端APP中集成微信支付功能。
商户APP调用微信提供的SDK调用微信支付模块,商户APP会跳转到微信中完成支付,支付完后跳回到商户APP内,最后展示支付结果。
下面看一下官方文档提供的业务流程图:
业务流程图
(ps:纵观所有的支付场景,包括博主接口过的支付方式比如Paypal(贝宝),Stripe(信用卡)等支付,他们的流程基本一致。)
概括起来就是:商户后端根据支付应用配置信息生成支付信息->App获取到支付信息->通过SDK发起支付请求->用户客户端输入密码->App 发起三方三方服务器支付请求->三方服务器回调商户后端服务器支付信息
Java demo实例
主要实现APP发起支付将支付金额传给后端服务器-后端服务器请求微信服务器生成预支付订单信息-返回APP端;
一、微信预支付订单实体对象
public class UnifiedorderResponse{
/**预付单信息**/
private String prepay_id;
/**签名**/
private String nonce_str;
/**微信开放平台申请的应用id**/
private String appid;
/**签名**/
private String sign;
/**请求方式**/
private String trade_type;
/**商户号id**/
private String mch_id;
/**返回提示信息**/
private String return_msg;
/**结果码**/
private String result_code;
/**返回码**/
private String return_code;
/**时间戳**/
private String timestamp;
}
二、微信统一下单实体对象
public class UnifiedorderModel {
/**微信开放平台申请的应用id**/
private String appid;
/**商户号*/
private String mch_id;
/**随机字符串*/
private String nonce_str;
/**签名(需要根据app应用商户密钥对整个统一下单实体对象就行签名)*/
private String sign;
/**签名方式*/
private String sign_type;
/**商品描述:腾讯充值中心-QQ会员充值*/
private String body;
/**商品详情**/
private String detail;
/**附加数据**/
private String attach;
/**支付订单号*/
private String out_trade_no;
/**总金额(分)*/
private Integer total_fee;
/**终端IP(8.8.8.8)*/
private String spbill_create_ip;
/**异步通知地址*/
private String notify_url;
/**交易类型**/
private String trade_type;
/**用户标识**/
private String openid;
/**商品id**/
private String product_id;
}
三、微信签名
对预支付请求实体属性进行字典排序,然后拼接字符串,用md5 加盐转大写生成签名字符串;
1、创建统一下单签名Map
/**
* 创建统一下单签名map
* @param request
* @param mchKey 商户密钥
* @return
*/
public static Map<String, String> createUnifiedSign(UnifiedorderModel request) {
Map<String, String> map = new HashMap<>();
map.put("appid", request.getAppid());
map.put("mch_id", request.getMch_id());
map.put("nonce_str", request.getNonce_str());
map.put("sign", request.getSign());
map.put("sign_type", request.getSign_type());
map.put("attach", request.getAttach());
map.put("body", request.getBody());
map.put("detail", request.getDetail());
map.put("notify_url", request.getNotify_url());
map.put("openid", request.getOpenid());
map.put("out_trade_no", request.getOut_trade_no());
map.put("spbill_create_ip", request.getSpbill_create_ip());
map.put("total_fee", String.valueOf(request.getTotal_fee()));
map.put("trade_type", request.getTrade_type());
return map;
}
2、按字典序排序微信统一签名
/**
* 微信统一签名
* @param params
* @return
*/
public static String sign(Map<String, String> params,String mchKey) {
SortedMap<String, String> sortedMap = new TreeMap<>(params);
StringBuilder toSign = new StringBuilder();
for (String key : sortedMap.keySet()) {
String value = params.get(key);
if (value!=null && !"".equals(value) && !"sign".equals(key)
&& !"key".equals(key)) {
toSign.append(key).append("=").append(value).append("&");
}
}
toSign.append("key=").append(mchKey);
return DigestUtils.md5Hex(toSign.toString()).toUpperCase();
}
四、XML格式转换
因为微信的请求和响应都是以xml的格式,所以需要将请求订单对象转换成XML 然后请求;
public class XMLUtil {
/**
* 解析字符串(XML)
*
* @param msg
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static Map<String, String> parseXml(String msg)
throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
InputStream inputStream = new ByteArrayInputStream(msg.getBytes("UTF-8"));
// 读取输入流
SAXReader reader = new SAXReader();
//防止微信支付XXE漏洞
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList){
map.put(e.getName(), e.getText());
}
// 释放资源
inputStream.close();
inputStream = null;
return map;
}
/**
* 扩展xstream,使其支持CDATA块
*/
private static XStream xstream = new XStream(new XppDriver(new NoNameCoder()) {
@Override
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点的转换都增加CDATA标记
boolean cdata = true;
@Override
@SuppressWarnings("rawtypes")
public void startNode(String name, Class clazz) {
super.startNode(name, clazz);
}
@Override
public String encodeNode(String name) {
return name;
}
@Override
protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write("<![CDATA[");
writer.write(text);
writer.write("]]>");
} else {
writer.write(text);
}
}
};
}
});
/**
* xml转对象
* @param xml
* @param objClass
* @return
*/
public static Object fromXML(String xml, Class<?> objClass) {
Serializer serializer = new Persister();
try {
return serializer.read(objClass, xml);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 对象转xml
* @param obj
* @return
*/
public static String toXMl(Object obj) {
//使用注解设置别名必须在使用之前加上注解类才有作用
xstream.processAnnotations(obj.getClass());
return xstream.toXML(obj);
}
private XStream inclueUnderlineXstream = new XStream(new DomDriver(null,new XmlFriendlyNameCoder("_-", "_")));
public XStream getXstreamInclueUnderline() {
return inclueUnderlineXstream;
}
public XStream xstream() {
return xstream;
}
}
这里用到的XML POM 配置:
<!-- xml解析 -->
<dependency>
<groupId>org.simpleframework</groupId>
<artifactId>simple-xml</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.3</version>
</dependency>
五、微信HTTP请求
1、加载微信证书生成SSL请求(微信证书可以在微信商户后台下载)
//随机字符串范围
private static final String RANDOM_STR = "abcdefghijklmnopqrstuvwxyzABCDEFGHI"
+ "JKLMNOPQRSTUVWXYZ0123456789";
private static final java.util.Random RANDOM = new java.util.Random();
//支付证书认证
private static SSLContext sslContext;
/**
* 初始化微信证书
* @param req
* @return
* @throws MyException
*/
public static SSLContext initSSLContext(HttpServletRequest req,
String mchId)
throws MyException {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(new File(req.getSession().getServletContext().
getRealPath("") + "/WEB-INF/classes"+BasicInfo.KeyPath));
} catch (IOException e) {
log.error("商户证书不正确-->>"+e);
throw new MyException("证书不正确!", e);
}
try {
KeyStore keystore = KeyStore.getInstance("PKCS12");
char[] partnerId2charArray = mchId.toCharArray();
keystore.load(inputStream, partnerId2charArray);
sslContext = SSLContexts.custom().loadKeyMaterial(keystore, partnerId2charArray).build();
return sslContext;
} catch (Exception e) {
log.error("商户秘钥不正确-->>"+e);
throw new MyException("秘钥不正确!", e);
} finally {
}
}
/**
* 生成随机字符串
* @return
*/
public static String getRandomStr() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 16; i++) {
sb.append(RANDOM_STR.charAt(RANDOM.nextInt(RANDOM_STR.length())));
}
return sb.toString();
}
/**
* 发起微信支付相关请求
* @return
*/
public static String ssl(String url,String data,
HttpServletRequest req,String mchId){
StringBuffer message = new StringBuffer();
try {
// KeyStore keyStore = KeyStore.getInstance("PKCS12");
SSLContext sslcontext = initSSLContext(req,mchId);
SSLConnectionSocketFactory sslsf =
new SSLConnectionSocketFactory(sslcontext,
new String[] { "TLSv1" },
null,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
CloseableHttpClient httpclient = HttpClients.custom().
setSSLSocketFactory(sslsf).build();
HttpPost httpost = new HttpPost(url);
httpost.addHeader("Connection", "keep-alive");
httpost.addHeader("Accept", "*/*");
httpost.addHeader("Content-Type",
"application/x-www-form-urlencoded; charset=UTF-8");
httpost.addHeader("Host", "api.mch.weixin.qq.com");
httpost.addHeader("X-Requested-With", "XMLHttpRequest");
httpost.addHeader("Cache-Control", "max-age=0");
httpost.addHeader("User-Agent",
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
httpost.setEntity(new StringEntity(data, "UTF-8"));
CloseableHttpResponse response = httpclient.execute(httpost);
try {
HttpEntity entity = response.getEntity();
log.info("----------------------------------------");
log.info(response.getStatusLine());
if (entity != null) {
log.info("Response content length: " + entity.getContentLength());
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent(),"UTF-8"));
String text;
while ((text = bufferedReader.readLine()) != null) {
message.append(text);
}
}
EntityUtils.consume(entity);
} catch (IOException e) {
log.error("发起微信支付请求过程异常--》》"+e);
e.printStackTrace();
} finally {
response.close();
}
} catch (Exception e1) {
log.error("发起微信支付请求异常--》》"+e1);
e1.printStackTrace();
}
return message.toString();
}
六、发起预支付
/**
* APP微信支付
* @param req
* @param model
* @return
* @throws Exception
*/
public static UnifiedorderResponse payApp(HttpServletRequest req,
UnifiedorderModel model) throws Exception {
model.setNonce_str(PayUtil.getRandomStr());
model.setSign(SignUtil.sign(SignUtil.createUnifiedSign(model),
BasicInfo.APP_MchKey));
try{
XMLUtil xmlUtil= new XMLUtil();
xmlUtil.xstream().alias("xml", model.getClass());
String xml = xmlUtil.xstream().toXML(model);
String response = PayUtil.ssl(BasicInfo.unifiedordersurl, xml,
req,BasicInfo.APP_MchId);
UnifiedorderResponse ret = (UnifiedorderResponse)
XMLUtil.fromXML(response,UnifiedorderResponse.class);
System.out.println("-------------------");
System.out.println(JSONObject.toJSONString(ret));
if("SUCCESS".equals(ret.getResult_code())){
//再次签名
Map<String, String> finalpackage = new TreeMap<String, String>();
String timestamp = (System.currentTimeMillis()/1000)+"";
String noncestr = PayUtil.getRandomStr();
finalpackage.put("appid", BasicInfo.APP_AppID);
finalpackage.put("timestamp", timestamp);
finalpackage.put("noncestr", noncestr);
finalpackage.put("prepayid", ret.getPrepay_id());
finalpackage.put("package", "Sign=WXPay");
finalpackage.put("partnerid",BasicInfo.APP_MchId);
String sign = SignUtil.sign(finalpackage, BasicInfo.APP_MchKey);
ret.setSign(sign);
ret.setNonce_str(noncestr);
ret.setTimestamp(timestamp);
return ret;
}else{
log.error("微信下单失败》》"+"错误码:"+ret.getReturn_code()+" ;"
+ "描述:"+ret.getReturn_msg());
return ret;
}
}catch (Exception e) {
log.error("微信下单异常》》"+e);
throw new MyException(SystemError.WX_PAY_ERROR.getCode(),
SystemError.WX_PAY_ERROR.getMessage());
}
}
预支付信息返回给前端;前端通过SDK发起支付请求;后端通过配置的回调地址接收微信的支付响应;
七、微信回调
@ResponseBody
@RequestMapping(value="/callBackWeiXinPay")
@Transactional(rollbackFor = Exception.class)
public void callBackWeiXinPay(HttpServletRequest req,
HttpServletResponse response) throws Exception{
try {
log.info("------------微信支付回调开始------------");
//支付回调
PayResponse pay = WxPay.callBackWeiXin(req);
//订单号
String orderNO = pay.getOut_trade_no();
log.info("微信支付回调订单号:"+orderNO);
//微信流水号
String thirdOrderNo = pay.getTransaction_id();
log.info("微信支付回调三方订单号:"+thirdOrderNo);
//支付金额
Integer money = pay.getTotal_fee();
log.info("微信支付回调金额(分):"+money);
//todo 业务处理比如更新订单状态等
//响应微信
CallBackWxModel call = new CallBackWxModel();
call.setReturn_code("SUCCESS");
call.setReturn_msg("");
// 响应xml格式数据
String resXml = XMLUtil.toXMl(call);
response.getWriter().write(resXml);
log.info("------------微信支付回调结束------------");
} catch (Exception e) {
log.error("微信支付回调异常CallBackController-->>callBackPay:\r\n"+ e.getMessage());
log.error("微信支付回调异常CallBackController-->>callBackPay:\r\n", e);
}
}
2、退款
一、退款实体对象
public class RefundModel {
/**Appid*/
private String appid;
/**商户号*/
private String mch_id;
/**随机字符串*/
private String nonce_str;
/**签名*/
private String sign;
/**签名方式*/
private String sign_type;
/**支付订单号*/
private String out_trade_no;
/**退款订单号*/
private String out_refund_no;
/**总金额*/
private Integer total_fee;
/**退款金额*/
private Integer refund_fee;
/**退款原因*/
private String refund_desc;
}
二、退款
/**
* 微信退款
* @param rdfund
* @param req
* @param type 1-公众号 2-小程序 3-app
* @return
* @throws Exception
*/
public static Map<String, Object> refundUtil(RefundModel rdfund
, HttpServletRequest req,Integer type) throws Exception{
Map<String, Object> mp = new HashMap<String, Object>();
//设置支付账户信息
String MchKey = ""; //商户号秘钥
String MchId = ""; //商户号id
rdfund.setAppid(BasicInfo.APP_AppID);
rdfund.setMch_id(BasicInfo.APP_MchId);
MchKey = BasicInfo.APP_MchKey;
MchId = BasicInfo.APP_MchId;
rdfund.setNonce_str(PayUtil.getRandomStr());
rdfund.setSign_type("MD5");
rdfund.setSign(SignUtil.sign(SignUtil.createRefundSign(rdfund),MchKey));
try{
XMLUtil xmlUtil= new XMLUtil();
xmlUtil.xstream().alias("xml", rdfund.getClass());
String xml = xmlUtil.xstream().toXML(rdfund);
String response = PayUtil.ssl(BasicInfo.refundurl, xml,req,MchId);
System.out.println(response);
Map<String, String> map = xmlUtil.parseXml(response);
if("SUCCESS".equals(map.get("result_code"))){
mp.put("stu", true);
return mp;
}else{
mp.put("stu", false);
mp.put("errMsg", map.get("return_msg"));
mp.put("errDes", map.get("err_code_des"));
log.error("微信退款失败》》"+"错误码:"+map.get("return_msg")+" ;"
+ "描述:"+map.get("err_code_des"));
return mp;
}
}catch (Exception e) {
log.error("微信退款异常》》"+e);
throw new MyException(SystemError.WX_RETREIEV_ERROR.getCode(),
SystemError.WX_RETREIEV_ERROR.getMessage());
}
}
主要逻辑跟支付差不多,可以参考前面的demo;
3、转账
一、转账实体对象
/***appId****/
private String mch_appid;
/***商户号****/
private String mchid;
/***随机字符串****/
private String nonce_str;
/***签名****/
private String sign;
/***商户订单号****/
private String partner_trade_no;
/***用户openid****/
private String openid;
/****校验用户姓名选项
* NO_CHECK:不校验真实姓名
FORCE_CHECK:强校验真实姓名***/
private String check_name;
/***收款用户姓名****/
private String re_user_name;
/****金额(分)***/
private Integer amount;
/***企业付款描述信息****/
private String desc;
/***Ip地址****/
private String spbill_create_ip;
二、转账
/**
* 企业转账接口
* @param transfer
* @param req
* @return
* @throws Exception
*/
public static Map<String, Object> transfersUtil(TransfersModel transfer
, HttpServletRequest req) throws Exception{
Map<String, Object> mp = new HashMap<String, Object>();
transfer.setMch_appid(BasicInfo.appID);
transfer.setMchid(BasicInfo.MchId);
transfer.setNonce_str(PayUtil.getRandomStr());
transfer.setCheck_name("NO_CHECK");
transfer.setSpbill_create_ip("127.0.0.1");
transfer.setSign(SignUtil.sign(SignUtil.createtransfersSign(transfer),BasicInfo.MchKey));
try{
XMLUtil xmlUtil= new XMLUtil();
xmlUtil.xstream().alias("xml", transfer.getClass());
String xml = xmlUtil.xstream().toXML(transfer);
String response = PayUtil.ssl(BasicInfo.transfersurl, xml,req,BasicInfo.MchKey);
System.out.println(response);
Map<String, String> map = xmlUtil.parseXml(response);
if("SUCCESS".equals(map.get("result_code"))){
mp.put("stu", true);
return mp;
}else{
mp.put("stu", false);
mp.put("errMsg", map.get("return_msg"));
mp.put("errDes", map.get("err_code_des"));
log.error("企业转账失败》》"+"错误码:"+map.get("return_msg")+" ;"
+ "描述:"+map.get("err_code_des"));
return mp;
}
}catch (Exception e) {
log.error("企业转账异常》》"+e);
throw new MyException(SystemError.WX_RETREIEV_ERROR.getCode(),
SystemError.WX_RETREIEV_ERROR.getMessage());
}
}
完整微信支付Demo:EzHomeSixGod.git