功能划分”
1)商户的注册与登录(需要验证码服务)。
2)商户应用的创建与查询(每个商户可以开通多个应用服务,包括最为基础的支付应用,后期可以扩展账单数据统计应用等)
3)该平台提供给用户的最核心的应用就是支付应用
a)支付服务类型与支付渠道类型的绑定(多对多关系,支付服务类型包括B2C,C2B,每个服务类型的渠道可以分为支付宝,微信等具体第三方支付渠道)
b)为商户的生成聚合支付二维码,该二维码提供聚合支付功能
数据库配置文件
#
# The MySQL database server configuration file.
#
# You can copy this to one of:
# - "/etc/mysql/my.cnf" to set global options,
# - "~/.my.cnf" to set user-specific options.
#
# One can use all long options that the program supports.
# Run program with --help to get a list of available options and with
# --print-defaults to see which it would actually understand and use.
#
# For explanations see
# http://dev.mysql.com/doc/mysql/en/server-system-variables.html
# This will be passed to all mysql clients
# It has been reported that passwords should be enclosed with ticks/quotes
# escpecially if they contain "#" chars...
# Remember to edit /etc/mysql/debian.cnf when changing the socket location.
# Here is entries for some specific programs
# The following values assume you have at least 32M ram
[mysqld_safe]
socket = /var/run/mysqld/mysqld.sock
nice = 0
[mysqld]
#
# * Basic Settings
#
user = mysql
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
port = 3306
basedir = /usr
datadir = /var/lib/mysql
tmpdir = /tmp
lc-messages-dir = /usr/share/mysql
skip-external-locking
#
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
# bind-address = 127.0.0.1
#
# * Fine Tuning
#
key_buffer_size = 16M
max_allowed_packet = 16M
thread_stack = 192K
thread_cache_size = 8
# This replaces the startup script and checks MyISAM tables if needed
# the first time they are touched
myisam-recover-options = BACKUP
#max_connections = 100
#table_open_cache = 64
#thread_concurrency = 10
#
# * Query Cache Configuration
#
query_cache_limit = 1M
query_cache_size = 16M
#
# * Logging and Replication
#
# Both location gets rotated by the cronjob.
# Be aware that this log type is a performance killer.
# As of 5.1 you can enable the log at runtime!
#general_log_file = /var/log/mysql/mysql.log
#general_log = 1
#
# Error log - should be very few entries.
#
log_error = /var/log/mysql/error.log
#
# Here you can see queries with especially long duration
#slow_query_log = 1
#slow_query_log_file = /var/log/mysql/mysql-slow.log
#long_query_time = 2
#log-queries-not-using-indexes
#
# The following can be used as easy to replay backup logs or for replication.
# note: if you are setting up a replication slave, see README.Debian about
# other settings you may need to change.
#server-id = 1
#log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size = 100M
#binlog_do_db = include_database_name
#binlog_ignore_db = include_database_name
#
# * InnoDB
#
# InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/.
# Read the manual for more InnoDB related options. There are many!
#
# * Security Features
#
# Read the manual, too, if you want chroot!
# chroot = /var/lib/mysql/
#
# For generating SSL certificates I recommend the OpenSSL GUI "tinyca".
#
# ssl-ca=/etc/mysql/cacert.pem
# ssl-cert=/etc/mysql/server-cert.pem
# ssl-key=/etc/mysql/server-key.pem
当前系统存在的问题
问题1:商户的创建的应用中有两个id,包括数据表的自增id(创建应用时系统生成),还有一个是业务id,这个id是采用UUID的生成的?
可能的改进1:商户创建的应用的业务id采用UUID,可不可以替换为其他算法。
可能的改进2: 如果商户应用表进行分库处理,那么数据库的id采用哪种分布式id生成策略比较合适。
问题2:支付平台中,支付的操作是最为频繁的,支付操作需要用到支付渠道参数,如何提高支付渠道参数的获取速度?
难点1:如何让支付渠道参数更快的被获取?
解决策略:将商户应用的支付渠道参数放入到redis中进行缓存
支付渠道参数如何使用?
1)保存支付渠道参数到数据库时,需要将支付渠道参数在redis中缓存一份
2)获取支付渠道参数时,首先从redis中获取,redis中获取不到再从数据库中查询,并将新的参数缓存到redis中。
难点2:支付渠道参数缓存更新(可以改进)
/**
* 将支付渠道参数列表放入到缓存中
* @param appId
* @param platformChannel
*/
// 这里更新缓存的步骤: 删除key--->查询数据库---> 设置新的key
// 调用该函数之前需要确保先 写入/更新数据库
private void updateCache(String appId, String platformChannel) {
//处理redis缓存
//1.key前缀格式实例:SJ_PAY_PARAM:b910da455bc84514b324656e1088320b:shanju_c2b
String redisKey = RedisUtil.keyBuilder(appId, platformChannel);
// (数据库与缓存一致性问题)
//2.查询redis,检查key是否存在
Boolean exists = cache.exists(redisKey);
if (exists) {//存在,则清除
cache.del(redisKey);
}
// 从数据库查询应用的服务类型对应的实际支付参数,并重新存入缓存
List<PayChannelParamDTO> paramDTOS = queryPayChannelParamByAppAndPlatform(appId,platformChannel);
if (paramDTOS != null) {
cache.set(redisKey, JSON.toJSON(paramDTOS).toString()); // 将支付渠道参数转化为json放入数据
}
}
问题:支付渠道参数列表的key的有效期该如何设置
问题3:用户扫码后,看到商品的订单信息后,然后点击确认支付,此时流程是怎样的?如何优化?
用户扫码(该二维码需要商户去平台设置支付渠道参数并于门店绑定去生成)--> 二维码本质上就是聚合平台的统一下单接口+相关参数
---> 判断用户扫描的客户端并调用对应支付渠道接口返会订单确认界面(包含商品名称,金额等信息),
用户点击确认支付--->调用交易微服务的下单接口 ----> 首先在订单表保存订单(通过雪花算法为订单生成唯一的编号),然后再调用第三方支付渠道下单接口 -----> 用户付款 --> 返回支付接口
订单信息获取优化点:用户点击支付,需要先保存订单信息到聚合支付平台的订单表,然后会查询数据库获得订单信息,去设置第三方支付的http请求参数,这里订单信息的获取可不可以在存入成功,放入缓存,然后查询的时候就不需要再查询数据库了。
问题4:消息队列的使用时消息丢失,消息重复与消息堆积该如何解决?
0 常用注解0-1 Springboot相关
@SpringBootTest
@SpringBootTest: Annotation that can be specified on a test class that runs Spring Boot based tests.
Class<?>[] classes
The annotated classes to use for loading an ApplicationContext.
String[] properties
Properties in form key=value that should be added to the Spring Environment before the test runs.
String[] value
0-2 SpringMVC相关
value: value指定请求的实际地址
method: 指定请求的method类型 GET、POST、PUT、DELETE等
consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html
produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回
params: 指定request中必须包含某些参数值是,才让该方法处理。
headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求。
-
@GetMapping是@RequestMapping(method = RequestMethod.GET)
-
@PostMapping是@RequestMapping(method = RequestMethod.POST)的缩写
用于绑定前端传递过来的参数,通过name属性设置绑定的参数是什么。
byte[] getBytes()
String getContentType()
InputStream getInputStream()
String getName()
String getOriginalFilename()
long getSize()
boolean isEmpty()
void transferTo(File dest)
default void transferTo(Path dest)
@ResponseBody:将控制层方法的返回值与http的body进行绑定
- Annotation that indicates a method return value should be bound to the web response body. Supported for annotated handler methods.
@RequestBody:将方法的参数与http请求的body的内容进行绑定
@PathVarialbe:将controller方法的参数与请求的路径中的参数绑定
- Annotation which indicates that a method parameter should be bound to a URI template variable
Annotation which indicates that a method parameter should be bound to a web request parameter.
Supported for annotated handler methods in Spring MVC and Spring WebFlux as follows:
In Spring MVC, "request parameters" map to query parameters, form data, and parts in multipart requests. This is because the Servlet API combines query parameters and form data into a single map called "parameters", and that includes automatic parsing of the request body.(该参数与请求中的查询参数,表单数据以及multipart请求中的部分参数)
In Spring WebFlux, "request parameters" map to query parameters only. To work with all 3, query, form data, and multipart data, you can use data binding to a command object annotated with ModelAttribute.
If the method parameter type is Map and a request parameter name is specified, then the request parameter value is converted to a Map assuming an appropriate conversion strategy is available.
If the method parameter is Map<String, String> or MultiValueMap<String, String> and a parameter name is not specified, then the map parameter is populated with all request parameter names and values.
1)AOP即动态代理
2)拦截器与过滤器是springMVC的组件,AOP是一种思想,这是两个层次不同的概念,不要混淆
@Controller与@Restcontroller
public @interface RestController
A convenience annotation that is itself annotated with @Controller and @ResponseBody.(组合注解)
0-3 Mybatis-plus的使用
/**
* Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
* <p>这个 Mapper 支持 id 泛型</p>
*/
public interface BaseMapper<T> {
int insert(T entity);
int deleteById(Serializable id);
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
int updateById(@Param(Constants.ENTITY) T entity);
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);
T selectById(Serializable id);
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 根据 Wrapper 条件,查询总记录数
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
}
#{} 和 ${} 在使用中的技巧和建议
(1)不论是单个参数,还是多个参数,一律都建议使用注解@Param("")
(2)能用 #{} 的地方就用 #{},不用或少用 ${}
(3)表名作参数时,必须用 ${}。如:select * from ${tableName}
(4)order by 时,必须用 ${}。如:select * from t_user order by ${columnName}
(5)使用 ${} 时,要注意何时加或不加单引号,即 ${} 和 '${}'
0-4 maven的声明周期
install:如果子项目用到了其他子项目,需要将其他子项目install到本地仓库,否则找不到依赖
0-5 数据库基本知识点
Text类型数据的改造方法:
1)使用es存储:在MySQL中,一般log表会存储text类型保存request或response类的数据,用于接口调用失败时去手动排查问题,使用频繁的很低。可以考虑写入本地log file,通过filebeat抽取到es中,按天索引,根据数据保留策略进行清理。
2)使用对象存储:有些业务场景表用到TEXT,BLOB类型,存储的一些图片信息,比如商品的图片,更新频率比较低,可以考虑使用对象存储,例如阿里云的OSS,AWS的S3都可以,能够方便且高效的实现这类需求。
06 dubbo的配置
07 docker的配置
vim /lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock
systemctl daemon-reload
systemctl restart docker
curl http://127.0.0.1:2375/version
制作并启动镜像
docker build -t pay_sms_service . # 使用当前目录的 Dockerfile 创建镜像,标签为 pay_sms_service
docker build -t pay_gateway_service . # 使用当前目录的 Dockerfile 创建镜像,标签为 pay_sms_service
docker build -t pay-saas-user-service .
docker build -t pay-saas-uua-service .
docker build -t pay-saas-uua-service .
docker build -t pay-saas-uua-service .
docker build -t rocketmq-console .
docker images # 查看镜像
docker run -d -p 56085:56085 pay_sms_service # 启动镜像 -d即后台启动,-p即端口映射
docker run -d --net=host pay_sms_service # 启动镜像,容器的模式设置为host
docker run -d --net=host pay_gateway_service
docker run -d --net=host pay-saas-user-service
docker run -d --net=host pay-saas-uua-service
docker run -d --net=host rocketmq-console
删除镜像:
1) docker rm ContainerId
2) docker rmi ImgageId
=========================
java -jar rocketmq-console-ng-2.0.0.jar
信息查询
docker ps # 查看正在运行的镜像
docker logs --since 30m cc8b7e6b6d28
docker ps // 查看所有正在运行容器
docker stop containerId // containerId 是容器的ID
docker ps -a // 查看所有容器
docker ps -a -q // 查看所有容器ID
docker start $(docker ps -a -q) // start启动所有停止的容器
docker stop $(docker ps -a -q) // stop停止所有容器
docker rm $(docker ps -a -q) // remove删除所有容器
修改镜像下载地址
编辑配置文件,如果文件不存在,以下命令会自动创建。
sudo nano /etc/docker/daemon.json
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://yzggwq9i.mirror.aliyuncs.com"]
}
sudo systemctl daemon-reload
sudo systemctl restart docker
http://106.13.71.208:56085/sailing/swagger-ui.html
08 ubuntu的安全性配置
sudo ufw allow ssh
sudo ufw enable
关于网络IP地址:防火设置IP过滤的时候,公网IP都是主机号因此子网掩码为0
看端口是否占用一般用两个
(1) netstat -ap|grep 8080
-p, --programs display PID/Program name for sockets #显示pid和程序名字
-l, --listening display listening server sockets #显示处于监听状态的套接字
-a, --all display all sockets (default: connected) #显示所有的套接字
(2) lsof -i:8080
区别:
1.netstat无权限控制,lsof有权限控制,只能看到本用户
2.losf能看到pid和用户,可以找到哪个进程占用了这个端口
09 前端环境搭建
step1:安装nodejs
sudo apt-get update
# 下方的下载地址,请根据需要更换
sudo wget https://nodejs.org/dist/v10.15.3/node-v10.15.3.tar.gz
# 下方的解压文件,根据下载的文件名更换
sudo tar xvf node-v10.15.3.tar.gz
# 下方的文件夹名称根据解压之后的文件名更换
cd node-v10.15.3
sudo ./configure
sudo make
sudo make test
sudo make install
sudo cp /usr/local/bin/node /usr/sbin/
测试命令:node -v
step2:安装yarn
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
sudo sh -c 'echo "deb https://dl.yarnpkg.com/debian/ stable main" >> /etc/apt/sources.list.d/yarn.list'
sudo apt update
sudo apt install yarn
yarn --version
step3:切换到项目目录
-
使用cmd命令进入shanjupay-web目录,
-
设置前端项目的后端请求地址
(base) god@god-MS-7C83:~/webproject/web_pages/project-juhezhifu-admin-vue$ vim vue.config.js
启动项目
执行yarn install --ignore-engines
执行yarn serve:
- 启动项目会报错,需要分析一下原因。
1-1 用户中心数据表shanjupay_merchant_service
表名称 | 表的字段 | 表的作用 |
---|---|---|
app | ||
merchant | ||
staff | ||
store | ||
store_staff |
1-2 交易服务数据库shanjupay_transaction
2 应用层与微服务层在项目中的结构2-1 功能概述
工程名称 | 工程作用 |
---|---|
商户平台应用层(shanjupay-merchant-application) | 为前端提供商户管理功能 |
商户微服务API(shanjupay-merchant-api) | 定义商户服务提供的接口 |
商户微服务(shanjupay-merchant-service) | 实现商户服务的所有接口 |
通用工具类(shanjupay-common) | 项目通用的工具类 |
关于微服务的个人理解:
- 单体应用,通常controller层(应用层)本地调用service层的多个service都是在1台计算机上的
- 微服务应用:将service层多个service拆分到多台服务器上。每个service访问部分数据。controller层通过服务注册中心获取微服务的调用地址并远程调用需要的微服务。
原因:单体的性能到达极限,顶不住,只能进行拆分。
服务架构
用户请求---> 网关 ---> 应用服务器 ---> 微服务服务器 --> 数据库
网关:
- 统一入口:未全部为服务提供一个唯一的入口,网关起到外部和内部隔离的作用,保障了后台服务的安全性。
- 鉴权校验:识别每个请求的权限,拒绝不符合要求的请求。
- 动态路由:动态的将请求路由到不同的后端集群中。
- 减少客户端与服务端的耦合:服务可以独立发展,通过网关层来做映射。
微服务的项目与单体项目区别
从上图中可以看到区别于小型的单体应用开发:
1)微服务项目将服务层拆分成多个微服务,每个微服务都是一个工程,每个工程中包含两个子工程,其中API工程定义了微服务的接口以及
DTO,而另外一个工程则是对接口的具体实现。
2)微服务项目中应用层与服务层不再放入同一个工程文件中,而是分别作用单独的子工程,每个子工程实际部署时,单独启动,借助服务注册中心进行服务调用
2-2 微服务开发常识
2-2-1 项目中数据传输的Object
对象名称 | 作用 |
---|---|
VO(View Object) | 视图对象,用于表示层,它的作用是把某个指定页面(或组件)的所有数据封装起来(用于在网页前台显示) |
DTO(Data Transfer Object) | 用于表示层与服务层之间的数据传输对象 |
DO(Domain Object) | 有形或无形的业务实体。 |
PO(Persistent Object) | 持久层对象,通常数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。 |
为什么每层的数据传输都专门定义对象?
1)方便层的扩展,如果表示层使用PO作为传输对象,如果有多个PO的数据要显示的话,仍然需要定义新的VO
2)降低层与层的耦合,每层维护自己的对象就行
2-2-2 Swagger接口工具
作用:自动生成restful接口文档的工具,前后端对接依赖于接口文档,后端开发需要提供接口文档给前端工程师
使用方法:在需要生成文档的工程中对swagger进行配置
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务
1. 使得前后端分离开发更加方便,有利于团队协作
2. 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
3. 功能测试
Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入
Springfox ,即可非常简单快捷的使用Swagger。
具体配置方法:
step1:配置扫描Restful接口定义的的包路径以及文档名称
package com.shanjupay.merchant.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author Administrator
* @version 1.0
*
* Swagger的配置类,用于自动生成Restful接口的文档,方便前端开发者查询
**/
@Configuration
@ConditionalOnProperty(prefix = "swagger",value = {"enable"},havingValue = "true")
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket buildDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(buildApiInfo())
.select()
// 要扫描的API(Controller)基础包,controller包中提供了Application的Restful的接口
.apis(RequestHandlerSelectors.basePackage("com.shanjupay.merchant.controller"))
.paths(PathSelectors.any())
.build();
}
/**
* @param
* @return springfox.documentation.service.ApiInfo
* @Title: 构建API基本信息
* @methodName: buildApiInfo
*/
private ApiInfo buildApiInfo() {
Contact contact = new Contact("开发者","","");
return new ApiInfoBuilder()
.title("闪聚支付-商户应用API文档")
.description("")
.contact(contact)
.version("1.0.0").build();
}
}
step2:配置网页可以访问swagger-ui文档即swagger-ui.html
该网页提供:
- 对接口的说明
- 后端开发者可以在网页上进行测试(get/post),测试应用层接口(http访问)是否正确执行(非主要功能,尽量使用Postman测试)
http://localhost:57010/merchant/swagger-ui.html
上图文档对应的Swagger注解:
package com.shanjupay.merchant.controller;
import com.shanjupay.merchant.api.MerchantService;
import com.shanjupay.merchant.api.dto.MerchantDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Api(value="商户平台应用接口",tags = "商户平台应用接口",description = "商户平台应用接口")
public class MerchantController {
@org.apache.dubbo.config.annotation.Reference
MerchantService merchantService;
@ApiOperation(value="根据id查询商户信息")
@GetMapping("/merchants/{id}")
public MerchantDTO queryMerchantById(@PathVariable("id") Long id){
MerchantDTO merchantDTO = merchantService.queryMerchantById(id);
return merchantDTO;
}
@ApiOperation("测试")
@GetMapping(path = "/hello")
public String hello(){
return "hello";
}
@ApiOperation("测试")
@ApiImplicitParam(name = "name", value = "姓名", required = true, dataType = "string")
@PostMapping(value = "/hi")
public String hi(String name) {
return "hi,"+name;
}
}
2-2-3 PostMan服务端者进行接口测试专用工具
3 验证码功能介绍3-1 功能需求分析
商户的分类:线上商户与线下商户
1)线下场所支付商户
使用线下场所支付的商户是指有实体经营场所的商家,也称为地面商户,一般包含酒店、餐厅、酒吧、美容、 美
发、 媒体、 影楼、 家政、 艺廊、 KTV、 会所等。
2)线上支付商户
使用线上支付的商户是指通过互联网进行经营服务的商家,常见的有:电商网站、团购网站、旅游网站等。
用户的注册流程:
1)用户填写手机号、账号、密码等信息 --> 2)点击获取手机验证码 --> 3)输入验证码,点击注册 --> 4) 商户注册成功
相关联的微服务
微服务名称 | 微服务的作用 | 编号 |
---|---|---|
商户平台应用 | 此应用主要为商户提供业务功能,包括:商户资质申请、员工管理、门店管理等功能 | A |
商户服务 | 提供商户管理的相关服务接口,供其它微服务调用 | B |
SaaS平台 | 商户注册的账号等信息需要写入SaaS平台,由SaaS平台统一管理账号,分配权限,商户统一通过SaaS平台登录闪聚支付。 | C |
验证码服务 | 提供获取短信验证码、校验验证码的接口 | D |
开发者实现的注册流程
1. 前端请求商户平台A应用进行注册
2. 商户平台A应用获取短信验证码
3. 前端携带手机验证码、账号、密码等信息请求商户平台应用确认注册
4. 验证码校验通过后请求商户服务新增商户
5. 商户服务请求SaaS平台新增租户并初始化管理员
6. SaaS平台返回创建成功给商户服务商户服务新增商户下根门店信息
7. 商户服务新增商户下员工信息
8. 注册成功
3-2 验证码服务的部署
验证码服务使用的是开源服务,验证码服务需要频繁的校验与发送,采用缓存加速。
短信验证码需要域名并向服务商申请,项目中测试时,不实际通过云服务商发送验证码,而是控制台直接打印验证码。
在项目的handler目录将短信的发送改为控制台输出测试
3-2-1 发送验证码接口参数
接口功能:远程调用该接口,并填入下面3个信息,验证码微服务会向用户手机发送验证码
1) http://localhost:56085/sailing/generate
2) http://localhost:56085/sailing/verify
3-2-1 校验验证码参数
接口功能:将用户填写的验证码以及之前发送验证码请求获取的key发送给验证码服务,来确认验证码是否正确
redis作用:存储验证key
3-3 spring中调用验证码微服务
验证码服务接口:验证码服务对外提供http接口!!,我们使用的postman和swagger-ui都属于http客户端的一种
- 注意:验证码服务并非是rpc接口
spring的http接口工具:RestTemplate是Spring提供的用于访问RESTful服务的客户端
使用前提:RestTemplate默认依赖JDK提供http连接的能力
常见的http客户端:
1) Apache HttpComponents
2) Netty提供的http接口
3) OkHttp提供的接口
4) JDK提供的http库(RestTemplate默认使用)
RestTemplate
知识点:url的?,#,&的区别?
1) #:用于定位网页中的位置(实例:下面这个连接会跳转到网页的2-4位置)
2)?:
3)&:不同参数的间隔符
3-3-1商品平台应用获取验证码接口1
接口功能(上图中的接口1):实现用户获取验证码
用户访问登录界面 => 填写手机号 => 点击获取验证码 => 调用/merchant/sms接口 => 后台调用验证码服务让其发送校验码返回key留待用户填写验证码后校验
key的作用:校验验证码正确性的时候需要将key与用户填写的验证码一同发送用户
注意点:接口1暴露的是http协议的接口,需要被外界调用。
3-4 验证码服务功能总结(重要)
应用层接口1:输入手机号请求验证码服务发送验证码(获取验证码)
通过请求验证码服务器暴露的http接口,在收到前端传递过来的手机号后,调用接口,让验证码服务器发送验证码到用户手机,并返回发送验证码在redis存储的key
验证码校验接口
应用层接受验证码和之前调用接口1获取的key,然后发送给验证码服务器进行校验,验证码服务器返回结果
注意:上面2个接口都是调用验证码的http协议接口(即restful接口)
4 商户注册功能4-1 商户注册信息存储的位置
商户的注册信息存储位置
1)商户表(该表中的手机号字段,商户状态字段需要商户注册信息)
2)员工表
3)SaaS系统
4-2 商品服务注册商户接口2(dubbo接口)
实现暴露dubbo协议的微服务接口:
/*接受前端传递过来的新增商户信息,并写入到数据库中*/
@Override
public MerchantDTO createMerchant(MerchantDTO merchantDTO) {
Merchant merchant = new Merchant();
merchant.setMobile(merchantDTO.getMobile());
merchant.setAuditStatus("0"); /*设置新增的商户信息未审核状态*/
merchantMapper.insert(merchant);
merchantDTO.setId(merchant.getId());
return merchantDTO;
}
4-3 商品服务注册商户接口3(http接口)
4-4 接口测试
商户验证码接口测试
结果:
商户注册验证码测试
4-5 vo与dto与entity的转换工具类
4-6 项目异常处理
1)自定义异常类放入公共工程中让所有项目使用
java异常的分类
错误与异常
错误:Error是程序无法处理的错误,它是由JVM产生和抛出的,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
异常:Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。
运行时异常(Unchecked Exception)和非运行时异常(Checked Exception)
运行时异常:RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
非运行时异常:RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
Try,catch,finally使用
1)try、catch、finally三个语句块均不能单独使用,三者可以组成 try...catch...finally、try...catch、try...finally三种结构,catch语句可以有一个或多个,finally语句最多一个。
2)try、catch、finally三个代码块中变量的作用域为代码块内部,分别独立而不能相互访问。如果要在三个块中都可以访问,则需要将变量定义到这些块的外面。
3)多个catch块时候,最多只会匹配其中一个异常类且只会执行该catch块代码,而不会再执行其它的catch块,且匹配catch语句的顺序为从上到下,也可能所有的catch都没执行。
4)先Catch子类异常再Catch父类异常。
4-7 商户注册功能总结
商户填写验证码后的注册接口
1) step1:通过http协议的验证码校验接口校验验证码是否正确
2) step2:如果正确,调用dubbo协议接口创建商户,传递商户的注册信息
3) step3:将商户的注册信息写入数据表
5 商户资质申请
商户平台应用:对外暴露服务供用户调用
商户服务:
七牛云:图片单独使用服务器存储
5-1 利用云服务器的对象存储功能存储商户图片对象
七牛云的密钥
AccessKey: OC4qecxU5asobMxluSWG2-8bxhR9ABRXEMZRRJyF
SecretKey: 1hBSr1w4TRIzM3DUxcIdTh_YMx0jogwF6ZuTjIMR
AccessKey:在API调用时均需要验证访问者的身份,以确保访问者具有相关权限。这种验证方式通过Access Key来实现
SecretKey:SecretKey为参数,配合适当的签名算法,可以得到原始信息的数字签名,防止内容在传递过程中被伪造或篡改。
使用场景:
使用场景
七牛推荐客户将 AccessKey和一个SecretKey 放在服务端,由服务端生成令牌后颁发给客户端使用。
比如,上传文件的时候:
业务服务器的服务端生成上传令牌(UploadToken)
客户端程序(iOS、Android 以及 Web)拿到这个上传令牌之后就可以直接将文件上传到七牛
5-2 完成商户图片上传接口并测试
接口测试界面
5-3 完成商户资质材料提交存储接口
相关概念
聚合支付:将微信,支付包等支付渠道汇聚为一个支付通道供用户使用
支付渠道:微信、支付宝等第三方支付机构提供的支付渠
支付应用:使用者主要是平台的商户,平台商户可以对自己的所有账单进行查询,统计与分析。
支付渠道参数配置流程
step1: 商户在微信与支付宝开通支付
step2: 商户在闪聚支付平台配置支付渠道参数(需要提供商户信息方便闪聚平台将商户接入支付宝)
step3: 聚合支付平台为商户分配聚合支付二维码
step4: 商户的用户使用聚合支付二维码付款
6-1 商户应用的创建,查询接口
商户应用创建
商户应用:在聚合支付平台注册的商户能够使用平台提供的应用,最基本的应用就是聚合支付二维码的申请,其余还可以扩展比如账单的管理,账单数据的统计分析,可视化等。
商户创建应用接口
1、接口描述
1)校验商户是否通过资质审核
如果商户资质审核没有通过不允许创建应用。
2)生成应用ID
应用Id使用UUID方式生成。
3)保存商户应用信息
应用名称需要校验唯一性
商户应用查询接口
1)根据商户id获取商户的应用列表
2)根据应用id获取指定的应用
6-2 商户的支付渠道类型与应用绑定
上图主要包含两个基本步骤:商户应用的服务类型绑定 +商户的服务渠道参数配置
业务流程
step1:商户通过商户应用绑定服务类型
1. 前端请求商户平台应用获取平台支持的所有服务类型列表
2. 请求交易服务查询列表
3. 返回服务类型列表给前端
4. 前端选择要绑定的服务类型请求商户平台应用
5. 请求交易服务绑定服务类型
6. 返回前端绑定成功
step2:支付渠道参数配置
1.前端请求获取第三方支付渠道列表
2.请求交易服务获取列表
3.返回结果给前端
4.前端请求配置支付渠道参数
5.商户平台应用请求交易服务保存参数配置
6.返回前端保存成功
支付参数的数据库表与数据初始化
1) platform_channel:平台服务类型(B2C,C2B)
2) pay_channel:支付渠道(支付宝,微信)
3) platform_pay_channel:平台服务类型对应第三方服务类型
为什么需要数据初始化?
因为该项目依赖于其他第三方支付平台,需要利用他们的API,因此需要将第三方的数据提前放入到数据库中存储。
数据库操作命令
use shanjupay_transaction;
LOCK TABLES `platform_channel` WRITE;
/*!40000 ALTER TABLE `platform_channel` DISABLE KEYS */;
INSERT INTO `platform_channel` (`ID`, `CHANNEL_NAME`, `CHANNEL_CODE`)
VALUES
(1,'闪聚B扫C','shanju_b2c'),
(2,'闪聚C扫B','shanju_c2b'),
(3,'微信Native支付','wx_native'),
(4,'支付宝手机网站支付','alipay_wap');
/*!40000 ALTER TABLE `platform_channel` ENABLE KEYS */;
UNLOCK TABLES;
LOCK TABLES `pay_channel` WRITE;
/*!40000 ALTER TABLE `pay_channel` DISABLE KEYS */;
INSERT INTO `pay_channel` (`ID`, `CHANNEL_NAME`, `CHANNEL_CODE`)
VALUES
(1,'微信JSAPI','WX_JSAPI'),
(2,'支付宝手机网站支付','ALIPAY_WAP'),
(3,'支付宝条码支付','ALIPAY_BAR_CODE'),
(4,'微信付款码支付','WX_MICROPAY'),
(5,'微信native支付','WX_NATIVE');
/*!40000 ALTER TABLE `pay_channel` ENABLE KEYS */;
UNLOCK TABLES;
LOCK TABLES `platform_pay_channel` WRITE;
/*!40000 ALTER TABLE `platform_pay_channel` DISABLE KEYS */;
INSERT INTO `platform_pay_channel` (`ID`, `PLATFORM_CHANNEL`, `PAY_CHANNEL`)
VALUES
(1,'shanju_b2c','WX_MICROPAY'),
(2,'shanju_b2c','ALIPAY_BAR_CODE'),
(3,'wx_native','WX_NATIVE'),
(4,'alipay_wap','ALIPAY_WAP'),
(5,'shanju_c2b','WX_JSAPI'),
(6,'shanju_c2b','ALIPAY_WAP');
/*!40000 ALTER TABLE `platform_pay_channel` ENABLE KEYS */;
UNLOCK TABLES;
应用绑定服务类型
多对多关系:
1)一种应用可以绑定多种服务类型
2)一种服务类型可以绑定多个应用
应用层接口
接口1:查询平台支持的所有能够绑定的服务类型的接口
接口2:服务绑定接口
接口3: 查询应用绑定服务的状态
6-3 支付渠道参数配置
1)支付渠道参数表:pay_channel_param表
总结:需要插入商户的ID,商户的支付渠道类型,该类型的支付渠道参数,以及该类型绑定的商户应用ID
查询商户应用绑定的服务类型的支付渠道
select
*
from
pay_channel pay,platform_pay_channel pac,platform_channel pla
where
pay.CHANNEL_CODE = pac.PAY_CHANNEL
and
pla.CHANNEL_CODE = pac.PLATFORM_CHANNEL
and
pla.CHANNEL_CODE = "wx_native";
数据表分析
支付平台提供的平台服务类型:platform_channel
平台服务类型与支付渠道的关系:platform_pay_channel
支付渠道类型以及对应的支付渠道名称:pay_channel
接口1:根据平台服务类型查询支付渠道
接口2:支付渠道参数保存到数据库中
支付渠道表包含参数:
1)支付渠道名称
2)商户ID
3) 支付渠道编码
4)支付渠道参数 (非二进制的文本类型,blob存储二进制的数据(图片,音乐))
5)应用于服务类型绑定ID (bigint)
问题:数据库设计为什么分为数据表id以及业务id?
数据表id: 数据表的自增id(减少随机IO,提升数据表增删查改的效率)
业务表id:通过雪花算法生成的id(这个id作为唯一性id,除了在本表使用,还会在其他表被使用)
问题:为什么保存支付渠道参数的http接口既支持put也支持POST?
@ApiOperation("商户配置支付渠道参数")
@ApiImplicitParams({
@ApiImplicitParam(name = "payChannelParam", value = "商户配置支付渠道参数", required = true, dataType = "PayChannelParamDTO", paramType = "body")
})
@RequestMapping(value = "/my/pay‐channel‐params",method = {RequestMethod.POST,RequestMethod.PUT})
public void createPayChannelParam(@RequestBody PayChannelParamDTO payChannelParamDTO){
Long merchantId = SecurityUtil.getMerchantId(); // 从前台获取商户的token
payChannelParamDTO.setMerchantId(merchantId);
payChannelService.savePayChannelParam(payChannelParamDTO); // 保存用户在前台输入的支付渠道参数
}
原因:POST接口用于新增配置参数,PUT接口修改配置参数。
POST与PUT的本质区别:在http协议规范中提供,PUT方法与GET方法具有幂等性,而POST没有。
因此如果使用PUT方法可以用于数据的更新,因为对相同的数据进行多次相同的更新操作,并不会影响服务器数据表的状态。
POST方法用于添加数据,多次新增数据可能会造成重复的数据记录,当然也可以从数据库层面避免重复的添加。
http接口测试:
{
"appId": "string",
"appPlatformChannelId": 0, // 不需要,从数据库中查到
"channelName": "string",
"id": 0, // 不需要
"merchantId": 0, // 商户id,从token中拿到
"param": "string", // json格式数据
"payChannel": "string",
"platformChannelCode": "string"
}
6-4 支付渠道参数缓存
基本思想:每次进行支付的时候都需要支付渠道参数,因此支付渠道参数属于热点数据,可以将其缓存在redis数据库,减少关系型数据库的压力。
7 Saas的对接
SaaS(可以理解为云服务)
SaaS软件不再是用户向软件供应商定制软件或进行二次开发,而是供应商将软件部署在自己的服务器上并通过互联网提供在线服务。SaaS软件不再是用户向软件供应商定制软件或进行二次开发,而是供应商将软件部署在自己的服务器上并通过互联网提供在线服务。
PaaS(云平台服务): MySQL数据库服务,消息队列模式
IaaS(基础设施层): 服务器
Saas(云应用层): 软件服务的租聘
SaaS的核心概念
多租户(Multi-tenant):“商户”是一类租户,“XX超市”则是一个具体的商户(租户)。
租户与用户概念的区分:
- “用户”是“组织”(租户)内成员,是软件平台的实际使用者,使用者凭身份(用户名)、凭证(密码)登入平台
7-1 商户接入Saas平台
商户注册,调用SaaS平台的新增租户与用户接口,每个步骤如下:
1、新增租户(上图第9步)
在商户平台新增商户。
调用SaaS系统的接口新增租户。
2、新增用户(上图第10-11步)
在商户平台 新增员工。
调用SaaS的接口新增用户,且自动设置用户和租户的绑定关系,并将该用户设置为该租户的管理员。
3、初始化租户权限(上图第12步)
商户注册成功为该租户设置默认权限。
4、更新管理员的权限(上图第13步)
为第2步新增的账号分配管理员权限,管理员权限即是租户的权限。
为什么需要SaaS系统?
1)通过SaaS系统实现统一账号的管理,权限的管理,认证的管理这个通用的功能,开发者聚焦于应用的实现。
2)SaaS系统是通用的组件的,实际企业中更多是对接SaaS系统,而不是开发Saas系统
商户平台接入通用的SaaS系统的步骤
1、部署SaaS系统
SaaS系统是闪聚支付平台独立的系统,我们需要部署SaaS系统并实现商户平台与其对接。
2、商户注册后调用SaaS系统的接口完成上边流程的对接
1)商户平台完成的功能如下:
新增商户
新增员工
新增门店
为门店管理员功能
2)调用SaaS系统的接口完成的功能如下:
新增租户
新增用户
自动设置用户和租户的绑定关系,并将该用户设置为该租户的管理员
为该租户设置默认权限
分配管理员权限
-
上面是SaaS系统管理的数据库
-
上面是需要接入SaaS的数据表
员工DTO
public class StaffDTO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主键")
private Long id;
@ApiModelProperty(value = "商户ID")
private Long merchantId;
@ApiModelProperty(value = "姓名")
private String fullName;
@ApiModelProperty(value = "职位")
private String position;
@ApiModelProperty(value = "用户名(关联统一用户)")
private String username;
@ApiModelProperty(value = "手机号(关联统一用户)")
private String mobile;
@ApiModelProperty(value = "员工所属门店")
private Long storeId;
@ApiModelProperty(value = "最后一次登录时间")
private LocalDateTime lastLoginTime;
@ApiModelProperty(value = "0表示禁用,1表示启用")
private Boolean staffStatus;
}
门店DTO
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(value="StoreDTO", description="")
public class StoreDTO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
@ApiModelProperty(value = "门店名称")
private String storeName;
@ApiModelProperty(value = "门店编号")
private Long storeNumber;
@ApiModelProperty(value = "所属商户")
private Long merchantId;
@ApiModelProperty(value = "父门店")
private Long parentId;
@ApiModelProperty(value = "0表示禁用,1表示启用")
private Boolean storeStatus;
@ApiModelProperty(value = "门店地址")
private String storeAddress;
}
10 前后端联合测试(待测试)
相关工作
1)部署前端
2)后端token的解析,通过Saas系统实现token的解析
3)
工具
node.js:是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好(服务端JavaScrpit环境)
yarn: Apache Hadoop YARN: (Yet Another Resource Negotiator,另一种资源协调者)是一种新的 Hadoop 资源管理器,它是一个通用资源管理系统,可为上层应用提供统一的资源管理和调度,它的引入为集群在利用率、资源统一管理和数据共享等方面带来了巨大好处
“Yarn是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具 ,正如官方文档中写的,Yarn 是为了弥补 npm 的一些缺陷而出现的。”这句话让我想起了使用npm时的坑了:
npm install的时候巨慢。特别是新的项目拉下来要等半天,删除node_modules,重新install的时候依旧如此。
Vue前端项目配置
const path = require('path')
const devServerPort = 8080
const name = '聚合支付'
const host = 'http://localhost:56010'
// const host = 'http://xfc.nat300.top'
// const host = 'http://211.103.136.242:7280/'
// const host = 'https://mock.boxuegu.com/mock/602'
module.exports = {
publicPath: '/',
lintOnSave: process.env.NODE_ENV === 'development',
devServer: {
port: devServerPort,
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
[process.env.VUE_APP_BASE_API + '/merchant']: {
target: host,
changeOrigin: true, // needed for virtual hosted sites
ws: true, // proxy websockets
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API + '/merchant']: '/merchant'
}
},
[process.env.VUE_APP_BASE_API + '/uaa']: {
target: host,
changeOrigin: true, // needed for virtual hosted sites
ws: true, // proxy websockets
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API + '/uaa']: '/uaa'
}
},
[process.env.VUE_APP_BASE_API + '/user']: {
target: host,
changeOrigin: true, // needed for virtual hosted sites
ws: true, // proxy websockets
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API + '/user']: '/user'
}
},
[process.env.VUE_APP_BASE_API + '/operation']: {
target: host,
changeOrigin: true, // needed for virtual hosted sites
ws: true, // proxy websockets
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API + '/operation']: '/operation'
}
},
[process.env.VUE_APP_BASE_API + '/zuul']: {
target: host,
changeOrigin: true, // needed for virtual hosted sites
ws: true, // proxy websockets
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API + '/zuul']: '/zuul'
}
}
}
},
pwa: {
name: name
},
pluginOptions: {
'style-resources-loader': {
preProcessor: 'scss',
patterns: [
path.resolve(__dirname, 'src/styles/_variables.scss'),
path.resolve(__dirname, 'src/styles/_mixins.scss')
]
}
},
chainWebpack(config) {
// Provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title.
config.set('name', name)
}
}
https://blog.csdn.net/aboutmn/article/details/87259762
https://www.cnblogs.com/Asterism-2012/p/10177345.html
、
11 C2B支付功能实现11-1 C2B概念
C2B:顾客(Customer)扫描商户(Business)提供的二维码来完成支付
1、商家出示付款二维码
2、客户打开支付宝或微信的扫一扫,扫描二维码
3、确认支付,完成支付
C2B的分类:
1)固定金额支付
2)输入金额支付
11-2 C2B的业务流程
用户的操作流程:
B端(商户端):门店管理 -> 选择应用 -> 选择门店生成二维码
C端(顾客端):扫描二维码->用户输入支付金额确认支付->用户输入密码完成支付
平台的业务流程:
1、为门店生成统一的支付二维码,用户扫一下二维码即可使用微信支付也可使用支付宝完成支付。
2、闪聚支付平台与微信、支付宝对接,闪聚支付作为中介,最终的支付动作(银行交易)仍通过微信、支付宝进
行。
3、闪聚平台作为中介调用微信、支付宝的下单接口,完成支付
11-3 支付宝的支付产品研究(聚合支付平台采用手机网站支付的方式)
- 有8种支付产品
方式1:当面付
当面付通过扫描二维码实现,分为商家扫客户和客户扫商家两种方式。
1. 商家通过扫描线下买家支付宝钱包中的条码、二维码等方式完成支付;
2. 线下买家通过使用支付宝钱包扫一扫,扫描商家的二维码等方式完成支付。
方式2:手机网站支付
11-4 支付宝沙箱环境
两种加密模式:
1)公钥证书
2)公钥(聚合支付平台选择公钥)
非对称加密的公匙和私钥
应用ID
2021000117697501
应用公钥
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsj/6UYTXqZ/7gKD+TXbvF5D6iaeITPKzIHDgKtWxySTbP8kWyVGcxg2ljCW98e9Rkrn9U4Xw09MG1UHDu0WstBaiZOraLIoDyQUIdkYsYX9VKOi1OEDyt9Y607+r3CZtekLMMFfnz2+jbFQOYYngrbhyOJy6s+eNrMyDWBW0pvcNxzrtK1oFuhPHSQzZCaXGJhCuiFM0EYhRIBtZ8tnABUC92PV/MLB+AG5SON4IF4K8ENh2o2H2uFXXFWvf6DO1ckBrN7k1zWN6ZBsEgVri1HnCnAOvTPkGvd2ZIR5Voc10+U4X83NJiyrf6nXFHtOIzDyulQpHf6v+7RrkPLhc8wIDAQAB
应用私钥
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyP/pRhNepn/uAoP5Ndu8XkPqJp4hM8rMgcOAq1bHJJNs/yRbJUZzGDaWMJb3x71GSuf1ThfDT0wbVQcO7Ray0FqJk6tosigPJBQh2Rixhf1Uo6LU4QPK31jrTv6vcJm16QswwV+fPb6NsVA5hieCtuHI4nLqz542szINYFbSm9w3HOu0rWgW6E8dJDNkJpcYmEK6IUzQRiFEgG1ny2cAFQL3Y9X8wsH4AblI43ggXgrwQ2HajYfa4VdcVa9/oM7VyQGs3uTXNY3pkGwSBWuLUecKcA69M+Qa93ZkhHlWhzXT5Thfzc0mLKt/qdcUe04jMPK6VCkd/q/7tGuQ8uFzzAgMBAAECggEBAI9OMmBxjbVY4qlyRaFf2i83JsWexE0g3nRZa0/kx+9vyzlH4SLvkzwDYrH+8evdPNba4tjQmWKjiR3Qpp0cEhIjFGJQEiG2v/5QJpJ4LlwgNAYUuQVF6h10hY0RzwjKeD/QDjtboQm7tkZ0ea9fWxwvat0q3EuhAN0I+xvJL5j/NKBT0f4xPw8ZlIs4loRjnbjfL307ZYvmJXLLYcISvCeB3Ye6hAv9LLTUWrYGCTXgRJVGIx7bJhNSbPDIC76CwfYhKHxXYyzCtOQZ/4ewdMl4IxTHvKCFRJgNQP8yo5DBcRkE0JPh8H2B9j8YGqVmXqaMn6eUlqW1OgzZzdqjQ3kCgYEA4uLC08KrBO7GPVspa6DDIVh+NqHdERhbqhsGTogTTJP7UZkjSHQNx/rwk9NdBaRTBxYzGhog0q1I8Oe4Ufa6d/ZRUoFc9GMDCaE7pjqITT15iymuyOD3B/5NUeHbQ0hm41gkKdDgNJe5lvDZpbfp1J95UPgXWDU+dfJbHxK9vBUCgYEAyR+FBFitihbvZfsPR0YdpvF+MjR85WmEzVc4Jp+j60f2YaATnO1rs8qRx0RDD11jTdm+g1Z3FRhz/p3kx76XQoDyk8VjkfoWgKKxo9XWphSkbt7E22up0GPQCe2ahkkcAgsEDcMFVlKUGU0cGOJQZuIQbbMBm11vivsb/pxwjucCgYEAlgGbEmsIq1A7HWHidthpauiZOgG2qZDTOhp4BwAM0nqclQyMuWCRpACTgwkh3ZMRmgPhcYaI4QHU0gJCaV6ZVqsyhTwmeyXjYkCJsZPNflQAwjOi7glfCpfmAxcy4r9B11n1Pvhs5BjUialgHSMFpKBzk0cUGCvLyiucd2TqsSkCgYBE6TbTWWsEkH0wTQhcHGsWg1IA87kDhdcJ4GON4E7y07JYmtd9gl/Pt42hYAM2JYJb70p2h86/fKRpzkHQKr56++GhvhUytCS3qIcDIsasGxCIKG383HPPwhNLA41Zi308OfgGmxaeECdMT/5bjFeOGNEWNSpMyIPqc8WQxJtQawKBgC2M5Zrk9sDTSNUHMsIH7VvnbxM8HlJ/c8oE0WEngYjVIjYAGWJpweBTJaJ2uTlfRMvlldyX9G8L3o+j++B4OkAwCRjINfN+r4h/DyrjuIJxBHSKzijZ4e52HWNrr5yyHU/ClwRRRXkhCDfiC43nkjKjjVoWklxhVmCco/YQEQny
支付宝公钥
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoMuDB0ouaUSXo5lI3wvZxusW7tAPkeC+kDnZqmLpet/J/KAY0f5F9YtumrrEBHdtFGPvvE8DTJe6/QEhr8TFNAKcmpH5E/0Z1j/xO4JKwpSCf3KrqFogGGUWQ3um04AFGv8jZzUHZfhQcXxJMbsGfyCmJ2Iwm+lijr2za3J+uZyrGTYTJIixDOOGoJk4UhejBCquXWTxo3TO9UHGpNEWTRo9SPqqi5uZ2gAGFNxv1+FOmSI88YKgQ4uQrKeSA80xfs51RbWq4IxkiuRvJC+rw4UK5BUaFE0HfpLfiI9F7+KrTw+GE/20DrIRUmp2Y/Uej5pkEUh8wQTG4GIX0sggPwIDAQAB
商家账号[email protected]
商户UID2088621956229925
登录密码111111
账户余额
0.00充值取现
买家账号[email protected]
登录密码111111
支付密码111111
用户名称ueuqux3189
证件类型身份证(IDENTITY_CARD)
证件号码552245196911072954
账户余额
99999.00充值取现
模拟器支付宝测试:
1)二维码生成工具,传入网址,生成图片二维码
2)将二维码链接在浏览器内部页面打开,然后
11-5 微信支付功能的实现
支付产品 | 业务模式 | 特点 | 是否适合聚合支付平台 |
---|---|---|---|
付款码支付 | B扫C模式 (商户扫顾客) | 顾客通过微信生成二维码 | 否 |
Native支付 | 顾客扫商家 | 商户通过微信生成二维码 | 否 |
JSAPI支付 | C扫B实现 | 用户在微信中打开商户的H5页面 | 是 |
APP支付 | |||
H5支付 | H5支付与JSAPI支付的区别在于H5支付不要求在微信客户端打开H5页面 | ||
小程序支付 |
总结:闪聚支付项目的支付的C2B采用微信的JSAPI支付,即顾客在微信中打开商户页面
12 手机网站支付宝支付接口接入步骤1:用户在浏览器中访问商家网页应用,选择商品下单、确认购买,进入支付环节,选择支付宝付款,用户点
击去支付,如下图1;
步骤2:进入到支付宝支付路由页面,支付宝处理支付请求,并尝试唤起支付宝客户端,如下图2;
步骤3:进入到支付宝页面,调起支付宝客户端支付,出现确认支付界面,如下图3;
步骤4:用户确认收款方和金额,点击立即支付后出现输入密码界面,如下图4;
步骤5:输入正确密码后,支付宝端显示支付结果,如下图5;
步骤6:自动回跳到浏览器中,商家根据付款结果个性化展示订单处理结果,如下图6。
12-1 接口调用流程
1、用户在商户的H5网站下单支付后,商户系统按照手机网站支付接口alipay.trade.wap.payAPI的参数规范生成订
单数据
2、前端页面通过Form表单的形式请求到支付宝。此时支付宝会自动将页面跳转至支付宝H5收银台页面,如果用
户手机上安装了支付宝APP,则自动唤起支付宝APP。
3、输入支付密码完成支付。
4、用户在支付宝APP或H5收银台完成支付后,会根据商户在手机网站支付API中传入的前台回跳地址return_url自
动跳转回商户页面,同时在URL请求中以Query String的形式附带上支付结果参数,详细回跳参数见“手机网站支付
接口alipay.trade.wap.pay”前台回跳参数。
5、支付宝还会根据原始支付API中传入的异步通知地址notify_url,通过POST请求的形式将支付结果作为参数通知
到商户系统,详情见支付结果异步通知
问题:聚合支付平台的订单与支付的订单的联系
支付宝生成订单后,完成支付后,会给聚合支付平台发送信息,聚合支付平台收到支付完成的信息后,生成自己的平台订单
12-2 支付宝的下单接口实现
从上面可以看到
外部商户创建订单并支付(商户发起的支付请求)
场景:我想要付钱给商家,那么就是发起支付请求。
支付宝的公共请求参数
参数 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
---|---|---|---|---|---|
app_id | String | 是 | 32 | 支付宝分配给开发者的应用 ID。 | 2014072300007148 |
method | String | 是 | 128 | 接口名称。 | alipay.trade.wap.pay |
format | String | 否 | 40 | 仅支持 JSON。 | JSON |
return_url | String | 否 | 256 | HTTP/HTTPS 开头字符串。 | |
charset | String | 是 | 10 | 请求使用的编码格式,如 utf-8,gbk、gb2312等。 | utf-8 |
sign_type | String | 是 | 10 | 商户生成签名字符串所使用的签名算法类型,目前支持 RSA2 和 RSA,推荐使用 RSA2。 | RSA2 |
sign | String | 是 | 256 | 商户请求参数的签名串,详见 签名 | 详见示例 |
timestamp | String | 是 | 19 | 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss"。 | 2014-07-24 03:07:50 |
version | String | 是 | 3 | 调用的接口版本,固定为:1.0。 | 1.0 |
notify_url | String | 否 | 256 | 支付宝服务器主动通知商户服务器里指定的页面http/https路径。 | |
biz_content | String | 是 | - | 业务请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档。 |
请求参数
参数 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
---|---|---|---|---|---|
body | String | 否 | 128 | 对一笔交易的具体描述信息。如果是多种商品,请将商品描述字符串累加传给 body。 | Iphone6 16G |
subject | String | 是 | 256 | 商品的标题/交易标题/订单标题/订单关键字等。 | 大乐透 |
out_trade_no | String | 是 | 64 | 商户网站唯一订单号。 | 70501111111S001111119 |
timeout_express | String | 否 | 6 | 该笔订单允许的最晚付款时间,逾期将关闭交易。取值范围:1m~15d。m-分钟,h-小时,d-天,1c-当天(1c-当天的情况下,无论交易何时创建,都在0点关闭)。 该参数数值不接受小数点, 如 1.5h,可转换为 90m。注意:此时间为创建订单成功后开始计时的时间;若为空,则默认为15d。 | 90m |
time_expire | String | 否 | 32 | 绝对超时时间,格式为yyyy-MM-dd HH:mm或yyyy-MM-dd HH:mm:ss。 注:1)以支付宝系统时间为准;2)如果和timeout_express参数同时传入,以time_expire为准。 | 2016-12-31 10:05 |
total_amount | Price | 是 | 9 | 订单总金额,单位为元,精确到小数点后两位,取值范围[0.01,100000000]。 | 9.00 |
auth_token | String | 否 | 40 | 针对用户授权接口,获取用户相关数据时,用于标识用户授权关系。注:若不属于支付宝业务经理提供签约服务的商户,暂不对外提供该功能,该参数使用无效。 | appopenBb64d181d0146481ab6a762c00714cC27 |
product_code | String | 是 | 64 | 销售产品码,商家和支付宝签约的产品码。该产品请填写固定值:QUICK_WAP_WAY。 | QUICK_WAP_WAY |
goods_type | String | 否 | 2 | 商品主类型:0—虚拟类商品,1—实物类商品。注:虚拟类商品不支持使用花呗渠道。 | 0 |
passback_params | String | 否 | 512 | 公用回传参数,如果请求时传递了该参数,则返回给商户时会回传该参数。支付宝会在异步通知时将该参数原样返回。本参数必须进行UrlEncode之后才可以发送给支付宝。 | merchantBizType%3d3C%26merchantBizNo%3d2016010101111 |
promo_params | String | 否 | 512 | 优惠参数 。注:仅与支付宝协商后可用。 | |
extend_params | String | 否 | 业务扩展参数,详见下面的 业务扩展参数说明。 | ||
enable_pay_channels | String | 否 | 128 | 可用渠道,用户只能在指定渠道范围内支付当有多个渠道时用“,”分隔。注:与 disable_pay_channels 互斥。 | pcredit、moneyFund、debitCardExpress |
disable_pay_channels | String | 否 | 128 | 禁用渠道,用户不可用指定渠道支付当有多个渠道时用“,”分隔。注:与 enable_pay_channels 互斥。 | pcredit、moneyFund,、debitCardExpress |
store_id | String | 否 | 32 | 商户门店编号。该参数用于请求参数中以区分各门店,非必传项。 | NJ_001 |
quit_url | String | 否 | 400 | 添加该参数后在h5支付收银台会出现返回按钮,可用于用户付款中途退出并返回到该参数指定的商户网站地址。注:该参数对支付宝钱包标准收银台下的跳转不生效。 | |
ext_user_info | ExtUserInfo | 否 | - |
回跳参数
公共参数
参数 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
---|---|---|---|---|---|
app_id | String | 是 | 32 | 支付宝分配给开发者的应用ID。 | 2016040501024706 |
method | String | 是 | 128 | 接口名称。 | alipay.trade.wap.pay.return |
sign_type | String | 是 | 10 | 签名算法类型,目前支持RSA2和RSA,推荐使用RSA2。 | RSA2 |
sign | String | 是 | 256 | 支付宝对本次支付结果的 签名,开发者必须使用支付宝公钥验证签名。 | 详见示例 |
charset | String | 是 | 10 | 编码格式,如utf-8,gbk,gb2312等。 | utf-8 |
timestamp | String | 是 | 19 | 前台回跳的时间,格式"yyyy-MM-dd HH:mm:ss"。 | 2016-08-11 19:36:01 |
version | String | 是 | 3 | 调用的接口版本,固定为:1.0。 | 1.0 |
业务参数
参数 | 类型 | 必填 | 最大长度 | 描述 | 示例值 |
---|---|---|---|---|---|
out_trade_no | String | 是 | 64 | 商户网站唯一订单号。 | 70501111111S001111119 |
trade_no | String | 是 | 64 | 该交易在支付宝系统中的交易流水号。最长64位。 | 2016081121001004630200142207 |
total_amount | Price | 是 | 9 | 该笔订单的资金总额,单位为RMB-Yuan。取值范围为[0.01,100000000.00],精确到小数点后两位。 | 9.00 |
seller_id | String | 是 | 16 | 收款支付宝账号对应的支付宝唯一用户号。 以2088开头的纯16位数字。 | 2088111111116894 |
接口测试类
package com.shanjupay.transaction.controller;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 支付宝接口对接测试类
* @author Administrator
* @version 1.0
**/
@Slf4j
@Controller
//@RestController//请求方法响应统一json格式
public class PayTestController {
/*支付宝的公共参数设置:支付宝用户的应用id,支付宝客户端的私匙,支付宝服务端提供的公匙,支付宝服务端的网关,签名算法类型*/
String APP_ID = "2021000117697501"; //应用id,应用私钥
String APP_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyP/pRhNepn/uAoP5Ndu8XkPqJp4hM8rMgcOAq1bHJJNs/yRbJUZzGDaWMJb3x71GSuf1ThfDT0wbVQcO7Ray0FqJk6tosigPJBQh2Rixhf1Uo6LU4QPK31jrTv6vcJm16QswwV+fPb6NsVA5hieCtuHI4nLqz542szINYFbSm9w3HOu0rWgW6E8dJDNkJpcYmEK6IUzQRiFEgG1ny2cAFQL3Y9X8wsH4AblI43ggXgrwQ2HajYfa4VdcVa9/oM7VyQGs3uTXNY3pkGwSBWuLUecKcA69M+Qa93ZkhHlWhzXT5Thfzc0mLKt/qdcUe04jMPK6VCkd/q/7tGuQ8uFzzAgMBAAECggEBAI9OMmBxjbVY4qlyRaFf2i83JsWexE0g3nRZa0/kx+9vyzlH4SLvkzwDYrH+8evdPNba4tjQmWKjiR3Qpp0cEhIjFGJQEiG2v/5QJpJ4LlwgNAYUuQVF6h10hY0RzwjKeD/QDjtboQm7tkZ0ea9fWxwvat0q3EuhAN0I+xvJL5j/NKBT0f4xPw8ZlIs4loRjnbjfL307ZYvmJXLLYcISvCeB3Ye6hAv9LLTUWrYGCTXgRJVGIx7bJhNSbPDIC76CwfYhKHxXYyzCtOQZ/4ewdMl4IxTHvKCFRJgNQP8yo5DBcRkE0JPh8H2B9j8YGqVmXqaMn6eUlqW1OgzZzdqjQ3kCgYEA4uLC08KrBO7GPVspa6DDIVh+NqHdERhbqhsGTogTTJP7UZkjSHQNx/rwk9NdBaRTBxYzGhog0q1I8Oe4Ufa6d/ZRUoFc9GMDCaE7pjqITT15iymuyOD3B/5NUeHbQ0hm41gkKdDgNJe5lvDZpbfp1J95UPgXWDU+dfJbHxK9vBUCgYEAyR+FBFitihbvZfsPR0YdpvF+MjR85WmEzVc4Jp+j60f2YaATnO1rs8qRx0RDD11jTdm+g1Z3FRhz/p3kx76XQoDyk8VjkfoWgKKxo9XWphSkbt7E22up0GPQCe2ahkkcAgsEDcMFVlKUGU0cGOJQZuIQbbMBm11vivsb/pxwjucCgYEAlgGbEmsIq1A7HWHidthpauiZOgG2qZDTOhp4BwAM0nqclQyMuWCRpACTgwkh3ZMRmgPhcYaI4QHU0gJCaV6ZVqsyhTwmeyXjYkCJsZPNflQAwjOi7glfCpfmAxcy4r9B11n1Pvhs5BjUialgHSMFpKBzk0cUGCvLyiucd2TqsSkCgYBE6TbTWWsEkH0wTQhcHGsWg1IA87kDhdcJ4GON4E7y07JYmtd9gl/Pt42hYAM2JYJb70p2h86/fKRpzkHQKr56++GhvhUytCS3qIcDIsasGxCIKG383HPPwhNLA41Zi308OfgGmxaeECdMT/5bjFeOGNEWNSpMyIPqc8WQxJtQawKBgC2M5Zrk9sDTSNUHMsIH7VvnbxM8HlJ/c8oE0WEngYjVIjYAGWJpweBTJaJ2uTlfRMvlldyX9G8L3o+j++B4OkAwCRjINfN+r4h/DyrjuIJxBHSKzijZ4e52HWNrr5yyHU/ClwRRRXkhCDfiC43nkjKjjVoWklxhVmCco/YQEQny";
String ALIPAY_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoMuDB0ouaUSXo5lI3wvZxusW7tAPkeC+kDnZqmLpet/J/KAY0f5F9YtumrrEBHdtFGPvvE8DTJe6/QEhr8TFNAKcmpH5E/0Z1j/xO4JKwpSCf3KrqFogGGUWQ3um04AFGv8jZzUHZfhQcXxJMbsGfyCmJ2Iwm+lijr2za3J+uZyrGTYTJIixDOOGoJk4UhejBCquXWTxo3TO9UHGpNEWTRo9SPqqi5uZ2gAGFNxv1+FOmSI88YKgQ4uQrKeSA80xfs51RbWq4IxkiuRvJC+rw4UK5BUaFE0HfpLfiI9F7+KrTw+GE/20DrIRUmp2Y/Uej5pkEUh8wQTG4GIX0sggPwIDAQAB"; //支付宝公钥
String CHARSET = "utf-8";
String serverUrl = "https://openapi.alipaydev.com/gateway.do"; //支付宝接口的网关地址,正式"https://openapi.alipay.com/gateway.do"
String sign_type = "RSA2"; //签名算法类型
@GetMapping("/alipaytest")
public void alipaytest(HttpServletRequest httpRequest,
HttpServletResponse httpResponse) throws ServletException, IOException {
//构造sdk的客户端对象
AlipayClient alipayClient = new DefaultAlipayClient(serverUrl, APP_ID, APP_PRIVATE_KEY, "json", CHARSET, ALIPAY_PUBLIC_KEY, sign_type); //获得初始化的AlipayClient
AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
// alipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");
// alipayRequest.setNotifyUrl("http://domain.com/CallBack/notify_url.jsp");//在公共参数中设置回跳和通知地址(非必选)
// 把业务参数合并到一个参数里面传输
alipayRequest.setBizContent("{" +
" \"out_trade_no\":\"20150420010101017\"," +
" \"total_amount\":\"88.88\"," +
" \"subject\":\"Iphone6 16G\"," +
" \"product_code\":\"QUICK_WAP_PAY\"" +
" }");//填充业务参数
String form="";
try {
//请求支付宝下单接口,发起http请求
form = alipayClient.pageExecute(alipayRequest).getBody(); //调用SDK生成表单
} catch (AlipayApiException e) {
e.printStackTrace();
}
httpResponse.setContentType("text/html;charset=" + CHARSET);
httpResponse.getWriter().write(form);//直接将完整的表单html输出到页面
httpResponse.getWriter().flush();
httpResponse.getWriter().close();
}
}
问题:支付宝网关地址和方法地址的关系与作用?
实际请求的接口(SDK自动设置):alipay.trade.wap.pay
支付宝的网关:"https://openapi.alipaydev.com/gateway.do"
所有请求都要发送到支付宝网关,具体的请求地址根据调用的方法自动设置,
form = alipayClient.pageExecute(alipayRequest).getBody(); //调用SDK生成表单
只要调用pageExecute方法,会自动设置URL
实际测试
手动设置二维码: https://localhost:56050/merchant/alipaytest
生成二维码
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIAQAAAACFI5MzAAABxklEQVR42u2YMa6DQAxEjSi25Ah7E7gYEkhcLNyEI1BSIPxnzEKUSL+dfH1liwj2UVhrz9gb89+WfclfJ6OZNe6bWZ+tW+sF77WUTO5Hs2d/2LBs1TosHntKsljb+GIVnjY+bdZ8gOyG2LDaZvoQ8SUdDLDD6egJ8zM5Ns3SjA9fMycgUaORmvLzWr0CEmu35Ktln9fhTcECMnl6NCNUchbHmJOaoDjmtc/cRKGyVq78yIinAyYRm6jWMaQiJbvBoSxvldMkwrDK6agI8nOcOp1oWPCM+daPhqBZwKUd2ugRJS0TbUNKxkydurVgyFSfE3qHlOB0WKjObxAlqrVbtQQSPahTOAVTg4JREyy8w6bgm5GpNMtJeBUlSsPaM5JUS8luRmEaw4JfY9NLbCpSHCrCMnrGs5eISJREzbAgENSKQSW1lJR2FeNDZOqeXVQk7BEH08KqLUy7WmspmRYWJVs2x0j2rO6eojUkFs6EgyznN6dpS8l5lznb1daxY9jl5CJSZlgWh4VO79hkpNxlokI6hvqsECFhgCiTNF8XTDmJ21QdAb6djoLEnZbaoFtSNM/5WkPKXSbEapzj/eqaKvL9F+efkR8ilfEmaYEBrwAAAABJRU5ErkJggg==
<form name="punchout_form" method="post" action="https://openapi.alipaydev.com/gateway.do?charset=utf-8&method=alipay.trade.wap.pay&sign=VRF%2Bhsy6SJ%2FhUEoSfEfIp%2BEr3heoBwrPtc3GFOgyPII3%2B2ZtEpktDw0AHnmAaH4qZLoLnS4kWdlDC6rdXNkDp9grb%2F4ILOpkZN7ExjV4zVZpLLCJDnmEUhPUCsSNl1T7m7rCCWhkMrFboSsIKeIAU8ZStVpdKjn%2BQzJ5Iq%2BHxFPRCKnytMbZ8GmtQ6by2BWa%2FkSGFriQLgEfe29xO06kEW7%2B3lG2VP%2FLSrsFKWj6Wohl%2Fv28nzPyqOfJRI5y4RRF5t5o6OIrIrO37RC5wlbopjxja8ZJ1aqA5EfXTbmQXXXsXrAAZ%2BF1y94ur9RhD44JwuhfbaBuCk2u%2B1htccc9PQ%3D%3D&version=1.0&app_id=2021000117697501&sign_type=RSA2×tamp=2021-07-31+16%3A35%3A55&alipay_sdk=alipay-sdk-java-3.7.73.ALL&format=json">
<input type="hidden" name="biz_content" value="{ "out_trade_no":"20150420010101017", "total_amount":"88.88", "subject":"Iphone6 16G", "product_code":"QUICK_WAP_PAY" }">
<input type="submit" value="立即支付" style="display:none" >
</form>
<script>document.forms[0].submit();</script>
支付宝应用app网站支付结合聚合平台的流程
用户扫描聚合平台二维码(获取聚合平台调用支付宝支付的接口)--> 对应接口调用支付开放的网站支付接口--->挑战到支付宝的网页---> 用户输入密码---> 完成支付
聚合平台:通过支付宝的SDK以及商户提供的公共参数(支付渠道参数)向支付宝的接口发起请求
13 微信的JSAPI支付接口接入流程
调用流程
1、商户系统生成二维码
2、用户使用微信客户端扫描二维码,请求商户系统,在商户系统创建商品订单
3、商户系统调用微信下单接口生成微信订单
4、商户系统向前端响应支付参数,在客户端调起微信客户端
5、用户在微信客户端请求微信进行支付,输入密码,完成支付。
6、微信异步通过商户系统支付结果。
7、商户系统调用微信接口查询支付结果
具体调用步骤
注意:上述参数需要注册企业微信公众号,由微信分配给商户使用
step1:获取授权码
step2:获取openid(微信商户的唯一标识)
问题:微信JSAPI接口与OAuth2.0协议的联系
微信JSAPI接口调用流程:
1)客户端申请授权码
2)微信认证授权服务器确认客户端身份后,返回授权码
3)客户端再携带授权码申请openid(具有时效性),服务端响应返回openid
4) 客户端携带openid等信息请求创建微信订单。
Oauth2.0协议:
授权码模式也是进行了2次交互,第一次验证客户端身份(认证),第2次确认资源拥有着身份(授权)
相比较JSAPI的认证:
1)第一个也是验证客户端身份(认证)
2)第2次则是对于客户端申请的权限的赋予(授权)
核心思想:认证与授权这两步必须分开来做,确保安全性
微信接口调用的源码分析
接口调用的公共参数
String appID = "wxd2bf2dba2e86a8c7"; // 应用ID
String mchID = "1502570431"; // 商户ID
String appSecret = "cec1a9185ad435abe1bced4b93f7ef2e"; // 应用的密匙
String key = "95fe355daca50f1ae82f0865c2ce87c8"; // 接口调用密码
//申请授权码地址
String wxOAuth2RequestUrl = "https://open.weixin.qq.com/connect/oauth2/authorize"; // 获取授权码地址
//授权回调地址
String wxOAuth2CodeReturnUrl = "http://xfc.nat300.top/transaction/wx-oauth-code-return"; // 授权码返回的回调地址
String state="";
注意点:
- 上面的参数需要注册企业微信公众号获取
- 授权码地址由微信提供,回调地址需要商户注册域名提供用于接受授权码
step1:获取授权码
//获取授权码,请求该地址等价于请求微信授权码地址
@GetMapping("/getWXOAuth2Code")
public String getWXOAuth2Code(HttpServletRequest request, HttpServletResponse response){
//https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
String url = String.format("https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect",
appID, wxOAuth2CodeReturnUrl
);
return "redirect:"+url;
}
聚合支付平台利用商户的微信支付渠道参数通过http请求微信授权码的地址
注意:
1)上面利用了重定向,虽然请求的是聚合支付平台的地址,平台会设置会设置好相关参数,然后再去请求微信的地址。
2)需要给微信平台提供聚合支付平台的回调地址,微信平台会通过回调地址传入授权码!!!!!!!!!!!!!!!!
step2:接受授权码并发起openid的申请
问题:聚合支付平台向微信平台申请验证码如何实现异步回调?
需要两个平台配合完成,聚合支付平台在申请授权码的同时需要将接受授权码的回调地址给传递过去,
微信平台接受到请求后,通过回调地址将授权码给传递给聚合支付平台。
实现
/**
* //授权码回调,传入授权码和state,/wx-oauth-code-return?code=授权码&state=
* @param code 授权码
* @param state 申请授权码传入微信的值,被原样返回
* @return
*/
@GetMapping("/wx-oauth-code-return")
public String wxOAuth2CodeReturn(@RequestParam String code, @RequestParam String state){
//https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
String url = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
appID, appSecret, code
);
//申请openid,请求url
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
//申请openid接口响应的内容,其中包括了openid
String body = exchange.getBody();
log.info("申请openid响应的内容:{}",body);
//获取openid
String openid = JSON.parseObject(body).getString("openid");
//重定向到统一下单接口
return "redirect:http://xfc.nat300.top/transaction/wxjspay?openid=" + openid;
}
注意:在接受授权码的回调地址中,拿到授权码立刻再去请求的openid,拿到之后直接请求下单接口(借助openid)。
step3:使用openid屌用微信接口下单
- 在调用统一下单接口完成后,此时需要打开微信客户端完成支付,这个过程是,使用用户扫描聚合支付的二维码最终的结果就是在微信
浏览器打开H5网页,在网页中执行 JS调起微信客户端
//统一下单,接收openid
@GetMapping("/wxjspay")
public ModelAndView wxjspay(HttpServletRequest request, HttpServletResponse response) throws Exception {
//step1:创建sdk客户端
WXPay wxPay = new WXPay(new WXPayConfigCustom());
//step2:构造微信下单接口需要的请求的参数
Map<String,String> requestParam = new HashMap<>();
requestParam.put("out_trade_no","10029293889"); //订单号(不能重复)
requestParam.put("body", "iphone8");//订单描述
requestParam.put("fee_type", "CNY");//人民币
requestParam.put("total_fee", String.valueOf(1)); //金额,微信的基本单位是分
requestParam.put("spbill_create_ip", "127.0.0.1");//客户端ip
requestParam.put("notify_url", "none"); //微信异步通知支付结果接口,暂时不用
requestParam.put("trade_type", "JSAPI");
String openid = request.getParameter("openid");
requestParam.put("openid",openid); // 将之前拿到的oepnid给设置好
//调用统一下单接口,获取微信的响应数据
Map<String, String> resp = wxPay.unifiedOrder(requestParam);
//step3:设置h5网页需要的数据
Map<String,String> jsapiPayParam = new HashMap<>();
jsapiPayParam.put("appId",appID);
jsapiPayParam.put("timeStamp",System.currentTimeMillis()/1000+"");
jsapiPayParam.put("nonceStr", UUID.randomUUID().toString());//随机字符串
jsapiPayParam.put("package","prepay_id="+resp.get("prepay_id"));
jsapiPayParam.put("signType","HMAC-SHA256");
/*这里需要按照规范设置好签名数据,此处采用的签名的hash函数是HMAC-SHA256 !!!!!!!!!!!!!!!!*/
jsapiPayParam.put("paySign", WXPayUtil.generateSignature(jsapiPayParam,key, WXPayConstants.SignType.HMACSHA256));
//step4: 将h5网页响应给前端(需要用户输入密码)
return new ModelAndView("wxpay",jsapiPayParam);
}
注意:拿到openid重定向到下单接口,使用sdk构造客户端请求微信,拿到微信返回的结果后,将结果写入到h5网页中,反馈给微信客户端端前端,此时用户可以通过微信客户端进行更进一步操作。
微信下单的参数:
requestParam.put("out_trade_no","10029293889"); //订单号(不能重复)
requestParam.put("body", "iphone8");//订单描述
requestParam.put("fee_type", "CNY");//人民币
requestParam.put("total_fee", String.valueOf(1)); //金额
requestParam.put("spbill_create_ip", "127.0.0.1");//客户端ip
requestParam.put("notify_url", "none"); //微信异步通知支付结果接口,暂时不用
requestParam.put("trade_type", "JSAPI");
String openid = request.getParameter("openid");
requestParam.put("openid",openid); // 将之前拿到的oepnid给设置好
问题:微信作为公网服务,在请求回调地址的时候,如何访问局域网中聚合平台的回调地址?
方法:采用内网穿透技术
外网服务访问内网的服务(通过域名提供商的内网穿透产品实现)。
NAT技术,内网两台主机A和B,
主机A: 内网ip1:本地应用端口8080:外网的公网ip:外网的应用端口
主机B: 内网ip2:本地应用端口8080:外网的公网ip:外网的应用端口
NAT表内容(通过NAT表两台应用使用相同端口同外部网络通信,但是在外网中他们的端口会不同,即服务器端是无法获取到内网的ip和端口的):
内网ip1:本地应用端口8080 <=> 公网ip:本地应用端口8080
内网ip2:本地应用端口8080 <=> 公网ip:本地应用端口8081
NAT技术进行端口映射(放入NAT表):
主机A: 公网ip:本地应用端口8080:外网的公网ip:外网的应用端口
主机B: 公网ip:本地应用端口8081:外网的公网ip:外网的应用端口
14 C2B的微信与支付宝的流程总结(重要)
基本思想:利用两个平台提供的下单接口。
支付宝的h5页面流程:
用户扫描聚合平台二维码(访问聚合平台的接口)--> 平台设置参数并调用支付开放的网站支付接口--->跳转到支付宝的网页---> 用户输入密码---> 完成支付
微信的JSAP流程:
用户扫描聚合平台二维码(访问聚合平台的接口) --> 平台设置参数调用微信支付接口 --> 返回微信的h5页面 --> 用户输入密码完成支付
区别:微信的JSAPI方法在请求支付接口前需要先申请授权码以及openid后才能去调用微信的下单接口
15 门店的二维码生成(支付入口url生成)(建立在12,13基础上)门店的二维码:商户从自己管辖的门店中选择二维码,点击平台生成
step1:实现根据商户id查询门店id的接口
@Api(value = "商户平台-门店管理", tags = "商户平台-门店管理", description = "商户平台-门店的增删改查")
@RestController
@Slf4j
public class StoreController {
@Reference
MerchantService merchantService;
@ApiOperation("根据当前商户的id查询当前商户所有的门店记录")
@ApiImplicitParams({
@ApiImplicitParam(name="pageNo",value = "页码",required = true,dataType = "int",paramType = "query"),
@ApiImplicitParam(name="pageSize",value = "每页记录数",required = true,dataType = "int",paramType = "query")
})
@PostMapping("/my/stores/merchants/page")
public PageVO<StoreDTO> queryStoreByPage(Integer pageNo, Integer pageSize){
//商户id
Long merchantId = SecurityUtil.getMerchantId(); // 通过jwt中的租户id获取相关联的商户id
//查询条件
StoreDTO storeDTO = new StoreDTO();
storeDTO.setMerchantId(merchantId); //商户id
//调用service查询列表
PageVO<StoreDTO> storeDTOS = merchantService.queryStoreByPage(storeDTO, pageNo, pageSize);
return storeDTOS;
}
}
问题:如何获取商户的id?
利用前端传递过来的jwt,每次客户端请求都需要携带jwt,商户应用平台通过解析jwt然后获取商户唯一的租户id,然后根据租户id获取相关联的商户id.
实现
public class SecurityUtil {
/*从前端请求中获取json web token(jwt)*/
public static LoginUser getUser() {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (servletRequestAttributes != null) {
HttpServletRequest request = servletRequestAttributes.getRequest();
Object jwt = request.getAttribute("jsonToken");
if (jwt instanceof LoginUser) {
return (LoginUser) jwt;
}
}
return new LoginUser();
}
public static Long getMerchantId(){
MerchantService merchantService = ApplicationContextHelper.getBean(MerchantService.class);
/*将jwt中的租户id去除*/
LoginUser user = getUser();
System.out.println(user.toString());
MerchantDTO merchant = merchantService.queryMerchantByTenantId(user.getTenantId());
Long merchantId = null;
if(merchant!=null){
merchantId = merchant.getId();
}
return merchantId;
}
/**
* 转换明文jsonToken为用户对象
* @param token
* @return
*/
public static LoginUser convertTokenToLoginUser(String token) {
// 实际传输过程中客户端会对token采用base64编码然后进行传递。
token = EncryptUtil.decodeUTF8StringBase64(token);
LoginUser user = new LoginUser();
JSONObject jsonObject = JSON.parseObject(token);
String payload = jsonObject.getString("payload");
Map<String, Object> payloadMap = JSON.parseObject(payload, Map.class);
user.setPayload(payloadMap);
user.setClientId(jsonObject.getString("client_id"));
user.setMobile(jsonObject.getString("mobile"));
user.setUsername(jsonObject.getString("user_name"));
return user;
}
}
jwt中存储的信息包括:
1) 客户端id
2) 用户手机号
3) 用户名称
4)
注意:long类型的精度丢失问题 ?
JavaScript 处理 Long 长度大于 17 位度丢失的问题对于 Long 类型的数据,如果我们在 Controller 层将结果序列化为 JSON,直接传给前端的话,在 Long 长度大于 17 位时会出现精度丢失的问题。如何避免精度丢失呢?最常用的办法就是将 Long 类型字段统一转成 String 类型。
根据商户id查询门店接口测试(待做):
由于已经接入SaaS,请求统一走网关的端口56010
问题:Zuul在项目中的作用?
分两种情况讨论:
1)当用户已经登录后,所有的请求都会携带jwt,当请求到达网关后,网关的filter会将jwt转换为json对象放入到请求中,
此时请求携带解析的好的json对象访问平台接口。
2)当用户没有登录,即没有jwt,则请求会转发给登录接口,先进行用户认证与授权。
实现
public class AuthFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof OAuth2Authentication)) { // 无token访问网关内资源的情况,目前仅有uaa服务直接暴露
return null;
}
OAuth2Authentication oauth2Authentication = (OAuth2Authentication) authentication;
Authentication userAuthentication = oauth2Authentication.getUserAuthentication();
Map<String, String> jsonToken = new HashMap<>(
oauth2Authentication.getOAuth2Request().getRequestParameters());
if (userAuthentication != null) {
jsonToken.put("user_name", userAuthentication.getName());
}
ctx.addZuulRequestHeader("jsonToken", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
return null;
}
}
step2:生成二维码
1、商户登录商户应用平台 ,查询门店列表
2、商户平台 请求交易 服务获取门店二维码URL
3、商户平台 根据 URL生成二维码
基本步骤:
1)用户查询当前所有的门店
2)选择一个门店,点击生成二维码
二维码生成
3)此时商户应用接口会调用交易服务的接口,交易服务接口产生二维码对应的url,并返回
4)商户平台将url转化为二维码并展示给商户。
生成门店二维码的url的方法
/**
* 生成门店二维码的url
*
* @param qrCodeDto@return 支付入口(url),要携带参数(将传入的参数转成json,用base64编码)
* @throws BusinessException
*/
//从配置文件读取支付入口地址
@Value("${shanjupay.payurl}")
String payurl;
@Override
public String createStoreQRCode(QRCodeDto qrCodeDto) throws BusinessException {
//01 校验商户id和应用id和门店id的合法性(再次确认当前商户与当前应用以及当前门店存在绑定关系)
verifyAppAndStore(qrCodeDto.getMerchantId(),qrCodeDto.getAppId(),qrCodeDto.getStoreId());
//02 生成扫描二维码所访问的url
PayOrderDTO payOrderDTO = new PayOrderDTO();
payOrderDTO.setMerchantId(qrCodeDto.getMerchantId());
payOrderDTO.setAppId(qrCodeDto.getAppId());
payOrderDTO.setStoreId(qrCodeDto.getStoreId());
payOrderDTO.setSubject(qrCodeDto.getSubject());//显示订单标题
payOrderDTO.setChannel("shanju_c2b");//服务类型,要写为c扫b的服务类型
payOrderDTO.setBody(qrCodeDto.getBody());//订单内容
String jsonString = JSON.toJSONString(payOrderDTO); //转成json
String ticket = EncryptUtil.encodeUTF8StringBase64(jsonString); //base64编码
// 03 目标是生成一个支付入口 的url,需要携带参数将传入的参数转成json,用base64编码
String url=payurl+ticket;
return url;
}
支付入口统一为:
shanjupay:
payurl: "http://127.0.0.1:56010/transaction/pay?entry/"
应用层的接口:
总结:
1)调用transcation微服务获取二维码对应的支付链接。
2)调用二维码工具类将支付链接转化为二维码图片。
step3:二维码生成接口测试(待完成)
16 支付订单页面的生成(扫描返回订单页面)订单页面展示给用户信息:
商户名称
订单描述
支付金额
支付确认按钮
场景:用户扫描聚合平台的二维码后,聚合平台请求成功后,会返回确认页面
/**
* 支付入口,客户扫描聚合支付平台的二维码后,实际上就会访问聚合支付平台的支付入口,该支付入口
* 平台只有一个,但是在生成二维码的时候,会将商户的信息通过base64编码作为参数附加在聚合支付路口的路径上,
* 这样就能明确当前的支付入口是向谁付款。
* 支付入口的作用:
* 1)解析请求路径从参数
* 2)解析第三方支付类型,比如支付宝/微信都是内部打开链接,因此可以通过客户端的类型确定当前的第三方支付类型。
* 3)根据第三方支付平台类型,返回支付确认界面
* @param ticket 传入数据,对json数据进行的base64编码
* @param request
* @return
*/
@RequestMapping("/pay-entry/{ticket}")
public String payEntry(@PathVariable("ticket")String ticket, HttpServletRequest request) throws Exception {
//1、准备确认页面所需要的数据
String jsonString = EncryptUtil.decodeUTF8StringBase64(ticket);
//将json串转成对象
PayOrderDTO payOrderDTO = JSON.parseObject(jsonString, PayOrderDTO.class);
//将对象的属性和值组成一个url的key/value串
String params = ParseURLPairUtil.parseURLPair(payOrderDTO);
//2、解析客户端的类型(微信、支付宝),通过客户端类型确定具体的第三方支付渠道
BrowserType browserType = BrowserType.valueOfUserAgent(request.getHeader("user-agent"));
switch (browserType){
case ALIPAY:
//转发到确认页面
return "forward:/pay-page?"+params;
case WECHAT:
//转发到确认页面
return "forward:/pay-page?"+params;
default:
}
//不支持客户端类型,转发到错误页面
return "forward:/pay-page-error";
}
问题:用户扫描商家提供的支付平台二维码,平台是如何辨别用户所使用的第三方支付平台呢?
这个问题可以转化为对扫码客户端类型的区分,在http请求头的键值对中,有一个key是user-agent,
支付宝与微信扫码打开对应的user agent不一样,通过这个区分支付方式。然后再去调用对应的第三方支付行下单接口。
返回支付确认界面。
注意:这里通过user agent的客户端判断,使得一种二维码能够使用多个平台扫码支付。
不同的第三方支付平台开发的下单接口的交互流程不一样,相比较支付宝提供B2C的网站支付产品接口,微信的JSAPI接口交互流程要复杂一点。
聚合支付平台实际上完成了不同第三方支付渠道参数接口的对接流程的业务代码,在用户扫码的时候,通过客户端类型去调用不同支付渠道的流程。
如何不是平台所支持的支付类型,或者商家没有绑定支付渠道参数,则返回错误页面。
17 立即支付功能实现
场景(当用户扫描二维码后,客户端浏览器显示页面):
用户输入金额点击确认支付 => 聚合支付平台保存订单发起支付请求 => 根据支付渠道不同调用不同的支付渠道参数并调用第三方发支付渠道接口发起支付请求 => 第三方支付平台完成支付返回支付平凭据 => 聚合支付平台解析支付凭证 => 聚合支付平台向用户返回支付凭证
整体执行流程如下:
1、顾客输入金额,点击立即支付
2、请求交易服务,交易服务保存订单
3、交易服务调用支付渠道代理服务的支付宝下单接口
4、支付渠道代理服务调用支付宝的统一下单接口。
5、支付凭证返回
问题:为什么需要将支付渠道代理服务作为单独的微服务?
原因:支付服务渠道服务主要是对接各种类型的第三方类型平台,这部分内容是很容易发生变化的,交易服务的业务逻辑是不会发生改变的,因此将交易服务与支付渠道代理服务
给分开,减少交易服务对于第三方支付平台的耦合性。
17-1 支付渠道代理服务中支付宝下单接口实现
@Service
@Slf4j
public class PayChannelAgentServiceImpl implements PayChannelAgentService {
/**
* 调用支付宝的下单接口
*
* @param aliConfigParam 支付渠道配置的参数(配置的支付宝的必要参数)
* @param alipayBean 业务参数(商户订单号,订单标题,订单描述,,)
* @return 统一返回PaymentResponseDTO
*/
@Override
public PaymentResponseDTO createPayOrderByAliWAP(AliConfigParam aliConfigParam, AlipayBean alipayBean) throws BusinessException {
/*支付宝交易接口的渠道参数*/
String url = aliConfigParam.getUrl(); //支付宝接口网关地址
String appId = aliConfigParam.getAppId(); //支付宝应用id
String rsaPrivateKey = aliConfigParam.getRsaPrivateKey(); //应用私钥
String format = aliConfigParam.getFormat(); //json格式
String charest = aliConfigParam.getCharest(); //编码
String alipayPublicKey = aliConfigParam.getAlipayPublicKey(); //支付宝公钥
String signtype = aliConfigParam.getSigntype(); //签名算法
String returnUrl = aliConfigParam.getReturnUrl(); //支付成功跳转的url
String notifyUrl = aliConfigParam.getNotifyUrl(); //支付结果异步通知的url
//构造sdk的客户端对象
AlipayClient alipayClient = new DefaultAlipayClient(url, appId, rsaPrivateKey, format, charest, alipayPublicKey, signtype); //获得初始化的AlipayClient
AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(alipayBean.getOutTradeNo());//商户的订单,就是闪聚平台的订单
model.setTotalAmount(alipayBean.getTotalAmount());//订单金额(元)
model.setSubject(alipayBean.getSubject());
model.setBody(alipayBean.getBody());
model.setProductCode("QUICK_WAP_PAY");//产品代码,固定QUICK_WAP_PAY
model.setTimeoutExpress(alipayBean.getExpireTime());//订单过期时间
alipayRequest.setBizModel(model);
alipayRequest.setReturnUrl(returnUrl);
alipayRequest.setNotifyUrl(notifyUrl);
try {
//请求支付宝下单接口,发起http请求
AlipayTradeWapPayResponse response = alipayClient.pageExecute(alipayRequest);
PaymentResponseDTO paymentResponseDTO = new PaymentResponseDTO();
log.info("调用支付宝下单接口,响应内容:{}",response.getBody());
paymentResponseDTO.setContent(response.getBody());//支付宝的响应结果
return paymentResponseDTO;
} catch (AlipayApiException e) {
e.printStackTrace();
throw new BusinessException(CommonErrorCode.E_400002);
}
}
}
17-2 交易服务中支付宝下单接口实现
交易服务:需要接受前端传递过来的参数,然后调用相应的支付接口
/**
* 支付宝的下单接口,前端订单确认页面,点击确认支付,请求进来
* @param orderConfirmVO 订单信息
* @param request
* @param response
*/
@ApiOperation("支付宝门店下单付款")
@PostMapping("/createAliPayOrder")
public void createAlipayOrderForStore(OrderConfirmVO orderConfirmVO, HttpServletRequest request, HttpServletResponse response) throws BusinessException, IOException {
// step1:解析前端传入的订单参数,用于保存到订单表中
PayOrderDTO payOrderDTO = PayOrderConvert.INSTANCE.vo2dto(orderConfirmVO);
String appId = payOrderDTO.getAppId();
AppDTO app = appService.getAppById(appId);
payOrderDTO.setMerchantId(app.getMerchantId());//商户id
payOrderDTO.setTotalAmount(Integer.parseInt(AmountUtil.changeY2F(orderConfirmVO.getTotalAmount().toString())));
payOrderDTO.setClientIp(IPUtil.getIpAddr(request));
//step2:保存订单信息到聚合支付平台,然后调用第三方给支付渠道接口下单(平台订单的保存在支付之前),返回第三方支付平台的响应信息
PaymentResponseDTO<String> paymentResponseDTO = transactionService.submitOrderByAli(payOrderDTO); //保存订单,调用支付渠道代理服务的支付宝下单
//step3:将支付宝的下单接口的http信息返回给客户端(用户点击确认支付看到的页面),这里的response写入展示是给客户的信息
String content = paymentResponseDTO.getContent();
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(content); //直接将完整的表单html输出到页面
response.getWriter().flush();
response.getWriter().close();
}
}
保存订单的实现service实现
问题:为什么订单号的生成采用雪花片算法?
这里订单号采用雪花片算法保证唯一性,但并没有将订单号设置为主键。
订单的信息:
1)主键id 2)聚合支付订单号(雪花算法生成) 3)所属商户 4)商户下门店 5)所属应用 6) 支付渠道 7)原始渠道商户id 8)原始渠道商户应用id 9)原始渠道订单号 10)聚合支付的渠道
11)商户订单号 12)商品标题 13)订单描述 14)币种CNY 15)订单总金额,单位为分
..... )订单过期时间 )支付成功时间
实现
/**
* 保存支付宝订单,1、保存订单到闪聚平台,2、调用支付渠道代理服务调用支付宝的接口
*
* @param payOrderDTO
* @return
* @throws BusinessException
*/
@Override
public PaymentResponseDTO submitOrderByAli(PayOrderDTO payOrderDTO) throws BusinessException {
payOrderDTO.setChannel("ALIPAY_WAP");//设置支付渠道
//保存订单到闪聚平台数据库(公用方法)
PayOrderDTO save = save(payOrderDTO);
//调用支付渠道代理服务支付宝下单接口
PaymentResponseDTO paymentResponseDTO = alipayH5(save.getTradeNo());
return paymentResponseDTO;
}
==============================================================================================================================
// 公用方法用于保存订单到聚合支付平台
//保存订单(公用)
private PayOrderDTO save(PayOrderDTO payOrderDTO) throws BusinessException {
PayOrder payOrder = PayOrderConvert.INSTANCE.dto2entity(payOrderDTO);
//订单号的生成采用雪花片算法
payOrder.setTradeNo(PaymentUtil.genUniquePayOrderNo()); //采用雪花片算法
payOrder.setCreateTime(LocalDateTime.now()); //创建时间
payOrder.setExpireTime(LocalDateTime.now().plus(30, ChronoUnit.MINUTES));//过期时间是30分钟后
payOrder.setCurrency("CNY");//人民币
payOrder.setTradeState("0");//订单状态,0:订单生成
payOrderMapper.insert(payOrder);//插入订单
return PayOrderConvert.INSTANCE.entity2dto(payOrder);
}
===================================支付宝支付接口,返回支付结果页面============================================================
/**
* 用户扫码后,点击确认支付,使用支付宝提供的SDK实现下单支付。
* @param tradeNo
* @return
*/
//调用支付渠道代理服务的支付宝下单接口
private PaymentResponseDTO alipayH5(String tradeNo){
//订单信息,从数据库查询订单
PayOrderDTO payOrderDTO = queryPayOrder(tradeNo);
//组装alipayBean
AlipayBean alipayBean = new AlipayBean();
alipayBean.setOutTradeNo(payOrderDTO.getTradeNo()); //设置支付宝的订单号
try {
alipayBean.setTotalAmount(AmountUtil.changeF2Y(payOrderDTO.getTotalAmount().toString()));
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException(CommonErrorCode.E_300006);
}
alipayBean.setSubject(payOrderDTO.getSubject()); // 设置订单的名称
alipayBean.setBody(payOrderDTO.getBody()); // 设置订单的内容
alipayBean.setExpireTime("30m"); // 设置订单的过期时间
//支付渠道配置参数,从数据库查询(这里支付渠道参数查询是会优先从redis缓存中获取)
//String appId,String platformChannel,String payChannel
PayChannelParamDTO payChannelParamDTO = payChannelService.queryParamByAppPlatformAndPayChannel(payOrderDTO.getAppId(), "shanju_c2b", "ALIPAY_WAP");
String paramJson = payChannelParamDTO.getParam();
AliConfigParam aliConfigParam = JSON.parseObject(paramJson, AliConfigParam.class); //支付渠道参数
aliConfigParam.setCharest("utf-8"); //字符编码
//AliConfigParam aliConfigParam, AlipayBean alipayBean
PaymentResponseDTO payOrderByAliWAP = payChannelAgentService.createPayOrderByAliWAP(aliConfigParam, alipayBean);
return payOrderByAliWAP;
}
/**
* 根据订单号从数据库中查询订单信息(这一步可以加缓存进行优化,系统没有这样做)
* @param tradeNo
* @return
*/
public PayOrderDTO queryPayOrder(String tradeNo){
PayOrder payOrder = payOrderMapper.selectOne(new LambdaQueryWrapper<PayOrder>().eq(PayOrder::getTradeNo, tradeNo));
return PayOrderConvert.INSTANCE.entity2dto(payOrder);
}
立即支付的http接口实现
@ApiOperation("用户看到生成的商户订单h5信息,点击确认支付后调用支付宝门店下单接口付款")
@PostMapping("/createAliPayOrder")
public void createAlipayOrderForStore(OrderConfirmVO orderConfirmVO, HttpServletRequest request, HttpServletResponse response) throws BusinessException, IOException {
// step1:整合前端传入的订单页面展示信息,用于保存到订单表中
PayOrderDTO payOrderDTO = PayOrderConvert.INSTANCE.vo2dto(orderConfirmVO);
String appId = payOrderDTO.getAppId();
AppDTO app = appService.getAppById(appId); // 从订单中获取应用id,根据应用id查询商户id并进行设置(是否可以优化)
payOrderDTO.setMerchantId(app.getMerchantId()); //订单中需要商户id,这里也需要设置一下
payOrderDTO.setTotalAmount(Integer.parseInt(AmountUtil.changeY2F(orderConfirmVO.getTotalAmount().toString())));
payOrderDTO.setClientIp(IPUtil.getIpAddr(request)); //设置订单表需要的客户端IP
//step2:保存订单信息到聚合支付平台,然后调用第三方给支付渠道接口下单(平台订单的保存在支付之前),返回第三方支付平台的响应信息
PaymentResponseDTO<String> paymentResponseDTO = transactionService.submitOrderByAli(payOrderDTO); //保存订单,调用支付渠道代理服务的支付宝下单
//step3:将支付宝的下单接口的http信息返回给客户端(用户点击确认支付看到的页面),这里的response写入展示是给客户的信息
String content = paymentResponseDTO.getContent();
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(content); //直接将完整的表单html输出到页面
response.getWriter().flush();
response.getWriter().close();
}
}
注意:
- 当请求支付宝下单接口后,后续用户输入密码,支付成功的交互步骤,是由支付宝完成,聚合支付平台只需要将正确请求下单接口返回的html网页返回即可。
- 支付的金额需要设置
17-3 理解支付接口测试(待做)
用户:输入金额,点击立即支付按钮(点击下单接口)
需要的信息:
1)支付渠道参数(以支付宝为例,渠道参数包括商户在支付宝平台签约支付产品获得的appId,应用的私匙,支付提供的公匙,签名算法类型,支付宝网关地址)
2)用户扫码生成的订单信息参数,存储数据库的订单(订单编号用雪花算法生成)(聚合平台商户的信息以及应用,订单的描述信息等)
接口成功测试结果界面如上所示
18 支付宝获取支付结果18-1 获取订单的支付结果的方法
职责划分:
-
支付渠道代理服务:专门用于第三方支付平台进行交互,调用支付接口并获取支付接口的结果
-
交易服务:维护订单的状态的状态,绑定服务类型,设置支付渠道参数,交易服务为了完成支付并更新订单状态,则必须去调用支付渠道代理服务提供的接口。
问题1:支付渠道代理获取到支付结果,如何传递给交易服务(异步通信)?
顾客支付完成后,平台需要及时得到支付结果并更新数据库中的订单状态。根据微服务职责划分,支付渠道代理服
务负责与支付宝、微信接口对接,交易服务负责维护订单的数据,支付渠道代理服务如何把查询到的订单结果通知、给交易服务呢?项目中会采用消息队列来完成
问题2:支付渠道服务如何从第三方支付平台获取支付接口?
一般第三方支付平台会提供以下两种方式
1)第三方支付系统主动通知闪聚支付平台支付结果。
2)闪聚支付平台主动从第三方支付系统查询支付结果。
项目中采取的策略:
1)第一种是由第三方支付系统主动通知闪聚支付平台,通过notify_url,当调用闪聚平台接口无法通信达到一定的次数后第三方支付系统将不再通知
2)第二种主动查询支付结果是必须要使用的(必须实现)。
由第三方支付通知并不是非常可靠,如何由于网络原因获取服务宕机,第三方支付平台在通知失败尝试到一定次数就不会再通知了,因此系统必须要能够主动查询结果,这样即使服务宕机一段时间,也能够通过主动查询获取结果。
问题3:下单成功延迟向第三方支付系统查询支付结果的策略?
- 支付完成结果的查询策略
在调用第三方支付下单接口之后此时用户正在支付中,所以需要延迟一定的时间再去查询支付结果。如果查询支付结果还没有支付再继续等待一定的时间再去查询,当达到订单的有效期还没有支付则不再查询。
18-2 使用RocketMQ查询第三方支付结果的思路
流程分析:
0)支付渠道代理服务调用第三方支付下单接口。(此时顾客开始输入密码进行支付)
1) 支付渠道代理向消息队列发送一条延迟消息(查询支付结果),消费方仍是支付渠道代理服务(消息对立的两端都是支付渠道代理微服务)
2) 支付渠道代理接收消息,调用支付宝接口查询支付结果
3) 支付渠道代理查询到支付结果,将支付结果发送至MQ,消费方是交易服务。
4) 交易服务接收到支付结果消息,更新订单状态
问题0:消息队列如何在项目中使用?
producer 队列 consumer 消息
支付渠道代理服务 延时队列 支付渠道代理服务 主动查询订单的请求
支付渠道代理服务 支付服务 查询到的订单结果
问题1:支付渠道代理服务查询第三方支付结果使用消息队列是否合理?
直接目的:使用消息队列的延迟队列功能,这样有没有必要,可不可以不用消息队列延时功能实现支付结果查询结果
根本动机:主要使用了消息队列的异步特性
问题2:使用延时队列如何解决消息堆积,消息重复,消息丢失问题?
18-3 查询支付宝订单状态接口实现
/**
* 查询支付宝订单状态
* @param aliConfigParam 支付渠道参数
* @param outTradeNo 闪聚平台的订单号
* @return
*/
public PaymentResponseDTO queryPayOrderByAli(AliConfigParam aliConfigParam,String outTradeNo) throws BusinessException;
该接口提供所有支付宝支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。 需要调用查询接口的情况: 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知; 调用支付接口后,返回系统错误或未知交易状态情况; 调用alipay.trade.pay,返回INPROCESS的状态; 调用alipay.trade.cancel之前,需确认支付状态;
公共请求参数
参数 | 类型 | 是否必填 | 最大长度 | 描述 | 示例值 |
---|---|---|---|---|---|
app_id | String | 是 | 32 | 支付宝分配给开发者的应用ID | 2014072300007148 |
method | String | 是 | 128 | 接口名称 | alipay.trade.query |
format | String | 否 | 40 | 仅支持JSON | JSON |
charset | String | 是 | 10 | 请求使用的编码格式,如utf-8,gbk,gb2312等 | utf-8 |
sign_type | String | 是 | 10 | 商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2 | RSA2 |
sign | String | 是 | 344 | 商户请求参数的签名串,详见签名 | 详见示例 |
timestamp | String | 是 | 19 | 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss" | 2014-07-24 03:07:50 |
version | String | 是 | 3 | 调用的接口版本,固定为:1.0 | 1.0 |
app_auth_token | String | 否 | 40 | ||
biz_content | String | 是 | 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档 |
请求参数
参数 | 类型 | 是否必填 | 最大长度 | 描述 | 示例值 |
---|---|---|---|---|---|
out_trade_no | String | 特殊可选 | 64 | 订单支付时传入的商户订单号,和支付宝交易号不能同时为空。 trade_no,out_trade_no如果同时存在优先取trade_no | 20150320010101001 |
trade_no | String | 特殊可选 | 64 | 支付宝交易号,和商户订单号不能同时为空 | 2014112611001004680 073956707 |
org_pid | String | 可选 | 16 | 银行间联模式下有用,其它场景请不要使用; 双联通过该参数指定需要查询的交易所属收单机构的pid; | 2088101117952222 |
query_options | String[] | 可选 | 1024 | 查询选项,商户传入该参数可定制本接口同步响应额外返回的信息字段,数组格式。支持枚举如下:trade_settle_info:返回的交易结算信息,包含分账、补差等信息。 fund_bill_list:交易支付使用的资金渠道。 | trade_settle_info |
公共响应参数
参数 | 类型 | 是否必填 | 最大长度 | 描述 | 示例值 |
---|---|---|---|---|---|
code | String | 是 | - | 网关返回码,详见文档 | 40004 |
msg | String | 是 | - | 网关返回码描述,详见文档 | Business Failed |
sub_code | String | 否 | - | 业务返回码,参见具体的API接口文档 | ACQ.TRADE_HAS_SUCCESS |
sub_msg | String | 否 | - | 业务返回码描述,参见具体的API接口文档 | 交易已被支付 |
sign | String | 是 | - | DZXh8eeTuAHoYE3w1J+POiPhfDxOYBfUNn1lkeT/V7P4zJdyojWEa6IZs6Hz0yDW5Cp/viufUb5I0/V5WENS3OYR8zRedqo6D+fUTdLHdc+EFyCkiQhBxIzgngPdPdfp1PIS7BdhhzrsZHbRqb7o4k3Dxc+AAnFauu4V6Zdwczo= |
响应参数
参数 | 类型 | 是否必填 | 最大长度 | 描述 | 示例值 |
---|---|---|---|---|---|
trade_no | String | 必填 | 64 | 支付宝交易号 | 2013112011001004330000121536 |
out_trade_no | String | 必填 | 64 | 商家订单号 | 6823789339978248 |
buyer_logon_id | String | 必填 | 100 | 买家支付宝账号 | 159****5620 |
trade_status | String | 必填 | 32 | 交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)、TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)、TRADE_SUCCESS(交易支付成功)、TRADE_FINISHED(交易结束,不可退款) | TRADE_CLOSED |
total_amount | Price | 必填 | 11 | 交易的订单金额,单位为元,两位小数。该参数的值为支付时传入的total_amount | 88.88 |
trans_currency | String | 选填 | 8 | 标价币种,该参数的值为支付时传入的trans_currency,支持英镑:GBP、港币:HKD、美元:USD、新加坡元:SGD、日元:JPY、加拿大元:CAD、澳元:AUD、欧元:EUR、新西兰元:NZD、韩元:KRW、泰铢:THB、瑞士法郎:CHF、瑞典克朗:SEK、丹麦克朗:DKK、挪威克朗:NOK、马来西亚林吉特:MYR、印尼卢比:IDR、菲律宾比索:PHP、毛里求斯卢比:MUR、以色列新谢克尔:ILS、斯里兰卡卢比:LKR、俄罗斯卢布:RUB、阿联酋迪拉姆:AED、捷克克朗:CZK、南非兰特:ZAR、人民币:CNY、新台币:TWD。当trans_currency 和 settle_currency 不一致时,trans_currency支持人民币:CNY、新台币:TWD | TWD |
settle_currency | String | 选填 | 8 | 订单结算币种,对应支付接口传入的settle_currency,支持英镑:GBP、港币:HKD、美元:USD、新加坡元:SGD、日元:JPY、加拿大元:CAD、澳元:AUD、欧元:EUR、新西兰元:NZD、韩元:KRW、泰铢:THB、瑞士法郎:CHF、瑞典克朗:SEK、丹麦克朗:DKK、挪威克朗:NOK、马来西亚林吉特:MYR、印尼卢比:IDR、菲律宾比索:PHP、毛里求斯卢比:MUR、以色列新谢克尔:ILS、斯里兰卡卢比:LKR、俄罗斯卢布:RUB、阿联酋迪拉姆:AED、捷克克朗:CZK、南非兰特:ZAR | USD |
settle_amount | Price | 选填 | 11 | 结算币种订单金额 | 2.96 |
pay_currency | Price | 选填 | 8 | 订单支付币种 | CNY |
pay_amount | String | 选填 | 11 | 支付币种订单金额 | 8.88 |
settle_trans_rate | String | 选填 | 11 | 结算币种兑换标价币种汇率 | 30.025 |
trans_pay_rate | String | 选填 | 11 | 标价币种兑换支付币种汇率 | 0.264 |
buyer_pay_amount | Price | 选填 | 11 | 买家实付金额,单位为元,两位小数。该金额代表该笔交易买家实际支付的金额,不包含商户折扣等金额 | 8.88 |
point_amount | Price | 选填 | 11 | 积分支付的金额,单位为元,两位小数。该金额代表该笔交易中用户使用积分支付的金额,比如集分宝或者支付宝实时优惠等 | 10 |
invoice_amount | Price | 选填 | 11 | 交易中用户支付的可开具发票的金额,单位为元,两位小数。该金额代表该笔交易中可以给用户开具发票的金额 | 12.11 |
send_pay_date | Date | 选填 | 32 | 本次交易打款给卖家的时间 | 2014-11-27 15:45:57 |
receipt_amount | String | 选填 | 11 | 实收金额,单位为元,两位小数。该金额为本笔交易,商户账户能够实际收到的金额 | 15.25 |
store_id | String | 选填 | 32 | 商户门店编号 | NJ_S_001 |
terminal_id | String | 选填 | 32 | 商户机具终端编号 | NJ_T_001 |
fund_bill_list | TradeFundBill | 必填 | 交易支付使用的资金渠道。 只有在签约中指定需要返回资金明细,或者入参的query_options中指定时才返回该字段信息。 | ||
store_name | String | 选填 | 512 | 请求交易支付中的商户店铺的名称 | 证大五道口店 |
buyer_user_id | String | 必填 | 16 | 买家在支付宝的用户id | 2088101117955611 |
charge_amount | String | 选填 | 11 | 该笔交易针对收款方的收费金额; 默认不返回该信息,需与支付宝约定后配置返回; | 8.88 |
charge_flags | String | 选填 | 64 | 费率活动标识,当交易享受活动优惠费率时,返回该活动的标识; 默认不返回该信息,需与支付宝约定后配置返回; 可能的返回值列表: 蓝海活动标识:bluesea_1 | bluesea_1 |
settlement_id | String | 选填 | 64 | 支付清算编号,用于清算对账使用; 只在银行间联交易场景下返回该信息; | 2018101610032004620239146945 |
trade_settle_info | TradeSettleInfo | 选填 | 返回的交易结算信息,包含分账、补差等信息。 只有在query_options中指定时才返回该字段信息。 | ||
auth_trade_pay_mode | String | 选填 | 64 | 预授权支付模式,该参数仅在信用预授权支付场景下返回。信用预授权支付:CREDIT_PREAUTH_PAY | CREDIT_PREAUTH_PAY |
buyer_user_type | String | 选填 | 18 | 买家用户类型。CORPORATE:企业用户;PRIVATE:个人用户。 | PRIVATE |
mdiscount_amount | String | 选填 | 11 | 商家优惠金额 | 88.88 |
discount_amount | String | 选填 | 11 | 平台优惠金额 | 88.88 |
subject | String | 选填 | 256 | 订单标题; 只在间连场景下返回; | Iphone6 16G |
body | String | 选填 | 1000 | 订单描述; 只在间连场景下返回; | Iphone6 16G |
alipay_sub_merchant_id | String | 选填 | 32 | 间连商户在支付宝端的商户编号; 只在间连场景下返回; | 2088301372182171 |
ext_infos | String | 选填 | 1024 | 交易额外信息,特殊场景下与支付宝约定返回。 json格式。 | |
hb_fq_pay_info | HbFqPayInfo | 选填 | 若用户使用花呗分期支付,且商家开通返回此通知参数,则会返回花呗分期信息。json格式其它说明详见花呗分期信息说明。 注意:商家需与支付宝约定后才返回本参数。 | ||
credit_pay_mode | String | 必填 | 64 | 信用支付模式。表示订单是采用信用支付方式(支付时买家没有出资,需要后续履约)。"creditAdvanceV2"表示芝麻先用后付模式,用户后续需要履约扣款。 此字段只有信用支付场景才有值,商户需要根据字段值单独处理。此字段以后可能扩展其他值,建议商户使用白名单方式识别,对于未识别的值做失败处理,并联系支付宝技术支持人员。 | creditAdvanceV2 |
credit_biz_order_id | String | 必填 | 64 | 信用业务单号。信用支付场景才有值,先用后付产品里是芝麻订单号。 | ZMCB99202103310000450000041833 |
业务错误码
错误码 | 错误描述 | 解决方案 |
---|---|---|
ACQ.SYSTEM_ERROR | 系统错误 | 重新发起请求 |
ACQ.INVALID_PARAMETER | 参数无效 | 检查请求参数,修改后重新发起请求 |
ACQ.TRADE_NOT_EXIST | 查询的交易不存在 | 检查传入的交易号是否正确,修改后重新发起请求 |
18-4 交易渠道代理微服务中订单查询接口的实现
/**
* 查询支付宝订单状态
*
* @param aliConfigParam 支付渠道参数
* @param outTradeNo 闪聚平台的订单号
* @return
*/
@Override
public PaymentResponseDTO queryPayOrderByAli(AliConfigParam aliConfigParam, String outTradeNo) throws BusinessException{
//step1:设置支付宝订单查询需要的参数(这里私匙并不会在网络传播,支付宝提供的SDK支持设置好加密算法类型以及应用私匙来生成
//请求参数的签名,因此这里需要设置一下)
String url = aliConfigParam.getUrl();//支付宝接口网关地址
String appId = aliConfigParam.getAppId();//支付宝应用id
String rsaPrivateKey = aliConfigParam.getRsaPrivateKey();//应用私钥
String format = aliConfigParam.getFormat();//json格式
String charest = aliConfigParam.getCharest();//编码
String alipayPublicKey = aliConfigParam.getAlipayPublicKey();//支付宝公钥
String signtype = aliConfigParam.getSigntype();//签名算法
String returnUrl = aliConfigParam.getReturnUrl();//支付成功跳转的url
String notifyUrl = aliConfigParam.getNotifyUrl();//支付结果异步通知的url
//step2:构造sdk的客户端对象
AlipayClient alipayClient = new DefaultAlipayClient(url, appId, rsaPrivateKey, format, charest, alipayPublicKey, signtype); //获得初始化的AlipayClient
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(outTradeNo); //商户的订单,就是闪聚平台的订单(请求中设置订单号)
request.setBizModel(model);
AlipayTradeQueryResponse response = null;
try {
//请求支付宝订单状态查询接口
response = alipayClient.execute(request);
//支付宝响应的code,10000表示接口调用成功
String code = response.getCode();
if(AliCodeConstants.SUCCESSCODE.equals(code)){
/*WAIT_BUYER_PAY(交易创建,等待买家付款)、
TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)、
TRADE_SUCCESS(交易支付成功)、
TRADE_FINISHED(交易结束,不可退款)*/
String tradeStatusString = response.getTradeStatus();
//解析支付宝返回的状态,解析成闪聚平台的TradeStatus(支付宝平台状态 => 聚合支付平台状态)
TradeStatus tradeStatus = covertAliTradeStatusToShanjuCode(tradeStatusString);
//String tradeNo(支付宝订单号), String outTradeNo(闪聚平台的订单号), TradeStatus tradeState(订单状态), String msg(返回信息)
return PaymentResponseDTO.success(response.getTradeNo(),response.getOutTradeNo(),tradeStatus,response.getMsg());
// 系统中订单的查询结果信息包括: 1) 支付宝订单号 2) 闪聚平台订单号 3) 支付宝返回的订单状态 4)
}
} catch (AlipayApiException e) {
e.printStackTrace();
}
//String msg, String outTradeNo, TradeStatus tradeState
return PaymentResponseDTO.fail("支付宝订单状态查询失败",outTradeNo,TradeStatus.UNKNOWN);
}
问题:接口调用的订单状态以及调用异常该如何处理,如何与聚合支付平台进行转换?
基本思想:枚举类+状态转换函数实现两套系统异常状态的转换。
/**
* @author Administrator
* @version 1.0
**/
/**
* 支付宝查询返回状态码常量类
*/
public class AliCodeConstants {
public static final String SUCCESSCODE = "10000"; // 支付成功或接口调用成功
public static final String PAYINGCODE = "10003"; // 用户支付中
public static final String FAILEDCODE = "40004"; // 失败
public static final String ERRORCODE = "20000"; // 系统异常
/**
* 支付宝交易状态
* WAIT_BUYER_PAY(交易创建,等待买家付款)
* TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)
* TRADE_SUCCESS(交易支付成功)
* TRADE_FINISHED(交易结束,不可退款)
*/
public static final String WAIT_BUYER_PAY = "WAIT_BUYER_PAY";
public static final String TRADE_CLOSED = "TRADE_CLOSED";
public static final String TRADE_SUCCESS = "TRADE_SUCCESS";
public static final String TRADE_FINISHED = "TRADE_FINISHED";
}
18-5 支付宝下单接口查询结果单元测试(待完成)
- 注意:测试时要先用模拟器下单生成订单号,用于测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestPaymentAgent {
@Autowired
PayChannelAgentServiceImpl payChannelAgentService;
/*测试查询支付宝订单状态的接口*/
@Test
public void testQueryPayOrderByAli(){
String APP_ID = "";
String APP_PRIVATE_KEY = "";
String ALIPAY_PUBLIC_KEY = "";
String CHARSET = "UTF‐8";
String serverUrl = "https://openapi.alipaydev.com/gateway.do";//正
AliConfigParam aliConfigParam = new AliConfigParam();
aliConfigParam.setUrl(serverUrl);
aliConfigParam.setCharest(CHARSET);
aliConfigParam.setAlipayPublicKey(ALIPAY_PUBLIC_KEY);
aliConfigParam.setRsaPrivateKey(APP_PRIVATE_KEY);
aliConfigParam.setAppId(APP_ID);
aliConfigParam.setFormat("json");
aliConfigParam.setSigntype("RSA2");
PaymentResponseDTO paymentResponseDTO =
payChannelAgentService.queryPayOrderByAli(aliConfigParam, "SJ1216325162370383873");
System.out.println(paymentResponseDTO);
}
}
18-6 实现延迟队列进行支付接口调用
消息对象的定义
public class PaymentResponseDTO<T> implements Serializable {
private String code = "0"; //系统状态 "0"代表请求成功
private String msg;
private String tradeNo;//原始渠道订单号() 聚合平台角度:原始交易号 商户应用角度:聚合平台的订单号
private String outTradeNo;//商户订单号 聚合平台角度:自己订单号 商户应用角度:自己订单号
private TradeStatus tradeState;//支付状态
private T content; // 用于存放支付渠道参数
}
/*==================支付宝的支付渠道参数=========================*/
public class AliConfigParam implements Serializable {
//应用id
public String appId;
//私钥
public String rsaPrivateKey;
//异步回调通知
public String notifyUrl;
//同步回调通知
public String returnUrl;
//支付宝网关
public String url;
//编码方式 UTF-8
public String charest;
//格式JSON
public String format;
//ali公钥
public String alipayPublicKey;
//日志保存路径, 目前不用
public String logPath;
//RSA2 户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2
public String signtype;
}
生产者
import com.alibaba.fastjson.JSON;
import com.shanjupay.paymentagent.api.dto.PaymentResponseDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
/**
* @author Administrator
* @version 1.0
**/
@Component
@Slf4j
public class PayProducer {
//订单结果查询主题(producer与consumer都是支付渠道代理服务)
private static final String TOPIC_ORDER = "TP_PAYMENT_ORDER";
@Autowired
RocketMQTemplate rocketMQTemplate;
//发送消息(查询支付宝订单状态)
public void payOrderNotice(PaymentResponseDTO paymentResponseDTO){
//发送延迟消息
Message<PaymentResponseDTO> message = MessageBuilder.withPayload(paymentResponseDTO).build();
//延迟第3级发送(延迟10秒)
rocketMQTemplate.syncSend(TOPIC_ORDER,message,1000,3);
log.info("支付渠道代理服务向mq发送订单查询的消息:{}", JSON.toJSONString(paymentResponseDTO));
}
//订单结果 主题(producer是支付渠道代理服务,consumer是支付服务)
private static final String TOPIC_RESULT = "TP_PAYMENT_RESULT";
public void payResultNotice(PaymentResponseDTO paymentResponseDTO){
rocketMQTemplate.convertAndSend(TOPIC_RESULT,paymentResponseDTO);
log.info("支付渠道代理服务向mq支付结果消息:{}", JSON.toJSONString(paymentResponseDTO));
}
}
消费者
import com.alibaba.fastjson.JSON;
import com.shanjupay.paymentagent.api.PayChannelAgentService;
import com.shanjupay.paymentagent.api.conf.AliConfigParam;
import com.shanjupay.paymentagent.api.dto.PaymentResponseDTO;
import com.shanjupay.paymentagent.api.dto.TradeStatus;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Administrator
* @version 1.0
**/
@Component
@RocketMQMessageListener(topic = "TP_PAYMENT_ORDER",consumerGroup="CID_PAYMENT_CONSUMER")
@Slf4j
public class PayConsumer implements RocketMQListener<MessageExt> {
@Autowired
PayChannelAgentService payChannelAgentService;
@Autowired
PayProducer payProducer;
@Override
public void onMessage(MessageExt messageExt) {
//step1:将消息的字节数据=> json串 => 订单号+支付渠道参数
byte[] body = messageExt.getBody();
String jsonString = new String(body);
log.info("支付渠道代理服务接收到查询订单的消息:{}", JSON.toJSONString(jsonString));
PaymentResponseDTO paymentResponseDTO = JSON.parseObject(jsonString, PaymentResponseDTO.class);
String outTradeNo = paymentResponseDTO.getOutTradeNo(); // 获取订单号
String params = String.valueOf(paymentResponseDTO.getContent()); // 获取支付渠道参数
AliConfigParam aliConfigParam = JSON.parseObject(params, AliConfigParam.class);
// step2:根据支付渠道类型,查询对应的支付结果
PaymentResponseDTO responseDTO= null;
if("ALIPAY_WAP".equals(paymentResponseDTO.getMsg())){
responseDTO = payChannelAgentService.queryPayOrderByAli(aliConfigParam, outTradeNo);
}else if("WX_JSAPI".equals(paymentResponseDTO.getMsg())){
}
//当没有获取到订单结果,抛出异常,再次重试消费
if(responseDTO == null || TradeStatus.UNKNOWN.equals(responseDTO.getTradeState()) || TradeStatus.USERPAYING.equals(responseDTO.getTradeState()) ){
throw new RuntimeException("支付状态未知,等待重试");
}
//如果重试的次数达到一次数量,不要再重试消费,将消息记录到数据库,由单独的程序或人工进行处理(待实现)
//step3: 成功获取支付结果,采用同步消息的方式将消息发送给支付服务
payProducer.payResultNotice(responseDTO);
}
}
支付宝下单接口中发送延迟消息代码
@Autowired
PayProducer payProducer;
@Override
public PaymentResponseDTO createPayOrderByAliWAP(AliConfigParam aliConfigParam, AlipayBean alipayBean) throws BusinessException {
// step1:获取支付宝交易接口的参数
String url = aliConfigParam.getUrl();//支付宝接口网关地址
String appId = aliConfigParam.getAppId();//支付宝应用id
String rsaPrivateKey = aliConfigParam.getRsaPrivateKey();//应用私钥
String format = aliConfigParam.getFormat();//json格式
String charest = aliConfigParam.getCharest();//编码
String alipayPublicKey = aliConfigParam.getAlipayPublicKey();//支付宝公钥
String signtype = aliConfigParam.getSigntype();//签名算法
String returnUrl = aliConfigParam.getReturnUrl();//支付成功跳转的url
String notifyUrl = aliConfigParam.getNotifyUrl();//支付结果异步通知的url
//step2: 使用接口的参数构造sdk的客户端对象
AlipayClient alipayClient = new DefaultAlipayClient(url, appId, rsaPrivateKey, format, charest, alipayPublicKey, signtype); //获得初始化的AlipayClient
AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
// step3: 设置下单的业务参数
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(alipayBean.getOutTradeNo());//商户的订单,就是闪聚平台的订单
model.setTotalAmount(alipayBean.getTotalAmount());//订单金额(元)
model.setSubject(alipayBean.getSubject());
model.setBody(alipayBean.getBody());
model.setProductCode("QUICK_WAP_PAY");//产品代码,固定QUICK_WAP_PAY
model.setTimeoutExpress(alipayBean.getExpireTime());//订单过期时间
alipayRequest.setBizModel(model);
// 设置异步通知url以及支付成功重定向的url
alipayRequest.setReturnUrl(returnUrl);
alipayRequest.setNotifyUrl(notifyUrl);
// step4:发起支付交易,获取支付宝响应内容
try {
//请求支付宝下单接口,发起http请求
AlipayTradeWapPayResponse response = alipayClient.pageExecute(alipayRequest);
PaymentResponseDTO paymentResponseDTO = new PaymentResponseDTO();
log.info("调用支付宝下单接口,响应内容:{}",response.getBody());
paymentResponseDTO.setContent(response.getBody());//支付宝的响应结果
// step5: 向MQ发一条用于查询支付结果的延迟消息,到达延时时间后,去查询支付结果
PaymentResponseDTO<AliConfigParam> notice = new PaymentResponseDTO<AliConfigParam>();
notice.setOutTradeNo(alipayBean.getOutTradeNo());//闪聚平台的订单
notice.setContent(aliConfigParam);
notice.setMsg("ALIPAY_WAP");//标识是查询支付宝的接口
payProducer.payOrderNotice(notice);
// step6:返回调用接口的请求()
return paymentResponseDTO;
} catch (AlipayApiException e) {
e.printStackTrace();
throw new BusinessException(CommonErrorCode.E_400002);
}
}
18-7 交易服务从消息队列中获取结果更新订单状态
注意点:区分聚合支付平台本身的订单号和第三方支付平台的订单号,更新时应该使用支付平台本身的订单号。
@Component
@RocketMQMessageListener(topic = "TP_PAYMENT_RESULT",consumerGroup="CID_ORDER_CONSUMER")
@Slf4j
public class TransactionPayConsumer implements RocketMQListener<MessageExt> {
@Autowired
TransactionService transactionService;
@Override
public void onMessage(MessageExt messageExt) {
byte[] body = messageExt.getBody();
String jsonString = new String(body);
log.info("交易服务向接收到支付结果消息:{}", JSON.toJSONString(jsonString));
//接收到消息,内容包括订单状态
PaymentResponseDTO paymentResponseDTO = JSON.parseObject(jsonString, PaymentResponseDTO.class);
String tradeNo = paymentResponseDTO.getTradeNo(); //支付宝微信的订单号订单号
String outTradeNo = paymentResponseDTO.getOutTradeNo(); //闪聚平台的订单号
//订单状态
TradeStatus tradeState = paymentResponseDTO.getTradeState();
//更新数据库
switch (tradeState){
case SUCCESS:
//String tradeNo, String payChannelTradeNo, String state
//成功
transactionService.updateOrderTradeNoAndTradeState(outTradeNo,tradeNo,"2");
return ;
case REVOKED:
//关闭
transactionService.updateOrderTradeNoAndTradeState(outTradeNo,tradeNo,"4");
return ;
case FAILED:
//失败
transactionService.updateOrderTradeNoAndTradeState(outTradeNo,tradeNo,"5");
return ;
default:
throw new RuntimeException(String.format("无法解析支付结果:%s",body));
}
}
}
用于更新订单状态的service
/*
* 传入支付渠道代理微服务传递过来的订单结果,根据查询结果更新数据库中的订单状态(在消息队列的消费端被调用)
* */
@Override
public void updateOrderTradeNoAndTradeState(String tradeNo, String payChannelTradeNo, String state) throws BusinessException {
LambdaUpdateWrapper<PayOrder> payOrderLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
payOrderLambdaUpdateWrapper.eq(PayOrder::getTradeNo,tradeNo)
.set(PayOrder::getTradeState,state)
.set(PayOrder::getPayChannelTradeNo,payChannelTradeNo);
if(state!=null && state.equals("2")){
payOrderLambdaUpdateWrapper.set(PayOrder::getPaySuccessTime,LocalDateTime.now());
}
payOrderMapper.update(null,payOrderLambdaUpdateWrapper);
}
18-8 支付宝支付结果功能测试(通过日志文件与数据库查看实现)(代做)
18-9 通过支付宝下单的流程总结
1)商户通过平台生成二维码,二维码本质是url = 网关地址+统一支付入口路径+ticket(base64编码包含有商户的信息)
2)用户扫描二维码后,交易微服务中入口路径的方法中会根据user agent参数分辨当前的支付渠道。
3) 假设是支付宝扫码,则生成订单,利用商户设置的支付渠道参数,交易微服务会委托支付渠道代理微服务调用支付宝下单接口。
4) 支付渠道代理微服务下单成功则发送延时订单结果查询的消息给自己,下单结果最终反馈给客户端,用户输入密码完成支付。
5) 支付渠道代理服务接收到延时消息后主动查询支付结果,查询成功,则将查询结果通过消息队列发送同步消息给支付服务
6)支付服务获取消息队列中的同步消息并更新订单状态。
19 微信获取支付结果
19-1 获取微信授权码(比支付宝的支付流程麻烦)
transaction-service.yaml中设置参数
weixin:
oauth2RequestUrl: "https://open.weixin.qq.com/connect/oauth2/authorize" //申请授权码的地址
oauth2CodeReturnUrl: "http://xfc.nat300.top/transaction/wx‐oauth‐code‐return" // 授权码申请成功,微信回调地址
oauth2Token: "https://api.weixin.qq.com/sns/oauth2/access_token" // 获取授权码,申请openid地址
19-1 获取授权码的service
/**
* 获取微信的授权码,controller中支付入口接口会调用该方法
* @param payOrderDTO
* @return
*/
//https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
@Override
public String getWXOAuth2Code(PayOrderDTO payOrderDTO) {
//step1: 闪聚平台的应用id
String appId = payOrderDTO.getAppId();
// 根据 appId,String platformChannel,String payChannel 获取支付渠道参数
PayChannelParamDTO payChannelParamDTO = payChannelService.queryParamByAppPlatformAndPayChannel(appId, "shanju_c2b", "WX_JSAPI");
String param = payChannelParamDTO.getParam();
WXConfigParam wxConfigParam = JSON.parseObject(param, WXConfigParam.class); //微信支付渠道参数
String jsonString = JSON.toJSONString(payOrderDTO);
String state = EncryptUtil.encodeUTF8StringBase64(jsonString); //state是一个原样返回的参数
try {
String url = String.format("%s?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=%s#wechat_redirect",
oauth2RequestUrl,wxConfigParam.getAppId(), oauth2CodeReturnUrl,state
); // 设置回调地址,state是原样返回的信息(base 64编码)
return "redirect:"+url;
} catch (Exception e) {
e.printStackTrace();
}
return "forward:/pay-page-error";
}
19-2 授权码回调地址
@ApiOperation("微信授权码回调")
@GetMapping("/wx‐oauth‐code‐return")
public String wxOAuth2CodeReturn(@RequestParam String code, @RequestParam String state) {
/*step1:微信回调该路径带来授权码*/
String jsonString = EncryptUtil.decodeUTF8StringBase64(state);
PayOrderDTO payOrderDTO = JSON.parseObject(jsonString, PayOrderDTO.class);
String appId = payOrderDTO.getAppId(); //闪聚平台的应用id
String openId = transactionService.getWXOAuthOpenId(code, appId); //接收到code授权码,申请openid
/*step2:获取到openid后,重定向到支付确认页面,在这个页面用户需要阅读顶点,点击确认支付*/
String params = null;
try {
params = ParseURLPairUtil.parseURLPair(payOrderDTO); //将对象的属性和值组成一个url的key/value串
String url = String.format("forward:/pay-page?openId=%s&%s", openId, params);
return url;
} catch (Exception e) {
e.printStackTrace();
return "forward:/pay-page-error";
}
}
申请openid的service
/**
* 申请openid
*
* @param code 授权码
* @param appId 闪聚平台的应用id,为了获取该应用的微信支付渠道参数
* @return
*/
@Override
public String getWXOAuthOpenId(String code, String appId) {
//获取微信支付渠道参数
//String appId,String platformChannel,String payChannel
PayChannelParamDTO payChannelParamDTO = payChannelService.queryParamByAppPlatformAndPayChannel(appId, "shanju_c2b", "WX_JSAPI");
String param = payChannelParamDTO.getParam();
//微信支付渠道参数
WXConfigParam wxConfigParam = JSON.parseObject(param, WXConfigParam.class);
//https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
String url = String.format("%s?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
oauth2Token,wxConfigParam.getAppId(), wxConfigParam.getAppSecret(), code);
//申请openid,请求url
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
//申请openid接口响应的内容,其中包括了openid
String body = exchange.getBody();
log.info("申请openid响应的内容:{}",body);
//获取openid
String openid = JSON.parseObject(body).getString("openid");
return openid;
}
总结:微信确认支付页面的流程?
1)用户扫码请求支付
2) 统一支付接口发现是微信支付,首先向微信平台申请授权码
3) 微信会回调平台的url传入授权码,平台拿到授权码后则申请openid
4) 聚合支付平台拿到openid才返回订单确认支付页面
5) 用户确认订单,点击立即支付
6) 聚合支付平台生成订单,并调用微信下单接口
19-4 微信立即支付功能实现
用微信支付的下单接口,具体的流程如下
- 点击立即支付调用第三方支付系统的下单接口,微信客户端扫码进入确认页面,点击立即支付则由渠道代理服务调
1、微信客户端扫码进入确认页面,点击立即支付请求交易服务微信下单接口
2、交易服务通过支付渠道代理服务调用微信下单接口
3、调用微信下单接口成功,返回H5网页
4、在H5网页调起微信客户端支付
19-5 微信下单的结果通过消息队列进行结果查询
19-6 微信的结果查询接口测试(代做)
1、生成门店c扫b的二维码
2、打开模拟器,使用微信扫码,进入支付确认页面
3、输入金额,点击立即支付
4、观察控制台日志,最终订单写入闪聚平台数据库
5、调起微信支付客户端,输入密码支付成功
观察控制台日志,支付结果是否发送至交易服务。
数据库订单状态是否正常更新