一:本文只针对native第三方pc平台扫码支付
1. 名词解释:
1、
微信公众平台
微信公众平台是微信公众账号申请入口和管理后台。商户可以在公众平台提交基本资料、业务资料、财务资料申请开通微信支付功能。
平台入口:http://mp.weixin.qq.com。
2、
微信开放平台
微信开放平台是商户APP接入微信支付开放接口的申请入口,通过此平台可申请微信APP支付。
平台入口:http://open.weixin.qq.com。
3、
微信商户平台
微信商户平台是微信支付相关的商户功能集合,包括参数配置、支付数据查询与统计、在线退款、代金券或立减优惠运营等功能。
平台入口:http://pay.weixin.qq.com。
4、
微信企业号
微信企业号是企业号的申请入口和管理后台,商户可以在企业号提交基本资料、业务资料、财务资料申请开通微信支付功能。
企业号入口:http://qy.weixin.qq.com。
5、
微信支付系统
微信支付系统是指完成微信支付流程中涉及的API接口、后台业务处理系统、账务系统、回调通知等系统的总称。
6、
商户收银系统
商户收银系统即商户的POS收银系统,是录入商品信息、生成订单、客户支付、打印小票等功能的系统。接入微信支付功能主要涉及到POS软件系统的开发和测试,所以在下文中提到的商户收银系统特指POS收银软件系统。
7、
商户后台系统
商户后台系统是商户后台处理业务系统的总称,例如:商户网站、收银系统、进销存系统、发货系统、客服系统等。
8、
扫码设备
一种输入设备,主要用于商户系统快速读取媒介上的图形编码信息。按读取码的类型不同,可分为条码扫码设备和二维码扫码设备。按读取物理原理可分为红外扫码设备、激光扫码设备。
9、
商户证书
商户证书是微信提供的二进制文件,商户系统发起与微信支付后台服务器通信请求的时候,作为微信支付后台识别商户真实身份的凭据。
10、
签名
商户后台和微信支付后台根据相同的密钥和算法生成一个结果,用于校验双方身份合法性。签名的算法由微信支付制定并公开,常用的签名方式有:MD5、SHA1、SHA256、HMAC等。
11、
JSAPI网页支付
JSAPI网页支付即前文说的公众号支付,可在微信公众号、朋友圈、聊天会话中点击页面链接,或者用微信“扫一扫”扫描页面地址二维码在微信中打开商户HTML5页面,在页面内下单完成支付。
12、
Native原生支付
Native原生支付即前文说的扫码支付,商户根据微信支付协议格式生成的二维码,用户通过微信“扫一扫”扫描二维码后即进入付款确认界面,输入密码即完成支付。
13、
支付密码
支付密码是用户开通微信支付时单独设置的密码,用于确认支付完成交易授权。该密码与微信登录密码不同。
14、
Openid
用户在公众号内的身份标识,不同公众号拥有不同的openid。商户后台系统通过登录授权、支付通知、查询订单等API可获取到用户的openid。主要用途是判断同一个用户,对用户发送客服消息、模版消息等。企业号用户需要使用企业号userid转openid接口将企业成员的userid转换成openid。
1、什么是微信商户平台:
地址:https://pay.weixin.qq.com
提供给商家使用,用于查看交易数据,提现等信息
2、常用的支付方式 公众号支付,扫码支付,app支付,小程序支付
官方地址:https://pay.weixin.qq.com/wiki/doc/api/index.html
案例演示: https://pay.weixin.qq.com/guide/webbased_payment.shtml
3、微信支付申请流程 https://pay.weixin.qq.com/guide/qrcode_payment.shtml
1)申请公众号(服务号) 认证费 300
2)开通微信支付
- 商户在微信公众平台或开放平台提交微信支付申请,微信支付工作人员审核资料无误后开通相应的微信支付权限。微信支付申请审核通过后,商户在申请资料填写的邮箱中收取到由微信支付小助手发送的邮件,此邮件包含开发时需要使用的支付账户信息。
二. 扫码支付流程图:
业务流程说明:
(1)商户后台系统根据用户选购的商品生成订单。
(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
(4)商户后台系统根据返回的code_url生成二维码。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
(8)微信支付系统根据用户授权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
(11)未收到支付通知的情况,商户后台系统调用【查询订单API】。
(12)商户确认订单已支付后给用户发货。
三. 流程解析:
3.1 微信支付相关工具类:
md5,uuid工具类
import java.security.MessageDigest;
import java.util.UUID;
/**
* 常用工具类的封装 md5 uuid等
*/
public class CommonUtils {
/**
* 生成32位uuid
* @return
*/
public static String generateUUID(){
String uuid = UUID.randomUUID().toString().replaceAll("-","")
.substring(0,32);
return uuid;
}
/**
* md5常用工具类
* @param data
* @return
*/
public static String MD5(String data){
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte [] array = md5.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
httpclient发送请求工具类:
import com.google.gson.Gson;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 封装http get post
*/
public class HttpUtils {
private static final Gson gson = new Gson();
/**
* get方法
* @param url
* @return
*/
public static Map<String,Object> doGet(String url){
Map<String,Object> map = new HashMap<>();
CloseableHttpClient httpClient = HttpClients.createDefault();
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000) //连接超时
.setConnectionRequestTimeout(5000)//请求超时
.setSocketTimeout(5000)
.setRedirectsEnabled(true) //允许自动重定向
.build();
HttpGet httpGet = new HttpGet(url);
httpGet.setConfig(requestConfig);
try{
HttpResponse httpResponse = httpClient.execute(httpGet);
if(httpResponse.getStatusLine().getStatusCode() == 200){
String jsonResult = EntityUtils.toString( httpResponse.getEntity());
map = gson.fromJson(jsonResult,map.getClass());
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return map;
}
/**
* 封装post
* @return
*/
public static String doPost(String url, String data,int timeout){
CloseableHttpClient httpClient = HttpClients.createDefault();
//超时设置
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(timeout) //连接超时
.setConnectionRequestTimeout(timeout)//请求超时
.setSocketTimeout(timeout)
.setRedirectsEnabled(true) //允许自动重定向
.build();
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
httpPost.addHeader("Content-Type","text/html; chartset=UTF-8");
if(data != null && data instanceof String){ //使用字符串传参
StringEntity stringEntity = new StringEntity(data,"UTF-8");
httpPost.setEntity(stringEntity);
}
try{
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
if(httpResponse.getStatusLine().getStatusCode() == 200){
String result = EntityUtils.toString(httpEntity);
return result;
}
}catch (Exception e){
e.printStackTrace();
}finally {
try{
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
}
ip获取工具类:
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.servlet.http.HttpServletRequest;
public class IpUtils {
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress="";
}
return ipAddress;
}
}
jwt工具类:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import net.xdclass.xdvideo.domain.User;
import java.util.Date;
/**
* jwt工具类
*/
public class JwtUtils {
public static final String SUBJECT = "lucky";
public static final long EXPIRE = 1000*60*60*24*7; //过期时间,毫秒,一周
//秘钥
public static final String APPSECRET = "lucky666";
/**
* 生成jwt
* @param user
* @return
*/
public static String geneJsonWebToken(User user){
if(user == null || user.getId() == null || user.getName() == null
|| user.getHeadImg()==null){
return null;
}
String token = Jwts.builder().setSubject(SUBJECT)
.claim("id",user.getId())
.claim("name",user.getName())
.claim("img",user.getHeadImg())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis()+EXPIRE))
.signWith(SignatureAlgorithm.HS256,APPSECRET).compact();
return token;
}
/**
* 校验token
* @param token
* @return
*/
public static Claims checkJWT(String token ){
try{
final Claims claims = Jwts.parser().setSigningKey(APPSECRET).
parseClaimsJws(token).getBody();
return claims;
}catch (Exception e){ }
return null;
}
}
微信支付工具类:(原微信支付官网也提供了:工具类下载)
import org.w3c.dom.Entity;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.*;
/**
* 微信支付工具类,xml转map,map转xml,生成签名
*/
public class WXPayUtil {
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
throw ex;
}
}
/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}
/**
* 生成微信支付sign
* @return
*/
public static String createSign(SortedMap<String, String> params, String key){
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
Iterator<Map.Entry<String,String>> it = es.iterator();
//生成 stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
while (it.hasNext()){
Map.Entry<String,String> entry = (Map.Entry<String,String>)it.next();
String k = (String)entry.getKey();
String v = (String)entry.getValue();
if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
sb.append(k+"="+v+"&");
}
}
sb.append("key=").append(key);
String sign = CommonUtils.MD5(sb.toString()).toUpperCase();
return sign;
}
/**
* 校验签名
* @param params
* @param key
* @return
*/
public static boolean isCorrectSign(SortedMap<String, String> params, String key){
String sign = createSign(params,key);
String weixinPaySign = params.get("sign").toUpperCase();
return weixinPaySign.equals(sign);
}
/**
* 获取有序map
* @param map
* @return
*/
public static SortedMap<String,String> getSortedMap(Map<String,String> map){
SortedMap<String, String> sortedMap = new TreeMap<>();
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()){
String key = (String)it.next();
String value = map.get(key);
String temp = "";
if( null != value){
temp = value.trim();
}
sortedMap.put(key,temp);
}
return sortedMap;
}
}
3.2 第一步:生成订单,统一下单:
具体说明如下:官方说明
统一下单URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder
统一下单部分必要参数构造:
1.sign签名构造:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3
生成签名之后一定要自己校验一下:
通过工具去校验
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1
/**
* 统一下单方法
* @return
*/
private String unifiedOrder(VideoOrder videoOrder) throws Exception {
//int i = 1/0; //模拟异常
//生成签名
SortedMap<String,String> params = new TreeMap<>();
params.put("appid",weChatConfig.getAppId());
params.put("mch_id", weChatConfig.getMchId());
params.put("nonce_str",CommonUtils.generateUUID());
params.put("body",videoOrder.getVideoTitle());
params.put("out_trade_no",videoOrder.getOutTradeNo());
params.put("total_fee",videoOrder.getTotalFee().toString());
params.put("spbill_create_ip",videoOrder.getIp());
params.put("notify_url",weChatConfig.getPayCallbackUrl());
params.put("trade_type","NATIVE");
//sign签名
String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
params.put("sign",sign);
//map转xml
String payXml = WXPayUtil.mapToXml(params);
System.out.println(payXml);
//统一下单
String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
if(null == orderStr) {
return null;
}
Map<String, String> unifiedOrderMap = WXPayUtil.xmlToMap(orderStr);
System.out.println(unifiedOrderMap.toString());
if(unifiedOrderMap != null) {
return unifiedOrderMap.get("code_url");
}
return null;
}
//返回code_url 根据链接生成二维码返回到前台,采用的是google的方法 单独方法 可写在controller层
try{
//生成二维码配置
Map<EncodeHintType,Object> hints = new HashMap<>();
//设置纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION,ErrorCorrectionLevel.L);
//编码类型
hints.put(EncodeHintType.CHARACTER_SET,"UTF-8");
BitMatrix bitMatrix = new MultiFormatWriter().encode(codeUrl,BarcodeFormat.QR_CODE,400,400,hints);
OutputStream out = response.getOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix,"png",out);
}catch (Exception e){
e.printStackTrace();
}
这里要明确的是签名是对所传送的数据或者接收的数据集合,采用MD5或者HMAC-SHA256加密,解密,类似于jwt,所以这里是对统一下单所需的所有参数进行sign签名,然后再将sign参数put进去,最后发送和接收请求,参数都必须转换成xml格式就行解析,可以发送的时候将其转换成xml,接受响应的时候将其xml转换成map格式,再从响应转换成的map里面获取 code_url,并将链接生成二维码图片。
遇到问题,根据错误码解决
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1
然后模式二的扫码支付几步都是微信客户端和微信支付系统交互,但是到了第10步的时候,他会异步调用商户的接口,通知支付结果。调用商户接口,里面根据微信支付系统传递过来的数据信息(xml格式的),更新订单状态,并且告诉微信支付系统。
商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
使用Ngrock本地接收微信回调,并开发回调接口:
支付结果通知回调地址在 “notify_url”中设置(params.put(“notify_url”,weChatConfig.getPayCallbackUrl()))
注意:
回调要用post方式,微信文档没有写回调的通知方式
可以用这个注解 @RequestMapping
1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起多次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
3、在订单状态不明或者没有收到微信支付结果通知的情况下,建议商户主动调用微信支付【查询订单API】确认订单状态。
特别提醒:
1、商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。
2、当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
3、技术人员可登进微信商户后台扫描加入接口报警群,获取接口告警信息。
参考链接:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8
/**
* 微信支付回调
*/
@RequestMapping("/order/callback")
public void orderCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
InputStream inputStream = request.getInputStream();
//BufferedReader是包装设计模式,性能更搞
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
StringBuffer sb = new StringBuffer();
String line ;
while ((line = in.readLine()) != null){
sb.append(line);
}
in.close();
inputStream.close();
Map<String,String> callbackMap = WXPayUtil.xmlToMap(sb.toString());
System.out.println(callbackMap.toString());
SortedMap<String,String> sortedMap = WXPayUtil.getSortedMap(callbackMap);
//判断签名是否正确
if(WXPayUtil.isCorrectSign(sortedMap,weChatConfig.getKey())){
if("SUCCESS".equals(sortedMap.get("result_code"))){
String outTradeNo = sortedMap.get("out_trade_no");
VideoOrder dbVideoOrder = videoOrderService.findByOutTradeNo(outTradeNo);
if(dbVideoOrder != null && dbVideoOrder.getState()==0){ //判断逻辑看业务场景
VideoOrder videoOrder = new VideoOrder();
videoOrder.setOpenid(sortedMap.get("openid"));
videoOrder.setOutTradeNo(outTradeNo);
videoOrder.setNotifyTime(new Date());
videoOrder.setState(1);
int rows = videoOrderService.updateVideoOderByOutTradeNo(videoOrder);
if(rows == 1){ //通知微信订单处理成功
response.setContentType("text/xml");
response.getWriter().println("success");
return;
}
}
}
}
//都处理失败
response.setContentType("text/xml");
response.getWriter().println("fail");
}
来源:CSDN
作者:lucky_dogwang
链接:https://blog.csdn.net/weixin_42567141/article/details/104212893