feat 聚合支付处理, 增加h5嵌入式项目支持

This commit is contained in:
bootx
2024-02-10 22:50:24 +08:00
parent fcc73d680e
commit 7d65add2ca
68 changed files with 1246 additions and 305 deletions

View File

@@ -17,6 +17,9 @@ public class DaxPayDemoProperties {
/** 服务地址 */
private String serverUrl;
/** 前端地址 */
private String frontUrl;
/** 签名方式 */
private SignTypeEnum signType = SignTypeEnum.MD5;

View File

@@ -0,0 +1,62 @@
package cn.bootx.platform.daxpay.demo.controller;
import cn.bootx.platform.common.core.annotation.IgnoreAuth;
import cn.bootx.platform.common.core.rest.Res;
import cn.bootx.platform.common.core.rest.ResResult;
import cn.bootx.platform.daxpay.demo.domain.AggregatePayInfo;
import cn.bootx.platform.daxpay.demo.param.AggregateSimplePayParam;
import cn.bootx.platform.daxpay.demo.result.PayOrderResult;
import cn.bootx.platform.daxpay.demo.service.AggregateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import static org.springframework.http.HttpHeaders.USER_AGENT;
/**
* 聚合支付
* @author xxm
* @since 2024/2/9
*/
@IgnoreAuth
@Tag(name = "聚合支付")
@RestController
@RequestMapping("/demo/aggregate")
@RequiredArgsConstructor
public class AggregateController {
private final AggregateService aggregateService;
@Operation(summary = "创建聚合支付码")
@PostMapping("/createUrl")
public ResResult<String> createUrl(@RequestBody AggregateSimplePayParam param) {
return Res.ok(aggregateService.createUrl(param));
}
@Operation(summary = "获取聚合支付信息")
@GetMapping("/getInfo")
public ResResult<AggregatePayInfo> getInfo(String code){
return Res.ok(aggregateService.getInfo(code));
}
@Operation(summary = "聚合支付扫码跳转中间页")
@GetMapping("/qrPayPage/{code}")
public ModelAndView qrPayPage(@PathVariable String code,@RequestHeader(USER_AGENT) String ua){
String url = aggregateService.qrPayPage(code, ua);
return new ModelAndView("redirect:" + url);
}
@Operation(summary = "通过聚合支付码发起支付")
@PostMapping("/qrPay")
public ResResult<PayOrderResult> qrPa(String code){
return Res.ok(aggregateService.aliQrPay(code));
}
@Operation(summary = "通过付款码发起支付")
@PostMapping("/barCodePay")
public ResResult<Void> barCodePay(@RequestBody AggregateSimplePayParam param){
return Res.ok();
}
}

View File

@@ -1,5 +1,6 @@
package cn.bootx.platform.daxpay.demo.controller;
import cn.bootx.platform.common.core.annotation.IgnoreAuth;
import cn.bootx.platform.common.core.rest.Res;
import cn.bootx.platform.common.core.rest.ResResult;
import cn.bootx.platform.common.core.util.ValidationUtil;
@@ -16,11 +17,12 @@ import org.springframework.web.bind.annotation.*;
* @author xxm
* @since 2024/2/8
*/
@IgnoreAuth
@Tag(name = "结算台演示")
@RestController
@RequestMapping("/demo/cashier")
@RequiredArgsConstructor
public class DaxPayCashierController {
public class CashierController {
private final DaxPayCashierService cashierService;

View File

@@ -0,0 +1,23 @@
package cn.bootx.platform.daxpay.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 嵌入式h5项目转发控制器, 不用输入 index.html也可以正常访问
* @author xxm
* @since 2024/2/10
*/
@Controller
@RequestMapping
public class RedirectH5Controller {
/**
* 将/h5/*的访问请求代理到/h5/index.html*
*/
@GetMapping("/h5/")
public String toH5(){
return "forward:/h5/index.html";
}
}

View File

@@ -0,0 +1,27 @@
package cn.bootx.platform.daxpay.demo.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 聚合支付发起信息
* @author xxm
* @since 2024/2/9
*/
@Data
@Accessors(chain = true)
@Schema(title = "聚合支付发起信息")
public class AggregatePayInfo {
/** 标题 */
@Schema(description = "标题")
private String title;
/** 订单业务号 */
@Schema(description = "订单业务号")
private String businessNo;
/** 支付金额 */
@Schema(description = "支付金额")
private Integer amount;
}

View File

@@ -0,0 +1,33 @@
package cn.bootx.platform.daxpay.demo.param;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 聚合简单支付
* @author xxm
* @since 2024/2/9
*/
@Data
@Accessors(chain = true)
@Schema(title = "聚合简单支付")
public class AggregateSimplePayParam {
@Schema(description = "业务号")
@NotNull
private String businessNo;
@Schema(description = "标题")
@NotNull
private String title;
@Schema(description = "金额")
@NotNull
private BigDecimal amount;
@Schema(description = "付款码")
private String authCode;
}

View File

@@ -0,0 +1,29 @@
package cn.bootx.platform.daxpay.demo.result;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 微信Jsapi预支付签名返回信息
* @author xxm
* @since 2024/2/10
*/
@Data
@Accessors(chain = true)
@Schema(title = "微信Jsapi预支付签名返回信息")
public class WxJsapiSignResult {
/** 公众号ID */
private String appId;
/** 时间戳(秒) */
private String timeStamp;
/** 随机串 */
private String nonceStr;
/** 预支付ID, 已经是 prepay_id=xxx 格式 */
private String prePayId;
/** 微信签名方式 */
private String signType;
/** 微信签名 */
private String paySign;
}

View File

@@ -0,0 +1,258 @@
package cn.bootx.platform.daxpay.demo.service;
import cn.bootx.platform.common.core.exception.BizException;
import cn.bootx.platform.common.redis.RedisClient;
import cn.bootx.platform.common.spring.util.WebServletUtil;
import cn.bootx.platform.daxpay.demo.config.DaxPayDemoProperties;
import cn.bootx.platform.daxpay.demo.domain.AggregatePayInfo;
import cn.bootx.platform.daxpay.demo.param.AggregateSimplePayParam;
import cn.bootx.platform.daxpay.demo.result.PayOrderResult;
import cn.bootx.platform.daxpay.demo.result.WxJsapiSignResult;
import cn.bootx.platform.daxpay.sdk.code.AggregatePayEnum;
import cn.bootx.platform.daxpay.sdk.code.PayChannelEnum;
import cn.bootx.platform.daxpay.sdk.code.PayWayEnum;
import cn.bootx.platform.daxpay.sdk.model.assist.WxAccessTokenModel;
import cn.bootx.platform.daxpay.sdk.model.assist.WxAuthUrlModel;
import cn.bootx.platform.daxpay.sdk.model.assist.WxJsapiSignModel;
import cn.bootx.platform.daxpay.sdk.model.pay.PayOrderModel;
import cn.bootx.platform.daxpay.sdk.net.DaxPayKit;
import cn.bootx.platform.daxpay.sdk.param.assist.WxAccessTokenParam;
import cn.bootx.platform.daxpay.sdk.param.assist.WxAuthUrlParam;
import cn.bootx.platform.daxpay.sdk.param.assist.WxJsapiSignParam;
import cn.bootx.platform.daxpay.sdk.param.channel.WeChatPayParam;
import cn.bootx.platform.daxpay.sdk.param.pay.SimplePayParam;
import cn.bootx.platform.daxpay.sdk.response.DaxPayResult;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Optional;
/**
* 聚合支付
* @author xxm
* @since 2024/2/9
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AggregateService {
private final DaxPayDemoProperties daxPayDemoProperties;
private final RedisClient redisClient;
private final String PREFIX_KEY = "payment:aggregate:";
/**
* 创建聚合支付码连接
*/
public String createUrl(AggregateSimplePayParam param) {
int amount = param.getAmount()
.multiply(BigDecimal.valueOf(100))
.intValue();
AggregatePayInfo aggregatePayInfo = new AggregatePayInfo()
.setTitle(param.getTitle())
.setBusinessNo(param.getBusinessNo())
.setAmount(amount);
String code = IdUtil.getSnowflakeNextIdStr();
String serverUrl = daxPayDemoProperties.getServerUrl();
// 有效期五分钟
redisClient.setWithTimeout(PREFIX_KEY + code, JSONUtil.toJsonStr(aggregatePayInfo), 5 * 60 * 1000);
return StrUtil.format("{}/demo/aggregate/qrPayPage/{}", serverUrl,code);
}
/**
* 获取聚合支付信息
*/
public AggregatePayInfo getInfo(String key) {
String jsonStr = Optional.ofNullable(redisClient.get(PREFIX_KEY + key))
.orElseThrow(() -> new BizException("支付超时"));
return JSONUtil.toBean(jsonStr, AggregatePayInfo.class);
}
/**
* 聚合支付扫码跳转中间页
*/
public String qrPayPage(String code, String ua){
// 判断是否过期
boolean exists = redisClient.exists(PREFIX_KEY + code);
if (!exists){
// 跳转到过期页面
return null;
}
// 根据UA判断是什么环境
if (ua.contains(AggregatePayEnum.UA_ALI_PAY.getCode())) {
// 跳转支付宝中间页
return StrUtil.format("{}/#/cashier/alipay", daxPayDemoProperties.getFrontUrl());
}
else if (ua.contains(AggregatePayEnum.UA_WECHAT_PAY.getCode())) {
// 微信重定向到中间页, 因为微信需要授权后才能发起支付
return this.wxJsapiAuth(code);
}
else {
// 跳转到异常页
return null;
}
}
/**
* 微信Jsapi发起支付, 返回预支付信息, 从页面调起支付, 分为下面三个步骤:
* 1. 获取微信OpenId
* 2. 调用支付接口拿到预支付ID
* 3. 对预支付ID签名, 拿到页面调起支付参数并返回
*/
public WxJsapiSignResult wxJsapiPau(String aggregateCode, String authCode) {
// 1. 获取微信OpenId
String openId = this.getOpenId(authCode);
// 2. 发起预支付
PayOrderModel payOrderModel = this.wxJsapiPrePay(aggregateCode, openId);
// 3. 预付订单再次签名, 调用起页面的支付弹窗
WxJsapiSignParam wxJsapiSignParam = new WxJsapiSignParam();
wxJsapiSignParam.setPrepayId(payOrderModel.getPayBody());
DaxPayResult<WxJsapiSignModel> execute = DaxPayKit.execute(wxJsapiSignParam);
// 判断是否支付成功
if (execute.getCode() != 0){
throw new BizException(execute.getMsg());
}
WxJsapiSignResult wxJsapiSignResult = new WxJsapiSignResult();
BeanUtil.copyProperties(execute.getData(), wxJsapiSignResult);
return wxJsapiSignResult;
}
/**
* 支付宝发起支付
*/
public PayOrderResult aliQrPay(String code) {
AggregatePayInfo aggregatePayInfo = getInfo(code);
SimplePayParam simplePayParam = new SimplePayParam();
simplePayParam.setBusinessNo(aggregatePayInfo.getBusinessNo());
simplePayParam.setTitle(aggregatePayInfo.getTitle());
simplePayParam.setAmount(aggregatePayInfo.getAmount());
simplePayParam.setChannel(PayChannelEnum.ALI.getCode());
simplePayParam.setPayWay(PayWayEnum.QRCODE.getCode());
String ip = Optional.ofNullable(WebServletUtil.getRequest())
.map(ServletUtil::getClientIP)
.orElse("127.0.0.1");
simplePayParam.setClientIp(ip);
// 异步回调地址
simplePayParam.setNotifyUrl("http://localhost:9000");
// 同步回调地址
simplePayParam.setReturnUrl("http://localhost:9000");
DaxPayResult<PayOrderModel> execute = DaxPayKit.execute(simplePayParam);
// 判断是否支付成功
if (execute.getCode() != 0){
throw new BizException(execute.getMsg());
}
PayOrderResult payOrderResult = new PayOrderResult();
BeanUtil.copyProperties(execute.getData(),payOrderResult);
return payOrderResult;
}
/**
* 根据付款码判断属于那种支付通道
*/
public PayChannelEnum getPayChannel(String authCode) {
if (StrUtil.isBlank(authCode)) {
throw new BizException("付款码不可为空");
}
String[] wx = { "10", "11", "12", "13", "14", "15" };
String[] ali = { "25", "26", "27", "28", "29", "30" };
// 微信
if (StrUtil.startWithAny(authCode.substring(0, 2), wx)) {
return PayChannelEnum.WECHAT;
}
// 支付宝
else if (StrUtil.startWithAny(authCode.substring(0, 2), ali)) {
return PayChannelEnum.ALI;
}
else {
throw new BizException("不支持的支付方式");
}
}
/**
* 微信jsapi支付 - 跳转到授权页面
*/
private String wxJsapiAuth(String code) {
// 回调地址为 结算台微信jsapi支付的回调地址
WxAuthUrlParam wxAuthUrlParam = new WxAuthUrlParam();
wxAuthUrlParam.setState(code);
String url = StrUtil.format("{}/#/cashier/wxJsapiPay", daxPayDemoProperties.getFrontUrl());
wxAuthUrlParam.setUrl(url);
DaxPayResult<WxAuthUrlModel> execute = DaxPayKit.execute(wxAuthUrlParam);
if (execute.getCode() != 0){
throw new BizException(execute.getMsg());
}
return execute.getData().getUrl();
}
/**
* 获取微信OpenId
*/
private String getOpenId(String authCode) {
// 获取OpenId
WxAccessTokenParam param = new WxAccessTokenParam();
param.setCode(authCode);
DaxPayResult<WxAccessTokenModel> result = DaxPayKit.execute(param);
// 判断是否支付成功
if (result.getCode() != 0){
throw new BizException(result.getMsg());
}
return result.getData().getOpenId();
}
/**
* 调用微信支付, 拿到预支付ID
*/
private PayOrderModel wxJsapiPrePay(String aggregateCode, String openId) {
AggregatePayInfo aggregatePayInfo = getInfo(aggregateCode);
// 拼装支付发起参数
SimplePayParam simplePayParam = new SimplePayParam();
simplePayParam.setBusinessNo(aggregatePayInfo.getBusinessNo());
simplePayParam.setTitle(aggregatePayInfo.getTitle());
simplePayParam.setAmount(aggregatePayInfo.getAmount());
simplePayParam.setChannel(PayChannelEnum.ALI.getCode());
simplePayParam.setPayWay(PayWayEnum.QRCODE.getCode());
// 设置微信专属请求参数
WeChatPayParam weChatPayParam = new WeChatPayParam();
weChatPayParam.setOpenId(openId);
simplePayParam.setChannelParam(weChatPayParam);
String ip = Optional.ofNullable(WebServletUtil.getRequest())
.map(ServletUtil::getClientIP)
.orElse("127.0.0.1");
simplePayParam.setClientIp(ip);
// 异步回调地址
simplePayParam.setNotifyUrl("http://localhost:9000");
// 同步回调地址
simplePayParam.setReturnUrl("http://localhost:9000");
DaxPayResult<PayOrderModel> execute = DaxPayKit.execute(simplePayParam);
// 判断是否支付成功
if (execute.getCode() != 0){
throw new BizException(execute.getMsg());
}
return execute.getData();
}
}