mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-09 22:45:58 +08:00
fix(ums): 修复时间解析必崩(OffsetDateTime缺时区) + billDate透传 + 移除直连商户实体
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<UmsDirectChannelMerchantResult> 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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<UmsDirectChannelMerchantMapper, UmsDirectChannelMerchant> {
|
||||
|
||||
/// 根据通道商户号查询
|
||||
public Optional<UmsDirectChannelMerchant> findByChannelMchNo(String channelMchNo) {
|
||||
return lambdaQuery()
|
||||
.eq(UmsDirectChannelMerchant::getChannelMchNo, channelMchNo)
|
||||
.oneOpt();
|
||||
}
|
||||
}
|
||||
@@ -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<UmsDirectChannelMerchant> {
|
||||
}
|
||||
@@ -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<UmsDirectChannelMerchantResult> {
|
||||
|
||||
/// 通道商户号(系统生成雪花号)
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<UmsDir
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private String channelMchNo;
|
||||
|
||||
/// 银联商务商户号(mid, 创建时录入不可修改)
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private String merchantNo;
|
||||
|
||||
/// 终端号(tid)
|
||||
private String terminalNo;
|
||||
|
||||
/// 银联商务应用 AppId
|
||||
private String umsAppId;
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ import lombok.experimental.Accessors;
|
||||
|
||||
/// # 银联商务直连通道商户绑定创建参数
|
||||
///
|
||||
/// 银联商务在创建通道商户时需要同时配置密钥(umsAppId/appKey/secretKey),
|
||||
/// 因为银联商务签名无证书, 密钥是支付调用的前置条件。
|
||||
/// 创建时仅录入商户身份(merchantNo mid)与商户名称,
|
||||
/// 应用ID(umsAppId)/终端号(tid)/应用密钥(appKey)/通讯密钥(secretKey)由密钥配置单独维护,
|
||||
/// 沙箱环境由支付产品配置(pay_md_product_config.activeEnv)决定。
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "银联商务直连通道商户绑定创建参数")
|
||||
@@ -22,32 +23,11 @@ public class UmsDirectChannelMerchantCreateParam {
|
||||
@Schema(description = "通道商户名称")
|
||||
private String channelMerchantName;
|
||||
|
||||
@NotBlank(message = "{validation.field.merchantNo.notBlank}")
|
||||
@Schema(description = "银联商务商户号")
|
||||
private String merchantNo;
|
||||
|
||||
@Schema(description = "终端号")
|
||||
private String terminalNo;
|
||||
|
||||
@Schema(description = "订单号前缀")
|
||||
private String orderPrefix;
|
||||
|
||||
@Schema(description = "是否沙箱环境")
|
||||
private boolean sandbox;
|
||||
|
||||
@NotBlank(message = "{validation.field.product.notBlank}")
|
||||
@Schema(description = "所属支付产品")
|
||||
private String product;
|
||||
|
||||
@NotBlank(message = "{validation.field.umsAppId.notBlank}")
|
||||
@Schema(description = "银联商务应用 AppId")
|
||||
private String umsAppId;
|
||||
|
||||
@NotBlank(message = "{validation.field.appKey.notBlank}")
|
||||
@Schema(description = "应用密钥(HmacSHA256 签名密钥)")
|
||||
private String appKey;
|
||||
|
||||
@NotBlank(message = "{validation.field.secretKey.notBlank}")
|
||||
@Schema(description = "通讯密钥(回调验签密钥)")
|
||||
private String secretKey;
|
||||
@NotBlank(message = "{validation.field.merchantNo.notBlank}")
|
||||
@Schema(description = "银联商务商户号(mid)")
|
||||
private String merchantNo;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 银联商务直连密钥配置保存参数
|
||||
///
|
||||
/// 以 channelMchNo(通道商户号) 作为唯一标识定位记录,
|
||||
/// mchNo(平台商户号)/merchantNo(银联 mid) 为不可变身份字段, 创建时写入后永不可改, 不参与保存。
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "银联商务直连密钥配置保存参数")
|
||||
@@ -15,9 +18,8 @@ public class UmsDirectKeyConfigParam {
|
||||
@Schema(description = "通道商户号")
|
||||
private String channelMchNo;
|
||||
|
||||
@NotBlank(message = "{validation.field.mchNo.notBlank}")
|
||||
@Schema(description = "商户号")
|
||||
private String mchNo;
|
||||
@Schema(description = "终端号(tid)")
|
||||
private String terminalNo;
|
||||
|
||||
@Schema(description = "银联商务应用 AppId")
|
||||
private String umsAppId;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package cn.daxpay.open.channel.ums.result.direct;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 银联商务直连通道商户绑定返回结果
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class UmsDirectChannelMerchantResult {
|
||||
|
||||
/// 通道商户号
|
||||
private String channelMchNo;
|
||||
|
||||
/// 所属支付产品
|
||||
private String product;
|
||||
|
||||
/// 银联商务商户号
|
||||
private String merchantNo;
|
||||
|
||||
/// 终端号
|
||||
private String terminalNo;
|
||||
|
||||
/// 订单号前缀
|
||||
private String orderPrefix;
|
||||
|
||||
/// 是否沙箱环境
|
||||
private boolean sandbox;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.daxpay.open.channel.ums.result.direct;
|
||||
|
||||
import cn.daxpay.open.platform.common.json.sensitive.SensitiveInfo;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@@ -11,13 +12,21 @@ public class UmsDirectKeyConfigResult {
|
||||
/// 通道商户号
|
||||
private String channelMchNo;
|
||||
|
||||
/// 银联商务商户号(mid)
|
||||
private String merchantNo;
|
||||
|
||||
/// 终端号(tid)
|
||||
private String terminalNo;
|
||||
|
||||
/// 银联商务应用 AppId
|
||||
private String umsAppId;
|
||||
|
||||
/// 应用密钥(脱敏)
|
||||
/// 应用密钥(脱敏返回, 保留前后各4位)
|
||||
@SensitiveInfo(front = 4, end = 4)
|
||||
private String appKey;
|
||||
|
||||
/// 通讯密钥(脱敏)
|
||||
/// 通讯密钥(脱敏返回, 保留前后各4位)
|
||||
@SensitiveInfo(front = 4, end = 4)
|
||||
private String secretKey;
|
||||
|
||||
/// 应用密钥是否已配置
|
||||
|
||||
@@ -6,6 +6,7 @@ import cn.daxpay.open.channel.ums.client.req.UmsCallbackParseReq;
|
||||
import cn.daxpay.open.channel.ums.client.resp.UmsCallbackParseResp;
|
||||
import cn.daxpay.open.channel.ums.code.UmsCode;
|
||||
import cn.daxpay.open.channel.ums.service.direct.UmsDirectConfigAssembler;
|
||||
import cn.daxpay.open.channel.ums.util.UmsDateUtil;
|
||||
import cn.daxpay.open.payment.common.callback.CallbackData;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.core.trade.service.PayCallbackService;
|
||||
@@ -19,8 +20,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -40,8 +39,6 @@ public class UmsPayCallbackService {
|
||||
private final UmsDirectConfigAssembler configAssembler;
|
||||
private final PayCallbackService payCallbackService;
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/// 支付回调处理
|
||||
public String payHandle(String mchNo, String channelMchNo, HttpServletRequest request) {
|
||||
// 1. 提取回调参数(JSON body → Map<String,String>, 嵌套对象转 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<UmsRefundResp> 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)
|
||||
|
||||
@@ -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<UmsRefundSyncResp> 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());
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user