第一步准备
(1)微信需要的公众服务号和商户号;沙箱有很多问题,所以本人以正式的配置实现,其中公众号需要配置授权路径
其中公众号需配置
商户号需到产品中心 -> 开发配置 -> 支付配置 ->添加JSAPI支付
(2)支付宝需要的商户号
(3)WEB项目:本人的项目基于SpringBoot+SSM框架的网站项目
(4)demo:github链接
第二步生成二维码
(1)在pom.xml引入依赖:
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
(2)编写工具类QrCodeUtils:
package com.meal.util;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Hashtable;
import java.util.Random;/**
* 工程名:meal
* 包名:com.meal.util
* 文件名:QrCodeUtils.java
* @author lcwen
* @version $Id: QrCodeUtils.java 2020年3月10日 上午11:53:55 $
*/
public class QrCodeUtils { private static final String CHARSET = "utf-8";
public static final String FORMAT = "JPG";
// 二维码尺寸
private static final int QRCODE_SIZE = 180;
// LOGO宽度
private static final int LOGO_WIDTH = 60;
// LOGO高度
private static final int LOGO_HEIGHT = 60; /**
* 生成二维码
*
* @param content 二维码内容
* @param logoPath logo地址
* @param needCompress 是否压缩logo
* @return 图片
* @throws Exception
*/
public static BufferedImage createImage(String content, String logoPath, boolean needCompress) throws Exception {
Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE,
hints);
int width = bitMatrix.getWidth();
int height = bitMatrix.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, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
if (logoPath == null || "".equals(logoPath)) {
return image;
}
// 插入图片
QrCodeUtils.insertImage(image, logoPath, needCompress);
return image;
} /**
* 插入LOGO
*
* @param source 二维码图片
* @param logoPath LOGO图片地址
* @param needCompress 是否压缩
* @throws IOException
*/
private static void insertImage(BufferedImage source, String logoPath, boolean needCompress) throws IOException {
InputStream inputStream = null;
try {
inputStream = FileUtils.getResourceAsStream(logoPath);
Image src = ImageIO.read(inputStream);
int width = src.getWidth(null);
int height = src.getHeight(null);
if (needCompress) { // 压缩LOGO
if (width > LOGO_WIDTH) {
width = LOGO_WIDTH;
}
if (height > LOGO_HEIGHT) {
height = LOGO_HEIGHT;
}
Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = tag.getGraphics();
g.drawImage(image, 0, 0, null); // 绘制缩小后的图
g.dispose();
src = image;
}
// 插入LOGO
Graphics2D graph = source.createGraphics();
int x = (QRCODE_SIZE - width) / 2;
int y = (QRCODE_SIZE - height) / 2;
graph.drawImage(src, x, y, width, height, null);
Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6);
graph.setStroke(new BasicStroke(3f));
graph.draw(shape);
graph.dispose();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
} /**
* 生成二维码(内嵌LOGO)
* 二维码文件名随机,文件名可能会有重复
*
* @param content 内容
* @param logoPath LOGO地址
* @param destPath 存放目录
* @param needCompress 是否压缩LOGO
* @throws Exception
*/
public static String encode(String content, String logoPath, String destPath, boolean needCompress) throws Exception {
BufferedImage image = QrCodeUtils.createImage(content, logoPath, needCompress);
mkdirs(destPath);
String fileName = new Random().nextInt(99999999) + "." + FORMAT.toLowerCase();
ImageIO.write(image, FORMAT, new File(destPath + "/" + fileName));
return fileName;
} /**
* 生成二维码(内嵌LOGO)
* 调用者指定二维码文件名
*
* @param content 内容
* @param logoPath LOGO地址
* @param destPath 存放目录
* @param fileName 二维码文件名
* @param needCompress 是否压缩LOGO
* @throws Exception
*/
public static String encode(String content, String logoPath, String destPath, String fileName, boolean needCompress) throws Exception {
BufferedImage image = QrCodeUtils.createImage(content, logoPath, needCompress);
mkdirs(destPath);
fileName = fileName.substring(0, fileName.indexOf(".") > 0 ? fileName.indexOf(".") : fileName.length())
+ "." + FORMAT.toLowerCase();
ImageIO.write(image, FORMAT, new File(destPath + "/" + fileName));
return fileName;
} /**
* 当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir.
* (mkdir如果父目录不存在则会抛出异常)
*
* @param destPath 存放目录
*/
public static void mkdirs(String destPath) {
File file = new File(destPath);
if (!file.exists() && !file.isDirectory()) {
file.mkdirs();
}
} /**
* 生成二维码(内嵌LOGO)
*
* @param content 内容
* @param logoPath LOGO地址
* @param destPath 存储地址
* @throws Exception
*/
public static String encode(String content, String logoPath, String destPath) throws Exception {
return QrCodeUtils.encode(content, logoPath, destPath, false);
} /**
* 生成二维码
*
* @param content 内容
* @param destPath 存储地址
* @param needCompress 是否压缩LOGO
* @throws Exception
*/
public static String encode(String content, String destPath, boolean needCompress) throws Exception {
return QrCodeUtils.encode(content, null, destPath, needCompress);
} /**
* 生成二维码
*
* @param content 内容
* @param destPath 存储地址
* @throws Exception
*/
public static String encode(String content, String destPath) throws Exception {
return QrCodeUtils.encode(content, null, destPath, false);
} /**
* 生成二维码(内嵌LOGO)
*
* @param content 内容
* @param logoPath LOGO地址
* @param output 输出流
* @param needCompress 是否压缩LOGO
* @throws Exception
*/
public static void encode(String content, String logoPath, OutputStream output, boolean needCompress)
throws Exception {
BufferedImage image = QrCodeUtils.createImage(content, logoPath, needCompress);
ImageIO.write(image, FORMAT, output);
} /**
* 生成二维码
*
* @param content 内容
* @param output 输出流
* @throws Exception
*/
public static void encode(String content, OutputStream output) throws Exception {
QrCodeUtils.encode(content, null, output, false);
}}
(3)调用工具类生产二维码(根据各自业务生成对应的二维码,其中二维码路径可用redis缓存并设置过期时间减少二维码频繁生成,过期可重新生成)
第三步:在微信和支付宝开发文档下载对应的SDK DEMO
(1)微信支付宝引入依赖
<!-- 支付宝SDK -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.5.ALL</version>
</dependency>
<!-- 微信支付SDK -->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
<!-- XML转bean对象 -->
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.10</version>
</dependency>
(2)对应的包目录
(3)对应的代码:链接
第四步:扫描进入二维码对应的URL
(1)控制层处理
/**
* 扫描二维码进入控制器处理具体业务逻辑
*/ @GetMapping(value = "****")
public String payh5(@PathVariable String ***) throws Exception{
String userAgent = request.getHeader("user-agent");
if (userAgent == null
|| !(userAgent.contains("AlipayClient") || userAgent
.contains("MicroMessenger"))) {
log.info("未知来源扫码进入付费模块,返回无页面...");
return "****";
}
if (userAgent.contains("AlipayClient")) { //支付宝
Map<String, Object> pay = vipChargeService.findParamForPay(***);//对应需要的参数
request.setAttribute("info", pay.get("info")); //前端H5页面需要的参数
request.setAttribute("payParam", pay.get("payParam"));//支付宝支付需要的参数
return "school/vip/alipay_h5";
}else{
Map<String, Object> pay = vipChargeService.findParamForPay(****);//对应需要的参数
request.setAttribute("appId", pay.get("appId"));//APPID - 公众号APPID
request.setAttribute("url", pay.get("url")); //微信静默授权成功后 跳转到微信支付页面的URL
request.setAttribute("state", pay.get("state"));//微信附加参数
return "**/wx_auth"; //微信授权页面
}
} /**
* 跳转到微信支付页 ,该处为微信授权成功后回调路径
* @param state 微信返回附加参数
* @param code 授权code
* @return
*/
@GetMapping(value = "/wxpayh5/")
public String getWxpayParam(String state ,String code){
if (StringUtils.isEmpty(state) || StringUtils.isEmpty(code)) {
return "***/expire_h5";//过期页面
}
try {
vipChargeService.findWxPayMwebUrl(request,***, code);
return "***/wxpay_h5";//支付页面
} catch (Exception e) {
log.error("微信获取支付参数出错,错误信息:", e.getMessage(), e);
e.printStackTrace();
}
return "***/expire_h5";//过期页面
}
(2)业务层处理vipChargeService
@Override
public Map<String, Object> findParamForPay(***, int payType) throws Exception {
long time = redisGetService.getOrderExpireTime(***); //获取剩余过期时间单位秒
Date expireTime = new Date(new Date().getTime() + 1000 * time);//获取过期的具体时间
String backParams = ****;//附加参数
Map<String, Object> result = new HashMap<String, Object>();
if (payType == 1) { //微信
WXPayConfig config = new WxpayConfig();
result.put("appId", config.getAppID());
result.put("url", ****);
result.put("state", backParams);
}else { //支付宝
String payParam = AlipayUtil.getPayParam(****);
payParam = payParam.replace("<script>document.forms[0].submit();</script>", "");
result.put("payParam", payParam);
Map<String, Object> info = new HashMap<String, Object>();
//TODO 添加页面需要的参数
result.put("info", info);
}
return result;
} @Override
public void findWxPayMwebUrl(HttpServletRequest request, ***, String code)
throws Exception {
String openid = WxSignUtil.accessWithOpenid(code);//获取微信用户openid //TODO 获取相关的参数
long time = redisGetService.getOrderExpireTime(***);
Date expireTime = new Date(new Date().getTime() + 1000 * time);
String ip = *****;//获取客户端IP地址 -> 自己百度
String notify_url = UrlConstants.VIP_NOTIFY_URL;
String attach = "****";//附带参数
Map<String, Object> result = WxPayUtil.getH5PayParam(***, openid);
if (result == null) {
throw new Exception("获取微信订单参数失败");
}
Map<String, Object> info = new HashMap<String, Object>();
//TODO 返回至支付页面需要的参数
request.setAttribute("info", info);
request.setAttribute("result", result);
}
(3)微信的授权页面、微信的支付页面、支付宝的支付页面:链接
第五步:异步通知
(1)支付宝异步通知/同步通知
package com.meal.module.school.api;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;import com.alipay.api.internal.util.AlipaySignature;
import com.meal.constant.UrlRoot;
import com.meal.module.admin.order.service.PcOrderService;
import com.meal.pay.alipay.config.AlipayConfig;/**
* 文件名:AlipayController.java
* @author lcwen
* @version $Id: AlipayController.java 2020年3月11日 上午11:34:45 $
*/
@Controller
@RequestMapping(value = UrlRoot.ALIPAY_NOTIFY_URL)
public class AlipayController {
private static final Logger log = LoggerFactory.getLogger(AlipayController.class);
@Autowired
private HttpServletRequest request;
@RequestMapping(value = "/notify_url")
public void notify_url(){
try {
boolean signVerified = signVerify(request);
if (!signVerified) {// 是否验证不成功
log.error("支付宝异步通知验证签名失败");
return;
}
String trade_status = new String(request.getParameter(
"trade_status").getBytes("ISO-8859-1"), "UTF-8");
if (!trade_status.equals("TRADE_FINISHED") && !trade_status.equals("TRADE_SUCCESS")) {
log.info("支付宝异步通知回调结果:失败");
return;
}
String out_trade_no = request.getParameter("out_trade_no");
String trade_no = request.getParameter("trade_no");
//TODO处理订单
//**********************
} catch (Exception e) {
log.error("支付宝异步通知出错,错误信息:",e.getMessage(),e);
e.printStackTrace();
}
}
/**
* 简要说明:同步路径 <br>
* 创建者:lcwen
* 创建时间:2020年3月12日 下午5:47:17
* @return
*/
@RequestMapping(value = "/return_url")
public String return_url(){
try {
boolean signVerified = signVerify(request);
if (!signVerified) {
request.setAttribute("msg", "支付失败,签名验证错误!");
return "******"; //支付失败页面
}
return "*****";//支付成功页面
} catch (Exception e) {
request.setAttribute("msg", "支付失败,同步通知出错!");
log.error("支付宝同步通知出错,错误信息:",e.getMessage(),e);
e.printStackTrace();
}
return "*****"; //支付失败页面
}
/**
* 简要说明:支付宝签名验证 <br>
*************************页面功能说明*************************
* 创建该页面文件时,请留心该页面文件中无任何HTML代码及空格。
* 该页面不能在本机电脑测试,请到服务器上做测试。请确保外部可以访问该页面。
* 如果没有收到该页面返回的 success
* 建议该页面只做支付成功的业务逻辑处理,退款的处理请以调用退款查询接口的结果为准。
* 创建者:lcwen
* 创建时间:2020年3月12日 下午5:00:28
* @param request
* @return
* @throws Exception
*/
private boolean signVerify(HttpServletRequest request) throws Exception{
boolean signVerified = false;
// 获取支付宝POST过来反馈信息
Map<String, String> params = new HashMap<String, String>();
Map<String, String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter
.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
// 乱码解决,这段代码在出现乱码时使用
valueStr = new String(valueStr.getBytes("utf-8"), "utf-8");
params.put(name, valueStr);
}
signVerified = AlipaySignature.rsaCheckV1(params,
AlipayConfig.ALIPAY_PUBLIC_KEY, AlipayConfig.CHARSET,
AlipayConfig.SIGNTYPE); // 调用SDK验证签名
return signVerified;
}}
(2)微信异步通知
package com.meal.module.school.api;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.math.BigDecimal;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;import com.github.wxpay.sdk.WXPayConfig;
import com.github.wxpay.sdk.WXPayUtil;
import com.meal.constant.UrlRoot;
import com.meal.module.admin.order.service.PcOrderService;
import com.meal.pay.wxpay.config.WxpayConfig;
import com.meal.pay.wxpay.pojo.NotifyInfo;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.XmlFriendlyNameCoder;
import com.thoughtworks.xstream.io.xml.XppDriver;/**
* 文件名:WxpayController.java
* @author lcwen
* @version $Id: WxpayController.java 2020年3月11日 上午11:35:02 $
*/
@Controller
@RequestMapping(value = ****)
public class WxpayController {
private static final Logger log = LoggerFactory.getLogger(WxpayController.class);
@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;
/**
* 简要说明:异步通知 <br>
* 详细说明:TODO
* 创建者:lcwen
* 创建时间:2020年3月13日 上午10:39:26
* 更新者:
* 更新时间:
*/
@RequestMapping(value = "/notify_url")
public void notify_url() throws Exception{
String notityXml = "";
String inputLine = "";
while ((inputLine = request.getReader().readLine()) != null) {
notityXml += inputLine;
}
request.getReader().close();
WXPayConfig config = new WxpayConfig();
XStream xs = new XStream(new XppDriver(new XmlFriendlyNameCoder("_-",
"_")));
xs.alias("xml", NotifyInfo.class);
NotifyInfo ntfInfo = (NotifyInfo)xs.fromXML(notityXml.toString());
// 验证签名是否正确
boolean isSign = WXPayUtil.isSignatureValid(notityXml, config.getKey());
if(!isSign){
signFail(response);
return;
}
if (!"SUCCESS".equals(ntfInfo.getReturn_code())
|| !"SUCCESS".equals(ntfInfo.getResult_code())) {
payFail(response,ntfInfo.getErr_code());
return;
}
//订单号、微信交易号、
String out_trade_no = ntfInfo.getOut_trade_no();
String trade_no = ntfInfo.getTransaction_id();
//TODO 订单处理
//**************************
payOk(response);
}
/**
* 支付成功
*/
private static void payOk(HttpServletResponse response){
log.info("================ 微信支付异步回调支付成功返回 =================");
String resXml = "<xml><return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg></xml> ";
flushResponse(response, resXml);
}
/**
* 支付失败
*/
private static void payFail(HttpServletResponse response ,String errCode){
log.info("================ 微信支付异步回调支付失败返回 =================");
String resXml = "<xml><return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[" + errCode + "]]></return_msg></xml>";
flushResponse(response, resXml);
}
/**
* 签名失败
*/
private static void signFail(HttpServletResponse response){
log.info("================ 微信支付异步回调签名失败 =================");
String resXml = "<xml><return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[签名有误]]></return_msg></xml> ";
flushResponse(response, resXml);
}
private static void flushResponse(HttpServletResponse response ,String resXml){
BufferedOutputStream out = null;
try {
out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes("utf-8"));
out.flush();
} catch (Exception e) {
e.printStackTrace();
}finally{
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}}
第六部:效果(实际项目)