記事ディレクトリ
- Alipay 決済バックエンドの実戦 - SpringBoot ベース
-
- 1. Alipay決済の紹介とアクセスガイド
- 2. プロジェクトの環境整備
- 3. 決済機能の開発
Alipay 決済バックエンドの実戦 - SpringBoot ベース
1. Alipay決済の紹介とアクセスガイド
1. Alipay のオープン機能の紹介
(1) 能力マップ
決済力、支払い延長、資本力、口コミ力、マーケティング力、会員力、業界力など。
Alipay オープン プラットフォームにログインするだけです: https://opendocs.alipay.com/home
(2) パソコンサイト決済商品
このプロジェクトでは、コンピューター Web サイトの決済製品を例に挙げています。
主な理解には、アプリケーション シナリオ、アクセス条件、課金モードなどが含まれます。
2. アクセスの準備
(1) オープンプラットフォームアカウント登録
Alipay アカウントでログインした後、オープン プラットフォーム アカウントに登録し、決済を選択します。この期間中、携帯電話番号を使用して確認コードを受け取ります。
登録が完了すると開発者となり、自分でアプリケーションを作成できるようになります。
(2) 定期アクセス処理
- アプリケーションの作成: アプリケーションの種類の選択、アプリケーションの基本情報の入力、アプリケーション機能の追加、アプリケーション環境の構成 (Alipay 公開キー、アプリケーション公開キー、アプリケーション秘密キー、Alipay ゲートウェイ アドレスの取得、インターフェイス コンテンツ暗号化方式の構成) )、APPIDを表示します
- アプリケーションのバインド: 開発者アカウントの APPID と販売者アカウントの PID をバインドします (販売者アカウントのアプリケーションには正式なビジネス ライセンスが必要です)。
- 構成キー: アプリケーション センターを作成するためのアプリケーション環境を構成する手順
- オンライン申請: 審査のために申請を送信します
- 署名機能:ビジネスライセンス、登録済みWebサイト情報などをマーチャントセンターにアップロードし、審査と署名のために送信します。
(3) サンドボックスを利用する
この実際のアプリケーションは主にサンドボックス環境に基づいており、営業ライセンスや Web サイトへの申請などの情報の提出とレビューを必要とせず、初心者にとって支払い方法を学ぶのに非常に優しいです。
-
サンドボックス環境の構成
- 対応するAPPIDとPIDを取得します(サンドボックス環境での自動生成に対応)
- インターフェイスの暗号化方法を設定し、システムのデフォルト キーを選択し、暗号化方法として公開キー暗号化を選択し、対応するアプリケーション公開キー、アプリケーション秘密キー、および Alipay 公開キーを確認します (上記は非対称暗号化プロセスで使用されます)。
-
Alipayゲートウェイアドレスを取得する
-
インターフェースコンテンツの暗号化方式を設定します(このコンテンツは主にアプリケーションインターフェースの暗号化に使用され、自動的に生成されます)
-
Alipay のサンドボックス バージョンをダウンロードし、ログインを選択します
2. プロジェクトの環境整備
1. フレームワーク環境の準備
- SpringBoot プロジェクト、バージョン 2.3.12.RELEASE
- JDK バージョンの選択 1.8
- Web プロジェクトの依存関係をインポートする
spring-boot-starter-web
- ホット デプロイメント ツールのインポート
spring-boot-devtools
- ロンボク島をインポート:
lombok
- SpringBootのテスト環境をインポートする
spring-boot-starter-test
- Spring のプロジェクト トランザクション管理モジュールの導入
spring-tx
- インターフェーステストツールの紹介
springfox-swagger2
- インターフェーステスト可視化ツールの導入
springfox-swagger-ui
- データベース接続の依存関係を導入する
mysql-connector-java
- 永続層プロジェクトの依存関係を導入する
mybatis-plus
- 永続化レイヤーツールの依存関係を導入する
mybatis-plus-boot-starter
- データベース接続プールの依存関係の導入 (Druid)
druid
- カスタムメタデータ情報をインポートする
spring-boot-configuration-processor
- json データ処理の依存関係を導入する (Google)
gson
- AlipayのSDK開発者ツールの紹介
alipay-sdk-java
対応する pom ファイルの依存関係は次のとおりです。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<!-- swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<!--生成自定义元数据项信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!--JSON数据处理-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- 引入支付宝的sdk-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.22.57.ALL</version>
</dependency>
2. データベース環境
- という名前の mysql データを作成します。
payment_demo
- 注文情報テーブル、支払情報テーブル、商品テーブル、返金情報テーブルの4つのテーブルを作成します。
4 つのテーブルの内容は次のとおりです。
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;
注: 製品情報シートには 4 つの情報を追加する必要があります。
3. Alipay サンドボックス支払い設定ファイル
# 支付宝支付相关参数
# 应用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
上記のサンドボックス支払い設定ファイルの情報は、Alipay Open Platform 公式 Web サイトで個人向けに生成された関連情報から取得されています。(主にAPPID、PID、アプリケーション秘密キー、Alipay公開キーが関係します)
同時に、ファイルをプロジェクトのリソース フォルダーに追加し、同時に選択してproject structure
、対応する構成を選択します (下図を参照)。プロパティ ファイルを選択し、プロジェクトに設定する必要があります。
4. 一般的なプロジェクト構成ファイル
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
- 対応するポートを 8090 に設定します
- アプリケーション名を次のように設定します。
payment-demo
- thymeleaf テンプレート エンジンを閉じて、json の日付形式を設定します。
- データソースを構成し、ユーザー名、パスワード、および対応する URL アドレスを設定します。
- mybatis-plusの構成情報の設定、標準ログ出力の設定、永続層のxmlファイルアドレスの設定
- 最後に、ログの出力レベルを info に設定します (通常は info)。
5. 関連する構成クラスを構成する
(1) Swagger インターフェース構成クラス
このクラスは主にインターフェイスのテストに関連する設定に使用されます
@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 コンテナに注入する手段を使用します。
(2) データソース関連の設定
Druid のデータ ソースを構成します (SpringBoot のプロジェクトはデフォルトでは Druid のデータ ソースで構成されていません)
@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")
構成クラスと構成ファイル間のマッピング関係を実装するために使用します(属性値の完全な自動挿入)
(3) mybatisplusの設定
@Configuration //定义为配置类
@MapperScan("com.example.mapper") //扫描mapper接口
@EnableTransactionManagement //启用事务管理(spring-tx)
public class MyBatisPlusConfig {
}
(4) Alipay決済クライアントクラスの設定
@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;
}
}
-
locate を使用して
@PropertySource("classpath:alipay-sandbox.properties")
構成ファイルの場所を確認します -
アノテーションを使用して
@Resource
、Environment オブジェクトを挿入します。このオブジェクトは、指定された場所にある構成ファイル情報の関連コンテンツを読み取り、getProperties(xxx)
指定された項目の構成情報を取得するために使用されます。 -
public メソッドを使用してカスタム Bean オブジェクトを生成し、それを IOC コンテナに注入します。
-
上記で返されたオブジェクトは
AlipayClient
、リクエストの署名を自動的に生成し、レスポンスの署名検証操作を完了します。
6. エンティティクラス関連の設定
(1) エンティティクラスオブジェクトBaseEntity
@Data
public class BaseEntity {
/**
* 主键
*/
@TableId(value = "id",type = IdType.AUTO)
private String id;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
- このクラスは、後続のすべてのエンティティ クラスに共通するいくつかの属性を定義します。IDや作成時刻、更新時刻など
- アノテーションを使用して
@TableId(value = "id",type = IdType.AUTO)
、この属性の dao レイヤー マッピングを設定します。値の値はデータベース内の ID に対応し、主キーの自動インクリメント戦略を設定します。
(2) 注文情報オブジェクト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")
データベースにマップする具体的な名前を表すテーブルを使用します。- 以前のパブリック BaseEntity オブジェクトを継承します
(3) 決済情報オブジェクト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
(3) 製品オブジェクトProduct
@Data
@TableName("t_product")
public class Product extends BaseEntity{
/**
* 产品名称
*/
private String title;
/**
* 产品价格
*/
private Integer price;
}
@TableName("t_product")
データベース内の特定のテーブルにマップする表現を設定します。- 継承する
BaseEntity
(4) 返金情報オブジェクト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
(5) フロントエンドとバックエンドの相互作用情報オブジェクト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;
}
}
7. 永続層関連の設定
主に注文情報、支払い情報、商品情報、返金情報の永続層を設定します
- OrderInfoMapper はデータベースの注文情報を処理するために使用されます
@Mapper
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
}
BaseMapper
CRUD メソッドは継承され、ジェネリック型が OrderInfo として指定されるため、CRUD メソッドを記述する必要はありません。@Mapper
注釈は、このインターフェイスの実装オブジェクトが MyBatisPlus の最下位層によって実装されていることを示します。
- PaymentInfoMapperは支払い情報を処理するために使用されます
@Mapper
public interface PaymentInfoMapper extends BaseMapper<PaymentInfo> {
}
BaseMapper
継承され、ジェネリック型が PaymentInfo として指定されるため、追加、削除、変更、およびクエリのメソッドを記述する必要はありません。@Mapper
注釈は、このインターフェイスの実装オブジェクトが MyBatisPlus の最下位層によって実装されていることを示します。
- ProductMapperは製品情報を処理するために使用されます
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
}
BaseMapper
継承され、ジェネリック型が Product として指定されるため、追加、削除、変更、およびクエリのメソッドを記述する必要はありません。@Mapper
注釈は、このインターフェイスの実装オブジェクトが MyBatisPlus の最下位層によって実装されていることを示します。
- RefundInfoMapper は返金情報を処理するために使用されます
@Mapper
public interface RefundInfoMapper extends BaseMapper<RefundInfo> {
}
BaseMapper
CRUD メソッドは継承され、ジェネリック型が RefundInfo として指定されるため、CRUD メソッドを記述する必要はありません。@Mapper
注釈は、このインターフェイスの実装オブジェクトが MyBatisPlus の最下位層によって実装されていることを示します。
8. 関連する列挙構成を定義します。
列挙構成は主に注文ステータスと支払いタイプの設定用です
- 注文の状況
@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;
}
- Alipay取引ステータス
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;
}
}
9. ツールの準備
- 注文情報を取得するためのツールクラス
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;
}
}
- http に関するツール クラス (ユーザーが要求されたデータ情報を読み取るための静的メソッドは 1 つだけあります)
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();
}
}
}
}
}
10. フロントエンドプロジェクトの準備
フロントエンドプロジェクトの内容は大まかに以下の通りです。
3. 決済機能の開発
コンピュータ Web サイトでの支払いのための支払いインターフェイス alipay.trade.page.pay (統合された受領、注文、支払いページ インターフェイス) の呼び出しシーケンス図は次のとおりです。
1. 注文の受け取り、発注、支払いのための統一されたページインターフェイス
(1) APIプレビュー
主な用途alipay.trade.page.pay
、つまりシーケンス図の 1.1 は、支払いリクエストを開始することです。
- 以下は、Alipay の公式設定からのリクエスト パラメーターです。その一部は上記の AlipayClient オブジェクトにカプセル化されており、一部は後で設定する必要があります。
- 以下は、パブリック リクエスト パラメータの内容です
biz_content
。これは特定のリクエスト パラメータであり、手動で設定する必要があります。(全て必須項目です)
- 以下は、Alipay オープン プラットフォームによって提供されるパブリック応答パラメーターであり、後で設定する必要があります。
- 以下は、Alipay オープン プラットフォームによって提供される応答パラメーター (特定のビジネスに関連する一部の応答コンテンツ) であり、後続の設定も必要です
(2) 決済ビジネス層メソッドの作成
a. 1 つ目は、注文情報のサービス層クラスとメソッドを作成することです (主に製品番号に基づいて注文を作成し、製品番号を通じてデータベース内の注文をクエリします)。
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;
}
}
- OrderInfoService はメソッドを記述する必要がなく、追加、削除、チェック、ページングなどのすべての操作は IService によって完了され、継承された IService インターフェイスのジェネリック型は対応するエンティティ クラスです。対応する特定のビジネス メソッドを設定するだけです。
getNoPayOrderByProductId
製品情報に基づいてユーザーの未払い注文を取得するために使用されます。使用されるQueryWrapper
クエリア- 同時に、永続化レイヤー操作オブジェクト
productMapper
とorderInfoMapper
b. 統合された受注と支払いページのインターフェース開発ドキュメントを表示します。
Alipay プラットフォームへの支払いリクエストを開始するには、対応する形式でコードを記述する必要があります。
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 クラスを参照してください)。 - 次に、Alipay への支払いリクエストを開始するリクエスト オブジェクトを作成し
AlipayTradePagePayRequest
、同時にJsonObject
そのクラスを使用してリクエストのパラメータを設定するリクエスト パラメータ オブジェクトを作成します。特定のパラメータ値は、新たに注文情報を作成しました。同時に、configを使用して支払い完了の返信先アドレスを設定します - 再度、組み立てたリクエストパラメータオブジェクトを
AlipayTradePagePayRequest
リクエストオブジェクトに設定します。 - 最後に、
alipayClient
オブジェクトはリクエストの実行に使用され、実行メソッドは ですpageExecute
。要求応答オブジェクトを取得しAlipayTradePagePayResponse
、要求応答オブジェクトを処理します。返された応答が成功した場合は、要求応答の本文情報をコントローラー層に返し、それ以外の場合は例外プロンプトをスローします。
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 クラスに実装されています)。- 最後に、json 文字列をフォームの形式で返し、最終情報をフロントエンドに返してフォーム形式にレンダリングし、自動送信スクリプトを実行します。支払いが正常に開始されました
(3) テスト使用
a. まず、Swagger でテストを実行します。
http://localhost:8090/swagger-ui.html ページにアクセスしてください
コントローラー層リクエストを実行し、以下の情報を取得します(返されたフォーム)
b. フロントエンドプロジェクトを開始する
http://localhost:8080 にアクセスすると、最前面に製品選択ページが表示されます。
支払いにAlipayを選択できます
c. コースを選択し、「支払いの確認」をクリックして、対応する支払いページに移動します。
サンドボックス アカウントを使用してログインして支払うことができます
正常に支払うには支払いパスワードを入力してください
コードをスキャンして支払いを完了することも選択できます
Alipay のサンドボックス バージョンを使用して、携帯電話で支払いを完了します。
決済が成功するとジャンプページは以下のようになります
しかし、まだ問題はあります。次の図に示すように、買い手は支払いましたが、売り手の注文情報はまだ更新されていません(支払いは成功しても、バックグラウンド データベースには支払いが行われていないことが示されています)。
その理由は、ユーザーの支払いが成功した後、Alipay は販売者に非同期通知を送信しておらず、販売者は Alipay からの支払い通知を受け取っていないため、当然注文情報は更新されません。
(4) 決済成功の非同期通知
決済成功の非同期通知は、主に Alipay から加盟店に結果通知を送信することを目的としていますが、加盟店のネットワーク環境はローカルエリアネットワークであるため、加盟店に通知するために Alipay プラットフォームはイントラネット侵入を実行する必要があります。イントラネット侵入用のツールを使用しますngrok
。
a. ngrok をインストールして構成する
- まずngrokをダウンロードしてインストールします
ngrok の公式 Web サイトにアクセスし、ログインしてダウンロードします。公式 Web サイトのアドレス: 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 構成ファイルが生成されます。
- 侵入する必要があるこのマシンのポートを開きます (このプロジェクトのポートは 8090 に基づいているため、ポート 8090 を開きます)
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 が開始されるたびに、対応するイントラネット ペネトレーション アドレスが変更されることに注意してください。対応する変更を構成ファイルで行う必要があります
b. 非同期通知パラメータのAPI解析
PC ウェブサイトの支払いトランザクションの場合、ユーザーの支払いが完了すると、Alipay は販売者が API で渡した notify_url に従って、POST リクエストの形式でパラメータとして支払い結果を販売者システムに通知します。
また、2 つの部分も含まれており、1 つの部分はパブリック パラメーターであり、もう 1 つの部分はビジネス パラメーターです。
パブリックパラメータセクション
その中のサインは署名を意味しており、後続加盟店は署名を確認する必要があり、アリペイからの通知であることが確認できれば運用され、そうでなければ無視される。
ビジネスパラメータ部分
さらに、次の点に特別な注意を払う必要があります。
マーチャントのプログラムが実行された後、「成功」が (引用符なしで) 出力される必要があります。販売者が Alipay にフィードバックした文字が成功の 7 文字でない場合、Alipay サーバーは24 時間 22 分を超えるまで通知を再送信し続けます。通常の状況では、25 時間以内に 8 件の通知が完了します (通知の間隔頻度は通常、4 分、10 分、10 分、1 時間、2 時間、6 時間、15 時間です)。
販売者が非同期通知リクエストの処理に失敗した場合、Alipay に「失敗」が返されます。
c. 非同期の戻り結果の署名検証
- 1 つ目は、非同期通知の初期検証です (以下は主なビジネス ロジックの処理フローです)。
ここで、加盟店側は、Alipayがリモートエンドから送信した非同期通知結果の署名を検証し(非対称暗号化のため、検証にはAlipayの公開鍵が使用されます)、Alipayプラットフォームから送信されたことが確認できれば、 、関連する操作が実行できることを証明します。
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.
}
- 次に、非同期通知に対して二次検証を実行します。主に注文番号、注文金額、対応するオペレーター、販売者 ID を検証します (主なビジネス処理ロジックは以下のとおりです)。
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;
(5) 署名検証の成功を加盟店システムに非同期的に通知する
非同期通知の結果が正しいことが確認された後、販売者システムは既存の記録された注文を処理する必要があります。販売者システムの処理には主に、ビジネスの処理、注文ステータスの変更、支払いログの記録などが含まれます。
a. 注文の作成および更新方法
マーチャントがシステム情報を更新する場合には、対応する注文ステータスも更新する必要があるため、対応する注文情報処理クラスに対応するメソッドを設定して注文ステータスの更新を処理する必要がある。
// 对应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);
}
}
- ここには主に、パラメータとして渡される注文番号と注文ステータスの 2 つの情報が含まれます。
- クエリ条件は引き続き適用されて
QueryWrapper<OrderInfo>
、対応する注文情報に対して同等のクエリを実行し、同時に注文ステータスを更新します。
b. 支払いのための支払いログを作成する
//创建支付日志接口并设置相应的方法
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
オブジェクトを挿入する - 実装メソッドで新しい支払い情報オブジェクトを作成し、params コレクションから対応する情報を取得してオブジェクトに設定し、最後にメソッドを使用してデータベース
paymentInfoMapper
にinsert
挿入します (挿入メソッドは MyBatisPlus によって実装されています)。
c. Alipay ペイメントは、ビジネス層で注文を処理する方法を作成します。
このメソッドは主に注文処理(Alipay の非同期通知を受信した後、署名検証が成功した注文処理)に使用されます。
// 异步通知处理接口方法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)
対応する例外が発生したときにロールバック操作が実行されることを示します。
d. 制御層は非同期通知結果を処理します
@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
署名検証が成功した場合、販売者システムは対応する文字を Alipay プラットフォームにフィードバックする必要があり(失敗した場合は失敗を返す)、その後、販売者システム自体が取得したパラメータに従って注文情報を処理する必要があります。(processOrder
メソッドの呼び出し)
e. 非同期通知受信後の更新順序テスト
- まずフロントエンド プロジェクトを開きます: http://localhost:8080 にアクセスします。
- 次に、ngrok ツールを使用してイントラネットに侵入します (この手順を実行しないと、Alipay からの非同期通知を取得できません)。再起動ごとに対応するアドレス マッピングが異なるため、 ngrok からフィードバックされた情報に従ってファイルを作成します。
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
- バックエンド プロジェクトを開始し、ページ上で支払いを行って、対応するコースを購入します。
- 最後に、注文テーブルで注文のステータスを確認し、支払い情報で支払いログ情報を確認します。
まず、対応する注文ステータスが正常に更新されます
前回の注文の支払いログ情報が正常に更新されました
ここまでで非同期通知の処理は完了です。
f. 重複した通知をフィルタリングする
既存の問題:
重複通知のフィルタリングは、加盟店が Alipay プラットフォームから非同期通知を受信し、対応する処理を実行して Alipay プラットフォームにフィードバックを提供するときに発生しますが、何らかの理由 (ネットワーク上の理由など) により、Alipay は非同期通知のフィードバックを受信できません。通知結果。 Alipay は販売者に非同期通知を送信し続け、販売者は引き続き Alipay から受信した 2 番目の非同期通知を処理し、2 番目の支払いログを記録します。
基本的に、これはインターフェイス呼び出しの冪等性を扱います。
- まず、注文ステータスのクエリ メソッドを (OrderInfoServiceImpl クラス内に) 記述します。
/**
* 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;
}
注文ステータスの更新と支払いログの記録は、注文ステータスが未払いの注文に対してのみ行われるため、取得した注文ステータスが未払いでない場合は、直接返却してください。
g. データロックを追加する
ここでもう一つ問題があり、上記の繰り返しの通知を処理する業務を踏まえると、複数のサーバーが同時に非同期通知を開始し、同時に注文状況を判定する場所に到着し、判定される可能性があります。同時に未払いになり、同じ注文を同時に更新し(影響が少ない)、同時に支払いログを記録する操作を実行します。この時点で、関数の再入力によるデータの混乱を避けるために、対応するビジネス ロジックにリエントラント ロック (データ ロック) を追加する必要があります。
/**
* 添加可重入锁对象,进行数据的并发控制
*/
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();
}
}
}
注文処理のビジネスでは、同時実行制御のためにリエントラント ロックを追加する必要があります。
2. Alipay 統合取得トランザクション終了インターフェイス
トランザクションの作成後、ユーザーが一定期間内に支払いを怠った場合、このインターフェイスを呼び出して未払いのトランザクションを直接終了できます。
(1) APIプレビュー
通関インターフェースのリクエストパラメータ (つまり、どのパラメータを通関リクエストに含める必要があるか)。
(2) 注文を閉じる
- まずビジネスレイヤーで注文をクローズするメソッドを作成します。
/**
* 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("关单接口调用出现异常");
}
}
注文をクローズするには、最初に注文クローズリクエストオブジェクトを作成し、次にリクエストパラメータカプセル化オブジェクトを作成し、注文番号を渡し、リクエストを実行し、それに応じて応答を処理する必要があります。
Alipay の税関注文インターフェイスを正常に呼び出した後、新しいトランザクション注文のステータスを設定する必要があります。
支払いプロセス中にログインするためのコードをスキャンしない場合、Alipay はこのトランザクションの記録を作成しません。つまり、リクエストのステータスが成功したかどうかを判断するときに、呼び出し失敗、正常終了メソッドを返し、変更を加えます。販売者システムの注文ステータスに直接表示されます。
- コントロール層にメソッドを記述する
/**
* 用户取消订单接口
* @param orderNo 订单号
* @return 返回取消结果
*/
@ApiOperation("用户取消订单")
@PostMapping("/trade/close/{orderNo}")
public Results cancel(@PathVariable String orderNo){
log.info("用户取消订单......");
//处理取消订单业务
aliPayService.cancelOrder(orderNo);
//返回订单取消信息
return Results.returnOk().setMessage("订单已取消");
}
注文番号に従って、対応する注文操作を終了します。
(3) テスト
3. 統合取得オフライントランザクションクエリ
このインターフェイスは、すべての Alipay 支払い注文のクエリを提供します。販売者は、このインターフェイスを通じて注文ステータスをアクティブにクエリして、次のステップのビジネス ロジックを完了できます。
クエリインターフェイスを呼び出す必要がある状況:
販売者のバックグラウンド、ネットワーク、サーバーなどが異常な場合、販売者のシステムが支払い通知を受信していない場合、支払いインターフェイスを呼び出した後、システムエラー
または不明な取引ステータスが返された場合、
呼び出しalipay.trade.pay、処理中ステータスを返します
。alipay.trade.cancel を呼び出す前に、支払いステータスを確認する必要があります。
(1) APIプレビュー
- パブリックリクエストパラメータ
1 つ目はパブリック リクエスト パラメータで、複数の必須パラメータを提供します。
- リクエストパラメータ
2 番目は、注文に必要なリクエスト パラメータをクエリすることです。
- パブリック応答パラメータ
再度、リクエストレスポンスのパブリックレスポンスパラメータ
- 応答パラメータ
最後に、リクエストの応答パラメータ
- レスポンスAPI
(2) 積極的に注文を問い合わせる
- まず、AlipayService で注文操作をクエリするための新しいメソッドを作成します。
/**
* 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 * * * * ?")
指定されたスケジュールされたタスクの期間を示します。
(3) 定期注文確認用のオーダーが作成されていない
このセクションでは主に、販売者側が注文関連情報を更新できるように、販売者側から Alipay に注文情報を問い合わせる方法について説明します。ローカル表示が決済されていないため、Alipay側で決済されていないことは保証できませんので、Alipay側で決済されている場合は、ローカル注文情報を決済対象に更新する必要があります。
- Alipay から注文情報をクエリするメソッドを追加する
/**
* 根据订单号查询支付宝端的订单状态
* 如果订单已经支付,则更新商户端订单状态,并记录支付日志
* 如果订单没有支付,则调用关单接口,并更新商户端订单状态
* 如果订单未创建,则直接更新商户端的订单状态即可
* @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);
}
}
}
- テスト
前提条件は、ユーザーが支払いに Alipay を選択すると、ユーザーがコードをスキャンしたかどうかに関係なく、加盟店システムは未払いの注文を自動的に生成するということです。ユーザーがコードをスキャンしないと、未払いと表示されます。ユーザーがコードをスキャンして支払うと、販売者システムは、Alipay がコールバック通知を開始して、ユーザーの支払いが成功したことを販売者に通知するのを待ちます。時間内にステータスを更新するよう要求されました。ネットワーク上の理由により、加盟店システムが Alipay からのコールバック通知を正しく受信できない場合、ユーザーは一定期間後に注文確認インターフェイスを積極的に呼び出し、フィードバック情報に基づいて対応する注文情報を更新する必要があります。
注文が Alipay で作成されない場合 (つまり、コード スキャンが実行されない場合)、1 分間のタイムアウト後にタイムアウトが終了すると、マーチャント システムは注文情報を自動的に更新します。
注文がスキャンされたが実際の支払いが行われていない場合、販売者システムは 1 分間のタイムアウト後にタイムアウト終了として注文情報を自動的に更新します。
注文が正常に支払われたが、ネットワーク上の理由により、加盟店システムが Alipay からのコールバック通知を受信していない場合 (この時点では、ユーザーは正常に支払いましたが、加盟店システムは通知を受け取っていないため、支払い情報は更新が間に合わなかった場合)、主に注文確認インターフェイスを呼び出す必要があります。クエリされた情報がすでに支払われている場合、販売者はローカル注文情報を積極的に更新し、支払いログを記録し、注文ステータスを更新する必要があります。
4. トランザクションを取得するための統一された返金インターフェイス
取引後一定期間内に購入者または販売者による返金が必要な場合、販売者は返金インターフェースを通じて購入者に支払いを返金することができ、Alipayは規定に従って返金を行います。支払いは元のルートで購入者のアカウントに返金されます。
(1) APIプレビュー
- リクエストパラメータ
- 応答パラメータ
(2)返金機能の実現
- 返金情報インターフェースの実装
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
永続層の操作(追加、削除、変更、業務照会)に使用する返金注文情報を注入し、返金注文番号が一致することを条件として返金情報を変更するオブジェクトを作成します。次に、返金情報オブジェクトを作成し、対応する返金ステータスと理由を設定して、更新操作を実行します。
- Alipay決済のサービス層メソッドに返金方法を追加
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
オブジェクトを挿入して返金情報オブジェクトを作成し、次に Alipay トランザクション返金リクエストを作成します。アセンブリ リクエストのパラメータは主に注文番号、注文金額、注文理由に対して組み立てられます。アセンブリが完了したら、対応する実行を実行して取得します。 Alipay からの応答。応答が成功したかどうかに応じて注文番号と返金注文情報を更新します。
- 制御層の関連情報を改善する
@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();
}
}
フロントエンドパラメータを取得した後、バックエンドで返金リクエストを呼び出して返金操作を実行します。
(3) 返金機能のテスト
返金結果表示:
5.統合取得トランザクション返金クエリ
販売者はこのインターフェースを使用して、alipay.trade.refund を通じて送信された返金リクエストが正常に実行されたかどうかを照会できます。
返金インターフェイスがネットワークまたはその他の理由により例外を返した場合、販売者は返金クエリ インターフェイス alipay.trade.fastpay.refund.query (統合取得トランザクション返金クエリ インターフェイス) を呼び出して、指定されたトランザクションの返金情報を問い合わせることができます。
(1) APIプレビュー
- リクエストパラメータ
(2)返金照会機能の実現
- 返金照会機能
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);
}
(3) 試験結果
Swagger でクエリ結果をテストする
6. ステートメントのダウンロード アドレスを問い合わせる
加盟店がアカウントを迅速にチェックできるようにするために、加盟店はこのインターフェイスを通じて加盟店のオフライン請求書のダウンロード アドレスを取得することがサポートされています。
(1) APIプレビュー
- リクエストパラメータ
- 応答パラメータ
(2) 請求書実装のダウンロード
- 請求先住所の取得の実装
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);
}
}
(3) テストダウンロード課金機能
対応する請求情報は次のとおりです。