C扫B,即顾客(Customer)扫描商户(Business)提供的二维码来完成支付。下图是支付宝提供的C扫B业务流程:
C扫B支付分为两种方式:一是固定金额支付,顾客扫描后无需输入金额直接确认支付即可;另外一种是输入金额,顾客扫描后需自己输入待支付的金额,然后完成支付。
C扫B固定金额比较常见的就是在自动售货机购买饮料时,当你选择一个饮料后屏幕会显示一个二维码,咱们扫描后只需确认支付即可,无需自己输入饮料的价格,这种情况大家可以根据下面的交互流程图来自行实现。
C扫B输入金额支付方式可以让买方自由输入金额,商户提前生成一个二维码,将二维码张贴在结账处,买方扫描二维码,输入当前消费的金额,完成支付。
本章节实现C扫B输入金额支付,业务流程如下:
1、商户点击组织管理-》门店管理-》打开门店列表页面
2、选择应用
3、点击指定门店的生成二维码按钮
4、顾客扫描生成的二维码,进入支付页面,输入金额来完成支付
根据业务流程的分析,闪聚支付平台实现C扫B输入金额支付功能,需要完成以下需求:
根据前边的需求分析,最重要的是闪聚支付平台作为中介,将用户的支付请求通过接口与微信、支付宝等第三方支付渠道进行对接,完成支付通道的聚合,所以首先需要调研微信、支付宝等第三方支付渠道的对接方式。
本项目首期上线要求集成微信和支付宝,下边对微信和支付宝的支付接口进行技术预研,包括:对接的流程,接口协议、接口测试等。
参考:闪聚支付-第3章-支付宝支付接入指南 、 闪聚支付-第3章-微信支付接入指南 。
1、商户点击组织管理-》门店管理-》打开门店列表页面
2、选择应用
3、点击指定门店的生成二维码按钮
ZXing是一个开源的,用Java编写的多格式的1D / 2D条码图像处理库,使用ZXing可以生成、识别QR Code(二维码)。常用的二维码处理库还有zbar,近几年已经不再更新代码,下边介绍ZXing生成二维码的方法。
(1)引入依赖
<dependency>
<groupId>com.google.zxinggroupId>
<artifactId>coreartifactId>
<version>3.3.3version>
dependency>
<dependency>
<groupId>com.google.zxinggroupId>
<artifactId>javaseartifactId>
<version>3.3.3version>
dependency>
(2)生成二维码方法
复制二维码工具类QRCodeUtil.java到项目中
测试根据内容生成二维码方法,在QRCodeUtil中添加main方法如下:
public static void main(String[] args) throws IOException {
QRCodeUtil qrCodeUtil = new QRCodeUtil();
System.out.println(qrCodeUtil.createQRCode("http://www.itcast.cn/", 200, 200));
}
运行main方法,将输出的内容复制到浏览器地址后回车
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIAQAAAACFI5MzAAABQElEQVR42u2YPZKDMAyF5aFIuUfIUThafDSOwhEoUzC8fZKMySSbrVI8ZuICBX8uIvtZPxjeDfuSf8liPi7LFSgrzRTvV3XCKawXYLptFobviz6ZzB2xEfTjhyS9OwXB3A7jbMSngLOQ0I4v2AZf96wqTWJ9+9/dYEHSx2RYqfg/oqUgiX3nFBVfcCepcSbiJP67iwZ1G+5+Am7kyTzW9OcW/kRAX+QJ953+uCl8zO5PV5UsaffUp8rqP5+jiySJU8jtNxcNrysetCNK6A/V4lEQeU+xa0eZREE1tOTpFYod0VKXsKCqvRqMkW5pkza8Ggy3WgEuTvZcz0dcUBc+9MneL1DqkXjQz0eaZA1LqVtmzcMffTKPiPwz1mh2zkGyNwtT9kguTVI7LWv6ul7DCpOjX9iaGV66HDny/ZL1WfILfc/hMHLUpekAAAAASUVORK5CYII=
1、接口描述
1)根据商户id和分页信息查询门店列表
2、接口定义如下:MerchantService
/**
* 分页条件查询商户下门店
* @param storeDTO 查询条件,必要参数:商户id
* @param pageNo 页码
* @param pageSize 分页记录数
* @return
*/
PageVO<StoreDTO> queryStoreByPage(StoreDTO storeDTO, Integer pageNo, Integer pageSize);
3、在MerchantServiceImpl中实现queryStoreByPage方法:
/**
* 门店列表的查询
* @param storeDTO 查询条件,必要参数:商户id
* @param pageNo 页码
* @param pageSize 分页记录数
* @return
*/
@Override
public PageVO<StoreDTO> queryStoreByPage(StoreDTO storeDTO, Integer pageNo, Integer pageSize) {
//分页条件
Page<Store> page = new Page<>(pageNo, pageSize);
//查询条件拼装
LambdaQueryWrapper<Store> lambdaQueryWrapper = new LambdaQueryWrapper<Store>();
//如果 传入商户id,此时要拼装 查询条件
if (storeDTO != null && storeDTO.getMerchantId() != null) {
lambdaQueryWrapper.eq(Store::getMerchantId, storeDTO.getMerchantId());
}
//再拼装其它查询条件 ,比如:门店名称
if (storeDTO != null && StringUtils.isNotEmpty(storeDTO.getStoreName())) {
lambdaQueryWrapper.eq(Store::getStoreName, storeDTO.getStoreName());
}
//分页查询数据库
IPage<Store> storeIPage = storeMapper.selectPage(page, lambdaQueryWrapper);
//查询列表
List<Store> records = storeIPage.getRecords();
//将包含entity的list转成包含dto的list
List<StoreDTO> storeDTOS = StoreConvert.INSTANCE.listentity2dto(records);
return new PageVO(storeDTOS, storeIPage.getTotal(), pageNo, pageSize);
}
1、接口描述
1)请求商户服务查询门店列表
2、接口定义如下:StoreController
package com.shanjupay.merchant.controller;
/**
* 门店管理相关接口定义
**/
@Api(value = "商户平台-门店管理", tags = "商户平台-门店管理", description = "商户平台-门店的增删改查")
@RestController
@Slf4j
public class StoreController {
@ApiOperation("分页条件查询商户下门店")
@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) {
}
}
前端JS在Long长度大于17位时会出现精度丢失的问题,由于项目中门店ID的长度会超过17位,所以在此处添加注解将返回给前端的门店ID自动转为string类型
1)使用jackson来完成自动转换,在shanjupay-merchant-api工程中添加依赖:
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.9.9version>
<scope>compilescope>
dependency>
2)在StoreDTO中添加注解:
@ApiModelProperty("门店Id")
@JsonSerialize(using= ToStringSerializer.class)
private Long id;
2、在StoreController中实现queryStoreByPage方法:
package com.shanjupay.merchant.controller;
import com.shanjupay.common.domain.PageVO;
import com.shanjupay.merchant.api.MerchantService;
import com.shanjupay.merchant.api.dto.StoreDTO;
import com.shanjupay.merchant.common.util.SecurityUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 门店管理相关接口定义
**/
@Api(value = "商户平台-门店管理", tags = "商户平台-门店管理", description = "商户平台-门店的增删改查")
@RestController
@Slf4j
public class StoreController {
@Reference
MerchantService merchantService;
@ApiOperation("分页条件查询商户下门店")
@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();
//查询条件
StoreDTO storeDTO = new StoreDTO();
storeDTO.setMerchantId(merchantId);//商户id
//调用service分页查询列表
PageVO<StoreDTO> stores = merchantService.queryStoreByPage(storeDTO, pageNo, pageSize);
return stores;
}
}
由于已经接入SaaS,请求统一走网关的端口56010。
1、首先请求认证获取token,及租户的id
(1) 启动三个SaaS服务
(2)使用账号申请token
2、使用Postman:POST http://localhost:56010/merchant/my/stores/merchants/page?pageNo=1&pageSize=20 查询门店列表
注意在header中添加 Authorization及tenantId。
访问:http://localhost:56020/uaa/oauth/token 拿到token。需要的参数如下图:
生成二维码的系统交互 流程如下:
1、商户登录商户应用平台 ,查询门店列表
2、商户平台 请求交易 服务获取门店二维码URL
3、商户平台 根据 URL生成二维码
接口描述:
生成门店的c扫b二维码
接口参数:
输入:商户id、应用id、门店id、标题,内容
输出:支付入口
1、在交易服务的api工程 创建dto
package com.shanjupay.transaction.api.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
public class QRCodeDto implements Serializable {
private Long merchantId;//商户id
private String appId;//应用id
private Long storeId;//门店id
private String subject;//商品标题
private String body;//订单描述
}
2、在交易服务shanjupay-transaction-api工程中创建接口TransactionService,定义如下方法
package com.shanjupay.transaction.api;
import com.shanjupay.common.domain.BusinessException;
import com.shanjupay.transaction.api.dto.QRCodeDto;
/**
* 交易相关的服务接口
*/
public interface TransactionService {
/**
* 生成门店二维码的url
* @param qrCodeDto 传入merchantId,appId、storeid、channel、subject、body
* @return 支付入口(url),要携带参数(将传入的参数转成json,用base64编码)
* @throws BusinessException
*/
String createStoreQRCode(QRCodeDto qrCodeDto) throws BusinessException;
}
商户生成二维码需要根据门店、应用来生成,设计接口需要对应用和门店的合法性来校验。
1、校验该应用是否属于该商户。
2、校验该门店是否属于该商户
1、接口描述
1)根据商户id和应用id查询应用信息,查询到则说明合法。
2、在shanjupay-merchant-api中的AppService下定义接口如下:
/**
* 查询应用是否属于某个商户
* @param appId
* @param merchantId
* @return
*/
Boolean queryAppInMerchant(String appId, Long merchantId);
3、接口实现如下
在AppServiceImpl中实现queryAppInMerchant接口
/**
* 查询应用是否属于某个商户
* @param appId
* @param merchantId
* @return true表示存在,false不表示存在
*/
@Override
public Boolean queryAppInMerchant(String appId, Long merchantId) {
Integer count = appMapper.selectCount(new LambdaQueryWrapper<App>().eq(App::getAppId, appId).eq(App::getMerchantId, merchantId));
return count > 0;
}
1、接口描述
1)根据商户id和门店id查询门店,查询到则说明合法。
2、在shanjupay-merchant-api工程的MerchantService中定义接口:
/**
* 查询门店是否属于某商户
* @param StoreId
* @param merchantId
* @return
*/
Boolean queryStoreInMerchant(Long StoreId, Long merchantId);
3、接口实现如下
在MerchantServiceImpl中实现queryStoreInMerchant接口
/**
* 查询门店是否属于某商户
*
* @param storeId
* @param merchantId
* @return true存在,false不存在
*/
@Override
public Boolean queryStoreInMerchant(Long storeId, Long merchantId) {
Integer count = storeMapper.selectCount(new LambdaQueryWrapper<Store>().eq(Store::getId, storeId).eq(Store::getMerchantId, merchantId));
return count > 0;
}
在Nacos中添加支付入口配置:transaction-service.yaml
支付入口是扫码支付的统一入口。
#支付入口url
shanjupay:
payurl: "http://127.0.0.1:56010/transaction/pay-entry/"
在shanjupay-transaction-service工程中新建TransactionServiceImpl类实现TransactionService接口中的createStoreQRCode方法:
package com.shanjupay.transaction.service;
import com.alibaba.fastjson.JSON;
import com.shanjupay.common.domain.BusinessException;
import com.shanjupay.common.domain.CommonErrorCode;
import com.shanjupay.common.util.EncryptUtil;
import com.shanjupay.merchant.api.AppService;
import com.shanjupay.merchant.api.MerchantService;
import com.shanjupay.transaction.api.TransactionService;
import com.shanjupay.transaction.api.dto.PayOrderDTO;
import com.shanjupay.transaction.api.dto.QRCodeDto;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.Reference;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Value;
@Service
@Slf4j
public class TransactionServiceImpl implements TransactionService {
//从配置文件读取支付入口地址
@Value("${shanjupay.payurl}")
private String payurl;
@Reference
AppService appService;
@Reference
MerchantService merchantService;
/**
* 生成门店二维码的url
*
* @param qrCodeDto 支付入口(url),要携带参数,商户id、应用id、门店id、标题、内容(将传入的参数转成json,用base64编码)
* @return
* @throws BusinessException
*/
@Override
public String createStoreQRCode(QRCodeDto qrCodeDto) throws BusinessException {
//校验商户id和应用id和门店id的合法性
verifyAppAndStore(qrCodeDto.getMerchantId(), qrCodeDto.getAppId(), qrCodeDto.getStoreId());
//组装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());//订单内容
//转成json
String jsonString = JSON.toJSONString(payOrderDTO);
//base64编码
String ticket = EncryptUtil.encodeUTF8StringBase64(jsonString);
//目标是生成一个支付入口 的url,需要携带参数将传入的参数转成json,用base64编码
String url = payurl + ticket;
log.info("transaction service createStoreQRCode,pay‐entry is {}", url);
return url;
}
//私有方法,校验商户id和应用id和门店id的合法性
private void verifyAppAndStore(Long merchantId, String appId, Long storeId) {
//根据应用id和商户id查询,判断应用是否属于当前商户
Boolean aBoolean = appService.queryAppInMerchant(appId, merchantId);
if (!aBoolean) {
throw new BusinessException(CommonErrorCode.E_200005);
}
//根据门店id和商户id查询,判断门店是否属于当前商户
Boolean aBoolean1 = merchantService.queryStoreInMerchant(storeId, merchantId);
if (!aBoolean1) {
throw new BusinessException(CommonErrorCode.E_200006);
}
}
}
1、配置参数
shanjupay:
c2b:
subject: "%s商品"
body: "向%s付款"
2、定义接口
//"%s商品" 门店二维码订单标题
@Value("${shanjupay.c2b.subject}")
String subject;
//"向%s付款" 门店二维码订单内容
@Value("${shanjupay.c2b.body}")
String body;
@Reference
TransactionService transactionService;
@ApiOperation("生成商户应用门店的二维码")
@ApiImplicitParams({
@ApiImplicitParam(name = "appId", value = "商户应用id", required = true, dataType = "String", paramType = "path"),
@ApiImplicitParam(name = "storeId", value = "商户门店id", required = true, dataType = "String", paramType = "path"),
})
@GetMapping(value = "/my/apps/{appId}/stores/{storeId}/app-store-qrcode")
public String createCScanBStoreQRCode(@PathVariable("storeId") Long storeId, @PathVariable("appId") String appId) throws IOException {
//获取商户id
Long merchantId = SecurityUtil.getMerchantId();
//商户信息
MerchantDTO merchantDTO = merchantService.queryMerchantById(merchantId);
QRCodeDto qrCodeDto = new QRCodeDto();
qrCodeDto.setMerchantId(merchantId);
qrCodeDto.setStoreId(storeId);
qrCodeDto.setAppId(appId);
//标题.用商户名称替换 %s
String subjectFormat = String.format(subject, merchantDTO.getMerchantName());
qrCodeDto.setSubject(subjectFormat);
//内容
String bodyFormat = String.format(body, merchantDTO.getMerchantName());
qrCodeDto.setBody(bodyFormat);
//获取二维码的URL
String storeQRCodeURL = transactionService.createStoreQRCode(qrCodeDto);
//调用工具类生成二维码图片
QRCodeUtil qrCodeUtil = new QRCodeUtil();
//二维码图片base64编码
String qrCode = qrCodeUtil.createQRCode(storeQRCodeURL, 200, 200);
return qrCode;
}
1、请求认证,获取token及租户id
2、请求获取二维码
http://localhost:56010/merchant/my/apps/{appId}/stores/{storeId}/app-store-qrcode
注意:应用及门店的合法性
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIAQAAAACFI5MzAAAGBklEQVR42u2YvZXsqhKFkQMpgAOpgSNSQA5IDqSAHJQaOJACcuBW97nPeqs5CVyNNf31aEH97No1aP560H8ESEbq7rpyY6jy7omkXR4jsiRtWM63MB94ovD6SeRNri1JxVZXeMXMxoVzJ5F5NqT+C9FJtfcZHNEju0Q6p/vfyUwkSkziVi2i3LKW/kLaUFGVSCrl2hurWt/K/+7zi2S0///zb9x+kTlbSyqRlizrxupnljv8m59fpFVy9+M+MaZyt2g/omrn1pZk3lfLCOu7U+bFfUMSKj/WpGRpqTzKO3YdrUrygA+4XpI3lMtD5p7r5JRkxFq46xGWpH/+9o7YhQfwk11U855tSQaLu3jmvAY9Wmtvu69KrViS15Os/QZxfO4Zd4mOTK1uawK16rdhrHXlyTuZGY5+PEsCvwzM4WenULrYZVIyu9qSlJJ1VO8pPFVDP92Ka7DnWZLZqeFbZ54f1VJVkZVkfLPwm7xPQiRkhNQpErUu7Ziyty1J69ggcqqQqb4ieQeB71KxJJ2V6JKF66LNu0Th4NBkekkycidFCm54d8hHMuqUfzr4N2ltQlXovI0jY7OFlvSd//TpT1KN9trvlm4RYb7LHVnddViS9obMVXZ34uIkibWqWiRzSeZw3ULuOntCZu/jjwHaqZfkBbnbPJZu4OPKzlPtqVFhSUrenmdQCCcbJCEN2mQkaUvSSbmL33UbiEENI5KO0FpYE6zSpxul8tvnMF15ue96Sd5WQedODRJDPLVIn5jqTJakZdKiuEE0RQkRqcyNlUgsSXkG3k7iFdQGqNhVuQrJuiXpklzhPvnh1X2603ySfZSwJFW0cJotQw+Te75XUkMk9qzJbjA66nZdCcOMOabfd3HNJenQ7gZhGBMQU8g0eaK7uluSkik53eV3qEWQI0516WKQJel6Vp0oRciFru5sJbVctSUpgwVoc0Oup3jjugpQzCEsyXATwrGzikHFqFVjVwGKaknm6VrfVWR55xiSbRhITNJLAiMJGwPDDLt5nQ5Ez1OuniXpGFkCF4TA7oja471KeU6xJG85t4gxJoMaEHMpOcyYpy1Jt+7bjUiE6EpGUI9md2tSZukWG8mgla9WYDxvd2RhSeYpnoq3G+SueHoMCycvJ1mTEI3Lm/+I/gDlE+HkzLslgXNufTt3foSryw2KkUvK2pJUl44qoX+fakXUJ1WnS24uSecCBiCUeUgSjNp8wW+cZE3eN3IoJ9EGZXUn0LvgUP64gJ9kSOINZxDOu8IA2M0xqPy6p99kthKuvIFAto/TRTu0SChzSZrfP7bMSwwuoEOew4CbkyWpGs56ed1OzBIHtfRGhOyWZJ5IDAxVpUDGOgKlGSRjvSTtHdIldEQpjwTBuQtMg+tZEpjfiXksCkhgYie0fZfsbksy1F0yOVnnSA3XrmqPWapektkRTAgBCoug6DGnmzfH1w3+Ju/9XPN+oXqPt0VWxV3Z+1Xy3wQmOPjMqvsWNbh8KI63b191+U0q63jfWtw82k4RypMMF98u+U3KichVjSizxePjPDPsH12vSYPqBbuOKAa7gZAUV7m+arkg59a5ldug4Bk41y0aCQZ/SQaBkSQ3kBjDhgqgG5e3WCxJ3jE4JtFKu7JVGbQTGyvaksyreOj51kUidVcXvNHj79T8TSrCmEEZQpdsF8yzBgvM8XWqv0k+PpYJhAXssNdZQmENd7olGdaqSMHhUrgphX1SdPCgeknajOCGXbhvD0pWLliQqfrT9T8JCHiioFqXF9BQEQyerqrqJck6Y9gc3jKOt7poReicXWFJWtqRe58QBeQ9f/0Q0qUtSSkTtnDswKLe3t0hVHDR363tN4EVmlUJx8AWbR1Juc3PF5cE5PKE+gNPCFmgFvZdsHqZLElGkj39Y4QkjLP9SFvFu2pLAkpsjRTwNlh1vQW5gL2lPUtSsTQO1lYEn4WEPqv4e2LyFwKhh1XlHvKAAGWuq37f+TdymqPuR7iHfmE+wZg6nrAkbRjuImV35XRnY7uTe8u7JhlBIZICDopCo4gqGRhR9yzJf/8X+03+AdB2dSBjS36OAAAAAElFTkSuQmCC
买方扫描门店二维码,进入支付入口 即进入订单确认页面,流程如下:
1)顾客扫描二维码
支付确认页面由服务端渲染生成,常用的技术有jsp、freemarker velocity Thymeleaf等。
项目采用freemarker模板引擎,参考 freemarker基础。
1、从资料->代码拷贝pay.html、pay_error.html到交易服务工程下。
2、在交易服务接口实现工程的pom.xml中引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-freemarkerartifactId>
dependency>
3、在Nacos中配置spring-boot-freemarker.yaml,Group: COMMON_GROUP
#freemarker基本配置
spring:
freemarker:
charset: UTF-8
request-context-attribute: rc
content-type: text/html
suffix: .html
enabled: true
resources:
add-mappings: false #关闭工程中默认的资源处理
mvc:
throw-exception-if-no-handler-found: true #出现错误时直接抛出异常
在shanjupay-transaction-service工程的bootstrap.yml引入spring-boot-freemarker.yaml
- refresh: true
data-id: spring-boot-freemarker.yaml # spring boot freemarker配置
group: COMMON_GROUP # 通用配置组
4、在shanjupay-transaction-service工程的config包下新建WebMvcConfig配置确认支付页面视图名:
package com.shanjupay.transaction.config;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Component
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/pay‐page").setViewName("pay");
}
}
5、定义支付入口接口
注意:PayController要向前端响应页面,使用@Controller注解。
package com.shanjupay.transaction.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
/**
* 支付相关接口
*/
@Slf4j
@Controller
public class PayController {
/**
* 支付入口
*
* @param ticket 传入数据,对json数据进行的base64编码
* @param request
* @return
*/
@RequestMapping(value = "/pay‐entry/{ticket}")
public String payEntry(@PathVariable("ticket") String ticket, HttpServletRequest request) throws Exception {
return "forward:/pay‐page";
}
}
6、测试页面渲染
transaction-service:
path: /transaction/**
stripPrefix: false
2)用chrome浏览器访问(用手机视图模式)二维码的统一入口地址。
http://127.0.0.1:56010/transaction/pay-entry/…
前边测试的支付确认页面由于没有数据所以显示为空,下边对页面进行完善。
1、页面完善需求如下:
1)进入支付入口,传入ticket,需要解析ticket得到订单信息,在支付确认页面显示。
2)解析出客户端类型,目前支付微信、支付宝两种。
拷贝资料–>代码下的PayOrderDTO.java、PayOrderConvert.java到交易服务工程。
解析ticket代码如下:
/**
* 支付入口
*
* @param ticket 传入数据,对json数据进行的base64编码
* @param request
* @return
*/
@RequestMapping(value = "/pay-entry/{ticket}")
public String payEntry(@PathVariable("ticket") String ticket, HttpServletRequest request) throws Exception {
//准备确认页面所需要的数据,将ticket的base64还原
String jsonString = EncryptUtil.decodeUTF8StringBase64(ticket);
//将json串转成对象
PayOrderDTO order = JSON.parseObject(jsonString, PayOrderDTO.class);
//将对象转成url格式,将对象的属性和值组成一个url的key/value串
String params = ParseURLPairUtil.parseURLPair(order);
return "forward:/pay‐page?" + params;
}
1、拷贝到资料–>代码下的BrowserType.java到交易服务controller包下,此类是系统定义的客户端配置类型。
2、修改支付入口代码:
/**
* 支付入口
*
* @param ticket 传入数据,对json数据进行的base64编码
* @param request
* @return
*/
@RequestMapping(value = "/pay-entry/{ticket}")
public String payEntry(@PathVariable("ticket") String ticket, HttpServletRequest request) throws Exception {
//准备确认页面所需要的数据,将ticket的base64还原
String jsonString = EncryptUtil.decodeUTF8StringBase64(ticket);
//将json串转成对象
PayOrderDTO order = JSON.parseObject(jsonString, PayOrderDTO.class);
//将对象转成url格式,将对象的属性和值组成一个url的key/value串
String params = ParseURLPairUtil.parseURLPair(order);
//2、解析客户端的类型(微信、支付宝)
//得到客户端类型
BrowserType browserType = BrowserType.valueOfUserAgent(request.getHeader("user-agent"));
switch (browserType) {
case ALIPAY:
//转发到确认页面,直接跳转收银台pay.html
return "forward:/pay-page?" + params;
case WECHAT:
//转发到确认页面,获取授权码(待实现)
return "forward:/pay-page?" + params;
default:
}
//不支持客户端类型,转发到错误页面
return "forward:/pay-page-error";
}
由于添加上了客户端类型的解析,使用微信或支付宝扫码方可进入支付入口,需要使用模拟器进行测试。
1、修改支付入口地址
修改为模拟器可以访问到的地址,模拟器安装在开发电脑上,支付入口地址修改为开发电脑局域网的地址。
2、生成门店二维码
3、使用模拟器运行支付宝沙箱APP,扫描二维码查看支付确认页面
顾客扫码进入支付确认页面,输入金额,点击立即支付,打开支付客户端(微信或支付宝),输入支付密码完成支付。
立即支付需要调用第三方支付渠道的统一下单接口,本章节完成支付宝统一下单接口对接。
有支付需求的微服务统一通过支付渠道代理服务调用“第三方支付服务”提供的接口,这样做的好处由支付渠道代理服务将第三方支付系统和闪聚支付内部服务进行解耦合。
整体执行流程如下:
支付渠道代理服务包括如下工程:
服务名 | 职责 |
---|---|
支付渠道代理服务API(shanjupay-payment-agent-api) | 定义支付渠道代理服务提供的接口 |
支付渠道代理服务(shanjupay-payment-agent-service) | 实现支付渠道代理服务的所有接口 |
1、复制提供的shanjupay-payment-agent目录到shanjupay根目录
2、添加Module到IDEA中
3、在Nacos中添加payment-agent-service.yaml配置,Group: SHANJUPAY_GROUP
server:
servlet:
context-path: /payment-receiver
4、打开shanjupay-payment-agent-service工程的bootstrap.yml,将其中的namespace替换为自己创建的dev命名空间ID
支付渠道代理服务调用支付宝手机网站下单接口,下边梳理接口的参数:
公共参数如下:
标记蓝色的由sdk设置、标记红色的已在支付渠道参数配置中(需要支付渠道代理服务接口处理),标记绿色的需支付渠道代理服务接口处理。
业务参数如下:
1、接口描述:
调用支付宝手机wap下单接口
2、接口定义
在shanjupay-payment-agent-api工程中新建PayChannelAgentService接口类定义接口:
package com.shanjupay.paymentagent.api;
import com.shanjupay.common.domain.BusinessException;
import com.shanjupay.paymentagent.api.conf.AliConfigParam;
import com.shanjupay.paymentagent.api.dto.AlipayBean;
import com.shanjupay.paymentagent.api.dto.PaymentResponseDTO;
/**
* 与第三支付渠道进行交互,调用支付宝手机WAP下单接口
*/
public interface PayChannelAgentService {
/**
* 调用支付宝的下单接口
* @param aliConfigParam 支付渠道配置的参数(配置的支付宝的必要参数)
* @param alipayBean 业务参数,请求支付参数(商户订单号,订单标题,订单描述,,)
* @return 统一返回PaymentResponseDTO
*/
public PaymentResponseDTO createPayOrderByAliWAP(AliConfigParam aliConfigParam, AlipayBean alipayBean) throws BusinessException;
}
在shanjupay-payment-agent-service工程中新建service包,创建PayChannelAgentServiceImpl实现类
package com.shanjupay.paymentagent.service;
import com.alibaba.fastjson.JSON;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.response.AlipayTradeWapPayResponse;
import com.shanjupay.common.domain.BusinessException;
import com.shanjupay.common.domain.CommonErrorCode;
import com.shanjupay.paymentagent.api.PayChannelAgentService;
import com.shanjupay.paymentagent.api.conf.AliConfigParam;
import com.shanjupay.paymentagent.api.dto.AlipayBean;
import com.shanjupay.paymentagent.api.dto.PaymentResponseDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.Service;
@Slf4j
@Service
public class PayChannelAgentServiceImpl implements PayChannelAgentService {
/**
* 调用支付宝的下单接口
* @param aliConfigParam 支付渠道配置的参数(配置的支付宝的必要参数)
* @param alipayBean 业务参数,请求支付参数(商户订单号,订单标题,订单描述,,)
* @return 统一返回PaymentResponseDTO
*/
@Override
public PaymentResponseDTO createPayOrderByAliWAP(AliConfigParam aliConfigParam, AlipayBean alipayBean) throws BusinessException {
log.info("支付宝请求参数", alipayBean.toString());
//支付宝渠道参数
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的客户端对象,支付宝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_WAY
model.setTimeoutExpress(alipayBean.getExpireTime());//订单过期时间
alipayRequest.setBizModel(model);//请求参数集合
log.info("createPayOrderByAliWAP..alipayRequest:{}", JSON.toJSONString(alipayBean));
alipayRequest.setReturnUrl(returnUrl);//设置同步地址
alipayRequest.setNotifyUrl(notifyUrl);//设置异步通知地址
try {
//请求支付宝下单接口,发起http请求,调用SDK提交表单
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);//支付宝确认支付失败
}
}
}
交易服务支付宝下单是提供给支付入口请求的支付宝付款接口,当用户用支付宝客户端扫描二维码进入确认支付页面,点击确认支付即将请求此接口。
1、接口描述
1)接收前端支付请求
2)保存订单信息到闪聚支付平台
3)调用支付渠道代理服务请求支付宝下单接口
4)将支付宝下单接口响应结果返回到前端,前端开始进行支付
2、接口定义
在shanjupay-transaction-service工程的PayController中定义接口如下:
定义OrderConfirmVO接收前端请求的支付参数:
package com.shanjupay.transaction.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
@ApiModel(value = "OrderConfirmVO", description = "订单确认信息")
@Data
public class OrderConfirmVO {
private String appId; //应用id
private String tradeNo;//交易单号
private String openId;//微信openid
private String storeId;//门店id
private String channel; //服务类型
private String body;//订单描述
private String subject;//订单标题
private String totalAmount;//金额
}
接口定义如下:
/**
* 支付宝的下单接口,前端订单确认页面,点击确认支付,请求进来
* @param orderConfirmVO 订单信息
* @param request
* @param response
*/
@ApiOperation("支付宝门店下单付款")
@PostMapping("/createAliPayOrder")
public void createAlipayOrderForStore(OrderConfirmVO orderConfirmVO, HttpServletRequest request, HttpServletResponse response) throws BusinessException, IOException {
}
1、拷贝资料–》代码中的IdWorkerUtils.java、PaymentUtil.java工具类到common 工程。
2、在TransactionService中定义接口submitOrderByAli方法:
/**
* 保存支付宝订单,1、保存订单到闪聚平台,2、调用支付渠道代理服务调用支付宝的接口
* @param payOrderDTO
* @return
* @throws BusinessException
*/
public PaymentResponseDTO submitOrderByAli(PayOrderDTO payOrderDTO) throws BusinessException;
2、在TransactionServiceImpl中编写submitOrderByAli方法。
/**
* 保存支付宝订单,1、保存订单到闪聚平台,2、调用支付渠道代理服务调用支付宝的接口
* @param payOrderDTO
* @return
* @throws BusinessException
*/
@Override
public PaymentResponseDTO submitOrderByAli(PayOrderDTO payOrderDTO) throws BusinessException {
payOrderDTO.setChannel("ALIPAY_WAP");//支付渠道
//保存订单到闪聚平台数据库
PayOrderDTO save = save(payOrderDTO);
//调用支付渠道代理服务支付宝下单接口
//...
return null;
}
3、编写保存订单的方法
//保存订单到闪聚平台(公用)
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);
}
在TransactionServiceImpl中编写私有方法,实现请求代理服务调用支付宝下单接口。
此私有方法被submitOrderByAli方法调用。
//调用支付渠道代理服务的支付宝下单接口
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.setStoreId(payOrderDTO.getStoreId());
alipayBean.setExpireTime("30m");
//支付渠道配置参数,从数据库查询,根据应用、服务类型、支付渠道查询支付渠道参数
//String appId,String platformChannel,String payChannel
PayChannelParamDTO payChannelParamDTO = payChannelService.queryParamByAppPlatformAndPayChannel(payOrderDTO.getAppId(), "shanju_c2b", "ALIPAY_WAP");
if (payChannelParamDTO == null) {
throw new BusinessException(CommonErrorCode.E_300007);
}
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);
log.info("支付宝H5支付响应Content:" + payOrderByAliWAP.getContent());
return payOrderByAliWAP;
}
定义根据订单号查询订单信息。
在shanjupay-transaction-api工程的TransactionService中定义如下接口:
/**
* 根据订单号查询订单号
* @param tradeNo
* @return
*/
public PayOrderDTO queryPayOrder(String tradeNo);
在shanjupay-transaction-service工程的TransactionServiceImpl中实现接口,如下:
/**
* 根据订单号查询订单信息
* @param tradeNo
* @return
*/
public PayOrderDTO queryPayOrder(String tradeNo) {
PayOrder payOrder = payOrderMapper.selectOne(new LambdaQueryWrapper<PayOrder>().eq(PayOrder::getTradeNo, tradeNo));
return PayOrderConvert.INSTANCE.entity2dto(payOrder);
}
完善submitOrderByAli方法,调用alipayH5方法:
/**
* 保存支付宝订单,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;
}
在shanjupay-transaction-service工程的PayController类中完善createAliPayOrder接口,调用submitOrderByAli提交支付宝订单。
/**
* 支付宝的下单接口,前端订单确认页面,点击确认支付,请求进来
* @param orderConfirmVO 订单信息
* @param request
* @param response
*/
@ApiOperation("支付宝门店下单付款")
@PostMapping("/createAliPayOrder")
public void createAlipayOrderForStore(OrderConfirmVO orderConfirmVO, HttpServletRequest request, HttpServletResponse response) throws BusinessException, IOException {
if (StringUtils.isBlank(orderConfirmVO.getAppId())) {
throw new BusinessException(CommonErrorCode.E_300003);
}
PayOrderDTO payOrderDTO = PayOrderConvert.INSTANCE.vo2dto(orderConfirmVO);
//应用id
String appId = payOrderDTO.getAppId();
//获取下单应用信息
AppDTO app = appService.getAppById(appId);
//设置所属商户
payOrderDTO.setMerchantId(app.getMerchantId());//商户id
//将前端输入的元转成分
payOrderDTO.setTotalAmount(Integer.parseInt(AmountUtil.changeY2F(orderConfirmVO.getTotalAmount().toString())));
//客户端ip
payOrderDTO.setClientIp(IPUtil.getIpAddr(request));
//保存订单,调用支付渠道代理服务的支付宝下单
PaymentResponseDTO<String> paymentResponseDTO = transactionService.submitOrderByAli(payOrderDTO);
//支付宝下单接口响应
String content = paymentResponseDTO.getContent();
log.info("支付宝H5支付响应的结果:" + content);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(content);//直接将完整的表单html输出到页面
response.getWriter().flush();
response.getWriter().close();
}
1、生成二维码
注意:二维码的URL可以被模拟器访问。
2、扫码进入支付入口,进入支付确认页面
3、输入金额,点击确认支付。
订单写入闪聚平台数据库。
调用支付宝下单接口是否成功。
获取支付结果的需求包括如下几个方面:
顾客支付完成后,平台需要及时得到支付结果并更新数据库中的订单状态。根据微服务职责划分,支付渠道代理服务负责与支付宝、微信接口对接,交易服务负责维护订单的数据,支付渠道代理服务如何把查询到的订单结果通知给交易服务呢?项目中会采用消息队列来完成。
完成支付后第三方支付系统提供两种方式获取支付结果,如下图:
1)第三方支付系统主动通知闪聚支付平台支付结果。
2)闪聚支付平台主动从第三方支付系统查询支付结果。
以上两种方法在实际项目中可以都用,其中第二种是必须用的,因为第一种是由第三方支付系统主动通知闪聚支付平台,当调用闪聚平台接口无法通信达到一定的次数后第三方支付系统将不再通知。
本项目支付渠道代理服务集成第二种方法完成支付结果的查询。
在调用第三方支付下单接口之后此时用户正在支付中,所以需要延迟一定的时间再去查询支付结果。
如果查询支付结果还没有支付再继续等待一定的时间再去查询,当达到订单的有效期还没有支付则不再查询。
参考: RocketMQ研究 。
项目使用消息队列RocketMQ完成支付渠道代理服务与交易服务之间的通信,如下图:
参考手机网支付产品介绍文档(https://docs.open.alipay.com/203),查看alipay.trade.query交易状态查询接口文档。
公共请求参数基本都是支付渠道设置的参数,实现方法参考支付宝下单接口即可。
业务请求参数中out_trade_no即闪聚支付平台订单号,根据此订单号查询支付状态。
支付宝交易状态查询接口的响应参数如下:
以上参数主要解析code和trade_status:
1、根据code判断接口请求是否成功
参考:https://docs.open.alipay.com/common/105806
2、根据trade_status判断具体的支付状态
交易状态如下:
接口描述:
1)使用支付宝SDK发起支付结果查询请求 2)返回查询结果
在PayChannelAgentService定义如下接口:
/**
* 查询支付宝订单状态
* @param aliConfigParam 支付渠道参数
* @param outTradeNo 闪聚平台的订单号
* @return
*/
public PaymentResponseDTO queryPayOrderByAli(AliConfigParam aliConfigParam, String outTradeNo) throws BusinessException;
参考支付宝官方提供的样例代码与支付宝通信。https://docs.open.alipay.com/api_1/alipay.trade.query
1)定义支付宝查询返回状态码常量类:AliCodeConstants
package com.shanjupay.paymentagent.common.constant;
/**
* 支付宝查询返回状态码常量类
*/
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"; // 系统异常
/**
* 支付宝交易状态
*/
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";//(交易结束,不可退款)
}
2)sdk示例如下
参考sdk示例:https://docs.open.alipay.com/api_1/alipay.trade.query/
AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do", "app_id", "your private_key", "json", "GBK", "alipay_public_key", "RSA2");
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent("{" +
"\"out_trade_no\":\"20150320010101001\"," +
"\"trade_no\":\"2014112611001004680 073956707\"," +
"\"org_pid\":\"2088101117952222\"," +
" \"query_options\":[" +
" \"TRADE_SETTLE_INFO\"" +
" ]" +
" }");
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
System.out.println("调用成功");
} else {
System.out.println("调用失败");
}
3)接口实现如下
在shanjupay-payment-agent-service工程的shanjupay-payment-agent-service实现类中,支付宝交易状态查询实现方法如下:
/**
* 查询支付宝订单状态
* @param aliConfigParam 支付渠道参数
* @param outTradeNo 闪聚平台的订单号
* @return
*/
@Override
public PaymentResponseDTO queryPayOrderByAli(AliConfigParam aliConfigParam, String outTradeNo) 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
log.info("C扫B请求支付宝查询订单,参数:{}", JSON.toJSONString(aliConfigParam));
//构造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)) {
String tradeStatusString = response.getTradeStatus();
//解析支付宝返回的状态,解析成闪聚平台的TradeStatus
TradeStatus tradeStatus = covertAliTradeStatusToShanjuCode(tradeStatusString);
//String tradeNo(支付宝订单号), String outTradeNo(闪聚平台的订单号), TradeStatus tradeState(订单状态), String msg(返回信息)
PaymentResponseDTO<Object> dto = PaymentResponseDTO.success(response.getTradeNo(), response.getOutTradeNo(), tradeStatus, response.getMsg());
log.info("‐‐‐‐查询支付宝H5支付结果" + JSON.toJSONString(dto));
return dto;
}
} catch (AlipayApiException e) {
e.printStackTrace();
}
//String msg, String outTradeNo, TradeStatus tradeState
return PaymentResponseDTO.fail("支付宝订单状态查询失败", outTradeNo, TradeStatus.UNKNOWN);
}
定义支付宝响应状态与闪聚平台的转换方法:
/**
* 解析支付宝的订单状态为闪聚平台的状态
* 将支付宝查询时订单状态trade_status 转换为闪聚订单状态
* @param aliTradeStatus 支付宝交易状态
* @return
*/
private TradeStatus covertAliTradeStatusToShanjuCode(String aliTradeStatus) {
switch (aliTradeStatus) {
case AliCodeConstants.WAIT_BUYER_PAY://(交易创建,等待买家付款)
return TradeStatus.USERPAYING;//交易新建,等待支付
case AliCodeConstants.TRADE_FINISHED://(交易结束,不可退款)
case AliCodeConstants.TRADE_SUCCESS://(交易支付成功)
return TradeStatus.SUCCESS;//业务交易支付 明确成功
case AliCodeConstants.TRADE_CLOSED://(未付款交易超时关闭,或支付完成后全额退款)
return TradeStatus.REVOKED;//交易已撤销
default:
return TradeStatus.FAILED;//交易失败
}
}
对queryPayOrderByAli方法进行单元测试:
1、通过C扫B进行支付定下单,从数据库找到订单号
2、编写测试类
在shanjupay-payment-agent-service的test包下创建测试类
APP_ID、APP_PRIVATE_KEY、ALIPAY_PUBLIC_KEY使用自己申请的支付宝沙箱参数。
package com.shanjupay.paymentagent.service;
import com.shanjupay.paymentagent.api.PayChannelAgentService;
import com.shanjupay.paymentagent.api.conf.AliConfigParam;
import com.shanjupay.paymentagent.api.dto.PaymentResponseDTO;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestPayChannelAgentService {
@Autowired
PayChannelAgentService payChannelAgentService;
@Test
public void testqueryPayOrderByAli() {
//沙箱应用APPID
String APP_ID = "2021000118620802";
//沙箱应用私钥
String APP_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCWIa5n7rnlTYpgC7AUXC5n1I9CtFfK40wBkQoPea8dpsuMWcOFOM7TDzD1ImQXO01oak5VGdpRKj4WIOH3o/n2VYwaFipBldGLfFOQhPZbqbFEI2I7vWf0r716SuN5cG4mny8ZbhP56S4ccNpCsLsd7ZExjDPMR30loc0sBYkwomBk3To9HzD+xvYB0Ld2orhjFTB7/VCPYblDYZkwfRiZ/BoJY5cpyt3hgF9uVa0KQq6whhrvnX0HzzTNtYClCrOemLu0BqTzX6g5mFxGJwfF7hhwvbgHqTS8Gonn9+ha3VTEMikPHjVCg03PmtBoJyaMhQwhATELNe2UWOMmlRJLAgMBAAECggEADw/b/oNd1Rp9anths/k3kqUppkiPkkRRiMqzVrAfmHr2auNKkWAMp/IbOEy1+/qwHmyj5TfNxlzVk8TCxuSFnGgiwS8+GAxe1H6pp5MfYDzbEvn1zgaHmm3TNaSzw6g69Nb9k7COgoEZZjMQQqaWbz85VN47CCCX9qGQAv2fMOjBnYXz9/5cexEYFM5n881ocWh4CbmRwn3S5M2EqVXMvxkYP27STtv808GvrozrODzR+D4k3yubkLC7/U9HDnazY5sAN0YNCf3sBudAgeU42HyBHq2sxIUWyZy9UaxTnqNoYo+8lldGS3QbdrXIHZM40V6tWKYbGMA2BrYAFNCRCQKBgQDnvFjmxEZRQOt9f9u5fF4wL2TRYPQajsajBm5bXq2MF+YQnNnUzh1n+Lh/GLSCND+97mNftaM0zYgGxj241KaLF83w9KqFTl1RCT1CSMN4+PRhsMfzU/Hpx/13ZYtvOb+qLnczjkTYa4wx79n5I54ib9noQcKGsEtRyHygU+tqLwKBgQCl2fFTb2l2IdwgeopRkE+Ak+bTIBzks/VS8+4pbFPFKb94VI0eJCTtLaMun9ElB01WfnYqVfQaCeieRzHWnpo00XR6r4qtmeoBV91JDpHmpnRqHjEMr5gr5RBhzgLUgxOA2O2RtX68Pe8Dd/siSEKHGz9gyw8Eus1gdj2RjrH+pQKBgQC9MO8f0ARcl/Tqa/V2VMwM6NSVgGMqP4B6XmjAneZwJp7E11mcPH6TgOMXmJLebkvQA40L+Z36IQa6CSUg/jPOASw4WXfSB6112GYz9HXqEM5r50kHJnStWYJc9QFGWE5bYT4eUDtyuTMnHdvGZEbZdJnh3bY0AkArz9O3jWv4LwKBgC793oO+eIoxM9a8Ab70faI3xdoiKi2e067KULvJ5r5hgs/MXSOiKBhPqwHF5JNySzZrpH2AVyadkhxuna9qxtSaWD9+x3NCvevdgmR1zV8l4KxEm681fY9KWubrYR/nd7o1PLLhUuRxQ+yerThccwUm8kExp7K2XwSq2+0HGmXFAoGBAIxgJ57Pcmix0HO/xnEcHYhADrSDGDyKDCmNdoW/rxI5JKq6/SucPvDGwWv9/1bwp46y7CSWUM5UqCOqcnsbTDmoOz1oO3Lm7ESKydv7/IXQdrmDXUPzrXVGwne4JpCKkuwhYaCeT6uICJWOs4ZFv0kw9l9n9nYQBrQB06Hc4mzd";
//支付宝公钥
String ALIPAY_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZsrdXBmfuatiXIQWT84KNAirI9XrGnFNFmXpoZUaU9kC40fFZiamPoCaHASs3gsuJka3gezE+rMoZHPNzHIoWsHnE6kPgv2n2AdBFW7Bo4BL8fkiNj+2iNPfF2Cn9CgpsplTWCBygJCdf6QoRblQRyQzpnMT8aqcid/5cXxUNTsXqtcunCmQyZMqN+KJcgskk46RFIvdgkQMNuiTKJI3Pg+pPRDFXoxpY4YcWiJNDRbzc7UC4jr+sR6qAYkd7mDiMSSK8t8ybtltbfyvIIYnTG87HCl/atMCGYWUcyohjWxduX1PDQP3IxxgKpRSDDujVb8s/Le4LZ4LEZocasUgQIDAQAB";
//签名算法类型
String CHARSET = "UTF-8";
//支付宝接口的网关地址,正式"https://openapi.alipay.com/gateway.do"
String serverUrl = "https://openapi.alipaydev.com/gateway.do";
//签名算法类型
String sign_type = "RSA2";
//支付渠道参数
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(sign_type);
//AliConfigParam aliConfigParam,String outTradeNo
PaymentResponseDTO paymentResponseDTO = payChannelAgentService.queryPayOrderByAli(aliConfigParam, "SJ1217987323129917440");
System.out.println(paymentResponseDTO);
}
}
根据技术方案的分析,交互流程如下:
1)在支付渠道代理工程中添加RocketMQ依赖:
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-spring-boot-starterartifactId>
<version>2.0.2version>
dependency>
2)在Nacos中添加spring-boot-starter-rocketmq.yaml配置,Data ID: spring-boot-starter-rocketmq.yaml,Group: COMMON_GROUP
rocketmq:
nameServer: 127.0.0.1:9876
producer:
group: PID_PAY_PRODUCER
3)在shanjupay-payment-agent-service工程bootstrap.yml中引入此配置:
-
refresh: true
data-id: spring-boot-starter-rocketmq.yaml # rocketmq配置
group: COMMON_GROUP # 通用配置组
1、修改支付宝下单调用方法createPayOrderByAliWAP,调用PayProducer发送消息。
发送支付结果查询延迟消息代码如下:
try {
//请求支付宝下单接口,发起http请求,调用SDK提交表单
AlipayTradeWapPayResponse response = alipayClient.pageExecute(alipayRequest);
PaymentResponseDTO paymentResponseDTO = new PaymentResponseDTO();
log.info("调用支付宝下单接口,响应内容:{}", response.getBody());
paymentResponseDTO.setContent(response.getBody());//支付宝的响应结果
//向MQ发一条延迟消息,发送支付结果查询延迟消息
PaymentResponseDTO<AliConfigParam> notice = new PaymentResponseDTO<AliConfigParam>();
notice.setOutTradeNo(alipayBean.getOutTradeNo());//闪聚平台的订单号
notice.setContent(aliConfigParam);
notice.setMsg("ALIPAY_WAP");//标识是查询支付宝的接口
//发送消息
payProducer.payOrderNotice(notice);
return paymentResponseDTO;
} catch (AlipayApiException e) {
e.printStackTrace();
throw new BusinessException(CommonErrorCode.E_400002);//支付宝确认支付失败
}
2、在支付渠道代理服务中编写生产消息类PayProducer
package com.shanjupay.paymentagent.message;
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;
@Component
@Slf4j
public class PayProducer {
//订单结果查询主题
private static final String TOPIC_ORDER = "TP_PAYMENT_ORDER";
@Autowired
RocketMQTemplate rocketMQTemplate;
//发送消息(查询支付宝订单状态)
public void payOrderNotice(PaymentResponseDTO paymentResponseDTO) {
log.info("支付通知发送延迟消息:{}", paymentResponseDTO);
try {
//发送延迟消息,处理消息存储格式
Message<PaymentResponseDTO> message = MessageBuilder.withPayload(paymentResponseDTO).build();
//延迟第3级发送(延迟10秒)
rocketMQTemplate.syncSend(TOPIC_ORDER, message, 1000, 3);
log.info("支付渠道代理服务向mq发送订单查询的消息:{}", JSON.toJSONString(paymentResponseDTO));
} catch (Exception e) {
log.warn(e.getMessage(), e);
}
}
}
定义PayConsumer类,监听“支付结果查询”消息队列。
package com.shanjupay.paymentagent.message;
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;
@Component
@RocketMQMessageListener(topic = "TP_PAYMENT_ORDER", consumerGroup = "CID_PAYMENT_CONSUMER")
@Slf4j
public class PayConsumer implements RocketMQListener<MessageExt> {
@Autowired
PayChannelAgentService payChannelAgentService;
@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 outTradeNo = paymentResponseDTO.getOutTradeNo();//订单号
String params = String.valueOf(paymentResponseDTO.getContent());//支付渠道参数
//params转成对象
AliConfigParam aliConfigParam = JSON.parseObject(params, AliConfigParam.class);
PaymentResponseDTO responseDTO = null;
//判断是支付宝还是微信
if ("ALIPAY_WAP".equals(paymentResponseDTO.getMsg())) {
//调用支付宝订单状态查询接口,查询支付宝支付结果
//AliConfigParam aliConfigParam,String outTradeNo
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())) {
//在支付状态未知或支付中,抛出异常会重新消息此消息
//如果重试的次数达到一次数量,不要再重试消费,将消息记录到数据库,由单独的程序或人工进行处理
log.info("支付代理‐‐‐支付状态未知,等待重试");
throw new RuntimeException("支付状态未知,等待重试");
}
}
}
支付渠道代理服务查询到支付结果,将支付结果更新消息发送给交易服务,实现订单状态更新,流程如下:
在支付渠道代理服务的PayProducer中定义发送“支付结果更新”消息方法
//订单结果 主题
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));
}
修改支付渠道代理服务的PayConsumer,在查询到支付结果后调用payResultNotice
//... ...
//将订单状态,再次发到mq...
//不管支付成功还是失败都需要发送支付结果消息
log.info("交易中心处理支付结果通知,支付代理发送消息:{}", responseDTO);
payProducer.payResultNotice(responseDTO);
//... ...
在shanjupay-transaction-api工程中的TransactionService定义接口:
/**
* 更新订单支付状态
* @param tradeNo 闪聚平台订单号
* @param payChannelTradeNo 支付宝或微信的交易流水号(第三方支付系统的订单)
* @param state 订单状态 交易状态支付状态,0-订单生成,1-支付中(目前未使用),2-支付成功,4-关闭 5--失败
*/
public void updateOrderTradeNoAndTradeState(String tradeNo, String payChannelTradeNo, String state) throws BusinessException;
在TransactionServiceImpl中实现updateOrderTradeNoAndTradeState方法,根据闪聚支付订单号和支付宝订单号更新订单状态:
/**
* 更新订单支付状态
* @param tradeNo 闪聚平台订单号
* @param payChannelTradeNo 支付宝或微信的交易流水号(第三方支付系统的订单)
* @param state 订单状态 交易状态支付状态,0-订单生成,1-支付中(目前未使用),2-支付成功,4-关闭 5--失败
*/
@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);
}
1)在交易服务工程中添加RocketMQ依赖:
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-spring-boot-starterartifactId>
<version>2.0.2version>
dependency>
2)在shanjupay-transaction-service的bootstrap.yml中引入此配置:
- refresh: true
data-id: spring-boot-starter-rocketmq.yaml # rocketmq配置
group: COMMON_GROUP # 通用配置组
package com.shanjupay.transaction.message;
import com.alibaba.fastjson.JSON;
import com.shanjupay.paymentagent.api.dto.PaymentResponseDTO;
import com.shanjupay.paymentagent.api.dto.TradeStatus;
import com.shanjupay.transaction.api.TransactionService;
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;
@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));
}
}
}
闪聚支付平台是将各各常用的第三方支付渠道统一为一个支付通道,前边实现了C扫B支付宝支付的流程,下边接入微信支付,根据接入支付宝的流程分析接入微信需要实现的如下:
1、支付入口
顾客扫码进入支付入口,根据客户端类型判断是微信还是支付宝,是支付宝则直接进入收银台,如果是微信则需要首先获取openid,再进入收银台。
2、立即支付
点击立即支付调用微信的统一下单接口,若下单成功则唤起微信客户端开始支付。
3、获取支付结果
调用微信的“支付结果查询”接口获取支付结果。
参考:闪聚支付-第3章-微信支付接入指南
用户进入支付入口,判断客户端类型如果是微信则获取微信授权码。
根据获取openid的流程得知,第一步获取微信授权码,这里需要生成获取微信授权码的URL,由页面重定向即可。
1、在nacos中交易服务的主配置文件中添加如下参数:
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"
2、在交易服务TransactionService中定义接口,如下:
/**
* 申请微信授权码
* @param payOrderDTO
* @return 申请授权码的地址
*/
public String getWXOAuth2Code(PayOrderDTO payOrderDTO);
3、在交易服务TransactionServiceImpl实现类中添加获取微信授权码方法。
@Value("${weixin.oauth2RequestUrl}")
String oauth2RequestUrl;
@Value("${weixin.oauth2CodeReturnUrl}")
String oauth2CodeReturnUrl;
@Value("${weixin.oauth2Token}")
String oauth2Token;
/**
* 申请微信授权码
*
* @param payOrderDTO
* @return 申请授权码的地址
*/
@Override
public String getWXOAuth2Code(PayOrderDTO payOrderDTO) {
//闪聚平台的应用id
String appId = payOrderDTO.getAppId();
//获取微信支付渠道参数
//String appId,String platformChannel,String payChannel,获取微信支付渠道参数,根据应用、服务类型、支付渠道查询支付渠道参数
PayChannelParamDTO payChannelParamDTO = payChannelService.queryParamByAppPlatformAndPayChannel(appId, "shanju_c2b", "WX_JSAPI");
if (payChannelParamDTO == null) {
throw new BusinessException(CommonErrorCode.E_300007);
}
//支付渠道参数
String param = payChannelParamDTO.getParam();
//微信支付渠道参数
WXConfigParam wxConfigParam = JSON.parseObject(param, WXConfigParam.class);
//state是一个原样返回的参数
String jsonString = JSON.toJSONString(payOrderDTO);
//将订单信息封装到state参数中
String state = EncryptUtil.encodeUTF8StringBase64(jsonString);
//https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
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);
log.info("微信生成授权码url:{}", url);
return "redirect:" + url;
} catch (Exception e) {
e.printStackTrace();
}
return "forward:/pay-page-error";//生成获取授权码链接失败
}
3、在支付入口PayController中调用微信授权码获取方法
/**
* 支付入口
* @param ticket 传入数据,对json数据进行的base64编码
* @param request
* @return
*/
@RequestMapping(value = "/pay-entry/{ticket}")
public String payEntry(@PathVariable("ticket") String ticket, HttpServletRequest request) throws Exception {
//... ...
BrowserType browserType = BrowserType.valueOfUserAgent(request.getHeader("user-agent"));
switch (browserType) {
case ALIPAY:
//转发到确认页面,直接跳转收银台pay.html
return "forward:/pay-page?" + params;
case WECHAT:
//转发到确认页面,获取授权码(待实现)
//return "forward:/pay-page?" + params;
//先获取授权码,申请openid,再到支付确认页面
return transactionService.getWXOAuth2Code(payOrderDTO);
default:
}
// ......
}
授权码获取成功后微信会将授权码传入授权码回调URL,在授权码回调接口中实现获取openid。
1、在PayController中定义微信授权码回调接口
/**
* 授权码回调,申请获取授权码,微信将授权码请求到此地址
* @param code 授权码
* @param state 订单信息
* @return
*/
@ApiOperation("微信授权码回调")
@GetMapping("/wx-oauth-code-return")
public String wxOAuth2CodeReturn(@RequestParam String code, @RequestParam String state) {
//获取openid
//重定向到支付确认页面
}
2、在TransactionService中定义获取openid方法
/**
* 申请openid
* @param code 授权码
* @param appId 闪聚平台的应用id,为了获取该应用的微信支付渠道参数
* @return
*/
public String getWXOAuthOpenId(String code, String appId);
1、添加RestTemplate配置
使用RestTemplate发起http请求,在shanjupay-transaction-service工程的pom.xml中添加依赖:
<dependency>
<groupId>com.squareup.okhttp3groupId>
<artifactId>okhttpartifactId>
dependency>
2、添加获取openid地址配置
在nacos中向交易服务添加获取openid地址配置,如下:
weixin:
oauth2Token: "https://api.weixin.qq.com/sns/oauth2/access_token"
3、在交易服务的TransactionServiceImpl中实现getWXOAuthOpenId接口实现
/**
* 获取微信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;
}
4、在PayController中实现微信授权码回调接口实现
/**
* 授权码回调,申请获取授权码,微信将授权码请求到此地址
* @param code 授权码
* @param state 订单信息
* @return
*/
@ApiOperation("微信授权码回调")
@GetMapping("/wx-oauth-code-return")
public String wxOAuth2CodeReturn(@RequestParam String code, @RequestParam String state) {
//获取openid
//重定向到支付确认页面
//将之前state中保存的订单信息读取出来
String jsonString = EncryptUtil.decodeUTF8StringBase64(state);
PayOrderDTO payOrderDTO = JSON.parseObject(jsonString, PayOrderDTO.class);
//闪聚平台的应用id
String appId = payOrderDTO.getAppId();
//接收到code授权码,申请openid
String openId = transactionService.getWXOAuthOpenId(code, appId);
//将对象的属性和值组成一个url的key/value串
String params = null;
try {
params = ParseURLPairUtil.parseURLPair(payOrderDTO);
//转发到支付确认页面
String url = String.format("forward:/pay-page?openId=%s&%s", openId, params);
return url;
} catch (Exception e) {
e.printStackTrace();
return "forward:/pay-page-error";
}
}
1、生成门店c扫b的二维码
2、打开内网穿透工具
3、打开模拟器,使用微信扫码
4、观察程序输出日志,确认openid是否生成成功,支付确认页面是否正常打开
点击立即支付调用第三方支付系统的下单接口,微信客户端扫码进入确认页面,点击立即支付则由渠道代理服务调用微信支付的下单接口,具体的流程如下:
1、微信客户端扫码进入确认页面,点击立即支付请求交易服务微信下单接口
2、交易服务通过支付渠道代理服务调用微信下单接口
3、调用微信下单接口成功,返回H5网页
1、 微信支付下单接口参数
请求参数如下,主要关注必填项目:
红色:支付渠道参数配置的内容
蓝色:微信sdk自动配置
绿色:程序设置
响应参数:
2、 支付渠道代理服务下单接口定义
1)接口描述:
调用微信jsapi下单接口
2)接口定义
在PayChannelAgentService中定义接口:
/**
* 微信下单接口
* @param wxConfigParam 微信支付渠道参数
* @param weChatBean 订单业务数据
* @return h5网页的数据
*/
public Map<String, String> createPayOrderByWeChatJSAPI(WXConfigParam wxConfigParam, WeChatBean weChatBean);
将资料–>代码下的WXSDKConfig.java工具类拷贝至支付渠道代理服务。
1、在shanjupay-payment-agent-service工程的pom.xml引入依赖:
<dependency>
<groupId>com.github.tedzhdzgroupId>
<artifactId>wxpay-sdkartifactId>
<version>3.0.10version>
dependency>
<dependency>
<groupId>com.github.binarywanggroupId>
<artifactId>weixin-java-payartifactId>
<version>3.4.0version>
dependency>
2、在PayChannelAgentService中实现createPayOrderByWeChatJSAPI方法
/**
* 微信下单接口
* @param wxConfigParam 微信支付渠道参数
* @param weChatBean 订单业务数据
* @return h5网页的数据
*/
@Override
public Map<String, String> createPayOrderByWeChatJSAPI(WXConfigParam wxConfigParam, WeChatBean weChatBean) {
WXSDKConfig config = new WXSDKConfig(wxConfigParam);//通过实际支付参数匹配
Map<String, String> jsapiPayParam = null;
try {
//创建sdk客户端
WXPay wxPay = new WXPay(config);
//按照微信统一下单接口要求构造请求参数
Map<String, String> requestParam = new HashMap<>();
requestParam.put("out_trade_no", weChatBean.getOutTradeNo());//订单号
requestParam.put("body", weChatBean.getBody());//订单描述
requestParam.put("fee_type", "CNY");//人民币
requestParam.put("total_fee", String.valueOf(weChatBean.getTotalFee())); //金额
requestParam.put("spbill_create_ip", weChatBean.getSpbillCreateIp());//客户端ip
requestParam.put("notify_url", weChatBean.getNotifyUrl());//微信异步通知支付结果接口,暂时不用
requestParam.put("trade_type", "JSAPI");
//从请求中获取openid
String openid = weChatBean.getOpenId();
requestParam.put("openid", openid);
//调用统一下单接口
Map<String, String> resp = wxPay.unifiedOrder(requestParam);
//=====向mq写入订单查询的消息=====
PaymentResponseDTO paymentResponseDTO = new PaymentResponseDTO();
//订单号
paymentResponseDTO.setOutTradeNo(weChatBean.getOutTradeNo());
//支付渠道参数
paymentResponseDTO.setContent(wxConfigParam);
//msg
paymentResponseDTO.setMsg("WX_JSAPI");
payProducer.payOrderNotice(paymentResponseDTO);
//准备h5网页需要的数据
jsapiPayParam = new HashMap<>();
jsapiPayParam.put("appId", wxConfigParam.getAppId());
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");
//将h5网页响应给前端
jsapiPayParam.put("paySign", WXPayUtil.generateSignature(jsapiPayParam, wxConfigParam.getKey(), WXPayConstants.SignType.HMACSHA256));
log.info("微信JSAPI支付响应内容:" + jsapiPayParam);
return jsapiPayParam;
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException(CommonErrorCode.E_400001);
}
}
交易服务微信下单是提供给支付入口请求的微信付款的接口,当用户用微信客户端扫描二维码进入确认支付页面,点击确认支付即将请求此接口。
按照微信官方例子编写调起微信客户端支付的H5页面,从资料文件夹拷贝“wxpay.html”到交易服务下
1、接口描述
1)接收前端支付请求
2)保存订单信息到闪聚支付平台
3)调用支付渠道代理服务请求微信下单接口
2、接口定义
1、在TransactionService中编写submitOrderByWechat接口。
/**
* 1、保存订单到闪聚平台,2、调用支付渠道代理服务调用微信的接口
* @param payOrderDTO
* @return h5页面所需要的数据
*/
Map<String, String> submitOrderByWechat(PayOrderDTO payOrderDTO) throws BusinessException;
2、在PayController中定义接口如下:
//微信下单 /wxjspay
@ApiOperation("微信门店下单付款")
@PostMapping("/wxjspay")
public ModelAndView createWXOrderForStore(OrderConfirmVO orderConfirmVO, HttpServletRequest request) {
if (StringUtils.isBlank(orderConfirmVO.getOpenId())) {
throw new BusinessException(CommonErrorCode.E_300002);
}
PayOrderDTO payOrderDTO = PayOrderConvert.INSTANCE.vo2dto(orderConfirmVO);
//应用id
String appId = payOrderDTO.getAppId();
AppDTO app = appService.getAppById(appId);
//商户id
payOrderDTO.setMerchantId(app.getMerchantId());
//客户端ip
payOrderDTO.setClientIp(IPUtil.getIpAddr(request));
//将前端输入的元转成分
payOrderDTO.setTotalAmount(Integer.parseInt(AmountUtil.changeY2F(orderConfirmVO.getTotalAmount().toString())));
//调用微信下单接口 submitOrderByWechat
Map<String, String> model = transactionService.submitOrderByWechat(payOrderDTO);
log.info("/wxjspay 微信门店下单接口响应内容:{}",model);
return new ModelAndView("wxpay", model);
}
本接口实现两部分内容:
1)保存订单到闪聚支付数据库
2)调用支付渠道代理服务请求微信下单接口
1、保存订单
实现方法同支付宝下单,需要注意订单信息的支付渠道标识为WX_JSAPI:
/**
* 微信确认支付
* 1、保存订单到闪聚平台,
* 2、调用支付渠道代理服务调用微信的接口
* @param payOrderDTO
* @return h5页面所需要的数据
*/
@Override
public Map<String, String> submitOrderByWechat(PayOrderDTO payOrderDTO) throws BusinessException {
//微信openid
String openId = payOrderDTO.getOpenId();
//支付渠道
payOrderDTO.setChannel("WX_JSAPI");
//保存订单到闪聚平台数据库
PayOrderDTO save = save(payOrderDTO);
//调用支付渠道代理服务,调用微信下单接口
return weChatJsapi(openId,save.getTradeNo());
}
2、请求支付渠道代理服务进行微信下单
//微信jsapi 调用支付渠道代理
private Map<String, String> weChatJsapi(String openId, String tradeNo) {
//根据订单号查询订单详情
PayOrderDTO payOrderDTO = queryPayOrder(tradeNo);
if (payOrderDTO == null) {
throw new BusinessException(CommonErrorCode.E_400002);
}
//构造微信订单参数实体
WeChatBean weChatBean = new WeChatBean();
weChatBean.setOpenId(openId);//微信openid
weChatBean.setOutTradeNo(payOrderDTO.getTradeNo());//闪聚平台的订单号
weChatBean.setTotalFee(payOrderDTO.getTotalAmount());//金额(分)
weChatBean.setSpbillCreateIp(payOrderDTO.getClientIp());//客户ip
weChatBean.setBody(payOrderDTO.getBody());//订单描述
weChatBean.setNotifyUrl("none");//异步接收微信通知支付结果的地址(暂时不用)
String appId = payOrderDTO.getAppId();
//根据应用、服务类型、支付渠道查询支付渠道参数,从数据库查询
//String appId,String platformChannel,String payChannel
PayChannelParamDTO payChannelParamDTO = payChannelService.queryParamByAppPlatformAndPayChannel(appId, "shanju_c2b", "WX_JSAPI");
String paramJson = payChannelParamDTO.getParam();
WXConfigParam wxConfigParam = JSON.parseObject(paramJson, WXConfigParam.class);
//WXConfigParam wxConfigParam, WeChatBean weChatBean
Map<String, String> payOrderByWeChatJSAPI = payChannelAgentService.createPayOrderByWeChatJSAPI(wxConfigParam, weChatBean);
return payOrderByWeChatJSAPI;
}
1、生成门店c扫b的二维码
2、打开模拟器,使用微信扫码,进入支付确认页面
3、输入金额,点击立即支付
4、观察控制台日志,最终订单写入闪聚平台数据库
5、调起微信支付客户端,输入密码支付成功
根据获取支付结果的技术方案,接入微信需要请求微信查询支付结果,接口参数如下:
请求参数:
响应参数:
以下字段在return_code 、result_code、trade_state都为SUCCESS时有返回 ,如trade_state不为 SUCCESS,则只返回out_trade_no(必传)和attach(选传)。
支付结果:
接口描述:
1)使用微信SDK发起支付结果查询请求 2)返回查询结果
在PayChannelAgentService定义如下接口:
/**
* 查询微信订单状态
* @param wxConfigParam 支付渠道参数
* @param outTradeNo 闪聚平台的订单号
* @return
* @throws BusinessException
*/
public PaymentResponseDTO queryPayOrderByWeChat(WXConfigParam wxConfigParam, String outTradeNo) throws BusinessException;
/**
* 查询微信订单状态,查询微信支付结果
* @param wxConfigParam 支付渠道参数
* @param outTradeNo 闪聚平台的订单号
* @return
* @throws BusinessException
*/
@Override
public PaymentResponseDTO queryPayOrderByWeChat(WXConfigParam wxConfigParam, String outTradeNo) throws BusinessException {
WXSDKConfig config = new WXSDKConfig(wxConfigParam);//通过实际支付参数匹配
Map<String, String> result = null;
try {
//创建sdk客户端
WXPay wxPay = new WXPay(config);
Map<String, String> map = new HashMap<>();
map.put("out_trade_no", outTradeNo);//闪聚平台的订单号
//调用微信的订单查询接口
result = wxPay.orderQuery(map);
} catch (Exception e) {
log.warn(e.getMessage(), e);
return PaymentResponseDTO.fail("调用微信订单查询接口失败", outTradeNo, TradeStatus.UNKNOWN);
}
String return_code = result.get("return_code");
String return_msg = result.get("return_msg");
String result_code = result.get("result_code");
String trade_state = result.get("trade_state");//订单状态
String transaction_id = result.get("transaction_id");//微信订单号
if ("SUCCESS".equals(return_code) && "SUCCESS".equals(result_code)) {
if ("SUCCESS".equals(trade_state)) { //支付成功
return PaymentResponseDTO.success(transaction_id, outTradeNo, TradeStatus.SUCCESS, return_msg);
} else if ("CLOSED".equals(trade_state)) {//交易关闭
return PaymentResponseDTO.success(transaction_id, outTradeNo, TradeStatus.REVOKED, return_msg);
} else if ("USERPAYING".equals(trade_state)) {//支付中
return PaymentResponseDTO.success(transaction_id, outTradeNo, TradeStatus.USERPAYING, return_msg);
} else if ("PAYERROR".equals(trade_state)) {//支付失败
return PaymentResponseDTO.success(transaction_id, outTradeNo, TradeStatus.FAILED, return_msg);
}
}
return PaymentResponseDTO.success("不可识别的微信订单状态", transaction_id, outTradeNo, TradeStatus.UNKNOWN);
}
在shanjupay-payment-agent-service工程的测试类中进行测试
@Test
public void testQueryPayOrderByWeChat() {
String appID = "wxd2bf2dba2e86a8c7";
String mchID = "1502570431";
String appSecret = "cec1a9185ad435abe1bced4b93f7ef2e";
String key = "95fe355daca50f1ae82f0865c2ce87c8";
WXConfigParam wxConfigParam = new WXConfigParam();
wxConfigParam.setKey(key);
wxConfigParam.setAppSecret(appSecret);
wxConfigParam.setAppId(appID);
wxConfigParam.setMchId(mchID);
//WXConfigParam wxConfigParam,String outTradeNo
PaymentResponseDTO paymentResponseDTO = payChannelAgentService.queryPayOrderByWeChat(wxConfigParam, "SJ1218090459880816640");
System.out.println(paymentResponseDTO);
}
支付渠道代理服务完成微信下单接口的调用即向MQ发送支付结果查询消息
修改支付渠道代理服务的createPayOrderByWeChatJSAPI方法:
修改支付渠道代理服务的PayConsumer
if ("ALIPAY_WAP".equals(paymentResponseDTO.getMsg())) {
//调用支付宝订单状态查询接口,查询支付宝支付结果
//AliConfigParam aliConfigParam,String outTradeNo
responseDTO = payChannelAgentService.queryPayOrderByAli(aliConfigParam, outTradeNo);
} else if ("WX_JSAPI".equals(paymentResponseDTO.getMsg())) {
//调用微信的接口去查询订单状态,查询微信支付结果
WXConfigParam wxConfigParam = JSON.parseObject(params, WXConfigParam.class);
responseDTO = payChannelAgentService.queryPayOrderByWeChat(wxConfigParam, outTradeNo);
}
//当没有获取到订单结果,抛出异常,再次重试消费,返回查询获得的支付状态
if (responseDTO == null || TradeStatus.UNKNOWN.equals(responseDTO.getTradeState()) || TradeStatus.USERPAYING.equals(responseDTO.getTradeState())) {
//在支付状态未知或支付中,抛出异常会重新消息此消息
//如果重试的次数达到一次数量,不要再重试消费,将消息记录到数据库,由单独的程序或人工进行处理
log.info("支付代理‐‐‐支付状态未知,等待重试");
throw new RuntimeException("支付状态未知,等待重试");
}
1、生成门店c扫b的二维码
2、打开模拟器,使用微信扫码,进入支付确认页面
3、输入金额,点击立即支付
4、观察控制台日志,最终订单写入闪聚平台数据库
5、调起微信支付客户端,输入密码支付成功
观察控制台日志,支付结果是否发送至交易服务。