From e76058d05e4980a67ed1c6301e71c1b0dae621f9 Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Sat, 11 Jul 2026 09:27:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(payment):=20=E6=96=B0=E5=A2=9E=E7=BD=91?= =?UTF-8?q?=E5=85=B3=E6=94=AF=E4=BB=98(=E8=81=9A=E5=90=88=E6=89=AB?= =?UTF-8?q?=E7=A0=81)=E9=A2=86=E5=9F=9F=E4=B8=8E=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=9B=9E=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 落地 GatewayPayOrder 实体/管理端与 Uni 接口、商户聚合配置、超时 MQ/Job,并按 trade_type 回写普通/网关业务容器;补充收银台场景与网关相关枚举 i18n。 --- .../common/enums/CashierSceneEnum.java | 38 +++ .../common/enums/GatewayOrderStatusEnum.java | 42 ++++ .../common/enums/GatewayPayTypeEnum.java | 36 +++ .../common/enums/PayTradeTypeEnum.java | 2 + .../admin/GatewayPayOrderAdminController.java | 62 +++++ .../trade/convert/GatewayPayOrderConvert.java | 15 ++ .../trade/dao/GatewayPayOrderManager.java | 66 +++++ .../core/trade/dao/GatewayPayOrderMapper.java | 10 + .../core/trade/dao/PayTradeManager.java | 20 ++ .../core/trade/entity/GatewayPayOrder.java | 112 +++++++++ .../core/trade/job/GatewayTimeoutJob.java | 57 +++++ .../core/trade/mq/GatewayTimeoutConsumer.java | 34 +++ .../core/trade/mq/GatewayTimeoutMessage.java | 18 ++ .../core/trade/mq/PayArtemisConstants.java | 4 + .../trade/param/GatewayPayOrderQuery.java | 55 +++++ .../trade/result/GatewayPayOrderResult.java | 96 ++++++++ .../service/GatewayAggregatePayService.java | 64 +++++ .../service/GatewayOrderQueryService.java | 78 ++++++ .../service/GatewayPayAssistService.java | 182 ++++++++++++++ .../service/GatewayPayHandleService.java | 232 ++++++++++++++++++ .../trade/service/GatewayTimeoutService.java | 65 +++++ .../core/trade/service/PayRefundService.java | 3 +- .../trade/service/PayUniHandleService.java | 118 ++++++--- .../admin/GatewayPayOrderAdminService.java | 86 +++++++ .../admin/NormalPayOrderAdminService.java | 6 +- ...GatewayAggregateConfigAdminController.java | 42 ++++ .../GatewayAggregateConfigManager.java | 19 ++ .../gateway/GatewayAggregateConfigMapper.java | 10 + .../gateway/GatewayAggregateConfig.java | 44 ++++ .../gateway/GatewayAggregateConfigParam.java | 49 ++++ .../gateway/GatewayAggregateConfigResult.java | 42 ++++ .../GatewayAggregateConfigService.java | 58 +++++ .../param/gateway/AggregateQrPayParam.java | 36 +++ .../param/gateway/GatewayOrderQueryParam.java | 32 +++ .../param/gateway/GatewayPrePayParam.java | 69 ++++++ .../result/gateway/GatewayOrderResult.java | 68 +++++ .../result/gateway/GatewayPrePayResult.java | 29 +++ .../controller/GatewayClientController.java | 44 ++++ .../controller/UniGatewayPayController.java | 52 ++++ .../service/NormalPayOrderQueryService.java | 3 +- .../i18n/en-US/enum/cashier_scene.json | 5 + .../i18n/en-US/enum/gateway_order_status.json | 7 + .../i18n/en-US/enum/gateway_pay_type.json | 4 + .../main/resources/i18n/en-US/pay/error.json | 11 +- .../i18n/zh-CN/enum/cashier_scene.json | 5 + .../i18n/zh-CN/enum/gateway_order_status.json | 7 + .../i18n/zh-CN/enum/gateway_pay_type.json | 4 + .../main/resources/i18n/zh-CN/pay/error.json | 11 +- .../social/SocialLoginConfigController.java | 8 +- 49 files changed, 2112 insertions(+), 48 deletions(-) create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/CashierSceneEnum.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/GatewayOrderStatusEnum.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/GatewayPayTypeEnum.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/controller/admin/GatewayPayOrderAdminController.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/convert/GatewayPayOrderConvert.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/GatewayPayOrderManager.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/GatewayPayOrderMapper.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/entity/GatewayPayOrder.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/job/GatewayTimeoutJob.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/GatewayTimeoutConsumer.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/GatewayTimeoutMessage.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/param/GatewayPayOrderQuery.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/result/GatewayPayOrderResult.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayAggregatePayService.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayOrderQueryService.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayPayAssistService.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayPayHandleService.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayTimeoutService.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/admin/GatewayPayOrderAdminService.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/controller/admin/gateway/GatewayAggregateConfigAdminController.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/gateway/GatewayAggregateConfigManager.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/gateway/GatewayAggregateConfigMapper.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/entity/gateway/GatewayAggregateConfig.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/param/gateway/GatewayAggregateConfigParam.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/result/gateway/GatewayAggregateConfigResult.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/service/gateway/GatewayAggregateConfigService.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/AggregateQrPayParam.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayOrderQueryParam.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayPrePayParam.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/GatewayOrderResult.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/GatewayPrePayResult.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/gateway/controller/GatewayClientController.java create mode 100644 daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/unipay/controller/UniGatewayPayController.java create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/cashier_scene.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/gateway_order_status.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/gateway_pay_type.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/cashier_scene.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/gateway_order_status.json create mode 100644 daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/gateway_pay_type.json diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/CashierSceneEnum.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/CashierSceneEnum.java new file mode 100644 index 000000000..d1de43507 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/CashierSceneEnum.java @@ -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)); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/GatewayOrderStatusEnum.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/GatewayOrderStatusEnum.java new file mode 100644 index 000000000..3ca11e159 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/GatewayOrderStatusEnum.java @@ -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)); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/GatewayPayTypeEnum.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/GatewayPayTypeEnum.java new file mode 100644 index 000000000..b043cfe1c --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/GatewayPayTypeEnum.java @@ -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)); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/PayTradeTypeEnum.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/PayTradeTypeEnum.java index 94e341101..4efb24914 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/PayTradeTypeEnum.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/enums/PayTradeTypeEnum.java @@ -17,6 +17,8 @@ public enum PayTradeTypeEnum implements I18nSupport { /// 普通支付 NORMAL("normal"), + /// 网关支付(聚合扫码/收银台) + GATEWAY("gateway"), /// 预授权冻结 AUTHORIZE("authorize"), /// 预授权捕获扣款 diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/controller/admin/GatewayPayOrderAdminController.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/controller/admin/GatewayPayOrderAdminController.java new file mode 100644 index 000000000..3ffad2894 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/controller/admin/GatewayPayOrderAdminController.java @@ -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> 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 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 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 close( + @NotNull(message = "{validation.field.id.notNull}") Long id, + @RequestParam(defaultValue = "false") boolean useCancel) { + gatewayPayOrderAdminService.close(id, useCancel); + return Res.ok(); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/convert/GatewayPayOrderConvert.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/convert/GatewayPayOrderConvert.java new file mode 100644 index 000000000..6a55c5d71 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/convert/GatewayPayOrderConvert.java @@ -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); +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/GatewayPayOrderManager.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/GatewayPayOrderManager.java new file mode 100644 index 000000000..42dd608ba --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/GatewayPayOrderManager.java @@ -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 { + + /// 按平台网关单号查询 + public Optional findByOrderNo(String orderNo) { + return lambdaQuery() + .eq(GatewayPayOrder::getOrderNo, orderNo) + .oneOpt(); + } + + /// 按平台网关单号查询(忽略租户, H5 落地页用) + @IgnoreTenant + public Optional findByOrderNoNotTenant(String orderNo) { + return lambdaQuery() + .eq(GatewayPayOrder::getOrderNo, orderNo) + .oneOpt(); + } + + /// 按商户业务单号 + 应用号查询 + public Optional findByBizOrderNo(String bizOrderNo, String appId) { + return lambdaQuery() + .eq(GatewayPayOrder::getBizOrderNo, bizOrderNo) + .eq(GatewayPayOrder::getAppId, appId) + .oneOpt(); + } + + /// 分页查询 + public Page page(PageParam pageParam, GatewayPayOrderQuery query) { + Page mpPage = MpUtil.getMpPage(pageParam); + QueryWrapper wrapper = QueryGenerator.generator(query); + wrapper.orderByDesc("create_time"); + return this.page(mpPage, wrapper); + } + + /// 已过期仍待支付/支付中的网关单(超时兜底) + @IgnoreTenant + public List 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(); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/GatewayPayOrderMapper.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/GatewayPayOrderMapper.java new file mode 100644 index 000000000..eb0b59e74 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/GatewayPayOrderMapper.java @@ -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 { +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/PayTradeManager.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/PayTradeManager.java index 753a335df..b813bce43 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/PayTradeManager.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/dao/PayTradeManager.java @@ -49,6 +49,26 @@ public class PayTradeManager extends BaseManager { .oneOpt(); } + /// 根据容器ID + 交易形态查询 + public Optional findByContainerId(Long containerId, String tradeType) { + return lambdaQuery() + .eq(PayTrade::getContainerId, containerId) + .eq(PayTrade::getTradeType, tradeType) + .oneOpt(); + } + + /// 查询网关支付已超时但仍处理中的资金交易(兜底) + @IgnoreTenant + public List 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 page(PageParam pageParam, PayTradeQuery query) { Page mpPage = MpUtil.getMpPage(pageParam); diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/entity/GatewayPayOrder.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/entity/GatewayPayOrder.java new file mode 100644 index 000000000..533707b5e --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/entity/GatewayPayOrder.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/job/GatewayTimeoutJob.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/job/GatewayTimeoutJob.java new file mode 100644 index 000000000..dda957809 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/job/GatewayTimeoutJob.java @@ -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 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 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); + } + } + } + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/GatewayTimeoutConsumer.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/GatewayTimeoutConsumer.java new file mode 100644 index 000000000..b763c7329 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/GatewayTimeoutConsumer.java @@ -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); + } + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/GatewayTimeoutMessage.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/GatewayTimeoutMessage.java new file mode 100644 index 000000000..ee589bcf5 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/GatewayTimeoutMessage.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/PayArtemisConstants.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/PayArtemisConstants.java index c562c31f1..6ba26c681 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/PayArtemisConstants.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/mq/PayArtemisConstants.java @@ -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"; } diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/param/GatewayPayOrderQuery.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/param/GatewayPayOrderQuery.java new file mode 100644 index 000000000..d47dc18f6 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/param/GatewayPayOrderQuery.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/result/GatewayPayOrderResult.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/result/GatewayPayOrderResult.java new file mode 100644 index 000000000..600b5e001 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/result/GatewayPayOrderResult.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayAggregatePayService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayAggregatePayService.java new file mode 100644 index 000000000..e21d4f04e --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayAggregatePayService.java @@ -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); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayOrderQueryService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayOrderQueryService.java new file mode 100644 index 000000000..1ceb80776 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayOrderQueryService.java @@ -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; + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayPayAssistService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayPayAssistService.java new file mode 100644 index 000000000..7f07ee6f2 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayPayAssistService.java @@ -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); + } + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayPayHandleService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayPayHandleService.java new file mode 100644 index 000000000..93d4959cd --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayPayHandleService.java @@ -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()); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayTimeoutService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayTimeoutService.java new file mode 100644 index 000000000..927b34fb1 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/GatewayTimeoutService.java @@ -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); + } + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/PayRefundService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/PayRefundService.java index 099fc67ef..6ae623acb 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/PayRefundService.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/PayRefundService.java @@ -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")); } diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/PayUniHandleService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/PayUniHandleService.java index c56801356..39a8a0d5e 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/PayUniHandleService.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/PayUniHandleService.java @@ -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()); } } diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/admin/GatewayPayOrderAdminService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/admin/GatewayPayOrderAdminService.java new file mode 100644 index 000000000..db6d31907 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/admin/GatewayPayOrderAdminService.java @@ -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 page(PageParam pageParam, GatewayPayOrderQuery query) { + Page page = gatewayPayOrderManager.page(pageParam, query); + var records = page.getRecords().stream() + .map(GatewayPayOrderConvert.CONVERT::toResult) + .toList(); + return new PageResult() + .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); + } + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/admin/NormalPayOrderAdminService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/admin/NormalPayOrderAdminService.java index d93329490..e51b679a6 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/admin/NormalPayOrderAdminService.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/core/trade/service/admin/NormalPayOrderAdminService.java @@ -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); } diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/controller/admin/gateway/GatewayAggregateConfigAdminController.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/controller/admin/gateway/GatewayAggregateConfigAdminController.java new file mode 100644 index 000000000..a1e71f344 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/controller/admin/gateway/GatewayAggregateConfigAdminController.java @@ -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 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 saveOrUpdate(@RequestBody @Validated GatewayAggregateConfigParam param) { + gatewayAggregateConfigService.saveOrUpdate(param); + return Res.ok(); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/gateway/GatewayAggregateConfigManager.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/gateway/GatewayAggregateConfigManager.java new file mode 100644 index 000000000..d30b857b9 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/gateway/GatewayAggregateConfigManager.java @@ -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 { + + /// 按应用号查询 + public Optional findByAppId(String appId) { + return lambdaQuery() + .eq(GatewayAggregateConfig::getAppId, appId) + .oneOpt(); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/gateway/GatewayAggregateConfigMapper.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/gateway/GatewayAggregateConfigMapper.java new file mode 100644 index 000000000..c42162327 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/gateway/GatewayAggregateConfigMapper.java @@ -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 { +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/entity/gateway/GatewayAggregateConfig.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/entity/gateway/GatewayAggregateConfig.java new file mode 100644 index 000000000..c4cba23f8 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/entity/gateway/GatewayAggregateConfig.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/param/gateway/GatewayAggregateConfigParam.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/param/gateway/GatewayAggregateConfigParam.java new file mode 100644 index 000000000..5db571b8a --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/param/gateway/GatewayAggregateConfigParam.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/result/gateway/GatewayAggregateConfigResult.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/result/gateway/GatewayAggregateConfigResult.java new file mode 100644 index 000000000..af162f260 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/result/gateway/GatewayAggregateConfigResult.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/service/gateway/GatewayAggregateConfigService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/service/gateway/GatewayAggregateConfigService.java new file mode 100644 index 000000000..a05075db0 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/service/gateway/GatewayAggregateConfigService.java @@ -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; + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/AggregateQrPayParam.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/AggregateQrPayParam.java new file mode 100644 index 000000000..b6819ddb9 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/AggregateQrPayParam.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayOrderQueryParam.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayOrderQueryParam.java new file mode 100644 index 000000000..5d2799ea2 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayOrderQueryParam.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayPrePayParam.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayPrePayParam.java new file mode 100644 index 000000000..f55d9d6b9 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/param/gateway/GatewayPrePayParam.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/GatewayOrderResult.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/GatewayOrderResult.java new file mode 100644 index 000000000..91cc6b3ca --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/GatewayOrderResult.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/GatewayPrePayResult.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/GatewayPrePayResult.java new file mode 100644 index 000000000..932c35a81 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/unipay/result/gateway/GatewayPrePayResult.java @@ -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; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/gateway/controller/GatewayClientController.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/gateway/controller/GatewayClientController.java new file mode 100644 index 000000000..85cb8c322 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/gateway/controller/GatewayClientController.java @@ -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 getOrder( + @NotBlank(message = "{validation.field.orderNo.notBlank}") String orderNo) { + return Res.ok(gatewayOrderQueryService.queryByOrderNoNotTenant(orderNo)); + } + + @Operation(summary = "聚合扫码发起支付") + @PostMapping("/aggregate/pay") + public Result aggregatePay(@RequestBody @Validated AggregateQrPayParam param) { + return Res.ok(gatewayAggregatePayService.aggregateQrPay(param)); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/unipay/controller/UniGatewayPayController.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/unipay/controller/UniGatewayPayController.java new file mode 100644 index 000000000..02d598a3c --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/unipay/controller/UniGatewayPayController.java @@ -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 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 query(@RequestBody GatewayOrderQueryParam param) { + return DaxRes.ok(gatewayOrderQueryService.query(param)); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/unipay/service/NormalPayOrderQueryService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/unipay/service/NormalPayOrderQueryService.java index 5a6232e52..05ee50505 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/unipay/service/NormalPayOrderQueryService.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/web/unipay/service/NormalPayOrderQueryService.java @@ -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); } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/cashier_scene.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/cashier_scene.json new file mode 100644 index 000000000..342838846 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/cashier_scene.json @@ -0,0 +1,5 @@ +{ + "wechat_pay": "WeChat", + "alipay": "Alipay", + "union_pay": "UnionPay" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/gateway_order_status.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/gateway_order_status.json new file mode 100644 index 000000000..0c4848e41 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/gateway_order_status.json @@ -0,0 +1,7 @@ +{ + "wait_pay": "Wait Pay", + "paying": "Paying", + "paid": "Paid", + "closed": "Closed", + "expired": "Expired" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/gateway_pay_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/gateway_pay_type.json new file mode 100644 index 000000000..e461d9857 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/gateway_pay_type.json @@ -0,0 +1,4 @@ +{ + "cashier": "Cashier", + "aggregate": "Aggregate QR" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json index 11d32b5b4..9107c28a8 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json @@ -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", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/cashier_scene.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/cashier_scene.json new file mode 100644 index 000000000..8ab2ec793 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/cashier_scene.json @@ -0,0 +1,5 @@ +{ + "wechat_pay": "微信环境", + "alipay": "支付宝环境", + "union_pay": "云闪付环境" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/gateway_order_status.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/gateway_order_status.json new file mode 100644 index 000000000..5bbf1c68c --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/gateway_order_status.json @@ -0,0 +1,7 @@ +{ + "wait_pay": "待支付", + "paying": "支付中", + "paid": "已支付", + "closed": "已关闭", + "expired": "已过期" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/gateway_pay_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/gateway_pay_type.json new file mode 100644 index 000000000..f9c653eac --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/gateway_pay_type.json @@ -0,0 +1,4 @@ +{ + "cashier": "统一收银台", + "aggregate": "聚合扫码" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json index 47e610bdd..4b5bf149b 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json @@ -40,7 +40,16 @@ "closeProcessing": "支付订单已在关闭中,请勿重复发起", "closeFailed": "关闭订单失败", "syncNotStarted": "订单未开始支付, 请重新确认支付状态", - "syncProcessing": "支付同步处理中,请勿重复操作" + "syncProcessing": "支付同步处理中,请勿重复操作", + "processing": "支付处理中,请勿重复操作" + }, + "gateway": { + "cashierNotReady": "收银台支付尚未开放", + "typeMismatch": "网关订单类型不匹配", + "sceneNotSupport": "不支持的收银场景", + "sceneNotConfigured": "该场景未配置支付产品/方式", + "aggregateConfigMissing": "应用未配置聚合扫码支付", + "channelLocked": "订单已锁定支付方式,请勿切换" }, "refund": { "processing": "退款处理中,请勿重复操作", diff --git a/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/controller/social/SocialLoginConfigController.java b/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/controller/social/SocialLoginConfigController.java index 78dd96e5c..498913456 100644 --- a/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/controller/social/SocialLoginConfigController.java +++ b/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/controller/social/SocialLoginConfigController.java @@ -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> 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 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 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 updateEnabled(