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

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

3. 用户扫码付款

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

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

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

如何解决内网穿透问题?

内网穿透工具
Natapp 下载地址



在配置页面我们可以看到authtoken,根据authToken启动natapp客户端,进入natapp所在的目录,打开命令行输入如下命令:
propertienatapp -authtoken=你的authtoken
2.6.2 测试 ​
创建springboot工程,版本为2.3.0.RELEASE
<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>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.1</version>
</dependency>
</dependencies>定义启动类
@SpringBootApplication
public class PayApplication {
public static void main(String[] args) {
SpringApplication.run(PayApplication.class, args);
}
}定义配置类
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
// 1. new 出一个config对象
Config config = new Config();
// 2. 配置Config对象
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress("redis://192.168.153.130:6379");
// 设置序列化
config.setCodec(new JsonJacksonCodec());
// 3. 创建redis客户端 该对象在创建的时候,就会像redis-server发起连接请求
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
@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
@RestController
public class payController {
@Autowired
AlipayConfig alipayConfig;
@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";
}
}
}3. 表结构设计 ​
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 '支付类型(微信 支付宝)',
`total_amount` decimal(10,2) DEFAULT NULL COMMENT '支付金额',
`subject` varchar(200) DEFAULT NULL COMMENT '交易内容,利用商品名称拼接',
`payment_status` varchar(20) DEFAULT NULL COMMENT '支付状态',
`trade_no` varchar(50) 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='支付信息表';4. 接口 ​
4.1 去支付 ​

@RequestMapping("/pay/alipay/submit/{orderId}")
@ResponseBody
public String submitOrder(@PathVariable Long orderId){
String form = alipayService.createAliPay(orderId);
// 在上面这个支付的方法中,需要做的事情有如下几个
// 1. 校验支付对应的订单状态是否为未支付,如果是已支付或已关闭,则直接返回
// 2. 保存支付记录到支付表
// 3. 调用支付宝SDK,生成支付表单(支付表单实际上就是一个支付页面,是一个html的字符串)
// 4. 返回支付表单
return form;
}这里有一点需要注意,在上面的第3步生成表单的时候,考虑到订单超时取消功能的实现,我们需要设置支付表单的过期时间,需要和订单自动超时时间同步 或者是 比订单自动超时取消时间 稍晚一点(比如晚1s)即可。
4.2 支付完成 ​
需要说明的是,同步回调和异步回调都会携带参数,在当前项目中,我们使用同步回调来跳转到支付成功页面;使用异步回调来修改订单状态等。这也是支付宝推荐的使用方式,新版本的支付接口已经取消了同步回调的支付结果的传递。

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

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
@SneakyThrows
public 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";
}