需求分析

C扫B的概念

C扫B,即顾客(Customer)扫描商户(Business)提供的二维码来完成支付。下图是支付宝提供的C扫B业务流程:

  1. 商家出示付款二维码
  2. 客户打开支付宝或微信的扫一扫,扫描二维码
  3. 确认支付,完成支付。


C扫B支付分为两种方式:一是固定金额支付,顾客扫描后无需输入金额直接确认支付即可;另外一种是输入金额,顾客扫描后需自己输入待支付的金额,然后完成支付。

什么是固定金额支付?

C扫B固定金额比较常见的就是在自动售货机购买饮料时,当你选择一个饮料后屏幕会显示一个二维码,咱们扫描后只需确认支付即可,无需自己输入饮料的价格,这种情况大家可以根据下面的交互流程图来自行实现。

什么是输入金额支付?

C扫B输入金额支付方式可以让买方自由输入金额,商户提前生成一个二维码,将二维码张贴在结账处,买方扫描二维码,输入当前消费的金额,完成支付。

业务流程

本章节实现C扫B输入金额支付,业务流程如下:

1、商户点击组织管理-》门店管理-》打开门店列表页面

2、选择应用

3、点击指定门店的生成二维码按钮

4、顾客扫描生成的二维码,进入支付页面,输入金额来完成支付

需求列表

根据业务流程的分析,闪聚支付平台实现C扫B输入金额支付功能,需要完成以下需求:

  1. 为门店生成统一的支付二维码,用户扫一下二维码即可使用微信支付也可使用支付宝完成支付。
  2. 闪聚支付平台与微信、支付宝对接,闪聚支付作为中介,最终的支付动作(银行交易)仍通过微信、支付宝进行。
  3. 闪聚平台作为中介调用微信、支付宝的下单接口,完成支付。

支付接口技术预研

根据前边的需求分析,最重要的是闪聚支付平台作为中介,将用户的支付请求通过接口与微信、支付宝等第三方支付渠道进行对接,完成支付通道的聚合,所以首先需要调研微信、支付宝等第三方支付渠道的对接方式。

本项目首期上线要求集成微信和支付宝,下边对微信和支付宝的支付接口进行技术预研,包括:对接的流程,接口协议、接口测试等。

参考:闪聚支付-第3章-支付宝支付接入指南 、 闪聚支付-第3章-微信支付接入指南 。

生成门店二维码

业务流程

1、商户点击组织管理-》门店管理-》打开门店列表页面

2、选择应用

3、点击指定门店的生成二维码按钮

生成二维码技术预研

ZXing是一个开源的,用Java编写的多格式的1D / 2D条码图像处理库,使用ZXing可以生成、识别QR Code(二维码)。常用的二维码处理库还有zbar,近几年已经不再更新代码,下边介绍ZXing生成二维码的方法。

(1)引入依赖

<!-- 二维码生成&识别组件 -->
<dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.3.3</version>
</dependency><dependency><groupId>com.google.zxing</groupId><artifactId>javase</artifactId><version>3.3.3</version>
</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的listList<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.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.9</version><scope>compile</scope>
</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 {@ReferenceMerchantService 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) {//获取商户idLong 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生成二维码

交易服务生成二维码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;//商户idprivate String appId;//应用idprivate Long storeId;//门店idprivate 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;@ReferenceAppService appService;@ReferenceMerchantService merchantService;/*** 生成门店二维码的url** @param qrCodeDto 支付入口(url),要携带参数,商户id、应用id、门店id、标题、内容(将传入的参数转成json,用base64编码)* @return* @throws BusinessException*/@Overridepublic 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());//订单内容//转成jsonString 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、配置参数

定义c扫b二维码的标题和内容

内容 如下:

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 {//获取商户idLong merchantId = SecurityUtil.getMerchantId();//商户信息MerchantDTO merchantDTO = merchantService.queryMerchantById(merchantId);QRCodeDto qrCodeDto = new QRCodeDto();qrCodeDto.setMerchantId(merchantId);qrCodeDto.setStoreId(storeId);qrCodeDto.setAppId(appId);//标题.用商户名称替换 %sString subjectFormat = String.format(subject, merchantDTO.getMerchantName());qrCodeDto.setSubject(subjectFormat);//内容String bodyFormat = String.format(body, merchantDTO.getMerchantName());qrCodeDto.setBody(bodyFormat);//获取二维码的URLString 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

注意:应用及门店的合法性

同样,先生成token




生成如下二维码:

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)顾客扫描二维码

2)进入订单确认页面

Freemarker技术预研

支付确认页面由服务端渲染生成,常用的技术有jsp、freemarker velocity Thymeleaf等。

项目采用freemarker模板引擎,参考 freemarker基础。

交易服务支付入口

交互流程如下:

  1. 顾客扫描二维码,请求交易服务支付入口
  2. 交易服务解析请求,生成支付确认页面
  3. 交易服务向服务响应支付确认页面
支付确认页面

1、从资料->代码拷贝pay.html、pay_error.html到交易服务工程下。

2、在交易服务接口实现工程的pom.xml中引入依赖

<!--freemarker依赖-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

3、在Nacos中配置spring-boot-freemarker.yaml,Group: COMMON_GROUP

#freemarker基本配置
spring:freemarker:charset: UTF-8request-context-attribute: rccontent-type: text/htmlsuffix: .htmlenabled: trueresources:add-mappings: false #关闭工程中默认的资源处理mvc:throw-exception-if-no-handler-found: true #出现错误时直接抛出异常


在shanjupay-transaction-service工程的bootstrap.yml引入spring-boot-freemarker.yaml

     - refresh: truedata-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 {@Overridepublic 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、测试页面渲染

1)在nacos配置网关转发到交易服务的路由

    transaction-service:path: /transaction/**stripPrefix: false

2)用chrome浏览器访问(用手机视图模式)二维码的统一入口地址。

http://127.0.0.1:56010/transaction/pay-entry/…

注意:由于没有数据,订单信息均为空。

页面完善

前边测试的支付确认页面由于没有数据所以显示为空,下边对页面进行完善。

1、页面完善需求如下:

1)进入支付入口,传入ticket,需要解析ticket得到订单信息,在支付确认页面显示。

2)解析出客户端类型,目前支付微信、支付宝两种。

解析ticket

拷贝资料–>代码下的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.htmlreturn "forward:/pay-page?" + params;case WECHAT://转发到确认页面,获取授权码(待实现)return "forward:/pay-page?" + params;default:}//不支持客户端类型,转发到错误页面return "forward:/pay-page-error";
}

在WebMvcConfig类中添加如下:

接口测试

由于添加上了客户端类型的解析,使用微信或支付宝扫码方可进入支付入口,需要使用模拟器进行测试。

1、修改支付入口地址

修改为模拟器可以访问到的地址,模拟器安装在开发电脑上,支付入口地址修改为开发电脑局域网的地址。


2、生成门店二维码

3、使用模拟器运行支付宝沙箱APP,扫描二维码查看支付确认页面

立即支付

需求分析

顾客扫码进入支付确认页面,输入金额,点击立即支付,打开支付客户端(微信或支付宝),输入支付密码完成支付。

立即支付需要调用第三方支付渠道的统一下单接口,本章节完成支付宝统一下单接口对接。

交互流程如下:

支付渠道代理服务介绍:

有支付需求的微服务统一通过支付渠道代理服务调用“第三方支付服务”提供的接口,这样做的好处由支付渠道代理服务将第三方支付系统和闪聚支付内部服务进行解耦合。

整体执行流程如下:

  1. 顾客输入金额,点击立即支付
  2. 请求交易服务,交易服务保存订单
  3. 交易服务调用支付渠道代理服务的支付宝下单接口
  4. 支付渠道代理服务调用支付宝的统一下单接口。
  5. 支付凭证返回

搭建支付渠道代理工程

支付渠道代理服务包括如下工程:

服务名 职责
支付渠道代理服务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

5、启动PaymentAgentBootstrap测试:

支付渠道代理服务支付宝下单

接口定义
支付宝接口参数

支付渠道代理服务调用支付宝手机网站下单接口,下边梳理接口的参数:

公共参数如下:

标记蓝色的由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*/@Overridepublic PaymentResponseDTO createPayOrderByAliWAP(AliConfigParam aliConfigParam, AlipayBean alipayBean) throws BusinessException {log.info("支付宝请求参数", alipayBean.toString());//支付宝渠道参数String url = aliConfigParam.getUrl();//支付宝接口网关地址,下单接口地址String appId = aliConfigParam.getAppId();//支付宝应用idString rsaPrivateKey = aliConfigParam.getRsaPrivateKey();//应用私钥String format = aliConfigParam.getFormat();//数据格式jsonString charest = aliConfigParam.getCharest();//字符编码String alipayPublicKey = aliConfigParam.getAlipayPublicKey();//支付宝公钥String signtype = aliConfigParam.getSigntype();//签名算法类型String returnUrl = aliConfigParam.getReturnUrl();//支付成功跳转的urlString notifyUrl = aliConfigParam.getNotifyUrl();//支付结果异步通知的url//构造sdk的客户端对象,支付宝sdk客户端AlipayClient alipayClient = new DefaultAlipayClient(url, appId, rsaPrivateKey, format, charest, alipayPublicKey, signtype); //获得初始化的AlipayClient//封装请求支付信息AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的requestAlipayTradeWapPayModel 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_WAYmodel.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; //应用idprivate String tradeNo;//交易单号private String openId;//微信openidprivate String storeId;//门店idprivate 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 payChannelPayChannelParamDTO 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 alipayBeanPaymentResponseDTO 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);//应用idString appId = payOrderDTO.getAppId();//获取下单应用信息AppDTO app = appService.getAppById(appId);//设置所属商户payOrderDTO.setMerchantId(app.getMerchantId());//商户id//将前端输入的元转成分payOrderDTO.setTotalAmount(Integer.parseInt(AmountUtil.changeY2F(orderConfirmVO.getTotalAmount().toString())));//客户端ippayOrderDTO.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、实现第三方支付系统支付结果查询接口

完成支付后第三方支付系统提供两种方式获取支付结果,如下图:

1)第三方支付系统主动通知闪聚支付平台支付结果。

2)闪聚支付平台主动从第三方支付系统查询支付结果。

以上两种方法在实际项目中可以都用,其中第二种是必须用的,因为第一种是由第三方支付系统主动通知闪聚支付平台,当调用闪聚平台接口无法通信达到一定的次数后第三方支付系统将不再通知。

本项目支付渠道代理服务集成第二种方法完成支付结果的查询。

3、下单成功延迟向第三方支付系统查询支付结果

在调用第三方支付下单接口之后此时用户正在支付中,所以需要延迟一定的时间再去查询支付结果。

如果查询支付结果还没有支付再继续等待一定的时间再去查询,当达到订单的有效期还没有支付则不再查询。

RocketMQ技术预研

参考: RocketMQ研究 。

技术方案

项目使用消息队列RocketMQ完成支付渠道代理服务与交易服务之间的通信,如下图:

  1. 支付渠道代理服务调用第三方支付下单接口。(此时顾客开始输入密码进行支付)
  2. 支付渠道代理向消息队列发送一条延迟消息(查询支付结果),消费方仍是支付渠道代理服务。
  3. 支付渠道代理接收消息,调用支付宝接口查询支付结果
  4. 支付渠道代理查询到支付结果,将支付结果发送至MQ,消费方是交易服务。
  5. 交易服务接收到支付结果消息,更新订单状态。

支付渠道代理查询支付宝交易状态

支付宝交易状态查询接口

参考手机网支付产品介绍文档(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判断具体的支付状态

交易状态如下:

  • WAIT_BUYER_PAY(交易创建,等待买家付款)
  • TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)
  • TRADE_SUCCESS(交易支付成功)
  • TRADE_FINISHED(交易结束,不可退款)
支付渠道代理接口定义
接口定义

接口描述:

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();//支付宝应用idString rsaPrivateKey = aliConfigParam.getRsaPrivateKey();//应用私钥String format = aliConfigParam.getFormat();//json格式String charest = aliConfigParam.getCharest();//编码String alipayPublicKey = aliConfigParam.getAlipayPublicKey();//支付宝公钥String signtype = aliConfigParam.getSigntype();//签名算法String returnUrl = aliConfigParam.getReturnUrl();//支付成功跳转的urlString notifyUrl = aliConfigParam.getNotifyUrl();//支付结果异步通知的urllog.info("C扫B请求支付宝查询订单,参数:{}", JSON.toJSONString(aliConfigParam));//构造sdk的客户端对象AlipayClient alipayClient = new DefaultAlipayClient(url, appId, rsaPrivateKey, format, charest, alipayPublicKey, signtype); //获得初始化的AlipayClientAlipayTradeQueryRequest 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();//解析支付宝返回的状态,解析成闪聚平台的TradeStatusTradeStatus 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 tradeStatereturn 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 {@AutowiredPayChannelAgentService payChannelAgentService;@Testpublic void testqueryPayOrderByAli() {//沙箱应用APPIDString 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 outTradeNoPaymentResponseDTO paymentResponseDTO = payChannelAgentService.queryPayOrderByAli(aliConfigParam, "SJ1217987323129917440");System.out.println(paymentResponseDTO);}
}

支付结果查询

交互流程

根据技术方案的分析,交互流程如下:

  1. 支付渠道代理服务调用支付宝下单接口完成后向MQ发送“支付结果查询”消息(延迟消息),消费方为支付渠道代理服务。
  2. 支付渠道代理服务监听消息队列,接收“支付结果查询”消息。
  3. 支付渠道代理服务调用第三方支付系统的支付结果查询接口。
发送消息
配置RocketMQ

1)在支付渠道代理工程中添加RocketMQ依赖:

<dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-spring-boot-starter</artifactId><version>2.0.2</version>
</dependency>

2)在Nacos中添加spring-boot-starter-rocketmq.yaml配置,Data ID: spring-boot-starter-rocketmq.yaml,Group: COMMON_GROUP

rocketmq:nameServer: 127.0.0.1:9876producer:group: PID_PAY_PRODUCER


3)在shanjupay-payment-agent-service工程bootstrap.yml中引入此配置:

    -refresh: truedata-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";@AutowiredRocketMQTemplate 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> {@AutowiredPayChannelAgentService payChannelAgentService;@Overridepublic 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 outTradeNoresponseDTO = 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("支付状态未知,等待重试");}}
}

支付结果更新

交互流程

支付渠道代理服务查询到支付结果,将支付结果更新消息发送给交易服务,实现订单状态更新,流程如下:

  1. 支付渠道代理服务查询到支付结果
  2. 向MQ发送“支付结果更新”消息
  3. 交易服务监听“支付结果更新”消息队列
  4. 交易服务接收到“支付结果更新”消息,更新订单状态
发送消息

在支付渠道代理服务的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.rocketmq</groupId><artifactId>rocketmq-spring-boot-starter</artifactId><version>2.0.2</version>
</dependency>

2)在shanjupay-transaction-service的bootstrap.yml中引入此配置:

 - refresh: truedata-id: spring-boot-starter-rocketmq.yaml # rocketmq配置group: COMMON_GROUP # 通用配置组


3)在交易服务定义“支付结果消息”消费类。

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> {@AutowiredTransactionService transactionService;@Overridepublic 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、获取支付结果

调用微信的“支付结果查询”接口获取支付结果。

支付入口

获取openid接口

参考:闪聚支付-第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) {//闪聚平台的应用idString 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_redirecttry {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.htmlreturn "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中添加依赖:

<!--okhttp3-->
<dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId>
</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 payChannelPayChannelParamDTO 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_codeString url = String.format("%s?appid=%s&secret=%s&code=%s&grant_type=authorization_code",oauth2Token, wxConfigParam.getAppId(), wxConfigParam.getAppSecret(), code);//申请openid,请求urlRestTemplate restTemplate = new RestTemplate();ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class);//申请openid接口响应的内容,其中包括了openidString body = exchange.getBody();log.info("申请openid响应的内容:{}", body);//获取openidString 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);//闪聚平台的应用idString appId = payOrderDTO.getAppId();//接收到code授权码,申请openidString 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网页

4、在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引入依赖:

<!--微信支付SDK-->
<dependency><groupId>com.github.tedzhdz</groupId><artifactId>wxpay-sdk</artifactId><version>3.0.10</version>
</dependency>
<dependency><groupId>com.github.binarywang</groupId><artifactId>weixin-java-pay</artifactId><version>3.4.0</version>
</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());//客户端iprequestParam.put("notify_url", weChatBean.getNotifyUrl());//微信异步通知支付结果接口,暂时不用requestParam.put("trade_type", "JSAPI");//从请求中获取openidString 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);//msgpaymentResponseDTO.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页面

按照微信官方例子编写调起微信客户端支付的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);//应用idString appId = payOrderDTO.getAppId();AppDTO app = appService.getAppById(appId);//商户idpayOrderDTO.setMerchantId(app.getMerchantId());//客户端ippayOrderDTO.setClientIp(IPUtil.getIpAddr(request));//将前端输入的元转成分payOrderDTO.setTotalAmount(Integer.parseInt(AmountUtil.changeY2F(orderConfirmVO.getTotalAmount().toString())));//调用微信下单接口 submitOrderByWechatMap<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 {//微信openidString 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);//微信openidweChatBean.setOutTradeNo(payOrderDTO.getTradeNo());//闪聚平台的订单号weChatBean.setTotalFee(payOrderDTO.getTotalAmount());//金额(分)weChatBean.setSpbillCreateIp(payOrderDTO.getClientIp());//客户ipweChatBean.setBody(payOrderDTO.getBody());//订单描述weChatBean.setNotifyUrl("none");//异步接收微信通知支付结果的地址(暂时不用)String appId = payOrderDTO.getAppId();//根据应用、服务类型、支付渠道查询支付渠道参数,从数据库查询//String appId,String platformChannel,String payChannelPayChannelParamDTO payChannelParamDTO = payChannelService.queryParamByAppPlatformAndPayChannel(appId, "shanju_c2b", "WX_JSAPI");String paramJson = payChannelParamDTO.getParam();WXConfigParam wxConfigParam = JSON.parseObject(paramJson, WXConfigParam.class);//WXConfigParam wxConfigParam, WeChatBean weChatBeanMap<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(选传)。



支付结果:

  • SUCCESS—支付成功
  • REFUND—转入退款
  • NOTPAY—未支付
  • CLOSED—已关闭
  • REVOKED—已撤销(付款码支付)
  • USERPAYING–用户支付中(付款码支付)
  • PAYERROR–支付失败(其他原因,如银行返回失败)
支付渠道代理接口定义
接口定义

接口描述:

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 outTradeNoPaymentResponseDTO paymentResponseDTO = payChannelAgentService.queryPayOrderByWeChat(wxConfigParam, "SJ1218090459880816640");System.out.println(paymentResponseDTO);
}
支付查询
发送支付结果查询消息

支付渠道代理服务完成微信下单接口的调用即向MQ发送支付结果查询消息

修改支付渠道代理服务的createPayOrderByWeChatJSAPI方法:

消费支付结果查询消息

修改支付渠道代理服务的PayConsumer

if ("ALIPAY_WAP".equals(paymentResponseDTO.getMsg())) {//调用支付宝订单状态查询接口,查询支付宝支付结果//AliConfigParam aliConfigParam,String outTradeNoresponseDTO = 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、调起微信支付客户端,输入密码支付成功

观察控制台日志,支付结果是否发送至交易服务。

数据库订单状态是否正常更新。

代码仓库

闪聚支付 第3章-C扫B支付相关推荐

  1. 微信支付:支付流程分析、微信扫码支付(HttpClient)、微信支付二维码生成、检测支付状态、订单状态操作准备工作、支付信息回调、MQ处理支付回调状态、定时处理订单状态

    微信支付 微信支付开发的整体思路 生成支付二维码 查询支付状态(微信的服务器) 实现订单状态的修改.删除订单 支付状态回查->微信服务器将支付状态返回给支付微服务 MQ处理支付回调状态 Rabb ...

  2. 微信PC端扫码支付 java 模式二的扫码支付

    前言 这次分享的是java对接微信的支付接口,实现电脑端扫码支付后,跳转支付成功页面的例子.之所以分享是微信的Api太坑了.留下的文档也少,对接过程中容易出现各种各样的问题,在实现这扫码支付功能的时候 ...

  3. 微信扫码支付html,pc端微信扫码支付和支付宝在线支付

    本DEMO演示了PHP支付宝和微信扫码在线支付,支付成功后,在回调地址显示支付相关信息. 难易:中级 下载资源 下载积分: 888 积分 操作步骤: 1.修改配置文件 Application/comm ...

  4. 微信支付的两种模式,扫码支付

    微信支付 微信支付的两种模式 1:由微信生成二维码,客户扫描二维码后,确认支付,微信回调给当前系统. 2:由系统调用统一下单API,取得预支付交易信息后,根据信息生成二维码,然后后台循环查询订单API ...

  5. 监听网页微信扫码支付成功_PC网页微信扫码支付(模式二)

    WeixinWebUtil(微信请求工具类) public class WeixinWebUtil { private static Logger log = LoggerFactory.getLog ...

  6. html+css+js实现微信和支付宝扫码支付前端

    本章教程,主要利用html+css+js技术实现微信和支付宝扫码支付前端页面. 目录 一.效果图预览 (1)支付宝扫码支付 (2)微信扫码支付 二.项目部分源码文件 (1)目录结构 (2)alipay ...

  7. 移动二维码支付 推进二维码扫码智能POS

    现如今:银币.再到纸币,演变到"钱"变得越来越轻薄.在现在这个移动支付市场中,互联网技术的支撑下,刷手机乘公交.刷支付宝买菜购物.缴水电费--日常生活中使用"现金&quo ...

  8. 支付宝支付-扫码支付详解

    此项目已开源欢迎Start.PR.发起Issues一起讨论交流共同进步 https://github.com/Javen205/IJPay http://git.oschina.net/javen20 ...

  9. 聚合支付/免签支付/第四方支付/在线扫码支付/个人收款/第三方收款接口详解,能给用户提供什么价值,背景介绍

    随着互联网的发展,电子商务变成21世纪非常主流的一种经营模式,既然是线上商城必然离不开线上支付,从最早的银行卡支付.银联支付,到现在的扫码支付.一键支付.支付方式百花绽放,各大巨头纷纷推出越来越便捷的 ...

最新文章

  1. 涉及的一些操作的命令
  2. python遍历指定文件夹的所有文件_python 统计指定文件夹下所有的文件数量,BFS方式...
  3. 拉结尔6月21日服务器维护,拉结尔6月23日停服维护公告
  4. PDGAN: A Novel Poisoning Defense Method in Federated Learning Using Generative Adversarial Network笔记
  5. 【Java】GUI桌面弹球游戏
  6. MATLAB PDE工具箱电磁场仿真平行电容板及电偶极子详解
  7. Java编程:获取输入的三种方法
  8. phpstrom自己定义和原本的常用快捷键
  9. python实现微信自动回复
  10. UNIAPP中IOS和安卓应用热更新和整包更新app的方法
  11. 网络状态检测的利器 - ss命令
  12. Linux下常见音频格式之间的转换方法【转】
  13. 音视频封装格式:MPEG-PS
  14. 小学计算机教师证面试题目,2019下半年小学信息技术教师资格面试真题完整版...
  15. 纪中暑假培训 :Date:7 终章-剑之魂
  16. 2021年最新ABAQUS复合材料建模仿真与应用专题培训
  17. 台式计算机后面的端口示意图,如何识别戴尔台式 PC 上的集成视频端口
  18. 小码农也有大梦想!Java程序培训哪里好
  19. 第二十章:异步和文件I/O.(十九)
  20. ADAS推高毫米波雷达需求,上游企业如何跟进?

热门文章

  1. R语言ggplot画双坐标-柱状图、折线图
  2. Python正则表达式中使用findall函数遇到括号嵌套的小坑
  3. 【博客573】linux内核层丢包排查方法汇总
  4. 万般皆下品,惟有读书高
  5. Java面试宝典——数据库部分(六)
  6. matlab实践作业,matlab高等工程数学作业-实践报告
  7. 个人技术博客 友情链接,关注
  8. 强力卸载XCode方法
  9. C语言小游戏第二弹~1-100猜数字(无聊时候摸鱼必备)
  10. 什么才是社交APP该有的样子