一:简介
手机网站支付常用于HTML5应用,常见于微信公众号上的应用。手机网站支付文档
手机网站支付的流程图:
1.用户点击H5应用中的支付按钮
2.点击支付按钮会请求后台接口,后台接口请求支付宝的支付接口,支付接口会返回一段html代码其中包括一个form表单和一段js代码用于自动提交表单,表单提交后就会自动跳转到支付宝的支付页面(如果手机中装了支付App就去打开APP,如果没有就在网页版支付。
3.支付成功后会调用支付时设置的同步url, 然后跳转到商户的后台系统,一般情况下商户系统会展示一下支付成功,以及购买的商品信息等视图流程图如下:
在签约的时候需要提供经营信息和服务接入渠道对应的信息,经营信息一般需要人事或者老板提供、服务接入渠道一般需要产品来提供,这些资料可能不会立马能给到你,如果签约不成功就没法测试,基于此,支付宝提供了一个【沙箱环境】,所谓的沙箱环境就是支付宝帮你创建了一个临时用于开发测试的应用,称为沙箱应用,并签约了所有功能,沙箱环境提供一套账号信息(包括卖家账号和买家账号)称之为沙箱账号,还提供一个沙箱支付宝安卓版的应用,可以使用沙箱账号登录沙箱应用来进行支付操作,所有的支付都是假的,你也不用担心你的钱会扣掉,因为登录的是沙箱账号而不是自己的账号,也不用担心买家的钱不够用,因为自己可以随意充值,沙箱环境用于前期的研发以及个人学习研究使用。请参照支付宝沙箱环境地址:https://docs.open.alipay.com/200/105311/
二:SpringBoot集成步骤
集成前需要先创建应用、配置密钥、回调地址等
1. dependency
2. application.yml
手机网站支付需要支付成功后同步的地址returnUrl,支付成功后跳转到的页面,这个值既可以配在这里也可以在Java代码调用支付宝接口时也可以设置为别的值
# 沙箱账号
pay:
alipay:
gatewayUrl: https://openapi.alipaydev.com/gateway.do
appid: 2016091400508498
appPrivateKey: 根据自己的沙箱环境填写私钥
alipayPublicKey: 根据自己的沙箱环境填写阿里支付宝的公钥
returnUrl: http://localhost:8080/alipay/return
notifyUrl: http://localhost:8080/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 致远
* @version 1.0
* @since 2019/1/29
*/
@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
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 致远
* @version 1.0
* @since 2019/1/29
*/
@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
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
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
wapPaySuccess.html
wapPayFail.html
三:运行结果
首先访问去支付页面:http://localhost:8080/toPay