赞
踩
需求:系统A对接支付宝,实现支持用户扫码支付
可选的支付方式有:
这里选择扫码付的方式,点击下单后,返回支付二维码,用户扫码支付。
画个下单流程的时序图:
大致流程:
查询支付结果流程:
退款流程同上查询支付结果。PS:注意下单、退款过程中,相关订单的业务数据落库到系统A。
支付信息不能在网络上明文传输,以防被篡改。系统A到支付宝的方向,采用:
同理,支付宝返回支付结果时,就是在支付宝中用系统A的公钥加密+支付宝的私钥签名,传输到系统A后,则是先用支付宝的公钥验签,再用系统A的私钥解密支付结果
调试过程中,可采用支付宝提供的沙箱环境,点击右上角控制台,登录后选择沙箱:
这里有一套可调试的APPID、系统A的公钥、密钥、支付宝的公钥、支付宝的网关地址,以及商家账户和用户账户(用于后续登录沙箱版本支付宝APP完成支付)
点击【沙箱工具】侧边栏,下载沙箱版支付宝APP,等于上面的买家账户。
前面提到,用户支付成功后,支付宝需要回调系统A接口来通知系统A,但我的开发环境在内网,支付宝访问不到,考虑做内网穿透,让支付宝通知到一个中转地址,再由中转地址到我的内网。穿透工具选择cpolar,下载地址 https://dashboard.cpolar.com/get-started,下载后,解压并安装msi包
双击exe文件,执行认证:
cpolar authtoken xxxx
创建隧道,建立链接:
cpolar http 9527
//返回结果
Forwarding http://maggie.cpolar.io -> localhost:9527
Forwarding https://maggie.cpolar.io -> localhost:9527
转发成功。此时,给支付宝访问forward的地址即可,比如系统A的异步通知接口:
localhost:9527/notify
那就是:
http://maggie.cpolar.io/notify
二维码是消息的载体。平时玩可直接在草料二维码UI页面,这里需要给系统A的订单服务用代码生成二维码。二维码中的信息自然是支付宝预下单返回的url。Java生成二维码可集成zxing库,但这样得自己两层for填充方格子:【SpringBoot整合ZXing创建二维码和条形码】 ,这里选择hutool工具类库(对zxing的二次封装),引入依赖:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
调用方式:
//生成直到url对应的二维码,宽高均300像素,可到路径,也可到Http响应
QrCodeUtil.generate("https://url/path", 300, 300, "png", httpServletResponse.getOutPutStream());
也可引入QrConfig对象,设置其他属性:
QrConfig config = new QrConfig(300, 300);
//纠错级别
config.setErrorCorrection(ErrorCorrectionLevel.H);
//二维码颜色
config.setBackColor(Color.BLUE);
QrCodeUtil.generate("https://www.baidu.com", config, new File("D:\\code.png"));
支付宝提供的SDK 中已经对加签验签逻辑做了封装,使用 SDK 时传入支付宝公钥等内容可直接通过 SDK 自动进行加验签。 SDK文档地址:https://opendocs.alipay.com/open/54/103419?pathHash=d6bc7c2b 。支付宝提供了两种SDK:
官网有通用版的API代码示例,这里走简易版的。引入简易版SDK的依赖:
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-easysdk -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-easysdk</artifactId>
<version>2.2.0</version>
</dependency>
在application.yml配置文件中统一写密钥、通知地址等(生产环境不要将私钥信息配置在源码中,例如配置为常量或储存在配置文件中,这样源码一丢,这些保密信息都泄漏了,放安全区域或服务器,运行时读取即可)
alipay:
easy:
protocol: https
gatewayHost: openapi-sandbox.dl.alipaydev.com
signType: RSA2
appId: 9021000133624745
merchantPrivateKey: MIIEvQIBADANBgkqhkiG9w0B
alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOC
notifyUrl: http://maggie.cpolar.io/notify
server:
port: 9527
@ConfigurationProperties注解统一读到:
@Configuration @Data @ConfigurationProperties(prefix = "alipay.easy") public class AliPayConfigInfo { /** * 请求协议 */ private String protocol; /** * 请求网关 */ private String gatewayHost; /** * 签名类型 */ private String signType; /** * 应用ID(来自支付宝申请) */ private String appId; /** * 应用秘钥 */ private String merchantPrivateKey; /** * 支付宝公钥 */ private String alipayPublicKey; /** * 支付结果异步通知的地址 */ private String notifyUrl; /** * 设施AES秘钥 */ private String encryptKey; }
将配置处理成Config类型的Bean,方便后面传入Config对象:
@Configuration public class AliPayConfig { @Bean public Config config(AliPayConfigInfo configInfo){ Config config = new Config(); config.protocol = configInfo.getProtocol(); config.gatewayHost = configInfo.getGatewayHost(); config.signType = configInfo.getSignType(); config.appId = configInfo.getAppId(); config.merchantPrivateKey = configInfo.getMerchantPrivateKey(); config.alipayPublicKey = configInfo.getAlipayPublicKey(); config.notifyUrl = configInfo.getNotifyUrl(); config.encryptKey = ""; return config; } }
写下单接口,响应一个二维码给前端,这里业务数据、订单编号直接写死,只做示意:
@RestController @Slf4j public class PayController { @Resource private Config config; /** * 收银台点击结账 * 发起下单请求 */ @GetMapping("/pay") public void pay(HttpServletRequest request, HttpServletResponse response) throws Exception { Factory.setOptions(config); //调用支付宝的接口 AlipayTradePrecreateResponse payResponse = Factory.Payment.FaceToFace().preCreate("订单主题:Mac笔记本", "LS123qwe123", "19999"); //参照官方文档响应示例,解析返回结果 String httpBodyStr = payResponse.getHttpBody(); JSONObject jsonObject = JSONObject.parseObject(httpBodyStr); String qrUrl = jsonObject.getJSONObject("alipay_trade_precreate_response").get("qr_code").toString(); QrCodeUtil.generate(qrUrl, 300, 300, "png", response.getOutputStream()); } }
异步回调参考文档:https://opendocs.alipay.com/open/194/103296?pathHash=e43f422e&ref=api,实现先全放Controller层了:
@RestController @Slf4j public class PayController { @Resource private Config config; /** * 给支付宝的回调接口 */ @PostMapping("/notify") public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception { Map<String, String> params = new HashMap<>(); //获取支付宝POST过来反馈信息,将异步通知中收到的待验证所有参数都存放到map中 Map<String, String[]> parameterMap = request.getParameterMap(); for (String name : parameterMap.keySet()) { String[] values = parameterMap.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("ISO-8859-1"), "utf-8"); params.put(name, valueStr); } //验签 Boolean signResult = Factory.Payment.Common().verifyNotify(params); if (signResult) { log.info("收到支付宝发送的支付结果通知"); String out_trade_no = request.getParameter("out_trade_no"); log.info("交易流水号:{}", out_trade_no); //交易状态 String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8"); //交易成功 switch (trade_status) { case "TRADE_SUCCESS": //支付成功的业务逻辑,比如落库,开vip权限等 log.info("订单:{} 交易成功", out_trade_no); break; case "TRADE_FINISHED": log.info("交易结束,不可退款"); //其余业务逻辑 break; case "TRADE_CLOSED": log.info("超时未支付,交易已关闭,或支付完成后全额退款"); //其余业务逻辑 break; case "WAIT_BUYER_PAY": log.info("交易创建,等待买家付款"); //其余业务逻辑 break; } response.getWriter().write("success"); //返回success给支付宝,表示消息我已收到,不用重调 } else { response.getWriter().write("fail"); ///返回fail给支付宝,表示消息我没收到,请重试 } } }
到此,看下效果,请求下单接口:
用沙箱版app扫码:
支付,查看余额:
主动查询用户的支付结果,订单编号依然写死:
@RestController
@Slf4j
public class PayController {
@Resource
private Config config;
@GetMapping("/query")
public String query() throws Exception {
Factory.setOptions(config);
AlipayTradeQueryResponse result = Factory.Payment.Common().query("LS123qwe123");
return result.getHttpBody();
}
}
退款操作:
@RestController
@Slf4j
public class PayController {
@Resource
private Config config;
@GetMapping("/refund")
public String refund() throws Exception {
Factory.setOptions(config);
AlipayTradeRefundResponse refundResponse = Factory.Payment.Common().refund("LS123qwe123", "19999");
return refundResponse.getHttpBody();
}
}
官方文档就是以这个SDK为例的,贴个代码示例:
private static final String GATEWAY_URL = "https://openapi.alipaydev.com/gateway.do"; private static final String FORMAT = "JSON"; private static final String CHARSET = "UTF-8"; //签名方式 private static final String SIGN_TYPE = "RSA2"; @Resource private AliPayConfig aliPayConfig; @Resource private OrdersMapper ordersMapper; @GetMapping("/pay") // &subject=xxx&traceNo=xxx&totalAmount=xxx public void pay(AliPay aliPay, HttpServletResponse httpResponse) throws Exception { // 1. 创建Client,通用SDK提供的Client,负责调用支付宝的API AlipayClient alipayClient = new DefaultAlipayClient(GATEWAY_URL, aliPayConfig.getAppId(), aliPayConfig.getAppPrivateKey(), FORMAT, CHARSET, aliPayConfig.getAlipayPublicKey(), SIGN_TYPE); // 2. 创建 Request并设置Request参数 AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); // 发送请求的 Request类 request.setNotifyUrl(aliPayConfig.getNotifyUrl()); JSONObject bizContent = new JSONObject(); bizContent.set("out_trade_no", aliPay.getTraceNo()); // 我们自己生成的订单编号 bizContent.set("total_amount", aliPay.getTotalAmount()); // 订单的总金额 bizContent.set("subject", aliPay.getSubject()); // 支付的名称 bizContent.set("product_code", "FAST_INSTANT_TRADE_PAY"); // 固定配置 request.setBizContent(bizContent.toString()); // 执行请求,拿到响应的结果,返回给浏览器 String form = ""; try { form = alipayClient.pageExecute(request).getBody(); // 调用SDK生成表单 } catch (AlipayApiException e) { e.printStackTrace(); } httpResponse.setContentType("text/html;charset=" + CHARSET); httpResponse.getWriter().write(form);// 直接将完整的表单html输出到页面 httpResponse.getWriter().flush(); httpResponse.getWriter().close(); }
具体有业务数据逻辑的对接支付宝接口,可跳转【支付宝业务对接】
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。