实用干货 | 一步一步教你在SpringBoot集成支付宝手机网站支付
手机网站支付文档手机网站支付的流程图:用户点击H5应用中的支付按钮点击支付按钮会请求后台接口,后台接口请求支付宝的支付接口,支付接口会返回一段html代码其中包括一个form表单和一段js代码用于自动提交表单,表单提交后就会自动跳转到支付宝的支付页面:支付宝集成-准备工作1.de...
一:简介
手机网站支付常用于HTML5应用,常见于微信公众号上的应用。手机网站支付文档
手机网站支付的流程图:
- 用户点击H5应用中的支付按钮
- 点击支付按钮会请求后台接口,后台接口请求支付宝的支付接口,支付接口会返回一段html代码其中包括一个form表单和一段js代码用于自动提交表单,表单提交后就会自动跳转到支付宝的支付页面(如果手机中装了支付App就去打开APP,如果没有就在网页版支付
- 支付成功后会调用支付时设置的同步url, 然后跳转到商户的后台系统,一般情况下商户系统会展示一下支付成功,以及购买的商品信息等视图
流程图
二:集成步骤
0. 创建应用、配置密钥
集成前需要先创建应用、配置密钥、回调地址等,具体操作请查看Spring Boot入门教程(三十五):支付宝集成-准备工作
1. dependency
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-thymeleaf
com.alipay
alipay-sdk-java
20170725114550
com.alipay
alipay-trade-sdk
20161215
com.google.code.gson
gson
commons-configuration
commons-configuration
1.10
com.google.zxing
core
3.2.1
org.projectlombok
lombok
2. application.yml
手机网站支付需要支付成功后同步的地址returnUrl,支付成功后跳转到的页面,这个值既可以配在这里也可以在Java代码调用支付宝接口时也可以设置为别的值
# 沙箱账号
pay:
alipay:
gatewayUrl: https://openapi.alipaydev.com/gateway.do
appid: 2016091400508498
appPrivateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCtXKWs+trRSuCxEUvlsEeSAuLWs3B/Dh74R8223BnfzoA29aGeoycAqfKlBMcbzU2G1KayESvZKGpwBLeejSbecRYjgZsQDyEaDimQQJtGFvZVV6u4XNnvIJ72eQzEaEIQfuiorjBTLm6DQuds4R0GxftqON6QFoIZkWB9ZrZKd02cuy16dW+UqtLVGGAHcCIAkB63zUiKSNfweMddneZ7MVs3lvu3xhMnD+5us/+n2Vp4qhfmpYLcdqIW6InU4GypeoOpyUTrfUGpgdR0l924vHy/GQJZEKFaRcK3cYK+ECyKpSIoqaJJFLHbkqsliuPpMUG+rM3jiqeIAH4psAznAgMBAAECggEBAJ5jyEbbxrJzrAh7GhHX1fwUQPYSadTbrPYAfHX2cHlnrQMJtsk+nTLhEv0r+VJwZ8WpYkfMong8kcqYtL7ajcmsHqMAFhE9EWxBxj2ymWsXLabZe93sj30IG9Rq0nxcGQgDO0RqKWLGSFgK93Al2KRInKT3InkY53K+vR61ihVLmGf7+GwPtIZteBy+tuAyvcj2RlkYvjiFi3ySyZ1wA3sJIlgrGiixd6fj20XFGNE3CnAwu0BJgXXbP/S9J4C0RRa3ZXj8fX7oONhVxz2xKxn7AT4e8OWjt7J41H2LRct8Fgl9pqgz2FJYv/WfbkG8x9fGiKYYvPXoxjjrk/tkewkCgYEA8f9Lcu5JPrE9rpw9zlwhm5cOO81xLxdwL5R5/1bRP48BZGIYuqlCbVvjJVqtO8eTnLhUwH7fG8B7cmoeO9bGr9GQrtfyCqz6FtVymTBieJlfgZDVhtzyv2qKOBMIFE8jsbSBK/NHHMvykJ+XdQ1riwCeQDdXICRuYTTFwGk2OsUCgYEAt2SoN95tVmVrvKG6ATLNEtge/ozeVywA4GjltrSw/G9vqp+DkkT2pY19uROuzMazoTzKWpPho2q/qzNlv/ANbOFM2GEmKamQ7CO88JgRxMsPTvc/HxCLU/ClMJU8LcOf9LfP2KYZpPwuheKJoF4vDGj8NsbFmccJyYSdpkNEk7sCgYBJlL2FMaz1sgC2Ue19DIhvfaunRV1P20mSPgwmNmijccETm7w3LXX0OIdFeV/JGHLqqSWj7i+6iXk/ncKZoUGCfi8G6sQ+uL/GJ5qTt6GJV+ExTS+PtSjeSO/EAw1m13Vb+C16hpstx1l23f+4aJ81gbecgPct38XsKpaiXZtOnQKBgQCMsN7QRYYxwoq9YoDUzIlAzKYyeBVWYL6najHYUZR5hG/xQIBqZRem9/4cTvpJxKInrwA6LrrqaEl0aHDFp75U6h7O3PCvA5PXZK9dD/yJsZIj7U/yX/nTQokn1UUegrYiwiTkusBvrrtuINWePsLvTVc4GpObHnPmsiNTWsWwYwKBgENaeTNOCHV2km/ysXQSEIhKbtlAMQPsgWHCt/bzHlF9m18izb1LrJyjzcSsd+Zy78R+pv4G50Q27c3e/DFPz/wYxN/yHWRbyLBA8ipJbCtMtPEdS9krpmN6cChIdLGbz4CVUqOPSRzNb9lhhgPCcCNRq6DG3HBceb1Se9VnO3zk
alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApFQKccMq+wPoWh93DcX3XYrnT7FJ3gntJA/jEwgk6Jd3iEVS2CyUCCgFVcQn8xjXT81YbZHYvoC50IBuu+A+Ey+J8VIgsBm5g9uwbOLRf66GrZjuKOlalHm5gHXjcL2gZRMBJEStOxstcO2YQriqhQzdL3EKp+HQc9u14IOVFpZdR8qq1l7CzKHn1vQo/1fUPfUrTLQqSuQTU9Ospv/QHFqOJA7DPetUQ+jnaZ082f3clr4ITw4EE8A6IWqTXcETTx5j/udCGP84g2Y4j+8i9DqYGyD5ePVgt4G0ICBX1bi1qNlylfxRg8Y3c1DFrRGyr0NpKQxSVXkYaVNvrCoudQIDAQAB
returnUrl: http://yxep7y.natappfree.cc/alipay/return
notifyUrl: http://yxep7y.natappfree.cc/alipay/notify
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML5
encoding: UTF-8
3.AlipayProperties
@Data
@Slf4j
@ConfigurationProperties(prefix = "pay.alipay")
public class AlipayProperties {
/** 支付宝gatewayUrl */
private String gatewayUrl;
/** 商户应用id */
private String appid;
/** RSA私钥,用于对商户请求报文加签 */
private String appPrivateKey;
/** 支付宝RSA公钥,用于验签支付宝应答 */
private String alipayPublicKey;
/** 签名类型 */
private String signType = "RSA2";
/** 格式 */
private String formate = "json";
/** 编码 */
private String charset = "UTF-8";
/** 同步地址 */
private String returnUrl;
/** 异步地址 */
private String notifyUrl;
/** 最大查询次数 */
private static int maxQueryRetry = 5;
/** 查询间隔(毫秒) */
private static long queryDuration = 5000;
/** 最大撤销次数 */
private static int maxCancelRetry = 3;
/** 撤销间隔(毫秒) */
private static long cancelDuration = 3000;
private AlipayProperties() {}
/**
* PostContruct是spring框架的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。
*/
@PostConstruct
public void init() {
log.info(description());
}
public String description() {
StringBuilder sb = new StringBuilder("\nConfigs{");
sb.append("支付宝网关: ").append(gatewayUrl).append("\n");
sb.append(", appid: ").append(appid).append("\n");
sb.append(", 商户RSA私钥: ").append(getKeyDescription(appPrivateKey)).append("\n");
sb.append(", 支付宝RSA公钥: ").append(getKeyDescription(alipayPublicKey)).append("\n");
sb.append(", 签名类型: ").append(signType).append("\n");
sb.append(", 查询重试次数: ").append(maxQueryRetry).append("\n");
sb.append(", 查询间隔(毫秒): ").append(queryDuration).append("\n");
sb.append(", 撤销尝试次数: ").append(maxCancelRetry).append("\n");
sb.append(", 撤销重试间隔(毫秒): ").append(cancelDuration).append("\n");
sb.append("}");
return sb.toString();
}
private String getKeyDescription(String key) {
int showLength = 6;
if (StringUtils.isNotEmpty(key) && key.length() > showLength) {
return new StringBuilder(key.substring(0, showLength)).append("******")
.append(key.substring(key.length() - showLength)).toString();
}
return null;
}
}
4.AlipayConfiguration
@Configuration
@EnableConfigurationProperties(AlipayProperties.class)
public class AlipayConfiguration {
@Autowired
private AlipayProperties properties;
@Bean
public AlipayTradeService alipayTradeService() {
return new AlipayTradeServiceImpl.ClientBuilder()
.setGatewayUrl(properties.getGatewayUrl())
.setAppid(properties.getAppid())
.setPrivateKey(properties.getAppPrivateKey())
.setAlipayPublicKey(properties.getAlipayPublicKey())
.setSignType(properties.getSignType())
.build();
}
@Bean
public AlipayClient alipayClient(){
return new DefaultAlipayClient(properties.getGatewayUrl(),
properties.getAppid(),
properties.getAppPrivateKey(),
properties.getFormate(),
properties.getCharset(),
properties.getAlipayPublicKey(),
properties.getSignType());
}
}
5. AlipayWAPPayController
/**
* 支付宝-手机网站支付.
*
* 手机网站支付
*
* @author Mengday Zhang
* @version 1.0
* @since 2018/6/11
*/
@Slf4j
@Controller
@RequestMapping("/alipay/wap")
public class AlipayWAPPayController {
@Autowired
private AlipayProperties alipayProperties;
@Autowired
private AlipayClient alipayClient;
/**
* 去支付
*
* 支付宝返回一个form表单,并自动提交,跳转到支付宝页面
*
* @param response
* @throws Exception
*/
@PostMapping("/alipage")
public void gotoPayPage(HttpServletResponse response) throws AlipayApiException, IOException {
// 订单模型
String productCode="QUICK_WAP_WAY";
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(UUID.randomUUID().toString());
model.setSubject("支付测试");
model.setTotalAmount("0.01");
model.setBody("支付测试,共0.01元");
model.setTimeoutExpress("2m");
model.setProductCode(productCode);
AlipayTradeWapPayRequest wapPayRequest =new AlipayTradeWapPayRequest();
wapPayRequest.setReturnUrl("http://yxep7y.natappfree.cc/alipay/wap/returnUrl");
wapPayRequest.setNotifyUrl(alipayProperties.getNotifyUrl());
wapPayRequest.setBizModel(model);
// 调用SDK生成表单, 并直接将完整的表单html输出到页面
String form = alipayClient.pageExecute(wapPayRequest).getBody();
System.out.println(form);
response.setContentType("text/html;charset=" + alipayProperties.getCharset());
response.getWriter().write(form);
response.getWriter().flush();
response.getWriter().close();
}
/**
* 支付宝页面跳转同步通知页面
* @param request
* @return
* @throws UnsupportedEncodingException
* @throws AlipayApiException
*/
@RequestMapping("/returnUrl")
public String returnUrl(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException, AlipayApiException {
response.setContentType("text/html;charset=" + alipayProperties.getCharset());
//获取支付宝GET过来反馈信息
Map params = new HashMap<>();
Map requestParams = request.getParameterMap();
for (Iterator 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] + ",";
}
//乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化
valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
boolean verifyResult = AlipaySignature.rsaCheckV1(params, alipayProperties.getAlipayPublicKey(), alipayProperties.getCharset(), "RSA2");
if(verifyResult){
//验证成功
//请在这里加上商户的业务逻辑程序代码,如保存支付宝交易号
//商户订单号
String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8");
//支付宝交易号
String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8");
return "wapPaySuccess";
}else{
return "wapPayFail";
}
}
/**
* 退款
* @param orderNo 商户订单号
* @return
*/
@PostMapping("/refund")
@ResponseBody
public String refund(String orderNo) throws AlipayApiException {
AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest();
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
// 商户订单号
model.setOutTradeNo(orderNo);
// 退款金额
model.setRefundAmount("0.01");
// 退款原因
model.setRefundReason("无理由退货");
// 退款订单号(同一个订单可以分多次部分退款,当分多次时必传)
// model.setOutRequestNo(UUID.randomUUID().toString());
alipayRequest.setBizModel(model);
AlipayTradeRefundResponse alipayResponse = alipayClient.execute(alipayRequest);
System.out.println(alipayResponse.getBody());
return alipayResponse.getBody();
}
/**
* 退款查询
* @param orderNo 商户订单号
* @param refundOrderNo 请求退款接口时,传入的退款请求号,如果在退款请求时未传入,则该值为创建交易时的外部订单号
* @return
* @throws AlipayApiException
*/
@GetMapping("/refundQuery")
@ResponseBody
public String refundQuery(String orderNo, String refundOrderNo) throws AlipayApiException {
AlipayTradeFastpayRefundQueryRequest alipayRequest = new AlipayTradeFastpayRefundQueryRequest();
AlipayTradeFastpayRefundQueryModel model=new AlipayTradeFastpayRefundQueryModel();
model.setOutTradeNo(orderNo);
model.setOutRequestNo(refundOrderNo);
alipayRequest.setBizModel(model);
AlipayTradeFastpayRefundQueryResponse alipayResponse = alipayClient.execute(alipayRequest);
System.out.println(alipayResponse.getBody());
return alipayResponse.getBody();
}
/**
* 关闭交易
* @param orderNo
* @return
* @throws AlipayApiException
*/
@PostMapping("/close")
@ResponseBody
public String close(String orderNo) throws AlipayApiException {
AlipayTradeCloseRequest alipayRequest = new AlipayTradeCloseRequest();
AlipayTradeCloseModel model =new AlipayTradeCloseModel();
model.setOutTradeNo(orderNo);
alipayRequest.setBizModel(model);
AlipayTradeCloseResponse alipayResponse= alipayClient.execute(alipayRequest);
System.out.println(alipayResponse.getBody());
return alipayResponse.getBody();
}
}
6. AlipayController
/**
* 支付宝通用接口.
*
* detailed description
*
* @author Mengday Zhang
* @version 1.0
* @since 2018/6/13
*/
@Slf4j
@RestController
@RequestMapping("/alipay")
public class AlipayController {
@Autowired
private AlipayProperties aliPayProperties;
@Autowired
private AlipayTradeService alipayTradeService;
/**
* 支付异步通知
*
* https://docs.open.alipay.com/194/103296
*/
@RequestMapping("/notify")
public String notify(HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
// 一定要验签,防止黑客篡改参数
Map parameterMap = request.getParameterMap();
StringBuilder notifyBuild = new StringBuilder("/****************************** alipay notify ******************************/\n");
parameterMap.forEach((key, value) -> notifyBuild.append(key + "=" + value[0] + "\n") );
log.info(notifyBuild.toString());
// https://docs.open.alipay.com/54/106370
// 获取支付宝POST过来反馈信息
Map params = new HashMap<>();
Map requestParams = request.getParameterMap();
for (Iterator 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] + ",";
}
params.put(name, valueStr);
}
boolean flag = AlipaySignature.rsaCheckV1(params,
aliPayProperties.getAlipayPublicKey(),
aliPayProperties.getCharset(),
aliPayProperties.getSignType());
if (flag) {
/**
* TODO 需要严格按照如下描述校验通知数据的正确性
*
* 商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,
* 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),
* 同时需要校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email),
*
* 上述有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。
* 在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。
* 在支付宝的业务通知中,只有交易通知状态为TRADE_SUCCESS或TRADE_FINISHED时,支付宝才会认定为买家付款成功。
*/
//交易状态
String tradeStatus = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");
// TRADE_FINISHED(表示交易已经成功结束,并不能再对该交易做后续操作);
// TRADE_SUCCESS(表示交易已经成功结束,可以对该交易做后续操作,如:分润、退款等);
if(tradeStatus.equals("TRADE_FINISHED")){
//判断该笔订单是否在商户网站中已经做过处理
//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,
// 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序
//请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
//如果有做过处理,不执行商户的业务程序
//注意:
//如果签约的是可退款协议,退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知
//如果没有签约可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。
} else if (tradeStatus.equals("TRADE_SUCCESS")){
//判断该笔订单是否在商户网站中已经做过处理
//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,
// 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序
//请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
//如果有做过处理,不执行商户的业务程序
//注意:
//如果签约的是可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。
}
return "success";
}
return "fail";
}
/**
* 订单查询(最主要用于查询订单的支付状态)
* @param orderNo 商户订单号
* @return
*/
@GetMapping("/query")
public String query(String orderNo){
AlipayTradeQueryRequestBuilder builder = new AlipayTradeQueryRequestBuilder()
.setOutTradeNo(orderNo);
AlipayF2FQueryResult result = alipayTradeService.queryTradeResult(builder);
switch (result.getTradeStatus()) {
case SUCCESS:
log.info("查询返回该订单支付成功: )");
AlipayTradeQueryResponse resp = result.getResponse();
log.info(resp.getTradeStatus());
// log.info(resp.getFundBillList());
break;
case FAILED:
log.error("查询返回该订单支付失败!!!");
break;
case UNKNOWN:
log.error("系统异常,订单支付状态未知!!!");
break;
default:
log.error("不支持的交易状态,交易返回异常!!!");
break;
}
return result.getResponse().getBody();
}
}
7. WebMvcConfiguration
通过访问http://localhost:8080/toPay来跳转到toPay.html页面
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Override
protected void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/toPay").setViewName("toPay");
super.addViewControllers(registry);
}
}
8. templates
toPay.html
Title
bdall.peaapk.com
xtrb.peaapk.com
hdwxxg.peaapk.com
qhdceo.peaapk.com
tsxxg.peaapk.com
lyshangdu.peaapk.com
hsrb.peaapk.com
lanfxx.peaapk.com
chengdenews.peaapk.com
szyongdian.peaapk.com
dtnews.peaapk.com
yqnews.peaapk.com
changzhinews.peaapk.com
jcnews.peaapk.com
sxjzxww.peaapk.com
sxycrb.peaapk.com
lfxww.peaapk.com
sxllnews.peaapk.com
nmgnews.peaapk.com
baotounews.peaapk.com
tlsxxg.peaapk.com
bynesrmtzx.peaapk.com
shenyangx.peaapk.com
dlxxgs.peaapk.com
northtimes.peaapk.com
jilin.peaapk.com
ccxxg.peaapk.com
0437.peaapk.com
ybrbnews.peaapk.com
avapk
wapPaySuccess.html
Title
WAP 支付成功,请及时享用!欢迎下次再来
wapPayFail.html
Title
WAP 支付失败,请重新支付
三:运行结果
首先访问去支付页面:http://localhost:8080/toPay
获取源码
关注并私信“支付宝手机网站支付”获取源代码。
推荐阅读:梦想女人网