官方文檔
https://pay.weixin.qq.com/static/product/product_intro.shtml?name=app
開(kāi)發(fā)之前建議先梳理下官方文檔提供的業(yè)務(wù)流程
開(kāi)發(fā)前準(zhǔn)備
appId:應(yīng)用id
mchId:商戶號(hào)
apiKey:api密鑰
以上參數(shù)參考https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_1.shtml
如需退款等接口參考https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_3.shtml
1.創(chuàng)建訂單
1.1支付config
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
/**
* 微信支付配置類
*/
@Component
@PropertySource(value = {"classpath:wechatpay.properties"})
@Data
public class WeChatPayConfig {
/**
* 應(yīng)用ID
*/
@Value("${wxappId}")
public String appId;
/**
* 商戶號(hào)
*/
@Value("${mchId}")
public String mchId;
/**
* api 密鑰
*/
@Value("${apiKey}")
public String apiKey;
/**
* 異步通知地址
*/
@Value("${wxnotifyUrl}")
public String notifyUrl;
@Value("${placeUrl}")
public String placeUrl;
}
1.2 工具類
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class WeChatUtil {
/**
* 第一次簽名
*
* @param parameters 數(shù)據(jù)為服務(wù)器生成,下單時(shí)必須的字段排序簽名
* @param key
* @return
*/
public static String createSign(SortedMap<String, Object> parameters, String key) {
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();//所有參與傳參的參數(shù)按照accsii排序(升序)
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
Object v = entry.getValue();
if (null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + key);
return encodeMD5(sb.toString());
}
/**
* MD5 簽名
*
* @param str
* @return 簽名后的字符串信息
*/
public static String encodeMD5(String str) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] inputByteArray = (str).getBytes();
messageDigest.update(inputByteArray);
byte[] resultByteArray = messageDigest.digest();
return byteArrayToHex(resultByteArray);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
// 輔助 encodeMD5 方法實(shí)現(xiàn)
private static String byteArrayToHex(byte[] byteArray) {
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] resultCharArray = new char[byteArray.length * 2];
int index = 0;
for (byte b : byteArray) {
resultCharArray[index++] = hexDigits[b >>> 4 & 0xf];
resultCharArray[index++] = hexDigits[b & 0xf];
}
// 字符數(shù)組組合成字符串返回
return new String(resultCharArray);
}
/**
* 執(zhí)行 POST 方法的 HTTP 請(qǐng)求
*
* @param url
* @param parameters
* @return
* @throws IOException
*/
public static String executeHttpPost(String url, SortedMap<String, Object> parameters) throws IOException {
HttpClient client = HttpClients.createDefault();
HttpPost request = new HttpPost(url);
request.setHeader("Content-type", "application/xml");
request.setHeader("Accept", "application/xml");
request.setEntity(new StringEntity(transferMapToXml(parameters), "UTF-8"));
HttpResponse response = client.execute(request);
return readResponse(response);
}
/**
* 將 Map 轉(zhuǎn)化為 XML
*
* @param map
* @return
*/
public static String transferMapToXml(SortedMap<String, Object> map) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
for (String key : map.keySet()) {
sb.append("<").append(key).append(">")
.append(map.get(key))
.append("</").append(key).append(">");
}
return sb.append("</xml>").toString();
}
/**
* 讀取 response body 內(nèi)容為字符串
*/
public static String readResponse(HttpResponse response) throws IOException {
BufferedReader in = new BufferedReader(
new InputStreamReader(response.getEntity().getContent()));
String result = new String();
String line;
while ((line = in.readLine()) != null) {
result += line;
}
return result;
}
/**
* 第二次簽名
*
* @param result 數(shù)據(jù)為微信返回給服務(wù)器的數(shù)據(jù)(XML 的 String)墩弯,再次簽名后傳回給客戶端(APP)使用
* @param key 密鑰
* @return
* @throws IOException
*/
public static Map createSign2(String result, String key) throws IOException {
SortedMap<String, Object> map = new TreeMap<>(transferXmlToMap(result));
Map app = new HashMap();
app.put("appid", map.get("appid"));
app.put("partnerid", map.get("mch_id"));
app.put("prepayid", map.get("prepay_id"));
app.put("package", "Sign=WXPay"); // 固定字段,保留,不可修改
app.put("noncestr", map.get("nonce_str"));
app.put("timestamp", new Date().getTime() / 1000); // 時(shí)間為秒瘩例,JDK 生成的是毫秒,故除以 1000
app.put("sign", createSign(new TreeMap<>(app), key));
return app;
}
/**
* 將 XML 轉(zhuǎn)化為 map
*
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map transferXmlToMap(String strxml) throws IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if (null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = null;
try {
doc = builder.build(in);
} catch (JDOMException e) {
throw new IOException(e.getMessage()); // 統(tǒng)一轉(zhuǎn)化為 IO 異常輸出
}
// 解析 DOM
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if (children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = getChildrenText(children);
}
m.put(k, v);
}
//關(guān)閉流
in.close();
return m;
}
// 輔助 transferXmlToMap 方法遞歸提取子節(jié)點(diǎn)數(shù)據(jù)
private static String getChildrenText(List<Element> children) {
StringBuffer sb = new StringBuffer();
if (!children.isEmpty()) {
Iterator<Element> it = children.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List<Element> list = e.getChildren();
sb.append("<" + name + ">");
if (!list.isEmpty()) {
sb.append(getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
/**
* 驗(yàn)證簽名是否正確
*
* @return boolean
* @throws Exception
*/
public static boolean checkSign(SortedMap<String, Object> parameters, String key) throws Exception {
String signWx = parameters.get("sign").toString();
if (signWx == null) return false;
parameters.remove("sign"); // 需要去掉原 map 中包含的 sign 字段再進(jìn)行簽名
String signMe = createSign(parameters, key);
return signWx.equals(signMe);
}
/**
* 讀取 request body 內(nèi)容作為字符串
*
* @param request
* @return
* @throws IOException
*/
public static String readRequest(HttpServletRequest request) throws IOException {
InputStream inputStream;
StringBuffer sb = new StringBuffer();
inputStream = request.getInputStream();
String str;
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
while ((str = in.readLine()) != null) {
sb.append(str);
}
in.close();
inputStream.close();
return sb.toString();
}
/**
* 微信回調(diào)接口成功返回值
* @return
*/
public static String success(){
return "<xml>\n" +
" <return_code><![CDATA[SUCCESS]]></return_code>\n" +
" <return_msg><![CDATA[OK]]></return_msg>\n" +
"</xml>";
}
/**
* 微信回調(diào)接口失敗返回值
* @return
*/
public static String fail(){
return "<xml>\n" +
" <return_code><![CDATA[FAIL]]></return_code>\n" +
" <return_msg><![CDATA[]]></return_msg>\n" +
"</xml>";
}
1.3創(chuàng)建訂單
import com.alibaba.fastjson.JSON;
import com.smt.amblyopia.nd.app.util.CommonUtil;
import com.smt.amblyopia.nd.app.util.WeChatUtil;
import com.smt.amblyopia.nd.comm.util.RandomUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.SortedMap;
import java.util.TreeMap;
@Component
@Slf4j
public class WeChatPay {
@Autowired
private WeChatPayConfig weChatPayConfig;
/**
*
* @param body 購(gòu)買的商品名稱 如:商品1 商品2
* @param outtradeno 訂單號(hào)
* @param amount 商品價(jià)格
* @param request
* @return
* @throws IOException
*/
public String weChatCreateOrder(String body, String outtradeno, BigDecimal amount, HttpServletRequest request) throws IOException {
String ip = CommonUtil.getIpAddr(request);
SortedMap<String, Object> data = new TreeMap<>();
data.put("appid", weChatPayConfig.getAppId());//應(yīng)用ID
data.put("mch_id", weChatPayConfig.getMchId());//商戶號(hào)
data.put("device_info", "WEB"); // 默認(rèn) "WEB"
data.put("body", body);
data.put("nonce_str", RandomUtils.getRandomStr(10));//隨機(jī)字符串data.put("trade_type" , "APP");//支付類型
data.put("notify_url", weChatPayConfig.getNotifyUrl());
data.put("out_trade_no", outtradeno);
data.put("total_fee", amount.longValue() + ""); //分
data.put("spbill_create_ip", ip);
data.put("trade_type", "APP");//支付類型
data.put("sign", WeChatUtil.createSign(data, weChatPayConfig.apiKey));//支付類型 ;
String result = WeChatUtil.executeHttpPost(weChatPayConfig.getPlaceUrl(), data); // 執(zhí)行 HTTP 請(qǐng)求,獲取接收的字符串(一段 XML)
String resp = JSON.toJSONString(WeChatUtil.createSign2(result, weChatPayConfig.apiKey));
return resp;
}
1.4 本地訂單處理
訂單創(chuàng)建成功庶诡,本地需要記錄原始訂單,根據(jù)回調(diào)接口的返回值咆课,改變本地訂單狀態(tài)末誓;
2.支付回調(diào)接口
2.1微信支付回調(diào)
import com.alibaba.fastjson.JSON;
import com.smt.amblyopia.nd.app.util.WeChatUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
@Component
@Slf4j
public class WeChatPay {
@Autowired
private WeChatPayConfig weChatPayConfig;
public Map<String, String> weChatCallBack(HttpServletRequest request, HttpServletResponse response) throws Exception {
Map<String, String> result = new HashMap<>();
// 預(yù)先設(shè)定返回的 response 類型為 xml
response.setHeader("Content-type", "application/xml");
// 讀取參數(shù),解析Xml為map
Map<String, String> map = WeChatUtil.transferXmlToMap(WeChatUtil.readRequest(request));
log.info("微信返回值轉(zhuǎn)map:{}" , JSON.toJSONString(map));
// 轉(zhuǎn)換為有序 map书蚪,判斷簽名是否正確
boolean isSignSuccess = WeChatUtil.checkSign(new TreeMap<String, Object>(map), weChatPayConfig.apiKey);
if (isSignSuccess) {
log.info("校驗(yàn)簽名成功 isSignSuccess:{}" , isSignSuccess);
// 簽名校驗(yàn)成功喇澡,說(shuō)明是微信服務(wù)器發(fā)出的數(shù)據(jù)
String orderId = map.get("out_trade_no");
String returnCode = map.get("return_code");
result.put("orderId", orderId);
result.put("returnCode", returnCode);
//map.get("return_code").equals("SUCCESS")
return result;
}else{
return null;
}
}
2.2本地訂單更新
根據(jù)returnCode的值,更新本地訂單狀態(tài)善炫;