当前位置:   article > 正文

神领物流day03下-支付微服务_黑马神领物流项目

黑马神领物流项目

课程安排

  • 支付微服务的需求

  • 了解项目中的代码规范

  • 阅读渠道管理相关的代码

  • 理解分布式锁的应用

  • 阅读支付宝扫码支付的代码

  • 阅读微信支付扫码支付的代码

  • xxl-job的入门学习

  • 读懂同步支付状态的两种方式

1、背景说明

新入职的你加入了开发一组,也接到了开发任务,并且你也顺利的完成了网关的鉴权业务的开发。现在开发三组所负责的支付微服务需要你来支援一下,目前支付微服务完成了支付宝和微信的对接,主要实现的功能有支付渠道的维护、扫码支付(微信称Native支付,支付宝称当面付)、退款等功能。 其中扫码支付功能是快递员上门取件时,会亮出二维码,用户可以通过支付宝或微信进行扫描后,对运费的支付。

2、需求分析

2.1、整体流程

流程说明:

  • 用户下单成功后,系统会为其分配快递员

  • 快递员根据取件任务进行上门取件,与用户确认物品信息、重量、体积、运费等内容,确认无误后,取件成功

  • 快递员会询问用户,是支付宝还是微信付款,根据用户的选择,展现支付二维码

  • 用户使用手机,打开支付宝或微信进行扫描操作,用户进行付款操作,最终会有支付成功或失败情况

  • 后续的逻辑暂时不考虑,支付微服务只考虑支付部分的逻辑即可

2.2、业务功能

image-20220810153452854.png

image-20220810153615785.png

2.3、产品需求

【付款方式】判断寄付/到付交互

  1. 寄付→点击【取件】进入取件成功页面,点击左上方返回按钮返回待取件任务列表;点击【去收款】按钮进入扫码支付页面,此时用户有双向选择:

    1. 在用户端【待支付】页面进行支付

    2. 在快递员端【扫码支付】页面进行支付,可选择微信或支付宝进行支付,分别生成不同的收款码,用户进行扫码支付;

    3. 点击页面左上方返回按钮页面返回至上一页;

    4. 两种方式支付成功,均显示支付成功页面,点击【知道了】,返回任务列表首页

  2. 到付→点击【取件】按钮,进入取件成功页面,点击返回主页按钮进入任务列表主页

2.4、分析

支付业务与其他业务相比,相对独立,所以比较适合将支付业务划分为一个微服务,而支付业务并不关系物流业务中运输、取派件等业务,只关心付款金额、付款平台、所支付的订单等。 支付微服务在整个系统架构中的业务时序图:

2.5、开发环境

2.5.1、微服务工程规范

在神领物流项目中,微服务代码是独立的工程(非聚合项目结构),这样更适合多团队间的协作,在部署方面更加的独立方便。 1个微服务需要创建3个工程,分别是:

  • sl-express-ms-xxx-api(定义Feign接口)

  • sl-express-ms-xxx-domain(定义DTO、枚举对象)

  • sl-express-ms-xxx-service(微服务的实现)

它们之间的依赖关系如下:

2.5.2、拉取代码

需要拉取的工程有3个:

在idea中拉取开发会有2种方式:

  • 每一个工程打开一个idea窗口

  • 将多个工程合并到一个idea窗口开发(非maven聚合),每一个工程作为一个module进行开发

在这里我们建议使用第2中方法,这样在开发过程中可以减少多窗口间的切换。 拉取代码完成后,需要添加到项目的modules中:

image.png

image.png

git分支说明:

image-20220802180251513.png

在学习阶段我们统一使用master分支。 下面展现了支付微服务的工程结构:

├─sl-express-ms-trade-api               支付Feign接口
├─sl-express-ms-trade-domain            接口DTO实体
└─sl-express-ms-trade-service           支付具体实现
    ├─com.sl.ms.trade.config				配置包,二维码、Redisson、xxl-job
	├─com.sl.ms.trade.constant				常量类包
	├─com.sl.ms.trade.controller			web控制器包
	├─com.sl.ms.trade.entity				数据库实体包
	├─com.sl.ms.trade.enums					枚举包
	├─com.sl.ms.trade.handler				三方平台的对接实现(支付宝、微信)
	├─com.sl.ms.trade.job					定时任务,扫描支付状态
	├─com.sl.ms.trade.mapper				mybatis接口
	├─com.sl.ms.trade.service				服务包
	├─com.sl.ms.trade.util					工具包

2.5.3、代码规范

2.5.3.1、DTO对象

在神领物流项目中,微服务之间的对象传输都使用DTO,命名规范:XxxxDTO(DTO必须大写),并且将DTO类放置到domain工程中,如下:

image.png

DTO类中统一使用lombok的@Data注解进行标注。

image.png

2.5.3.2、数据校验

微服务之间的接口调用,对于传输的数据是需要做校验的,一般校验方式有2种:

  • 方式一:采用hibernate-validator注解方式校验,如下:

    image.png

  • 方式二:在程序中通过if()进行判断,如下:

    image.png

我们采用哪一种方式呢?实际上在项目中,我们采用二者结合的方式进行校验。 对于第一种方式的补充说明:

  • 在Controller中需要增加@Validated注解,来开启校验

    image.png

  • 对于表单、url参数校验,在Controller中方法增加校验规则

    image-20220802194215132.png

  • 对于@RequestBody对象的校验,校验规则写的DTO对象中,统一通过Spring的AOP进行校验,具体在common工程中的com.sl.transport.common.aspect.ValidatedAspect进实现:

    image.png

    image-20220802194942996.png

2.5.3.3、自定义异常

在神领物流项目中,我们统一做了自定义异常的处理。 定义了2个异常:

  • com.sl.transport.common.exception.SLException

    • 用于微服务之前接口调用抛出的异常

  • com.sl.transport.common.exception.SLWebException

    • 用于前后端交互时抛出的异常

SLException的定义:

image-20220802200045853.png

SLWebException的定义:

image-20220802200108011.png

这两个异常的区别在于code、status的值不同。

疑问:为什么不使用一个,而是要设置两个?

这个主要是前端和后端的设计不同,一般在微服务间接口调用时会采用标准的RESTful方式,按照RESTful的规范响应的状态码要使用标准的http状态码,成功->200,失败->500,没有权限->401等。

而前后端进行交互时,一般都是响应200,即使出错也是200,只是响应结果中通过msg和code进行表达是否成功。

基于以上的场景,所以设置了两个异常类。

统一异常处理: 具体的业务逻辑在com.sl.transport.common.handler.GlobalExceptionHandler中实现。 关键代码如下:

image-20220802201311058.png

在该类中对于4种异常做处理,分别是:

  • ValidationException

    image-20220802201538202.png

  • SLException

    image-20220802201553399.png

  • SLWebException

    image-20220802201608636.png

  • Exception

    image-20220802201620980.png

2.5.3.4、@Resource注入

在项目中,涉及到注入Spring容器中bean对象时,均使用@Resource,目前IDEA不推荐使用@Autowired,原因是它是Spring提供,并非是Java标准,而@Resource是Java标准中定义的,建议使用。 如果想要使用@Autowired的话,建议通过构造器注入。

image.png

image.png

两者区别:

  • @Autowired:默认是ByType,可以使用@Qualifier指定Name,可以对构造器、方法、参数、字段使用。

  • @Resource:默认ByName,如果找不到则ByType,只能对方法、字段使用,不能用于构造器。

  • @Autowired是Spring提供的,@Resource是JSR-250提供的。

  • 总结:基本上@Resource可以完全替代@Autowired。

2.5.4、配置文件

2.5.4.1、SpringBoot配置文件

image.png

文件说明
bootstrap.yml通用配置项,服务名、日志文件、swagger配置等
bootstrap-local.yml多环境配置,本地开发环境
bootstrap-prod.yml多环境配置,生成环境(学习阶段忽略该文件)
bootstrap-stu.yml多环境配置,学生101环境
bootstrap-test.yml多环境配置,开发组测试环境(学习阶段忽略该文件)

对于配置文件的补充说明:

  • 关于swagger的配置,统一在【com.sl.transport.common.properties.SwaggerConfigProperties】中读取,并且在【com.sl.transport.common.config.Knife4jConfiguration】中进行了初始化Knife4j。

  • spring.profiles.active默认local,部署发布到101机器,在Jenkins中发布时设置为stu。

  1. #启动dokcer命令
  2. docker run -d -p $SERVER_PORT:8080 --name $SERVER_NAME -e SERVER_PORT=8080 -e SPRING_CLOUD_NACOS_DISCOVERY_IP=${SPRING_CLOUD_NACOS_DISCOVERY_IP} -e SPRING_CLOUD_NACOS_DISCOVERY_PORT=${port} -e SPRING_PROFILES_ACTIVE=stu $SERVER_NAME:$SERVER_VERSION

通过环境变量的方式配置了spring.profiles.active、发布到注册中心的ip和端口。

规则:环境变量统一采用大写字母,不允许使用.-符号,采用下划线“_”取代点“.” 减号“-”直接删除。 

  • 为了与101环境中服务互通,所以在local环境中固定设置了注册到注册中心的服务地址

    image-20220803100404395.png

  • 具体的一些项目配置统一使用nacos的配置中心管理,并且在这里使用nacos的共享配置机制,这样可以在多个项目中共享相同的配置

    image.png

2.5.4.2、seata配置

shared-spring-seata.yml

  1. seata:
  2. registry:
  3. type: nacos
  4. nacos:
  5. server-addr: 192.168.150.101:8848
  6. namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
  7. group: DEFAULT_GROUP
  8. application: seata-server
  9. username: nacos
  10. password: nacos
  11. tx-service-group: sl-seata # 事务组名称
  12. service:
  13. vgroup-mapping: # 事务组与cluster的映射关系
  14. sl-seata: default

seata服务的配置:

seata-server.properties

  1. #指定seata存储的数据库
  2. store.mode = db
  3. store.db.datasource = druid
  4. store.db.dbType = mysql
  5. store.db.driverClassName = com.mysql.cj.jdbc.Driver
  6. store.db.url = jdbc:mysql://192.168.150.101:3306/seata?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
  7. store.db.user = root
  8. store.db.password = 123
  9. store.db.minConn = 5
  10. store.db.maxConn = 100
  11. store.db.globalTable = global_table
  12. store.db.branchTable = branch_table
  13. store.db.lockTable = lock_table
  14. store.db.distributedLockTable = distributed_lock
  15. store.db.queryLimit = 100
  16. store.db.maxWait = 5000

seata服务地址:  sl-express.com - The domain is available for purchase   账号信息:seata/seata

2.5.4.3、mysql配置

shared-spring-mysql.yml

  1. spring:
  2. datasource: #数据库的配置
  3. driver-class-name: ${jdbc.driver:com.mysql.cj.jdbc.Driver}
  4. url: ${jdbc.url}
  5. username: ${jdbc.username}
  6. password: ${jdbc.password}

具体的配置项在每个微服务自己的配置文件中,例如支付服务:

sl-express-ms-trade.properties

  1. jdbc.url = jdbc:mysql://192.168.150.101:3306/sl_trade?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
  2. jdbc.username = root
  3. jdbc.password = 123

需要说明的是,${jdbc.driver:com.mysql.cj.jdbc.Driver} 这种写法冒号后面的是默认值,如果不配置jdbc.driver就采用默认值。 

2.5.4.4、mybatis-plus配置

shared-spring-mybatis-plus.yml

  1. mybatis-plus:
  2. configuration:
  3. #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
  4. map-underscore-to-camel-case: true
  5. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  6. #log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
  7. global-config:
  8. db-config:
  9. id-type: ASSIGN_ID

在配置文件中指定的默认的id策略为ASSIGN_ID,只当插入对象ID为空时,自动填充雪花id。

2.5.4.5、redis配置

shared-spring-redis.yml

  1. spring:
  2. redis: #redis的配置
  3. port: ${redis.port}
  4. host: ${redis.host}
  5. password: ${redis.password}

具体的配置在微服务自身的配置文件中:

image.png

2.5.4.6、xxl-job配置

shared-spring-xxl-job.yml

  1. xxl:
  2. job:
  3. admin:
  4. addresses: http://192.168.150.101:28080/xxl-job-admin
  5. executor:
  6. ip: 192.168.150.101
  7. appname: ${xxl.job.executor.appname}
  8. #执行器运行日志文件存储磁盘路径
  9. logpath: /data/applogs/xxl-job/jobhandler
  10. #执行器日志文件保存天数
  11. logretentiondays: 30

2.5.5、日志

项目中统一使用logback日志框架,其配置文件如下:

logback-spring.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!--scan: 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。-->
  3. <!--scanPeriod: 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。-->
  4. <!--debug: 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。-->
  5. <configuration debug="false" scan="false" scanPeriod="60 seconds">
  6. <springProperty scope="context" name="appName" source="spring.application.name"/>
  7. <!--文件名-->
  8. <property name="logback.appname" value="${appName}"/>
  9. <!--文件位置-->
  10. <property name="logback.logdir" value="/data/logs"/>
  11. <!-- 定义控制台输出 -->
  12. <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
  13. <layout class="ch.qos.logback.classic.PatternLayout">
  14. <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} - [%thread] - %-5level - %logger{50} - %msg%n</pattern>
  15. </layout>
  16. </appender>
  17. <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
  18. <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
  19. <level>DEBUG</level>
  20. </filter>
  21. <File>${logback.logdir}/${logback.appname}/${logback.appname}.log</File>
  22. <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
  23. <FileNamePattern>${logback.logdir}/${logback.appname}/${logback.appname}.%d{yyyy-MM-dd}.log.zip</FileNamePattern>
  24. <maxHistory>90</maxHistory>
  25. </rollingPolicy>
  26. <encoder>
  27. <charset>UTF-8</charset>
  28. <pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</pattern>
  29. </encoder>
  30. </appender>
  31. <!--evel:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALLOFF,-->
  32. <!--不能设置为INHERITED或者同义词NULL。默认是DEBUG。-->
  33. <root level="INFO">
  34. <appender-ref ref="stdout"/>
  35. </root>
  36. </configuration>

3、支付渠道管理【阅读代码】

支付是对接支付平台完成的,例如支付宝、微信、京东支付等,一般在这些平台上需要申请账号信息,通过这些账号信息完成与支付平台的交互,在我们的支付微服务中,将这些数据称之为【支付渠道】,并且将其存储到数据库中,通过程序可以支付渠道进行管理。

3.1、表结构

支付微服务的数据是:sl_trade,支付渠道的表为:sl_pay_channel,表结构如下:

image-20220811113247995.png

其中表中已经包含了2条数据,分别是支付宝和微信的账号信息,可以直接与支付平台对接。

image-20220811114631642.png

3.2、阅读代码

阅读代码顺序:Entity → Mapper → Service → Controller 代码git地址:

注意:由于渠道管理目前项目中没有需求进行维护操作,所以不对外提供Feign接口。

3.2.1、PayChannelEntity

PayChannelEntity类是对sl_pay_channel表的映射,Entity类继承BaseEntity,在BaseEntity中统一定义了id、created、updated,其中created、updated是使用MybatisPlus自动填充的。 

  1. package com.sl.ms.trade.entity;
  2. import com.baomidou.mybatisplus.annotation.TableName;
  3. import com.sl.transport.common.entity.BaseEntity;
  4. import io.swagger.annotations.ApiModelProperty;
  5. import lombok.AllArgsConstructor;
  6. import lombok.Data;
  7. import lombok.EqualsAndHashCode;
  8. import lombok.NoArgsConstructor;
  9. /**
  10. * @Description:交易渠道表
  11. */
  12. @Data
  13. @NoArgsConstructor
  14. @AllArgsConstructor
  15. @EqualsAndHashCode(callSuper = true)
  16. @TableName("sl_pay_channel")
  17. public class PayChannelEntity extends BaseEntity {
  18. private static final long serialVersionUID = -1452774366739615656L;
  19. @ApiModelProperty(value = "通道名称")
  20. private String channelName;
  21. @ApiModelProperty(value = "通道唯一标记")
  22. private String channelLabel;
  23. @ApiModelProperty(value = "域名")
  24. private String domain;
  25. @ApiModelProperty(value = "商户appid")
  26. private String appId;
  27. @ApiModelProperty(value = "支付公钥")
  28. private String publicKey;
  29. @ApiModelProperty(value = "商户私钥")
  30. private String merchantPrivateKey;
  31. @ApiModelProperty(value = "其他配置")
  32. private String otherConfig;
  33. @ApiModelProperty(value = "AES混淆密钥")
  34. private String encryptKey;
  35. @ApiModelProperty(value = "说明")
  36. private String remark;
  37. @ApiModelProperty(value = "回调地址")
  38. private String notifyUrl;
  39. @ApiModelProperty(value = "是否有效")
  40. protected String enableFlag;
  41. @ApiModelProperty(value = "商户号")
  42. private Long enterpriseId;
  43. }

3.2.2、PayChannelMapper

PayChannelMapper继承了MP的BaseMapper,并且加了@Mapper注解。

  1. package com.sl.ms.trade.mapper;
  2. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  3. import com.sl.ms.trade.entity.PayChannelEntity;
  4. import org.apache.ibatis.annotations.Mapper;
  5. /**
  6. * 交易渠道表Mapper接口
  7. */
  8. @Mapper
  9. public interface PayChannelMapper extends BaseMapper<PayChannelEntity> {
  10. }

3.2.3、PayChannelService

该Service中定义了6个方法,可以对支付渠道的数据进行CRUD的管理,其中findByEnterpriseId()方法将是我们常用的一个方法,根据业务商户id查询和通道唯一标记符查询支付渠道。该方法是需要对数据做缓存的,目前并没有实现缓存,这个需要由你来实现。 

  1. package com.sl.ms.trade.service;
  2. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  3. import com.baomidou.mybatisplus.extension.service.IService;
  4. import com.sl.ms.trade.domain.PayChannelDTO;
  5. import com.sl.ms.trade.entity.PayChannelEntity;
  6. import java.util.List;
  7. /**
  8. * @Description: 支付通道服务类
  9. */
  10. public interface PayChannelService extends IService<PayChannelEntity> {
  11. /**
  12. * @param payChannelDTO 查询条件
  13. * @param pageNum 当前页
  14. * @param pageSize 当前页
  15. * @return Page<PayChannel> 分页对象
  16. * @Description 支付通道列表
  17. */
  18. Page<PayChannelEntity> findPayChannelPage(PayChannelDTO payChannelDTO, int pageNum, int pageSize);
  19. /**
  20. * 根据商户id查询渠道配置,该配置会被缓存10分钟
  21. *
  22. * @param enterpriseId 商户id
  23. * @param channelLabel 通道唯一标记
  24. * @return PayChannelEntity 交易渠道对象
  25. */
  26. PayChannelEntity findByEnterpriseId(Long enterpriseId, String channelLabel);
  27. /**
  28. * @param payChannelDTO 对象信息
  29. * @return PayChannelEntity 交易渠道对象
  30. * @Description 创建支付通道
  31. */
  32. PayChannelEntity createPayChannel(PayChannelDTO payChannelDTO);
  33. /**
  34. * @param payChannelDTO 对象信息
  35. * @return Boolean 是否成功
  36. * @Description 修改支付通道
  37. */
  38. Boolean updatePayChannel(PayChannelDTO payChannelDTO);
  39. /**
  40. * @param checkedIds 选择的支付通道ID
  41. * @return Boolean 是否成功
  42. * @Description 删除支付通道
  43. */
  44. Boolean deletePayChannel(String[] checkedIds);
  45. /**
  46. * @param channelLabel 支付通道标识
  47. * @return 支付通道列表
  48. * @Description 查找渠道标识
  49. */
  50. List<PayChannelEntity> findPayChannelList(String channelLabel);
  51. }

3.2.4、PayChannelServiceImpl

  • 该类继承了MP的ServiceImpl,可以实现基本的CRUD方法

  • findByEnterpriseId()方法中的TODO需要在实战中完成 

  1. package com.sl.ms.trade.service.impl;
  2. import cn.hutool.core.bean.BeanUtil;
  3. import cn.hutool.core.util.StrUtil;
  4. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  5. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  6. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  7. import com.sl.ms.trade.constant.Constants;
  8. import com.sl.ms.trade.domain.PayChannelDTO;
  9. import com.sl.ms.trade.entity.PayChannelEntity;
  10. import com.sl.ms.trade.mapper.PayChannelMapper;
  11. import com.sl.ms.trade.service.PayChannelService;
  12. import org.springframework.stereotype.Service;
  13. import java.util.Arrays;
  14. import java.util.List;
  15. /**
  16. * @Description: 服务实现类
  17. */
  18. @Service
  19. public class PayChannelServiceImpl extends ServiceImpl<PayChannelMapper, PayChannelEntity> implements PayChannelService {
  20. @Override
  21. public Page<PayChannelEntity> findPayChannelPage(PayChannelDTO payChannelDTO, int pageNum, int pageSize) {
  22. Page<PayChannelEntity> page = new Page<>(pageNum, pageSize);
  23. LambdaQueryWrapper<PayChannelEntity> queryWrapper = new LambdaQueryWrapper<>();
  24. //设置条件
  25. queryWrapper.eq(StrUtil.isNotEmpty(payChannelDTO.getChannelLabel()), PayChannelEntity::getChannelLabel, payChannelDTO.getChannelLabel());
  26. queryWrapper.likeRight(StrUtil.isNotEmpty(payChannelDTO.getChannelName()), PayChannelEntity::getChannelName, payChannelDTO.getChannelName());
  27. queryWrapper.eq(StrUtil.isNotEmpty(payChannelDTO.getEnableFlag()), PayChannelEntity::getEnableFlag, payChannelDTO.getEnableFlag());
  28. //设置排序
  29. queryWrapper.orderByAsc(PayChannelEntity::getCreated);
  30. return super.page(page, queryWrapper);
  31. }
  32. @Override
  33. public PayChannelEntity findByEnterpriseId(Long enterpriseId, String channelLabel) {
  34. LambdaQueryWrapper<PayChannelEntity> queryWrapper = new LambdaQueryWrapper<>();
  35. queryWrapper.eq(PayChannelEntity::getEnterpriseId, enterpriseId)
  36. .eq(PayChannelEntity::getChannelLabel, channelLabel)
  37. .eq(PayChannelEntity::getEnableFlag, Constants.YES);
  38. //TODO 缓存
  39. return super.getOne(queryWrapper);
  40. }
  41. @Override
  42. public PayChannelEntity createPayChannel(PayChannelDTO payChannelDTO) {
  43. PayChannelEntity payChannel = BeanUtil.toBean(payChannelDTO, PayChannelEntity.class);
  44. boolean flag = super.save(payChannel);
  45. if (flag) {
  46. return payChannel;
  47. }
  48. return null;
  49. }
  50. @Override
  51. public Boolean updatePayChannel(PayChannelDTO payChannelDTO) {
  52. PayChannelEntity payChannel = BeanUtil.toBean(payChannelDTO, PayChannelEntity.class);
  53. return super.updateById(payChannel);
  54. }
  55. @Override
  56. public Boolean deletePayChannel(String[] checkedIds) {
  57. List<String> ids = Arrays.asList(checkedIds);
  58. return super.removeByIds(ids);
  59. }
  60. @Override
  61. public List<PayChannelEntity> findPayChannelList(String channelLabel) {
  62. LambdaQueryWrapper<PayChannelEntity> queryWrapper = new LambdaQueryWrapper<>();
  63. queryWrapper.eq(PayChannelEntity::getChannelLabel, channelLabel)
  64. .eq(PayChannelEntity::getEnableFlag, Constants.YES);
  65. return list(queryWrapper);
  66. }
  67. }

3.2.5、PayChannelController

该类中对于支付渠道维护的各种方法的维护,确保可以对外提供服务。 

  1. package com.sl.ms.trade.controller;
  2. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  3. import com.sl.ms.trade.domain.PayChannelDTO;
  4. import com.sl.ms.trade.entity.PayChannelEntity;
  5. import com.sl.ms.trade.service.PayChannelService;
  6. import com.sl.transport.common.exception.SLException;
  7. import com.sl.transport.common.util.PageResponse;
  8. import io.swagger.annotations.Api;
  9. import io.swagger.annotations.ApiImplicitParam;
  10. import io.swagger.annotations.ApiImplicitParams;
  11. import io.swagger.annotations.ApiOperation;
  12. import lombok.extern.slf4j.Slf4j;
  13. import org.springframework.http.HttpStatus;
  14. import org.springframework.web.bind.annotation.*;
  15. import javax.annotation.Resource;
  16. @Slf4j
  17. @RestController
  18. @Api(tags = "支付通道")
  19. @RequestMapping("payChannel")
  20. public class PayChannelController {
  21. @Resource
  22. private PayChannelService payChannelService;
  23. /**
  24. * 支付通道列表
  25. *
  26. * @param payChannelDTO 查询条件
  27. * @return 分页数据对象
  28. */
  29. @PostMapping("page/{pageNum}/{pageSize}")
  30. @ApiOperation(value = "查询支付通道分页", notes = "查询支付通道分页")
  31. @ApiImplicitParams({
  32. @ApiImplicitParam(name = "payChannelDTO", value = "支付通道查询对象", required = true),
  33. @ApiImplicitParam(name = "pageNum", value = "页码"),
  34. @ApiImplicitParam(name = "pageSize", value = "每页条数")
  35. })
  36. public PageResponse<PayChannelDTO> findPayChannelPage(
  37. @RequestBody PayChannelDTO payChannelDTO,
  38. @PathVariable("pageNum") int pageNum,
  39. @PathVariable("pageSize") int pageSize) {
  40. Page<PayChannelEntity> payChannelVoPage = payChannelService.findPayChannelPage(payChannelDTO, pageNum, pageSize);
  41. return new PageResponse<>(payChannelVoPage, PayChannelDTO.class);
  42. }
  43. /**
  44. * 添加支付通道
  45. *
  46. * @param payChannelDTO 对象信息
  47. */
  48. @PostMapping
  49. @ApiOperation(value = "添加支付通道", notes = "添加支付通道")
  50. @ApiImplicitParam(name = "payChannelDTO", value = "支付通道对象", required = true)
  51. public void createPayChannel(@RequestBody PayChannelDTO payChannelDTO) {
  52. PayChannelEntity payChannel = this.payChannelService.createPayChannel(payChannelDTO);
  53. if (null != payChannel) {
  54. return;
  55. }
  56. throw new SLException("添加支付通道失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
  57. }
  58. /**
  59. * 修改支付通道
  60. *
  61. * @param payChannelDTO 对象信息
  62. */
  63. @PutMapping
  64. @ApiOperation(value = "修改支付通道", notes = "修改支付通道")
  65. @ApiImplicitParam(name = "payChannelDTO", value = "支付通道对象", required = true)
  66. public void updatePayChannel(@RequestBody PayChannelDTO payChannelDTO) {
  67. Boolean flag = this.payChannelService.updatePayChannel(payChannelDTO);
  68. if (flag) {
  69. return;
  70. }
  71. throw new SLException("修改支付通道失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
  72. }
  73. /**
  74. * 删除支付通道
  75. *
  76. * @param payChannelDTO 查询对象
  77. */
  78. @DeleteMapping
  79. @ApiOperation(value = "删除支付通道", notes = "删除支付通道")
  80. @ApiImplicitParam(name = "payChannelDTO", value = "支付通道查询对象", required = true)
  81. public void deletePayChannel(@RequestBody PayChannelDTO payChannelDTO) {
  82. String[] checkedIds = payChannelDTO.getCheckedIds();
  83. Boolean flag = this.payChannelService.deletePayChannel(checkedIds);
  84. if (flag) {
  85. return;
  86. }
  87. throw new SLException("删除支付通道失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
  88. }
  89. @PutMapping("update-payChannel-enableFlag")
  90. @ApiOperation(value = "修改支付通道状态", notes = "修改支付通道状态")
  91. @ApiImplicitParam(name = "payChannelDTO", value = "支付通道查询对象", required = true)
  92. public void updatePayChannelEnableFlag(@RequestBody PayChannelDTO payChannelDTO) {
  93. Boolean flag = this.payChannelService.updatePayChannel(payChannelDTO);
  94. if (flag) {
  95. return;
  96. }
  97. throw new SLException("修改支付通道状态失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
  98. }
  99. }

3.3、测试

通过swagger接口进行测试:http://192.168.150.101:18096/doc.html

image.png

image.png

其他的方法就不进行测试了,同学们可以自行测试。

4、扫码支付【阅读代码】

扫码支付的基本原理就是通过调用支付平台的接口,提交支付请求,支付平台会返回支付链接,将此支付链接生成二维码,用户通过手机上的支付宝或微信进行扫码支付。流程如下:

4.1、交易单表结构

【交易单表 sl_trading】是指,针对于订单进行支付的记录表,其中记录了订单号,支付状态、支付平台、金额、是否有退款等信息。具体表结构如下:

image.png

4.2、代码流程

下面展现了整体的扫描支付代码调用流程,我们将按照下面的流程进行代码的阅读。

4.3、幂等性处理

在向支付平台申请支付之前对交易单对象做幂等性处理,主要是防止重复的生成交易单以及一些业务逻辑的处理,具体是在com.sl.ms.trade.handler.impl.BeforePayHandlerImpl#idempotentCreateTrading()方法中完成的。 其代码如下:

  1. @Override
  2. public void idempotentCreateTrading(TradingEntity tradingEntity) throws SLException {
  3. TradingEntity trading = tradingService.findTradByProductOrderNo(tradingEntity.getProductOrderNo());
  4. if (ObjectUtil.isEmpty(trading)) {
  5. //新交易单,生成交易号
  6. Long id = Convert.toLong(identifierGenerator.nextId(tradingEntity));
  7. tradingEntity.setId(id);
  8. tradingEntity.setTradingOrderNo(id);
  9. return;
  10. }
  11. TradingStateEnum tradingState = trading.getTradingState();
  12. if (ObjectUtil.equalsAny(tradingState, TradingStateEnum.YJS, TradingStateEnum.MD)) {
  13. //已结算、免单:直接抛出重复支付异常
  14. throw new SLException(TradingEnum.TRADING_STATE_SUCCEED);
  15. } else if (ObjectUtil.equals(TradingStateEnum.FKZ, tradingState)) {
  16. //付款中,如果支付渠道一致,说明是重复,抛出支付中异常,否则需要更换支付渠道
  17. //举例:第一次通过支付宝付款,付款中用户取消,改换了微信支付
  18. if (StrUtil.equals(trading.getTradingChannel(), tradingEntity.getTradingChannel())) {
  19. throw new SLException(TradingEnum.TRADING_STATE_PAYING);
  20. } else {
  21. tradingEntity.setId(trading.getId()); // id设置为原订单的id
  22. //重新生成交易号,在这里就会出现id 与 TradingOrderNo 数据不同的情况,其他情况下是一样的
  23. tradingEntity.setTradingOrderNo(Convert.toLong(identifierGenerator.nextId(tradingEntity)));
  24. }
  25. } else if (ObjectUtil.equalsAny(tradingState, TradingStateEnum.QXDD, TradingStateEnum.GZ)) {
  26. //取消订单,挂账:创建交易号,对原交易单发起支付
  27. tradingEntity.setId(trading.getId()); // id设置为原订单的id
  28. //重新生成交易号,在这里就会出现id 与 TradingOrderNo 数据不同的情况,其他情况下是一样的
  29. tradingEntity.setTradingOrderNo(Convert.toLong(identifierGenerator.nextId(tradingEntity)));
  30. } else {
  31. //其他情况:直接交易失败
  32. throw new SLException(TradingEnum.PAYING_TRADING_FAIL);
  33. }
  34. }

在此代码中,主要是逻辑是:

  • 如果根据订单号查询交易单数据,如果不存在说明新交易单,生成交易单号后直接返回,这里的交易单号也是使用雪花id。

  • 如果支付状态是已经【支付成功】或是【免单 - 不需要支付】,直接抛出异常。

  • 如果支付状态是【付款中】,此时有两种情况

    • 如果支付渠道相同(此前使用支付宝付款,本次也是使用支付宝付款),这种情况抛出异常

    • 如果支付渠道不同,我们是允许在生成二维码后更换支付渠道,此时需要重新生成交易单号,此时交易单号与id将不同。

  • 如果支付状态是【取消订单】或【挂账】,将id设置为原交易号,交易号重新生成,这样做的目的是既保留了原订单的交易号,又可以生成新的交易号(不重新生成的话,没有办法在支付平台进行支付申请),与之前不会有影响。

4.4、HandlerFactory

对于NativePayHandler会有不同平台的实现,比如:支付宝、微信,每个平台的接口参数、返回值都不一样,所以是没有办法共用的,只要是每个平台都去编写一个实现类。 那问题来了,我们该如何选择呢? 在这里我们采用了工厂模式进行获取对应的NativePayHandler实例,在不同的渠道实现类中,都指定了@PayChannel注解,通过type属性指定具体的平台(支付宝/微信):

image.png

image.png

有了这个注解标识后,在HandlerFactory中就可以根据指定的参数获取对应的渠道实现。 核心代码如下:

com.sl.ms.trade.handler.HandlerFactory#get(com.sl.ms.trade.enums.PayChannelEnum, java.lang.Class<T>)

  1. public static <T> T get(PayChannelEnum payChannel, Class<T> handler) {
  2. Map<String, T> beans = SpringUtil.getBeansOfType(handler);
  3. for (Map.Entry<String, T> entry : beans.entrySet()) {
  4. PayChannel payChannelAnnotation = entry.getValue().getClass().getAnnotation(PayChannel.class);
  5. if (ObjectUtil.isNotEmpty(payChannelAnnotation) && ObjectUtil.equal(payChannel, payChannelAnnotation.type())) {
  6. return entry.getValue();
  7. }
  8. }
  9. return null;
  10. }

使用:

image.png

4.4、生成二维码

支付宝或微信的扫码支付返回是一个链接,并不是二维码,所以我们需要根据链接生成二维码,生成二维码的库使用的是:(最终生成的二维码图片使用的base64字符串返回给前端)

https://www.yuque.com/r/goto?url=https%3A%2F%2Fgithub.com%2Fzxing%2Fzxing

具体代码实现:

QRCodeServiceImpl

  1. package com.sl.ms.trade.service.impl;
  2. import cn.hutool.core.img.ImgUtil;
  3. import cn.hutool.core.util.HexUtil;
  4. import cn.hutool.core.util.ObjectUtil;
  5. import cn.hutool.extra.qrcode.QrCodeUtil;
  6. import cn.hutool.extra.qrcode.QrConfig;
  7. import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
  8. import com.sl.ms.trade.config.QRCodeConfig;
  9. import com.sl.ms.trade.enums.PayChannelEnum;
  10. import com.sl.ms.trade.service.QRCodeService;
  11. import org.springframework.stereotype.Service;
  12. import javax.annotation.Resource;
  13. @Service
  14. public class QRCodeServiceImpl implements QRCodeService {
  15. @Resource
  16. private QRCodeConfig qrCodeConfig;
  17. @Override
  18. public String generate(String content, PayChannelEnum payChannel) {
  19. QrConfig qrConfig = new QrConfig();
  20. //设置边距
  21. qrConfig.setMargin(this.qrCodeConfig.getMargin());
  22. //二维码颜色
  23. qrConfig.setForeColor(HexUtil.decodeColor(this.qrCodeConfig.getForeColor()));
  24. //设置背景色
  25. qrConfig.setBackColor(HexUtil.decodeColor(this.qrCodeConfig.getBackColor()));
  26. //纠错级别
  27. qrConfig.setErrorCorrection(ErrorCorrectionLevel.valueOf(this.qrCodeConfig.getErrorCorrectionLevel()));
  28. //设置宽
  29. qrConfig.setWidth(this.qrCodeConfig.getWidth());
  30. //设置高
  31. qrConfig.setHeight(this.qrCodeConfig.getHeight());
  32. if (ObjectUtil.isNotEmpty(payChannel)) {
  33. //设置logo
  34. qrConfig.setImg(this.qrCodeConfig.getLogo(payChannel));
  35. }
  36. return QrCodeUtil.generateAsBase64(content, qrConfig, ImgUtil.IMAGE_TYPE_PNG);
  37. }
  38. @Override
  39. public String generate(String content) {
  40. return generate(content, null);
  41. }
  42. }

具体的配置存储在nacos中:

sl-express-ms-trade.properties

  1. #二维码配置
  2. #边距,二维码和背景之间的边距
  3. qrcode.margin = 2
  4. #二维码颜色,默认黑色
  5. qrcode.fore-color = #000000
  6. #背景色,默认白色
  7. qrcode.back-color = #ffffff
  8. #低级别的像素块更大,可以远距离识别,但是遮挡就会造成无法识别。高级别则相反,像素块小,允许遮挡一定范围,但是像素块更密集。
  9. #纠错级别,可选参数:L、M、Q、H,默认:M
  10. qrcode.error-correction-level = M
  11. #宽
  12. qrcode.width = 300
  13. #高
  14. qrcode.height = 300

配置的映射类:

QRCodeConfig

  1. package com.sl.ms.trade.config;
  2. import cn.hutool.core.img.ImgUtil;
  3. import cn.hutool.core.io.resource.ResourceUtil;
  4. import com.sl.ms.trade.enums.PayChannelEnum;
  5. import lombok.Data;
  6. import org.springframework.boot.context.properties.ConfigurationProperties;
  7. import org.springframework.context.annotation.Configuration;
  8. import java.awt.*;
  9. /**
  10. * 二维码生成参数配置
  11. */
  12. @Data
  13. @Configuration
  14. @ConfigurationProperties(prefix = "sl.qrcode")
  15. public class QRCodeConfig {
  16. private static Image WECHAT_LOGO;
  17. private static Image ALIPAY_LOGO;
  18. static {
  19. WECHAT_LOGO = ImgUtil.read(ResourceUtil.getResource("logos/wechat.png"));
  20. ALIPAY_LOGO = ImgUtil.read(ResourceUtil.getResource("logos/alipay.png"));
  21. }
  22. //边距,二维码和背景之间的边距
  23. private Integer margin = 2;
  24. // 二维码颜色,默认黑色
  25. private String foreColor = "#000000";
  26. //背景色,默认白色
  27. private String backColor = "#ffffff";
  28. //纠错级别,可选参数:L、M、Q、H,默认:M
  29. //低级别的像素块更大,可以远距离识别,但是遮挡就会造成无法识别。高级别则相反,像素块小,允许遮挡一定范围,但是像素块更密集。
  30. private String errorCorrectionLevel = "M";
  31. //宽
  32. private Integer width = 300;
  33. //高
  34. private Integer height = 300;
  35. public Image getLogo(PayChannelEnum payChannelEnum) {
  36. switch (payChannelEnum) {
  37. case ALI_PAY: {
  38. return ALIPAY_LOGO;
  39. }
  40. case WECHAT_PAY: {
  41. return WECHAT_LOGO;
  42. }
  43. default: {
  44. return null;
  45. }
  46. }
  47. }
  48. }

生成的效果:

image.png

image.png

image.png

在线base64转图片工具:

https://www.qvdv.net/tools/qvdv-img2base64.html

4.5、测试

image.png

请求参数 

  1. {
  2. "enterpriseId": 2088241317544335,
  3. "memo": "运费",
  4. "productOrderNo": 11112241,
  5. "tradingAmount": 1,
  6. "tradingChannel": "ALI_PAY"
  7. }

响应

  1. {
  2. "qrCode": "data:image/png;base64,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",
  3. "productOrderNo": "11112241",
  4. "tradingOrderNo": "1559096271641808897",
  5. "tradingChannel": "ALI_PAY"
  6. }

image.png

56e099385ef1cc6a55ba45496d4fa29.jpg

4.6、优化(练习)

在生成二维码时,我们采用的是服务端生成二维码方式,这种方式会比较消耗服务器的CPU、内存资源,比较好的做法是生成二维码的动作交由客户端(前端)来生成。 实际上,我们前端已经做了兼容处理,在返回的【qrCode】字段中,如果内容以【data:image/png;】开头,直接展现,否则就将返回的数据(支付宝或微信返回的原始数据,例如:支付宝)生成二维码。 这个优化交由学生来完成。

4.7、支付宝扫码支付

4.7.1、AlipayConfig

在项目中,通com.sl.ms.trade.handler.alipay.AlipayConfig#getConfig(Long enterpriseId)方法可以按照商户id查询支付宝的配置,如果配置查询不到会抛出异常。

代码如下:

  1. package com.sl.ms.trade.handler.alipay;
  2. import cn.hutool.core.convert.Convert;
  3. import cn.hutool.core.util.ObjectUtil;
  4. import cn.hutool.core.util.StrUtil;
  5. import cn.hutool.extra.spring.SpringUtil;
  6. import com.alipay.easysdk.kernel.Config;
  7. import com.sl.ms.trade.constant.TradingConstant;
  8. import com.sl.ms.trade.entity.PayChannelEntity;
  9. import com.sl.ms.trade.enums.TradingEnum;
  10. import com.sl.ms.trade.service.PayChannelService;
  11. import com.sl.transport.common.exception.SLException;
  12. /**
  13. * 支付宝支付的配置
  14. */
  15. public class AlipayConfig {
  16. /**
  17. * 将支付渠道配置转化为支付宝的配置
  18. *
  19. * @param enterpriseId 商户ID
  20. * @return 支付宝的配置
  21. */
  22. public static Config getConfig(Long enterpriseId) {
  23. // 查询配置
  24. PayChannelService payChannelService = SpringUtil.getBean(PayChannelService.class);
  25. PayChannelEntity payChannel = payChannelService.findByEnterpriseId(enterpriseId, TradingConstant.TRADING_CHANNEL_ALI_PAY);
  26. if (ObjectUtil.isEmpty(payChannel)) {
  27. throw new SLException(TradingEnum.CONFIG_EMPTY);
  28. }
  29. Config config = new Config();
  30. config.protocol = "https";
  31. config.gatewayHost = payChannel.getDomain();
  32. config.signType = "RSA2";
  33. config.appId = payChannel.getAppId();
  34. //配置应用私钥
  35. config.merchantPrivateKey = payChannel.getMerchantPrivateKey();
  36. //配置支付宝公钥
  37. config.alipayPublicKey = payChannel.getPublicKey();
  38. //可设置异步通知接收服务地址(可选)
  39. config.notifyUrl = StrUtil.replace(payChannel.getNotifyUrl(), "{enterpriseId}", Convert.toStr(enterpriseId));
  40. //设置AES密钥,调用AES加解密相关接口时需要(可选)
  41. config.encryptKey = payChannel.getEncryptKey();
  42. return config;
  43. }
  44. }

关于异步通知的url的说明:

数据库表中存储的数据类似这样:https://61d25503.cpolar.cn/trade/notify/alipay/%7BenterpriseId%7D

 其中,61d25503.cpolar.cn这个域名是内网穿透的地址,后面会将,暂时忽略。{enterpriseId}这个是占位符,在真正设置值时,会用【商户id】进行替换,最终的通知地址类似:

https://61d25503.cpolar.cn/trade/notify/alipay/2088241317544335

4.7.2、具体实现

  1. package com.sl.ms.trade.handler.alipay;
  2. import cn.hutool.core.convert.Convert;
  3. import cn.hutool.json.JSONUtil;
  4. import com.alipay.easysdk.factory.Factory;
  5. import com.alipay.easysdk.kernel.Config;
  6. import com.alipay.easysdk.kernel.util.ResponseChecker;
  7. import com.alipay.easysdk.payment.facetoface.models.AlipayTradePrecreateResponse;
  8. import com.sl.ms.trade.annotation.PayChannel;
  9. import com.sl.ms.trade.entity.TradingEntity;
  10. import com.sl.ms.trade.enums.PayChannelEnum;
  11. import com.sl.ms.trade.enums.TradingEnum;
  12. import com.sl.ms.trade.enums.TradingStateEnum;
  13. import com.sl.ms.trade.handler.NativePayHandler;
  14. import com.sl.transport.common.exception.SLException;
  15. import lombok.extern.slf4j.Slf4j;
  16. import org.springframework.stereotype.Component;
  17. /**
  18. * 支付宝的扫描支付的具体实现
  19. */
  20. @Slf4j
  21. @Component("aliNativePayHandler")
  22. @PayChannel(type = PayChannelEnum.ALI_PAY)
  23. public class AliNativePayHandler implements NativePayHandler {
  24. @Override
  25. public void createDownLineTrading(TradingEntity tradingEntity) throws SLException {
  26. //查询配置
  27. Config config = AlipayConfig.getConfig(tradingEntity.getEnterpriseId());
  28. //Factory使用配置
  29. Factory.setOptions(config);
  30. AlipayTradePrecreateResponse response;
  31. try {
  32. //调用支付宝API面对面支付
  33. response = Factory
  34. .Payment
  35. .FaceToFace()
  36. .preCreate(tradingEntity.getMemo(), //订单描述
  37. Convert.toStr(tradingEntity.getTradingOrderNo()), //业务订单号
  38. Convert.toStr(tradingEntity.getTradingAmount())); //金额
  39. } catch (Exception e) {
  40. log.error("支付宝统一下单创建失败:tradingEntity = {}", tradingEntity, e);
  41. throw new SLException(TradingEnum.NATIVE_PAY_FAIL, e);
  42. }
  43. //受理结果【只表示请求是否成功,而不是支付是否成功】
  44. boolean isSuccess = ResponseChecker.success(response);
  45. //6.1、受理成功:修改交易单
  46. if (isSuccess) {
  47. String subCode = response.getSubCode();
  48. String subMsg = response.getQrCode();
  49. tradingEntity.setPlaceOrderCode(subCode); //返回的编码
  50. tradingEntity.setPlaceOrderMsg(subMsg); //二维码需要展现的信息
  51. tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response));
  52. tradingEntity.setTradingState(TradingStateEnum.FKZ);
  53. return;
  54. }
  55. throw new SLException(JSONUtil.toJsonStr(response), TradingEnum.NATIVE_PAY_FAIL.getCode(), TradingEnum.NATIVE_PAY_FAIL.getStatus());
  56. }
  57. }

4.8、微信扫码支付

4.8.1、SDK二次封装

在sl_pay_channel表中已经提供了微信对接的相关信息。

WechatPayHttpClient.java 

  1. package com.sl.ms.trade.handler.wechat;
  2. import cn.hutool.core.net.url.UrlBuilder;
  3. import cn.hutool.core.net.url.UrlPath;
  4. import cn.hutool.core.net.url.UrlQuery;
  5. import cn.hutool.core.util.CharsetUtil;
  6. import cn.hutool.core.util.ObjectUtil;
  7. import cn.hutool.core.util.StrUtil;
  8. import cn.hutool.extra.spring.SpringUtil;
  9. import cn.hutool.json.JSONObject;
  10. import cn.hutool.json.JSONUtil;
  11. import com.sl.ms.trade.constant.TradingConstant;
  12. import com.sl.ms.trade.entity.PayChannelEntity;
  13. import com.sl.ms.trade.enums.TradingEnum;
  14. import com.sl.ms.trade.handler.wechat.response.WeChatResponse;
  15. import com.sl.ms.trade.service.PayChannelService;
  16. import com.sl.transport.common.exception.SLException;
  17. import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
  18. import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
  19. import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
  20. import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
  21. import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
  22. import lombok.AllArgsConstructor;
  23. import lombok.Builder;
  24. import lombok.Data;
  25. import lombok.NoArgsConstructor;
  26. import org.apache.http.client.methods.CloseableHttpResponse;
  27. import org.apache.http.client.methods.HttpGet;
  28. import org.apache.http.client.methods.HttpPost;
  29. import org.apache.http.entity.StringEntity;
  30. import org.apache.http.impl.client.CloseableHttpClient;
  31. import java.io.ByteArrayInputStream;
  32. import java.net.URI;
  33. import java.nio.charset.StandardCharsets;
  34. import java.security.PrivateKey;
  35. import java.util.Map;
  36. /**
  37. * 微信支付远程调用对象
  38. */
  39. @Data
  40. @Builder
  41. @NoArgsConstructor
  42. @AllArgsConstructor
  43. public class WechatPayHttpClient {
  44. private String mchId; //商户号
  45. private String appId; //应用号
  46. private String privateKey; //私钥字符串
  47. private String mchSerialNo; //商户证书序列号
  48. private String apiV3Key; //V3密钥
  49. private String domain; //请求域名
  50. private String notifyUrl; //请求地址
  51. public static WechatPayHttpClient get(Long enterpriseId) {
  52. // 查询配置
  53. PayChannelService payChannelService = SpringUtil.getBean(PayChannelService.class);
  54. PayChannelEntity payChannel = payChannelService.findByEnterpriseId(enterpriseId, TradingConstant.TRADING_CHANNEL_WECHAT_PAY);
  55. if (ObjectUtil.isEmpty(payChannel)) {
  56. throw new SLException(TradingEnum.CONFIG_EMPTY);
  57. }
  58. //通过渠道对象转化成微信支付的client对象
  59. JSONObject otherConfig = JSONUtil.parseObj(payChannel.getOtherConfig());
  60. return WechatPayHttpClient.builder()
  61. .appId(payChannel.getAppId())
  62. .domain(payChannel.getDomain())
  63. .privateKey(payChannel.getMerchantPrivateKey())
  64. .mchId(otherConfig.getStr("mchId"))
  65. .mchSerialNo(otherConfig.getStr("mchSerialNo"))
  66. .apiV3Key(otherConfig.getStr("apiV3Key"))
  67. .notifyUrl(payChannel.getNotifyUrl())
  68. .build();
  69. }
  70. /***
  71. * 构建CloseableHttpClient远程请求对象
  72. * @return org.apache.http.impl.client.CloseableHttpClient
  73. */
  74. public CloseableHttpClient createHttpClient() throws Exception {
  75. // 加载商户私钥(privateKey:私钥字符串)
  76. PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8)));
  77. // 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
  78. PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, merchantPrivateKey);
  79. WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
  80. // 向证书管理器增加需要自动更新平台证书的商户信息
  81. CertificatesManager certificatesManager = CertificatesManager.getInstance();
  82. certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
  83. // 初始化httpClient
  84. return com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder.create()
  85. .withMerchant(mchId, mchSerialNo, merchantPrivateKey)
  86. .withValidator(new WechatPay2Validator(certificatesManager.getVerifier(mchId)))
  87. .build();
  88. }
  89. /***
  90. * 支持post请求的远程调用
  91. *
  92. * @param apiPath api地址
  93. * @param params 携带请求参数
  94. * @return 返回字符串
  95. */
  96. public WeChatResponse doPost(String apiPath, Map<String, Object> params) throws Exception {
  97. String url = StrUtil.format("https://{}{}", this.domain, apiPath);
  98. HttpPost httpPost = new HttpPost(url);
  99. httpPost.addHeader("Accept", "application/json");
  100. httpPost.addHeader("Content-type", "application/json; charset=utf-8");
  101. String body = JSONUtil.toJsonStr(params);
  102. httpPost.setEntity(new StringEntity(body, CharsetUtil.UTF_8));
  103. CloseableHttpResponse response = this.createHttpClient().execute(httpPost);
  104. return new WeChatResponse(response);
  105. }
  106. /***
  107. * 支持get请求的远程调用
  108. * @param apiPath api地址
  109. * @param params 在路径中请求的参数
  110. * @return 返回字符串
  111. */
  112. public WeChatResponse doGet(String apiPath, Map<String, Object> params) throws Exception {
  113. URI uri = UrlBuilder.create()
  114. .setHost(this.domain)
  115. .setScheme("https")
  116. .setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8))
  117. .setQuery(UrlQuery.of(params))
  118. .setCharset(CharsetUtil.CHARSET_UTF_8)
  119. .toURI();
  120. return this.doGet(uri);
  121. }
  122. /***
  123. * 支持get请求的远程调用
  124. * @param apiPath api地址
  125. * @return 返回字符串
  126. */
  127. public WeChatResponse doGet(String apiPath) throws Exception {
  128. URI uri = UrlBuilder.create()
  129. .setHost(this.domain)
  130. .setScheme("https")
  131. .setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8))
  132. .setCharset(CharsetUtil.CHARSET_UTF_8)
  133. .toURI();
  134. return this.doGet(uri);
  135. }
  136. private WeChatResponse doGet(URI uri) throws Exception {
  137. HttpGet httpGet = new HttpGet(uri);
  138. httpGet.addHeader("Accept", "application/json");
  139. CloseableHttpResponse response = this.createHttpClient().execute(httpGet);
  140. return new WeChatResponse(response);
  141. }
  142. }

代码说明:

  • 通过get(Long enterpriseId)方法查询商户对应的配置信息,最后封装到WechatPayHttpClient对象中。

  • 通过createHttpClient()方法封装了请求微信接口必要的参数,最后返回CloseableHttpClient对象。

  • 封装了doGet()、doPost()方便对微信接口进行调用。

4.8.2、具体实现

  1. package com.sl.ms.trade.handler.wechat;
  2. import cn.hutool.core.convert.Convert;
  3. import cn.hutool.core.map.MapUtil;
  4. import cn.hutool.core.util.NumberUtil;
  5. import cn.hutool.json.JSONUtil;
  6. import com.sl.ms.trade.annotation.PayChannel;
  7. import com.sl.ms.trade.entity.TradingEntity;
  8. import com.sl.ms.trade.enums.PayChannelEnum;
  9. import com.sl.ms.trade.enums.TradingEnum;
  10. import com.sl.ms.trade.enums.TradingStateEnum;
  11. import com.sl.ms.trade.handler.NativePayHandler;
  12. import com.sl.ms.trade.handler.wechat.response.WeChatResponse;
  13. import com.sl.ms.trade.service.PayChannelService;
  14. import com.sl.transport.common.exception.SLException;
  15. import org.springframework.stereotype.Component;
  16. import javax.annotation.Resource;
  17. import java.util.Map;
  18. /**
  19. * 微信二维码支付
  20. */
  21. @Component("wechatNativePayHandler")
  22. @PayChannel(type = PayChannelEnum.WECHAT_PAY)
  23. public class WechatNativePayHandler implements NativePayHandler {
  24. @Override
  25. public void createDownLineTrading(TradingEntity tradingEntity) throws SLException {
  26. // 查询配置
  27. WechatPayHttpClient client = WechatPayHttpClient.get(tradingEntity.getEnterpriseId());
  28. //请求地址
  29. String apiPath = "/v3/pay/transactions/native";
  30. //请求参数
  31. Map<String, Object> params = MapUtil.<String, Object>builder()
  32. .put("mchid", client.getMchId())
  33. .put("appid", client.getAppId())
  34. .put("description", tradingEntity.getMemo())
  35. .put("notify_url", client.getNotifyUrl())
  36. .put("out_trade_no", Convert.toStr(tradingEntity.getTradingOrderNo()))
  37. .put("amount", MapUtil.<String, Object>builder()
  38. .put("total", Convert.toInt(NumberUtil.mul(tradingEntity.getTradingAmount(), 100))) //金额,单位:分
  39. .put("currency", "CNY") //人民币
  40. .build())
  41. .build();
  42. try {
  43. WeChatResponse response = client.doPost(apiPath, params);
  44. if (!response.isOk()) {
  45. //下单失败
  46. throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
  47. }
  48. //指定统一下单code
  49. tradingEntity.setPlaceOrderCode(Convert.toStr(response.getStatus()));
  50. //二维码需要展现的信息
  51. tradingEntity.setPlaceOrderMsg(JSONUtil.parseObj(response.getBody()).getStr("code_url"));
  52. //指定统一下单json字符串
  53. tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response));
  54. //指定交易状态
  55. tradingEntity.setTradingState(TradingStateEnum.FKZ);
  56. } catch (Exception e) {
  57. throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
  58. }
  59. }
  60. }

5、基础服务【阅读代码】

在支付宝或微信平台中,支付方式是多种多样的,对于一些服务而言是通用的,比如:查询交易单、退款、查询退款等,所以我们将基于这些通用的接口封装基础服务。

5.1、查询交易

用户创建交易后,到底有没有支付成功,还是取消支付,这个可以通过查询交易单接口查询的,支付宝和微信也都提供了这样的接口服务。

5.1.1、Controller

com.sl.ms.trade.controller.BasicPayController

  1. /***
  2. * 统一收单线下交易查询
  3. * 该接口提供所有支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。
  4. *
  5. * @param tradingOrderNo 交易单号
  6. * @return 交易单
  7. */
  8. @PostMapping("query/{tradingOrderNo}")
  9. @ApiOperation(value = "查询统一收单线下交易", notes = "查询统一收单线下交易")
  10. @ApiImplicitParam(name = "tradingOrderNo", value = "交易单", required = true)
  11. public TradingDTO queryTrading(@PathVariable("tradingOrderNo") Long tradingOrderNo) {
  12. return this.basicPayService.queryTrading(tradingOrderNo);
  13. }

5.1.2、Service

在Service中实现了交易单查询的逻辑,代码结构与扫描支付类似。具体与支付平台的对接由BasicPayHandler完成。

com.sl.ms.trade.service.impl.BasicPayServiceImpl

  1. @Override
  2. public TradingDTO queryTrading(Long tradingOrderNo) throws SLException {
  3. //通过单号查询交易单数据
  4. TradingEntity trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo);
  5. //查询前置处理:检测交易单参数
  6. this.beforePayHandler.checkQueryTrading(trading);
  7. String key = TradingCacheConstant.QUERY_PAY + tradingOrderNo;
  8. RLock lock = redissonClient.getFairLock(key);
  9. try {
  10. //获取锁
  11. if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
  12. //选取不同的支付渠道实现
  13. BasicPayHandler handler = HandlerFactory.get(trading.getTradingChannel(), BasicPayHandler.class);
  14. Boolean result = handler.queryTrading(trading);
  15. if (result) {
  16. //如果交易单已经完成,需要将二维码数据删除,节省数据库空间,如果有需要可以再次生成
  17. if (ObjectUtil.equalsAny(trading.getTradingState(), TradingStateEnum.YJS, TradingStateEnum.QXDD)) {
  18. trading.setQrCode("");
  19. }
  20. //更新数据
  21. this.tradingService.saveOrUpdate(trading);
  22. }
  23. return BeanUtil.toBean(trading, TradingDTO.class);
  24. }
  25. throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
  26. } catch (SLException e) {
  27. throw e;
  28. } catch (Exception e) {
  29. log.error("查询交易单数据异常: trading = {}", trading, e);
  30. throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
  31. } finally {
  32. lock.unlock();
  33. }
  34. }

5.1.3、支付宝实现

com.sl.ms.trade.handler.alipay.AliBasicPayHandler

  1. @Override
  2. public Boolean queryTrading(TradingEntity trading) throws SLException {
  3. //查询配置
  4. Config config = AlipayConfig.getConfig(trading.getEnterpriseId());
  5. //Factory使用配置
  6. Factory.setOptions(config);
  7. AlipayTradeQueryResponse queryResponse;
  8. try {
  9. //调用支付宝API:通用查询支付情况
  10. queryResponse = Factory
  11. .Payment
  12. .Common()
  13. .query(String.valueOf(trading.getTradingOrderNo()));
  14. } catch (Exception e) {
  15. String msg = StrUtil.format("查询支付宝统一下单失败:trading = {}", trading);
  16. log.error(msg, e);
  17. throw new SLException(msg, TradingEnum.NATIVE_QUERY_FAIL.getCode(), TradingEnum.NATIVE_QUERY_FAIL.getStatus());
  18. }
  19. //修改交易单状态
  20. trading.setResultCode(queryResponse.getSubCode());
  21. trading.setResultMsg(queryResponse.getSubMsg());
  22. trading.setResultJson(JSONUtil.toJsonStr(queryResponse));
  23. boolean success = ResponseChecker.success(queryResponse);
  24. //响应成功,分析交易状态
  25. if (success) {
  26. String tradeStatus = queryResponse.getTradeStatus();
  27. if (StrUtil.equals(TradingConstant.ALI_TRADE_CLOSED, tradeStatus)) {
  28. //支付取消:TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)
  29. trading.setTradingState(TradingStateEnum.QXDD);
  30. } else if (StrUtil.equalsAny(tradeStatus, TradingConstant.ALI_TRADE_SUCCESS, TradingConstant.ALI_TRADE_FINISHED)) {
  31. // TRADE_SUCCESS(交易支付成功)
  32. // TRADE_FINISHED(交易结束,不可退款)
  33. trading.setTradingState(TradingStateEnum.YJS);
  34. } else {
  35. //非最终状态不处理,当前交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)不处理
  36. return false;
  37. }
  38. return true;
  39. }
  40. throw new SLException(trading.getResultJson(), TradingEnum.NATIVE_QUERY_FAIL.getCode(), TradingEnum.NATIVE_QUERY_FAIL.getStatus());
  41. }

5.1.4、微信支付实现

com.sl.ms.trade.handler.wechat.WeChatBasicPayHandler

  1. @Override
  2. public Boolean queryTrading(TradingEntity trading) throws SLException {
  3. // 获取微信支付的client对象
  4. WechatPayHttpClient client = WechatPayHttpClient.get(trading.getEnterpriseId());
  5. //请求地址
  6. String apiPath = StrUtil.format("/v3/pay/transactions/out-trade-no/{}", trading.getTradingOrderNo());
  7. //请求参数
  8. Map<String, Object> params = MapUtil.<String, Object>builder()
  9. .put("mchid", client.getMchId())
  10. .build();
  11. WeChatResponse response;
  12. try {
  13. response = client.doGet(apiPath, params);
  14. } catch (Exception e) {
  15. log.error("调用微信接口出错!apiPath = {}, params = {}", apiPath, JSONUtil.toJsonStr(params), e);
  16. throw new SLException(NATIVE_REFUND_FAIL, e);
  17. }
  18. if (response.isOk()) {
  19. JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
  20. // 交易状态,枚举值:
  21. // SUCCESS:支付成功
  22. // REFUND:转入退款
  23. // NOTPAY:未支付
  24. // CLOSED:已关闭
  25. // REVOKED:已撤销(仅付款码支付会返回)
  26. // USERPAYING:用户支付中(仅付款码支付会返回)
  27. // PAYERROR:支付失败(仅付款码支付会返回)
  28. String tradeStatus = jsonObject.getStr("trade_state");
  29. if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_CLOSED, TradingConstant.WECHAT_TRADE_REVOKED)) {
  30. trading.setTradingState(TradingStateEnum.QXDD);
  31. } else if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_REFUND_SUCCESS, TradingConstant.WECHAT_TRADE_REFUND)) {
  32. trading.setTradingState(TradingStateEnum.YJS);
  33. } else if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_NOTPAY)) {
  34. //如果是未支付,需要判断下时间,超过2小时未知的订单需要关闭订单以及设置状态为QXDD
  35. long between = LocalDateTimeUtil.between(trading.getCreated(), LocalDateTimeUtil.now(), ChronoUnit.HOURS);
  36. if (between >= 2) {
  37. return this.closeTrading(trading);
  38. }
  39. } else {
  40. //非最终状态不处理
  41. return false;
  42. }
  43. //修改交易单状态
  44. trading.setResultCode(tradeStatus);
  45. trading.setResultMsg(jsonObject.getStr("trade_state_desc"));
  46. trading.setResultJson(response.getBody());
  47. return true;
  48. }
  49. throw new SLException(response.getBody(), NATIVE_REFUND_FAIL.getCode(), NATIVE_REFUND_FAIL.getCode());
  50. }

5.2、退款

5.2.1、Controller

com.sl.ms.trade.controller.BasicPayController

  1. /***
  2. * 统一收单交易退款接口
  3. * 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,
  4. * 将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
  5. * @param tradingOrderNo 交易单号
  6. * @param refundAmount 退款金额
  7. * @return
  8. */
  9. @PostMapping("refund")
  10. @ApiOperation(value = "统一收单交易退款", notes = "统一收单交易退款")
  11. @ApiImplicitParams({
  12. @ApiImplicitParam(name = "tradingOrderNo", value = "交易单号", required = true),
  13. @ApiImplicitParam(name = "refundAmount", value = "退款金额", required = true)
  14. })
  15. public void refundTrading(@RequestParam("tradingOrderNo") Long tradingOrderNo,
  16. @RequestParam("refundAmount") BigDecimal refundAmount) {
  17. Boolean result = this.basicPayService.refundTrading(tradingOrderNo, refundAmount);
  18. if (!result) {
  19. throw new SLException(TradingEnum.BASIC_REFUND_COUNT_OUT_FAIL);
  20. }
  21. }

5.2.2、Service

com.sl.ms.trade.service.impl.BasicPayServiceImpl

  1. @Override
  2. @Transactional
  3. public Boolean refundTrading(Long tradingOrderNo, BigDecimal refundAmount) throws SLException {
  4. //通过单号查询交易单数据
  5. TradingEntity trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo);
  6. //设置退款金额
  7. trading.setRefund(NumberUtil.add(refundAmount, trading.getRefund()));
  8. //入库前置检查
  9. this.beforePayHandler.checkRefundTrading(trading);
  10. String key = TradingCacheConstant.REFUND_PAY + tradingOrderNo;
  11. RLock lock = redissonClient.getFairLock(key);
  12. try {
  13. //获取锁
  14. if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
  15. //幂等性的检查
  16. RefundRecordEntity refundRecord = this.beforePayHandler.idempotentRefundTrading(trading, refundAmount);
  17. if (null == refundRecord) {
  18. return false;
  19. }
  20. //选取不同的支付渠道实现
  21. BasicPayHandler handler = HandlerFactory.get(refundRecord.getTradingChannel(), BasicPayHandler.class);
  22. Boolean result = handler.refundTrading(refundRecord);
  23. if (result) {
  24. //更新退款记录数据
  25. this.refundRecordService.saveOrUpdate(refundRecord);
  26. //设置交易单是退款订单
  27. trading.setIsRefund(Constants.YES);
  28. this.tradingService.saveOrUpdate(trading);
  29. }
  30. return true;
  31. }
  32. throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
  33. } catch (SLException e) {
  34. throw e;
  35. } catch (Exception e) {
  36. log.error("查询交易单数据异常:{}", ExceptionUtil.stacktraceToString(e));
  37. throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
  38. } finally {
  39. lock.unlock();
  40. }
  41. }

5.2.3、支付宝实现

com.sl.ms.trade.handler.alipay.AliBasicPayHandler

  1. @Override
  2. public Boolean refundTrading(RefundRecordEntity refundRecord) throws SLException {
  3. //查询配置
  4. Config config = AlipayConfig.getConfig(refundRecord.getEnterpriseId());
  5. //Factory使用配置
  6. Factory.setOptions(config);
  7. //调用支付宝API:通用查询支付情况
  8. AlipayTradeRefundResponse refundResponse;
  9. try {
  10. // 支付宝easy sdk
  11. refundResponse = Factory
  12. .Payment
  13. .Common()
  14. //扩展参数:退款单号
  15. .optional("out_request_no", refundRecord.getRefundNo())
  16. .refund(Convert.toStr(refundRecord.getTradingOrderNo()),
  17. Convert.toStr(refundRecord.getRefundAmount()));
  18. } catch (Exception e) {
  19. String msg = StrUtil.format("调用支付宝退款接口出错!refundRecord = {}", refundRecord);
  20. log.error(msg, e);
  21. throw new SLException(msg, TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
  22. }
  23. refundRecord.setRefundCode(refundResponse.getSubCode());
  24. refundRecord.setRefundMsg(JSONUtil.toJsonStr(refundResponse));
  25. boolean success = ResponseChecker.success(refundResponse);
  26. if (success) {
  27. refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
  28. return true;
  29. }
  30. throw new SLException(refundRecord.getRefundMsg(), TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
  31. }

5.2.4、微信实现

com.sl.ms.trade.handler.wechat.WeChatBasicPayHandler

  1. @Override
  2. public Boolean refundTrading(RefundRecordEntity refundRecord) throws SLException {
  3. // 获取微信支付的client对象
  4. WechatPayHttpClient client = WechatPayHttpClient.get(refundRecord.getEnterpriseId());
  5. //请求地址
  6. String apiPath = "/v3/refund/domestic/refunds";
  7. //请求参数
  8. Map<String, Object> params = MapUtil.<String, Object>builder()
  9. .put("out_refund_no", Convert.toStr(refundRecord.getRefundNo()))
  10. .put("out_trade_no", Convert.toStr(refundRecord.getTradingOrderNo()))
  11. .put("amount", MapUtil.<String, Object>builder()
  12. .put("refund", NumberUtil.mul(refundRecord.getRefundAmount(), 100)) //本次退款金额
  13. .put("total", NumberUtil.mul(refundRecord.getTotal(), 100)) //原订单金额
  14. .put("currency", "CNY") //币种
  15. .build())
  16. .build();
  17. WeChatResponse response;
  18. try {
  19. response = client.doPost(apiPath, params);
  20. } catch (Exception e) {
  21. log.error("调用微信接口出错!apiPath = {}, params = {}", apiPath, JSONUtil.toJsonStr(params), e);
  22. throw new SLException(NATIVE_REFUND_FAIL, e);
  23. }
  24. refundRecord.setRefundCode(Convert.toStr(response.getStatus()));
  25. refundRecord.setRefundMsg(response.getBody());
  26. if (response.isOk()) {
  27. JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
  28. // SUCCESS:退款成功
  29. // CLOSED:退款关闭
  30. // PROCESSING:退款处理中
  31. // ABNORMAL:退款异常
  32. String status = jsonObject.getStr("status");
  33. if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_PROCESSING)) {
  34. refundRecord.setRefundStatus(RefundStatusEnum.SENDING);
  35. } else if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_SUCCESS)) {
  36. refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
  37. } else {
  38. refundRecord.setRefundStatus(RefundStatusEnum.FAIL);
  39. }
  40. return true;
  41. }
  42. throw new SLException(refundRecord.getRefundMsg(), NATIVE_REFUND_FAIL.getCode(), NATIVE_REFUND_FAIL.getStatus());
  43. }

5.3、查询退款

5.3.1、Controller

com.sl.ms.trade.controller.BasicPayController

  1. /***
  2. * 统一收单交易退款查询接口
  3. * @param refundNo 退款交易单号
  4. * @return
  5. */
  6. @PostMapping("refund/{refundNo}")
  7. @ApiOperation(value = "查询统一收单交易退款", notes = "查询统一收单交易退款")
  8. @ApiImplicitParam(name = "refundNo", value = "退款交易单", required = true)
  9. public RefundRecordDTO queryRefundDownLineTrading(@PathVariable("refundNo") Long refundNo) {
  10. return this.basicPayService.queryRefundTrading(refundNo);
  11. }

5.3.2、Service

com.sl.ms.trade.service.impl.BasicPayServiceImpl

  1. @Override
  2. public RefundRecordDTO queryRefundTrading(Long refundNo) throws SLException {
  3. //通过单号查询交易单数据
  4. RefundRecordEntity refundRecord = this.refundRecordService.findByRefundNo(refundNo);
  5. //查询前置处理
  6. this.beforePayHandler.checkQueryRefundTrading(refundRecord);
  7. String key = TradingCacheConstant.REFUND_QUERY_PAY + refundNo;
  8. RLock lock = redissonClient.getFairLock(key);
  9. try {
  10. //获取锁
  11. if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
  12. //选取不同的支付渠道实现
  13. BasicPayHandler handler = HandlerFactory.get(refundRecord.getTradingChannel(), BasicPayHandler.class);
  14. Boolean result = handler.queryRefundTrading(refundRecord);
  15. if (result) {
  16. //更新数据
  17. this.refundRecordService.saveOrUpdate(refundRecord);
  18. }
  19. return BeanUtil.toBean(refundRecord, RefundRecordDTO.class);
  20. }
  21. throw new SLException(TradingEnum.REFUND_FAIL);
  22. } catch (SLException e) {
  23. throw e;
  24. } catch (Exception e) {
  25. log.error("查询退款交易单数据异常: refundRecord = {}", refundRecord, e);
  26. throw new SLException(TradingEnum.REFUND_FAIL);
  27. } finally {
  28. lock.unlock();
  29. }
  30. }

5.3.3、支付宝实现

com.sl.ms.trade.handler.alipay.AliBasicPayHandler

  1. @Override
  2. public Boolean queryRefundTrading(RefundRecordEntity refundRecord) throws SLException {
  3. //查询配置
  4. Config config = AlipayConfig.getConfig(refundRecord.getEnterpriseId());
  5. //Factory使用配置
  6. Factory.setOptions(config);
  7. AlipayTradeFastpayRefundQueryResponse response;
  8. try {
  9. response = Factory.Payment.Common().queryRefund(
  10. Convert.toStr(refundRecord.getTradingOrderNo()),
  11. Convert.toStr(refundRecord.getRefundNo()));
  12. } catch (Exception e) {
  13. log.error("调用支付宝查询退款接口出错!refundRecord = {}", refundRecord, e);
  14. throw new SLException(TradingEnum.NATIVE_REFUND_FAIL, e);
  15. }
  16. refundRecord.setRefundCode(response.getSubCode());
  17. refundRecord.setRefundMsg(JSONUtil.toJsonStr(response));
  18. boolean success = ResponseChecker.success(response);
  19. if (success) {
  20. refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
  21. return true;
  22. }
  23. throw new SLException(refundRecord.getRefundMsg(), TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
  24. }

5.3.4、微信支付实现

com.sl.ms.trade.handler.wechat.WeChatBasicPayHandler

  1. @Override
  2. public Boolean queryRefundTrading(RefundRecordEntity refundRecord) throws SLException {
  3. // 获取微信支付的client对象
  4. WechatPayHttpClient client = WechatPayHttpClient.get(refundRecord.getEnterpriseId());
  5. //请求地址
  6. String apiPath = StrUtil.format("/v3/refund/domestic/refunds/{}", refundRecord.getRefundNo());
  7. WeChatResponse response;
  8. try {
  9. response = client.doGet(apiPath);
  10. } catch (Exception e) {
  11. log.error("调用微信接口出错!apiPath = {}", apiPath, e);
  12. throw new SLException(NATIVE_QUERY_REFUND_FAIL, e);
  13. }
  14. refundRecord.setRefundCode(Convert.toStr(response.getStatus()));
  15. refundRecord.setRefundMsg(response.getBody());
  16. if (response.isOk()) {
  17. JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
  18. // SUCCESS:退款成功
  19. // CLOSED:退款关闭
  20. // PROCESSING:退款处理中
  21. // ABNORMAL:退款异常
  22. String status = jsonObject.getStr("status");
  23. if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_PROCESSING)) {
  24. refundRecord.setRefundStatus(RefundStatusEnum.SENDING);
  25. } else if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_SUCCESS)) {
  26. refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
  27. } else {
  28. refundRecord.setRefundStatus(RefundStatusEnum.FAIL);
  29. }
  30. return true;
  31. }
  32. throw new SLException(response.getBody(), NATIVE_QUERY_REFUND_FAIL.getCode(), NATIVE_QUERY_REFUND_FAIL.getStatus());
  33. }

6、同步支付状态【阅读代码】

在支付平台创建交易单后,如果用户支付成功,我们怎么知道支付成功了呢?一般的做法有两种,分别是【异步通知】和【主动查询】,基本的流程如下:

说明:

  • 在用户支付成功后,【步骤4】支付平台会通知【支付微服务】,这个就是异步通知,需要在【支付微服务】中对外暴露接口

  • 由于网络的不确定性,异步通知可能出现故障【步骤6】

  • 支付微服务中需要有定时任务,查询正在支付中的订单的状态

  • 可以看出【异步通知】与【主动定时查询】这两种方式是互不的,缺一不可。

6.1、异步通知

支付宝和微信都提供了异步通知功能,具体参考官方文档:

异步通知参数-支付宝

支付通知API-微信

6.1.1、内网穿透

异步通知的是需要通过外网的域名地址请求到的,由于我们还没有真正上线,那支付平台如何请求到我们本地服务的呢?

这里可以使用【内网穿透】技术来实现,通过【内网穿透软件】将内网与外网通过隧道打通,外网可以读取内网中的数据。

在这里推荐2个免费的内网穿透服务,分别是:

cpolar-安全的内网穿透工具

NATAPP - 内网穿透 基于ngrok的国内高速内网映射工具

这里以【cpolar】为例,介绍使用方法:

第一步,安装cpolar: Windows的安装包在资料目录中,101机器的已经按照完成,在 /usr/local/src/cpolar目录下。

第二步,注册账号并且登录。

第三步,设置token: 请求 cpolar - secure introspectable tunnels to localhost 页面,查看命令./cpolar authtoken xxxx后面的【xxxx】就是你自己的token,每个人是不一样的。token只需要设置一次。

第四步,设置端口映射:

例如:./cpolar http 18096端口改成你自己的端口。

在线查看:

image.png

https协议的url写入到sl_pay_channel表的notify_url字段中,例如:

https://39808c89.vip.cpolar.cn/trade/notify/wx/%7BenterpriseId%7D

注意:cpolar的域名每次启动服务都不一样,每个人的也都不一样,需要改成你自己的那个域名。 

6.1.2、NotifyController

com.sl.ms.trade.controller.NotifyController

  1. package com.sl.ms.trade.controller;
  2. import cn.hutool.core.map.MapUtil;
  3. import com.sl.ms.trade.service.NotifyService;
  4. import com.sl.transport.common.exception.SLException;
  5. import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
  6. import io.swagger.annotations.Api;
  7. import org.springframework.http.HttpEntity;
  8. import org.springframework.http.HttpHeaders;
  9. import org.springframework.http.HttpStatus;
  10. import org.springframework.http.ResponseEntity;
  11. import org.springframework.web.bind.annotation.PathVariable;
  12. import org.springframework.web.bind.annotation.PostMapping;
  13. import org.springframework.web.bind.annotation.RequestMapping;
  14. import org.springframework.web.bind.annotation.RestController;
  15. import javax.annotation.Resource;
  16. import javax.servlet.http.HttpServletRequest;
  17. import java.util.Map;
  18. /**
  19. * 支付结果的通知
  20. */
  21. @RestController
  22. @Api(tags = "支付通知")
  23. @RequestMapping("notify")
  24. public class NotifyController {
  25. @Resource
  26. private NotifyService notifyService;
  27. /**
  28. * 微信支付成功回调(成功后无需响应内容)
  29. *
  30. * @param httpEntity 微信请求信息
  31. * @param enterpriseId 商户id
  32. * @return 正常响应200,否则响应500
  33. */
  34. @PostMapping("wx/{enterpriseId}")
  35. public ResponseEntity<Object> wxPayNotify(HttpEntity<String> httpEntity, @PathVariable("enterpriseId") Long enterpriseId) {
  36. try {
  37. //获取请求头
  38. HttpHeaders headers = httpEntity.getHeaders();
  39. //构建微信请求数据对象
  40. NotificationRequest request = new NotificationRequest.Builder()
  41. .withSerialNumber(headers.getFirst("Wechatpay-Serial")) //证书序列号(微信平台)
  42. .withNonce(headers.getFirst("Wechatpay-Nonce")) //随机串
  43. .withTimestamp(headers.getFirst("Wechatpay-Timestamp")) //时间戳
  44. .withSignature(headers.getFirst("Wechatpay-Signature")) //签名字符串
  45. .withBody(httpEntity.getBody())
  46. .build();
  47. //微信通知的业务处理
  48. this.notifyService.wxPayNotify(request, enterpriseId);
  49. } catch (SLException e) {
  50. Map<String, Object> result = MapUtil.<String, Object>builder()
  51. .put("code", "FAIL")
  52. .put("message", e.getMsg())
  53. .build();
  54. //响应500
  55. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
  56. }
  57. return ResponseEntity.ok(null);
  58. }
  59. /**
  60. * 支付宝支付成功回调(成功后需要响应success)
  61. *
  62. * @param enterpriseId 商户id
  63. * @return 正常响应200,否则响应500
  64. */
  65. @PostMapping("alipay/{enterpriseId}")
  66. public ResponseEntity<String> aliPayNotify(HttpServletRequest request,
  67. @PathVariable("enterpriseId") Long enterpriseId) {
  68. try {
  69. //支付宝通知的业务处理
  70. this.notifyService.aliPayNotify(request, enterpriseId);
  71. } catch (SLException e) {
  72. //响应500
  73. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
  74. }
  75. return ResponseEntity.ok("success");
  76. }
  77. }

异步通知debug测试时,三方支付平台会发起多个重试请求,会导致debug无法拦截每个请求,需要将debug模式设置成单线程模式,如下:(断点红球上点右键进行设置)

image.png

6.1.3、NotifyService

com.sl.ms.trade.service.NotifyService

  1. package com.sl.ms.trade.service;
  2. import com.sl.transport.common.exception.SLException;
  3. import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
  4. import javax.servlet.http.HttpServletRequest;
  5. /**
  6. * 支付通知
  7. */
  8. public interface NotifyService {
  9. /**
  10. * 微信支付通知,官方文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
  11. *
  12. * @param request 微信请求对象
  13. * @param enterpriseId 商户id
  14. * @throws SLException 抛出SL异常,通过异常决定是否响应200
  15. */
  16. void wxPayNotify(NotificationRequest request, Long enterpriseId) throws SLException;
  17. /**
  18. * 支付宝支付通知,官方文档:https://opendocs.alipay.com/open/194/103296?ref=api
  19. *
  20. * @param request 请求对象
  21. * @param enterpriseId 商户id
  22. * @throws SLException 抛出SL异常,通过异常决定是否响应200
  23. */
  24. void aliPayNotify(HttpServletRequest request, Long enterpriseId) throws SLException;
  25. }

6.1.4、NotifyServiceImpl

注意:

  • 支付成功的通知请求,一定要确保是真正来自支付平台,防止伪造请求造成数据错误,导致财产损失

  • 对于响应会数据需要进行解密处理 

com.sl.ms.trade.service.impl.NotifyServiceImpl

  1. package com.sl.ms.trade.service.impl;
  2. import cn.hutool.core.convert.Convert;
  3. import cn.hutool.core.util.StrUtil;
  4. import cn.hutool.json.JSONObject;
  5. import cn.hutool.json.JSONUtil;
  6. import com.alipay.easysdk.factory.Factory;
  7. import com.alipay.easysdk.kernel.Config;
  8. import com.sl.ms.base.api.common.MQFeign;
  9. import com.sl.ms.trade.constant.TradingCacheConstant;
  10. import com.sl.ms.trade.constant.TradingConstant;
  11. import com.sl.ms.trade.entity.TradingEntity;
  12. import com.sl.ms.trade.enums.TradingStateEnum;
  13. import com.sl.ms.trade.handler.alipay.AlipayConfig;
  14. import com.sl.ms.trade.handler.wechat.WechatPayHttpClient;
  15. import com.sl.ms.trade.service.NotifyService;
  16. import com.sl.ms.trade.service.TradingService;
  17. import com.sl.transport.common.constant.Constants;
  18. import com.sl.transport.common.exception.SLException;
  19. import com.sl.transport.common.vo.TradeStatusMsg;
  20. import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
  21. import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
  22. import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
  23. import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
  24. import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
  25. import lombok.extern.slf4j.Slf4j;
  26. import org.redisson.api.RLock;
  27. import org.redisson.api.RedissonClient;
  28. import org.springframework.stereotype.Service;
  29. import javax.annotation.Resource;
  30. import javax.servlet.http.HttpServletRequest;
  31. import java.nio.charset.StandardCharsets;
  32. import java.util.Collections;
  33. import java.util.HashMap;
  34. import java.util.Map;
  35. import java.util.concurrent.TimeUnit;
  36. /**
  37. * 支付成功的通知处理
  38. */
  39. @Slf4j
  40. @Service
  41. public class NotifyServiceImpl implements NotifyService {
  42. @Resource
  43. private TradingService tradingService;
  44. @Resource
  45. private RedissonClient redissonClient;
  46. @Resource
  47. private MQFeign mqFeign;
  48. @Override
  49. public void wxPayNotify(NotificationRequest request, Long enterpriseId) throws SLException {
  50. // 查询配置
  51. WechatPayHttpClient client = WechatPayHttpClient.get(enterpriseId);
  52. JSONObject jsonData;
  53. //验证签名,确保请求来自微信
  54. try {
  55. //确保在管理器中存在自动更新的商户证书
  56. client.createHttpClient();
  57. CertificatesManager certificatesManager = CertificatesManager.getInstance();
  58. Verifier verifier = certificatesManager.getVerifier(client.getMchId());
  59. //验签和解析请求数据
  60. NotificationHandler notificationHandler = new NotificationHandler(verifier, client.getApiV3Key().getBytes(StandardCharsets.UTF_8));
  61. Notification notification = notificationHandler.parse(request);
  62. if (!StrUtil.equals("TRANSACTION.SUCCESS", notification.getEventType())) {
  63. //非成功请求直接返回,理论上都是成功的请求
  64. return;
  65. }
  66. //获取解密后的数据
  67. jsonData = JSONUtil.parseObj(notification.getDecryptData());
  68. } catch (Exception e) {
  69. throw new SLException("验签失败");
  70. }
  71. if (!StrUtil.equals(jsonData.getStr("trade_state"), TradingConstant.WECHAT_TRADE_SUCCESS)) {
  72. return;
  73. }
  74. //交易单号
  75. Long tradingOrderNo = jsonData.getLong("out_trade_no");
  76. log.info("微信支付通知:tradingOrderNo = {}, data = {}", tradingOrderNo, jsonData);
  77. //更新交易单
  78. this.updateTrading(tradingOrderNo, jsonData.getStr("trade_state_desc"), jsonData.toString());
  79. }
  80. private void updateTrading(Long tradingOrderNo, String resultMsg, String resultJson) {
  81. String key = TradingCacheConstant.CREATE_PAY + tradingOrderNo;
  82. RLock lock = redissonClient.getFairLock(key);
  83. try {
  84. //获取锁
  85. if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
  86. TradingEntity trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo);
  87. if (trading.getTradingState() == TradingStateEnum.YJS) {
  88. // 已付款
  89. return;
  90. }
  91. //设置成付款成功
  92. trading.setTradingState(TradingStateEnum.YJS);
  93. //清空二维码数据
  94. trading.setQrCode("");
  95. trading.setResultMsg(resultMsg);
  96. trading.setResultJson(resultJson);
  97. this.tradingService.saveOrUpdate(trading);
  98. // 发消息通知其他系统支付成功
  99. TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
  100. .tradingOrderNo(trading.getTradingOrderNo())
  101. .productOrderNo(trading.getProductOrderNo())
  102. .statusCode(TradingStateEnum.YJS.getCode())
  103. .statusName(TradingStateEnum.YJS.name())
  104. .build();
  105. String msg = JSONUtil.toJsonStr(Collections.singletonList(tradeStatusMsg));
  106. this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, Constants.MQ.RoutingKeys.TRADE_UPDATE_STATUS, msg);
  107. return;
  108. }
  109. } catch (Exception e) {
  110. throw new SLException("处理业务失败");
  111. } finally {
  112. lock.unlock();
  113. }
  114. throw new SLException("处理业务失败");
  115. }
  116. @Override
  117. public void aliPayNotify(HttpServletRequest request, Long enterpriseId) throws SLException {
  118. //获取参数
  119. Map<String, String[]> parameterMap = request.getParameterMap();
  120. Map<String, String> param = new HashMap<>();
  121. for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
  122. param.put(entry.getKey(), StrUtil.join(",", entry.getValue()));
  123. }
  124. String tradeStatus = param.get("trade_status");
  125. if (!StrUtil.equals(tradeStatus, TradingConstant.ALI_TRADE_SUCCESS)) {
  126. return;
  127. }
  128. //查询配置
  129. Config config = AlipayConfig.getConfig(enterpriseId);
  130. Factory.setOptions(config);
  131. try {
  132. Boolean result = Factory
  133. .Payment
  134. .Common().verifyNotify(param);
  135. if (!result) {
  136. throw new SLException("验签失败");
  137. }
  138. } catch (Exception e) {
  139. throw new SLException("验签失败");
  140. }
  141. //获取交易单号
  142. Long tradingOrderNo = Convert.toLong(param.get("out_trade_no"));
  143. //更新交易单
  144. this.updateTrading(tradingOrderNo, "支付成功", JSONUtil.toJsonStr(param));
  145. }
  146. }

6.1.5、网关对外暴露接口

bootsarp-{profile}.yml文件中增加如下内容:

  1. - id: sl-express-ms-trade
  2. uri: lb://sl-express-ms-trade
  3. predicates:
  4. - Path=/trade/notify/**
  5. filters:
  6. - StripPrefix=1
  7. - AddRequestHeader=X-Request-From, sl-express-gateway

说明:对于支付系统在网关中的暴露仅仅暴露通知接口,其他接口不暴露。

6.2、定时任务

一般在项目中实现定时任务主要是两种技术方案,一种是Spring Task,另一种是xxl-job,其中Spring Task是适合单体项目中使用,而xxl-job是分布式任务调度框架,更适合在分布式项目中使用,所以在支付微服务中我们将采用xxl-job来实现。

6.2.1、分布式任务调度

微服务架构体系中,服务之间通过网络交互来完成业务处理的,在分布式架构下,一个服务往往会部署多个实例来运行我们的业务,如果在这种分布式系统环境下运行任务调度,我们称之为分布式任务调度

image-20210729230059884.png

分布式系统的特点,并且提高任务的调度处理能力:

  • 并行任务调度

    • 集群部署单个服务,这样就可以多台计算机共同去完成任务调度,我们可以将任务分割为若干个分片,由不同的实例并行执行,来提高任务调度的处理效率。

  • 高可用

    • 若某一个实例宕机,不影响其他实例来执行任务。

  • 弹性扩容

    • 当集群中增加实例就可以提高并执行任务的处理效率。

  • 任务管理与监测

    • 对系统中存在的所有定时任务进行统一的管理及监测。

    • 让开发人员及运维人员能够时刻了解任务执行情况,从而做出快速的应急处理响应。

6.2.2、xxl-Job简介

XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 官网地址: xxl-job架构图(官图):

image.png

6.2.3、部署安装

我们采用docker进行部署安装xxl-job的调度中心,目前已经安装完成,直接访问即可:http://xxl-job.sl-express.com/xxl-job-admin/

image.png

安装命令:

  1. docker run \
  2. -e PARAMS="--spring.datasource.url=jdbc:mysql://192.168.150.101:3306/xxl_job?Unicode=true&characterEncoding=UTF-8 \
  3. --spring.datasource.username=root \
  4. --spring.datasource.password=123" \
  5. --restart=always \
  6. -p 28080:8080 \
  7. -v xxl-job-admin-applogs:/data/applogs \
  8. --name xxl-job-admin \
  9. -d \
  10. xuxueli/xxl-job-admin:2.3.0
  • 默认端口映射到28080

  • 日志挂载到/var/lib/docker/volumes/xxl-job-admin-applogs

  • 通过PARAMS环境变量设置数据库链接参数

  • 数据库脚本:doc/db/tables_xxl_job.sql · 许雪里/xxl-job - Gitee.com ::: xxl-job共用到8张表,如下:

    image.png

  • xxl_job_lock:任务调度锁表;

  • xxl_job_group:执行器信息表,维护任务执行器信息;

  • xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;

  • xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;

  • xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到;

  • xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;

  • xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;

  • xxl_job_user:系统用户表; 

6.2.4、编写任务代码

拉取编写任务的示例代码进行学习:http://git.sl-express.com/sl/sl-express-xxl-job

image.png

运行之前,首先需要创建执行管理器:

image.png

创建任务:

image.png

image.png

xxl-job支持的路由策略非常丰富:

  • FIRST(第一个):固定选择第一个机器;

  • LAST(最后一个):固定选择最后一个机器;

  • ROUND(轮询):在线的机器按照顺序一次执行一个

  • RANDOM(随机):随机选择在线的机器;

  • CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。

  • LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;

  • LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;

  • FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;

  • BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;

  • SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;

xxl-job配置:

  1. package com.sl.xxljob.config;
  2. import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.beans.factory.annotation.Value;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. /**
  9. * xxl-job config
  10. */
  11. @Configuration
  12. public class XxlJobConfig {
  13. private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
  14. @Value("${xxl.job.admin.addresses}")
  15. private String adminAddresses;
  16. @Value("${xxl.job.accessToken:}")
  17. private String accessToken;
  18. @Value("${xxl.job.executor.appname}")
  19. private String appname;
  20. @Value("${xxl.job.executor.address:}")
  21. private String address;
  22. @Value("${xxl.job.executor.ip:}")
  23. private String ip;
  24. @Value("${xxl.job.executor.port:0}")
  25. private int port;
  26. @Value("${xxl.job.executor.logpath:}")
  27. private String logPath;
  28. @Value("${xxl.job.executor.logretentiondays:}")
  29. private int logRetentionDays;
  30. @Bean
  31. public XxlJobSpringExecutor xxlJobExecutor() {
  32. logger.info(">>>>>>>>>>> xxl-job config init.");
  33. XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
  34. xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
  35. xxlJobSpringExecutor.setAppname(appname);
  36. xxlJobSpringExecutor.setAddress(address);
  37. xxlJobSpringExecutor.setIp(ip);
  38. xxlJobSpringExecutor.setPort(port);
  39. xxlJobSpringExecutor.setAccessToken(accessToken);
  40. xxlJobSpringExecutor.setLogPath(logPath);
  41. xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
  42. return xxlJobSpringExecutor;
  43. }
  44. }

任务代码:

com.sl.xxljob.job.JobHandler

  1. package com.sl.xxljob.job;
  2. import cn.hutool.core.util.NumberUtil;
  3. import cn.hutool.core.util.RandomUtil;
  4. import com.xxl.job.core.context.XxlJobHelper;
  5. import com.xxl.job.core.handler.annotation.XxlJob;
  6. import org.springframework.stereotype.Component;
  7. import java.time.LocalDateTime;
  8. import java.util.Arrays;
  9. import java.util.List;
  10. /**
  11. * 任务处理器
  12. */
  13. @Component
  14. public class JobHandler {
  15. private List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5);
  16. /**
  17. * 普通任务
  18. */
  19. @XxlJob("firstJob")
  20. public void firstJob() throws Exception {
  21. System.out.println("firstJob执行了.... " + LocalDateTime.now());
  22. for (Integer data : dataList) {
  23. XxlJobHelper.log("data= {}", data);
  24. Thread.sleep(RandomUtil.randomInt(100, 500));
  25. }
  26. System.out.println("firstJob执行结束了.... " + LocalDateTime.now());
  27. }
  28. /**
  29. * 分片式任务
  30. */
  31. @XxlJob("shardingJob")
  32. public void shardingJob() throws Exception {
  33. // 分片参数
  34. // 分片节点总数
  35. int shardTotal = XxlJobHelper.getShardTotal();
  36. // 当前节点下标,从0开始
  37. int shardIndex = XxlJobHelper.getShardIndex();
  38. System.out.println("shardingJob执行了.... " + LocalDateTime.now());
  39. for (Integer data : dataList) {
  40. if (data % shardTotal == shardIndex) {
  41. XxlJobHelper.log("data= {}", data);
  42. Thread.sleep(RandomUtil.randomInt(100, 500));
  43. }
  44. }
  45. System.out.println("shardingJob执行结束了.... " + LocalDateTime.now());
  46. }
  47. }

分片式任务的测试:

节点1

  1. 2022-08-17 19:32:45 [com.xxl.job.core.thread.JobThread#run]-[130]-[Thread-10]
  2. ----------- xxl-job job execute start -----------
  3. ----------- Param:
  4. 2022-08-17 19:32:45 [com.sl.xxljob.job.JobHandler#shardingJob]-[40]-[Thread-10] data= 1
  5. 2022-08-17 19:32:45 [com.sl.xxljob.job.JobHandler#shardingJob]-[40]-[Thread-10] data= 3
  6. 2022-08-17 19:32:45 [com.sl.xxljob.job.JobHandler#shardingJob]-[40]-[Thread-10] data= 5
  7. 2022-08-17 19:32:46 [com.xxl.job.core.thread.JobThread#run]-[176]-[Thread-10]
  8. ----------- xxl-job job execute end(finish) -----------
  9. ----------- Result: handleCode=200, handleMsg = null
  10. 2022-08-17 19:32:46 [com.xxl.job.core.thread.TriggerCallbackThread#callbackLog]-[197]-[xxl-job, executor TriggerCallbackThread]
  11. ----------- xxl-job job callback finish.
  12. [Load Log Finish]

节点2 

  1. 2022-08-17 19:32:45 [com.xxl.job.core.thread.JobThread#run]-[130]-[Thread-10]
  2. ----------- xxl-job job execute start -----------
  3. ----------- Param:
  4. 2022-08-17 19:32:45 [com.sl.xxljob.job.JobHandler#shardingJob]-[40]-[Thread-10] data= 2
  5. 2022-08-17 19:32:45 [com.sl.xxljob.job.JobHandler#shardingJob]-[40]-[Thread-10] data= 4
  6. 2022-08-17 19:32:45 [com.xxl.job.core.thread.JobThread#run]-[176]-[Thread-10]
  7. ----------- xxl-job job execute end(finish) -----------
  8. ----------- Result: handleCode=200, handleMsg = null
  9. 2022-08-17 19:32:45 [com.xxl.job.core.thread.TriggerCallbackThread#callbackLog]-[197]-[xxl-job, executor TriggerCallbackThread]
  10. ----------- xxl-job job callback finish.
  11. [Load Log Finish]

可以看出,2个节点共同完成的任务处理,并且没有重复,这样提高了任务处理能力。

6.2.5、调度流程

6.3、TradeJob

在此任务中包含两个任务,一个是查询支付状态,另一个是查询退款状态。

  1. package com.sl.ms.trade.job;
  2. import cn.hutool.core.collection.CollUtil;
  3. import cn.hutool.core.util.NumberUtil;
  4. import cn.hutool.json.JSONUtil;
  5. import com.sl.ms.base.api.common.MQFeign;
  6. import com.sl.ms.trade.domain.RefundRecordDTO;
  7. import com.sl.ms.trade.domain.TradingDTO;
  8. import com.sl.ms.trade.entity.RefundRecordEntity;
  9. import com.sl.ms.trade.entity.TradingEntity;
  10. import com.sl.ms.trade.enums.RefundStatusEnum;
  11. import com.sl.ms.trade.enums.TradingStateEnum;
  12. import com.sl.ms.trade.service.BasicPayService;
  13. import com.sl.ms.trade.service.RefundRecordService;
  14. import com.sl.ms.trade.service.TradingService;
  15. import com.sl.transport.common.constant.Constants;
  16. import com.sl.transport.common.vo.TradeStatusMsg;
  17. import com.xxl.job.core.context.XxlJobHelper;
  18. import com.xxl.job.core.handler.annotation.XxlJob;
  19. import lombok.extern.slf4j.Slf4j;
  20. import org.springframework.beans.factory.annotation.Value;
  21. import org.springframework.stereotype.Component;
  22. import javax.annotation.Resource;
  23. import java.util.ArrayList;
  24. import java.util.List;
  25. /**
  26. * 交易任务,主要是查询订单的支付状态 和 退款的成功状态
  27. */
  28. @Slf4j
  29. @Component
  30. public class TradeJob {
  31. @Value("${sl.job.trading.count:100}")
  32. private Integer tradingCount;
  33. @Value("${sl.job.refund.count:100}")
  34. private Integer refundCount;
  35. @Resource
  36. private TradingService tradingService;
  37. @Resource
  38. private RefundRecordService refundRecordService;
  39. @Resource
  40. private BasicPayService basicPayService;
  41. @Resource
  42. private MQFeign mqFeign;
  43. /**
  44. * 分片广播方式查询支付状态
  45. * 逻辑:每次最多查询{tradingCount}个未完成的交易单,交易单id与shardTotal取模,值等于shardIndex进行处理
  46. */
  47. @XxlJob("tradingJob")
  48. public void tradingJob() {
  49. // 分片参数
  50. int shardIndex = NumberUtil.max(XxlJobHelper.getShardIndex(), 0);
  51. int shardTotal = NumberUtil.max(XxlJobHelper.getShardTotal(), 1);
  52. List<TradingEntity> list = this.tradingService.findListByTradingState(TradingStateEnum.FKZ, tradingCount);
  53. if (CollUtil.isEmpty(list)) {
  54. XxlJobHelper.log("查询到交易单列表为空!shardIndex = {}, shardTotal = {}", shardIndex, shardTotal);
  55. return;
  56. }
  57. //定义消息通知列表,只要是状态不为【付款中】就需要通知其他系统
  58. List<TradeStatusMsg> tradeMsgList = new ArrayList<>();
  59. for (TradingEntity trading : list) {
  60. if (trading.getTradingOrderNo() % shardTotal != shardIndex) {
  61. continue;
  62. }
  63. try {
  64. //查询交易单
  65. TradingDTO tradingDTO = this.basicPayService.queryTrading(trading.getTradingOrderNo());
  66. if (TradingStateEnum.FKZ != tradingDTO.getTradingState()) {
  67. TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
  68. .tradingOrderNo(trading.getTradingOrderNo())
  69. .productOrderNo(trading.getProductOrderNo())
  70. .statusCode(tradingDTO.getTradingState().getCode())
  71. .statusName(tradingDTO.getTradingState().name())
  72. .build();
  73. tradeMsgList.add(tradeStatusMsg);
  74. }
  75. } catch (Exception e) {
  76. XxlJobHelper.log("查询交易单出错!shardIndex = {}, shardTotal = {}, trading = {}", shardIndex, shardTotal, trading, e);
  77. }
  78. }
  79. if (CollUtil.isEmpty(tradeMsgList)) {
  80. return;
  81. }
  82. //发送消息通知其他系统
  83. String msg = JSONUtil.toJsonStr(tradeMsgList);
  84. this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, Constants.MQ.RoutingKeys.TRADE_UPDATE_STATUS, msg);
  85. }
  86. /**
  87. * 分片广播方式查询退款状态
  88. */
  89. @XxlJob("refundJob")
  90. public void refundJob() {
  91. // 分片参数
  92. int shardIndex = NumberUtil.max(XxlJobHelper.getShardIndex(), 0);
  93. int shardTotal = NumberUtil.max(XxlJobHelper.getShardTotal(), 1);
  94. List<RefundRecordEntity> list = this.refundRecordService.findListByRefundStatus(RefundStatusEnum.SENDING, refundCount);
  95. if (CollUtil.isEmpty(list)) {
  96. XxlJobHelper.log("查询到退款单列表为空!shardIndex = {}, shardTotal = {}", shardIndex, shardTotal);
  97. return;
  98. }
  99. //定义消息通知列表,只要是状态不为【退款中】就需要通知其他系统
  100. List<TradeStatusMsg> tradeMsgList = new ArrayList<>();
  101. for (RefundRecordEntity refundRecord : list) {
  102. if (refundRecord.getRefundNo() % shardTotal != shardIndex) {
  103. continue;
  104. }
  105. try {
  106. //查询退款单
  107. RefundRecordDTO refundRecordDTO = this.basicPayService.queryRefundTrading(refundRecord.getRefundNo());
  108. if (RefundStatusEnum.SENDING != refundRecordDTO.getRefundStatus()) {
  109. TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
  110. .tradingOrderNo(refundRecord.getTradingOrderNo())
  111. .productOrderNo(refundRecord.getProductOrderNo())
  112. .refundNo(refundRecord.getRefundNo())
  113. .statusCode(refundRecord.getRefundStatus().getCode())
  114. .statusName(refundRecord.getRefundStatus().name())
  115. .build();
  116. tradeMsgList.add(tradeStatusMsg);
  117. }
  118. } catch (Exception e) {
  119. XxlJobHelper.log("查询退款单出错!shardIndex = {}, shardTotal = {}, refundRecord = {}", shardIndex, shardTotal, refundRecord, e);
  120. }
  121. }
  122. if (CollUtil.isEmpty(tradeMsgList)) {
  123. return;
  124. }
  125. //发送消息通知其他系统
  126. String msg = JSONUtil.toJsonStr(tradeMsgList);
  127. this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, Constants.MQ.RoutingKeys.REFUND_UPDATE_STATUS, msg);
  128. }
  129. }

6.4、xxl-job任务

创建xxl-job的任务,首先创建执行器(AppName在nacos中的sl-express-ms-trade.properties配置文件中指定):

image.png

创建【查询支付状态】任务:

image.png

创建【查询退款状态】任务:

image.png

本地启动服务后会看到注册的ip地址,可能是在101机器无法访问的,如下:

image.png

如果出现此情况,需要在配置文件中设置参数指定ip地址,如下: 配置可参考官方文档:分布式任务调度平台XXL-JOB

bootstrap-local.yml 

  1. xxl:
  2. job:
  3. executor:
  4. ip: 192.168.150.1

重新启动,效果如下:

image.png

7、面试连环问

能说一下你们的支付服务是如何设计的吗?(中台思想、哪些表、支付渠道)

整体的支付流程是怎样的?

  • 细节:如何保证订单幂等性?

  • 细节:抽取支付通用服务的作用?

  • 细节:交易服务设计 -> 交易单表的作用?

  • 细节:支付服务涉及哪几张表?

  • 细节:如何对接支付宝和微信?

  • 细节:支付功能为什么要加分布式锁?

  • 细节:订单有哪些状态?

  • 细节:如何得知支付的结果?项目中是如何处理的?(异步通知,主动轮询)

  • 细节:订单金额字段类型?

  • 细节:如何保证支付安全?(重点 验签!!)

  • 细节:微信和支付宝,需要哪些配置,配置存在哪里?

  • 细节:支付接口需要传递哪些参数,调哪个接口,返回哪个值?

  • 细节:如何生成支付二维码?

  • 细节:如何动态的根据支付方式选择对应的支付?

  • 细节:对接支付宝和微信有哪些不同?

  • 细节:项目中是否有对账功能?

  • 细节:用户支付的钱打到了哪里?

  • 细节:如何实现一笔交易,分多批次退款?

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小惠珠哦/article/detail/945618
推荐阅读
相关标签
  

闽ICP备14008679号