[TOC] # 库存&支付 ## 1 支付宝支付 ### 1.1 需求说明 订单支付如图所示: pay 在支付页面点击**确认支付**按钮此时就需要对接第三方支付系统,给用户展示出第三方支付系统的收银台。 **查看接口文档:** 支付接口地址及返回结果: ```json get /payment/alipay/submitAlipay/{orderNo} 返回结果: 支付宝支付H5表单 ``` ### 1.2 支付宝支付 官网地址:https://open.alipay.com/ [支付宝(中国)网络技术有限公司](https://baike.baidu.com/item/支付宝(中国)网络技术有限公司/23241708) [1] 是国内的第三方支付平台,致力于提供“简单、安全、快速”的支付解决方案 [2] 。支付宝公司从2004年建立开始,始终以“信任”作为产品和服务的核心。旗下有“支付宝”与“支付宝钱包”两个独立品牌。自2014年第二季度开始成为当前全球最大的[移动支付](https://baike.baidu.com/item/移动支付/565488)厂商。 #### 1.2.1 产品介绍 ##### 1 产品特色 选择手机网站支付:https://open.alipay.com/api/detail?code=I1080300001000041949 ![image-20230709160020998](images/image-20230709160020998-17320303915883.png) 手机网站支付是指商家在移动端网页展示商品或服务,用户在商家页面确认使用支付宝支付后,浏览器自动跳转支付宝 App 或支付宝网页完成付款的 支付产品。该产品在签约完成后,需要技术集成方可使用。 ##### 2 使用示例 ![image](images/image-17320303915884.png) ##### 3 申请条件 支持的账号类型:[支付宝企业账号](https://opendocs.alipay.com/common/02kkum)、[支付宝个人账号](https://opendocs.alipay.com/common/02kg61)。 签约申请提交材料要求: - 提供网站地址,网站能正常访问且页面显示完整,网站需要明确经营内容且有完整的商品信息。 - 网站必须通过 ICP 备案,且备案主体需与支付宝账号主体一致。若网站备案主体与当前账号主体不同时需上传授权函。 - 个人账号申请,需提供营业执照,且支付宝账号名称需与营业执照主体一致。 **注意**:需按照要求提交材料,若部分材料不合格,收款额度将受到限制(单笔收款 ≤ 2000 元,单日收款 ≤ 20000 元)。若签约时未能提供相关材料 (如营业执照),请在合约生效后的 30 天内补全,否则会影响正常收款。 ##### 4 费率 | **收费模式** | **费率** | | ------------ | --------- | | 单笔收费 | 0.6%-1.0% | 特殊行业费率为 1.0%,非特殊行业费率为 0.6%。特殊行业包含:休闲游戏、网络游戏点卡、游戏渠道代理、游戏系统商、网游周边服务、交易平台、网游运营商(含网页游戏)等。 #### 1.2.2 接入准备 官方文档:https://opendocs.alipay.com/open/203/107084?pathHash=a33de091 整体流程: ![image-20230709170008020](images/image-20230709170008020-17320303915885.png) 为了提供数据传输的安全性,在进行传输的时候需要对数据进行加密: 常见的加密方式: 1、不可逆加密:只能对数据进行加密不能解密 2、可逆加密:可以对数据加密也可以解密 可逆加密可以再细分为: 1、对称加密: 加密和解密使用同一个秘钥 ![image-20230709170626255](images/image-20230709170626255-17320303915896.png) 2、非对称加密:加密和解密使用的是不同的秘钥 - 私钥加密公钥解密:身份认证 - 公钥加密私钥解密:加密 ![image-20230709170703058](images/image-20230709170703058-17320303915897.png) 支付宝为了提供数据传输的安全性使用了两个秘钥对: ![image-20230709170850260](images/image-20230709170850260-17320303915898.png) 沙箱环境使用步骤: 1、注册登录支付宝开放平台 2、进入支付宝开放平台控制台,选择沙箱环境 ![image-20230709165323586](images/image-20230709165323586.png) 3、沙箱环境提供的应用已经绑定了相关产品 4、配置应用公钥: ![image-20230130234642091](images/image-20230130234642091-17320303915899.png) 需要下载支付宝秘钥生成器【https://opendocs.alipay.com/common/02khjo】,然后生成秘钥。 **注意**:使用沙箱买家账号进行测试,提前充值。 #### 1.2.3 手机网站支付快速接入 官方文档:https://opendocs.alipay.com/open/203/105285?pathHash=ada1de5b 系统交互流程图: ![image-20230709164753985](images/image-20230709164753985-173203039158910.png) 作为我们的项目来讲只需要将支付宝的收银台展示给用户即可,后续支付的动作和我们的系统就没有关系了。支付成功以后,支付宝开放平台会请求我 们系统的接口通知支付结果,我们的系统也可以调用支付宝交易查询接口获取支付结果。 #### 1.2.4 官方demo 1、官方demo下载地址:https://opendocs.alipay.com/open/203/105910?pathHash=1a2e3a94 2、将访问demo的eclipse项目更改为idea的maven项目(jdk8) 3、在AlipayConfig类中填写参数信息 4、启动项目进行测试 ### 1.3 搭建支付模块 #### 1.3.1 新建模块 在spzx-modules模块下新建子模块spzx-payment #### 1.3.2 pom.xml ```xml com.spzx spzx-modules 3.6.3 4.0.0 spzx-payment spzx-payment支付模块 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config com.alibaba.cloud spring-cloud-starter-alibaba-sentinel org.springframework.boot spring-boot-starter-actuator com.mysql mysql-connector-j com.spzx spzx-common-datascope com.spzx spzx-common-log ${project.artifactId} org.springframework.boot spring-boot-maven-plugin repackage org.apache.maven.plugins maven-compiler-plugin 17 17 ``` #### 1.3.3 banner.txt 在resources目录下新建banner.txt ```text Spring Boot Version: ${spring-boot.version} Spring Application Name: ${spring.application.name} _ _ (_) | | _ __ _ _ ___ _ _ _ ______ ___ _ _ ___ | |_ ___ _ __ ___ | '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \ | | | |_| || (_) || |_| || | \__ \| |_| |\__ \| |_ | __/| | | | | | |_| \__,_| \___/ \__, ||_| |___/ \__, ||___/ \__| \___||_| |_| |_| __/ | __/ | |___/ |___/ ``` #### 1.3.4 bootstrap.yml 在resources目录下新建bootstrap.yml ```yaml # 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} ``` #### 1.3.5 spzx-payment-dev.yml 在nacos上添加商品服务配置文件 ```yaml 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 ``` #### 1.3.6 logback.xml 在resources目录下新建logback.xml ```xml ${log.pattern} ${log.path}/info.log ${log.path}/info.%d{yyyy-MM-dd}.log 60 ${log.pattern} INFO ACCEPT DENY ${log.path}/error.log ${log.path}/error.%d{yyyy-MM-dd}.log 60 ${log.pattern} ERROR ACCEPT DENY ``` #### 1.3.7 SpzxPaymentApplication 添加启动类 ```java 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" + " ''-' `'-' `-..-' "); } } ``` #### 1.3.8 配置网关 在spzx-gateway-dev.yml配置文件中添加会员服务的网关信息 ```yaml # 支付服务 - id: spzx-payment uri: lb://spzx-payment predicates: - Path=/payment/** filters: - StripPrefix=1 ``` ### 1.4 搭建订单服务远程接口模块 订单支付需要调用订单服务接口 #### 1.4.1 新建模块 在spzx-api模块下新建子模块spzx-api-order #### 1.4.2 pom.xml ```xml com.spzx spzx-api 3.6.3 4.0.0 spzx-api-order spzx-api-order订单接口模块 com.spzx spzx-common-core ``` #### 1.4.3 spzx-modules模块引入api依赖 spzx-modules模块引入api依赖 ```xml com.spzx spzx-api-order 3.6.3 ``` ### 1.5 根据订单号获取订单信息 操作模块:`spzx-order` #### 1.5.1 远程调用接口开发 ##### 1 OrderInfoController ```java @Operation(summary = "根据订单号获取订单信息") @InnerAuth @GetMapping("/getByOrderNo/{orderNo}") public R getByOrderNo(@PathVariable String orderNo) { OrderInfo orderInfo = orderInfoService.getByOrderNo(orderNo); return R.ok(orderInfo); } ``` ##### 2 IOrderInfoService ```java OrderInfo getByOrderNo(String orderNo); ``` ##### 3 OrderInfoServiceI ```java @Override public OrderInfo getByOrderNo(String orderNo) { //订单 OrderInfo orderInfo = baseMapper.selectOne( new LambdaQueryWrapper().eq(OrderInfo::getOrderNo, orderNo) ); //订单详情 List orderItemList = orderItemMapper.selectList( new LambdaQueryWrapper().eq(OrderItem::getOrderId, orderInfo.getId()) ); orderInfo.setOrderItemList(orderItemList); return orderInfo; } ``` #### 1.5.2 openFeign接口定义 操作模块:`spzx-api-order` **注意:先将spzx-order模块中的OrderInfo类和OrderItem类移动到spzx-api-order 模块 ** ##### 1 RemoteOrderInfoService ```java package com.spzx.order.api; @FeignClient( contextId = "remoteUserInfoService", value = ServiceNameConstants.ORDER_SERVICE, fallbackFactory = RemoteOrderInfoFallbackFactory.class ) public interface RemoteOrderInfoService{ @GetMapping("/orderInfo/getByOrderNo/{orderNo}") public R getByOrderNo( @PathVariable("orderNo") String orderNo, @RequestHeader(SecurityConstants.FROM_SOURCE) String source ); } ``` ##### 2 ServiceNameConstants ```java /** * 订单服务的serviceid */ public static final String ORDER_SERVICE = "spzx-order"; ``` ##### 3 RemoteOrderInfoFallbackFactory ```java package com.spzx.order.api.factory; @Slf4j public class RemoteOrderInfoFallbackFactory implements FallbackFactory { @Override public RemoteOrderInfoService create(Throwable throwable) { log.error("订单服务调用失败:{}", throwable.getMessage()); return new RemoteOrderInfoService() { @Override public R getByOrderNo(String orderNo, String source) { return R.fail("根据订单号获取订单信息失败:" + throwable.getMessage()); } }; } } ``` ##### 4 加载配置类 resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ``` com.spzx.order.api.factory.RemoteOrderInfoFallbackFactory ``` ### 1.6 代码生成 生成代码 ### 1.7 支付接口 操作模块:`spzx-payment` #### 1.7.1 引入依赖 ```xml com.alipay.sdk alipay-sdk-java ``` #### 1.7.2 spzx-payment-dev.yml 在nacos上添加支付宝所需要的参数 正式服务器版本: ```yaml 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 ``` 沙箱版本 ```yaml 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 ``` #### 1.7.3 AlipayConfig ```java 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; } } ``` #### 1.7.4 AlipayController ```java 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); } } ``` #### 1.7.5 IAlipayService ```java package com.spzx.payment.service; public interface IAlipayService{ String submitAlipay(String orderNo); } ``` #### 1.7.6 AlipayServiceImpl 支付宝示例demo:https://opendocs.alipay.com/open/203/105285?pathHash=ada1de5b ```java 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生成表单; } } ``` #### 1.7.7 IPaymentInfoService ```java PaymentInfo savePaymentInfo(String orderNo); ``` #### 1.7.8 PaymentInfoServiceImpl ```java @Autowired private RemoteOrderInfoService remoteOrderInfoService; @Override public PaymentInfo savePaymentInfo(String orderNo) { //防止重复提交:如果支付日志已存在则直接返回 PaymentInfo paymentInfo = baseMapper.selectOne( new LambdaQueryWrapper().eq(PaymentInfo::getOrderNo, orderNo) ); if(paymentInfo != null) { return paymentInfo; } //根据订单号获取订单信息 R 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; } ``` ### 1.8 支付宝异步回调 操作模块:`spzx-payment` #### 1.8.1 AlipayController ```java @RequestMapping("/callback/notify") @ResponseBody public String alipayNotify(@RequestParam Map paramMap, HttpServletRequest request) { log.info("AlipayController...alipayNotify方法执行了..."); return "success" ; } ``` #### 1.8.2 配置网关 ```yaml # 不校验白名单 ignore: whites: ... - /payment/alipay/callback/* ``` #### 1.8.3 内网穿透介绍 当支付成功以后支付宝无法调用本地接口,因为本地接口是位于一个私有IP地址范围内,并且被路由器或防火墙等设备保护起来。这个私有的网络设备无法直接从公共网络访问,该问题的解决可以使用内网穿透技术。 **内网穿透**:内网穿透(Intranet Port Forwarding)是一种将本地网络中的服务暴露给公共网络访问的技术。 内网穿透通过在公共网络上建立一个中转服务器,使得公共网络上的设备可以通过该中转服务器访问内网中的设备和服务。具体而言,内网穿透技术允 许您在公共网络上使用一个公网IP地址和端口号来映射到内网中的某个设备或服务的私有IP地址和端口号。 ![image-20230710185519318](images/image-20230710185519318-173203039158911.png) 常见的内网穿透工具包括**natapp**、Ngrok、frp、花生壳等。 官网地址:https://natapp.cn/ 试用步骤: 1、注册用户 2、购买隧道 image-20230219170135784 3、购买二级域名,绑定隧道 ![image-20230219170206186](images/image-20230219170206186-173203039158913.png) 4、下载客户端 ![image-20230219170337316](images/image-20230219170337316-173203039159014.png) 5、客户端使用教程:https://natapp.cn/article/nohup ```java natapp.exe -authtoken=xxxxx ``` authtoken信息获取: ![image-20230219171016742](images/image-20230219171016742-173203039159015.png) #### 1.8.4 异步通知说明 官网地址:https://opendocs.alipay.com/open/203/105286?pathHash=022a439c&ref=api ![image-20230710180507583](images/image-20230710180507583-173203039159016.png) 当用户支付成功以后,支付宝系统会调用我们系统的接口通知支付结果 #### 1.8.5 验证签名 操作模块:`spzx-payment` ##### pom.xml 添加依赖 ```xml com.spzx spzx-common-rabbit 3.6.3 ``` ##### AlipayController ```java @Autowired private IPaymentInfoService paymentInfoService; @Autowired private RemoteOrderInfoService remoteOrderInfoService; @Autowired private RabbitService rabbitService; @PostMapping("/callback/notify") @ResponseBody public String alipayNotify(@RequestParam Map 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 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; } ``` ##### IPaymentInfoService 更新支付信息 ```java void updatePaymentStatus(Map map, Integer payType); ``` ##### PaymentInfoServiceImpl ```java @Transactional(rollbackFor = Exception.class) @Override public void updatePaymentStatus(Map map, Integer payType) { PaymentInfo paymentInfo = baseMapper.selectOne( new LambdaQueryWrapper() .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); } ``` ## 2 支付成功处理 ### 2.1 更改订单状态 操作模块:`spzx-order` 订单支付成功后,我们已经更改了订单支付记录状态,接下来我还有**更改订单状态**,因为他们是不同的微服务模块,所以我们采用**消息队列**的方式,保证**数据最终一致性**; #### 2.1.1 OrderReceiver ```java 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); } } ``` #### 2.1.2 IOrderInfoService ```java void processPaySuccess(String orderNo); ``` #### 2.1.3 OrderInfoServiceImpl ```java @Override public void processPaySuccess(String orderNo) { //获取订单信息 OrderInfo orderInfo = orderInfoMapper.selectOne( new LambdaQueryWrapper() .eq(OrderInfo::getOrderNo, orderNo) .select(OrderInfo::getId, OrderInfo::getOrderStatus) ); if(orderInfo.getOrderStatus().intValue() == 0) { orderInfo.setOrderStatus(1);//已支付 orderInfo.setPaymentTime(new Date()); orderInfoMapper.updateById(orderInfo); } } ``` ### 2.2 扣减商品库存 订单模块除了更改订单支付状态,还要发送消息通知商品服务模块扣减库存 #### 2.2.1 ProductReceiver ```java /** * 扣减库存 * @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); } ``` #### 2.3.2 IProductService ```java void minus(String orderNo); ``` #### 2.2.3 ProductServiceImpl ```java @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 skuLockVoList = (List)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); } ``` #### 2.2.4 SkuStockMapper ```java Integer minus(@Param("skuId") Long skuId, @Param("num")Integer num); ``` #### 2.2.5 SkuStockMapper.xml ```xml update sku_stock set lock_num = lock_num - #{num}, total_num = total_num - #{num}, sale_num = sale_num + #{num} where sku_id = #{skuId} ```