mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-08 14:15:34 +08:00
feat(payment): 新增网关支付(聚合扫码)领域与统一处理回写
落地 GatewayPayOrder 实体/管理端与 Uni 接口、商户聚合配置、超时 MQ/Job,并按 trade_type 回写普通/网关业务容器;补充收银台场景与网关相关枚举 i18n。
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package cn.daxpay.open.payment.common.enums;
|
||||
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.daxpay.open.platform.core.i18n.I18nSupport;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/// # 收银场景(聚合扫码环境识别)
|
||||
///
|
||||
/// 字典: cashier_scene
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum CashierSceneEnum implements I18nSupport {
|
||||
|
||||
/// 微信环境
|
||||
WECHAT_PAY("wechat_pay"),
|
||||
/// 支付宝环境
|
||||
ALIPAY("alipay"),
|
||||
/// 云闪付环境
|
||||
UNION_PAY("union_pay"),
|
||||
;
|
||||
|
||||
private final String code;
|
||||
|
||||
@Override
|
||||
public String getI18nPrefix() {
|
||||
return "enum.cashier_scene";
|
||||
}
|
||||
|
||||
public static CashierSceneEnum findByCode(String code) {
|
||||
return Arrays.stream(values())
|
||||
.filter(e -> e.getCode().equals(code))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new DataNotExistException("error.common.cashierSceneNotExist", code));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.daxpay.open.payment.common.enums;
|
||||
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.daxpay.open.platform.core.i18n.I18nSupport;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/// # 网关支付业务单状态
|
||||
///
|
||||
/// 字典: gateway_order_status
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum GatewayOrderStatusEnum implements I18nSupport {
|
||||
|
||||
/// 待支付(已预下单, 未建 Trade)
|
||||
WAIT_PAY("wait_pay"),
|
||||
/// 支付中(已建 Trade, 通道处理中)
|
||||
PAYING("paying"),
|
||||
/// 已支付
|
||||
PAID("paid"),
|
||||
/// 已关闭
|
||||
CLOSED("closed"),
|
||||
/// 已过期
|
||||
EXPIRED("expired"),
|
||||
;
|
||||
|
||||
private final String code;
|
||||
|
||||
@Override
|
||||
public String getI18nPrefix() {
|
||||
return "enum.gateway_order_status";
|
||||
}
|
||||
|
||||
public static GatewayOrderStatusEnum findByCode(String code) {
|
||||
return Arrays.stream(values())
|
||||
.filter(e -> e.getCode().equals(code))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new DataNotExistException("error.common.gatewayOrderStatusNotExist", code));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.daxpay.open.payment.common.enums;
|
||||
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.daxpay.open.platform.core.i18n.I18nSupport;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/// # 网关支付类型
|
||||
///
|
||||
/// 字典: gateway_pay_type
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum GatewayPayTypeEnum implements I18nSupport {
|
||||
|
||||
/// 统一收银台(设备自适应, 本期仅预留下单类型)
|
||||
CASHIER("cashier"),
|
||||
/// 聚合扫码一码多付
|
||||
AGGREGATE("aggregate"),
|
||||
;
|
||||
|
||||
private final String code;
|
||||
|
||||
@Override
|
||||
public String getI18nPrefix() {
|
||||
return "enum.gateway_pay_type";
|
||||
}
|
||||
|
||||
public static GatewayPayTypeEnum findByCode(String code) {
|
||||
return Arrays.stream(values())
|
||||
.filter(e -> e.getCode().equals(code))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new DataNotExistException("error.common.gatewayPayTypeNotExist", code));
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ public enum PayTradeTypeEnum implements I18nSupport {
|
||||
|
||||
/// 普通支付
|
||||
NORMAL("normal"),
|
||||
/// 网关支付(聚合扫码/收银台)
|
||||
GATEWAY("gateway"),
|
||||
/// 预授权冻结
|
||||
AUTHORIZE("authorize"),
|
||||
/// 预授权捕获扣款
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package cn.daxpay.open.payment.core.controller.admin;
|
||||
|
||||
import cn.daxpay.open.payment.core.trade.param.GatewayPayOrderQuery;
|
||||
import cn.daxpay.open.payment.core.trade.result.GatewayPayOrderResult;
|
||||
import cn.daxpay.open.payment.core.trade.service.admin.GatewayPayOrderAdminService;
|
||||
import cn.daxpay.open.payment.unipay.result.trade.pay.NormalPaySyncResult;
|
||||
import cn.daxpay.open.platform.core.annotation.PermCode;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.param.PageParam;
|
||||
import cn.daxpay.open.platform.core.rest.result.PageResult;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/// # 网关支付业务单(管理)
|
||||
@PermCode(menuCode = "payment:gateway-order")
|
||||
@Validated
|
||||
@Tag(name = "网关支付业务单(管理)")
|
||||
@RestController
|
||||
@RequestMapping("/admin/order/gateway-pay")
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayPayOrderAdminController {
|
||||
|
||||
private final GatewayPayOrderAdminService gatewayPayOrderAdminService;
|
||||
|
||||
@PermCode(code = "view", nameCn = "订单查看", nameEn = "Order View")
|
||||
@Operation(summary = "网关支付业务单分页")
|
||||
@GetMapping("/page")
|
||||
public Result<PageResult<GatewayPayOrderResult>> page(PageParam pageParam, GatewayPayOrderQuery query) {
|
||||
return Res.ok(gatewayPayOrderAdminService.page(pageParam, query));
|
||||
}
|
||||
|
||||
@PermCode(code = "view", nameCn = "订单查看", nameEn = "Order View")
|
||||
@Operation(summary = "根据ID查询详情")
|
||||
@GetMapping("/get-by-id")
|
||||
public Result<GatewayPayOrderResult> findById(
|
||||
@NotNull(message = "{validation.field.id.notNull}") Long id) {
|
||||
return Res.ok(gatewayPayOrderAdminService.findById(id));
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "订单管理", nameEn = "Order Manage")
|
||||
@Operation(summary = "同步支付状态")
|
||||
@PostMapping("/sync")
|
||||
public Result<NormalPaySyncResult> sync(
|
||||
@NotNull(message = "{validation.field.id.notNull}") Long id) {
|
||||
return Res.ok(gatewayPayOrderAdminService.sync(id));
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "订单管理", nameEn = "Order Manage")
|
||||
@Operation(summary = "关闭订单")
|
||||
@PostMapping("/close")
|
||||
public Result<Void> close(
|
||||
@NotNull(message = "{validation.field.id.notNull}") Long id,
|
||||
@RequestParam(defaultValue = "false") boolean useCancel) {
|
||||
gatewayPayOrderAdminService.close(id, useCancel);
|
||||
return Res.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.daxpay.open.payment.core.trade.convert;
|
||||
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.result.GatewayPayOrderResult;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/// # 网关支付业务单转换
|
||||
@Mapper
|
||||
public interface GatewayPayOrderConvert {
|
||||
|
||||
GatewayPayOrderConvert CONVERT = Mappers.getMapper(GatewayPayOrderConvert.class);
|
||||
|
||||
GatewayPayOrderResult toResult(GatewayPayOrder entity);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cn.daxpay.open.payment.core.trade.dao;
|
||||
|
||||
import cn.daxpay.open.payment.common.enums.GatewayOrderStatusEnum;
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.param.GatewayPayOrderQuery;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.query.generator.QueryGenerator;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil;
|
||||
import cn.daxpay.open.platform.core.annotation.IgnoreTenant;
|
||||
import cn.daxpay.open.platform.core.rest.param.PageParam;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/// # 网关支付业务单管理器
|
||||
@Repository
|
||||
public class GatewayPayOrderManager extends BaseManager<GatewayPayOrderMapper, GatewayPayOrder> {
|
||||
|
||||
/// 按平台网关单号查询
|
||||
public Optional<GatewayPayOrder> findByOrderNo(String orderNo) {
|
||||
return lambdaQuery()
|
||||
.eq(GatewayPayOrder::getOrderNo, orderNo)
|
||||
.oneOpt();
|
||||
}
|
||||
|
||||
/// 按平台网关单号查询(忽略租户, H5 落地页用)
|
||||
@IgnoreTenant
|
||||
public Optional<GatewayPayOrder> findByOrderNoNotTenant(String orderNo) {
|
||||
return lambdaQuery()
|
||||
.eq(GatewayPayOrder::getOrderNo, orderNo)
|
||||
.oneOpt();
|
||||
}
|
||||
|
||||
/// 按商户业务单号 + 应用号查询
|
||||
public Optional<GatewayPayOrder> findByBizOrderNo(String bizOrderNo, String appId) {
|
||||
return lambdaQuery()
|
||||
.eq(GatewayPayOrder::getBizOrderNo, bizOrderNo)
|
||||
.eq(GatewayPayOrder::getAppId, appId)
|
||||
.oneOpt();
|
||||
}
|
||||
|
||||
/// 分页查询
|
||||
public Page<GatewayPayOrder> page(PageParam pageParam, GatewayPayOrderQuery query) {
|
||||
Page<GatewayPayOrder> mpPage = MpUtil.getMpPage(pageParam);
|
||||
QueryWrapper<GatewayPayOrder> wrapper = QueryGenerator.generator(query);
|
||||
wrapper.orderByDesc("create_time");
|
||||
return this.page(mpPage, wrapper);
|
||||
}
|
||||
|
||||
/// 已过期仍待支付/支付中的网关单(超时兜底)
|
||||
@IgnoreTenant
|
||||
public List<GatewayPayOrder> findTimeoutOrders(OffsetDateTime now) {
|
||||
return lambdaQuery()
|
||||
.in(GatewayPayOrder::getStatus,
|
||||
GatewayOrderStatusEnum.WAIT_PAY.getCode(),
|
||||
GatewayOrderStatusEnum.PAYING.getCode())
|
||||
.lt(GatewayPayOrder::getExpiredTime, now)
|
||||
.orderByAsc(GatewayPayOrder::getExpiredTime)
|
||||
.last("limit 500")
|
||||
.list();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package cn.daxpay.open.payment.core.trade.dao;
|
||||
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # 网关支付业务单 Mapper
|
||||
@Mapper
|
||||
public interface GatewayPayOrderMapper extends MPJBaseMapper<GatewayPayOrder> {
|
||||
}
|
||||
@@ -49,6 +49,26 @@ public class PayTradeManager extends BaseManager<PayTradeMapper, PayTrade> {
|
||||
.oneOpt();
|
||||
}
|
||||
|
||||
/// 根据容器ID + 交易形态查询
|
||||
public Optional<PayTrade> findByContainerId(Long containerId, String tradeType) {
|
||||
return lambdaQuery()
|
||||
.eq(PayTrade::getContainerId, containerId)
|
||||
.eq(PayTrade::getTradeType, tradeType)
|
||||
.oneOpt();
|
||||
}
|
||||
|
||||
/// 查询网关支付已超时但仍处理中的资金交易(兜底)
|
||||
@IgnoreTenant
|
||||
public List<PayTrade> findGatewayTimeoutTrades(OffsetDateTime now) {
|
||||
return lambdaQuery()
|
||||
.eq(PayTrade::getTradeType, PayTradeTypeEnum.GATEWAY.getCode())
|
||||
.eq(PayTrade::getStatus, PayFundStatusEnum.PROCESSING.getCode())
|
||||
.lt(PayTrade::getExpiredTime, now)
|
||||
.orderByAsc(PayTrade::getExpiredTime)
|
||||
.last("limit 500")
|
||||
.list();
|
||||
}
|
||||
|
||||
/// 分页查询(管理端), 默认按创建时间倒序
|
||||
public Page<PayTrade> page(PageParam pageParam, PayTradeQuery query) {
|
||||
Page<PayTrade> mpPage = MpUtil.getMpPage(pageParam);
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package cn.daxpay.open.payment.core.trade.entity;
|
||||
|
||||
import cn.daxpay.open.payment.common.entity.merchant.MchBaseEntity;
|
||||
import cn.daxpay.open.payment.common.enums.GatewayOrderStatusEnum;
|
||||
import cn.daxpay.open.payment.common.enums.GatewayPayTypeEnum;
|
||||
import cn.daxpay.open.payment.unipay.param.trade.pay.GoodsDetail;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.CurrencyEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.PayMethodEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/// # 网关支付业务单容器
|
||||
///
|
||||
/// 聚合扫码/收银台预下单场景: 创建时不知具体通道, 仅承载收款意图。
|
||||
/// 用户真正支付时再创建 pay_trade(trade_type=gateway) 并回填 channel/product/method。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "pay_gateway_order", autoResultMap = true)
|
||||
public class GatewayPayOrder extends MchBaseEntity {
|
||||
|
||||
/// 应用号
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private String appId;
|
||||
|
||||
/// 平台网关单号(URL 用)
|
||||
private String orderNo;
|
||||
|
||||
/// 商户业务单号
|
||||
private String bizOrderNo;
|
||||
|
||||
/// 网关类型
|
||||
/// @see GatewayPayTypeEnum
|
||||
private String gatewayType;
|
||||
|
||||
/// 标题
|
||||
private String title;
|
||||
|
||||
/// 描述
|
||||
private String description;
|
||||
|
||||
/// 业务状态
|
||||
/// @see GatewayOrderStatusEnum
|
||||
private String status;
|
||||
|
||||
/// 异步通知地址
|
||||
private String notifyUrl;
|
||||
|
||||
/// 同步跳转地址
|
||||
private String returnUrl;
|
||||
|
||||
/// 商户附加参数
|
||||
private String attach;
|
||||
|
||||
/// 过期时间
|
||||
private OffsetDateTime expiredTime;
|
||||
|
||||
/// 金额(最小货币单位)
|
||||
private Long amount;
|
||||
|
||||
/// 币种
|
||||
/// @see CurrencyEnum
|
||||
private String currency;
|
||||
|
||||
/// 支付通道(支付后冗余)
|
||||
private String channel;
|
||||
|
||||
/// 支付方式
|
||||
/// @see PayMethodEnum
|
||||
private String method;
|
||||
|
||||
/// 支付产品
|
||||
/// @see ProductEnum
|
||||
private String product;
|
||||
|
||||
/// 支付能力(路由回填)
|
||||
private String capability;
|
||||
|
||||
/// 通道商户号(路由回填)
|
||||
private String channelMchNo;
|
||||
|
||||
/// 收银场景 wechat_pay/alipay/union_pay
|
||||
private String scene;
|
||||
|
||||
/// 最后发起设备 mobile/pc
|
||||
private String device;
|
||||
|
||||
/// 支付成功时间
|
||||
private OffsetDateTime payTime;
|
||||
|
||||
/// 关闭时间
|
||||
private OffsetDateTime closeTime;
|
||||
|
||||
/// 客户端 IP
|
||||
private String clientIp;
|
||||
|
||||
/// 终端设备编码
|
||||
private String terminalNo;
|
||||
|
||||
/// 商品明细
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<GoodsDetail> goodsDetail;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.daxpay.open.payment.core.trade.job;
|
||||
|
||||
import cn.daxpay.open.payment.core.trade.dao.GatewayPayOrderManager;
|
||||
import cn.daxpay.open.payment.core.trade.dao.PayTradeManager;
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayTrade;
|
||||
import cn.daxpay.open.payment.core.trade.service.GatewayTimeoutService;
|
||||
import cn.daxpay.open.payment.core.trade.service.PayCloseService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
/// # 网关支付超时关单兜底定时任务
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayTimeoutJob {
|
||||
|
||||
private final GatewayPayOrderManager gatewayPayOrderManager;
|
||||
private final PayTradeManager payTradeManager;
|
||||
private final GatewayTimeoutService gatewayTimeoutService;
|
||||
private final PayCloseService payCloseService;
|
||||
|
||||
@Scheduled(fixedDelay = 300_000L)
|
||||
public void scanTimeoutOrders() {
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
// 1. 容器层超时(含尚未建 Trade 的 wait_pay)
|
||||
List<GatewayPayOrder> timeoutOrders = gatewayPayOrderManager.findTimeoutOrders(now);
|
||||
if (!timeoutOrders.isEmpty()) {
|
||||
log.info("网关超时关单兜底扫描命中容器 {} 笔", timeoutOrders.size());
|
||||
for (GatewayPayOrder order : timeoutOrders) {
|
||||
try {
|
||||
gatewayTimeoutService.closeForTimeout(order.getOrderNo());
|
||||
} catch (Exception e) {
|
||||
log.error("网关超时关单兜底失败, orderNo={}", order.getOrderNo(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. Trade 层超时(processing)
|
||||
List<PayTrade> timeoutTrades = payTradeManager.findGatewayTimeoutTrades(now);
|
||||
if (!timeoutTrades.isEmpty()) {
|
||||
log.info("网关超时关单兜底扫描命中交易 {} 笔", timeoutTrades.size());
|
||||
for (PayTrade trade : timeoutTrades) {
|
||||
try {
|
||||
payCloseService.closeForTimeout(trade.getTradeNo());
|
||||
} catch (Exception e) {
|
||||
log.error("网关交易超时关单兜底失败, tradeNo={}", trade.getTradeNo(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.daxpay.open.payment.core.trade.mq;
|
||||
|
||||
import cn.daxpay.open.payment.core.trade.service.GatewayTimeoutService;
|
||||
import cn.daxpay.open.platform.common.json.util.JacksonUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jms.annotation.JmsListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/// # 网关支付超时关单消费者
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayTimeoutConsumer {
|
||||
|
||||
private final GatewayTimeoutService gatewayTimeoutService;
|
||||
|
||||
@JmsListener(destination = PayArtemisConstants.GATEWAY_TIMEOUT_QUEUE)
|
||||
public void onMessage(String json) {
|
||||
GatewayTimeoutMessage message;
|
||||
try {
|
||||
message = JacksonUtil.toBean(json, GatewayTimeoutMessage.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("网关超时关单消息解析失败, 丢弃: json={}, error={}", json, e.getMessage());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
gatewayTimeoutService.closeForTimeout(message.getOrderNo());
|
||||
} catch (Exception e) {
|
||||
log.error("网关超时关单处理失败, orderNo={}, bizOrderNo={}",
|
||||
message.getOrderNo(), message.getBizOrderNo(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.daxpay.open.payment.core.trade.mq;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 网关支付超时关单消息体
|
||||
///
|
||||
/// 预下单时按过期时间投递, 消费端只信任 orderNo 重新查库做幂等。
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class GatewayTimeoutMessage {
|
||||
|
||||
/// 平台网关单号
|
||||
private String orderNo;
|
||||
|
||||
/// 商户业务单号(日志追踪)
|
||||
private String bizOrderNo;
|
||||
}
|
||||
@@ -12,4 +12,8 @@ public interface PayArtemisConstants {
|
||||
/// 下单时通过 `sendDelayAt(...)` 按订单过期时间定时投递,到期后由
|
||||
/// [NormalPayTimeoutConsumer] 消费触发自动关单。
|
||||
String NORMAL_TIMEOUT_QUEUE = "pay.normal.timeout";
|
||||
|
||||
/// 网关支付超时关单队列 address
|
||||
/// 预下单时按容器过期时间投递,到期后由 [GatewayTimeoutConsumer] 消费。
|
||||
String GATEWAY_TIMEOUT_QUEUE = "pay.gateway.timeout";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package cn.daxpay.open.payment.core.trade.param;
|
||||
|
||||
import cn.daxpay.open.platform.core.annotation.QueryParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 网关支付业务单查询参数(管理)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "网关支付业务单查询参数")
|
||||
public class GatewayPayOrderQuery {
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
|
||||
@Schema(description = "商户号")
|
||||
private String mchNo;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
|
||||
@Schema(description = "应用号")
|
||||
private String appId;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.LIKE)
|
||||
@Schema(description = "平台网关单号")
|
||||
private String orderNo;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.LIKE)
|
||||
@Schema(description = "商户业务单号")
|
||||
private String bizOrderNo;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
|
||||
@Schema(description = "网关类型")
|
||||
private String gatewayType;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
|
||||
@Schema(description = "业务状态")
|
||||
private String status;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
|
||||
@Schema(description = "支付通道")
|
||||
private String channel;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
|
||||
@Schema(description = "支付产品")
|
||||
private String product;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.GE, fieldName = "create_time")
|
||||
@Schema(description = "创建时间-开始")
|
||||
private OffsetDateTime createTimeStart;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.LE, fieldName = "create_time")
|
||||
@Schema(description = "创建时间-结束")
|
||||
private OffsetDateTime createTimeEnd;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package cn.daxpay.open.payment.core.trade.result;
|
||||
|
||||
import cn.daxpay.open.platform.core.result.BaseResult;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 网关支付业务单(管理)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "网关支付业务单")
|
||||
public class GatewayPayOrderResult extends BaseResult {
|
||||
|
||||
@Schema(description = "商户号")
|
||||
private String mchNo;
|
||||
|
||||
@Schema(description = "应用号")
|
||||
private String appId;
|
||||
|
||||
@Schema(description = "平台网关单号")
|
||||
private String orderNo;
|
||||
|
||||
@Schema(description = "商户业务单号")
|
||||
private String bizOrderNo;
|
||||
|
||||
@Schema(description = "网关类型")
|
||||
private String gatewayType;
|
||||
|
||||
@Schema(description = "标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "业务状态")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "金额(分)")
|
||||
private Long amount;
|
||||
|
||||
@Schema(description = "币种")
|
||||
private String currency;
|
||||
|
||||
@Schema(description = "过期时间")
|
||||
private OffsetDateTime expiredTime;
|
||||
|
||||
@Schema(description = "支付通道")
|
||||
private String channel;
|
||||
|
||||
@Schema(description = "支付方式")
|
||||
private String method;
|
||||
|
||||
@Schema(description = "支付产品")
|
||||
private String product;
|
||||
|
||||
@Schema(description = "收银场景")
|
||||
private String scene;
|
||||
|
||||
@Schema(description = "设备")
|
||||
private String device;
|
||||
|
||||
@Schema(description = "支付成功时间")
|
||||
private OffsetDateTime payTime;
|
||||
|
||||
@Schema(description = "关闭时间")
|
||||
private OffsetDateTime closeTime;
|
||||
|
||||
@Schema(description = "异步通知地址")
|
||||
private String notifyUrl;
|
||||
|
||||
@Schema(description = "同步跳转地址")
|
||||
private String returnUrl;
|
||||
|
||||
@Schema(description = "商户附加参数")
|
||||
private String attach;
|
||||
|
||||
// ===== Trade 联表 =====
|
||||
@Schema(description = "资金交易号")
|
||||
private String tradeNo;
|
||||
|
||||
@Schema(description = "通道订单号")
|
||||
private String outOrderNo;
|
||||
|
||||
@Schema(description = "资金状态")
|
||||
private String fundStatus;
|
||||
|
||||
@Schema(description = "可退金额")
|
||||
private Long refundableBalance;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMsg;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package cn.daxpay.open.payment.core.trade.service;
|
||||
|
||||
import cn.daxpay.open.payment.common.enums.CashierSceneEnum;
|
||||
import cn.daxpay.open.payment.common.enums.GatewayPayTypeEnum;
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import cn.daxpay.open.payment.merchant.entity.gateway.GatewayAggregateConfig;
|
||||
import cn.daxpay.open.payment.merchant.service.gateway.GatewayAggregateConfigService;
|
||||
import cn.daxpay.open.payment.unipay.param.gateway.AggregateQrPayParam;
|
||||
import cn.daxpay.open.payment.unipay.result.trade.pay.NormalPayResult;
|
||||
import cn.daxpay.open.platform.common.spring.util.WebServletUtil;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 网关聚合扫码支付服务
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayAggregatePayService {
|
||||
|
||||
private final GatewayPayAssistService gatewayPayAssistService;
|
||||
private final GatewayAggregateConfigService gatewayAggregateConfigService;
|
||||
private final GatewayPayHandleService gatewayPayHandleService;
|
||||
|
||||
/// 聚合扫码发起支付
|
||||
public NormalPayResult aggregateQrPay(AggregateQrPayParam param) {
|
||||
GatewayPayOrder order = gatewayPayAssistService.getOrderAndCheck(param.getOrderNo());
|
||||
if (!Objects.equals(order.getGatewayType(), GatewayPayTypeEnum.AGGREGATE.getCode())) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.gateway.typeMismatch");
|
||||
}
|
||||
CashierSceneEnum scene = CashierSceneEnum.findByCode(param.getScene());
|
||||
GatewayAggregateConfig config = gatewayAggregateConfigService.getRequiredByAppId(order.getAppId());
|
||||
String product;
|
||||
String method;
|
||||
switch (scene) {
|
||||
case WECHAT_PAY -> {
|
||||
product = config.getWxProduct();
|
||||
method = config.getWxMethod();
|
||||
}
|
||||
case ALIPAY -> {
|
||||
product = config.getAlipayProduct();
|
||||
method = config.getAlipayMethod();
|
||||
}
|
||||
case UNION_PAY -> {
|
||||
product = config.getUnionProduct();
|
||||
method = config.getUnionMethod();
|
||||
}
|
||||
default -> throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.error.gateway.sceneNotSupport");
|
||||
}
|
||||
if (StrUtil.isBlank(product) || StrUtil.isBlank(method)) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.error.gateway.sceneNotConfigured");
|
||||
}
|
||||
String clientIp = StrUtil.blankToDefault(param.getClientIp(), WebServletUtil.getClientIp());
|
||||
return gatewayPayHandleService.handle(order, product, method,
|
||||
param.getOpenId(), scene.getCode(), param.getDevice(), clientIp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cn.daxpay.open.payment.core.trade.service;
|
||||
|
||||
import cn.daxpay.open.payment.common.enums.PayTradeTypeEnum;
|
||||
import cn.daxpay.open.payment.core.trade.dao.GatewayPayOrderManager;
|
||||
import cn.daxpay.open.payment.core.trade.dao.PayTradeManager;
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayTrade;
|
||||
import cn.daxpay.open.payment.unipay.param.gateway.GatewayOrderQueryParam;
|
||||
import cn.daxpay.open.payment.unipay.result.gateway.GatewayOrderResult;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 网关订单查询服务
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayOrderQueryService {
|
||||
|
||||
private final GatewayPayOrderManager gatewayPayOrderManager;
|
||||
private final PayTradeManager payTradeManager;
|
||||
|
||||
public GatewayOrderResult query(GatewayOrderQueryParam param) {
|
||||
if (StrUtil.isBlank(param.getOrderNo()) && StrUtil.isBlank(param.getBizOrderNo())) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.orderNoRequired");
|
||||
}
|
||||
GatewayPayOrder order = null;
|
||||
if (StrUtil.isNotBlank(param.getOrderNo())) {
|
||||
order = gatewayPayOrderManager.findByOrderNo(param.getOrderNo()).orElse(null);
|
||||
}
|
||||
if (order == null && StrUtil.isNotBlank(param.getBizOrderNo())) {
|
||||
if (StrUtil.isBlank(param.getAppId())) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "validation.field.appId.notBlank");
|
||||
}
|
||||
order = gatewayPayOrderManager.findByBizOrderNo(param.getBizOrderNo(), param.getAppId()).orElse(null);
|
||||
}
|
||||
if (order == null) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.payOrderNotExist");
|
||||
}
|
||||
return this.toResult(order);
|
||||
}
|
||||
|
||||
/// H5 侧按 orderNo 查(忽略租户)
|
||||
public GatewayOrderResult queryByOrderNoNotTenant(String orderNo) {
|
||||
GatewayPayOrder order = gatewayPayOrderManager.findByOrderNoNotTenant(orderNo)
|
||||
.orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.error.payOrderNotExist"));
|
||||
return this.toResult(order);
|
||||
}
|
||||
|
||||
public GatewayOrderResult toResult(GatewayPayOrder order) {
|
||||
GatewayOrderResult result = new GatewayOrderResult()
|
||||
.setOrderNo(order.getOrderNo())
|
||||
.setBizOrderNo(order.getBizOrderNo())
|
||||
.setGatewayType(order.getGatewayType())
|
||||
.setTitle(order.getTitle())
|
||||
.setDescription(order.getDescription())
|
||||
.setAmount(order.getAmount())
|
||||
.setCurrency(order.getCurrency())
|
||||
.setStatus(order.getStatus())
|
||||
.setExpiredTime(order.getExpiredTime())
|
||||
.setPayTime(order.getPayTime())
|
||||
.setChannel(order.getChannel())
|
||||
.setMethod(order.getMethod())
|
||||
.setProduct(order.getProduct())
|
||||
.setAttach(order.getAttach())
|
||||
.setReturnUrl(order.getReturnUrl());
|
||||
PayTrade trade = payTradeManager.findByContainerId(order.getId(), PayTradeTypeEnum.GATEWAY.getCode())
|
||||
.orElse(null);
|
||||
if (trade != null) {
|
||||
result.setTradeNo(trade.getTradeNo())
|
||||
.setOutOrderNo(trade.getOutOrderNo())
|
||||
.setFundStatus(trade.getStatus());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package cn.daxpay.open.payment.core.trade.service;
|
||||
|
||||
import cn.daxpay.open.payment.common.enums.GatewayOrderStatusEnum;
|
||||
import cn.daxpay.open.payment.common.enums.GatewayPayTypeEnum;
|
||||
import cn.daxpay.open.payment.common.service.MerchantContextLoader;
|
||||
import cn.daxpay.open.payment.core.trade.dao.GatewayPayOrderManager;
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.mq.GatewayTimeoutMessage;
|
||||
import cn.daxpay.open.payment.core.trade.mq.PayArtemisConstants;
|
||||
import cn.daxpay.open.payment.unipay.param.gateway.GatewayPrePayParam;
|
||||
import cn.daxpay.open.payment.unipay.result.gateway.GatewayPrePayResult;
|
||||
import cn.daxpay.open.platform.common.artemis.service.ArtemisTemplateService;
|
||||
import cn.daxpay.open.platform.common.json.util.JacksonUtil;
|
||||
import cn.daxpay.open.platform.common.spring.util.WebServletUtil;
|
||||
import cn.daxpay.open.platform.core.code.CommonCode;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.CurrencyEnum;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.core.exception.operation.OperationFailException;
|
||||
import cn.daxpay.open.platform.core.util.DateTimeUtil;
|
||||
import cn.daxpay.open.platform.core.util.TradeNoGenerateUtil;
|
||||
import cn.daxpay.open.platform.system.entity.config.platform.infra.PlatformUrlConfig;
|
||||
import cn.daxpay.open.platform.system.service.config.infra.PlatformUrlConfigService;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.lock.LockInfo;
|
||||
import com.baomidou.lock.LockTemplate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 网关支付支撑服务
|
||||
///
|
||||
/// 预下单建容器、URL 生成、状态校验、超时消息注册。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayPayAssistService {
|
||||
|
||||
private final GatewayPayOrderManager gatewayPayOrderManager;
|
||||
private final MerchantContextLoader merchantContextLoader;
|
||||
private final PlatformUrlConfigService platformUrlConfigService;
|
||||
private final ArtemisTemplateService artemisTemplateService;
|
||||
private final LockTemplate lockTemplate;
|
||||
private final PayAssistService payAssistService;
|
||||
|
||||
/// 预下单: 仅创建容器, 返回落地页 URL
|
||||
public GatewayPrePayResult prePay(GatewayPrePayParam param) {
|
||||
if (StrUtil.isBlank(param.getClientIp())) {
|
||||
param.setClientIp(WebServletUtil.getClientIp());
|
||||
}
|
||||
payAssistService.validationExpiredTime(param.getExpiredTime());
|
||||
GatewayPayTypeEnum typeEnum = GatewayPayTypeEnum.findByCode(param.getGatewayPayType());
|
||||
// 本期仅实现聚合扫码, 收银台类型允许预留但提示
|
||||
if (typeEnum == GatewayPayTypeEnum.CASHIER) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.gateway.cashierNotReady");
|
||||
}
|
||||
|
||||
var mchApp = merchantContextLoader.resolveApp(param.getMchNo(), param.getAppId());
|
||||
param.setAppId(mchApp.getAppId());
|
||||
merchantContextLoader.initMch(param.getMchNo());
|
||||
|
||||
LockInfo lock = lockTemplate.lock(
|
||||
"payment:gateway:pre:" + param.getAppId() + ":" + param.getBizOrderNo(), 10000, 200);
|
||||
if (Objects.isNull(lock)) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.processing");
|
||||
}
|
||||
try {
|
||||
return cn.hutool.extra.spring.SpringUtil.getBean(this.getClass()).doPrePay(param, typeEnum);
|
||||
} finally {
|
||||
lockTemplate.releaseLock(lock);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public GatewayPrePayResult doPrePay(GatewayPrePayParam param, GatewayPayTypeEnum typeEnum) {
|
||||
// 幂等: 已有未终态单则返回原 URL
|
||||
var existing = gatewayPayOrderManager.findByBizOrderNo(param.getBizOrderNo(), param.getAppId());
|
||||
if (existing.isPresent()) {
|
||||
GatewayPayOrder order = existing.get();
|
||||
String status = order.getStatus();
|
||||
if (Objects.equals(status, GatewayOrderStatusEnum.PAID.getCode())) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.alreadySuccess");
|
||||
}
|
||||
if (List.of(GatewayOrderStatusEnum.CLOSED.getCode(), GatewayOrderStatusEnum.EXPIRED.getCode())
|
||||
.contains(status)) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.failedOrClosed");
|
||||
}
|
||||
return this.buildPrePayResult(order);
|
||||
}
|
||||
|
||||
OffsetDateTime expiredTime = payAssistService.getExpiredTime(param.getExpiredTime());
|
||||
String terminalNo = param.getTerminal() != null ? param.getTerminal().getTerminalNo() : null;
|
||||
|
||||
GatewayPayOrder order = new GatewayPayOrder();
|
||||
order.setAppId(param.getAppId());
|
||||
order.setOrderNo(TradeNoGenerateUtil.pay());
|
||||
order.setBizOrderNo(param.getBizOrderNo());
|
||||
order.setGatewayType(typeEnum.getCode());
|
||||
order.setTitle(param.getTitle());
|
||||
order.setDescription(param.getDescription());
|
||||
order.setStatus(GatewayOrderStatusEnum.WAIT_PAY.getCode());
|
||||
order.setNotifyUrl(param.getNotifyUrl());
|
||||
order.setReturnUrl(param.getReturnUrl());
|
||||
order.setAttach(param.getAttach());
|
||||
order.setExpiredTime(expiredTime);
|
||||
order.setAmount(param.getAmount());
|
||||
order.setCurrency(CurrencyEnum.CNY.getCode());
|
||||
order.setClientIp(param.getClientIp());
|
||||
order.setTerminalNo(terminalNo);
|
||||
order.setGoodsDetail(param.getGoodsDetail());
|
||||
gatewayPayOrderManager.save(order);
|
||||
|
||||
this.registerTimeout(order.getOrderNo(), order.getBizOrderNo(), expiredTime);
|
||||
return this.buildPrePayResult(order);
|
||||
}
|
||||
|
||||
/// 按 orderNo 加载并校验可支付
|
||||
public GatewayPayOrder getOrderAndCheck(String orderNo) {
|
||||
GatewayPayOrder order = gatewayPayOrderManager.findByOrderNoNotTenant(orderNo)
|
||||
.orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.error.payOrderNotExist"));
|
||||
this.checkPayable(order);
|
||||
return order;
|
||||
}
|
||||
|
||||
/// 校验容器可继续支付
|
||||
public void checkPayable(GatewayPayOrder order) {
|
||||
String status = order.getStatus();
|
||||
if (Objects.equals(status, GatewayOrderStatusEnum.PAID.getCode())) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.alreadySuccess");
|
||||
}
|
||||
if (List.of(GatewayOrderStatusEnum.CLOSED.getCode(), GatewayOrderStatusEnum.EXPIRED.getCode())
|
||||
.contains(status)) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.failedOrClosed");
|
||||
}
|
||||
if (Objects.nonNull(order.getExpiredTime())
|
||||
&& DateTimeUtil.ge(OffsetDateTime.now(ZoneOffset.UTC), order.getExpiredTime())) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.timeoutRetry");
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成落地页 URL
|
||||
public String buildGatewayUrl(GatewayPayOrder order) {
|
||||
PlatformUrlConfig urlConfig = platformUrlConfigService.getUrlConfig();
|
||||
String gatewayBase = urlConfig.getPaymentGatewayBaseUrl();
|
||||
if (StrUtil.isBlank(gatewayBase)) {
|
||||
throw new OperationFailException(CommonCode.FAIL_CODE, "error.common.gatewayUrlNotConfigured");
|
||||
}
|
||||
gatewayBase = StrUtil.removeSuffix(gatewayBase, "/");
|
||||
if (Objects.equals(order.getGatewayType(), GatewayPayTypeEnum.AGGREGATE.getCode())) {
|
||||
return gatewayBase + "/aggregate/" + order.getOrderNo();
|
||||
}
|
||||
return gatewayBase + "/cashier/" + order.getOrderNo();
|
||||
}
|
||||
|
||||
public GatewayPrePayResult buildPrePayResult(GatewayPayOrder order) {
|
||||
return new GatewayPrePayResult()
|
||||
.setOrderNo(order.getOrderNo())
|
||||
.setBizOrderNo(order.getBizOrderNo())
|
||||
.setStatus(order.getStatus())
|
||||
.setUrl(this.buildGatewayUrl(order))
|
||||
.setExpiredTime(order.getExpiredTime());
|
||||
}
|
||||
|
||||
private void registerTimeout(String orderNo, String bizOrderNo, OffsetDateTime expiredTime) {
|
||||
GatewayTimeoutMessage message = new GatewayTimeoutMessage()
|
||||
.setOrderNo(orderNo)
|
||||
.setBizOrderNo(bizOrderNo);
|
||||
String json = JacksonUtil.toJson(message);
|
||||
try {
|
||||
artemisTemplateService.sendDelayAt(PayArtemisConstants.GATEWAY_TIMEOUT_QUEUE, json, expiredTime);
|
||||
} catch (Exception e) {
|
||||
log.warn("注册网关超时关单延时消息失败, 由定时任务兜底, orderNo={}", orderNo, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package cn.daxpay.open.payment.core.trade.service;
|
||||
|
||||
import cn.daxpay.open.payment.common.enums.GatewayOrderStatusEnum;
|
||||
import cn.daxpay.open.payment.common.enums.PayFundStatusEnum;
|
||||
import cn.daxpay.open.payment.common.enums.PayTradeTypeEnum;
|
||||
import cn.daxpay.open.payment.common.service.MerchantContextLoader;
|
||||
import cn.daxpay.open.payment.core.route.PayRouteFacade;
|
||||
import cn.daxpay.open.payment.core.strategy.PaymentStrategyFactory;
|
||||
import cn.daxpay.open.payment.core.strategy.pay.AbsNormalPayStrategy;
|
||||
import cn.daxpay.open.payment.core.strategy.pay.PayStrategyContext;
|
||||
import cn.daxpay.open.payment.core.trade.bo.PayTradeResultBo;
|
||||
import cn.daxpay.open.payment.core.trade.dao.GatewayPayOrderManager;
|
||||
import cn.daxpay.open.payment.core.trade.dao.PayTradeManager;
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayTrade;
|
||||
import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam;
|
||||
import cn.daxpay.open.payment.unipay.result.trade.pay.NormalPayResult;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.CurrencyEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.trade.TradeSourceEnum;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.core.exception.PayFailureException;
|
||||
import cn.daxpay.open.platform.core.util.TradeNoGenerateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.baomidou.lock.LockInfo;
|
||||
import com.baomidou.lock.LockTemplate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 网关支付统一内核
|
||||
///
|
||||
/// 在 product/method 已解析后: 懒创建 Trade → 调通道策略 → 回写容器。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayPayHandleService {
|
||||
|
||||
private final GatewayPayOrderManager gatewayPayOrderManager;
|
||||
private final PayTradeManager payTradeManager;
|
||||
private final PayRouteFacade payRouteFacade;
|
||||
private final MerchantContextLoader merchantContextLoader;
|
||||
private final PayUniHandleService payUniHandleService;
|
||||
private final GatewayPayAssistService gatewayPayAssistService;
|
||||
private final LockTemplate lockTemplate;
|
||||
|
||||
/// 发起网关支付
|
||||
public NormalPayResult handle(GatewayPayOrder order, String product, String method,
|
||||
String openId, String scene, String device, String clientIp) {
|
||||
LockInfo lock = lockTemplate.lock("payment:gateway:pay:" + order.getOrderNo(), 10000, 200);
|
||||
if (Objects.isNull(lock)) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.processing");
|
||||
}
|
||||
try {
|
||||
// 重新加载最新状态
|
||||
order = gatewayPayOrderManager.findById(order.getId())
|
||||
.orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.error.payOrderNotExist"));
|
||||
gatewayPayAssistService.checkPayable(order);
|
||||
merchantContextLoader.initMch(order.getMchNo());
|
||||
|
||||
// 已有 Trade: 幂等 / 锁定规则
|
||||
PayTrade existing = payTradeManager.findByContainerId(order.getId(), PayTradeTypeEnum.GATEWAY.getCode())
|
||||
.orElse(null);
|
||||
if (existing != null) {
|
||||
if (Objects.equals(existing.getStatus(), PayFundStatusEnum.SUCCESS.getCode())) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.alreadySuccess");
|
||||
}
|
||||
if (Objects.equals(existing.getStatus(), PayFundStatusEnum.PROCESSING.getCode())
|
||||
|| Objects.equals(existing.getStatus(), PayFundStatusEnum.INIT.getCode())) {
|
||||
// 通道/方式不一致则拒绝换端
|
||||
if (!Objects.equals(existing.getProduct(), product)
|
||||
|| !Objects.equals(existing.getMethod(), method)) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.error.gateway.channelLocked");
|
||||
}
|
||||
if (StrUtil.isNotBlank(existing.getPayBody())) {
|
||||
return this.buildResult(order, existing);
|
||||
}
|
||||
} else {
|
||||
// fail/close 等终态: 首期不允许换方式重试
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.failedOrClosed");
|
||||
}
|
||||
}
|
||||
|
||||
// 组装路由用参数
|
||||
NormalPayParam payParam = this.buildPayParam(order, product, method, openId, clientIp);
|
||||
payRouteFacade.resolve(payParam);
|
||||
var payStrategy = PaymentStrategyFactory.createByProduct(payParam.getProduct(), AbsNormalPayStrategy.class);
|
||||
var context = new PayStrategyContext().setPayParam(payParam);
|
||||
payStrategy.doBeforePay(context);
|
||||
|
||||
// 建 Trade(若无) + 回填容器
|
||||
if (existing == null) {
|
||||
existing = SpringUtil.getBean(this.getClass())
|
||||
.createTrade(order, payParam, scene, device);
|
||||
} else {
|
||||
// 回填路由结果到容器
|
||||
this.fillRouteOnOrder(order, payParam, scene, device);
|
||||
gatewayPayOrderManager.updateById(order);
|
||||
}
|
||||
context.setTrade(existing);
|
||||
|
||||
PayTradeResultBo result;
|
||||
try {
|
||||
result = payStrategy.doPay(context);
|
||||
} catch (Exception e) {
|
||||
log.error("网关支付出现异常", e);
|
||||
existing.setStatus(PayFundStatusEnum.FAIL.getCode());
|
||||
if (e instanceof PayFailureException) {
|
||||
existing.setErrorMsg(e.getMessage());
|
||||
} else {
|
||||
existing.setErrorMsg("支付出现异常: " + e.getMessage());
|
||||
}
|
||||
payTradeManager.updateById(existing);
|
||||
throw e;
|
||||
}
|
||||
return SpringUtil.getBean(this.getClass()).paySuccess(order, existing, result);
|
||||
} finally {
|
||||
lockTemplate.releaseLock(lock);
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建资金凭证并更新容器为 paying
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PayTrade createTrade(GatewayPayOrder order, NormalPayParam payParam, String scene, String device) {
|
||||
var productEnum = ProductEnum.findByCode(payParam.getProduct());
|
||||
String channel = productEnum.getChannel();
|
||||
|
||||
PayTrade trade = new PayTrade();
|
||||
trade.setAppId(order.getAppId());
|
||||
trade.setTradeNo(TradeNoGenerateUtil.pay());
|
||||
trade.setTradeType(PayTradeTypeEnum.GATEWAY.getCode());
|
||||
trade.setContainerId(order.getId());
|
||||
trade.setBizOrderNo(order.getBizOrderNo());
|
||||
trade.setProduct(payParam.getProduct());
|
||||
trade.setChannel(channel);
|
||||
trade.setMethod(payParam.getMethod());
|
||||
trade.setChannelMchNo(payParam.getChannelMchNo());
|
||||
trade.setCapability(payParam.getCapability());
|
||||
trade.setClientIp(order.getClientIp());
|
||||
trade.setAmount(order.getAmount());
|
||||
trade.setCurrency(CurrencyEnum.CNY.getCode());
|
||||
trade.setRefundableBalance(order.getAmount());
|
||||
trade.setStatus(PayFundStatusEnum.PROCESSING.getCode());
|
||||
trade.setExpiredTime(order.getExpiredTime());
|
||||
trade.setSource(TradeSourceEnum.AGGRESS_PAY.getCode());
|
||||
trade.setOpenid(payParam.getOpenId());
|
||||
payTradeManager.save(trade);
|
||||
|
||||
this.fillRouteOnOrder(order, payParam, scene, device);
|
||||
order.setStatus(GatewayOrderStatusEnum.PAYING.getCode());
|
||||
order.setChannel(channel);
|
||||
order.setMethod(payParam.getMethod());
|
||||
order.setProduct(payParam.getProduct());
|
||||
gatewayPayOrderManager.updateById(order);
|
||||
return trade;
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public NormalPayResult paySuccess(GatewayPayOrder order, PayTrade trade, PayTradeResultBo result) {
|
||||
if (result.isComplete()) {
|
||||
trade.setStatus(PayFundStatusEnum.SUCCESS.getCode());
|
||||
trade.setPayTime(result.getFinishTime());
|
||||
}
|
||||
trade.setOutOrderNo(result.getOutOrderNo());
|
||||
trade.setTransOrderNo(result.getTransOrderNo());
|
||||
trade.setRelationOrderNo(result.getRelationOrderNo());
|
||||
trade.setBuyerId(result.getBuyerId());
|
||||
trade.setBuyerLogonId(result.getBuyerLogonId());
|
||||
trade.setTradeProduct(result.getTradeProduct());
|
||||
trade.setTradeWay(result.getTradeWay());
|
||||
trade.setBankType(result.getBankType());
|
||||
trade.setPromotionType(result.getPromotionType());
|
||||
trade.setPayBody(result.getPayBody());
|
||||
trade.setPayBodyType(Objects.nonNull(result.getPayBodyType())
|
||||
? result.getPayBodyType().getCode() : null);
|
||||
trade.setErrorMsg(null);
|
||||
payUniHandleService.payAfterHandel(trade);
|
||||
return this.buildResult(order, trade);
|
||||
}
|
||||
|
||||
private void fillRouteOnOrder(GatewayPayOrder order, NormalPayParam payParam, String scene, String device) {
|
||||
order.setChannelMchNo(payParam.getChannelMchNo());
|
||||
order.setCapability(payParam.getCapability());
|
||||
if (StrUtil.isNotBlank(scene)) {
|
||||
order.setScene(scene);
|
||||
}
|
||||
if (StrUtil.isNotBlank(device)) {
|
||||
order.setDevice(device);
|
||||
}
|
||||
if (StrUtil.isNotBlank(payParam.getClientIp())) {
|
||||
order.setClientIp(payParam.getClientIp());
|
||||
}
|
||||
}
|
||||
|
||||
private NormalPayParam buildPayParam(GatewayPayOrder order, String product, String method,
|
||||
String openId, String clientIp) {
|
||||
NormalPayParam payParam = new NormalPayParam();
|
||||
payParam.setMchNo(order.getMchNo());
|
||||
payParam.setAppId(order.getAppId());
|
||||
payParam.setBizOrderNo(order.getBizOrderNo());
|
||||
payParam.setTitle(order.getTitle());
|
||||
payParam.setDescription(order.getDescription());
|
||||
payParam.setAmount(order.getAmount());
|
||||
payParam.setProduct(product);
|
||||
payParam.setMethod(method);
|
||||
payParam.setOpenId(openId);
|
||||
payParam.setNotifyUrl(order.getNotifyUrl());
|
||||
payParam.setReturnUrl(order.getReturnUrl());
|
||||
payParam.setAttach(order.getAttach());
|
||||
payParam.setExpiredTime(order.getExpiredTime());
|
||||
payParam.setClientIp(StrUtil.blankToDefault(clientIp, order.getClientIp()));
|
||||
payParam.setGoodsDetail(order.getGoodsDetail());
|
||||
return payParam;
|
||||
}
|
||||
|
||||
private NormalPayResult buildResult(GatewayPayOrder order, PayTrade trade) {
|
||||
return new NormalPayResult()
|
||||
.setOrderId(order.getId())
|
||||
.setBizOrderNo(order.getBizOrderNo())
|
||||
.setOrderNo(trade.getTradeNo())
|
||||
.setStatus(trade.getStatus())
|
||||
.setPayBody(trade.getPayBody())
|
||||
.setPayBodyType(trade.getPayBodyType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package cn.daxpay.open.payment.core.trade.service;
|
||||
|
||||
import cn.daxpay.open.payment.common.enums.GatewayOrderStatusEnum;
|
||||
import cn.daxpay.open.payment.common.enums.PayTradeTypeEnum;
|
||||
import cn.daxpay.open.payment.core.trade.dao.GatewayPayOrderManager;
|
||||
import cn.daxpay.open.payment.core.trade.dao.PayTradeManager;
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayTrade;
|
||||
import com.baomidou.lock.LockInfo;
|
||||
import com.baomidou.lock.LockTemplate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 网关支付超时关单服务
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayTimeoutService {
|
||||
|
||||
private final GatewayPayOrderManager gatewayPayOrderManager;
|
||||
private final PayTradeManager payTradeManager;
|
||||
private final PayCloseService payCloseService;
|
||||
private final PayUniHandleService payUniHandleService;
|
||||
private final LockTemplate lockTemplate;
|
||||
|
||||
/// 按网关单号超时关闭(幂等)
|
||||
public void closeForTimeout(String orderNo) {
|
||||
GatewayPayOrder order = gatewayPayOrderManager.findByOrderNoNotTenant(orderNo).orElse(null);
|
||||
if (order == null) {
|
||||
return;
|
||||
}
|
||||
if (!List.of(GatewayOrderStatusEnum.WAIT_PAY.getCode(), GatewayOrderStatusEnum.PAYING.getCode())
|
||||
.contains(order.getStatus())) {
|
||||
return;
|
||||
}
|
||||
LockInfo lock = lockTemplate.lock("payment:gateway:timeout:" + order.getId(), 10000, 50);
|
||||
if (lock == null) {
|
||||
log.warn("网关超时关单获取锁失败, orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
order = gatewayPayOrderManager.findByOrderNoNotTenant(orderNo).orElse(null);
|
||||
if (order == null
|
||||
|| !List.of(GatewayOrderStatusEnum.WAIT_PAY.getCode(), GatewayOrderStatusEnum.PAYING.getCode())
|
||||
.contains(order.getStatus())) {
|
||||
return;
|
||||
}
|
||||
PayTrade trade = payTradeManager.findByContainerId(order.getId(), PayTradeTypeEnum.GATEWAY.getCode())
|
||||
.orElse(null);
|
||||
if (trade != null) {
|
||||
// 有资金凭证: 走统一超时关单(含通道关闭)
|
||||
payCloseService.closeForTimeout(trade.getTradeNo());
|
||||
} else {
|
||||
// 仅容器: 直接过期
|
||||
payUniHandleService.gatewayOrderTimeout(order);
|
||||
}
|
||||
} finally {
|
||||
lockTemplate.releaseLock(lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,8 @@ public class PayRefundService {
|
||||
// 按 bizOrderNo 查容器, 再查关联交易
|
||||
NormalPayOrder normalOrder = payNormalOrderManager.findByBizOrderNo(bizOrderNo)
|
||||
.orElseThrow(() -> new BizInfoException(DaxPayErrorCode.TRADE_STATUS_ERROR, "pay.error.notExists"));
|
||||
return payTradeManager.findByContainerId(normalOrder.getId())
|
||||
return payTradeManager.findByContainerId(normalOrder.getId(),
|
||||
cn.daxpay.open.payment.common.enums.PayTradeTypeEnum.NORMAL.getCode())
|
||||
.orElseThrow(() -> new BizInfoException(DaxPayErrorCode.TRADE_STATUS_ERROR, "pay.error.notExists"));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package cn.daxpay.open.payment.core.trade.service;
|
||||
|
||||
import cn.daxpay.open.payment.common.enums.GatewayOrderStatusEnum;
|
||||
import cn.daxpay.open.payment.common.enums.NormalPayOrderStatusEnum;
|
||||
import cn.daxpay.open.payment.common.enums.PayFundStatusEnum;
|
||||
import cn.daxpay.open.payment.common.enums.PayTradeTypeEnum;
|
||||
import cn.daxpay.open.payment.core.trade.dao.GatewayPayOrderManager;
|
||||
import cn.daxpay.open.payment.core.trade.dao.NormalPayOrderManager;
|
||||
import cn.daxpay.open.payment.core.trade.entity.NormalPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.dao.PayTradeManager;
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.entity.NormalPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayTrade;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -16,7 +20,7 @@ import java.util.Objects;
|
||||
|
||||
/// # 交易统一处理服务
|
||||
///
|
||||
/// 支付成功/失败/关闭后的统一处理逻辑
|
||||
/// 支付成功/失败/关闭后的统一处理逻辑, 按 trade_type 回写对应业务容器。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -24,71 +28,111 @@ public class PayUniHandleService {
|
||||
|
||||
private final PayTradeManager payTradeManager;
|
||||
private final NormalPayOrderManager payNormalOrderManager;
|
||||
private final GatewayPayOrderManager gatewayPayOrderManager;
|
||||
|
||||
/// 支付发起后处理(参考商业版 TradeUniHandleService.payAfterHandel)
|
||||
/// 支付发起后处理
|
||||
/// 不论是否完成都更新交易单; 仅资金状态为 SUCCESS 时同步容器为 PAID。
|
||||
/// 异步支付(complete=false)时容器保持 WAIT_PAY, 交易单保持 PROCESSING。
|
||||
public void payAfterHandel(PayTrade trade) {
|
||||
payTradeManager.updateById(trade);
|
||||
if (Objects.equals(trade.getStatus(), PayFundStatusEnum.SUCCESS.getCode())) {
|
||||
NormalPayOrder normalOrder = payNormalOrderManager.findById(trade.getContainerId())
|
||||
.orElse(null);
|
||||
if (normalOrder != null) {
|
||||
normalOrder.setStatus(NormalPayOrderStatusEnum.PAID.getCode());
|
||||
normalOrder.setPayTime(trade.getPayTime());
|
||||
payNormalOrderManager.updateById(normalOrder);
|
||||
}
|
||||
this.markContainerPaid(trade);
|
||||
}
|
||||
}
|
||||
|
||||
/// 支付成功后续处理(同步冗余时间线到容器)
|
||||
/// 支付成功后续处理
|
||||
public void paySuccess(PayTrade trade) {
|
||||
NormalPayOrder normalOrder = payNormalOrderManager.findById(trade.getContainerId())
|
||||
.orElse(null);
|
||||
if (normalOrder != null) {
|
||||
normalOrder.setStatus(NormalPayOrderStatusEnum.PAID.getCode());
|
||||
normalOrder.setPayTime(trade.getPayTime());
|
||||
payNormalOrderManager.updateById(normalOrder);
|
||||
}
|
||||
this.markContainerPaid(trade);
|
||||
payTradeManager.updateById(trade);
|
||||
}
|
||||
|
||||
/// 支付失败处理(同步冗余时间线到容器)
|
||||
public void payFail(PayTrade trade, NormalPayOrder normalOrder, String errMsg) {
|
||||
/// 支付失败处理
|
||||
public void payFail(PayTrade trade, String errMsg) {
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
trade.setStatus(PayFundStatusEnum.FAIL.getCode());
|
||||
trade.setErrorMsg(errMsg);
|
||||
trade.setCloseTime(now);
|
||||
normalOrder.setStatus(NormalPayOrderStatusEnum.CLOSED.getCode());
|
||||
normalOrder.setCloseTime(now);
|
||||
payTradeManager.updateById(trade);
|
||||
payNormalOrderManager.updateById(normalOrder);
|
||||
this.markContainerClosed(trade, now, false);
|
||||
}
|
||||
|
||||
/// 支付关闭处理(同步冗余时间线到容器)
|
||||
/// @param useCancel true=撤销(资金态置 CANCEL), false=关闭(资金态置 CLOSE); 容器统一置 CLOSED
|
||||
public void payClose(PayTrade trade, NormalPayOrder normalOrder, boolean useCancel) {
|
||||
/// 支付关闭处理
|
||||
/// @param useCancel true=撤销(资金态置 CANCEL), false=关闭(资金态置 CLOSE)
|
||||
public void payClose(PayTrade trade, boolean useCancel) {
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
trade.setStatus(useCancel
|
||||
? PayFundStatusEnum.CANCEL.getCode()
|
||||
: PayFundStatusEnum.CLOSE.getCode());
|
||||
trade.setCloseTime(now);
|
||||
normalOrder.setStatus(NormalPayOrderStatusEnum.CLOSED.getCode());
|
||||
normalOrder.setCloseTime(now);
|
||||
payTradeManager.updateById(trade);
|
||||
payNormalOrderManager.updateById(normalOrder);
|
||||
this.markContainerClosed(trade, now, false);
|
||||
}
|
||||
|
||||
/// 支付超时关闭处理
|
||||
/// 资金态置 CLOSE(与普通关闭等价), 容器态置 EXPIRED(区分业务语义),
|
||||
/// 供 MQ 延时关单 / 兜底定时任务 / 同步发现超时 三条路径统一调用。
|
||||
public void payTimeout(PayTrade trade, NormalPayOrder normalOrder) {
|
||||
/// 支付超时关闭: 资金态 CLOSE, 容器态 EXPIRED
|
||||
public void payTimeout(PayTrade trade) {
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
trade.setStatus(PayFundStatusEnum.CLOSE.getCode());
|
||||
trade.setCloseTime(now);
|
||||
normalOrder.setStatus(NormalPayOrderStatusEnum.EXPIRED.getCode());
|
||||
normalOrder.setCloseTime(now);
|
||||
payTradeManager.updateById(trade);
|
||||
payNormalOrderManager.updateById(normalOrder);
|
||||
this.markContainerClosed(trade, now, true);
|
||||
}
|
||||
|
||||
/// 仅关闭网关容器(尚未创建 Trade 的预下单超时)
|
||||
public void gatewayOrderTimeout(GatewayPayOrder order) {
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
order.setStatus(GatewayOrderStatusEnum.EXPIRED.getCode());
|
||||
order.setCloseTime(now);
|
||||
gatewayPayOrderManager.updateById(order);
|
||||
}
|
||||
|
||||
/// 仅关闭网关容器(商户主动关单且尚无 Trade)
|
||||
public void gatewayOrderClose(GatewayPayOrder order) {
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
order.setStatus(GatewayOrderStatusEnum.CLOSED.getCode());
|
||||
order.setCloseTime(now);
|
||||
gatewayPayOrderManager.updateById(order);
|
||||
}
|
||||
|
||||
private void markContainerPaid(PayTrade trade) {
|
||||
if (isGateway(trade)) {
|
||||
GatewayPayOrder order = gatewayPayOrderManager.findById(trade.getContainerId()).orElse(null);
|
||||
if (order != null) {
|
||||
order.setStatus(GatewayOrderStatusEnum.PAID.getCode());
|
||||
order.setPayTime(trade.getPayTime());
|
||||
gatewayPayOrderManager.updateById(order);
|
||||
}
|
||||
return;
|
||||
}
|
||||
NormalPayOrder normalOrder = payNormalOrderManager.findById(trade.getContainerId()).orElse(null);
|
||||
if (normalOrder != null) {
|
||||
normalOrder.setStatus(NormalPayOrderStatusEnum.PAID.getCode());
|
||||
normalOrder.setPayTime(trade.getPayTime());
|
||||
payNormalOrderManager.updateById(normalOrder);
|
||||
}
|
||||
}
|
||||
|
||||
private void markContainerClosed(PayTrade trade, OffsetDateTime now, boolean expired) {
|
||||
if (isGateway(trade)) {
|
||||
GatewayPayOrder order = gatewayPayOrderManager.findById(trade.getContainerId()).orElse(null);
|
||||
if (order != null) {
|
||||
order.setStatus(expired
|
||||
? GatewayOrderStatusEnum.EXPIRED.getCode()
|
||||
: GatewayOrderStatusEnum.CLOSED.getCode());
|
||||
order.setCloseTime(now);
|
||||
gatewayPayOrderManager.updateById(order);
|
||||
}
|
||||
return;
|
||||
}
|
||||
NormalPayOrder normalOrder = payNormalOrderManager.findById(trade.getContainerId()).orElse(null);
|
||||
if (normalOrder != null) {
|
||||
normalOrder.setStatus(expired
|
||||
? NormalPayOrderStatusEnum.EXPIRED.getCode()
|
||||
: NormalPayOrderStatusEnum.CLOSED.getCode());
|
||||
normalOrder.setCloseTime(now);
|
||||
payNormalOrderManager.updateById(normalOrder);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isGateway(PayTrade trade) {
|
||||
return Objects.equals(trade.getTradeType(), PayTradeTypeEnum.GATEWAY.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package cn.daxpay.open.payment.core.trade.service.admin;
|
||||
|
||||
import cn.daxpay.open.payment.common.enums.GatewayOrderStatusEnum;
|
||||
import cn.daxpay.open.payment.common.enums.PayTradeTypeEnum;
|
||||
import cn.daxpay.open.payment.core.trade.convert.GatewayPayOrderConvert;
|
||||
import cn.daxpay.open.payment.core.trade.dao.GatewayPayOrderManager;
|
||||
import cn.daxpay.open.payment.core.trade.dao.PayTradeManager;
|
||||
import cn.daxpay.open.payment.core.trade.entity.GatewayPayOrder;
|
||||
import cn.daxpay.open.payment.core.trade.entity.PayTrade;
|
||||
import cn.daxpay.open.payment.core.trade.param.GatewayPayOrderQuery;
|
||||
import cn.daxpay.open.payment.core.trade.result.GatewayPayOrderResult;
|
||||
import cn.daxpay.open.payment.core.trade.service.PayCloseService;
|
||||
import cn.daxpay.open.payment.core.trade.service.PaySyncService;
|
||||
import cn.daxpay.open.payment.core.trade.service.PayUniHandleService;
|
||||
import cn.daxpay.open.payment.unipay.result.trade.pay.NormalPaySyncResult;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.daxpay.open.platform.core.rest.param.PageParam;
|
||||
import cn.daxpay.open.platform.core.rest.result.PageResult;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 网关支付业务单管理服务
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayPayOrderAdminService {
|
||||
|
||||
private final GatewayPayOrderManager gatewayPayOrderManager;
|
||||
private final PayTradeManager payTradeManager;
|
||||
private final PaySyncService paySyncService;
|
||||
private final PayCloseService payCloseService;
|
||||
private final PayUniHandleService payUniHandleService;
|
||||
|
||||
public PageResult<GatewayPayOrderResult> page(PageParam pageParam, GatewayPayOrderQuery query) {
|
||||
Page<GatewayPayOrder> page = gatewayPayOrderManager.page(pageParam, query);
|
||||
var records = page.getRecords().stream()
|
||||
.map(GatewayPayOrderConvert.CONVERT::toResult)
|
||||
.toList();
|
||||
return new PageResult<GatewayPayOrderResult>()
|
||||
.setRecords(records)
|
||||
.setTotal(page.getTotal())
|
||||
.setSize(page.getSize())
|
||||
.setCurrent(page.getCurrent());
|
||||
}
|
||||
|
||||
public GatewayPayOrderResult findById(Long id) {
|
||||
GatewayPayOrder entity = gatewayPayOrderManager.findById(id)
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.payOrderNotExist"));
|
||||
GatewayPayOrderResult result = GatewayPayOrderConvert.CONVERT.toResult(entity);
|
||||
PayTrade trade = payTradeManager.findByContainerId(id, PayTradeTypeEnum.GATEWAY.getCode()).orElse(null);
|
||||
if (Objects.nonNull(trade)) {
|
||||
result.setTradeNo(trade.getTradeNo());
|
||||
result.setOutOrderNo(trade.getOutOrderNo());
|
||||
result.setFundStatus(trade.getStatus());
|
||||
result.setRefundableBalance(trade.getRefundableBalance());
|
||||
result.setErrorMsg(trade.getErrorMsg());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public NormalPaySyncResult sync(Long id) {
|
||||
PayTrade trade = payTradeManager.findByContainerId(id, PayTradeTypeEnum.GATEWAY.getCode())
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.payOrderNotExist"));
|
||||
return paySyncService.syncPayOrder(trade);
|
||||
}
|
||||
|
||||
public void close(Long id, boolean useCancel) {
|
||||
GatewayPayOrder order = gatewayPayOrderManager.findById(id)
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.payOrderNotExist"));
|
||||
if (!List.of(GatewayOrderStatusEnum.WAIT_PAY.getCode(), GatewayOrderStatusEnum.PAYING.getCode())
|
||||
.contains(order.getStatus())) {
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.pay.closeNotPaying");
|
||||
}
|
||||
PayTrade trade = payTradeManager.findByContainerId(id, PayTradeTypeEnum.GATEWAY.getCode()).orElse(null);
|
||||
if (trade != null) {
|
||||
payCloseService.closeOrder(trade, useCancel);
|
||||
} else {
|
||||
payUniHandleService.gatewayOrderClose(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public class NormalPayOrderAdminService {
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.payOrderNotExist"));
|
||||
NormalPayOrderResult result = NormalPayOrderConvert.CONVERT.toResult(entity);
|
||||
// 联表查询资金凭证, 补充交易字段
|
||||
PayTrade trade = payTradeManager.findByContainerId(id).orElse(null);
|
||||
PayTrade trade = payTradeManager.findByContainerId(id, cn.daxpay.open.payment.common.enums.PayTradeTypeEnum.NORMAL.getCode()).orElse(null);
|
||||
if (Objects.nonNull(trade)) {
|
||||
result.setTradeNo(trade.getTradeNo());
|
||||
result.setOutOrderNo(trade.getOutOrderNo());
|
||||
@@ -74,14 +74,14 @@ public class NormalPayOrderAdminService {
|
||||
|
||||
/// 同步支付状态(传入业务订单ID)
|
||||
public NormalPaySyncResult sync(Long id) {
|
||||
PayTrade trade = payTradeManager.findByContainerId(id)
|
||||
PayTrade trade = payTradeManager.findByContainerId(id, cn.daxpay.open.payment.common.enums.PayTradeTypeEnum.NORMAL.getCode())
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.payOrderNotExist"));
|
||||
return paySyncService.syncPayOrder(trade);
|
||||
}
|
||||
|
||||
/// 关闭/撤销订单(传入业务订单ID)
|
||||
public void close(Long id, boolean useCancel) {
|
||||
PayTrade trade = payTradeManager.findByContainerId(id)
|
||||
PayTrade trade = payTradeManager.findByContainerId(id, cn.daxpay.open.payment.common.enums.PayTradeTypeEnum.NORMAL.getCode())
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.payOrderNotExist"));
|
||||
payCloseService.closeOrder(trade, useCancel);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.daxpay.open.payment.merchant.controller.admin.gateway;
|
||||
|
||||
import cn.daxpay.open.payment.merchant.param.gateway.GatewayAggregateConfigParam;
|
||||
import cn.daxpay.open.payment.merchant.result.gateway.GatewayAggregateConfigResult;
|
||||
import cn.daxpay.open.payment.merchant.service.gateway.GatewayAggregateConfigService;
|
||||
import cn.daxpay.open.platform.core.annotation.PermCode;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/// # 网关聚合扫码配置(管理)
|
||||
@PermCode(menuCode = "merchant:gateway-aggregate")
|
||||
@Validated
|
||||
@Tag(name = "网关聚合扫码配置")
|
||||
@RestController
|
||||
@RequestMapping("/admin/gateway/aggregate-config")
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayAggregateConfigAdminController {
|
||||
|
||||
private final GatewayAggregateConfigService gatewayAggregateConfigService;
|
||||
|
||||
@PermCode(code = "view", nameCn = "配置查看", nameEn = "Config View")
|
||||
@Operation(summary = "按应用查询聚合扫码配置")
|
||||
@GetMapping("/get-by-app-id")
|
||||
public Result<GatewayAggregateConfigResult> getByAppId(
|
||||
@NotBlank(message = "{validation.field.appId.notBlank}") String appId) {
|
||||
return Res.ok(gatewayAggregateConfigService.findByAppId(appId));
|
||||
}
|
||||
|
||||
@PermCode(code = "update", nameCn = "配置更新", nameEn = "Config Update")
|
||||
@Operation(summary = "保存或更新聚合扫码配置")
|
||||
@PostMapping("/save-or-update")
|
||||
public Result<Void> saveOrUpdate(@RequestBody @Validated GatewayAggregateConfigParam param) {
|
||||
gatewayAggregateConfigService.saveOrUpdate(param);
|
||||
return Res.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.daxpay.open.payment.merchant.dao.gateway;
|
||||
|
||||
import cn.daxpay.open.payment.merchant.entity.gateway.GatewayAggregateConfig;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/// # 网关聚合扫码配置管理器
|
||||
@Repository
|
||||
public class GatewayAggregateConfigManager extends BaseManager<GatewayAggregateConfigMapper, GatewayAggregateConfig> {
|
||||
|
||||
/// 按应用号查询
|
||||
public Optional<GatewayAggregateConfig> findByAppId(String appId) {
|
||||
return lambdaQuery()
|
||||
.eq(GatewayAggregateConfig::getAppId, appId)
|
||||
.oneOpt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package cn.daxpay.open.payment.merchant.dao.gateway;
|
||||
|
||||
import cn.daxpay.open.payment.merchant.entity.gateway.GatewayAggregateConfig;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # 网关聚合扫码配置 Mapper
|
||||
@Mapper
|
||||
public interface GatewayAggregateConfigMapper extends MPJBaseMapper<GatewayAggregateConfig> {
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.daxpay.open.payment.merchant.entity.gateway;
|
||||
|
||||
import cn.daxpay.open.payment.common.entity.merchant.MchBaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 网关聚合扫码配置(应用级)
|
||||
///
|
||||
/// 按场景映射支付产品与支付方式, 扫码识别环境后用于建 Trade。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
@TableName("pay_gateway_aggregate_config")
|
||||
public class GatewayAggregateConfig extends MchBaseEntity {
|
||||
|
||||
/// 应用号
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private String appId;
|
||||
|
||||
/// 微信场景支付产品
|
||||
private String wxProduct;
|
||||
|
||||
/// 微信场景支付方式
|
||||
private String wxMethod;
|
||||
|
||||
/// 支付宝场景支付产品
|
||||
private String alipayProduct;
|
||||
|
||||
/// 支付宝场景支付方式
|
||||
private String alipayMethod;
|
||||
|
||||
/// 云闪付场景支付产品
|
||||
private String unionProduct;
|
||||
|
||||
/// 云闪付场景支付方式
|
||||
private String unionMethod;
|
||||
|
||||
/// 是否自动拉起支付
|
||||
private Boolean autoLaunch;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cn.daxpay.open.payment.merchant.param.gateway;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/// # 网关聚合扫码配置参数
|
||||
@Data
|
||||
@Schema(title = "网关聚合扫码配置参数")
|
||||
public class GatewayAggregateConfigParam {
|
||||
|
||||
@Schema(description = "商户号")
|
||||
@NotBlank(message = "{validation.field.mchNo.notBlank}")
|
||||
@Size(max = 32, message = "{validation.field.mchNo.size}")
|
||||
private String mchNo;
|
||||
|
||||
@Schema(description = "应用号")
|
||||
@NotBlank(message = "{validation.field.appId.notBlank}")
|
||||
@Size(max = 32, message = "{validation.field.appId.size}")
|
||||
private String appId;
|
||||
|
||||
@Schema(description = "微信场景支付产品")
|
||||
@Size(max = 32, message = "{validation.field.product.size}")
|
||||
private String wxProduct;
|
||||
|
||||
@Schema(description = "微信场景支付方式")
|
||||
@Size(max = 32, message = "{validation.field.method.size}")
|
||||
private String wxMethod;
|
||||
|
||||
@Schema(description = "支付宝场景支付产品")
|
||||
@Size(max = 32, message = "{validation.field.product.size}")
|
||||
private String alipayProduct;
|
||||
|
||||
@Schema(description = "支付宝场景支付方式")
|
||||
@Size(max = 32, message = "{validation.field.method.size}")
|
||||
private String alipayMethod;
|
||||
|
||||
@Schema(description = "云闪付场景支付产品")
|
||||
@Size(max = 32, message = "{validation.field.product.size}")
|
||||
private String unionProduct;
|
||||
|
||||
@Schema(description = "云闪付场景支付方式")
|
||||
@Size(max = 32, message = "{validation.field.method.size}")
|
||||
private String unionMethod;
|
||||
|
||||
@Schema(description = "是否自动拉起支付")
|
||||
private Boolean autoLaunch;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.daxpay.open.payment.merchant.result.gateway;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 网关聚合扫码配置结果
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "网关聚合扫码配置结果")
|
||||
public class GatewayAggregateConfigResult {
|
||||
|
||||
@Schema(description = "主键")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "商户号")
|
||||
private String mchNo;
|
||||
|
||||
@Schema(description = "应用号")
|
||||
private String appId;
|
||||
|
||||
@Schema(description = "微信场景支付产品")
|
||||
private String wxProduct;
|
||||
|
||||
@Schema(description = "微信场景支付方式")
|
||||
private String wxMethod;
|
||||
|
||||
@Schema(description = "支付宝场景支付产品")
|
||||
private String alipayProduct;
|
||||
|
||||
@Schema(description = "支付宝场景支付方式")
|
||||
private String alipayMethod;
|
||||
|
||||
@Schema(description = "云闪付场景支付产品")
|
||||
private String unionProduct;
|
||||
|
||||
@Schema(description = "云闪付场景支付方式")
|
||||
private String unionMethod;
|
||||
|
||||
@Schema(description = "是否自动拉起支付")
|
||||
private Boolean autoLaunch;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package cn.daxpay.open.payment.merchant.service.gateway;
|
||||
|
||||
import cn.daxpay.open.payment.merchant.dao.gateway.GatewayAggregateConfigManager;
|
||||
import cn.daxpay.open.payment.merchant.entity.gateway.GatewayAggregateConfig;
|
||||
import cn.daxpay.open.payment.merchant.param.gateway.GatewayAggregateConfigParam;
|
||||
import cn.daxpay.open.payment.merchant.result.gateway.GatewayAggregateConfigResult;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/// # 网关聚合扫码配置服务
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayAggregateConfigService {
|
||||
|
||||
private final GatewayAggregateConfigManager configManager;
|
||||
|
||||
/// 按应用查询, 不存在返回空对象字段
|
||||
public GatewayAggregateConfigResult findByAppId(String appId) {
|
||||
return configManager.findByAppId(appId)
|
||||
.map(this::toResult)
|
||||
.orElseGet(() -> new GatewayAggregateConfigResult().setAppId(appId));
|
||||
}
|
||||
|
||||
/// 支付时必须已配置
|
||||
public GatewayAggregateConfig getRequiredByAppId(String appId) {
|
||||
return configManager.findByAppId(appId)
|
||||
.orElseThrow(() -> new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR,
|
||||
"pay.error.gateway.aggregateConfigMissing"));
|
||||
}
|
||||
|
||||
/// 保存或更新
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveOrUpdate(GatewayAggregateConfigParam param) {
|
||||
var existing = configManager.findByAppId(param.getAppId());
|
||||
if (existing.isPresent()) {
|
||||
GatewayAggregateConfig entity = existing.get();
|
||||
BeanUtil.copyProperties(param, entity, "id", "appId", "mchNo", "version", "deleted");
|
||||
configManager.updateById(entity);
|
||||
} else {
|
||||
GatewayAggregateConfig entity = new GatewayAggregateConfig();
|
||||
BeanUtil.copyProperties(param, entity);
|
||||
configManager.save(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private GatewayAggregateConfigResult toResult(GatewayAggregateConfig entity) {
|
||||
GatewayAggregateConfigResult result = new GatewayAggregateConfigResult();
|
||||
BeanUtil.copyProperties(entity, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.daxpay.open.payment.unipay.param.gateway;
|
||||
|
||||
import cn.daxpay.open.payment.common.enums.CashierSceneEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/// # 聚合扫码发起支付参数(H5 侧)
|
||||
@Data
|
||||
@Schema(title = "聚合扫码支付参数")
|
||||
public class AggregateQrPayParam {
|
||||
|
||||
@Schema(description = "平台网关单号")
|
||||
@NotBlank(message = "{validation.field.orderNo.notBlank}")
|
||||
@Size(max = 64, message = "{validation.field.orderNo.size}")
|
||||
private String orderNo;
|
||||
|
||||
/// @see CashierSceneEnum
|
||||
@Schema(description = "收银场景 wechat_pay/alipay/union_pay")
|
||||
@NotBlank(message = "{validation.field.scene.notBlank}")
|
||||
@Size(max = 32, message = "{validation.field.scene.size}")
|
||||
private String scene;
|
||||
|
||||
@Schema(description = "OpenId(微信 JSAPI 等)")
|
||||
@Size(max = 128, message = "{validation.field.openId.size}")
|
||||
private String openId;
|
||||
|
||||
@Schema(description = "设备 mobile/pc")
|
||||
@Size(max = 16, message = "{validation.field.device.size}")
|
||||
private String device;
|
||||
|
||||
@Schema(description = "客户端IP")
|
||||
@Size(max = 64, message = "{validation.field.clientIp.size}")
|
||||
private String clientIp;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.daxpay.open.payment.unipay.param.gateway;
|
||||
|
||||
import cn.daxpay.open.payment.unipay.param.PaymentCommonParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/// # 网关订单查询参数
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Schema(title = "网关订单查询参数")
|
||||
public class GatewayOrderQueryParam extends PaymentCommonParam {
|
||||
|
||||
@Schema(description = "平台网关单号")
|
||||
@Size(max = 64, message = "{validation.field.orderNo.size}")
|
||||
private String orderNo;
|
||||
|
||||
@Schema(description = "商户业务单号")
|
||||
@Size(max = 100, message = "{validation.field.bizOrderNo.size}")
|
||||
private String bizOrderNo;
|
||||
|
||||
@Schema(description = "应用号")
|
||||
@Size(max = 32, message = "{validation.field.appId.size}")
|
||||
private String appId;
|
||||
|
||||
@Schema(description = "商户号")
|
||||
@NotBlank(message = "{validation.field.mchNo.notBlank}")
|
||||
@Size(max = 32, message = "{validation.field.mchNo.size}")
|
||||
private String mchNo;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package cn.daxpay.open.payment.unipay.param.gateway;
|
||||
|
||||
import cn.daxpay.open.payment.common.enums.GatewayPayTypeEnum;
|
||||
import cn.daxpay.open.payment.unipay.param.MerchantPaymentCommonParam;
|
||||
import cn.daxpay.open.payment.unipay.param.trade.pay.GoodsDetail;
|
||||
import cn.daxpay.open.payment.unipay.param.trade.pay.TerminalInfo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/// # 网关预下单参数
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Schema(title = "网关预下单参数")
|
||||
public class GatewayPrePayParam extends MerchantPaymentCommonParam {
|
||||
|
||||
@Schema(description = "商户订单号")
|
||||
@NotBlank(message = "{validation.field.bizOrderNo.notBlank}")
|
||||
@Size(max = 100, message = "{validation.field.bizOrderNo.size}")
|
||||
private String bizOrderNo;
|
||||
|
||||
@Schema(description = "支付标题")
|
||||
@NotBlank(message = "{validation.field.title.notBlank}")
|
||||
@Size(max = 100, message = "{validation.field.title.size}")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "支付描述")
|
||||
@Size(max = 50, message = "{validation.field.description.size}")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "支付金额(分)")
|
||||
@NotNull(message = "{validation.field.amount.notNull}")
|
||||
@Min(value = 1, message = "{validation.field.amount.min}")
|
||||
@Max(value = 9999999999L, message = "{validation.field.amount.max}")
|
||||
private Long amount;
|
||||
|
||||
/// @see GatewayPayTypeEnum
|
||||
@Schema(description = "网关支付类型 cashier/aggregate")
|
||||
@NotBlank(message = "{validation.field.gatewayPayType.notBlank}")
|
||||
@Size(max = 32, message = "{validation.field.gatewayPayType.size}")
|
||||
private String gatewayPayType;
|
||||
|
||||
@Schema(description = "异步通知地址")
|
||||
@Size(max = 256, message = "{validation.field.notifyUrl.size}")
|
||||
private String notifyUrl;
|
||||
|
||||
@Schema(description = "同步跳转地址")
|
||||
@Size(max = 256, message = "{validation.field.returnUrl.size}")
|
||||
private String returnUrl;
|
||||
|
||||
@Schema(description = "商户附加参数")
|
||||
@Size(max = 512, message = "{validation.field.attach.size}")
|
||||
private String attach;
|
||||
|
||||
@Schema(description = "过期时间")
|
||||
private OffsetDateTime expiredTime;
|
||||
|
||||
@Valid
|
||||
@Schema(description = "终端信息")
|
||||
private TerminalInfo terminal;
|
||||
|
||||
@Schema(description = "商品明细")
|
||||
private List<GoodsDetail> goodsDetail;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package cn.daxpay.open.payment.unipay.result.gateway;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 网关订单查询结果
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "网关订单查询结果")
|
||||
public class GatewayOrderResult {
|
||||
|
||||
@Schema(description = "平台网关单号")
|
||||
private String orderNo;
|
||||
|
||||
@Schema(description = "商户业务单号")
|
||||
private String bizOrderNo;
|
||||
|
||||
@Schema(description = "网关类型")
|
||||
private String gatewayType;
|
||||
|
||||
@Schema(description = "标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "金额(分)")
|
||||
private Long amount;
|
||||
|
||||
@Schema(description = "币种")
|
||||
private String currency;
|
||||
|
||||
@Schema(description = "业务状态")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "过期时间")
|
||||
private OffsetDateTime expiredTime;
|
||||
|
||||
@Schema(description = "支付成功时间")
|
||||
private OffsetDateTime payTime;
|
||||
|
||||
@Schema(description = "支付通道")
|
||||
private String channel;
|
||||
|
||||
@Schema(description = "支付方式")
|
||||
private String method;
|
||||
|
||||
@Schema(description = "支付产品")
|
||||
private String product;
|
||||
|
||||
@Schema(description = "资金交易号")
|
||||
private String tradeNo;
|
||||
|
||||
@Schema(description = "通道订单号")
|
||||
private String outOrderNo;
|
||||
|
||||
@Schema(description = "资金状态")
|
||||
private String fundStatus;
|
||||
|
||||
@Schema(description = "商户附加参数")
|
||||
private String attach;
|
||||
|
||||
@Schema(description = "同步跳转地址")
|
||||
private String returnUrl;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.daxpay.open.payment.unipay.result.gateway;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 网关预下单结果
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "网关预下单结果")
|
||||
public class GatewayPrePayResult {
|
||||
|
||||
@Schema(description = "平台网关单号")
|
||||
private String orderNo;
|
||||
|
||||
@Schema(description = "商户业务单号")
|
||||
private String bizOrderNo;
|
||||
|
||||
@Schema(description = "业务状态")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "网关落地页 URL")
|
||||
private String url;
|
||||
|
||||
@Schema(description = "过期时间")
|
||||
private OffsetDateTime expiredTime;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.daxpay.open.payment.web.gateway.controller;
|
||||
|
||||
import cn.daxpay.open.payment.core.trade.service.GatewayAggregatePayService;
|
||||
import cn.daxpay.open.payment.core.trade.service.GatewayOrderQueryService;
|
||||
import cn.daxpay.open.payment.unipay.param.gateway.AggregateQrPayParam;
|
||||
import cn.daxpay.open.payment.unipay.result.gateway.GatewayOrderResult;
|
||||
import cn.daxpay.open.payment.unipay.result.trade.pay.NormalPayResult;
|
||||
import cn.daxpay.open.platform.core.annotation.IgnoreAuth;
|
||||
import cn.daxpay.open.platform.core.annotation.IgnoreTenant;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/// # 网关 H5 侧接口(无商户签名)
|
||||
@IgnoreAuth
|
||||
@IgnoreTenant
|
||||
@Validated
|
||||
@Tag(name = "网关支付(H5)")
|
||||
@RestController
|
||||
@RequestMapping("/client/gateway")
|
||||
@RequiredArgsConstructor
|
||||
public class GatewayClientController {
|
||||
|
||||
private final GatewayOrderQueryService gatewayOrderQueryService;
|
||||
private final GatewayAggregatePayService gatewayAggregatePayService;
|
||||
|
||||
@Operation(summary = "查询网关订单摘要")
|
||||
@GetMapping("/order")
|
||||
public Result<GatewayOrderResult> getOrder(
|
||||
@NotBlank(message = "{validation.field.orderNo.notBlank}") String orderNo) {
|
||||
return Res.ok(gatewayOrderQueryService.queryByOrderNoNotTenant(orderNo));
|
||||
}
|
||||
|
||||
@Operation(summary = "聚合扫码发起支付")
|
||||
@PostMapping("/aggregate/pay")
|
||||
public Result<NormalPayResult> aggregatePay(@RequestBody @Validated AggregateQrPayParam param) {
|
||||
return Res.ok(gatewayAggregatePayService.aggregateQrPay(param));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package cn.daxpay.open.payment.web.unipay.controller;
|
||||
|
||||
import cn.daxpay.open.payment.common.aop.PaymentVerify;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.common.util.DaxRes;
|
||||
import cn.daxpay.open.payment.core.trade.service.GatewayOrderQueryService;
|
||||
import cn.daxpay.open.payment.core.trade.service.GatewayPayAssistService;
|
||||
import cn.daxpay.open.payment.merchant.service.permission.MerchantPermissionService;
|
||||
import cn.daxpay.open.payment.unipay.param.gateway.GatewayOrderQueryParam;
|
||||
import cn.daxpay.open.payment.unipay.param.gateway.GatewayPrePayParam;
|
||||
import cn.daxpay.open.payment.unipay.result.gateway.GatewayOrderResult;
|
||||
import cn.daxpay.open.payment.unipay.result.gateway.GatewayPrePayResult;
|
||||
import cn.daxpay.open.platform.core.annotation.IgnoreAuth;
|
||||
import cn.daxpay.open.platform.core.code.CommonCode;
|
||||
import cn.daxpay.open.platform.core.enums.common.PaymentApiEnum;
|
||||
import cn.daxpay.open.platform.core.exception.operation.OperationFailException;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/// # 网关支付开放接口
|
||||
@PaymentVerify
|
||||
@IgnoreAuth
|
||||
@Tag(name = "网关支付接口")
|
||||
@RestController
|
||||
@RequestMapping("/unipay/gateway")
|
||||
@RequiredArgsConstructor
|
||||
public class UniGatewayPayController {
|
||||
|
||||
private final GatewayPayAssistService gatewayPayAssistService;
|
||||
private final GatewayOrderQueryService gatewayOrderQueryService;
|
||||
private final MerchantPermissionService permConfigService;
|
||||
|
||||
@Operation(summary = "网关预下单")
|
||||
@PostMapping("/pre-pay")
|
||||
public DaxResult<GatewayPrePayResult> prePay(@RequestBody GatewayPrePayParam param) {
|
||||
if (!permConfigService.hasApiPerm(PaymentApiEnum.PRE_PAY.getCode())) {
|
||||
throw new OperationFailException(CommonCode.FAIL_CODE, "error.payment.order.merchantNoApiPermission");
|
||||
}
|
||||
return DaxRes.ok(gatewayPayAssistService.prePay(param));
|
||||
}
|
||||
|
||||
@Operation(summary = "网关订单查询")
|
||||
@PostMapping("/query")
|
||||
public DaxResult<GatewayOrderResult> query(@RequestBody GatewayOrderQueryParam param) {
|
||||
return DaxRes.ok(gatewayOrderQueryService.query(param));
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,8 @@ public class NormalPayOrderQueryService {
|
||||
.orElseThrow(() -> new DataNotExistException("pay.error.payOrderNotExist"));
|
||||
}
|
||||
// 联表资金凭证补充交易字段
|
||||
PayTrade trade = payTradeManager.findByContainerId(order.getId()).orElse(null);
|
||||
PayTrade trade = payTradeManager.findByContainerId(order.getId(),
|
||||
cn.daxpay.open.payment.common.enums.PayTradeTypeEnum.NORMAL.getCode()).orElse(null);
|
||||
return UnipayNormalPayOrderConvert.CONVERT.toResult(order, trade);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"wechat_pay": "WeChat",
|
||||
"alipay": "Alipay",
|
||||
"union_pay": "UnionPay"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"wait_pay": "Wait Pay",
|
||||
"paying": "Paying",
|
||||
"paid": "Paid",
|
||||
"closed": "Closed",
|
||||
"expired": "Expired"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"cashier": "Cashier",
|
||||
"aggregate": "Aggregate QR"
|
||||
}
|
||||
@@ -41,7 +41,16 @@
|
||||
"closeProcessing": "Order closing in progress, do not repeat",
|
||||
"closeFailed": "Close order failed",
|
||||
"syncNotStarted": "Order payment not started, please confirm payment status",
|
||||
"syncProcessing": "Payment sync in progress, do not repeat"
|
||||
"syncProcessing": "Payment sync in progress, do not repeat",
|
||||
"processing": "Payment processing, do not repeat"
|
||||
},
|
||||
"gateway": {
|
||||
"cashierNotReady": "Cashier payment is not available yet",
|
||||
"typeMismatch": "Gateway order type mismatch",
|
||||
"sceneNotSupport": "Unsupported cashier scene",
|
||||
"sceneNotConfigured": "Payment product/method not configured for this scene",
|
||||
"aggregateConfigMissing": "Aggregate QR pay config missing for app",
|
||||
"channelLocked": "Payment method locked for this order"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "Refund processing, do not repeat",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"wechat_pay": "微信环境",
|
||||
"alipay": "支付宝环境",
|
||||
"union_pay": "云闪付环境"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"wait_pay": "待支付",
|
||||
"paying": "支付中",
|
||||
"paid": "已支付",
|
||||
"closed": "已关闭",
|
||||
"expired": "已过期"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"cashier": "统一收银台",
|
||||
"aggregate": "聚合扫码"
|
||||
}
|
||||
@@ -40,7 +40,16 @@
|
||||
"closeProcessing": "支付订单已在关闭中,请勿重复发起",
|
||||
"closeFailed": "关闭订单失败",
|
||||
"syncNotStarted": "订单未开始支付, 请重新确认支付状态",
|
||||
"syncProcessing": "支付同步处理中,请勿重复操作"
|
||||
"syncProcessing": "支付同步处理中,请勿重复操作",
|
||||
"processing": "支付处理中,请勿重复操作"
|
||||
},
|
||||
"gateway": {
|
||||
"cashierNotReady": "收银台支付尚未开放",
|
||||
"typeMismatch": "网关订单类型不匹配",
|
||||
"sceneNotSupport": "不支持的收银场景",
|
||||
"sceneNotConfigured": "该场景未配置支付产品/方式",
|
||||
"aggregateConfigMissing": "应用未配置聚合扫码支付",
|
||||
"channelLocked": "订单已锁定支付方式,请勿切换"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "退款处理中,请勿重复操作",
|
||||
|
||||
@@ -32,21 +32,21 @@ public class SocialLoginConfigController {
|
||||
|
||||
private final SocialLoginConfigService socialLoginConfigService;
|
||||
|
||||
@PermCode(code = "view", nameCn = "配置查看", nameEn = "Config View")
|
||||
@PermCode(code = "view", nameCn = "社交登录配置查看", nameEn = "Social Login Config View")
|
||||
@Operation(summary = "全量查询平台配置(枚举驱动, 读时初始化缺失平台)")
|
||||
@GetMapping("/find-all")
|
||||
public Result<List<SocialLoginConfigResult>> findAll() {
|
||||
return Res.ok(socialLoginConfigService.findAll());
|
||||
}
|
||||
|
||||
@PermCode(code = "view", nameCn = "配置查看", nameEn = "Config View")
|
||||
@PermCode(code = "view", nameCn = "社交登录配置查看", nameEn = "Social Login Config View")
|
||||
@Operation(summary = "根据平台编码查询(不存在则初始化占位记录)")
|
||||
@GetMapping("/get-by-source")
|
||||
public Result<SocialLoginConfigResult> findBySource(@NotBlank(message = "{validation.field.source.notBlank}") String source) {
|
||||
return Res.ok(socialLoginConfigService.findBySource(source));
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "配置管理", nameEn = "Config Manage")
|
||||
@PermCode(code = "manage", nameCn = "社交登录配置管理", nameEn = "Social Login Config Manage")
|
||||
@Operation(summary = "修改平台配置")
|
||||
@PostMapping("/update")
|
||||
public Result<Void> update(@RequestBody @Validated SocialLoginConfigParam param) {
|
||||
@@ -54,7 +54,7 @@ public class SocialLoginConfigController {
|
||||
return Res.ok();
|
||||
}
|
||||
|
||||
@PermCode(code = "manage", nameCn = "配置管理", nameEn = "Config Manage")
|
||||
@PermCode(code = "manage", nameCn = "社交登录配置管理", nameEn = "Social Login Config Manage")
|
||||
@Operation(summary = "切换平台启用状态(仅已配置平台可启停)")
|
||||
@PostMapping("/update-enabled")
|
||||
public Result<Void> updateEnabled(
|
||||
|
||||
Reference in New Issue
Block a user