(十八)在线教育网站搭建一支付实现

支付宝沙箱开发使用

1. Order表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
drop table if exists edu_order;

create table edu_order(
id char(19) not null comment '订单ID',
trade_no char(40) null comment '支付宝交易号',
course_id char(19) not null comment '课程ID',
ucenter_id char(19) not null comment '用户ID',
ucenter_username varchar(50) default null comment '用户名',
ucenter_mobile varchar(20) not null comment '用户手机号',
course_title varchar(50) not null comment '课程标题',
course_price decimal(10,2) unsigned not null default '0.00' comment '课程价格',
trade_status int(2) not null default 0 comment '交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)0、TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)1、TRADE_SUCCESS(交易支付成功)2、TRADE_FINISHED(交易结束,不可退款)3',
gmt_create datetime not null comment '创建时间',
gmt_modified datetime not null comment '更新时间',
primary key(id)
)

select * from edu_order;

2. https://open.alipay.com/platform/home.htm开发中心

支付宝扫码登录后,进入网页和应用开发,创建一个新应用

3. https://opendocs.alipay.com/open/270/105899开发文档

这里需要下载一个开发助手,用于生成公钥和私钥

支付宝采用公钥+内容匹配私钥的方式

生成密钥 https://opendocs.alipay.com/open/291/105971

复制公钥到指定位置后等待验审上线,但应用的签约功能需要营业执照。

选用支付宝沙箱开发

沙箱环境https://openhome.alipay.com/platform/appDaily.htm?tab=info

4. 沙箱环境

沙箱环境是开放平台提供给开发者调试接口的环境,模拟真实的支付环境

配置信息 https://opendocs.alipay.com/open/200/105311

5. 支付接入

配置说明 https://opendocs.alipay.com/open/270/105899

5.1. 支付时序图

5.2. 下单实现

5.2.1. controller

通过请求中有的logintoken取得用户头像,id,昵称信息,并根据课程信息生成订单

返回orderId是为了支付的时候接收订单号

1
2
3
4
5
6
7
8
9
10
@PostMapping("/create")
public R createOrder(HttpServletRequest request,
@RequestBody EduOrder eduOrder){
String token =request.getHeader("logintoken");
String orderId=eduOrderService.createOrder(token,eduOrder);
if(!StringUtils.isEmpty(orderId)) {
return R.ok().message("订单创建成功,及时付款").data("orderId",orderId);
}
return R.error().message("订单创建失败,请稍后再试");
}

5.2.2. service

将下单信息通过rabbit中的order-queue队列,实现分布式事务

memberClient 来自SpringCloud中的openfeign,调用用户表中的路由得到手机号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public String createOrder(String token, EduOrder eduOrder) {

Claims claims = JwtUtils.checkJWT(token);
String ucenterId = (String)claims.get("id");
String ucenterName = (String) claims.get("nickname");
if(StringUtils.isEmpty(ucenterId)){
return null;
}

R result = memberClient.getMember(ucenterId);
Map<String,Object> data = result.getData();
String mobile = (String)data.get("mobile");

//UcenterMember member = (UcenterMember) obj;

eduOrder.setUcenterId(ucenterId);
eduOrder.setUcenterUsername(ucenterName);
eduOrder.setUcenterMobile(mobile);

baseMapper.insert(eduOrder);

//发送订单信息
rabbitTemplate.convertAndSend("order-exchange","order-binding",eduOrder);

return eduOrder.getId();
}

5.3. Maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.79.ALL</version>
</dependency>

<!-- rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

5.4. 支付实现

5.4.1. 发起支付请求

5.4.1.1. application.properties中相关配置

这里的回调地址和返回地址都必须是公网域名,通过ngrok,花生壳等注册域名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
server.port=8008
spring.application.name=edu-alipay

#eureka
eureka.instance.prefer-ip-address=true
eureka.client.service-url.defaultZone=http://127.0.0.1:8010/eureka

#mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/online
spring.datasource.username=<<账号>>
spring.datasource.password=<<密码>>


alipay.config.APP_ID = <<沙箱中的app_id>>
alipay.config.APP_PRIVATE_KEY = <<私钥>>
alipay.config.GATEWAY_URL=https://openapi.alipaydev.com/gateway.do
alipay.config.SIGN_TYPE=RSA2
alipay.config.CHARSET=utf-8
alipay.config.FORMAT=json
alipay.config.ALIPAY_PUBLIC_KEY=
alipay.config.NOTIFY_URL=http://<<内网穿透注册的域名>>/alipay/edu-order/payback
alipay.config.RETURN_URL=http://<<内网穿透注册的域名>>/alipay/edu-order/return
alipay.config.SERVICE_PROVIDER_ID=<<沙箱中卖家app_id>>

spring.rabbitmq.host=192.168.0.112
spring.rabbitmq.port=5672
spring.rabbitmq.listener.direct.acknowledge-mode=manual

#全局时间
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#mapper配置指定
mybatis-plus.mapper-locations=classpath:com/online/edu/alipay/mapper/xml/*.xml

5.4.1.2. config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.online.edu.alipay.config;

import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "alipay.config")
public class AlipayConfig {

private String APP_ID;
private String APP_PRIVATE_KEY;
private String CHARSET;
private String ALIPAY_PUBLIC_KEY;
//这是沙箱接口路径,正式路径为https://openapi.alipay.com/gateway.do
private String GATEWAY_URL;
private String FORMAT;
//签名方式
private String SIGN_TYPE;
//支付宝异步通知路径,付款完毕后会异步调用本项目的方法,必须为公网地址
private String NOTIFY_URL;
//支付宝同步通知路径,也就是当付款完毕后跳转本项目的页面,可以不是公网地址
private String RETURN_URL;

private String SERVICE_PROVIDER_ID;

public AlipayClient getClient(){
AlipayClient alipayClient = new DefaultAlipayClient( this.getGATEWAY_URL() ,
this.getAPP_ID(), this.getAPP_PRIVATE_KEY(), this.getFORMAT(),
this.getCHARSET(), this.getALIPAY_PUBLIC_KEY(), this.getSIGN_TYPE());
return alipayClient;
}
}

5.4.1.3. controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.online.edu.alipay.controller;

import com.online.edu.alipay.config.AlipayConfig;
import com.online.edu.alipay.entity.Query.FileVo;
import com.online.edu.alipay.service.PayService;
import com.online.edu.common.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Controller
@CrossOrigin
@RequestMapping("/alipay/edu-order")
public class AlipayController {

@Autowired
PayService payService;

@Autowired
AlipayConfig alipayConfig;

@PostMapping("/pay")
public void pay(@RequestParam("orderId") String orderId,
HttpServletResponse response) throws IOException {

payService.createForm(orderId,response);
}

@PostMapping("/payback")
@ResponseBody
public R payBack(HttpServletRequest request, HttpServletResponse response){

boolean flag = payService.notify(request,response);

if (flag){
return R.ok().message("验签成功");
}else{
return R.error().message("验签失败");
}
}

@GetMapping("/return")
@ResponseBody
public void returnUrl(HttpServletRequest request,HttpServletResponse response) throws IOException {

String courseId = payService.backtToCourseId(request);
response.sendRedirect("http://localhost:3000/course/"+courseId);
}
}

5.4.1.4. service

5.4.1.4.1. 发起请求

这里的rabbit最终会将信息放到死信队列中,得到30分钟交易关闭后的交易订单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Override
public void createForm(String orderId, HttpServletResponse response) {

EduOrder eduOrder = eduOrderService.getById(orderId);
BigDecimal coursePrice = eduOrder.getCoursePrice();
String courseTitle=eduOrder.getCourseTitle();

//获得初始化的AlipayClient
AlipayClient alipayClient = alipayConfig.getClient();
//创建API对应的request
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
//在公共参数中设置回跳和通知地址
alipayRequest.setReturnUrl(alipayConfig.getRETURN_URL() );
alipayRequest.setNotifyUrl(alipayConfig.getNOTIFY_URL());


String SERVICE_PROVIDER_ID = alipayConfig.getSERVICE_PROVIDER_ID();

//out_trade_no 商户订单号
//product_code 销售产品码,与支付宝签约的产品码名称
//total_amount 订单总金额,单位为元,精确到小数点后两位
//subject 订单标题
//body 订单描述
//sys_service_provider_id 系统商编号
alipayRequest.setBizContent( "{" +
" \"out_trade_no\":\""+orderId+"\"," +
" \"product_code\":\"FAST_INSTANT_TRADE_PAY\"," +
" \"total_amount\":"+coursePrice+"," +
" \"subject\":\""+courseTitle+"\"," +
" \"body\":\""+courseTitle+"\"," +
" \"timeout_express\":\"30m\"," +
" \"extend_params\":{" +
" \"sys_service_provider_id\":\""+SERVICE_PROVIDER_ID+"\"" +
" }" +
" }" ); //填充业务参数
String form= "" ;
try {
form = alipayClient.pageExecute(alipayRequest).getBody(); //调用SDK生成表单


response.setContentType( "text/html;charset=" + alipayConfig.getCHARSET());
//直接将完整的表单html输出到页面
response.getWriter().write(form);
response.getWriter().flush();
response.getWriter().close();

//支付交易号创建后,无论怎样,30m后过期
rabbitTemplate.convertAndSend("user.order.delay.exchange","user.order.delay.queue",orderId);

} catch (AlipayApiException e) {
log.error("getAliPayOrderStr error :{}", e);
} catch (IOException e){
log.error("Alipay response error :{}", e);
}
}
5.4.1.4.2. 回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Override
public boolean notify(HttpServletRequest request, HttpServletResponse response) {

// 获取支付宝GET过来反馈信息
Map<String, String> paramsMap = new HashMap<>();
Map<String, String[]> param = request.getParameterMap();

//System.out.println("请求参数 "+param.toString());

Set<String> keys = param.keySet();

for(String key:keys){
String[] values = param.get(key);

String value = "";
for (int i = 0; i < values.length; i++) {
value = (i == values.length - 1) ? value + values[i] : value + values[i] + ",";
}
paramsMap.put(key,value);
}

//System.out.println("转换后参数 "+paramsMap);

//调用SDK验证签名
try {
boolean signVerified =AlipaySignature.rsaCheckV1(paramsMap, alipayConfig.getALIPAY_PUBLIC_KEY(),
alipayConfig.getCHARSET(), alipayConfig.getSIGN_TYPE());
if (signVerified){

log.info("订单创建时间戳:{} 商户订单号:{} 交易金额:{} 支付宝交易号:{} 收款支付宝账号:{}",
paramsMap.get("gmt_create"),
paramsMap.get("out_trade_no"),
paramsMap.get("total_amount"),
paramsMap.get("trade_no"),
paramsMap.get("seller_id"));

String orderId = paramsMap.get("out_trade_no");
String trade_no = paramsMap.get("trade_no");
String status = paramsMap.get("trade_status");

Integer resultStatus = new StatusUtils().getStatus(status);

//更新订单状态
EduOrder order = eduOrderService.getById(orderId);
order.setTradeNo(trade_no);
order.setTradeStatus(resultStatus);

eduOrderService.updateById(order);


return true;
}
} catch (AlipayApiException e) {
log.error("getAliPayOrderStr error :{}", e);
}


return false;
}

返回参数列表

参数定义

https://opendocs.alipay.com/apis/api_1/alipay.trade.page.pay

5.4.1.4.3. 成功返回到课程页
1
2
3
4
5
6
@Override
public String backtToCourseId(HttpServletRequest request) {
String orderId = request.getParameter("out_trade_no");
EduOrder order = eduOrderService.getById(orderId);
return order.getCourseId();
}

5.4.2. 支付过程

5.4.3. 账单下载

https://opendocs.alipay.com/apis/api_15/alipay.data.dataservice.bill.downloadurl.query

传入的是文件要保存的路径和要下载的时间日期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
import lombok.Data;

@Data
public class FileVo {

private String date;
private String path;
}
*/

@Override
public String getMonthInfo(FileVo file) {

//获得初始化的AlipayClient
AlipayClient alipayClient = alipayConfig.getClient();
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest(); //创建API对应的request类
AlipayDataDataserviceBillDownloadurlQueryModel model = new AlipayDataDataserviceBillDownloadurlQueryModel();

model.setBillDate(file.getDate());
model.setBillType("trade");

request.setBizModel(model);

AlipayDataDataserviceBillDownloadurlQueryResponse response = null;
try {
response = alipayClient.execute(request);

} catch (AlipayApiException e) {
log.error("Alipay response error :{}", e);
}
log.info("response body: {}",response.getBody());
//根据response中的结果继续业务逻辑处理
//将接口返回的对账单下载地址传入urlStr
//http://dwbillcenter.alipaydev.com/downloadBillFile.resource?bizType=trade&userId=20881021802170590156&fileType=csv.zip&bizDates=20200325&downloadFileName=20881021802170590156_20200325.csv.zip&fileId=%2Ftrade%2F20881021802170590156%2F20200325.csv.zip&timestamp=1585244116&token=4eeaed9a05fb4ea3c91c035fbc52d3bb
String urlStr = response.getBillDownloadUrl();

//指定希望保存的文件路径
String filePath = file.getPath();

//创建文件夹
File outFile = new File(filePath);
outFile.mkdirs();

//System.out.println(filePath);

URL url = null ;
HttpURLConnection httpUrlConnection = null ;
InputStream fis = null ;
FileOutputStream fos = null ;
try {
url = new URL(urlStr);
httpUrlConnection = (HttpURLConnection) url.openConnection();
httpUrlConnection.setConnectTimeout( 5 * 1000 );
httpUrlConnection.setDoInput( true );
httpUrlConnection.setDoOutput( true );
httpUrlConnection.setUseCaches( false );
httpUrlConnection.setRequestMethod( "GET" );
httpUrlConnection.setRequestProperty( "Charsert" , "UTF-8" );
httpUrlConnection.connect();
fis = httpUrlConnection.getInputStream();
byte [] temp = new byte [ 1024 ];
int b;
fos = new FileOutputStream(new File(outFile,file.getDate()+".csv.zip"));

while ((b = fis.read(temp)) != - 1 ) {
fos.write(temp, 0 , b);
fos.flush();
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
throw new EduException(20001,"文件路径不存在");
} finally {
try {
if (fis!= null ) fis.close();
if (fos!= null ) fos.close();
if (httpUrlConnection!= null ) httpUrlConnection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}

return filePath+"/"+file.getDate()+".csv.zip";
}

5.4.3.1. Error

1
{"alipay_data_dataservice_bill_downloadurl_query_response":{"code":"40004","msg":"Business Failed","sub_code":"isv.invalid_arguments","sub_msg":"入参不合法"},"sign":"PDmYgxlhULgz/pPFT/qCU6XafHNhwLqbGYQ+NhBeqlg34UTU6toOTNCuocfGJ/sJn3lO2bO0eqMVE3PGrwRWH+QkuoJ3hSKXylfroJCkus5FZhWi5ErczVnIWRXy49t/MIOwOaBXWYzZTJb86aNyBYDurXS+t7LVr6SA7yDDxNsdksHC3IDLWqcM76ox4fKlrtTOpljrsZAMnubBk0WCMju3NT2kD6h3cMirdXGI7WJX8Fw70o19cM9+qScQTFjobaXWD3o8NrFCJHd9drUt5NRBymmkHAyR5W4CXW3JaiksLGeUABXAXgApf48mdNWxFMCLi2BDhFIwCCLA+BYu7g=="}2019-10-06 14:06:00.611  INFO 1010 --- [freshExecutor-0] com.netflix.discovery.DiscoveryClient    : Disable delta property : false

时间不合适,只有得到今日之前的账单

5.4.4. 得到账单信息

传入 订单ID 和 支付交易号

https://opendocs.alipay.com/apis/api_1/alipay.trade.query

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public String getPayInfoById(String orderId) {

EduOrder order = eduOrderService.getById(orderId);

AlipayClient alipayClient = alipayConfig.getClient();
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(orderId);
model.setTradeNo(order.getTradeNo());

request.setBizModel(model);

AlipayTradeQueryResponse response = null;

try {
response=alipayClient.execute(request);
return response.getBody();
} catch (AlipayApiException e) {
log.error("alipay error: {}",e);
}

return null;
}

5.4.5. 超时后支付url

https://excashier.alipaydev.com/standard/auth.htm?payOrderId=107792034adb49d8958183b198f7c817.00

https://excashier.alipaydev.com/standard/timeOutPage.htm?payOrderId=1112dd699d4a468e9bf5fca83477178b.00

本文结束  感谢您的阅读