支付能力、支付扩展、资金能力、口碑能力、营销能力、会员能力、行业能力等等
登录支付宝开放平台即可:https://opendocs.alipay.com/home
本次项目以电脑网站支付产品为例
主要了解包括应用场景、准入条件、计费模式等内容。
使用支付宝帐号登录之后,进行开放平台帐号注册,选择入驻,期间会使用手机号接收验证码。
注册完成之后,就成为了一名开发者,可以自行创建应用了。
本次应用实战主要基于沙箱环境下进行的,不需要涉及诸如营业执照、网站备案等信息的提交与审核,对于新手学习支付十分友好。
沙箱环境的配置
获取支付宝网关地址
设置接口内容加密方式(该内容主要用于应用接口的加密,自动生成)
下载沙箱版支付宝并选择登录
spring-boot-starter-web
spring-boot-devtools
lombok
spring-boot-starter-test
spring-tx
springfox-swagger2
springfox-swagger-ui
mysql-connector-java
mybatis-plus
mybatis-plus-boot-starter
druid
spring-boot-configuration-processor
gson
alipay-sdk-java
对应的pom文件依赖为:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<version>2.3.12.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-txartifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plusartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
<dependency>
<groupId>com.google.code.gsongroupId>
<artifactId>gsonartifactId>
dependency>
<dependency>
<groupId>com.alipay.sdkgroupId>
<artifactId>alipay-sdk-javaartifactId>
<version>4.22.57.ALLversion>
dependency>
payment_demo
四张表内容如下所示:
CREATE TABLE `t_order_info` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单id',
`title` varchar(256) DEFAULT NULL COMMENT '订单标题',
`order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
`product_id` bigint(20) DEFAULT NULL COMMENT '支付产品id',
`total_fee` int(11) DEFAULT NULL COMMENT '订单金额(分)',
`code_url` varchar(50) DEFAULT NULL COMMENT '订单二维码连接',
`order_status` varchar(10) DEFAULT NULL COMMENT '订单状态',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
/*Table structure for table `t_payment_info` */
CREATE TABLE `t_payment_info` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付记录id',
`order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
`transaction_id` varchar(50) DEFAULT NULL COMMENT '支付系统交易编号',
`payment_type` varchar(20) DEFAULT NULL COMMENT '支付类型',
`trade_type` varchar(20) DEFAULT NULL COMMENT '交易类型',
`trade_state` varchar(50) DEFAULT NULL COMMENT '交易状态',
`payer_total` int(11) DEFAULT NULL COMMENT '支付金额(分)',
`content` text COMMENT '通知参数',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
/*Table structure for table `t_product` */
CREATE TABLE `t_product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id',
`title` varchar(20) DEFAULT NULL COMMENT '商品名称',
`price` int(11) DEFAULT NULL COMMENT '价格(分)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
/*Data for the table `t_product` */
insert into `t_product`(`title`,`price`) values ('Java课程',1);
insert into `t_product`(`title`,`price`) values ('大数据课程',1);
insert into `t_product`(`title`,`price`) values ('前端课程',1);
insert into `t_product`(`title`,`price`) values ('UI课程',1);
/*Table structure for table `t_refund_info` */
CREATE TABLE `t_refund_info` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '退款单id',
`order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
`refund_no` varchar(50) DEFAULT NULL COMMENT '商户退款单编号',
`refund_id` varchar(50) DEFAULT NULL COMMENT '支付系统退款单号',
`total_fee` int(11) DEFAULT NULL COMMENT '原订单金额(分)',
`refund` int(11) DEFAULT NULL COMMENT '退款金额(分)',
`reason` varchar(50) DEFAULT NULL COMMENT '退款原因',
`refund_status` varchar(10) DEFAULT NULL COMMENT '退款状态',
`content_return` text COMMENT '申请退款返回参数',
`content_notify` text COMMENT '退款结果通知参数',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
注意:需要在产品信息表中添加四条信息
# 支付宝支付相关参数
# 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
alipay.app-id=2021000120603279
# 商户PID,卖家支付宝账号ID
alipay.seller-id=2088621959241092
# 支付宝网关
alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
# 商户私钥,您的PKCS8格式RSA2私钥(应用私钥)
alipay.merchant-private-key=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBvqvExLVwdcXKE9IaYI1oI5a57SMAZrwlCXw40g3+04PmNiIxfkKJVDzhqEm2OmlO5Wl45q2jwvm5UdqgKtwHIyFWt2hPJ/QRSGFFO/4NiUWkMVs5Q74jvAePapy434lxjhhtuZdHUjalNqkb21SJh22XQJl8hFf5mACHDl4hEw/YUC9DM94jZ+FsBctYLN1usQlIAUW2OVWWBeAJIWWjtk2fNjOQaZHWH+Y5dOd4x7OiHXvxuNpVFgxPcM6IsgarkNWMzZb7p7j9ymcw48d0JjOIhnW8qkrU/bskp6VCXjw0x2azvd/HYcfpSBjeFHUKNX5CMUpks1/k9CJWpeJTAgMBAAECggEAeJC99Xnv6ubvSZxh/9YbyTV0Y4lFYceMx4OKkRVubiiUCRug1anbn/gS1t5R2Juq0tUCeKEcZy87Fe7xHQDu4WYkJgGGYNPdFzAyj9IQe73z34RzX0Rfu38UOVQ/6O/6aPbjDs0SbeikZtWIEPTBO8BSG3Cw0wLMeF713RW8z9kkQZOPaiixZVPLoFTIL4KuCZhYdJK2RRchYuZnEYHRRFAqFoKN1jII5pR8EyxmvocFx7UJ7idRGrSWc1UB5xEyn2emYyiTu3uaVaa49ecBNZqvRRdAoHcVQOGIYiUSNlrqDYVOLOicdSOlO6bS6jmRk41pdgdze089uYT/ilh0iQKBgQD+RIS3dGUvVk5AysqzoA0v7zYEixMeqALrAxYKAP02bIHGISw39+O4Q2GbzKUqDt6dmGPBlQ9jW/o9h0zqLf+7aFiEEIystbD6gx3TeWAVDyoF9zNfFOCapKaTbDZ2BGj3P/CpFLm7DNUyk9f03/BykqskXJ+4ZeeYUtqg+iofhwKBgQDDEJdrCrwBQEcjDIEcWZQaN09d98VgmWLv3L0EQ230YrcsLL1rJFJOzQr3zSf1ibY9S/zrrIFlb0Vq9qUBNbLN7GY8nXjrmoordgdYwHwTYoGa+R0Rv5yB24tKkra4ZQBCD1au1+0Klgc3mhJgKJ8HrRwH1UexDLukEABthfzh1QKBgCtSRUJ0hGDiVYbYhlzAYj7OhOeVQnawrX6ZEgI2VO4W4q19LWmDxLq6UEEZRvK5gdhcBHMREIQfQa2GBebIW4/0oVAu+ajbdAHaoRRM08ACy2gkzA3hIrt2XiM0Brto2PF3ZWuJanOiJhjt85d3KCJ9NseFOHlUc3cSdsmClfa1AoGAT1F60Mr/od6aTpUyFu4R/AsLmeE7gEk+4tw2e/pTRrGxXCQhLeUKFwLnd9YTbpN96DTy9n4h67YwWwtKE1DbkUKUXAeIeP1RO9T1rdAvY86FdxffCy2IHYHBhSRdamOflD0aeWRR/iD9dE2RNUqvR/bLVCAU09iioFblZaO7LbUCgYEA43FByiYEuVCiGEJD7eRSwRsN2lCJS3ZxoE60iKIeTxs5uWQIBE0bTkqvb35JUBdeuAlB0H0GPZaEO9yL3tPL0i+PbMmpBPaZ6tnOk6Pc9sREQw2PIlrpscWHU2gL2BodKCDVOe2rW3Der39MrFMhW6yoTjCFBRW6qGaM5llVQEc=
# 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥
alipay.alipay-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApX9WFCjD08ErIlv1WvSF5hgM6kt9D+UJYYIDoMHpMw886DlFUiqPLYb+ZTy5eEE7N7TBS4Wl3NaUvsY2Z3dlwSk3HBpogsskgScm+qmdIEm/hEXL7xVB7WG7GD/M/ko8uihwQmH3WjOe9NU8HWUT4N4B6vwU6KrR6IHAmoPQ86zqWuQbUPrKZMZczhnF4uUcp+7DzpSWkz91U/TKdW18lFB7md8cwHEvKiQe23OEJMNS4utwDhaWIYhATxrxaEW5Yfj2VPt9NnaBbYYC2FUtHL4NLnJCF6uTgUuXzPauedeushS3WF0+mDrV8oRTKnBDtg6lF3JTrFoiocDJ076YlwIDAQAB
# 接口内容加密秘钥,对称秘钥
alipay.content-key=D8entyfafkkFwtMbUqj3Mw==
# 页面跳转同步通知页面路径
alipay.return-url=http://localhost:8080/#/success
# 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
alipay.notify-url=https://2b4a-202-192-72-1.jp.ngrok.io/api/ali-pay/trade/notify
以上沙箱支付配置文件信息均来自于支付宝开放平台官网为个人生成的相关信息。(主要涉及APPID、PID,应用私钥、支付宝公钥)
同时将该文件加入到项目中的resources文件夹下,同时选择project structure
,选择相应的配置(如下图所示)。需要选择该properties文件,设置到项目中即可。
server:
port: 8090
spring:
application:
name: payment-demo
thymeleaf:
cache: false
jackson:
date-format: yyyy:MM:dd HH:mm:ss # 定义json的时间格式
time-zone: GMT+8
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/payment_demo?CharacterEncoding=utf87serverTimeZone=GMT%2B8&useUnicode=true
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #sql日志
mapper-locations: classpath:com/example/mapper/xml/*.xml #配置xml文件的地址
logging:
level:
root: info
payment-demo
该类主要用于接口测试的相关配置
@Configuration
@EnableSwagger2
public class Swagger2Config {
ApiInfoBuilder apiInfoBuilder=new ApiInfoBuilder();
@Bean
public Docket getDocket(){
return new Docket( DocumentationType.SWAGGER_2)
.apiInfo(apiInfoBuilder.title("支付宝支付案例").description("payment -demo").build());
}
}
@EnableSwagger2
注解,开启swagger2的服务@Bean
表示将该方法的返回值对象注入到IOC容器中配置德鲁伊的数据源(SpringBoot的项目默认配置的不是德鲁伊数据源)
@Configuration
public class DataSourceConfig {
/**
* @ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中
* 前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中
* @return
*
*/
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource getDataSource(){
return new DruidDataSource();
}
}
使用@ConfigurationProperties(prefix = "spring.datasource")
实现配置类与配置文件的映射关系(完成属性值的自动注入)
@Configuration //定义为配置类
@MapperScan("com.example.mapper") //扫描mapper接口
@EnableTransactionManagement //启用事务管理(spring-tx)
public class MyBatisPlusConfig {
}
@Configuration
@PropertySource("classpath:alipay-sandbox.properties")
public class AliPayClientConfig {
/**
* 利用Environment对象获取配置文件alipay-sandbox.properties文件中的所有内容
*/
@Resource
private Environment config;
/**
* 创建一个获取AlipayClient对象的方法,用于封装签名的自动实现
* @return AlipayClient
*/
@Bean
public AlipayClient getAlipayClient() throws AlipayApiException {
//创建alipay配置对象,并设置相应的参数
AlipayConfig alipayConfig = new AlipayConfig();
//设置网关地址
alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
//设置应用Id
alipayConfig.setAppId(config.getProperty("alipay.app-id"));
//设置应用私钥
alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
//设置请求格式,固定值json
alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
//设置字符集
alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
//设置支付宝公钥
alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
//设置签名类型
alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
//构造client
AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
return alipayClient;
}
}
使用@PropertySource("classpath:alipay-sandbox.properties")
定位到配置文件的位置
利用@Resource
注解注入Evironment对象,用于读取指定位置的配置文件信息的相关内容,通过getProperties(xxx)
获取指定项的配置信息。
利用公有方法生成自定义的bean对象,注入到IOC容器中
以上返回的AlipayClient
对象会自动为请求生成签名,并对响应完成验签操作
BaseEntity
@Data
public class BaseEntity {
/**
* 主键
*/
@TableId(value = "id",type = IdType.AUTO)
private String id;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
@TableId(value = "id",type = IdType.AUTO)
注解为该属性设置dao层映射,value值与数据库中的id对应,并且设置主键的自增策略。OrderInfo
@Data
@TableName("t_order_info") //表示指定表名
public class OrderInfo extends BaseEntity{
/**
* 订单标题
*/
private String title;
/**
* 订单编号
*/
private String orderNo;
/**
* 用户ID
*/
private Long userId;
/**
* 产品ID
*/
private Long productId;
/**
* 订单金额
*/
private Integer totalFee;
/**
* 订单二维码链接
*/
private String codeUrl;
/**
* 订单状态
*/
private String orderStatus;
}
@TableName("t_order_info")
表示映射到数据库的具体名称的表中paymentInfo
@Data
@TableName("t_payment_info")
public class PaymentInfo extends BaseEntity{
/**
* 订单编号
*/
private String orderNo;
/**
* 交易系统支付编号
*/
private String transactionId;
/**
* 支付类型
*/
private String paymentType;
/**
* 交易类型
*/
private String tradeType;
/**
* 交易状态
*/
private String tradeState;
/**
* 支付金额
*/
private Integer payerTotal;
/**
* 通知参数
*/
private String content;
}
@TableName("t_payment_info")
表示将该对象映射到数据库的具体的表中BaseEntity
Product
@Data
@TableName("t_product")
public class Product extends BaseEntity{
/**
* 产品名称
*/
private String title;
/**
* 产品价格
*/
private Integer price;
}
@TableName("t_product")
表示映射到数据库的具体的表中BaseEntity
RefundInfo
@Data
@TableName("t_refund_info")
public class RefundInfo extends BaseEntity {
/**
* 商品订单编号
*/
private String orderNo;
/**
* 商品退款编号
*/
private String refundNo;
/**
* 支付系统退款单号
*/
private String refundId;
/**
* 原订单金额
*/
private Integer totalFee;
/**
* 退款金额
*/
private Integer refund;
/**
* 退款原因
*/
private String reason;
/**
* 退款单状态
*/
private String refundStatus;
/**
* 申请退款返回参数
*/
private String contentReturn;
/**
* 退款结果通知参数
*/
private String contentNotify;
}
@TableName("t_refund_info")
表示映射到具体的某张表中BaseEntity
Results
/**
* @author lambda
* 该类用于前后端交互,为前端设置一个标准的响应结果
* 即该类设置了需要交给前端的数据
*
*/
@Data
@Accessors(chain = true)
public class Results {
/**
* 响应码
*/
private Integer code;
/**
* 响应消息
*/
private String message;
/**
* 封装其他信息
*/
private Map<String, Object> data =new HashMap<>();
/**
* 用于返回正确的结果显示
* @return Results 表示返回数据对象
*/
public static Results returnOk(){
Results results = new Results();
results.setCode(0);
results.setMessage("Succeed!");
return results;
}
/**
* 返回错误的显示信息
* @return Results
*/
public static Results returnError(){
Results results = new Results();
results.setCode(-1);
results.setMessage("Failed");
return results;
}
/**
* 用于返回k-v的信息
* @param key 给前端传递的键
* @param value 给前端传递的值
* @return Results
*/
public Results returnData(String key,Object value){
this.data.put(key, value);
return this;
}
}
主要是对订单信息、支付信息、产品信息、以及退款信息作持久层的设置
@Mapper
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
}
不需要编写任何的增删改查方法,因为继承了BaseMapper
,并且指定了泛型的类型为OrderInfo。@Mapper
注解表示该接口的实现对象交由MyBatisPlus底层去实现。
@Mapper
public interface PaymentInfoMapper extends BaseMapper<PaymentInfo> {
}
不需要编写任何的增删改查方法,因为继承了BaseMapper
,并且指定了泛型的类型为PaymentInfo。@Mapper
注解表示该接口的实现对象交由MyBatisPlus底层去实现。
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
}
不需要编写任何的增删改查方法,因为继承了BaseMapper
,并且指定了泛型的类型为Product。@Mapper
注解表示该接口的实现对象交由MyBatisPlus底层去实现。
@Mapper
public interface RefundInfoMapper extends BaseMapper<RefundInfo> {
}
不需要编写任何的增删改查方法,因为继承了BaseMapper
,并且指定了泛型的类型为RefundInfo。@Mapper
注解表示该接口的实现对象交由MyBatisPlus底层去实现。
枚举配置主要针对订单状态、支付类型设置
@AllArgsConstructor
@Getter
public enum OrderStatus {
/**
* 未支付
*/
NOTPAY("未支付"),
/**
* 支付成功
*/
SUCCESS("支付成功"),
/**
* 已关闭
*/
CLOSED("超时已关闭"),
/**
* 已取消
*/
CANCEL("用户已取消"),
/**
* 退款中
*/
REFUND_PROCESSING("退款中"),
/**
* 已退款
*/
REFUND_SUCCESS("已退款"),
/**
* 退款异常
*/
REFUND_ABNORMAL("退款异常");
/**
* 类型
*/
private final String type;
}
@AllArgsConstructor
@Getter
public enum PayType {
/**
* 微信
*/
WXPAY("微信"),
/**
* 支付宝
*/
ALIPAY("支付宝");
/**
* 类型
*/
private final String type;
}
public enum AliPayTradeState {
/**
* 支付成功
*/
SUCCESS("TRADE_SUCCESS"),
/**
* 未支付
*/
NOTPAY("WAIT_BUYER_PAY"),
/**
* 订单关闭
*/
CLOSED("TRADE_SUCCESS"),
/**
* 退款成功
*/
REFUND_SUCCESS("REFUND_SUCCESS"),
/**
* 退款失败
*/
REFUND_ERROR("REFUND_ERROR");
private final String type;
private AliPayTradeState(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
public class OrderNoUtils {
/**
* 获取订单编号
* @return
*/
public static String getOrderNo() {
return "ORDER_" + getNo();
}
/**
* 获取退款单编号
* @return
*/
public static String getRefundNo() {
return "REFUND_" + getNo();
}
/**
* 获取编号
* @return
*/
public static String getNo() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String newDate = sdf.format(new Date());
String result = "";
Random random = new Random();
for (int i = 0; i < 3; i++) {
result += random.nextInt(10);
}
return newDate + result;
}
}
public class HttpUtils {
/**
* 将通知参数转化为字符串
* @param request
* @return
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
前端项目的内容大致如下:
电脑网站支付的支付接口 alipay.trade.page.pay(统一收单下单并支付页面接口)调用时序图如下:
主要使用alipay.trade.page.pay
,即时序图中的1.1,发起支付请求
biz_content
内容,该内容是具体的请求参数,需要我们手动设置。(均为必填项目)a、首先是编写针对订单信息的服务层类以及方法(主要是根据产品号创建订单、通过产品号在数据库中查询订单)
public interface OrderInfoService extends IService<OrderInfo> {
/**
* Create order by product id order info.
* 根据产品的id生成对应的订单信息
*
* @param productId the product id
* @return the order info
*/
OrderInfo createOrderByProductId(Long productId);
}
//对应的实现类为:OrderInfoServiceImpl
@Service
@Slf4j
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Resource
private ProductMapper productMapper;
@Resource
private OrderInfoMapper orderInfoMapper;
/**
* 根据产品id创建订单信息
* @param productId the product id
* @return 订单信息
*/
@Override
public OrderInfo createOrderByProductId(Long productId) {
//查找已存在,但是并未支付的订单信息
OrderInfo orderInfoNoPay = getNoPayOrderByProductId(productId);
if (orderInfoNoPay!=null){
return orderInfoNoPay;
}
//获取商品的对象
Product product = productMapper.selectById(productId);
//生成订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setTitle(product.getTitle());
//订单号
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());
orderInfo.setTotalFee(product.getPrice());
orderInfo.setProductId(productId);
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//将订单信息存入数据库
orderInfoMapper.insert(orderInfo);
return orderInfo;
}
/**
* 该方法用于获取用户未支付的订单(由于只在该类中使用,所以定义为私有方法)
* @param productId
* @return
*/
private OrderInfo getNoPayOrderByProductId(Long productId){
//使用MyBatis-plus的查询器
QueryWrapper<OrderInfo> orderInfoQueryWrapper = new QueryWrapper<>();
//设置判断条件,id和类型信息
orderInfoQueryWrapper.eq("product_id", productId);
orderInfoQueryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
//使用自带的selectOne方法判断是否同时满足条件
OrderInfo orderInfo = orderInfoMapper.selectOne(orderInfoQueryWrapper);
return orderInfo;
}
}
getNoPayOrderByProductId
用于根据产品信息获取用户未支付的订单。使用了 QueryWrapper
查询器productMapper
以及orderInfoMapper
b、查看统一收单下单并支付页面接口开发文档
需要我们编写对应格式的代码向支付宝平台发起支付请求。
c、编写对应的请求业务方法
//对应的业务层接口
public interface AliPayService {
/**
* Trade create string.
* 创建支付宝支付订单
*
* @param productId the product id
* @return the string
*/
String tradeCreate(Long productId);
}
//对应业务层实现方法
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
@Resource
private Environment config;//配置环境参数
/**
* 根据订单号创建订单并发起支付请求获取平台响应返回到前端
* @param productId the product id
* @return 返回支付请求调用的响应主体信息,返回到controller层
*/
@Transactional(rollbackFor = Exception.class)
@Override
public String tradeCreate(Long productId) {
try {
log.info("生成订单....");
//调用orderInfoService对象在数据库中创建订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
//调用支付宝接口
//创建支付宝请求对象
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
//设置请求处理完成后的跳转的地址
request.setReturnUrl(config.getProperty("alipay.return-url"));
//创建具体请求参数对象,用于组装请求信息
JSONObject bizContent = new JSONObject();
//设置商户订单号
bizContent.put("out_trade_no", orderInfo.getOrderNo());
//设置订单总金额,由于订单金额单位为分,而参数中需要的是元,因此需要bigDecimal进行转换
BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));
bizContent.put("total_amount", total);
//设置订单标题
bizContent.put("subject", orderInfo.getTitle());
//设置支付产品码,比较固定(电脑支付场景下只支持一种类型)
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
//设置完成后,将bizContent具体请求对象转换成json并放置在请求中
request.setBizContent(bizContent.toString());
//利用alipay客户端执行请求
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
//判断请求是否成功
if (response.isSuccess()){
//打印响应信息主体
log.info("调用成功====》{}",response.getBody());
}else {
log.info("调用失败====》{},返回码"+response.getCode()+",返回描述为:"+response.getMsg());
throw new RuntimeException("创建支付交易失败.....");
}
return response.getBody();
}catch (AlipayApiException e){
throw new RuntimeException("创建支付交易失败.....");
}
}
}
OrderInfoService
和alipayClient
对象,用于相应的业务处理。OrderInfoService
的通过订单号创建订单方法,新生成一个订单(具体的创建流程参考OrderInforServiceImpl类)AlipayTradePagePayRequest
,同时使用JsonObject
类创建一个请求参数对象,用于设置请求的参数,具体的参数值需要从新创建的订单信息中获取。同时利用config设置支付完成的返回地址AlipayTradePagePayRequest
请求对象中。alipayClient
对象执行请求,执行方法为pageExecute
。得到一个请求的响应对象AlipayTradePagePayResponse
,后续对请求的响应对象进行处理,如果返回的响应成功,则将请求响应的主体信息返回到controller层,否则抛出异常提示。d、编写控制层跳转方法
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
@ApiOperation("统一收单下单并支付页面接口")
@PostMapping("/trade/page/pay/{productId}")
public Results tradePagePay(@PathVariable Long productId){
log.info("统一收单下单并支付页面接口");
//发起交易请求后,会返回一个form表单格式的字符串(要有前端渲染)
String formStr=aliPayService.tradeCreate(productId);
//最后需要将支付宝的响应信息传递给前端,到前端之后会自动提交表单到action指定的支付宝开放平台中
//从而为用户展示支付页面。
return Results.returnOk().returnData("formStr",formStr);
}
}
@CrossOrigin
设置请求跨域访问AliPayService
对象依赖AliPayService
对象的创建交易方法tradeCreate
(具体实现在AliPayServiceImpl类中)a、首先在swagger上执行测试
访问;http://localhost:8090/swagger-ui.html页面
执行controller层请求,得到如下信息(返回的form表单)
b、启动前端项目
访问http://localhost:8080,即可看到前台的选择产品页面
可以选择支付宝支付
c、选择一项课程点击确认支付,即可跳转到相应的支付页面
可以使用沙箱帐号登陆支付
输入支付密码成功支付
也可以选择使用扫码完成支付
使用沙箱版支付宝在手机端完成支付。
支付成功后跳转页面如下
但是依旧存在问题,买家已经付款,但是卖家的订单信息仍旧未更新(即使是支付成功,但后台数据库是显示未支付),如下图所示:
原因在于:用户付款成功后,支付宝尚未对商户发起异步的通知结果,商户未收到支付宝的付款通知,自然也就不会对订单信息进行更新。
支付成功异步通知主要是支付宝端向商户端发送结果通知,由于商户的网络环境是处于局域网,因此支付宝平台要想通知到商户必须要进行内网穿透。使用ngrok
工具来进行内网穿透。
前往ngrok的官网登陆后下载,官网地址:https://ngrok.com/download
此次测试基于linux环境下测试,因此,下载linux系统下的压缩包。
打开终端解压至/usr/local/bin
目录下
sudo tar xvzf ~/Downloads/ngrok-v3-stable-linux-amd64.tgz -C /usr/local/bin
之后测试ngrok是否安装成功
ngrok -v
ngrok version 3.0.3
# 此时表明ngrok安装成功
ngrok config add-authtoken 29ds9En84SWW7uOuqwIEMvjWnAy_71i4aLWQpTjoAXnsuVuEX
该操作会在.config/ngrok
目录下生成ngrok.yml配置文件
ngrok http 8090
ngrok (Ctrl+C to quit)
Session Status online
Account binbin (Plan: Free)
Version 3.0.3
Region Japan (jp)
Latency calculating...
Web Interface http://127.0.0.1:4040
Forwarding https://2b4a-202-192-72-1.jp.ngrok.io -> http://localhost:8090
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
需要注意的是每次启动ngrok,对应的内网穿透地址都会发生改变。需要在配置文件中进行相应的更改
对于 PC 网站支付的交易,在用户支付完成之后,支付宝会根据 API 中商户传入的 notify_url,通过 POST 请求的形式将支付结果作为参数通知到商户系统。
其中也包括两部分内容,一部分是公共参数,一部分是业务参数。
公共参数部分
其中的sign表示签名,后续商户需要对签名进行验证,如果确认是支付宝的通知,则进行操作,如果不是则不予理会。
业务参数部分
此外需要特别注意的是:
商户的程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是 success 这7个字符,支付宝服务器会不断重发通知,直到超过 24 小时 22 分钟。一般情况下,25 小时以内完成 8 次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h)。
如果商户处理异步通知请求失败,则向支付宝端返回“failure”。
此处是由商户端对支付宝从远端发来的异步通知结果进行签名验证(支付宝公钥进行验证,因为是非对称加密),如果确认是支付宝平台发送的,则证明可以执行相关操作。
Map<String, String> paramsMap = ... //将异步通知中收到的所有参数都存放到map中
boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, ALIPAY_PUBLIC_KEY, CHARSET, SIGN_TYPE) //调用SDK验证签名
if(signVerified){
// TODO 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
}else{
// TODO 验签失败则记录异常日志,并在response中返回failure.
}
String result = "failure";
//异步通知验签(使用我们引入的支付宝SDK验证签名)
//一个是异步通知的结果参数,一个是支付宝的公钥,一个是字符集,一个是加密方式,得到一个布尔值结果
boolean signVerified = AlipaySignature.rsaCheckV1(params, config.getProperty("alipay.alipay-public-key"),
AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);
//对签名结果进行判断
if (!signVerified) {
//TODO:验签失败则记录异常日志,并在response中返回failure
log.error("支付成功,异步通知验签失败......");
return result;
}
//TODO:验证成功,按照支付结果异步通知中的描述,
// 对支付结果中的业务内容进行二次校验,
// 校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
log.info("支付成功,异步通知验签成功.......");
//1.商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号;
//获取对应的订单号
String outTradeNo = params.get("out_trade_no");
//利用获取的订单号查询对应的订单信息(返回一个订单对象)
OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);
//对订单对象进行判断
if (order==null){
log.error("订单不存在......");
return result;
}
//2.判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)
//从参数中获取金额(单位为元),但是数据库中的单位为分,因此需要进行转换
String totalAmount = params.get("total_amount");
int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
//获取订单中的金额
int totalFeeInt = order.getTotalFee();
if (totalFeeInt!=totalAmountInt){
//如果不等,则说明金额不对
log.error("金额校验失败");
return result;
}
//3.校验通知中的 seller_id(对应商户的PID)(或者 seller_email) 是否为 out_trade_no
// 这笔单据的对应的操作方(有的时候,一个商户可能有多个 seller_id/seller_email)
String sellerId = params.get("seller_id");
//获取实际的商户PID
String pid = config.getProperty("alipay.seller-id");
if (!sellerId.equals(pid)){
//用商户的PID与参数中的sellerID进行比较
log.error("商家PID校验失败....");
return result;
}
//4.验证 app_id 是否为该商户本身
String appId = params.get("app_id");
String appIdProperty = config.getProperty("alipay.app-id");
if (!appId.equals(appIdProperty)){
log.error("appId校验失败");
return result;
}
//在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS
// 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。
//获取交易状态
String tradeStatus = params.get("trade_status");
if (!"TRADE_SUCCESS".equals(tradeStatus)){
//如果不满足交易状态成功参数,则直接返回failure
log.error("支付未成功....");
return result;
}
//以上4步校验成功后设置为success,之后返回结果,商户可以自身进行后续的处理
//商户处理自身业务
result = "success";
return result;
异步通知结果确认无误之后,商户系统需要对现有记录的订单进行处理,商户系统的处理主要包括:处理业务、修改订单状态、记录支付日志等等。
由于在商户更新其系统信息之时,需要更新对应的订单状态,因此需要在对应的订单信息处理的类中设置相应的方法处理订单状态的更新。
// 对应OrderInfoService接口添加新方法
public interface OrderInfoService extends IService<OrderInfo> {
/**
* Create order by product id order info.
* 根据产品的id生成对应的订单信息
*
* @param productId the product id
* @return the order info
*/
OrderInfo createOrderByProductId(Long productId);
/**
* Update status by order no.
* 根据订单号更新数据库中的订单状态
*
* @param orderNo the order no
* @param orderStatus the order status
*/
void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus);
}
//对应OrderInfoServiceImpl的实现方法
@Service
@Slf4j
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
xxxxxxxx
/**
* 此方法用于根据订单编号来更新数据库中的订单状态
* @param orderNo 订单编号
* @param orderStatus 成功响应码
*/
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
log.info("更新数据库中的订单状态=======>"+orderStatus.getType());
//创建一个查询条件,主要针对OrderInfo订单信息
QueryWrapper<OrderInfo> orderInfoQueryWrapper = new QueryWrapper<>();
//编写查询条件
orderInfoQueryWrapper.eq("order_no",orderNo);
//创建一个订单信息对象
OrderInfo orderInfo = new OrderInfo();
//设置要更新的订单状态
orderInfo.setOrderStatus(orderStatus.getType());
//执行更新操作
orderInfoMapper.update(orderInfo,orderInfoQueryWrapper);
}
}
QueryWrapper
查询条件来对对应的订单信息作等值查询,并同时更新订单状态。//创建支付日志接口并设置相应的方法
public interface PaymentInfoService {
/**
* Create payment info for ali pay.
*为支付创建日志记录
* @param params the params
*/
void createPaymentInfoForAliPay(Map<String, String> params);
}
//为支付日志接口创建实现类,并实现创建支付日志方法
@Service
@Slf4j
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {
@Resource
private PaymentInfoMapper paymentInfoMapper;
/**
* 记录支付宝的支付日志
* @param params the params 支付通知参数
*/
@Override
public void createPaymentInfoForAliPay(Map<String, String> params) {
log.info("记录支付宝支付日志.....");
//创建支付信息对象
PaymentInfo paymentInfo = new PaymentInfo();
paymentInfo.setOrderNo(params.get("out_trade_no"));
paymentInfo.setPaymentType(PayType.ALIPAY.getType());
//设置业务编号(支付宝对应的是trade_no)
paymentInfo.setTransactionId(params.get("trade_no"));
//设置支付的场景
paymentInfo.setTradeType("电脑网站支付");
//设置交易状态
paymentInfo.setTradeState(params.get("trade_status"));
//设置交易金额,此处依旧需要转换(支付宝端对应的是元,数据库中对应分)
int totalAmount=new BigDecimal(params.get("total_amount")).multiply(new BigDecimal("100")).intValue();
paymentInfo.setPayerTotal(totalAmount);
//之后设置备注信息,需要将平台传入的map集合信息转成字符串类型存入数据库
Gson gson = new Gson();
String content = gson.toJson(params, HashMap.class);
paymentInfo.setContent(content);
//将信息插入数据库中
paymentInfoMapper.insert(paymentInfo);
}
}
createPaymentInfoForAliPay
方法需要传入平台传递给我们的正确的参数,是一个集合类型的。@Resource
注解在类中注入 PaymentInfoMapper paymentInfoMapper
的对象paymentInfoMapper
的insert
方法插入到数据库中(insert方法已有MyBatisPlus实现)此方法主要用于订单处理(接收到支付宝异步通知后验签成功的订单处理)
// 异步通知处理接口方法processOrder
public interface AliPayService {
xxx
/**
* Process order.
*订单处理方法
* @param params the params
*/
void processOrder(Map<String, String> params);
}
//对应接口的实现方法
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AlipayClient alipayClient;
@Resource
private Environment config;
@Resource
private PaymentInfoService paymentInfoService;
xxxxx
/**
* 商户系统订单处理
* @param params 支付宝平台异步通知传递的参数
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map<String, String> params) {
log.info("处理订单.......");
//获取传递信息中的订单号
String orderNo = params.get("out_trade_no");
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfoForAliPay(params);
}
PaymentInfoService paymentInfoService
用于支付信息处理。 @Transactional(rollbackFor = Exception.class)
表示当遇到对应的异常时候进行回滚操作。@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
@Resource
private Environment config;
@Resource
private OrderInfoService orderInfoService;
/**
* 支付宝异步通知处理结果
* @param params 支付宝异步通知发过来的参数
* @return 最终返回商户程序给予支付宝平台的信息
*/
@ApiOperation("支付通知")
@PostMapping("/trade/notify")
public String tradeNotify(@RequestParam Map<String,String> params) {
try {
//@RequestParam表示将参数从请求中取出放入map集合中
log.info("支付通知正在执行");
log.info("通知参数----》{}", params);
//result表示商家需要给支付宝反馈的异步通知结果(success表示成功,需要后续的业务来规定是否为
// success)
String result = "failure";
//异步通知验签(使用我们引入的支付宝SDK验证签名)
//一个是异步通知的结果参数,一个是支付宝的公钥,一个是字符集,一个是加密方式,得到一个布尔值结果
boolean signVerified = AlipaySignature.rsaCheckV1(params, config.getProperty("alipay.alipay-public-key"),
AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);
//对签名结果进行判断
if (!signVerified) {
//TODO:验签失败则记录异常日志,并在response中返回failure
log.error("支付成功,异步通知验签失败......");
return result;
}
//TODO:验证成功,按照支付结果异步通知中的描述,
// 对支付结果中的业务内容进行二次校验,
// 校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
//1.商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号;
//获取对应的订单号
String outTradeNo = params.get("out_trade_no");
//利用获取的订单号查询对应的订单信息(返回一个订单对象)
OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);
//对订单对象进行判断
if (order==null){
log.error("订单不存在......");
return result;
}
//2.判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)
//从参数中获取金额(单位为元),但是数据库中的单位为分,因此需要进行转换
String totalAmount = params.get("total_amount");
int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
//获取订单中的金额
int totalFeeInt = order.getTotalFee();
if (totalFeeInt!=totalAmountInt){
//如果不等,则说明金额不对
log.error("金额校验失败");
return result;
}
//3.校验通知中的 seller_id(对应商户的PID)(或者 seller_email) 是否为 out_trade_no
// 这笔单据的对应的操作方(有的时候,一个商户可能有多个 seller_id/seller_email)
String sellerId = params.get("seller_id");
//获取实际的商户PID
String pid = config.getProperty("alipay.seller-id");
if (!sellerId.equals(pid)){
//用商户的PID与参数中的sellerID进行比较
log.error("商家PID校验失败....");
return result;
}
//4.验证 app_id 是否为该商户本身
String appId = params.get("app_id");
String appIdProperty = config.getProperty("alipay.app-id");
if (!appId.equals(appIdProperty)){
log.error("appId校验失败");
return result;
}
//在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS
// 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。
//获取交易状态
String tradeStatus = params.get("trade_status");
if (!"TRADE_SUCCESS".equals(tradeStatus)){
//如果不满足交易状态成功参数,则直接返回failure
log.error("支付未成功....");
return result;
}
//以上4步校验成功后设置为success,之后返回结果,商户可以自身进行后续的处理
//商户处理自身业务
aliPayService.processOrder(params);
result = "success";
log.info("支付成功,异步通知验签成功.......");
return result;
}catch (AlipayApiException e) {
e.printStackTrace();
throw new RuntimeException("异步通知验证签名出现异常....");
}
}
}
Success
字符给支付宝平台(如果失败则返回failure),之后商户系统自身需要依据获得的参数对订单信息进行处理。(调用processOrder
方法)ngrok http 8090
# 由于需要通知到后端工程,需要开放对应的后端工程的端口
# 返回的信息
ngrok (Ctrl+C to quit)
Session Status online
Account binbin (Plan: Free)
Version 3.0.3
Region Japan (jp)
Latency 69.9073ms
Web Interface http://127.0.0.1:4040
Forwarding https://8141-183-238-79-57.jp.ngrok.io -> http://localhost:8090
Connections ttl opn rt1 rt5 p50 p90
1 0 0.00 0.00 60.53 60.53
首先对应的订单状态更新成功
最后订单的支付日志信息更新成功
至此,异步通知的处理完成。
存在的问题:
过滤重复通知发生在商户端接收到了支付宝平台的异步通知,并进行了相应的处理,给支付宝平台反馈信息,但由于某些原因(诸如网络等原因),支付宝并没有收到异步通知结果的反馈,支付宝会继续发送异步通知给商户端,商户接收到支付宝的二次异步通知仍旧会进行处理,记录第二次支付日志。
本质上来说是处理接口调用的幂等性问题。
/**
* Gets order status.
* 获取订单状态
*
* @param orderNo the order no
* @return the order status
*/
String getOrderStatus(String orderNo);
/**
* 根据订单号获取订单状态
* @param orderNo the order no
* @return
*/
@Override
public String getOrderStatus(String orderNo) {
//进行查询订单的操作
QueryWrapper<OrderInfo> orderInfoQueryWrapper = new QueryWrapper<>();
//构造查询条件
orderInfoQueryWrapper.eq("order_no",orderNo);
//根据订单号查询的订单信息必须是唯一的,因此使用selectOne
OrderInfo orderInfo = orderInfoMapper.selectOne(orderInfoQueryWrapper);
//判断订单信息是否为空,如果为空,直接将订单状态设置为null
if (orderInfo==null){
return null;
}
return orderInfo.getOrderStatus();
}
首先需要传入一个订单号,创建对应的条件查询器,利用eq
方法做相等比较(订单号唯一),利用orderInfoMapper
持久层对象做查询操作selectOne
,如果获取到的订单对象为空,则状态直接返回为空,否则返回对应的订单状态。
在AliPayServiceImpl
类中的processOrder
方法中进行逻辑处理。
//接口调用幂等性问题:在更新订单状态,记录支付日志之前过滤重复通知(无论接口被调用多少次,以下只执行一次)
//首先获取订单状态
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)){
//如果订单状态不是未支付,则直接返回,不需要任何处理
return;
}
如果获取到的订单状态不是未支付,则直接返回,因为更新订单状态以及记录支付日志只针对订单状态为未支付的订单。
此处还有一个问题,基于上述处理重复通知的业务,可能存在同时多台服务器发起异步通知,同时到达判断订单状态的地方,同时判断为未支付,继而同时执行更新同一订单操作(影响较小),同时执行记录支付日志操作。此时就需要在对应的业务逻辑中添加可重入锁(数据锁),避免因为函数重入造成的数据混乱。
/**
* 添加可重入锁对象,进行数据的并发控制
*/
private final ReentrantLock lock=new ReentrantLock();
/**
* 在对业务数据进行状态检查之前需要利用数据锁进行处理,进行并发控制
* 避免数据重入造成混乱,
* 此处使用尝试获取锁的判断,如果没有获取锁,此时则返回false,直接进行下面的操作
* 不会等待锁释放,造成阻塞。
*/
if (lock.tryLock()) {
try {
//接口调用幂等性问题:在更新订单状态,记录支付日志之前过滤重复通知(无论接口被调用多少次,以下只执行一次)
//首先获取订单状态
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)){
//如果订单状态不是未支付,则直接返回,不需要任何处理
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfoForAliPay(params);
}finally {
//必须要主动释放锁
lock.unlock();
}
}
}
对于进行订单处理的业务,则需要添加可重入锁进行并发控制。
用于交易创建后,用户在一定时间内未进行支付,可调用该接口直接将未付款的交易进行关闭。
关单接口中的请求参数(即发起关单请求需要携带哪些参数)。
/**
* Cancel order.
* 根据订单号取消订单
*
* @param orderNo the order no
*/
void cancelOrder(String orderNo);
/**
* 用户取消订单方法编写
* @param orderNo 订单号
*/
@Override
public void cancelOrder(String orderNo) {
//调用支付统一收单交易关闭接口
this.closeOrder(orderNo);
//更新用户的订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);
}
/**
* 关单接口调用
* @param orderNo 订单号
*/
private void closeOrder(String orderNo) {
try {
log.info("关单接口调用,订单号---》{}", orderNo);
//创建关单请求
AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
//创建请求参数对象
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
//将对应的参数设置到请求对象中
request.setBizContent(bizContent.toString());
//使用支付客户端对象执行请求
AlipayTradeCloseResponse response = alipayClient.execute(request);
//判断请求是否成功
if (response.isSuccess()){
//打印响应信息主体
log.info("调用成功====》{}",response.getBody());
}else {
log.info("调用失败====》{},返回码"+response.getCode()+",返回描述为:"+response.getMsg());
// throw new RuntimeException("关单接口调用失败....."); 让其正常结束
}
} catch (AlipayApiException e) {
throw new RuntimeException("关单接口调用出现异常");
}
}
关闭订单需要先创建关闭订单请求对象,之后创建一个请求参数封装对象,将订单号传入,执行请求,并对响应进行相应的处理。
调用支付宝端关单接口成功之后,需要设置新的交易订单的状态。
如果在支付过程中,并未扫码登录,则支付宝端并不创建此次交易的记录,也就是在判断请求状态是否成功的时候,会返回调用失败,正常结束方法,在商户系统中直接修改订单状态。
/**
* 用户取消订单接口
* @param orderNo 订单号
* @return 返回取消结果
*/
@ApiOperation("用户取消订单")
@PostMapping("/trade/close/{orderNo}")
public Results cancel(@PathVariable String orderNo){
log.info("用户取消订单......");
//处理取消订单业务
aliPayService.cancelOrder(orderNo);
//返回订单取消信息
return Results.returnOk().setMessage("订单已取消");
}
根据订单号关闭相应的订单操作
该接口提供所有支付宝支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。
需要调用查询接口的情况:
当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知;
调用支付接口后,返回系统错误或未知交易状态情况;
调用alipay.trade.pay,返回INPROCESS的状态;
调用alipay.trade.cancel之前,需确认支付状态;
首先是公共请求参数,提供了多个必填参数
其次是查询订单所需要的请求参数
再次是请求响应的公共响应参数
最后是请求的响应参数
/**
* The interface Ali pay service.
*
* @author lambda
*/
public interface AliPayService {
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
/**
* Query order string.
* 商户向支付宝端查询订单结果
*
* @param orderNo the order no
* @return the string
*/
String queryOrder(String orderNo);
}
/**
* 商户查询订单信息
* @param orderNo 订单号
* @return 返回订单查询结果,如果返回为null,说明支付宝端没有创建订单
*/
@Override
public String queryOrder(String orderNo) {
try {
log.info("查单接口调用----》{}", orderNo);
//首先创建交易查询对象
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
//组装请求参数对象(向支付宝端查单需要提供哪些参数)
JSONObject bizContent = new JSONObject();
//组装订单号
bizContent.put("out_trade_no", orderNo);
request.setBizContent(bizContent.toString());
//执行查询请求
AlipayTradeQueryResponse response= alipayClient.execute(request);
if (response.isSuccess()){
log.info("调用成功,返回结果---》{}",response.getBody());
return response.getBody();
}else{
log.info("调用失败,返回响应码"+response.getCode()+",响应结果为"+response.getBody());
// throw new RuntimeException("响应失败....");
//调用失败直接返回为null
return null;
}
} catch (AlipayApiException e) {
throw new RuntimeException("查询订单接口调用失败.....");
}
}
/**
*商户查询订单接口
* 商户根据订单号查询相应的订单信息
* @param orderNo 订单号
* @return
*/
@ApiOperation("商户查询订单")
@GetMapping("/trade/query/{orderNo}")
public Results queryOrder(@PathVariable String orderNo){
log.info("商户查询订单====》{}",orderNo);
//调用支付宝支付服务的查询订单方法
String result=aliPayService.queryOrder(orderNo);
return Results.returnOk().setMessage("查询订单信息").returnData("result",result);
}
获取到查询的信息之后返回给前端。
//orderInfoService
public interface OrderInfoService extends IService<OrderInfo> {
/**
* Gets no pay order by duration.
* 查询超过指定时间未支付的订单
*
* @param minutes the
* @param paymentType the payment type
* @return the no pay order by duration
*/
List<OrderInfo> getNoPayOrderByDuration(int minutes,String paymentType);
}
//orderInfoServiceImpl
@Service
@Slf4j
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
/**
*查询超过指定时间未支付的订单集合
* @param minutes the
* @return
*/
@Override
public List<OrderInfo> getNoPayOrderByDuration(int minutes,String paymentType) {
//创建一个时间实例,减去超时时间的时间实例,与订单的创建时间相比
Instant minus = Instant.now().minus(Duration.ofMinutes(minutes));
//创建一个查询订单对象
QueryWrapper<OrderInfo> orderInfoQueryWrapper = new QueryWrapper<>();
//组装订单的查询信息,首先是未支付
orderInfoQueryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
//如果当前时间减去超时时间的时间值比创建时间晚,则说明已经超时了
orderInfoQueryWrapper.le("create_time",minus);
orderInfoQueryWrapper.eq("payment_type",paymentType);
//最后将查询的结果返回
return orderInfoMapper.selectList(orderInfoQueryWrapper);
}
}
根据订单号以及支付的类型来查询尚未支付的订单。
@Slf4j
@Component
public class AliPayTask {
@Resource
private OrderInfoService orderInfoService;
/**
* 每30秒查询一次订单信息,查询创建1分钟并且未支付的订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm(){
log.info("定时查询订单任务启动");
//调用查询未支付订单的方法获取所有的订单信息
List<OrderInfo> noPayOrderList = orderInfoService.getNoPayOrderByDuration(1, PayType.ALIPAY.getType());
//遍历超时订单
for (OrderInfo orderInfo : noPayOrderList) {
String orderNo = orderInfo.getOrderNo();
log.info("超时1分钟未支付的订单---》{}",orderNo);
}
}
}
@Scheduled(cron = "0/30 * * * * ?")
表示指定定时任务的周期。
此处主要说明在商户端发起向支付宝端发起的查询订单信息,以便商户端更新订单相关信息。因为本地显示未支付付,不能保证在支付宝端也是未支付的,如果支付宝端已经支付则需要更新本地的订单信息为已支付。
/**
* 根据订单号查询支付宝端的订单状态
* 如果订单已经支付,则更新商户端订单状态,并记录支付日志
* 如果订单没有支付,则调用关单接口,并更新商户端订单状态
* 如果订单未创建,则直接更新商户端的订单状态即可
* @param orderNo 订单号
*/
@Override
public void checkOrderStatus(String orderNo) {
log.warn("根据订单号核实订单状态---》{}",orderNo);
//商户端向支付宝端查询订单信息
String result = this.queryOrder(orderNo);
//1.订单未创建状态
if (result==null){
log.warn("核实订单未创建---》{}",orderNo);
//更新本地订单状态(设置关闭)
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
}
//2.如果订单未支付,则调用关单接口并更新商户端订单状态
Gson gson = new Gson();
//由于result的值中也是属于键值对,String-{xxx:xxx,xxxx:xxxx,xxx:xxxx}
Map<String, LinkedTreeMap> resultMap = gson.fromJson(result, HashMap.class);
//参见统一收单线下交易查询中的响应示例
LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");
//从map中获取订单状态(trade_status)
String tradeStatus = (String)alipayTradeQueryResponse.get("trade_status");
if (AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){
//判断如果订单未支付
log.warn("核实订单未支付---》{}",orderNo);
//订单未支付,则调用关单接口
this.closeOrder(orderNo);
//更新商户端状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
}
//3.如果订单已经支付,则更新商户端的订单状态,并记录支付日志
if (AliPayTradeState.SUCCESS.getType().equals(tradeStatus)){
//判断订单已经支付
log.warn("核实订单已支付---》{}",orderNo);
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);
paymentInfoService.createPaymentInfoForAliPay(alipayTradeQueryResponse);
}
}
如果订单已经支付,则更新商户端订单状态,并记录支付日志
如果订单没有支付,则调用关单接口,并更新商户端订单状态
如果订单未创建,则直接更新商户端的订单状态即可
@Slf4j
@Component
public class AliPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private AliPayService aliPayService;
/**
* 每30秒查询一次订单信息,查询创建1分钟并且未支付的订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm(){
log.info("定时查询订单任务启动");
//调用查询未支付订单的方法获取所有的订单信息
List<OrderInfo> noPayOrderList = orderInfoService.getNoPayOrderByDuration(1, PayType.ALIPAY.getType());
//遍历超时订单
for (OrderInfo orderInfo : noPayOrderList) {
String orderNo = orderInfo.getOrderNo();
log.info("超时1分钟未支付的订单---》{}",orderNo);
//核实订单状态,调用支付宝端查单接口
aliPayService.checkOrderStatus(orderNo);
}
}
}
前提是:当用户选择支付宝支付的时候,商户系统会自动生成一个未支付的订单,无论用户是否扫码。如果用户未扫码,则显示未支付,若用户扫码并支付,则商户系统会等待支付宝端发起回调通知,通知商户用户支付成功,请商户及时更新状态。如果因为网络原因,商户系统无法正确接收支付宝端的回调通知,则用户需要在一段时间之后主动调用查单接口,根据反馈的信息进行相应的订单信息的更新。
如果订单没有在支付宝端创建(即没有进行扫码),超时一分钟后,商户系统会自动更新本次订单信息为超时已关闭。
如果订单已经扫码,但是没有进行实际的支付,超时一分钟后,商户系统也会自动更新本次订单信息为超时已关闭。
如果订单已经成功支付,但是由于网络原因,商户系统没有收到支付宝端的回调通知(此时尽管用户已经支付成功,但是商户系统并未收到通知,因此没有及时更新支付信息),此时就需要主要调用查单接口,如果查询到的信息是已经支付的,则需要商户主动更新本地订单信息,并且记录支付日志,更新订单状态。
当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,支付宝将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
public interface RefundInfoService extends IService<RefundInfo> {
/**
* Create refund by order no refund info.
* 根据订单号创建退款订单
*
* @param orderNo the order no
* @param reason the reason
* @return the refund info
*/
RefundInfo createRefundByOrderNo(String orderNo, String reason);
/**
* Update refund.
* 更新退款信息
*
* @param bodyAsString the body as string
*/
void updateRefund(String bodyAsString);
/**
* Update refund for ali pay.
* 支付宝支付退款
* @param refundNo the refund no
* @param content the content
* @param refundStatus the refund status
*/
void updateRefundForAliPay(String refundNo, String content, String refundStatus);
}
@Service
public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundInfo> implements RefundInfoService {
@Resource
private OrderInfoService orderInfoService;
@Resource
private RefundInfoMapper refundInfoMapper;
/**
*
* @param orderNo 订单编号
* @param reason 退款原因
* @return RefundInfo 退款单信息
*/
@Override
public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
//根据订单号处理订单信息
OrderInfo orderInfo=orderInfoService.getOrderByOrderNo(orderNo);
//根据订单号生成退款单记录
RefundInfo refundInfo = new RefundInfo();
//订单编号
refundInfo.setOrderNo(orderNo);
//退款单编号
refundInfo.setRefundNo(OrderNoUtils.getRefundNo());
//原来订单金额
refundInfo.setTotalFee(orderInfo.getTotalFee());
//退款金额
refundInfo.setRefund(orderInfo.getTotalFee());
//退款原因
refundInfo.setReason(reason);
//将退款信息插入数据库
refundInfoMapper.insert(refundInfo);
return refundInfo;
}
@Override
public void updateRefund(String content) {
//将退款请求响应的返回对象转成Map类型信息
Gson gson = new Gson();
Map<String, String> resultMap = gson.fromJson(content, HashMap.class);
//根据退款单编号,修改退款单
QueryWrapper<RefundInfo> refundInfoQueryWrapper = new QueryWrapper<>();
refundInfoQueryWrapper.eq("refund_no",resultMap.get("out_refund_no"));
//设置要修改的字段
RefundInfo refundInfo = new RefundInfo();
//微信支付退款单号
refundInfo.setRefundId(resultMap.get("refund_id"));
//查询申请退款和退款中的返回参数(退款中)
if (resultMap.get("status")!=null){
//设置退款状态
refundInfo.setRefundStatus(resultMap.get("status"));
//将全部响应结果存入数据库的content字段中
refundInfo.setContentReturn(content);
}
//退款回调中的回调参数(这是退款之后的状态)
if (resultMap.get("refund_status")!=null){
refundInfo.setRefundStatus(resultMap.get("refund-status"));
//将全部响应结果存入数据库的content字段中
refundInfo.setContentNotify(content);
}
//更新退款单
refundInfoMapper.update(refundInfo,refundInfoQueryWrapper);
}
/**
*
* @param refundNo 退款单号
* @param content 退款信息主体
* @param refundStatus 退款结果类型
*/
@Override
public void updateRefundForAliPay(String refundNo, String content, String refundStatus) {
//根据退款单号修改退款单
QueryWrapper<RefundInfo> refundInfoQueryWrapper = new QueryWrapper<>();
refundInfoQueryWrapper.eq("refund_no",refundNo);
//设置要修改的字段(新建一个退款单)
RefundInfo refundInfo = new RefundInfo();
//refundInfo.setRefundNo(refundNo);
refundInfo.setRefundStatus(refundStatus);
refundInfo.setContentReturn(content);
//执行更新操作
refundInfoMapper.update(refundInfo,refundInfoQueryWrapper);
}
首先,注入RefundInfoMapper
用于操作持久层的退款单信息(执行增删改查业务),接着创建一个修改退款信息的对象,匹配条件是退款单号一致即可。之后创建一个退款信息对象,设置对应的退款状态以及原因,执行更新操作。
public interface AliPayService {
/**
* Refund.
* 退款操作
* @param orderNo the order no
* @param reason the reason
*/
void refund(String orderNo, String reason);
}
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
@Resource
private RefundInfoService refundInfoService
......
/**
* 商户发起退款请求
* @param orderNo 退款单号
* @param reason 原因
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) {
try {
log.info("调用退款API");
//调用退款信息方法创建退款信息
RefundInfo refundInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);
//创建统一交易退款请求
AlipayTradeRefundRequest request=new AlipayTradeRefundRequest();
//组装当前业务交易的请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no",orderNo);
//设置退款单金额(需要除以100),分转化成元
BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));
bizContent.put("refund_amount",refund);
bizContent.put("refund_reason",reason);
//将参数设置到请求中
request.setBizContent(bizContent.toString());
AlipayTradeRefundResponse response = alipayClient.execute(request);
if (response.isSuccess()){
log.info("退款交易成功,对应信息为:"+response.getBody());
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_SUCCESS);
//更新退款单
refundInfoService.updateRefundForAliPay( //表示退款成功
refundInfo.getRefundNo(),response.getBody(),AliPayTradeState.REFUND_SUCCESS.getType());
}else{
log.warn("退款交易失败,对应状态码为:"+response.getCode()+",返回体为:"+response.getBody());
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_ABNORMAL);
//更新退款单
refundInfoService.updateRefundForAliPay(
refundInfo.getRefundNo(),response.getBody(),AliPayTradeState.REFUND_ERROR.getType()
);
}
} catch (AlipayApiException e) {
throw new RuntimeException("退款交易失败.....");
}
}
首先注入RefundInfoService
对象,用于创建退款信息对象,之后创建支付宝交易退款请求,组装请求参数主要针对订单号、订单金额以及订单原因来进行组装,组合完毕之后进行相应的执行,得到支付宝端的响应,根据响应是否成功分别更新订单号以及退款单信息。
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
/**
* 商户退款接口
* @param orderNo 退款单号
* @param reason 退款原因
* @return
*/
@ApiOperation("商户退款接口")
@PostMapping("/trade/refund/{orderNo}/{reason}")
public Results refunds(@PathVariable String orderNo,@PathVariable String reason){
log.info("申请退款....");
//调用服务层退款方法
aliPayService.refund(orderNo,reason);
return Results.returnOk();
}
}
获取前端参数之后调用后端的退款请求,执行退款操作
退款结果展示:
商户可使用该接口查询自已通过alipay.trade.refund提交的退款请求是否执行成功。
若退款接口由于网络等原因返回异常,商户可调用退款查询接口 alipay.trade.fastpay.refund.query(统一收单交易退款查询接口)查询指定交易的退款信息。
public interface AliPayService {
...........
/**
* Query refund string.
* 查询退款结果
*
* @param orderNo the order no
* @return the string
*/
String queryRefund(String orderNo);
}
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
/**
* 根据订单号查询退款
* @param orderNo the order no 订单号
* @return 返回退款查询的结果
*/
@Override
public String queryRefund(String orderNo) {
try {
log.info("查询退款接口调用---》{}",orderNo);
//定义一个查询退款的请求对象
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
//组装请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no",orderNo);
//out_request_no表示退款请求号,如果退款的时候没有传入,则以订单号作为退款请求号。
bizContent.put("out_request_no",orderNo);
//组装到请求中
request.setBizContent(bizContent.toString());
//执行请求
AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()){
log.info("调用成功,返回结果---》{}",response.getBody());
return response.getBody();
}else {
log.info("调用失败,对应的响应码为:"+response.getCode()+",对应的响应内容为:"+response.getBody());
//如果调用失败,返回空
return null;
}
} catch (AlipayApiException e) {
throw new RuntimeException("退款查询请求执行失败");
}
}
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
@Resource
private AliPayService aliPayService;
@Resource
private Environment config;
@Resource
private OrderInfoService orderInfoService;
/**
* 退款结果查询(商户向支付宝端查询)
* @param orderNo 订单号
* @return 返回查询的结果
*/
@ApiOperation("查询退款")
@GetMapping("/trade/fastpay/refund/{orderNo}")
public Results queryRefunds(@PathVariable("orderNo") String orderNo){
log.info("查询退款.......");
//执行退款查询并接收返回的字符串结果
String result=aliPayService.queryRefund(orderNo);
return Results.returnOk().setMessage("查询成功").returnData("result",result);
}
在swagger中测试查询结果
为方便商户快速查账,支持商户通过本接口获取商户离线账单下载地址
public interface AliPayService {
..............
/**
* Query bill string.
* 查询订单下载地址
* @param billDate the bill date
* @param type the type
* @return the string
*/
String queryBill(String billDate, String type);
}
//根据账单的日期和类型查询
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
.........
/**
* 获取账单地址实现
* @param billDate the bill date 账单日期
* @param type the type 账单类型
* @return
*/
@Override
public String queryBill(String billDate, String type) {
try {
//设置查询账单请求对象
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
//组装请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("bill_type",type);
bizContent.put("bill_date",billDate);
//将请求参数设置到请求中
request.setBizContent(bizContent.toString());
//执行请求
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()){
log.info("查询账单url地址请求成功---》{}",response.getBody());
//获取账单的下载地址
Gson gson = new Gson();
Map<String,LinkedTreeMap> resultMap=gson.fromJson(response.getBody(),HashMap.class);
//获取交易账单地址
LinkedTreeMap billDownLoadUrl= resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
String billDownloadUrl = (String)billDownLoadUrl.get("bill_download_url");
//返回url地址
return billDownloadUrl;
}else {
log.info("查询账单地址失败。对应的响应码为:"+response.getCode()+",对应的响应体为:"+response.getBody());
throw new RuntimeException("查询账单地址失败....");
}
} catch (AlipayApiException e) {
throw new RuntimeException("查询账单请求执行失败.......");
}
}
}
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
.......
/**
* 根据账单类型和日期获取账单的url地址
* @param billDate 账单的日期
* @param type 账单的类型
* @return 返回账单的url地址
*/
@ApiOperation("获取账单url")
@GetMapping("/bill/downloadurl/query/{billDate}/{type}")
public Results queryTradeBill(@PathVariable String billDate,
@PathVariable String type){
log.info("获取账单的url地址");
//获取账单的url地址
String downloadUrl=aliPayService.queryBill(billDate,type);
return Results.returnOk().setMessage("获取账单地址成功")
.returnData("downloadUrl",downloadUrl);
}
}
对应的账单信息如下: