diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsRefundReq.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsRefundReq.java index 7dab1b15c..1fab074a9 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsRefundReq.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsRefundReq.java @@ -4,6 +4,8 @@ import cn.daxpay.open.channel.ums.client.credential.UmsSdkCredential; import cn.daxpay.open.channel.ums.client.enums.UmsPayMethod; import lombok.Data; +import java.time.OffsetDateTime; + /// # 银联商务通道退款请求 @Data public class UmsRefundReq { @@ -11,8 +13,8 @@ public class UmsRefundReq { /// 原商户订单号 private String outTradeNo; - /// 原订单日期(yyyy-MM-dd, 扫码退款必填) - private String billDate; + /// 原订单创建时间(UTC), 由子应用按通道时区转换为银联商务 billDate(yyyy-MM-dd) + private OffsetDateTime billDate; /// 退款单号 private String outRefundNo; diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsRefundSyncReq.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsRefundSyncReq.java index 693118d92..3af344cc6 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsRefundSyncReq.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsRefundSyncReq.java @@ -4,6 +4,8 @@ import cn.daxpay.open.channel.ums.client.credential.UmsSdkCredential; import cn.daxpay.open.channel.ums.client.enums.UmsPayMethod; import lombok.Data; +import java.time.OffsetDateTime; + /// # 银联商务通道退款同步请求 @Data public class UmsRefundSyncReq { @@ -14,8 +16,8 @@ public class UmsRefundSyncReq { /// 原商户订单号 private String outTradeNo; - /// 原订单日期(yyyy-MM-dd, 扫码退款查询必填) - private String billDate; + /// 原订单创建时间(UTC), 由子应用按通道时区转换为银联商务 billDate(yyyy-MM-dd) + private OffsetDateTime billDate; /// 支付方式(区分扫码/H5 退款查询) private UmsPayMethod method; diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsSyncReq.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsSyncReq.java index d3e0cdf83..3995d4a1a 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsSyncReq.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/client/req/UmsSyncReq.java @@ -4,6 +4,8 @@ import cn.daxpay.open.channel.ums.client.credential.UmsSdkCredential; import cn.daxpay.open.channel.ums.client.enums.UmsPayMethod; import lombok.Data; +import java.time.OffsetDateTime; + /// # 银联商务通道支付同步请求 @Data public class UmsSyncReq { @@ -11,8 +13,8 @@ public class UmsSyncReq { /// 商户订单号 private String outTradeNo; - /// 订单日期(yyyy-MM-dd, 扫码查询必填) - private String billDate; + /// 原订单创建时间(UTC), 由子应用按通道时区转换为银联商务 billDate(yyyy-MM-dd) + private OffsetDateTime billDate; /// 支付方式(区分扫码/H5 查询) private UmsPayMethod method; diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/controller/direct/UmsDirectChannelMerchantController.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/controller/direct/UmsDirectChannelMerchantController.java index 02323b718..a9fecd805 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/controller/direct/UmsDirectChannelMerchantController.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/controller/direct/UmsDirectChannelMerchantController.java @@ -2,7 +2,6 @@ package cn.daxpay.open.channel.ums.controller.direct; import cn.daxpay.open.channel.ums.param.direct.UmsDirectChannelMerchantCreateParam; import cn.daxpay.open.channel.ums.param.direct.UmsDirectKeyConfigParam; -import cn.daxpay.open.channel.ums.result.direct.UmsDirectChannelMerchantResult; import cn.daxpay.open.channel.ums.result.direct.UmsDirectKeyConfigResult; import cn.daxpay.open.channel.ums.service.direct.UmsDirectChannelMerchantService; import cn.daxpay.open.channel.ums.service.direct.UmsDirectKeyConfigService; @@ -22,8 +21,8 @@ import org.springframework.web.bind.annotation.RestController; /// # 银联商务直连通道商户管理 /// -/// 提供通道商户创建、查询和密钥配置管理。 -/// 银联商务签名无证书, 密钥配置为 umsAppId/appKey/secretKey 三件套。 +/// 提供通道商户创建和密钥配置管理。 +/// 商户身份(mid)创建时录入, 应用ID/终端号(tid)/应用密钥/通讯密钥由密钥配置维护。 @PermCode(menuCode = "channel:merchant") @Validated @Tag(name = "银联商务直连通道商户管理") @@ -35,14 +34,6 @@ public class UmsDirectChannelMerchantController { private final UmsDirectChannelMerchantService umsDirectChannelMerchantService; private final UmsDirectKeyConfigService umsDirectKeyConfigService; - @PermCode(code = "view", nameCn = "通道商户查看", nameEn = "Channel Merchant View") - @Operation(summary = "根据通道商户号查询银联商务直连通道商户配置") - @GetMapping("/find-by-channel-mch-no") - public Result findByChannelMchNo( - @NotBlank(message = "{validation.field.channelMerchantNo.notBlank}") String channelMchNo) { - return Res.ok(umsDirectChannelMerchantService.findByChannelMchNo(channelMchNo)); - } - @PermCode(code = "manage", nameCn = "通道商户管理", nameEn = "Channel Merchant Manage") @Operation(summary = "创建银联商务直连通道商户") @PostMapping("/create") diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/convert/direct/UmsDirectChannelMerchantConvert.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/convert/direct/UmsDirectChannelMerchantConvert.java deleted file mode 100644 index 63fcc6e59..000000000 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/convert/direct/UmsDirectChannelMerchantConvert.java +++ /dev/null @@ -1,14 +0,0 @@ -package cn.daxpay.open.channel.ums.convert.direct; - -import cn.daxpay.open.channel.ums.entity.direct.UmsDirectChannelMerchant; -import cn.daxpay.open.channel.ums.result.direct.UmsDirectChannelMerchantResult; -import org.mapstruct.Mapper; -import org.mapstruct.factory.Mappers; - -/// # 银联商务直连通道商户绑定转换 -@Mapper -public interface UmsDirectChannelMerchantConvert { - UmsDirectChannelMerchantConvert CONVERT = Mappers.getMapper(UmsDirectChannelMerchantConvert.class); - - UmsDirectChannelMerchantResult toResult(UmsDirectChannelMerchant entity); -} diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/dao/direct/UmsDirectChannelMerchantManager.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/dao/direct/UmsDirectChannelMerchantManager.java deleted file mode 100644 index 65aa1187c..000000000 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/dao/direct/UmsDirectChannelMerchantManager.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.daxpay.open.channel.ums.dao.direct; - -import cn.daxpay.open.channel.ums.entity.direct.UmsDirectChannelMerchant; -import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -/// # 银联商务直连通道商户绑定 -@Repository -public class UmsDirectChannelMerchantManager extends BaseManager { - - /// 根据通道商户号查询 - public Optional findByChannelMchNo(String channelMchNo) { - return lambdaQuery() - .eq(UmsDirectChannelMerchant::getChannelMchNo, channelMchNo) - .oneOpt(); - } -} diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/dao/direct/UmsDirectChannelMerchantMapper.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/dao/direct/UmsDirectChannelMerchantMapper.java deleted file mode 100644 index 245a1bee3..000000000 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/dao/direct/UmsDirectChannelMerchantMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package cn.daxpay.open.channel.ums.dao.direct; - -import cn.daxpay.open.channel.ums.entity.direct.UmsDirectChannelMerchant; -import com.github.yulichang.base.MPJBaseMapper; -import org.apache.ibatis.annotations.Mapper; - -/// # 银联商务直连通道商户绑定 -@Mapper -public interface UmsDirectChannelMerchantMapper extends MPJBaseMapper { -} diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/entity/direct/UmsDirectChannelMerchant.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/entity/direct/UmsDirectChannelMerchant.java deleted file mode 100644 index e874f0711..000000000 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/entity/direct/UmsDirectChannelMerchant.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.daxpay.open.channel.ums.entity.direct; - -import cn.daxpay.open.channel.ums.convert.direct.UmsDirectChannelMerchantConvert; -import cn.daxpay.open.channel.ums.result.direct.UmsDirectChannelMerchantResult; -import cn.daxpay.open.payment.common.entity.merchant.MchBaseEntity; -import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.experimental.Accessors; - -/// # 银联商务直连通道商户绑定 -/// -/// 一个银联商务商户号(merchantNo)对应一个 channelMchNo, 包含终端号与订单前缀。 -/// 银联商务为聚合支付, 一个商户号同时支持支付宝/微信/银联的扫码与 H5。 -@EqualsAndHashCode(callSuper = true) -@Data -@Accessors(chain = true) -@TableName(value = "ums_direct_channel_merchant", autoResultMap = true) -public class UmsDirectChannelMerchant extends MchBaseEntity implements ToResult { - - /// 通道商户号(系统生成雪花号) - private String channelMchNo; - - /// 所属支付产品 - /// @see cn.daxpay.open.platform.core.enums.pay.channel.ProductEnum - private String product; - - /// 银联商务商户号(mid) - private String merchantNo; - - /// 终端号(tid) - private String terminalNo; - - /// 订单号前缀(用于生成关联订单号) - private String orderPrefix; - - /// 是否沙箱环境 - private boolean sandbox; - - @Override - public UmsDirectChannelMerchantResult toResult() { - return UmsDirectChannelMerchantConvert.CONVERT.toResult(this); - } -} diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/entity/direct/UmsDirectKeyConfig.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/entity/direct/UmsDirectKeyConfig.java index 4d89404e1..22534ee15 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/entity/direct/UmsDirectKeyConfig.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/entity/direct/UmsDirectKeyConfig.java @@ -12,9 +12,10 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; -/// # 银联商务直连密钥配置 +/// # 银联商务直连配置 /// -/// 直连商户维度的密钥配置, 敏感字段(appKey/secretKey)加密存储。 +/// 直连商户维度的通道配置, 含商户身份(mid/tid)与签名密钥, 敏感字段(appKey/secretKey)加密存储。 +/// merchantNo(mid) 创建时录入不可修改, terminalNo(tid)/umsAppId/appKey/secretKey 由密钥配置维护。 /// 银联商务签名无需证书, 仅依赖 appKey(HmacSHA256) 与 secretKey(回调验签)。 @EqualsAndHashCode(callSuper = true) @Data @@ -26,6 +27,13 @@ public class UmsDirectKeyConfig extends MchBaseEntity implements ToResult, 嵌套对象转 JSON 字符串) @@ -103,10 +100,8 @@ public class UmsPayCallbackService { data.setTradeNo(resp.getOutTradeNo()); // resp.targetOrderId 是第三方交易号 data.setOutTradeNo(resp.getTargetOrderId()); - // 完成时间 - if (StrUtil.isNotBlank(resp.getFinishTime())) { - data.setFinishTime(OffsetDateTime.parse(resp.getFinishTime(), TIME_FORMAT)); - } + // 完成时间(银联商务返回东八区本地时间, 由 UmsDateUtil 解析为带偏移的 OffsetDateTime) + data.setFinishTime(UmsDateUtil.parseCst(resp.getFinishTime())); // 交易状态映射 data.setTradeStatus(this.mapStatus(resp.getTradeStatus())); return data; diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectChannelMerchantService.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectChannelMerchantService.java index 75535ba4d..6e3dce6e3 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectChannelMerchantService.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectChannelMerchantService.java @@ -1,14 +1,11 @@ package cn.daxpay.open.channel.ums.service.direct; -import cn.daxpay.open.channel.ums.dao.direct.UmsDirectChannelMerchantManager; -import cn.daxpay.open.channel.ums.entity.direct.UmsDirectChannelMerchant; +import cn.daxpay.open.channel.ums.dao.direct.UmsDirectKeyConfigManager; +import cn.daxpay.open.channel.ums.entity.direct.UmsDirectKeyConfig; import cn.daxpay.open.channel.ums.param.direct.UmsDirectChannelMerchantCreateParam; -import cn.daxpay.open.channel.ums.param.direct.UmsDirectKeyConfigParam; -import cn.daxpay.open.channel.ums.result.direct.UmsDirectChannelMerchantResult; import cn.daxpay.open.payment.channel.dao.mch.ChannelMerchantManager; import cn.daxpay.open.payment.channel.entity.mch.ChannelMerchant; import cn.daxpay.open.platform.core.enums.channel.ChannelMerchantSourceEnum; -import cn.daxpay.open.platform.core.exception.DataNotExistException; import cn.hutool.core.util.IdUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,20 +14,22 @@ import org.springframework.transaction.annotation.Transactional; /// # 银联商务直连通道商户管理 /// -/// 管理通道商户绑定的创建/查询/删除。 -/// 银联商务签名无证书, 创建时需同时写入密钥配置(umsAppId/appKey/secretKey)。 +/// 管理通道商户绑定的创建/删除。 +/// 商户身份(mid)在创建时录入直连配置表, +/// 应用ID(umsAppId)/终端号(tid)/应用密钥(appKey)/通讯密钥(secretKey)由密钥配置单独维护, +/// 沙箱环境运行时读取支付产品配置(pay_md_product_config.activeEnv)。 @Slf4j @Service @RequiredArgsConstructor public class UmsDirectChannelMerchantService { private final ChannelMerchantManager channelMerchantManager; - private final UmsDirectChannelMerchantManager umsDirectChannelMerchantManager; - private final UmsDirectKeyConfigService umsDirectKeyConfigService; + private final UmsDirectKeyConfigManager umsDirectKeyConfigManager; /// 创建通道商户绑定 /// - /// 同时写入通用通道商户主表、银联商务直连绑定表和密钥配置表。 + /// 同时写入通用通道商户主表和银联商务直连配置表(含商户号 mid)。 + /// umsAppId/terminalNo/appKey/secretKey 由密钥配置单独维护。 @Transactional(rollbackFor = Exception.class) public void create(UmsDirectChannelMerchantCreateParam param) { // 生成通道商户号(雪花号) @@ -44,40 +43,18 @@ public class UmsDirectChannelMerchantService { channelMerchant.setSource(ChannelMerchantSourceEnum.MANUAL.getCode()); channelMerchant.setEnable(true); channelMerchantManager.save(channelMerchant); - // 写银联商务直连绑定表 - var entity = new UmsDirectChannelMerchant(); - entity.setMchNo(param.getMchNo()); - entity.setChannelMchNo(channelMchNo); - entity.setProduct(param.getProduct()); - entity.setMerchantNo(param.getMerchantNo()); - entity.setTerminalNo(param.getTerminalNo()); - entity.setOrderPrefix(param.getOrderPrefix()); - entity.setSandbox(param.isSandbox()); - umsDirectChannelMerchantManager.save(entity); - // 同时保存密钥配置(umsAppId/appKey/secretKey) - var keyParam = new UmsDirectKeyConfigParam(); - keyParam.setChannelMchNo(channelMchNo); - keyParam.setMchNo(param.getMchNo()); - keyParam.setUmsAppId(param.getUmsAppId()); - keyParam.setAppKey(param.getAppKey()); - keyParam.setSecretKey(param.getSecretKey()); - umsDirectKeyConfigService.save(keyParam); + // 写银联商务直连配置(仅商户身份 mid, umsAppId/terminalNo/appKey/secretKey 由密钥配置单独维护) + var keyConfig = new UmsDirectKeyConfig() + .setChannelMchNo(channelMchNo) + .setMerchantNo(param.getMerchantNo()); + // mchNo 继承自 MchBaseEntity, 链式返回父类型, 单独赋值 + keyConfig.setMchNo(param.getMchNo()); + umsDirectKeyConfigManager.save(keyConfig); } - /// 根据通道商户号查询银联商务直连通道商户配置 - public UmsDirectChannelMerchantResult findByChannelMchNo(String channelMchNo) { - return umsDirectChannelMerchantManager.findByChannelMchNo(channelMchNo) - .map(UmsDirectChannelMerchant::toResult) - // 通道: 通道商户配置不存在 - .orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist")); - } - - /// 根据通道商户号删除(级联删除密钥) + /// 根据通道商户号删除(级联删除直连配置) @Transactional(rollbackFor = Exception.class) public void deleteByChannelMchNo(String channelMchNo) { - umsDirectKeyConfigService.deleteByChannelMchNo(channelMchNo); - umsDirectChannelMerchantManager.lambdaUpdate() - .eq(UmsDirectChannelMerchant::getChannelMchNo, channelMchNo) - .remove(); + umsDirectKeyConfigManager.deleteByChannelMchNo(channelMchNo); } } diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectConfigAssembler.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectConfigAssembler.java index de94746c7..e304f4fdd 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectConfigAssembler.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectConfigAssembler.java @@ -1,9 +1,12 @@ package cn.daxpay.open.channel.ums.service.direct; import cn.daxpay.open.channel.ums.client.credential.UmsSdkCredential; -import cn.daxpay.open.channel.ums.dao.direct.UmsDirectChannelMerchantManager; -import cn.daxpay.open.channel.ums.entity.direct.UmsDirectChannelMerchant; import cn.daxpay.open.channel.ums.entity.direct.UmsDirectKeyConfig; +import cn.daxpay.open.payment.channel.dao.mch.ChannelMerchantManager; +import cn.daxpay.open.payment.channel.entity.mch.ChannelMerchant; +import cn.daxpay.open.payment.masterdata.constants.product.dao.PayProductConfigManager; +import cn.daxpay.open.payment.masterdata.constants.product.entity.PayProductConfig; +import cn.daxpay.open.platform.core.enums.pay.config.PayEnvEnum; import cn.daxpay.open.platform.core.exception.DataNotExistException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -11,8 +14,10 @@ import org.springframework.stereotype.Service; /// # 银联商务直连通道凭证组装器 /// -/// 从进件实体([UmsDirectChannelMerchant] + [UmsDirectKeyConfig])读取 -/// appId / appKey / 商户号 / 终端号 / 通讯密钥, 组装为下发给子应用的通道调用凭证 [UmsSdkCredential]。 +/// 从通用通道商户主表([ChannelMerchant])读取 product, 从直连配置([UmsDirectKeyConfig])读取 +/// 商户身份(mid/tid) 与签名密钥(appId/appKey/secretKey), 组装为下发给子应用的通道调用凭证 [UmsSdkCredential]。 +/// +/// 沙箱状态不挂在商户配置上, 运行时读取支付产品配置([PayProductConfig] 的 activeEnv)判断。 /// /// 银联商务为聚合支付, 不区分应用能力(capability 参数保留但不使用)。 @Slf4j @@ -21,30 +26,38 @@ import org.springframework.stereotype.Service; public class UmsDirectConfigAssembler { private final UmsDirectKeyConfigService keyConfigService; - private final UmsDirectChannelMerchantManager channelMerchantManager; + private final ChannelMerchantManager channelMerchantManager; + private final PayProductConfigManager payProductConfigManager; /// 组装直连商户的通道调用凭证(下发给子应用) /// - /// @param mchNo 商户号(兜底定位) - /// @param channelMchNo 通道商户号(定位密钥/商户绑定) + /// @param mchNo 商户号(定位通用通道商户主表) + /// @param channelMchNo 通道商户号(定位直连配置) /// @param capability 支付能力编码(UMS 不使用, 保留对齐签名) /// @return 银联商务 SDK 凭证 public UmsSdkCredential buildConfig(String mchNo, String channelMchNo, String capability) { - // 1. 读取通道商户绑定(获取商户号/终端号/沙箱标志) - UmsDirectChannelMerchant merchant = channelMerchantManager.findByChannelMchNo(channelMchNo) - .orElseThrow(() -> new DataNotExistException("error.channel.ums.mchNotFound")); + // 1. 通用通道商户主表(获取 product, 用于查支付产品配置的沙箱环境) + ChannelMerchant channelMerchant = channelMerchantManager.findByMchNoAndChannelMchNo(mchNo, channelMchNo) + // 通道: 通道商户配置不存在 + .orElseThrow(() -> new DataNotExistException("error.payment.channel.channelMerchantNotExist")); - // 2. 读取密钥配置(appId/appKey/secretKey) + // 2. 直连配置(商户身份 mid/tid + 密钥 appId/appKey/secretKey) UmsDirectKeyConfig keyConfig = keyConfigService.findByChannelMchNo(channelMchNo); - // 3. 组装凭证 + // 3. 沙箱状态读取支付产品配置的生效环境(不挂在商户配置上) + boolean sandbox = payProductConfigManager.findByProduct(channelMerchant.getProduct()) + .map(PayProductConfig::getActiveEnv) + .map(PayEnvEnum.SANDBOX.getCode()::equals) + .orElse(false); + + // 4. 组装凭证 var credential = new UmsSdkCredential(); credential.setUmsAppId(keyConfig.getUmsAppId()); credential.setAppKey(keyConfig.getAppKey()); - credential.setMerchantNo(merchant.getMerchantNo()); - credential.setTerminalNo(merchant.getTerminalNo()); + credential.setMerchantNo(keyConfig.getMerchantNo()); + credential.setTerminalNo(keyConfig.getTerminalNo()); credential.setSecretKey(keyConfig.getSecretKey()); - credential.setSandbox(merchant.isSandbox()); + credential.setSandbox(sandbox); return credential; } } diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectKeyConfigService.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectKeyConfigService.java index 0348f7d2b..b339474f8 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectKeyConfigService.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/direct/UmsDirectKeyConfigService.java @@ -1,10 +1,10 @@ package cn.daxpay.open.channel.ums.service.direct; import cn.daxpay.open.channel.ums.convert.direct.UmsDirectKeyConfigConvert; -import cn.daxpay.open.channel.ums.dao.direct.UmsDirectChannelMerchantManager; import cn.daxpay.open.channel.ums.dao.direct.UmsDirectKeyConfigManager; import cn.daxpay.open.channel.ums.entity.direct.UmsDirectKeyConfig; import cn.daxpay.open.channel.ums.param.direct.UmsDirectKeyConfigParam; +import cn.daxpay.open.payment.channel.dao.mch.ChannelMerchantManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -19,7 +19,7 @@ import org.springframework.transaction.annotation.Transactional; public class UmsDirectKeyConfigService { private final UmsDirectKeyConfigManager umsDirectKeyConfigManager; - private final UmsDirectChannelMerchantManager umsDirectChannelMerchantManager; + private final ChannelMerchantManager channelMerchantManager; /// 根据通道商户号查询密钥配置, 不存在则创建默认记录 @Transactional(rollbackFor = Exception.class) @@ -30,18 +30,20 @@ public class UmsDirectKeyConfigService { } var config = new UmsDirectKeyConfig() .setChannelMchNo(channelMchNo); - // 查询通道商户记录填充商户号 - umsDirectChannelMerchantManager.findByChannelMchNo(channelMchNo) + // 查询通用通道商户主表填充商户号 + channelMerchantManager.findByChannelMchNo(channelMchNo) .ifPresent(mch -> config.setMchNo(mch.getMchNo())); umsDirectKeyConfigManager.save(config); return config; } /// 保存密钥配置(更新) + /// + /// 以 channelMchNo 定位记录, 仅更新可编辑字段(terminalNo/umsAppId/appKey/secretKey); + /// mchNo/merchantNo 为不可变身份字段(实体 FieldStrategy.NEVER), 由 findByChannelMchNo 从 DB 加载后保持不变。 @Transactional(rollbackFor = Exception.class) public void save(UmsDirectKeyConfigParam param) { var config = this.findByChannelMchNo(param.getChannelMchNo()); - config.setMchNo(param.getMchNo()); UmsDirectKeyConfigConvert.CONVERT.copy(param, config); umsDirectKeyConfigManager.updateById(config); } diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/refund/UmsRefundService.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/refund/UmsRefundService.java index ac9c3dbd0..af630e986 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/refund/UmsRefundService.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/refund/UmsRefundService.java @@ -6,19 +6,19 @@ import cn.daxpay.open.channel.ums.client.enums.UmsPayMethod; import cn.daxpay.open.channel.ums.client.req.UmsRefundReq; import cn.daxpay.open.channel.ums.client.resp.UmsRefundResp; import cn.daxpay.open.channel.ums.code.UmsCode; +import cn.daxpay.open.channel.ums.util.UmsDateUtil; import cn.daxpay.open.payment.common.enums.RefundOrderStatusEnum; import cn.daxpay.open.payment.common.result.DaxResult; import cn.daxpay.open.payment.core.trade.bo.RefundResultBo; +import cn.daxpay.open.payment.core.trade.dao.PayTradeManager; import cn.daxpay.open.payment.core.trade.entity.PayRefundOrder; +import cn.daxpay.open.payment.core.trade.entity.PayTrade; import cn.daxpay.open.platform.system.service.config.PlatformUrlConfigService; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; - /// # 银联商务退款业务服务 /// /// 通过 [UmsChannelClient] 调用子应用发起银联商务退款。 @@ -30,9 +30,7 @@ public class UmsRefundService { private final UmsChannelClient umsChannelClient; private final PlatformUrlConfigService platformUrlConfigService; - - /// 银联商务退款时间格式 - private static final DateTimeFormatter REFUND_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final PayTradeManager payTradeManager; /// 执行银联商务退款 public RefundResultBo refund(PayRefundOrder refundOrder, UmsSdkCredential credential) { @@ -46,6 +44,13 @@ public class UmsRefundService { req.setMethod(UmsPayMethod.QRCODE); req.setCredential(credential); + // 银联商务扫码退款需要 billDate(原订单创建日, 子应用按通道时区转换) + payTradeManager.findByTradeNo(refundOrder.getOrderNo()) + .ifPresentOrElse( + t -> req.setBillDate(t.getCreateTime()), + () -> log.warn("银联商务退款未查到原交易({}), billDate 未填充, 银商可能拒绝", + refundOrder.getOrderNo())); + DaxResult result = umsChannelClient.refund(req); if (result.getCode() != 0) { log.error("银联商务通道退款失败: refundNo={}, msg={}", refundOrder.getRefundNo(), result.getMsg()); @@ -73,10 +78,8 @@ public class UmsRefundService { private RefundResultBo toRefundResult(UmsRefundResp resp) { RefundResultBo bo = new RefundResultBo(); bo.setOutRefundNo(resp.getOutRefundNo()); - // 退款完成时间(yyyy-MM-dd HH:mm:ss → OffsetDateTime) - if (StrUtil.isNotBlank(resp.getFinishTime())) { - bo.setFinishTime(OffsetDateTime.parse(resp.getFinishTime(), REFUND_TIME_FORMAT)); - } + // 退款完成时间(银联商务返回东八区本地时间, 由 UmsDateUtil 解析为带偏移的 OffsetDateTime) + bo.setFinishTime(UmsDateUtil.parseCst(resp.getFinishTime())); // SUCCESS 退款即时成功; 其他状态需同步查询确认 if (UmsCode.REFUND_STATUS_SUCCESS.equals(resp.getRefundStatus())) { bo.setComplete(true) diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/refund/UmsRefundSyncService.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/refund/UmsRefundSyncService.java index bd9d62a45..5d5415ba8 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/refund/UmsRefundSyncService.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/refund/UmsRefundSyncService.java @@ -6,18 +6,18 @@ import cn.daxpay.open.channel.ums.client.enums.UmsPayMethod; import cn.daxpay.open.channel.ums.client.req.UmsRefundSyncReq; import cn.daxpay.open.channel.ums.client.resp.UmsRefundSyncResp; import cn.daxpay.open.channel.ums.code.UmsCode; +import cn.daxpay.open.channel.ums.util.UmsDateUtil; import cn.daxpay.open.payment.common.enums.RefundOrderStatusEnum; import cn.daxpay.open.payment.common.result.DaxResult; import cn.daxpay.open.payment.core.trade.bo.RefundResultBo; +import cn.daxpay.open.payment.core.trade.dao.PayTradeManager; import cn.daxpay.open.payment.core.trade.entity.PayRefundOrder; +import cn.daxpay.open.payment.core.trade.entity.PayTrade; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; - /// # 银联商务退款同步业务服务 /// /// 通过 [UmsChannelClient] 调用子应用查询银联商务退款状态。 @@ -27,8 +27,7 @@ import java.time.format.DateTimeFormatter; public class UmsRefundSyncService { private final UmsChannelClient umsChannelClient; - - private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private final PayTradeManager payTradeManager; /// 执行银联商务退款同步查询 public RefundResultBo sync(PayRefundOrder refundOrder, UmsSdkCredential credential) { @@ -39,6 +38,13 @@ public class UmsRefundSyncService { req.setMethod(UmsPayMethod.QRCODE); req.setCredential(credential); + // 银联商务扫码退款查询需要 billDate(原订单创建日, 子应用按通道时区转换) + payTradeManager.findByTradeNo(refundOrder.getOrderNo()) + .ifPresentOrElse( + t -> req.setBillDate(t.getCreateTime()), + () -> log.warn("银联商务退款同步未查到原交易({}), billDate 未填充, 银商可能拒绝", + refundOrder.getOrderNo())); + DaxResult result = umsChannelClient.refundSync(req); if (result.getCode() != 0) { log.error("银联商务通道退款同步失败: refundNo={}, msg={}", refundOrder.getRefundNo(), result.getMsg()); @@ -54,10 +60,8 @@ public class UmsRefundSyncService { private RefundResultBo toSyncResult(UmsRefundSyncResp resp) { RefundResultBo bo = new RefundResultBo(); bo.setOutRefundNo(resp.getOutRefundNo()); - // 退款完成时间 - if (StrUtil.isNotBlank(resp.getFinishTime())) { - bo.setFinishTime(OffsetDateTime.parse(resp.getFinishTime(), TIME_FORMAT)); - } + // 退款完成时间(银联商务返回东八区本地时间, 由 UmsDateUtil 解析为带偏移的 OffsetDateTime) + bo.setFinishTime(UmsDateUtil.parseCst(resp.getFinishTime())); // 退款金额 bo.setRefundAmount(resp.getRefundAmount()); diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/sync/UmsSyncService.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/sync/UmsSyncService.java index 8b03ea1f9..708b8086b 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/sync/UmsSyncService.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/service/payment/sync/UmsSyncService.java @@ -6,6 +6,7 @@ import cn.daxpay.open.channel.ums.client.enums.UmsPayMethod; import cn.daxpay.open.channel.ums.client.req.UmsSyncReq; import cn.daxpay.open.channel.ums.client.resp.UmsSyncResp; import cn.daxpay.open.channel.ums.code.UmsCode; +import cn.daxpay.open.channel.ums.util.UmsDateUtil; import cn.daxpay.open.payment.common.enums.PayFundStatusEnum; import cn.daxpay.open.payment.common.result.DaxResult; import cn.daxpay.open.payment.core.trade.bo.PaySyncResultBo; @@ -15,9 +16,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; - /// # 银联商务支付同步业务服务 /// /// 通过 [UmsChannelClient] 调用子应用查询银联商务订单状态, @@ -29,13 +27,12 @@ public class UmsSyncService { private final UmsChannelClient umsChannelClient; - /// 银联商务时间格式 - private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - /// 执行银联商务支付同步 public PaySyncResultBo sync(PayTrade trade, UmsSdkCredential credential) { UmsSyncReq req = new UmsSyncReq(); req.setOutTradeNo(trade.getTradeNo()); + // 原订单创建时间(UTC), 子应用按通道时区转换为银联商务 billDate(yyyy-MM-dd) + req.setBillDate(trade.getCreateTime()); // 首期默认扫码查询 req.setMethod(UmsPayMethod.QRCODE); req.setCredential(credential); @@ -59,10 +56,8 @@ public class UmsSyncService { bo.setRealAmount(resp.getTotalAmount()); // 买家标识 bo.setBuyerId(resp.getBuyerId()); - // 支付成功时间 - if (StrUtil.isNotBlank(resp.getPayTime())) { - bo.setFinishTime(OffsetDateTime.parse(resp.getPayTime(), TIME_FORMAT)); - } + // 支付成功时间(银联商务返回东八区本地时间, 由 UmsDateUtil 解析为带偏移的 OffsetDateTime) + bo.setFinishTime(UmsDateUtil.parseCst(resp.getPayTime())); String tradeStatus = resp.getTradeStatus(); if (StrUtil.isBlank(tradeStatus)) { diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/util/UmsDateUtil.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/util/UmsDateUtil.java new file mode 100644 index 000000000..93cda78e6 --- /dev/null +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/util/UmsDateUtil.java @@ -0,0 +1,42 @@ +package cn.daxpay.open.channel.ums.util; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +/// # 银联商务通道日期工具(主应用侧) +/// +/// 银联商务属国内通道, 接口返回的时间字段(payTime / finishTime 等) +/// 一律为东八区(GMT+8)本地时间字面量(yyyy-MM-dd HH:mm:ss), 无时区后缀。 +/// +/// 历史问题: 早期直接用 `OffsetDateTime.parse(str, ofPattern("yyyy-MM-dd HH:mm:ss"))`, +/// 因 formatter 不含时区字段、解析出的 TemporalAccessor 缺 OFFSET_SECONDS, +/// 触发 `Unable to obtain OffsetDateTime from TemporalAccessor` 异常。 +/// +/// 本工具显式按东八区(+08:00)将本地时间字面量转换为带偏移的绝对时间 [OffsetDateTime], +/// 与子应用(channel-one)侧 [UmsDateUtil] 的时区处理姿势一致, 不再依赖运行环境时区。 +public final class UmsDateUtil { + + /// 银联商务时间格式(yyyy-MM-dd HH:mm:ss) + private static final DateTimeFormatter DATETIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /// 东八区偏移(北京时间) + private static final ZoneOffset CST = ZoneOffset.ofHours(8); + + private UmsDateUtil() { + } + + /// 解析银联商务东八区时间字面量为带偏移的 [OffsetDateTime] + /// + /// 通道返回的 `yyyy-MM-dd HH:mm:ss` 是东八区本地时间, 先按 [LocalDateTime] 解析, + /// 再附加 +08:00 偏移得到绝对时间点, 入库(timestamptz)时区正确。 + /// 传入 null 或空白字符串时返回 null。 + public static OffsetDateTime parseCst(String text) { + if (text == null || text.isBlank()) { + return null; + } + return LocalDateTime.parse(text, DATETIME_FORMATTER).atOffset(CST); + } +}