[TOC]
订单支付如图所示:
在支付页面点击确认支付按钮此时就需要对接第三方支付系统,给用户展示出第三方支付系统的收银台。
查看接口文档:
支付接口地址及返回结果:
get /payment/alipay/submitAlipay/{orderNo}
返回结果:
支付宝支付H5表单
支付宝(中国)网络技术有限公司 [1] 是国内的第三方支付平台,致力于提供“简单、安全、快速”的支付解决方案 [2] 。支付宝公司从2004年建立开始,始终以“信任”作为产品和服务的核心。旗下有“支付宝”与“支付宝钱包”两个独立品牌。自2014年第二季度开始成为当前全球最大的移动支付厂商。
选择手机网站支付:https://open.alipay.com/api/detail?code=I1080300001000041949
手机网站支付是指商家在移动端网页展示商品或服务,用户在商家页面确认使用支付宝支付后,浏览器自动跳转支付宝 App 或支付宝网页完成付款的
支付产品。该产品在签约完成后,需要技术集成方可使用。
签约申请提交材料要求:
注意:需按照要求提交材料,若部分材料不合格,收款额度将受到限制(单笔收款 ≤ 2000 元,单日收款 ≤ 20000 元)。若签约时未能提供相关材料
(如营业执照),请在合约生效后的 30 天内补全,否则会影响正常收款。
收费模式 | 费率 |
---|---|
单笔收费 | 0.6%-1.0% |
特殊行业费率为 1.0%,非特殊行业费率为 0.6%。特殊行业包含:休闲游戏、网络游戏点卡、游戏渠道代理、游戏系统商、网游周边服务、交易平台、网游运营商(含网页游戏)等。
官方文档:https://opendocs.alipay.com/open/203/107084?pathHash=a33de091
整体流程:
为了提供数据传输的安全性,在进行传输的时候需要对数据进行加密:
常见的加密方式:
1、不可逆加密:只能对数据进行加密不能解密
2、可逆加密:可以对数据加密也可以解密
可逆加密可以再细分为:
1、对称加密: 加密和解密使用同一个秘钥
2、非对称加密:加密和解密使用的是不同的秘钥
支付宝为了提供数据传输的安全性使用了两个秘钥对:
沙箱环境使用步骤:
1、注册登录支付宝开放平台
2、进入支付宝开放平台控制台,选择沙箱环境
3、沙箱环境提供的应用已经绑定了相关产品
4、配置应用公钥:
需要下载支付宝秘钥生成器【https://opendocs.alipay.com/common/02khjo】,然后生成秘钥。
注意:使用沙箱买家账号进行测试,提前充值。
官方文档:https://opendocs.alipay.com/open/203/105285?pathHash=ada1de5b
系统交互流程图:
作为我们的项目来讲只需要将支付宝的收银台展示给用户即可,后续支付的动作和我们的系统就没有关系了。支付成功以后,支付宝开放平台会请求我
们系统的接口通知支付结果,我们的系统也可以调用支付宝交易查询接口获取支付结果。
1、官方demo下载地址:https://opendocs.alipay.com/open/203/105910?pathHash=1a2e3a94
2、将访问demo的eclipse项目更改为idea的maven项目(jdk8)
3、在AlipayConfig类中填写参数信息
4、启动项目进行测试
在spzx-modules模块下新建子模块spzx-payment
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.spzx</groupId>
<artifactId>spzx-modules</artifactId>
<version>3.6.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spzx-payment</artifactId>
<description>
spzx-payment支付模块
</description>
<dependencies>
<!-- SpringCloud Alibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- SpringCloud Alibaba Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- SpringBoot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Mysql Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- spzx Common DataScope -->
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-common-datascope</artifactId>
</dependency>
<!-- spzx Common Log -->
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-common-log</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
在resources目录下新建banner.txt
Spring Boot Version: ${spring-boot.version}
Spring Application Name: ${spring.application.name}
_ _
(_) | |
_ __ _ _ ___ _ _ _ ______ ___ _ _ ___ | |_ ___ _ __ ___
| '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \
| | | |_| || (_) || |_| || | \__ \| |_| |\__ \| |_ | __/| | | | | |
|_| \__,_| \___/ \__, ||_| |___/ \__, ||___/ \__| \___||_| |_| |_|
__/ | __/ |
|___/ |___/
在resources目录下新建bootstrap.yml
# Tomcat
server:
port: 9210
# Spring
spring:
application:
# 应用名称
name: spzx-payment
profiles:
# 环境配置
active: dev
main:
allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: 192.168.100.131:8848
config:
# 配置中心地址
server-addr: 192.168.100.131:8848
# 配置文件格式
file-extension: yml
# 共享配置
shared-configs:
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
在nacos上添加商品服务配置文件
mybatis-plus:
mapper-locations: classpath*:mapper/**/*Mapper.xml
type-aliases-package: com.spzx.**.domain
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志
# spring配置
spring:
data:
redis:
host: localhost
port: 6379
password:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
publisher-confirm-type: CORRELATED
publisher-returns: true
listener:
simple:
cknowledge-mode: manual #默认情况下消息消费者是自动确认消息的,如果要手动确认消息则需要修改确认模式为manual
prefetch: 1 # 消费者每次从队列获取的消息数量。此属性当不设置时为:轮询分发,设置为1为:公平分发
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/spzx-payment?characterEncoding=utf-8&useSSL=false
username: root
password: root
hikari:
connection-test-query: SELECT 1
connection-timeout: 60000
idle-timeout: 500000
max-lifetime: 540000
maximum-pool-size: 10
minimum-idle: 5
pool-name: GuliHikariPool
在resources目录下新建logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志存放路径 -->
<property name="log.path" value="logs/spzx-payment" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.spzx" level="info" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
</configuration>
添加启动类
package com.spzx.payment;
/**
* 购物车模块
*
*/
@EnableCustomConfig
@EnableRyFeignClients
@SpringBootApplication
public class SpzxPaymentApplication
{
public static void main(String[] args)
{
SpringApplication.run(SpzxPaymentApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 系统模块启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \n" +
" | ( ' ) | \\ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\\ \\ | || |(_,_)' \n" +
" | | \\ `' /| `-' / \n" +
" | | \\ / \\ / \n" +
" ''-' `'-' `-..-' ");
}
}
在spzx-gateway-dev.yml配置文件中添加会员服务的网关信息
# 支付服务
- id: spzx-payment
uri: lb://spzx-payment
predicates:
- Path=/payment/**
filters:
- StripPrefix=1
订单支付需要调用订单服务接口
在spzx-api模块下新建子模块spzx-api-order
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.spzx</groupId>
<artifactId>spzx-api</artifactId>
<version>3.6.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spzx-api-order</artifactId>
<description>
spzx-api-order订单接口模块
</description>
<dependencies>
<!-- spzx Common Core-->
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-common-core</artifactId>
</dependency>
</dependencies>
</project>
spzx-modules模块引入api依赖
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-api-order</artifactId>
<version>3.6.3</version>
</dependency>
操作模块:spzx-order
@Operation(summary = "根据订单号获取订单信息")
@InnerAuth
@GetMapping("/getByOrderNo/{orderNo}")
public R<OrderInfo> getByOrderNo(@PathVariable String orderNo) {
OrderInfo orderInfo = orderInfoService.getByOrderNo(orderNo);
return R.ok(orderInfo);
}
OrderInfo getByOrderNo(String orderNo);
@Override
public OrderInfo getByOrderNo(String orderNo) {
//订单
OrderInfo orderInfo = baseMapper.selectOne(
new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo, orderNo)
);
//订单详情
List<OrderItem> orderItemList = orderItemMapper.selectList(
new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderId, orderInfo.getId())
);
orderInfo.setOrderItemList(orderItemList);
return orderInfo;
}
操作模块:spzx-api-order
**注意:先将spzx-order模块中的OrderInfo类和OrderItem类移动到spzx-api-order 模块 **
package com.spzx.order.api;
@FeignClient(
contextId = "remoteUserInfoService",
value = ServiceNameConstants.ORDER_SERVICE,
fallbackFactory = RemoteOrderInfoFallbackFactory.class
)
public interface RemoteOrderInfoService{
@GetMapping("/orderInfo/getByOrderNo/{orderNo}")
public R<OrderInfo> getByOrderNo(
@PathVariable("orderNo") String orderNo,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source
);
}
/**
* 订单服务的serviceid
*/
public static final String ORDER_SERVICE = "spzx-order";
package com.spzx.order.api.factory;
@Slf4j
public class RemoteOrderInfoFallbackFactory implements FallbackFactory<RemoteOrderInfoService>
{
@Override
public RemoteOrderInfoService create(Throwable throwable)
{
log.error("订单服务调用失败:{}", throwable.getMessage());
return new RemoteOrderInfoService()
{
@Override
public R<OrderInfo> getByOrderNo(String orderNo, String source) {
return R.fail("根据订单号获取订单信息失败:" + throwable.getMessage());
}
};
}
}
resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.spzx.order.api.factory.RemoteOrderInfoFallbackFactory
生成代码
操作模块:spzx-payment
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
</dependency>
在nacos上添加支付宝所需要的参数
正式服务器版本:
alipay:
alipay_url: https://openapi.alipay.com/gateway.do
app_id: 2021001163617452
app_private_key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8Z7EZmanxyFGsK4LrIUeKKrrGxWAHIgPmUV8TtZDs+jeplJSw1ckSY63QhEU444D5qd6xruJHBuB33HG+ik4n8N8nRWi3AtMgpC061oq2DcgtIKMmQHO7/poYDwbpDZrOWXIyiNshFfUOSTUpnrS8UvEks6n6xR/G72r2FG07oZzO7g3XsPMr73wpYajMYC/bhTm6CJGEWZikONNDFkQpVHa+zgitwsqlBuvBvVwGwOHA9B8aRfokwAMl6BDXKoH8BNnSEMpWSTRSwbssayXAQWNU7XKDKGozbn4U2dEbl8GCFzikI/T7ybTNm5gs46ZZBGlq/YB4+v4D3t74Vl6nAgMBAAECggEAOidzhehliYkAlLk1huhV0bMQxewEkQ8RzxTM2SORIWS2q7R+FPtYPkHgU92QFFg85lNltsi5dZ0MylKUFXFRYIi8CL4m7V6E1q12fJPeawVkBXHuig8Y6i1TWRvCUUtuvkTjt++AW/0QECHOtBMVzI95eY+vZwVToq8h/+UcNmxKyVt66Qpo4+r+cUvlvGX5mXgQVC5Ftf/MtHA1i+kjtzBITC0xAvmSXKzjN1YhtcS9rXyMHXBiFhXLdmvOXjkn0Okosr2+tmesXfSwDGhH3ZlOdHzit4D602RNl0nTA1dOUWHuCncs1TrWbriax86P/EYvmzMiHWCVTmmNJC0bMQKBgQD0HAXKNsYsdjCQOV4t3SMqOKaul67x/KA20PmMZVfQ2sQkyjyFgWpL8C16Rzf3zI7df+zF5SkvhFY4+LRZVwX5okEFYTzAZ/NYouj1/DABYOPq0E0sY18/xtq7FJ/CIk8qmCqcczqoyaoxoaC1zAt9E4CYE89iEOnO+GhcI3H3LwKBgQDFlQzvbXhWRyRFkeft/a52XLnyj6t9iP7wNGbGCSeoMDrAu3ZgoqacUPWj5MgSFZdT48H9rF4pPixXoe3jfUNsWBUHqD1F2drDz7lpL0PbpSsgy6ei+D4RwTADsuyXwrkvrWrGro+h6pNJFyly3nea/gloDtJTzfhFFwtNfmqyCQKBgBXzMx4UwMscsY82aV6MZO4V+/71CrkdszZaoiXaswPHuB1qxfhnQ6yiYyR8pO62SR5ns120Fnj8WFh1HJpv9cyVp20ZakIO1tXgiDweOh7VnIjvxBC6usTcV6y81QS62w2Ec0hwIBUvVQtzciUGvP25NDX4igxSYwPGWHP4h/XnAoGAcQN2aKTnBgKfPqPcU4ac+drECXggESgBGof+mRu3cT5U/NS9Oz0Nq6+rMVm1DpMHAdbuqRikq1aCqoVWup51qE0hikWy9ndL6GCynvWIDOSGrLWQZ2kyp5kmy5bWOWAJ6Ll6r7Y9NdIk+NOkw614IFFaNAj2STUw4uPxdRvwD3ECgYEArwOZxR3zl/FZfsvVCXfK8/fhuZXMOp6Huwqky4tNpVLvOyihpOJOcIFj6ZJhoVdmiL8p1/1S+Sm/75gx1tpFurKMNcmYZbisEC7Ukx7RQohZhZTqMPgizlVBTu5nR3xkheaJC9odvyjrWQJ569efXo30gkW04aBp7A15VNG5Z/U=
alipay_public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkWs+3gXMosiWG+EbfRyotWB0waqU3t7qMQSBxU0r3JZoND53jvWQfzrGZ8W+obMc+OgwupODDVxhG/DEKVBIptuUQYdvAjCSH98m2hclFcksspuCy9xS7PyflPE47pVzS6vA3Slvw5OFQ2qUcku4paWnBxguLUGPjEncij5NcyFyk+/k57MmrVJwCZaI+lFOS3Eq2IXc07tWXO4s/2SWr3EJiwJutOGBdA1ddvv1Urrl0pWpEFg30pJB6J7YteuxdEL90kuO5ed/vnTK5qgQRvEelROkUW44xONk1784v28OJXmGICmNL1+KyM/SFbFOSgJZSV1tEXUzvL/xvzFpLwIDAQAB
return_payment_url: http://ry-spzx.atguigu.cn/#/pages/money/paySuccess
notify_payment_url: http://ry-spzx-api.atguigu.cn/payment/alipay/callback/notify
沙箱版本
alipay:
alipay_url: 沙箱支付宝网关地址
app_id: 沙箱APPID
app_private_key: 应用私钥
alipay_public_key: 支付宝公钥
return_payment_url: http://app前端服务器/#/pages/money/paySuccess
notify_payment_url: http://内网穿透地址/payment/alipay/callback/notify
package com.spzx.payment.config;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AlipayConfig {
@Value("${alipay.alipay_url}")
private String alipay_url;
@Value("${alipay.app_private_key}")
private String app_private_key;
public final static String format="json";
public final static String charset="utf-8";
public final static String sign_type="RSA2";
public static String return_payment_url;
public static String notify_payment_url;
public static String alipay_public_key;
public static String app_id;
@Value("${alipay.app_id}")
public void setApp_id(String app_id) {
AlipayConfig.app_id = app_id;
}
@Value("${alipay.alipay_public_key}")
public void setAlipay_public_key(String alipay_public_key) {
AlipayConfig.alipay_public_key = alipay_public_key;
}
@Value("${alipay.return_payment_url}")
public void setReturn_url(String return_payment_url) {
AlipayConfig.return_payment_url = return_payment_url;
}
@Value("${alipay.notify_payment_url}")
public void setNotify_url(String notify_payment_url) {
AlipayConfig.notify_payment_url = notify_payment_url;
}
@Bean
public AlipayClient alipayClient(){
AlipayClient alipayClient=new DefaultAlipayClient(alipay_url,app_id,app_private_key,format,charset, alipay_public_key,sign_type );
return alipayClient;
}
}
package com.spzx.payment.controller;
@Slf4j
@Controller
@RequestMapping("/alipay")
public class AlipayController extends BaseController {
@Autowired
private IAlipayService alipayService;
@Operation(summary = "支付宝下单")
@RequiresLogin
@RequestMapping("/submitAlipay/{orderNo}")
@ResponseBody
public AjaxResult submitAlipay(@PathVariable(value = "orderNo") String orderNo) {
String form = alipayService.submitAlipay(orderNo);
return success(form);
}
}
package com.spzx.payment.service;
public interface IAlipayService{
String submitAlipay(String orderNo);
}
支付宝示例demo:https://opendocs.alipay.com/open/203/105285?pathHash=ada1de5b
package com.spzx.payment.service.impl;
@Service
@Slf4j
public class AlipayServiceImpl implements IAlipayService {
@Autowired
private AlipayClient alipayClient;
@Autowired
private IPaymentInfoService paymentInfoService;
@SneakyThrows
@Override
public String submitAlipay(String orderNo) {
//保存支付记录
PaymentInfo paymentInfo = paymentInfoService.savePaymentInfo(orderNo);
// 创建请求对象
AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
// 同步回调
alipayRequest.setReturnUrl(AlipayConfig.return_payment_url);
// 异步回调
alipayRequest.setNotifyUrl(AlipayConfig.notify_payment_url);
/******必传参数******/
JSONObject bizContent = new JSONObject();
//商户订单号,商家自定义,保持唯一性
bizContent.put("out_trade_no", paymentInfo.getOrderNo());
//支付金额,最小值0.01元
//bizContent.put("total_amount", paymentInfo.getAmount());
bizContent.put("total_amount", 0.01);
//订单标题,不可使用特殊符号
bizContent.put("subject", paymentInfo.getContent());
/******可选参数******/
//手机网站支付默认传值QUICK_WAP_WAY
bizContent.put("product_code", "QUICK_WAP_WAY");
alipayRequest.setBizContent(bizContent.toString());
AlipayTradeWapPayResponse response = alipayClient.pageExecute(alipayRequest,"POST");
return response.getBody(); //调用SDK生成表单;
}
}
PaymentInfo savePaymentInfo(String orderNo);
@Autowired
private RemoteOrderInfoService remoteOrderInfoService;
@Override
public PaymentInfo savePaymentInfo(String orderNo) {
//防止重复提交:如果支付日志已存在则直接返回
PaymentInfo paymentInfo = baseMapper.selectOne(
new LambdaQueryWrapper<PaymentInfo>().eq(PaymentInfo::getOrderNo, orderNo)
);
if(paymentInfo != null) {
return paymentInfo;
}
//根据订单号获取订单信息
R<OrderInfo> orderInfoResult = remoteOrderInfoService.getByOrderNo(orderNo, SecurityConstants.INNER);
if (R.FAIL == orderInfoResult.getCode()) {
throw new ServiceException(orderInfoResult.getMsg());
}
OrderInfo orderInfo = orderInfoResult.getData();
paymentInfo = new PaymentInfo();
paymentInfo.setUserId(orderInfo.getUserId());
String content = orderInfo.getOrderItemList()
.stream()
// 将每个 OrderItem 变换为其 SKU 名称
.map(OrderItem::getSkuName)
// 使用空格连接所有 SKU 名称
.collect(Collectors.joining(" "));
paymentInfo.setContent(content);
paymentInfo.setAmount(orderInfo.getTotalAmount());
paymentInfo.setOrderNo(orderNo);
paymentInfo.setPaymentStatus(0);
baseMapper.insert(paymentInfo);
return paymentInfo;
}
操作模块:spzx-payment
@RequestMapping("/callback/notify")
@ResponseBody
public String alipayNotify(@RequestParam Map<String, String> paramMap, HttpServletRequest request) {
log.info("AlipayController...alipayNotify方法执行了...");
return "success" ;
}
# 不校验白名单
ignore:
whites:
...
- /payment/alipay/callback/*
当支付成功以后支付宝无法调用本地接口,因为本地接口是位于一个私有IP地址范围内,并且被路由器或防火墙等设备保护起来。这个私有的网络设备无法直接从公共网络访问,该问题的解决可以使用内网穿透技术。
内网穿透:内网穿透(Intranet Port Forwarding)是一种将本地网络中的服务暴露给公共网络访问的技术。
内网穿透通过在公共网络上建立一个中转服务器,使得公共网络上的设备可以通过该中转服务器访问内网中的设备和服务。具体而言,内网穿透技术允
许您在公共网络上使用一个公网IP地址和端口号来映射到内网中的某个设备或服务的私有IP地址和端口号。
常见的内网穿透工具包括natapp、Ngrok、frp、花生壳等。
官网地址:https://natapp.cn/
试用步骤:
1、注册用户
2、购买隧道
3、购买二级域名,绑定隧道
4、下载客户端
5、客户端使用教程:https://natapp.cn/article/nohup
natapp.exe -authtoken=xxxxx
authtoken信息获取:
官网地址:https://opendocs.alipay.com/open/203/105286?pathHash=022a439c&ref=api
当用户支付成功以后,支付宝系统会调用我们系统的接口通知支付结果
操作模块:spzx-payment
添加依赖
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-common-rabbit</artifactId>
<version>3.6.3</version>
</dependency>
@Autowired
private IPaymentInfoService paymentInfoService;
@Autowired
private RemoteOrderInfoService remoteOrderInfoService;
@Autowired
private RabbitService rabbitService;
@PostMapping("/callback/notify")
@ResponseBody
public String alipayNotify(@RequestParam Map<String, String> paramMap) {
log.info("alipayNotify方法执行了...");
String result = "failure";
try {
//调用SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(
paramMap,
AlipayConfig.alipay_public_key,
AlipayConfig.charset,
AlipayConfig.sign_type
);
//校验验签是否成功
String outTradeNo = paramMap.get("out_trade_no");
if(!signVerified){
log.error("订单 {} 验签失败", outTradeNo);
return result;
}
log.error("验签成功!");
//商家需要验证该通知数据中的 out_trade_no 是否为商家系统中创建的订单号。
R<OrderInfo> orderInfoResult = remoteOrderInfoService.getByOrderNo(outTradeNo, SecurityConstants.INNER);
if(R.FAIL == orderInfoResult.getCode()){
log.error("远程获取订单 {} 失败", outTradeNo);
return result;
}
if(orderInfoResult.getData() == null){
log.error("订单 {} 不存在", outTradeNo);
return result;
}
OrderInfo orderInfo = orderInfoResult.getData();
// 判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)。
String totalAmount = paramMap.get("total_amount");
if(orderInfo.getTotalAmount().compareTo(new BigDecimal(totalAmount)) != 0){
log.error("订单 {} 金额不一致", outTradeNo);
return result;
}
// 校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方(有的时候,一个商家可能有多个 seller_id/seller_email)。
// "2088721032347805" 沙箱中的 商户PID
String sellerId = paramMap.get("seller_id");
if(!"2088721032347805".equals(sellerId)){
log.error("订单 {} 商家不一致", outTradeNo);
return result;
}
//验证 app_id 是否为该商家本身。
String appId = paramMap.get("app_id");
if(!AlipayConfig.app_id.equals(appId)){
log.error("订单 {} appid不一致", outTradeNo);
return result;
}
// 交易状态
String tradeStatus = paramMap.get("trade_status");
if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
// 正常的支付成功,我们应该更新交易记录状态
paymentInfoService.updatePaymentStatus(paramMap, 2);
//基于MQ通知订单系统,修改订单状态
rabbitService.sendMessage(MqConst.EXCHANGE_PAYMENT_PAY, MqConst.ROUTING_PAYMENT_PAY, paymentInfo.getOrderNo());
//基于MQ通知商品系统,扣减库存
rabbitService.sendMessage(MqConst.EXCHANGE_PRODUCT, MqConst.ROUTING_MINUS, outTradeNo);
return "success";
}
} catch (AlipayApiException e) {
e.printStackTrace();
}
return result;
}
更新支付信息
void updatePaymentStatus(Map<String, String> map, Integer payType);
@Transactional(rollbackFor = Exception.class)
@Override
public void updatePaymentStatus(Map<String, String> map, Integer payType) {
PaymentInfo paymentInfo = baseMapper.selectOne(
new LambdaQueryWrapper<PaymentInfo>()
.eq(PaymentInfo::getOrderNo, map.get("out_trade_no"))
);
//已支付,直接返回
if (paymentInfo.getPaymentStatus() == 1) {
return;
}
//更新支付信息
paymentInfo.setPayType(payType);
paymentInfo.setPaymentStatus(1);
paymentInfo.setTradeNo(map.get("trade_no"));
paymentInfo.setCallbackTime(new Date());
paymentInfo.setCallbackContent(JSON.toJSONString(map));
baseMapper.updateById(paymentInfo);
}
操作模块:spzx-order
订单支付成功后,我们已经更改了订单支付记录状态,接下来我还有更改订单状态,因为他们是不同的微服务模块,所以我们采用消息队列的方式,保证数据最终一致性;
package com.spzx.order.receiver;
@Slf4j
@Component
public class OrderReceiver {
@Autowired
private IOrderInfoService orderInfoService;
/**
* 监听订单支付成功消息;更新订单状态
*
* @param orderNo
* @param message
* @param channel
*/
@SneakyThrows
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = MqConst.EXCHANGE_PAYMENT_PAY, durable = "true"),
value = @Queue(value = MqConst.QUEUE_PAYMENT_PAY, durable = "true"),
key = MqConst.ROUTING_PAYMENT_PAY
))
public void processPaySucess(String orderNo, Message message, Channel channel) {
//业务处理
if (StringUtils.isNotEmpty(orderNo)) {
log.info("[订单服务]监听订单支付成功消息:{}", orderNo);
//更改订单支付状态
orderInfoService.processPaySuccess(orderNo);
}
//手动应答
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
void processPaySuccess(String orderNo);
@Override
public void processPaySuccess(String orderNo) {
//获取订单信息
OrderInfo orderInfo = orderInfoMapper.selectOne(
new LambdaQueryWrapper<OrderInfo>()
.eq(OrderInfo::getOrderNo, orderNo)
.select(OrderInfo::getId, OrderInfo::getOrderStatus)
);
if(orderInfo.getOrderStatus().intValue() == 0) {
orderInfo.setOrderStatus(1);//已支付
orderInfo.setPaymentTime(new Date());
orderInfoMapper.updateById(orderInfo);
}
}
订单模块除了更改订单支付状态,还要发送消息通知商品服务模块扣减库存
/**
* 扣减库存
* @param orderNo 订单号
*/
@SneakyThrows
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = MqConst.EXCHANGE_PRODUCT, durable = "true"),
value = @Queue(value = MqConst.QUEUE_MINUS, durable = "true"),
key = {MqConst.ROUTING_MINUS}
))
public void minus(String orderNo, Channel channel, Message message) {
//业务处理
if (StringUtils.isNotEmpty(orderNo)){
log.info("[商品服务]监听减库存消息:{}", orderNo);
//扣减库存
productService.minus(orderNo);
}
//手动应答
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
void minus(String orderNo);
@Transactional(rollbackFor = {Exception.class})
@Override
public void minus(String orderNo) {
//幂等性处理
String key = "sku:minus:" + orderNo;
//业务去重,防止重复消费
Boolean isExist = redisTemplate.opsForValue().setIfAbsent(key, orderNo, 1, TimeUnit.HOURS);
if(!isExist) return;
// 获取锁定库存的缓存信息
String dataKey = "sku:lock:data:" + orderNo;
List<SkuLockVo> skuLockVoList = (List<SkuLockVo>)this.redisTemplate.opsForValue().get(dataKey);
if (CollectionUtils.isEmpty(skuLockVoList)){
return ;
}
// 减库存
skuLockVoList.forEach(skuLockVo -> {
int row = skuStockMapper.minus(skuLockVo.getSkuId(), skuLockVo.getSkuNum());
});
// 扣减库存之后,删除锁定库存的缓存。
this.redisTemplate.delete(dataKey);
}
Integer minus(@Param("skuId") Long skuId, @Param("num")Integer num);
<update id="minus">
update sku_stock
set lock_num = lock_num - #{num}, total_num = total_num - #{num}, sale_num = sale_num + #{num}
where sku_id = #{skuId}
</update>