概述
在线支付是指卖方与买方通过互联网上的电子商务网站进行交易时,银行为其提供网上资金结算服务的一种业务。它为企业和个人提供了一个安全、快捷、方便的电子商务应用环境和网上资金结算工具。在线支付不仅帮助企业实现了销售款项的快速归集,缩短收款周期,同时也为个人网上银行客户提供了网上消费支付结算方式,使客户真正做到足不出户,网上购物。
在线支付是一种通过第三方提供的与银行之间的支付接口进行支付的方式,这种方式的好处在于可以直接把资金从用户的银行卡中转账到网站账户中,汇款马上到账,不需要人工确认。与到银行转账[包括通过网上个人银行转账或者到银行柜台办理现金转账]的最大区别就在于可以自动确认预付款。
来源百度百科
代码地址 https://gitee.com/gaibianzlp/pay_demo.git
支付宝
开发前准备
登录支付宝开发平台
地址: https://open.alipay.com/
用支付宝扫码登入
研发服务
https://open.alipay.com/platform/developerIndex.htm
沙箱应用

配置秘钥
参照官方文档即可,按照步骤来保存私钥与公钥:官方文档

下载安装后生成秘钥
沙箱App下载
https://openhome.alipay.com/platform/appDaily.htm?tab=tool
外网环境映射
地址信息: https://www.ngrok.cc/
Sunny-Ngrok 开通隧道
首先在本站注册成为会员
- 注册会员
- 登陆
选择需要开通的服务器


本站提供Ngrok和Frp两种服务器,两种服务器都支持http、https、tcp转发,Ngrok不支持udp转发,但是Frp支持udp的。两个服务器在使用上没有太多差别。如果是调试支付回调或者微信开发的话,Ngrok会比Frp好更多,因为可以http请求重现。开通隧道

在上图中表单信息解释
隧道名称:可以随便填写,无关紧要只是为了一个备注
前置域名:服务器免费赠送的域名,请不要带上后缀,如果要 sunny.free.idcfengye.com 只需要填写 sunny 即可
本地端口:可以为同一个局域网内任意一台机器进行映射,只需要填对ip和端口就行,例如:192.168.1.1:80
http验证用户名:非必填项,在需要的时候填写,否则可以不填
http验证密码:非必填项,在需要的时候填写,否则可以不填
Ngrok 映射本地地址
客户端id <=> 隧道id
映射成功
http://zlpmall.free.idcfengye.com -> 127.0.0.1:8081 
SpringBoot 整合 Alipay
流程图
大体结构图:
详细流程图
退款流程图
引入依赖
<dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>3.4.27.ALL</version></dependency>
Alipay 基础信息配置
# 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号appId=2016092900622761# 商户私钥,您的PKCS8格式RSA2私钥privateKey=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCWmkxO0Tz2a65nPQFmdRfRDTIwqWl78g9ra9QSi/7FufXDuWbUjZinktDCdkLXqwqJcqQI+4OEF36leTHYhVYIU173HoePT0ozt/2F0y1VlUu9XULdFUudKrnPWAurEo/iQmYCsX/OxnDcVnT71vSErWp7mf1KGfrZBDl8N+v21csRc/d3MwNpkHU+1etKj7g0TKg4AAmLQgK3n116lpv3KWBiDMMnsNPXp7iIvIDKVPN0k3rixzbrA/mGFJnHqOogknhvkgH22F5s7JgUVrTKcHiqffhqHwjYelZjxHVWBxCojaY/w4ILEt++J5qSORbNBMBewYL/8W/Swzdr9CVJAgMBAAECggEBAI1m0DDMml2QGw8Jo2/sjvEUX8R6u/TJAJtOga7sTMifYlZjlF4LmIH2AhYjbX0LNfh5b/STGheC68E3+ooeHvnwNWD3BDaVLslCfkIRViZd5+AjWW002mr1u8InG8WCxx+cUiVDPQoLHdpzPqFaYPjVzf3BRqU1oWXzZnnwE5gMDCrr1aW94Y2FSXZg24cZY61S0os4dQFEPYMOYitlQJpv9/xFdnI51BltmvtLusNJy7m4tJwbG/8ZlTfKcVldEjiYWlFLCB47pIXq1O2Cut8lx37Dn/C5w6oQHzUDVVBXSuLwuRMegmJ8c44Fir530CFJ/lhq1diiCL/wkubkRAECgYEA5+EB8R+/jMPCTbgSQ0I+h0LA6NLCcqzUWydnOs8tsNs4m8XupxPgnifywbo4e7dQvssRKnp9allV0Ic4zwgRP0CL4dM339UkNKSAHMqEqVeSS66JQou+x4n9ssHVwiLLY74koed/n0Qs3PxyXo7fkJc1dBL/stPJRW0NAwzACskCgYEApkTioqG9lEfCxH1/cG75f/Zup5xHMdbwS5OMSloioQOmXtrCKIsMYTk6p+W7VOlkziYDveaYFqfyx6qTNrkopUXAsLkC7AVSnbWSV3MTfWoJb803wpoievYyR6AboXDcv1MHS0NaTMEJhgS9SjVOGxBJcI5VsDCVgtFX4rB+BoECgYAvwf4eZYWUPnV0gGbyh2BNh4gn69C0wbfzFkiCqk0k0eH0421l6AgpSt0K3YnooTb2NOjSMOxHzSKJknA249h5iJfG6TPm3AVa+k6Qn313S1wMTmRi2lluy5L/EEEwfO4bGvx8hCua5/W9hhkgNJkDNMGDqK4VbN1VeUJm0Gd1UQKBgFSmgZQ3MGPL3TdYhYdimJgziMbA4+ZV/eUca7J3DWZYi12DlpI5qMXe8yQD7EogwXfLFuCaBBiUzSAmkeisgCsUxjOzHe+4DgXJKYZoSiXx1HgdTSRAgQmFSrf4pdids2WdCfuhgNnO9eBLjWu4k9lng1xQuwOG2LfPiSUaoi8BAoGBAKhQ1g5ixHXUu0Elo0JJuXytEul/D9HbPUvmAaaZ7tWy53coTVkt3mqVAIboZj0hztrR3SaPkbFvJaq/6XToL0e/pits32Ra9leP7ZPgwnKrF0/y1yPAJ25r0dKD6kuMC3bnUebIrn+zlp3ePHyvDaPfZ/fUI3nzhKNTYOqAHLJ21# 支付宝公钥,不是应用公钥 查看地址:https://openhome.com/platform/keyManage.htm 对应APPID下的支付宝公钥。publicKey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1nmOEkowjWQE+zVyNf/EuNZWwXtlewZceiEWyl9thHEpWvjQPKooDEJ9Dgsb+7NsH2uycF7ANvvfvo6ECKla7NyLS5nRbfpBKjVs0KywBg/X+gf+mDN7aAqjRY+MH3leFLCzBUnwf2PQb5lfHNYWXdPzHyKo5kfvPm3xWw0bQkYtNftunrws5TzYgv9FapghtOIqpr0Ly4A4BVKmX/Gh1yWQsIcUERLJdsk3pMf2In+pUVJcuuBqF2WoIZqAmPeokPBz8aA+5ZS+wSTMQlMhy84UzsIaYx6TE2FL9hcn/oUjZdPTw+smR2Qw2y1MgbLjvvxsLbub4/XGyPoQbGZgawIDAQAB# 服务器异步通知页面路径需http://格式的完整路径,不能加?id=123这类自定义参数notifyUrl=http://zlpmall.free.idcfengye.com/order/notifyPayResult# 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数returnUrl=http://zlpmall.free.idcfengye.com/order/goPaySuccPage# 签名方式signType=RSA2# 字符编码格式charset=utf-8# 支付宝网关gatewayUrl=https://openapi.alipaydev.com/gateway.do# 日志路径logPath="d:\\data\\"
支付表结构设计
SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for oms_order-- ----------------------------DROP TABLE IF EXISTS `oms_order`;CREATE TABLE `oms_order` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',`order_sn` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '订单编号',`member_id` bigint(20) NOT NULL,`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',`member_username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户帐号',`pay_amount` decimal(10, 2) NULL DEFAULT NULL COMMENT '应付金额(实际支付金额)',`pay_type` int(1) NULL DEFAULT NULL COMMENT '支付方式:0->未支付;1->支付宝;2->微信',`source_type` int(1) NULL DEFAULT NULL COMMENT '订单来源:0->PC订单;1->app订单',`status` int(1) NULL DEFAULT NULL COMMENT '订单状态:0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单',`delete_status` int(1) NOT NULL DEFAULT 0 COMMENT '删除状态:0->未删除;1->已删除',`payment_time` datetime(0) NULL DEFAULT NULL COMMENT '支付时间',`create_time` datetime(0) NULL DEFAULT NULL COMMENT '提交时间',PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 48 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic;-- ------------------------------ Table structure for oms_pay-- ----------------------------DROP TABLE IF EXISTS `oms_pay`;CREATE TABLE `oms_pay` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',`order_sn` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '订单编号',`trade_no` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '支付流水号',`pay_type` int(1) NULL DEFAULT NULL COMMENT '支付方式:0->未支付;1->支付宝;2->微信',`pay_amount` decimal(10, 2) NULL DEFAULT NULL COMMENT '支付金额',`pay_status` int(1) NULL DEFAULT NULL COMMENT '支付状态:0->待支付;1->支付中;2->支付成功',`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',`payment_time` datetime(0) NULL DEFAULT NULL COMMENT '支付时间',`delete_status` int(1) NOT NULL DEFAULT 0 COMMENT '删除状态:0->未删除;1->已删除',`create_time` datetime(0) NULL DEFAULT NULL COMMENT '提交时间',PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '支付流水表' ROW_FORMAT = Dynamic;-- ------------------------------ Table structure for oms_pay_refund-- ----------------------------DROP TABLE IF EXISTS `oms_pay_refund`;CREATE TABLE `oms_pay_refund` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',`order_sn` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '订单编号',`member_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户ID',`refund_json` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '退款响应数据json',`trade_no` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '支付流水号',`refund_type` int(1) NULL DEFAULT NULL COMMENT '支付方式:0->未支付;1->支付宝;2->微信',`refund_amount` decimal(10, 2) NULL DEFAULT NULL COMMENT '支付金额',`refund_status` int(1) NULL DEFAULT NULL COMMENT '支付状态:0->待支付;1->支付中;2->支付成功',`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',`refund_time` datetime(0) NULL DEFAULT NULL COMMENT '支付时间',`delete_status` int(1) NOT NULL DEFAULT 0 COMMENT '删除状态:0->未删除;1->已删除',`create_time` datetime(0) NULL DEFAULT NULL COMMENT '提交时间',PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '支付退款表' ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;
公共请求参数
参考博客 https://blog.csdn.net/Jay_1989/article/details/82384315
前端页面
支付页面
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>order pay</title><script type="text/javascript" src="/js/jquery-1.8.2.js"></script></head><body><form action="/order/pay" method="post"><div><span>金额:</span><input id="orderAmount" name="orderAmount"><br></div><div><span>支付方式:</span><label><input name="payType" type="radio" value="1" />支付宝</label><label><input name="payType" type="radio" value="2" />微信</label></div><input type="submit" value="去支付"></form></body></html>
回调成功页面
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title></head><body>充值成功!</body></html>
支付宝支付工具类
/*** 支付宝支付工具类* @className AliPayUtil* @date 2020/5/12 10:47*/@Component@Slf4j(topic = "AliPayUtil")public class AliPayUtil {@Resourceprivate AlipayProperties alipayProperties;private String serverUrl;private String appId;private String privateKey ;private String format = "json";private String charset;private String alipayPublicKey;private String signType;private String returnUrl;private String notifyUrl;/*** 支付接口** @param alipayBean 封装的支付宝入参* @return 返回支付结果* @throws AlipayApiException 抛出异常*/public String pay(AlipayBean alipayBean) throws AlipayApiException {// 1、获得初始化的AlipayClientinit();AlipayClient alipayClient = new DefaultAlipayClient(serverUrl, appId, privateKey, format, charset, alipayPublicKey, signType);// 2、设置请求参数AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();// 页面跳转同步通知页面路径 htmlalipayRequest.setReturnUrl(returnUrl);// 服务器异步通知页面路径接口alipayRequest.setNotifyUrl(notifyUrl);// 封装参数alipayRequest.setBizContent(JSON.toJSONString(alipayBean));// 3、请求支付宝进行付款,并获取支付页面结果String body = alipayClient.pageExecute(alipayRequest).getBody();return body;}/*** 支付接口** @param payRefundBean 封装的支付宝入参* @return 返回支付结果* @throws AlipayApiException 抛出异常*/public String payRefund(PayRefundBean payRefundBean) {// 1、获得初始化的AlipayClientinit();AlipayClient alipayClient = new DefaultAlipayClient(serverUrl, appId, privateKey, format, charset, alipayPublicKey, signType);// 2、设置请求参数AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest();// 封装参数alipayRequest.setBizContent(JSON.toJSONString(payRefundBean));String result = null;try {AlipayTradeRefundResponse execute = alipayClient.execute(alipayRequest);log.info(JSON.toJSONString(execute));result = execute.getBody();} catch (Exception ex) {log.error("支付宝退款错误!error={}", ex.getMessage());}return result;}/*** 初始化配置信息* @date: 2021/11/4 17:53*/private void init(){serverUrl = alipayProperties.getGatewayUrl();appId = alipayProperties.getAppId();privateKey = alipayProperties.getPrivateKey();charset = alipayProperties.getCharset();alipayPublicKey = alipayProperties.getPublicKey();signType = alipayProperties.getSignType();returnUrl = alipayProperties.getReturnUrl();notifyUrl = alipayProperties.getNotifyUrl();}}
支付接口
请求参数封装
OrderReq
@Datapublic class OrderReq implements Serializable {/*** 订单金额*/private BigDecimal orderAmount;/*** 支付方式:0->银联;1->支付宝;2->微信*/private Integer payType;/*** 主体信息*/private String subject;}
AlipayBean
@Datapublic class AlipayBean implements Serializable {/*** 支付宝交易订单号*/private String trade_no;/*** 商户订单号,必填*/private String out_trade_no;/*** 订单名称,必填*/private String subject;/*** 付款金额,必填* 根据支付宝接口协议,必须使用下划线*/private String total_amount;/*** 商品描述,可空*/private String body;/*** 超时时间参数*/private String timeout_express = "10m";/*** 产品编号*/private String product_code = "FAST_INSTANT_TRADE_PAY";}
支付生成 HTML 页面
/*** 跳转到订单下单页面*/@RequestMapping("/goPay")public String goPay() {return "pay";}/*** 调用支付宝返回支付页面** @param orderReq* @throws AlipayApiException*/@PostMapping("/pay")public void pay(OrderReq orderReq, HttpServletResponse httpResponse) throws Exception {String payResult = orderService.orderPay(orderReq);httpResponse.setContentType("text/html;charset=" + alipayProperties.getCharset());httpResponse.getWriter().write(payResult);httpResponse.getWriter().flush();httpResponse.getWriter().close();}
@SneakyThrows@Overridepublic String orderPay(OrderReq orderReq) {log.info("orderPay.req orderReq={}", JSON.toJSONString(orderReq));BigDecimal orderAmount = orderReq.getOrderAmount();// 1.产生订单Order order = new Order();order.setOrderSn(System.currentTimeMillis() + "");order.setMemberId(1L);order.setMemberUsername("给自己一个smile");order.setPayAmount(orderAmount);order.setPayType(orderReq.getPayType());order.setStatus(OrderEnum.ORDER_STATUS_NOT_PAY.getStatus());order.setCreateTime(DateUtil.date());order.setUpdateTime(DateUtil.date());this.save(order);// 2. 调用支付宝AlipayBean alipayBean = new AlipayBean();alipayBean.setOut_trade_no(order.getOrderSn());alipayBean.setSubject("充值:" + order.getPayAmount());alipayBean.setTotal_amount(orderAmount.toString());String pay = aliPayUtil.pay(alipayBean);log.info("orderPay.resp pay={}", pay);return pay;}
异步回调
/*** 支付成功的回调接口** @return*/@ResponseBody@PostMapping("/notifyPayResult")public String notifyPayResult(HttpServletRequest request) {return orderService.notifyPayResult(request);}/*** 支付成功的跳转页面*/@RequestMapping("/goPaySuccPage")public String goPaySuccPage() {return "paySuccess";}
@Overridepublic String notifyPayResult(HttpServletRequest request) {log.info("notifyPayResult ===> Payment callback");// 1.从支付宝回调的request域中取值放到map中Map<String, String[]> requestParams = request.getParameterMap();Map<String, String> params = getReqParam(requestParams);//2.封装必须参数// 商户订单号String outTradeNo = params.get("out_trade_no");String tradeNo = params.get("trade_no");//交易状态String tradeStatus = params.get("trade_status");log.info("notifyPayResult.resp outTradeNo:{},tradeStatus:{}", outTradeNo, tradeStatus);// 3.签名验证(对支付宝返回的数据验证,确定是支付宝返回的)boolean signVerified = false;try {// 3.1 调用SDK验证签名signVerified = AlipaySignature.rsaCheckV1(params, alipayProperties.getPublicKey(), alipayProperties.getCharset(), alipayProperties.getSignType());} catch (Exception e) {e.printStackTrace();log.info("notifyPayResult.resp Signature verification failed:{}", e.getMessage());}Order order = this.getOrderByOrderNo(outTradeNo);//4.对验签进行处理if (!signVerified) {savePay(tradeNo, order, PaySatusEnum.PAY_FAIL.getStatus());log.info("notifyPayResult.resp ====>Signature verification failed");return "failure";}//只处理支付成功的订单: 修改交易表状态,支付成功if (StrUtil.isNotBlank(tradeStatus) &&("TRADE_FINISHED".equals(tradeStatus) || "TRADE_SUCCESS".equals(tradeStatus))) {//根据订单号查找订单,防止多次回调的问题(幂等性)if (Objects.nonNull(order) && order.getStatus() == OrderEnum.ORDER_STATUS_NOT_PAY.getStatus()) {//修改订单状态order.setStatus(OrderEnum.ORDER_STATUS_PAID.getStatus());order.setPaymentTime(DateUtil.date());this.updateById(order);// 支付流水savePay(tradeNo, order, PaySatusEnum.PAY_SUCCESS.getStatus());}return "success";}savePay(tradeNo, order, PaySatusEnum.PAY_FAIL.getStatus());log.info("notifyPayResult.resp ====>payment failed");return "failure";}
接口调用
http://127.0.0.1:10087/order/goPay
支付页面
支付成功
支付成功回调页面

退款接口
请求参数封装
@Data@EqualsAndHashCode(callSuper = false)@ApiModel(value="退款请求参数对象")public class RefundReq implements Serializable {@ApiModelProperty(value = "订单编号")private String orderNo;@ApiModelProperty(value = "退款金额")private BigDecimal refundAmount;@ApiModelProperty(value = "退款的原因说明")private String refundReason;}
@Datapublic class PayRefundBean implements Serializable {/*** 支付宝交易订单号*/private String trade_no;/*** 商户订单号,必填*/private String out_trade_no;/*** 退款货币类型*/private String refund_currency;/*** 退款金额*/private String refund_amount;/*** 退款货币类型*/private String org_pid;/*** 可选 退款原因*/private String refund_reason;/*** 可选 标识一次退款请求,同一笔交易多次退款需要保证唯一,如需部分退款,则此参数必传*/private String out_request_no;/*** 可选 代表 商户的操作员编号*/private String operator_id;/*** 可选 代表 商户的门店编号*/private String store_id;/*** 可选 代表 商户的终端编号*/private String terminal_id;}
接口方法
/*** 支付退款*/@ResponseBody@GetMapping("/refund")public R refund(RefundReq refundReq) {String result = orderService.refund(refundReq);return R.success(result);}
@Overridepublic String refund(RefundReq refundReq) {log.info("refund.req refundReq={}", JSON.toJSONString(refundReq));PayRefundBean payRefund = new PayRefundBean();Pay pay = payService.getByOrderNo(refundReq.getOrderNo());payRefund.setTrade_no(pay.getPayNo());payRefund.setOut_trade_no(pay.getOrderSn());payRefund.setRefund_reason(refundReq.getRefundReason());payRefund.setRefund_currency("CNY"); //人民币payRefund.setRefund_amount(refundReq.getRefundAmount().toString());payRefund.setOut_request_no("HZ01RF001");String result = aliPayUtil.payRefund(payRefund);log.info("payReturn.resp result={}", result);return result;}
接口调用
{"alipay_trade_refund_response": {"code": "10000","msg": "Success","trade_no": "支付宝交易号","out_trade_no": "6823789339978248","buyer_logon_id": "159****5620","fund_change": "Y","refund_fee": 88.88,"refund_currency": "USD","gmt_refund_pay": "2014-11-27 15:45:57"},"sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"}
">
接口返回