微信业务流程
(1)商户后台系统根据用户选购的商品生成订单。
(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
(4)商户后台系统根据返回的code_url生成二维码。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
(8)微信支付系统根据用户授权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
(11)未收到支付通知的情况,商户后台系统调用【查询订单API】。
(12)商户确认订单已支付后给用户发货。
简而言之:
调用微信生成预支付订单 –> 使用微信返回的微信地址生成二维码 –> 用户扫描二维码支付 –>
支付成功客户端下单时设置的回调地址调用 –> 写入成功后本地订单业务
基础配置:
- 下载微信sdk微信sdk、demo下载
- 打包sdk到maven本地仓库、引入sdk本地jar包引入本地maven仓库
- 注入sdk基础配置类、添加配置信息
package web.pay.config;
import com.github.wxpay.sdk.IWXPayDomain;
import com.github.wxpay.sdk.WXPayConfig;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 微信相关配置常量
*
* @author LiRui
*/
@Component
public class WeChatConfig extends WXPayConfig {
/**
* 交易类型
*/
public static final String TRADE_TYPE_JSAPI = "JSAPI";
/**
* 扫码支付
*/
public static final String TRADE_TYPE_NATIVE = "NATIVE";
/**
* app支付
*/
public static final String TRADE_TYPE_APP = "APP";
/**
* 加密Key
*/
@Value(value = "${wechat.key}")
private String key;
/**
* 微信appId值
*/
@Value(value = "${wechat.appid}")
private String appId;
/**
* 微信appSecret值
*/
@Value(value = "${wechat.appsecret}")
private String appSecret;
/**
* 商户ID
*/
@Value(value = "${wechat.mchid}")
private String mchId;
/**
* 通知地址(支付完成后,回调地址)
*/
@Value(value = "${wechat.paynotifyurl}")
private String notifyUrl;
/**
* 支付宝域名
*/
@Value(value = "${wechat.domain}")
private String domain;
/**
* 证书位置
*/
@Value(value = "${wechat.certPath}")
private String certPath;
/**
* 使用沙盒
*/
@Value(value = "${wechat.useSandbox}")
private boolean useSandbox;
@Override
public String getAppID() {
return appId;
}
@Override
public String getMchID() {
return mchId;
}
@Override
public String getKey() {
return key;
}
@Override
public InputStream getCertStream() {
if (StringUtils.isBlank(certPath)) {
return null;
}
//证书位置
File file = new File(certPath);
InputStream certStream;
try {
certStream = new FileInputStream(file);
certStream.read(new byte[(int) file.length()]);
certStream.close();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getAppSecret() {
return appSecret;
}
public String getDomain() {
return domain;
}
public String getCertPath() {
return certPath;
}
@Override
public IWXPayDomain getWXPayDomain() {
return new IWXPayDomainImpl(domain);
}
public String getNotifyUrl() {
return notifyUrl;
}
public boolean isUseSandbox() {
return useSandbox;
}
public static class IWXPayDomainImpl implements IWXPayDomain {
private static final Log LOG = LogFactory.getLog(IWXPayDomainImpl.class);
private String domain;
IWXPayDomainImpl(final String domain) {
this.domain = domain;
}
@Override
public void report(final String s, final long l, final Exception e) {
LOG.info(String.format("域名:%s,耗时:%S,网络请求异常信息:%s", s, l,
null == e ? "无" : e.getMessage()));
}
/**
* 设置域名
*
* @param wxPayConfig
* @return
*/
@Override
public DomainInfo getDomain(final WXPayConfig wxPayConfig) {
return new DomainInfo(this.domain, false);
}
}
}
配置Spring注入sdk连接
<!--引入配置文件-->
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>/WEB-INF/config/pay.properties</value>
</list>
</property>
</bean>
<!--微信支付-->
<bean name="wxPay" class="com.github.wxpay.sdk.WXPay">
<!--微信配置-->
<constructor-arg name="config" ref="weChatConfig"/>
<!--开启自动报告-->
<constructor-arg name="autoReport" value="${wechat.autoReport}"/>
<!--回调地址-->
<constructor-arg name="notifyUrl" value="${wechat.paynotifyurl}"/>
<!--是否使用沙箱-->
<constructor-arg name="useSandbox" value="${wechat.useSandbox}"/>
</bean>
pay.properties
#微信支付参数
#加密Key
wechat.key=xxxxxxxxx
#微信appId值
wechat.appid=xxxxxxx
#微信appSecret值
wechat.appsecret=xxxxxxx
#商户ID
wechat.mchid=xxxxx
#通知地址(支付完成后,回调地址)
wechat.paynotifyurl=http://www.xxxx.com/xx/xx/xxxxx.do
#微信域名
wechat.domain=api.mch.weixin.qq.com
#证书地址
wechat.certPath=xxxxx
#使用沙盒环境
wechat.useSandbox=true
#自动通知
wechat.autoReport=true
controller下单接口设计:
/**
* 微信扫码支付
*
* @return
*/
@RequestMapping(value = "/weChatPay", method = RequestMethod.GET)
@ResponseBody
public Map weChatPay(HttpServletRequest request, @RequestParam BigDecimal money) {
final String userSession = BaseUtil.getUserSession(request);
if (StringUtils.isBlank(userSession)) {
LOG.error("pc微信扫码支付用户登录过期");
return JsonWrapperResult.failureWrapper(ErrorCode.NOT_LOGIN);
}
//生成订单
Map<String, String> weChatPay;
try {
//生成订单
weChatPay = webUserPayManager
.requestWeChatPay(money, userSession, request.getRemoteAddr());
} catch (Exception e) {
e.printStackTrace();
LOG.error(String.format("pc微信扫码支付生成订单错误:%s", e.getMessage()), e);
return JsonWrapperResult.failureWrapperMsg("微信支付调用错误");
}
final String url = weChatPay.get("code_url");
return JsonWrapperResult
.successWrapper("img", QRCodeUtil.createQRCodeBase64(url), "orderId",
weChatPay.get("orderId"));
}
支付业务代码(service):
@Autowired
private WXPay wxPay;
@Autowired
private WeChatConfig weChatConfig;
/**
* 微信请求
*
* @return
* @throws PayException 支付异常,返回失败抛出异常 回滚订单数据
*/
@Transactional(rollbackFor = Exception.class)
public Map<String, String> requestWeChatPay(BigDecimal buyMoney, String userId, String ip)
throws Exception {
final WzzwwUserPay userPay = saveUserPay(buyMoney, userId, true);
//预下单
HashMap<String, String> paramMap = new HashMap<>(9);
String nonceStr = BaseUtil.generateUUID();
//公众账号ID
paramMap.put("appid", weChatConfig.getAppID());
//商户号
paramMap.put("mch_id", weChatConfig.getMchID());
//随机字符串
paramMap.put("nonce_str", nonceStr);
//商品描述
paramMap.put("body", "wzzww-coin");
//订单号
paramMap.put("out_trade_no", userPay.getOrderId());
//标价金额(单位为分)
paramMap.put("total_fee",
String.valueOf(userPay.getBuyMoney().multiply(BigDecimal.valueOf(100)).setScale(0,
BigDecimal.ROUND_DOWN)));
//通知地址
paramMap.put("notify_url", weChatConfig.getNotifyUrl());
//交易类型
paramMap.put("trade_type", WeChatConfig.TRADE_TYPE_NATIVE);
//商品类型,扫码支付必传
paramMap.put("product_id", "coin");
//ip
paramMap.put("spbill_create_ip", ip);
String sign = WXPayUtil.generateSignature(paramMap, weChatConfig.getKey());
paramMap.put("sign", sign);
final Map<String, String> stringMap = wxPay.unifiedOrder(paramMap);
if (!"SUCCESS".equals(stringMap.get("return_code"))) {
//预下单失败
throw new PayException(String.format("调用支付出错:%s", JSONObject.toJSONString(stringMap)));
}
stringMap.put("orderId", userPay.getOrderId());
return stringMap;
}
生成对应字符串二维码代码工具类QRCodeUtil
package common.util;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Hashtable;
import java.util.Map;
import javax.imageio.ImageIO;
import org.apache.commons.codec.binary.Base64;
/**
* 二维码
*
* @author LiRui
* @version 1.0
*/
public class QRCodeUtil {
private static final int BLACK = 0xFF000000;
private static final int WHITE = 0xFFFFFFFF;
/**
* 默认宽度
*/
private static final int WIDTH = 300;
/**
* 默认高度
*/
public static final int HEIGHT = 300;
/**
* 默认格式
*/
public static final String FORMAT = "gif";
private QRCodeUtil() {
}
/**
* 生成二维码
* 默认:300*300 gif 格式
*
* @param text 内容
* @param deposit 存放地址
* @param name 名称
* @throws Exception
*/
public static void createQRCode(String text, String deposit, String name) throws Exception {
createQRCode(text, WIDTH, HEIGHT, FORMAT, deposit, name);
}
/**
* 生成二维码
* 默认:300*300 gif 格式
*
* @param text 内容
*/
public static String createQRCodeBase64(String text) {
Map<EncodeHintType, Object> hints = new Hashtable<>();
String base64Img = "data:image/png;base64,";
// 指定编码格式
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
try {
// 生成输出流
BitMatrix bitMatrix1 = new MultiFormatWriter().encode(text,
BarcodeFormat.QR_CODE, WIDTH, HEIGHT, hints);
BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix1);
base64Img = base64Img + encodeToString("png", image);
} catch (Exception e) {
e.printStackTrace();
}
return base64Img;
}
/**
* 生成二维码
* 默认:300*300 gif 格式
*
* @param text 内容
* @throws Exception
*/
public static void createQRCode(String text, OutputStream stream)
throws WriterException, IOException {
Hashtable<EncodeHintType, String> hints = new Hashtable<>();
// 内容所使用字符集编码
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
BitMatrix bitMatrix = new MultiFormatWriter()
.encode(text, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, hints);
MatrixToImageWriter.writeToStream(bitMatrix, "gif", stream);
}
/**
* 生成二位码
*
* @param text 内容
* @param width 宽度
* @param height 高度
* @param format 格式
* @param deposit 存放地址
* @param name 图片名称
* @throws Exception
*/
public static void createQRCode(String text, int width, int height, String format,
String deposit, String name) throws Exception {
deposit = deposit.endsWith("/") ? deposit.substring(0, deposit.lastIndexOf("/")) : deposit;
Hashtable<EncodeHintType, String> hints = new Hashtable<>();
// 内容所使用字符集编码
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
BitMatrix bitMatrix = new MultiFormatWriter()
.encode(text, BarcodeFormat.QR_CODE, width, height, hints);
// 生成二维码
File outputFile = new File(deposit + File.separator + name + ".gif");
MatrixToImageWriter.writeToPath(bitMatrix, format, outputFile.toPath());
}
private static BufferedImage toBufferedImage(BitMatrix matrix) {
int width = matrix.getWidth();
int height = matrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE);
}
}
return image;
}
public static void writeToFile(BitMatrix matrix, String format, File file) throws IOException {
BufferedImage image = toBufferedImage(matrix);
if (!ImageIO.write(image, format, file)) {
throw new IOException("Could not write an image of format " + format + " to " + file);
}
}
public static void writeToStream(BitMatrix matrix, String format, OutputStream stream)
throws IOException {
BufferedImage image = toBufferedImage(matrix);
if (!ImageIO.write(image, format, stream)) {
throw new IOException("Could not write an image of format " + format);
}
}
/**
* 将图片转换成base64格式进行存储
*
* @param formatName 文件格式
* @param image 图片流
* @return base64字符串
*/
private static String encodeToString(String formatName, BufferedImage image) {
String imageString = null;
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
ImageIO.write(image, formatName, bos);
byte[] imageBytes = bos.toByteArray();
imageString = new String(Base64.encodeBase64(imageBytes));
} catch (IOException e) {
e.printStackTrace();
}
return imageString;
}
}
回调接口,即以上配置中配置的回调接口地址(wechat.paynotifyurl)
/**
* 微信支付后,回调通知地址
*
* @param request
*/
@RequestMapping("weChatPayNotify")
public void weChatPayNotify(HttpServletRequest request, HttpServletResponse response)
throws Exception {
BufferedReader reader = request.getReader();
String line;
StringBuffer inputString = new StringBuffer();
while ((line = reader.readLine()) != null) {
inputString.append(line);
}
String xmlString = inputString.toString();
request.getReader().close();
LOG.info("微信回调信息:" + xmlString);
//微信jdk提供的xml转Map
Map<String, String> requestParams = WXPayUtil.xmlToMap(xmlString);
//返回结果
String returnCode = requestParams.get("return_code");
//业务返回结果
String businessResultCode = requestParams.get("result_code");
response.setContentType("text/xml");
if (!"SUCCESS".equals(returnCode) && !"SUCCESS".equals(businessResultCode)) {
LOG.error("微信回调支付失败:" + xmlString);
// 返回错误
response.getWriter().write("<xml>\n"
+ " <return_code><![CDATA[FAIL]]></return_code>\n"
+ " <return_msg><![CDATA[下单失败]]></return_msg>\n"
+ "</xml>");
}
//订单号
String outTradeNo = requestParams.get("out_trade_no");
//返回金额
String totalFee = requestParams.get("total_fee");
//调用业务接口,返回成功失败
final boolean finishOrder = xxxx
.xxxxx(outTradeNo, BigDecimal.valueOf(Float.valueOf(totalFee)));
if (finishOrder) {
//返回成功
response.getWriter().write(
"<xml>\n"
+ " <return_code><![CDATA[SUCCESS]]></return_code>\n"
+ " <return_msg><![CDATA[OK]]></return_msg>\n"
+ "</xml>");
}
LOG.error("微信回调支付回写数据失败:" + xmlString);
//返回失败
response.getWriter().write("<xml>\n"
+ " <return_code><![CDATA[FAIL]]></return_code>\n"
+ " <return_msg><![CDATA[回写数据失败]]></return_msg>\n"
+ "</xml>");
}
这里可以使用注解使用Spring MVC返回xml
最后前端轮询查询订单状态展示相应页面即可。
这里整个支付流程就完成了,顺便说一下,官方sdk有测试流程说明文档,开启沙箱时返回的地址是不能扫描的,回调接口会自动调用,只需等待几秒查看返回结果就行。沙箱只能使用文档中几个金额,其他金额都会调用失败,每个金额对应的调用支付的不同情况,需要注意。微信仿真系统,最下面下载测试用例文档