支付
支付业务流程
业务场景如下,用户确认订单之后,跳转到支付界面,点击支付宝扫码支付的图标,就可以进行扫码支付了
1. 用户点击扫码支付
请求url:http://localhost/pay/alipay/submit/{orderId}

2. 跳转到支付宝付款页面

3. 用户扫码付款

4. 浏览器跳转到支付成功页面

支付宝支付
支付宝简介
支付宝是国内的第三方支付平台,致力于提供简单、安全、快速的支付解决方案。自2014年第二季度开始成为当前全球最大的移动支付)厂商。
支付详细流程
1. 用户点击扫码支付图标,然后由浏览器发起支付请求,发给Gateway服务
2. Gateway收到请求之后把请求路由分发给 service-pay 服务
3. 支付服务接受请求,收到参数OrderId,然后调用订单服务的接口,查询这个订单是否是未支付的状态
4. 支付服务保存一条支付记录到数据库,并且记录状态为《未支付》
5. 支付服务向支付宝发起请求,获取支付页
6. 把支付页交给浏览器渲染,用户在浏览器中看到如下效果
7. 用户扫码或者是 用户输入账号密码确认支付
8. 同步回调:用户支付完成之后,浏览器发起请求,跳转到支付成功页面
9. 异步回调:用户支付完成之后,支付宝服务器发起请求,把支付结果通知给我们的服务,我们的服务收到请求之后,需要做修改订单状态,修改支付状态,扣减库存等操作
接入支付流程

-
找到对应的开发者平台(支付宝开放平台、微信开发平台、百度开放平台、讯飞开放平台、顺丰开放平台)
-
首先申请相关的账号
-
在那里申请呢? 支付宝开放平台/微信开放平台+微信商户平台
-
申请什么账号呢?申请商户账号→ 商户id,在对应的平台上创建应用 → 应用id
-
申请账号需要什么资料呢?
需要提交公司的营业执照,法人代表等身份信息,保证公司的合法性
-
-
绑定商户账号和应用ID,并配置接口访问的秘钥以及下载配置商户
-
确定需要接入支付的类别
-
刷脸支付
-
扫码支付
-
APP支付
-
手机网站支付
-
电脑网站支付
-
-
查看你要接入的支付类别相对应的接口文档
-
编写代码
沙箱使用说明
由于我们目前在开发的时候,没有商户账号,也没有营业执照等,所以暂时没有条件在支付宝上申请到对应的账号与AppId,不过支付宝为了协助我们开发者进行开发联调测试,推出了一个沙箱环境。
沙箱环境是协助开发者进行接口开发及主要功能联调的模拟环境,目前仅支持网页/移动应用和小程序两种应用类型。在沙箱完成接口调试后,直接修改必要的配置就可以接入正式环境。
通过沙箱环境,我们需要获取的参数:
- APPID
- 支付宝网关地址
- 应用公钥
- 应用私钥
- 支付宝公钥
当然,我们也可以使用沙箱平台在线调试我们的代码。
支付宝接口说明
我们是网站应用,所以接入电脑网站支付。
在本项目中,使用到的接口有:
支付功能测试
内网穿透
-
为什么需要内网穿透?
我们当前的支付服务,网关服务等也是运行在网络上,为什么支付宝访问不到我们的支付服务或者是网关呢?

image-20230327115055579 -
如何解决`问题?

image-20230327120739464 -
内网穿透工具
-
Natapp 下载地址
-

-

-

-
在配置页面我们可以看到authtoken,根据authToken启动natapp客户端,进入natapp所在的目录,打开命令行输入如下命令:
natapp -authtoken=你的authtoken
-
测试
创建springboot工程,版本为2.7.17
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--导入支付宝支付sdk--> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.35.71.ALL</version> </dependency> </dependencies>定义启动类
@SpringBootApplicationpublic class PayApplication {
public static void main(String[] args) { SpringApplication.run(PayApplication.class, args); }}定义配置类
@Configurationpublic class MyAlipayConfig {
@Bean public AlipayConfig getPublicConfig() { String privateKey = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwH5CIl0zwjtnxXSVAom43F1QQFOz30V3y/ZswtHKQzC12e1dYkxLqeq7HOVYhBK225MU1WHZWM0he8V4ISGY7nCz0zE179OS8EcNR152MMkYlp6ZjspC6TtkdY/3WDbo/lgC6GN9T1bqiQnpAYL+61DAF4v70iOXVo6vWy/2IBlmG0Nfyn67RswFgmurNa/m/3MhbH3Ncd6hUUB8GnGgjtvXXTHJPniYNZzDUCBdA1YF5wQhG5D9zEpPitM10kPVBd0eJD+uThKQ1ZyKE7Wnb1l8VXIyFEOFkGqylAtCtjhz37F/YMOSn+QLGEZsc77TYwNABneHt1nuFKle8AMhfAgMBAAECggEAEoIOpzv3GuR4JLQcIRGwsVtjOxln2ZcH32wlLdYYn/zE3kmR4T37Y+amjUsKMQgT1T9vNe7o6KAU/90ve4FYNPVxh/wcPGV80AKx2tzksoHp+zUF+D4glWOJz1vdpevlYZ86zlOkzGOObFS+EhvYqiJ4NXYoQrxMIspDWZwwNWYAsqbUl2DITYo+JkBZP6U1oeQCs09KFsoIs73tsLc7UE5JLZL/fjmigo8wJvfGrAXAQwlg0ie8oAzpjlS2hK5YhaRezw9OqgcRb/KgnPOWApAepJ86RYOQsvhiuuP9SLctYSnCOWWxP5f+zLU1Hf1l4RAlctdy255QWaC6lbDHAQKBgQDi3Xl3Ccnv8zQVkIpiNSChjiYtjDELcdqbrdCSbLdUxRvjxg6t1NPVRTI9LzjCSIRWny01pYv6Ujt2+nAW28bGiPcMPg2J+Q5G70H33g85uJ6yy4aPTyfDi0XuUwn0Xaetm/qWFR4Mrvgy2a1eKMWXMN8OpD+Ebk/CKbLsr0kv3wKBgQDGveCk0GY+NOYx791nea9mWlFdY54Zx3HWf/cTnwcDGwr51jTYBgeoiWX0WCC2s4i6awCEedkTz0QdRRze/NLC9gYRwagHFA9rUwENYFih4HPiFcz13DjN6I0pjLam1VQf37ZWJyRwXXf4J2trMBDYFz8l84IPnxQWn2m7LQF3gQKBgQCWYVzMrW5wYfQaf09bvf+9V26zLoSsI3JXU6Y4CVyVEntkRrsgOz2X12Bv8kdbcZpXmPfs4amh6rSEL4nxfQmMPOoV8WQkGzV9i8dcuJO7HUgFGKg/gqbHFiDq05x7oUEu8X/v0Fu06J6ZhnVHPxuLFtgk6nc4H686800pWx/WXQKBgQCY3Bx3x86MFBXl3M8XMnHlMJyaTu+gdlWpnN0GG2/CRL+Jb+dPLDwhtiRT7qCixa3pbDmGq016vhVuyeSt4hmdWKtMZv39C8HcU4hgqHUjdMbM4uW1SL/sJ+zDQ3aNFVHR/jh5RTvyrQGEPZWSaPLbse2hHA0yRLGnwM8K50/UgQKBgHgd0L619bDKRsBKRag4fquNn+oU1kgWX8jjpGcLcDP/Eqpc7eE7AFSkMMuRS9aL8f59gyC4uv1DyuYpx3pgiPKER82uEkeLRrLGxJ31EtmOtc9XshOEw6v1x0MqkELVwCuJmf/DiW8MYEZDN07rkrx7doxKFBV+FrZQ5oJPnoCW"; String alipayPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqnyvBrR4QWg0vY30uYToeDyZ7mq71nYVgkUKsBqaCzKoGbrGt75kWw7r+07ZHI8CqiYeQGd9krQ7TkFZ/DYF4p0k86wn21kqAILAVBW5mDkZF8xznDXX754NIcRXyDeUBuwGQ+FtH3GWtQVHAoTkv9XTW3RWonC/9z7DumDG3CajKZGUq64PnX0xttfEAIkGnGd7jgT8TQCVC6CH31XQMvOSDmkpCMNKYQn/XNpwaoV+SlXpJ8vV7a/9iThGBhXXRceilzOhK1bAB6MBFaMu67KWOV075cP3AwCHJmJyPVxzK54h/zn9wx0gEFGMAsuWaMTS6QI6VJCODKLwOdkpKQIDAQAB"; AlipayConfig alipayConfig = new AlipayConfig(); alipayConfig.setServerUrl("https://openapi.alipaydev.com/gateway.do"); alipayConfig.setAppId("2021000117621153"); alipayConfig.setPrivateKey(privateKey); alipayConfig.setFormat("json"); alipayConfig.setAlipayPublicKey(alipayPublicKey); alipayConfig.setCharset("UTF8"); alipayConfig.setSignType("RSA2"); return alipayConfig; }}定义Controller
@RestControllerpublic class payController {
@Autowired MyAlipayConfig myalipayConfig;
@GetMapping("/test/page/pay") public String testPagePay() throws AlipayApiException {
// 创建AlipayAlipayClient AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig); // 创建Request对象 AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); // 创建Model对象封装请求参数 AlipayTradePagePayModel model = new AlipayTradePagePayModel(); model.setOutTradeNo("cskaoyan005"); model.setTotalAmount("88.88"); model.setSubject("Iphone6 16G"); model.setProductCode("FAST_INSTANT_TRADE_PAY");
request.setNotifyUrl("https://732sw00878.zicp.fun/callback/notify"); // 给Model对象 request.setBizModel(model); // 发起请求,获取结果 AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if (response.isSuccess()) { System.out.println("调用成功"); } else { System.out.println("调用失败"); } // 输出响应体(支付表单页面) return response.getBody(); }
@GetMapping("/test/pay/query") public String testPayQuery() throws AlipayApiException { // 创建AlipayAlipayClient AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig); // 创建Request对象 AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); // 创建Model对象封装具体的请求参数 AlipayTradeQueryModel model = new AlipayTradeQueryModel(); model.setOutTradeNo("cskaoyan005"); // 给Request设置具体的请求 request.setBizModel(model); // 发起请求并获取响应 AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) { System.out.println("调用成功"); } else { System.out.println("调用失败"); }
// 返回响应中查询到的订单支付状态 return response.getTradeStatus(); }
@GetMapping("return/notify") public String returnCallback(@RequestParam Map<String, String> paramsMap) { System.out.println(paramsMap); return "return notify success"; }
@GetMapping("/test/pay/close") public String testClosed() throws AlipayApiException { // 创建AlipayAlipayClient AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig); // 创建Request对象 AlipayTradeCloseRequest request = new AlipayTradeCloseRequest(); // 创建Model对象封装具体的请求参数 AlipayTradeCloseModel model = new AlipayTradeCloseModel(); model.setOutTradeNo("cskaoyan005"); // 给Request设置具体的请求 request.setBizModel(model); // 发起请求并获取响应 AlipayTradeCloseResponse response = alipayClient.execute(request);
if (response.isSuccess()) { System.out.println("调用成功"); } else { System.out.println("调用失败"); } // 输出响应体 return response.getBody(); }
@Autowired RedissonClient redissonClient;
@PostMapping("/callback/notify") public String notifyCallback(@RequestParam Map<String, String> paramsMap) throws AlipayApiException { @Autowired RedissonClient redissonClient;
@PostMapping("/callback/notify") public String notifyCallback(@RequestParam Map<String, String> paramsMap) throws AlipayApiException {
// 1. 调用SDK验证签名 boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, alipayConfig.getAlipayPublicKey(), alipayConfig.getCharset(), alipayConfig.getSignType());
// 2. 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验 if (signVerified) {
String outTradeNo = paramsMap.get("out_trade_no"); // 外部订单号 String totalAmount = paramsMap.get("total_amount"); // 交易总金额 String appId = paramsMap.get("app_id"); // appId String tradeStatus = paramsMap.get("trade_status"); // 交易状态
// 3. 二次参数校验,校验金额,校验appid // 根据订单id去数据库获取订单信息,以及其中的订单金额 BigDecimal dbTotalAmount = new BigDecimal("88.88"); int result = new BigDecimal(totalAmount).compareTo(dbTotalAmount); if (!appId.equals(alipayConfig.getAppId()) || result != 0) { return "failure"; } // 4. 交易状态校验 if (tradeStatus != null && (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED"))) {
// 5. 幂等性校验 String notifyId = paramsMap.get("notify_id"); String key = "pay:callback:notifyid" + notifyId; RBucket<String> bucket = redissonClient.getBucket(key); boolean ret = bucket.trySet(notifyId, 25, TimeUnit.HOURS); if (!ret) { System.out.println("failure"); return "failure"; }
// 执行业务逻辑 System.out.println("success"); return "success"; } else { System.out.println("failure"); return "failure"; } } else { System.out.println("failure"); return "failure"; } }
}表结构设计
CREATE TABLE `payment_info` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', `out_trade_no` varchar(50) DEFAULT NULL COMMENT '对外业务编号,创建订单的时候生成,从订单中获取', `order_id` varchar(50) DEFAULT NULL COMMENT '订单编号', `user_id` bigint(20) DEFAULT NULL COMMENT '用户id', `payment_type` varchar(20) DEFAULT NULL COMMENT '支付类型(微信 支付宝)', `trade_no` varchar(50) DEFAULT NULL COMMENT '交易编号,回调时生成', `total_amount` decimal(10,2) DEFAULT NULL COMMENT '支付金额', `subject` varchar(200) DEFAULT NULL COMMENT '交易内容,利用商品名称拼接。', `payment_status` varchar(20) DEFAULT NULL COMMENT '支付状态', `callback_time` datetime DEFAULT NULL COMMENT '回调时间', `callback_content` text COMMENT '回调信息', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `is_deleted` tinyint(3) NOT NULL DEFAULT '0', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8 COMMENT='支付信息表';接口
去支付

@RequestMapping("/pay/alipay/submit/{orderId}")@ResponseBodypublic String submitOrder(@PathVariable Long orderId){
String form = alipayService.createAliPay(orderId); // 在上面这个支付的方法中,需要做的事情有如下几个
// 1. 校验支付对应的订单状态是否为未支付,如果是已支付或已关闭,则直接返回
// 2. 保存支付记录到支付表
// 3. 调用支付宝SDK,生成支付表单(支付表单实际上就是一个支付页面,是一个html的字符串)
// 4. 返回支付表单 return form;}支付工厂
在第三步调用支付宝SDK的时候,我们可以用一个接口来定义对于支付服务提供商的访问行为如下:
public interface PayHelper {
/* 预下单方法,获取交易二维码字符串,或者交易表单字符串 */ String prePay(OrderInfoDTO orderInfo);
/* 根据订单号查询订单交易状态 */ String queryTradeStatus(String outTradeNo);
/* 根据订单编号关闭订单交易 */ void closeTrade(String outTradeNo);
}这样做的好处在于:
- 我们只需要访问PayHelper接口中定义的方法即可访问支付服务提供商的功能,代码书写简洁
- 因为调用的是接口方法,所以我们可以有不同的接口实现类,从而可以实现,在几乎不修改代码的情况下,访问不同支付服务提供商的支付功能
比如,如果我们使用支付宝支付时,我们就可以实现针对支付宝的实现类如下,同理如果我们如果还要访问微信支付,我们还可以定义微信支付的实现类。
@Componentpublic class AlipayHelper implements PayHelper {
@Autowired CsmallAlipayConfig csmallAlipayConfig;
@Autowired AlipayClient alipayClient;
/** * 向支付宝发起请求,生成支付页面(表单) */ @Override public String prePay(OrderInfoDTO orderInfo) { // 构建请求对象 AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
// 设置同步回调(不需要公网可访问) request.setReturnUrl(csmallAlipayConfig.getReturnPaymentUrl());
// 设置异步回调(公网可访问) request.setNotifyUrl(csmallAlipayConfig.getNotifyPaymentUrl());
// 构建参数 AlipayTradePagePayModel model = new AlipayTradePagePayModel(); model.setOutTradeNo(orderInfo.getOutTradeNo()); model.setTotalAmount(orderInfo.getTotalAmount().toString()); model.setSubject(orderInfo.getTradeBody()); model.setProductCode("FAST_INSTANT_TRADE_PAY"); // ...此处省略了一些代码(这些代码在4.1.2中解释) request.setBizModel(model);
// 向支付宝发起请求 AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
return response.getBody(); }
/** * 根据外部订单号 查询支付宝支付状态 */ @Override public String queryTradeStatus(String outTradeNo) { String tradeStatus = "ACQ.SYSTEM_ERROR"; try { AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(outTradeNo); request.setBizModel(model);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if ("ACQ.TRADE_NOT_EXIST".equals(response.getSubCode())) { return "ACQ.TRADE_NOT_EXIST"; }
if (response.isSuccess()) { tradeStatus = response.getTradeStatus(); } } catch (AlipayApiException e) { e.printStackTrace(); } return tradeStatus; }
/** * 关闭支付宝支付记录 */ public void closeTrade(String outTradeNo) {
AlipayTradeCloseModel model = new AlipayTradeCloseModel(); model.setOutTradeNo(outTradeNo);
AlipayTradeCloseRequest request = new AlipayTradeCloseRequest(); request.setBizModel(model);
try { alipayClient.execute(request); } catch (AlipayApiException e) { e.printStackTrace(); } }}如果在我们的项目中要接入不同的支付方式,很显然就需要用不同的PayHelper接口实现类对象,如何方便的获取这些PayHelper对象呢?我们可以使用前面学习过的简单工厂来实现:
public interface PayHelperFactory { // 获取PayHelper对象 PayHelper getPayHelper(PaymentType paymentType);}@Getterpublic enum PaymentType {
ALIPAY("支付宝"), WEIXIN("微信" );
private String comment ;
PaymentType(String comment ){ this.comment=comment; }}@Componentpublic class SimplePayHelperFactory implements PayHelperFactory{
@Autowired AlipayHelper alipayHelper; @Override public PayHelper getPayHelper(PaymentType paymentType) { if (PaymentType.ALIPAY.equals(paymentType)) { return alipayHelper; } return null; }}超时时间
这里有一点需要注意,在上面的第3步生成表单的时候,考虑到订单超市取消功能的实现,我们需要设置支付表单的过期时间,用当前时间 - 订单创建时间:
- 如果当前时间 - 订单创建时间 > 订单超时时间,则直接返回,不生成表单
- 否则,指定绝对时间 = 当前时间 + 剩余的订单超时时间(订单超时时间 - (当前时间 - 订单创建时间))
// 获取当前时间 long now = new Date().getTime(); // 距离下单已经过去的时间 long orderTimeSpan = now - createTime.getTime(); long timeOutSpan = 30; if (orderTimeSpan >= timeOutSpan * 60 * 1000) { // 超过了订单的超时时间 return "对不起,已经超过支付时间,请重新下单"; } String timeoutStr; // 四舍五入求超时时间 // 求以毫秒为单位的超时时间 long timeoutRemain = timeOutSpan * 60 * 1000 - orderTimeSpan; // 生成过期时间对应的日期格式字符串 timeoutStr = DateFormatUtils.format(new Date(now + timeoutRemain), "yyyy-MM-dd HH:mm:ss");
// 设置相对超时时间 model.setTimeExpire(timeoutStr);支付完成
需要说明的是,同步回调和异步回调都会携带参数,在当前项目中,我们使用同步回调来跳转到支付成功页面;使用异步回调来修改订单状态等。这也是支付宝推荐的使用方式,新版本的支付接口已经取消了同步回调的支付结果的传递。

同步回调
同步回调的作用是跳转到支付成功页面。

pay-service#PayApiController
/** * 支付成功:同步回调 */@RequestMapping("/pay/alipay/alipay/callback/return")public String callBack() { log.info("支付成功,同步回调! "); // 同步回调给用户展示信息 return "redirect:" + "http://localhost:8000/pay/success";}异步回调
异步回调是指在请求参数中传入 notify_url 参数,在用户支付成功后,支付宝服务器会按照这个异步地址使用 post 方式给 notify_url 来发送交易信息,参数示例。需要注意的是:notify_url 地址由商户自己定义保证可以正常使用外网 post 方式访问,否则是无法正常接收到异步通知的。

pay-service#PayApiController
/** * 支付成功:异步回调 */@PostMapping("/pay/alipay/callback/notify")@ResponseBody@SneakyThrowspublic String callbackNotify(@RequestParam Map<String, String> paramsMap){ log.info("支付成功,异步回调,paramMap:{}", JSON.toJSONString(paramsMap));
// 验证参数 // 验签 // 校验金额 // 校验appid // 校验交易状态 // 幂等性校验
// 1. 调用SDK验证签名 boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, alipayConfig.getAlipayPublicKey(), CsmallAlipayConfig.charset, CsmallAlipayConfig.sign_type);
// 2. 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验 if (signVerified){
String outTradeNo = paramsMap.get("out_trade_no"); // 外部订单号 String totalAmount = paramsMap.get("total_amount"); // 交易总金额 String appId = paramsMap.get("app_id"); // appId String tradeStatus = paramsMap.get("trade_status"); // 交易状态
PaymentInfoDTO paymentInfo = payService.queryPaymentInfoByOutTradeNoAndPaymentType(outTradeNo, PaymentType.ALIPAY.name());
// 3. 二次参数校验,校验金额,校验appid if (paymentInfo == null) return "failure"; BigDecimal dbTotalAmount = paymentInfo.getTotalAmount(); double m = Double.valueOf(totalAmount) - dbTotalAmount.doubleValue(); if (!appId.equals(alipayConfig.getAppId()) || m != 0) { return "failure"; } // 4. 交易状态校验 if (tradeStatus != null && (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED"))) {
// 5. 幂等性校验 String notifyId = paramsMap.get("notify_id"); String key = RedisConst.PAY_CALL_BACK_VERFY_PREFIX + notifyId; RBucket<String> bucket = redissonClient.getBucket(key); // setnx boolean ret = bucket.trySet(notifyId,RedisConst.PAY_CALL_BACK_EXPIRE_TIME, TimeUnit.SECONDS); if (!ret) { // 幂等标记存在,说明处理成功,重复的通知,返回success return "success"; }
// 6. 修改支付表状态为支付成功 //修改失败的话,需要删除幂等标记 } else { // 通知失败,返回failure return "failure" } }
// 回调成功,返回success return "success";}文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!