feat(pay): tenant-scope native pay and add legacy account/transaction import
This commit is contained in:
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -43,13 +44,19 @@ public class ApiAccessLogInterceptor implements HandlerInterceptor {
|
||||
|
||||
// 打印 request 日志
|
||||
if (!SpringUtils.isProd()) {
|
||||
Map<String, String> queryString = ServletUtils.getParamMap(request);
|
||||
String requestBody = ServletUtils.getBody(request);
|
||||
if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
|
||||
log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
|
||||
ApiAccessLog accessLog = handlerMethod != null
|
||||
? handlerMethod.getMethodAnnotation(ApiAccessLog.class) : null;
|
||||
if (accessLog != null && !accessLog.requestEnable()) {
|
||||
log.info("[preHandle][开始请求 URL({}) 参数日志已关闭]", request.getRequestURI());
|
||||
} else {
|
||||
log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(),
|
||||
StrUtil.blankToDefault(requestBody, queryString.toString()));
|
||||
Map<String, String> queryString = ServletUtils.getParamMap(request);
|
||||
String requestBody = ServletUtils.getBody(request);
|
||||
if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
|
||||
log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
|
||||
} else {
|
||||
log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(),
|
||||
StrUtil.blankToDefault(requestBody, queryString.toString()));
|
||||
}
|
||||
}
|
||||
// 计时
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
|
||||
@@ -5,6 +5,8 @@ import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor;
|
||||
import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
|
||||
import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
@@ -34,6 +36,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
@@ -372,9 +375,13 @@ public class GlobalExceptionHandler {
|
||||
errorLog.setTraceId(TracerUtils.getTraceId());
|
||||
errorLog.setApplicationName(applicationName);
|
||||
errorLog.setRequestUrl(request.getRequestURI());
|
||||
HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(
|
||||
ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD);
|
||||
ApiAccessLog accessLog = handlerMethod != null ? handlerMethod.getMethodAnnotation(ApiAccessLog.class) : null;
|
||||
Map<String, Object> requestParams = MapUtil.<String, Object>builder()
|
||||
.put("query", ServletUtils.getParamMap(request))
|
||||
.put("body", ServletUtils.getBody(request)).build();
|
||||
.put("body", accessLog != null && !accessLog.requestEnable() ? null : ServletUtils.getBody(request))
|
||||
.build();
|
||||
errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
|
||||
errorLog.setRequestMethod(request.getMethod());
|
||||
errorLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
|
||||
@@ -2,6 +2,9 @@ package cn.iocoder.yudao.module.pay.api.wallet.dto;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
@@ -18,6 +21,7 @@ public class PayWalletAddBalanceReqDTO {
|
||||
* 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性
|
||||
*/
|
||||
@NotNull(message = "用户编号不能为空")
|
||||
@Positive(message = "用户编号必须大于零")
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
@@ -31,6 +35,7 @@ public class PayWalletAddBalanceReqDTO {
|
||||
* 关联业务分类
|
||||
*/
|
||||
@NotNull(message = "关联业务分类不能为空")
|
||||
@InEnum(PayWalletBizTypeEnum.class)
|
||||
private Integer bizType;
|
||||
/**
|
||||
* 关联业务编号
|
||||
@@ -44,6 +49,7 @@ public class PayWalletAddBalanceReqDTO {
|
||||
* 正值表示余额增加,负值表示余额减少
|
||||
*/
|
||||
@NotNull(message = "交易金额不能为空")
|
||||
@Positive(message = "交易金额必须大于零")
|
||||
private Integer price;
|
||||
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ public class PayAppController {
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获得应用列表")
|
||||
@PreAuthorize("@ss.hasPermission('pay:merchant:query')")
|
||||
@PreAuthorize("@ss.hasPermission('pay:app:query')")
|
||||
public CommonResult<List<PayAppRespVO>> getAppList() {
|
||||
List<PayAppDO> appListDO = appService.getAppList();
|
||||
return success(PayAppConvert.INSTANCE.convertList(appListDO));
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportPageReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportRespVO;
|
||||
import cn.iocoder.yudao.module.pay.service.legacy.PayLegacyAccountImportService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
|
||||
@Tag(name = "管理后台 - 旧支付账号迁移")
|
||||
@RestController
|
||||
@RequestMapping("/pay/legacy-account-import")
|
||||
@Validated
|
||||
public class PayLegacyAccountImportController {
|
||||
|
||||
@Resource
|
||||
private PayLegacyAccountImportService importService;
|
||||
|
||||
@PostMapping("/import")
|
||||
@Operation(summary = "显式、幂等地导入一个旧租户支付账号")
|
||||
@ApiAccessLog(requestEnable = false)
|
||||
@PreAuthorize("@ss.hasPermission('pay:app:create') and @ss.hasPermission('pay:channel:create')")
|
||||
public CommonResult<PayLegacyAccountImportRespVO> importAccount(
|
||||
@Valid @RequestBody PayLegacyAccountImportReqVO reqVO) {
|
||||
return success(importService.importAccount(reqVO, getLoginUserId()));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得旧支付账号导入审计分页")
|
||||
@PreAuthorize("@ss.hasPermission('pay:app:query') and @ss.hasPermission('pay:channel:query')")
|
||||
public CommonResult<PageResult<PayLegacyAccountImportRespVO>> getImportPage(
|
||||
@Valid PayLegacyAccountImportPageReqVO reqVO) {
|
||||
return success(importService.getImportPage(reqVO));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.*;
|
||||
import cn.iocoder.yudao.module.pay.service.legacy.PayLegacyTransactionImportService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
|
||||
@Tag(name = "管理后台 - 旧支付交易迁移")
|
||||
@RestController
|
||||
@RequestMapping("/pay/legacy-transaction-import")
|
||||
@Validated
|
||||
public class PayLegacyTransactionImportController {
|
||||
@Resource private PayLegacyTransactionImportService importService;
|
||||
|
||||
@PostMapping("/import")
|
||||
@Operation(summary = "显式、幂等地导入一个已终结并对账的旧支付交易聚合")
|
||||
@ApiAccessLog(requestEnable = false)
|
||||
@PreAuthorize("@ss.hasPermission('pay:legacy-transaction:import')")
|
||||
public CommonResult<PayLegacyTransactionImportRespVO> importTransaction(
|
||||
@Valid @RequestBody PayLegacyTransactionImportReqVO reqVO) {
|
||||
return success(importService.importTransaction(reqVO, getLoginUserId()));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得旧支付交易导入审计分页")
|
||||
@PreAuthorize("@ss.hasPermission('pay:legacy-transaction:query')")
|
||||
public CommonResult<PageResult<PayLegacyTransactionImportRespVO>> getImportPage(
|
||||
@Valid PayLegacyTransactionImportPageReqVO reqVO) {
|
||||
return success(importService.getImportPage(reqVO));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.legacy.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Schema(description = "管理后台 - 旧支付账号导入记录分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PayLegacyAccountImportPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "旧系统租户 UUID")
|
||||
private String sourceTenantId;
|
||||
|
||||
@Schema(description = "规范化 Provider", example = "wechat_pay")
|
||||
private String normalizedProvider;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.legacy.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "管理后台 - 旧支付账号导入 Request VO")
|
||||
@Data
|
||||
public class PayLegacyAccountImportReqVO {
|
||||
|
||||
private static final String UUID_PATTERN =
|
||||
"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$";
|
||||
|
||||
@Schema(description = "旧系统租户 UUID,仅用于审计", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "旧系统租户 ID 不能为空")
|
||||
@Pattern(regexp = UUID_PATTERN, message = "旧系统租户 ID 必须为 UUID")
|
||||
private String sourceTenantId;
|
||||
|
||||
@Schema(description = "旧支付账号 UUID,作为幂等业务键", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "旧支付账号 ID 不能为空")
|
||||
@Pattern(regexp = UUID_PATTERN, message = "旧支付账号 ID 必须为 UUID")
|
||||
private String sourceAccountId;
|
||||
|
||||
@Schema(description = "旧账号及其密钥导出内容的 SHA-256", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "来源校验和不能为空")
|
||||
@Pattern(regexp = "^[0-9a-fA-F]{64}$", message = "来源校验和必须为 64 位 SHA-256 十六进制")
|
||||
private String sourceChecksumSha256;
|
||||
|
||||
@Schema(description = "旧 Provider,可使用受支持的历史别名", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "wechat_pay")
|
||||
@NotBlank(message = "旧 Provider 不能为空")
|
||||
@Size(max = 32, message = "旧 Provider 最长 32 个字符")
|
||||
private String sourceProvider;
|
||||
|
||||
@Schema(description = "旧收款模式;仅 tenant_collect 可安全自动迁移", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
allowableValues = {"platform_collect", "tenant_collect", "service_provider"})
|
||||
@NotBlank(message = "旧收款模式不能为空")
|
||||
@Pattern(regexp = "^(platform_collect|tenant_collect|service_provider)$", message = "旧收款模式不合法")
|
||||
private String sourceMode;
|
||||
|
||||
@Schema(description = "旧账号状态", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
allowableValues = {"active", "disabled", "pending"})
|
||||
@NotBlank(message = "旧账号状态不能为空")
|
||||
@Pattern(regexp = "^(active|disabled|pending)$", message = "旧账号状态不合法")
|
||||
private String sourceStatus;
|
||||
|
||||
@Schema(description = "显式选择的原生 Pay 渠道;不会根据 Provider 猜测", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "wx_lite")
|
||||
@NotBlank(message = "目标渠道不能为空")
|
||||
@InEnum(value = PayChannelEnum.class, message = "目标渠道不合法")
|
||||
private String targetChannelCode;
|
||||
|
||||
@Schema(description = "原生 Pay 应用标识", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "应用标识不能为空")
|
||||
@Size(max = 64, message = "应用标识最长 64 个字符")
|
||||
private String appKey;
|
||||
|
||||
@Schema(description = "原生 Pay 应用名称", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "应用名称不能为空")
|
||||
@Size(max = 64, message = "应用名称最长 64 个字符")
|
||||
private String appName;
|
||||
|
||||
@Schema(description = "新系统业务支付结果回调,不复用旧 Provider 回调", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "支付结果回调地址不能为空")
|
||||
@URL(message = "支付结果回调地址必须为 URL")
|
||||
private String orderNotifyUrl;
|
||||
|
||||
@Schema(description = "新系统业务退款结果回调,不复用旧 Provider 回调", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "退款结果回调地址不能为空")
|
||||
@URL(message = "退款结果回调地址必须为 URL")
|
||||
private String refundNotifyUrl;
|
||||
|
||||
@Schema(description = "新系统业务转账结果回调")
|
||||
@URL(message = "转账结果回调地址必须为 URL")
|
||||
private String transferNotifyUrl;
|
||||
|
||||
@Schema(description = "旧 tenant_payment_accounts.config_public", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "旧公开配置不能为空")
|
||||
private Map<String, Object> configPublic;
|
||||
|
||||
@Schema(description = "旧 app_private.tenant_secrets.secret_json;只写入原生 Pay 渠道配置,不写入导入审计",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, writeOnly = true)
|
||||
@NotNull(message = "旧私密配置不能为空")
|
||||
private Map<String, Object> secretJson;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.legacy.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 旧支付账号导入 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class PayLegacyAccountImportRespVO {
|
||||
|
||||
private Long id;
|
||||
private String sourceTenantId;
|
||||
private String sourceAccountId;
|
||||
private String normalizedProvider;
|
||||
private String sourceMode;
|
||||
private String sourceStatus;
|
||||
private Long targetAppId;
|
||||
private Long targetChannelId;
|
||||
private String targetChannelCode;
|
||||
private Integer targetStatus;
|
||||
private String normalizedConfigDigest;
|
||||
private List<String> mappingNotes;
|
||||
private Long importedBy;
|
||||
private LocalDateTime importedAt;
|
||||
private Boolean replayed;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.legacy.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 旧支付交易导入审计分页 Request VO")
|
||||
@Data
|
||||
public class PayLegacyTransactionImportPageReqVO extends PageParam {
|
||||
private String sourceTenantId;
|
||||
private String sourceOrderNo;
|
||||
private String sourceOrderStatus;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.legacy.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 旧支付交易聚合导入 Request VO")
|
||||
@Data
|
||||
public class PayLegacyTransactionImportReqVO {
|
||||
private static final String UUID =
|
||||
"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$";
|
||||
private static final String SHA256 = "^[0-9a-fA-F]{64}$";
|
||||
|
||||
@NotBlank @Pattern(regexp = UUID)
|
||||
private String sourceTenantId;
|
||||
@NotNull @Positive
|
||||
private Long sourceAccountImportId;
|
||||
@NotBlank @Pattern(regexp = UUID)
|
||||
private String sourceOrderId;
|
||||
@NotBlank @Pattern(regexp = SHA256)
|
||||
private String sourceChecksumSha256;
|
||||
@NotBlank @Size(max = 64)
|
||||
private String sourceOrderNo;
|
||||
@NotBlank @Pattern(regexp = "^(paid|failed|closed|partially_refunded|refunded)$")
|
||||
private String sourceOrderStatus;
|
||||
@Positive
|
||||
private Long targetUserId;
|
||||
@NotBlank @Size(max = 32)
|
||||
private String subject;
|
||||
@Size(max = 128)
|
||||
private String body;
|
||||
@NotNull @Positive
|
||||
private Integer price;
|
||||
@NotNull @PositiveOrZero
|
||||
private Integer refundedPrice;
|
||||
@NotNull
|
||||
private LocalDateTime sourceCreatedAt;
|
||||
private LocalDateTime sourcePaidAt;
|
||||
|
||||
@NotEmpty @Size(max = 20) @Valid
|
||||
private List<Payment> payments;
|
||||
@NotNull @Size(max = 50) @Valid
|
||||
private List<Refund> refunds;
|
||||
|
||||
@Data
|
||||
public static class Payment {
|
||||
@NotBlank @Pattern(regexp = UUID)
|
||||
private String sourcePaymentId;
|
||||
@NotBlank @Size(max = 32)
|
||||
private String sourceProvider;
|
||||
@Size(max = 40)
|
||||
private String sourceMethod;
|
||||
@NotBlank @Pattern(regexp = "^(paid|failed|cancelled|partially_refunded|refunded)$")
|
||||
private String sourceStatus;
|
||||
@NotNull @Positive
|
||||
private Integer amount;
|
||||
@NotBlank @Size(max = 64)
|
||||
private String providerOutTradeNo;
|
||||
@Size(max = 64)
|
||||
private String providerTradeNo;
|
||||
private LocalDateTime sourcePaidAt;
|
||||
@NotNull
|
||||
private LocalDateTime sourceCreatedAt;
|
||||
@NotNull @PositiveOrZero
|
||||
private Integer sourceEventCount;
|
||||
@Pattern(regexp = SHA256)
|
||||
private String sourceEventDigest;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Refund {
|
||||
@NotBlank @Pattern(regexp = UUID)
|
||||
private String sourceRefundId;
|
||||
@NotBlank @Size(max = 32)
|
||||
private String sourceProvider;
|
||||
@NotBlank @Pattern(regexp = "^(succeeded|failed|rejected|cancelled)$")
|
||||
private String sourceStatus;
|
||||
@NotBlank @Size(max = 64)
|
||||
private String refundNo;
|
||||
@Size(max = 64)
|
||||
private String providerRefundNo;
|
||||
@NotNull @Positive
|
||||
private Integer amount;
|
||||
@NotBlank @Size(max = 128)
|
||||
private String reason;
|
||||
private LocalDateTime sourceSucceededAt;
|
||||
@NotNull
|
||||
private LocalDateTime sourceCreatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.legacy.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 旧支付交易导入 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class PayLegacyTransactionImportRespVO {
|
||||
private Long id;
|
||||
private String sourceTenantId;
|
||||
private String sourceOrderId;
|
||||
private String sourceOrderNo;
|
||||
private String sourceOrderStatus;
|
||||
private Integer sourcePaymentCount;
|
||||
private Integer sourcePaymentEventCount;
|
||||
private Integer sourceRefundCount;
|
||||
private Integer sourceRefundedPrice;
|
||||
private Long sourceAccountImportId;
|
||||
private Long targetAppId;
|
||||
private Long targetChannelId;
|
||||
private Long targetOrderId;
|
||||
private String normalizedPayloadDigest;
|
||||
private List<String> mappingNotes;
|
||||
private Long importedBy;
|
||||
private LocalDateTime importedAt;
|
||||
private Boolean replayed;
|
||||
}
|
||||
@@ -62,8 +62,13 @@ public class PayWalletController {
|
||||
}
|
||||
|
||||
// 更新钱包余额
|
||||
payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()),
|
||||
PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance());
|
||||
if (updateReqVO.getBalance() > 0) {
|
||||
payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()),
|
||||
PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance());
|
||||
} else {
|
||||
payWalletService.reduceWalletBalance(wallet.getId(), updateReqVO.getUserId(),
|
||||
PayWalletBizTypeEnum.UPDATE_BALANCE, -updateReqVO.getBalance());
|
||||
}
|
||||
return success(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -40,6 +41,7 @@ public class PayWalletRechargeController {
|
||||
@PostMapping("/refund")
|
||||
@Operation(summary = "发起钱包充值退款")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('pay:wallet-recharge:refund')")
|
||||
public CommonResult<Boolean> refundWalletRecharge(@RequestParam("id") Long id) {
|
||||
walletRechargeService.refundWalletRecharge(id, getClientIP());
|
||||
return success(true);
|
||||
|
||||
@@ -3,7 +3,13 @@ package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.rechargepackage;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 充值套餐 Base VO,提供给添加、修改、详细的子 VO 使用
|
||||
@@ -13,19 +19,24 @@ import jakarta.validation.constraints.NotNull;
|
||||
public class WalletRechargePackageBaseVO {
|
||||
|
||||
@Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
|
||||
@NotNull(message = "套餐名不能为空")
|
||||
@NotBlank(message = "套餐名不能为空")
|
||||
@Size(max = 64, message = "套餐名不能超过 64 个字符")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "支付金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "16454")
|
||||
@NotNull(message = "支付金额不能为空")
|
||||
@Positive(message = "支付金额必须大于零")
|
||||
private Integer payPrice;
|
||||
|
||||
@Schema(description = "赠送金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20887")
|
||||
@NotNull(message = "赠送金额不能为空")
|
||||
@PositiveOrZero(message = "赠送金额不能小于零")
|
||||
private Integer bonusPrice;
|
||||
|
||||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||
@NotNull(message = "状态不能为空")
|
||||
@Min(value = 0, message = "状态必须为启用或停用")
|
||||
@Max(value = 1, message = "状态必须为启用或停用")
|
||||
private Byte status;
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 修改钱包余额 Request VO")
|
||||
@@ -10,10 +13,17 @@ public class PayWalletUpdateBalanceReqVO {
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788")
|
||||
@NotNull(message = "用户编号不能为空")
|
||||
@Positive(message = "用户编号必须大于零")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||
@NotNull(message = "变动余额不能为空")
|
||||
@Min(value = -2147483647, message = "变动余额超出支持范围")
|
||||
private Integer balance;
|
||||
|
||||
@AssertTrue(message = "变动余额不能为零")
|
||||
public boolean isBalanceChanged() {
|
||||
return balance == null || balance != 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import java.util.Objects;
|
||||
|
||||
@Schema(description = "用户 APP - 创建钱包充值 Request VO")
|
||||
@@ -16,6 +17,7 @@ public class AppPayWalletRechargeCreateReqVO {
|
||||
private Integer payPrice;
|
||||
|
||||
@Schema(description = "充值套餐编号", example = "1024")
|
||||
@Positive(message = "充值套餐编号必须大于零")
|
||||
private Long packageId;
|
||||
|
||||
@AssertTrue(message = "充值金额和充钱套餐不能同时为空")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.app;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@@ -24,7 +24,7 @@ import lombok.*;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayAppDO extends BaseDO {
|
||||
public class PayAppDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 应用编号,数据库自增
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@TableName(value = "pay_legacy_account_import", autoResultMap = true)
|
||||
@KeySequence("pay_legacy_account_import_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayLegacyAccountImportDO extends TenantBaseDO {
|
||||
|
||||
private Long id;
|
||||
private String sourceTenantId;
|
||||
private String sourceAccountId;
|
||||
private String sourceChecksumSha256;
|
||||
private String sourceProvider;
|
||||
private String normalizedProvider;
|
||||
private String sourceMode;
|
||||
private String sourceStatus;
|
||||
private Long targetAppId;
|
||||
private Long targetChannelId;
|
||||
private String targetChannelCode;
|
||||
private Integer targetStatus;
|
||||
private String normalizedConfigDigest;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> mappingNotes;
|
||||
|
||||
private Long importedBy;
|
||||
private LocalDateTime importedAt;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@TableName(value = "pay_legacy_transaction_import", autoResultMap = true)
|
||||
@KeySequence("pay_legacy_transaction_import_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayLegacyTransactionImportDO extends TenantBaseDO {
|
||||
private Long id;
|
||||
private String sourceTenantId;
|
||||
private String sourceOrderId;
|
||||
private String sourceChecksumSha256;
|
||||
private String sourceOrderNo;
|
||||
private String sourceOrderStatus;
|
||||
private Integer sourcePaymentCount;
|
||||
private Integer sourcePaymentEventCount;
|
||||
private Integer sourceRefundCount;
|
||||
private Integer sourceRefundedPrice;
|
||||
private Long sourceAccountImportId;
|
||||
private Long targetAppId;
|
||||
private Long targetChannelId;
|
||||
private Long targetOrderId;
|
||||
private String normalizedPayloadDigest;
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> mappingNotes;
|
||||
private Long importedBy;
|
||||
private LocalDateTime importedAt;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
|
||||
@TableName("pay_legacy_transaction_payment_import")
|
||||
@KeySequence("pay_legacy_transaction_payment_import_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayLegacyTransactionPaymentImportDO extends TenantBaseDO {
|
||||
private Long id;
|
||||
private Long transactionImportId;
|
||||
private String sourcePaymentId;
|
||||
private String sourceStatus;
|
||||
private String sourceProvider;
|
||||
private Integer sourceEventCount;
|
||||
private String sourceEventDigest;
|
||||
private Long targetExtensionId;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
|
||||
@TableName("pay_legacy_transaction_refund_import")
|
||||
@KeySequence("pay_legacy_transaction_refund_import_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayLegacyTransactionRefundImportDO extends TenantBaseDO {
|
||||
private Long id;
|
||||
private Long transactionImportId;
|
||||
private String sourceRefundId;
|
||||
private String sourceStatus;
|
||||
private Long targetRefundId;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.notify;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
@@ -19,7 +19,7 @@ import lombok.*;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayNotifyLogDO extends BaseDO {
|
||||
public class PayNotifyLogDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 日志编号,自增
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.order;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
|
||||
@@ -24,7 +24,7 @@ import java.time.LocalDateTime;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayOrderDO extends BaseDO {
|
||||
public class PayOrderDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 订单编号,数据库自增
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.order;
|
||||
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@@ -27,7 +27,7 @@ import java.util.Map;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayOrderExtensionDO extends BaseDO {
|
||||
public class PayOrderExtensionDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 订单拓展编号,数据库自增
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.refund;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@@ -30,7 +30,7 @@ import java.time.LocalDateTime;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayRefundDO extends BaseDO {
|
||||
public class PayRefundDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 退款单编号,数据库自增
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.transfer;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
|
||||
@@ -23,7 +23,7 @@ import java.util.Map;
|
||||
@TableName(value ="pay_transfer", autoResultMap = true)
|
||||
@KeySequence("pay_transfer_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
public class PayTransferDO extends BaseDO {
|
||||
public class PayTransferDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
@@ -149,4 +149,4 @@ public class PayTransferDO extends BaseDO {
|
||||
*/
|
||||
private String channelPackageInfo;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.wallet;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@@ -15,7 +15,7 @@ import lombok.Data;
|
||||
@TableName(value ="pay_wallet")
|
||||
@KeySequence("pay_wallet_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
public class PayWalletDO extends BaseDO {
|
||||
public class PayWalletDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.wallet;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
||||
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName(value ="pay_wallet_recharge")
|
||||
@KeySequence("pay_wallet_recharge_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
public class PayWalletRechargeDO extends BaseDO {
|
||||
public class PayWalletRechargeDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.wallet;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@@ -16,7 +16,7 @@ import lombok.Data;
|
||||
@TableName(value ="pay_wallet_recharge_package")
|
||||
@KeySequence("pay_wallet_recharge_package_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
public class PayWalletRechargePackageDO extends BaseDO {
|
||||
public class PayWalletRechargePackageDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.wallet;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
@@ -15,7 +15,7 @@ import lombok.Data;
|
||||
@TableName(value ="pay_wallet_transaction")
|
||||
@KeySequence("pay_wallet_transaction_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
public class PayWalletTransactionDO extends BaseDO {
|
||||
public class PayWalletTransactionDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.mysql.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportPageReqVO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.legacy.PayLegacyAccountImportDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PayLegacyAccountImportMapper extends BaseMapperX<PayLegacyAccountImportDO> {
|
||||
|
||||
default PayLegacyAccountImportDO selectBySourceAccountId(String sourceAccountId) {
|
||||
return selectOne(PayLegacyAccountImportDO::getSourceAccountId, sourceAccountId);
|
||||
}
|
||||
|
||||
default PageResult<PayLegacyAccountImportDO> selectPage(PayLegacyAccountImportPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<PayLegacyAccountImportDO>()
|
||||
.eqIfPresent(PayLegacyAccountImportDO::getSourceTenantId, reqVO.getSourceTenantId())
|
||||
.eqIfPresent(PayLegacyAccountImportDO::getNormalizedProvider, reqVO.getNormalizedProvider())
|
||||
.orderByDesc(PayLegacyAccountImportDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.mysql.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyTransactionImportPageReqVO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.legacy.PayLegacyTransactionImportDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PayLegacyTransactionImportMapper extends BaseMapperX<PayLegacyTransactionImportDO> {
|
||||
default PayLegacyTransactionImportDO selectBySourceOrderId(String sourceOrderId) {
|
||||
return selectOne(PayLegacyTransactionImportDO::getSourceOrderId, sourceOrderId);
|
||||
}
|
||||
|
||||
default PageResult<PayLegacyTransactionImportDO> selectPage(PayLegacyTransactionImportPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<PayLegacyTransactionImportDO>()
|
||||
.eqIfPresent(PayLegacyTransactionImportDO::getSourceTenantId, reqVO.getSourceTenantId())
|
||||
.likeIfPresent(PayLegacyTransactionImportDO::getSourceOrderNo, reqVO.getSourceOrderNo())
|
||||
.eqIfPresent(PayLegacyTransactionImportDO::getSourceOrderStatus, reqVO.getSourceOrderStatus())
|
||||
.orderByDesc(PayLegacyTransactionImportDO::getId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.mysql.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.legacy.PayLegacyTransactionPaymentImportDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PayLegacyTransactionPaymentImportMapper extends BaseMapperX<PayLegacyTransactionPaymentImportDO> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.mysql.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.legacy.PayLegacyTransactionRefundImportDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PayLegacyTransactionRefundImportMapper extends BaseMapperX<PayLegacyTransactionRefundImportDO> {
|
||||
}
|
||||
@@ -81,6 +81,16 @@ public interface PayWalletMapper extends BaseMapperX<PayWalletDO> {
|
||||
update(null, lambdaUpdateWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员调减余额,不计入会员消费累计。
|
||||
*/
|
||||
default int updateWhenSubtract(Long id, Integer price) {
|
||||
return update(null, new LambdaUpdateWrapper<PayWalletDO>()
|
||||
.setSql(" balance = balance - " + price)
|
||||
.eq(PayWalletDO::getId, id)
|
||||
.ge(PayWalletDO::getBalance, price));
|
||||
}
|
||||
|
||||
/**
|
||||
* 冻结钱包部分余额
|
||||
*
|
||||
@@ -131,4 +141,3 @@ public interface PayWalletMapper extends BaseMapperX<PayWalletDO> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ public interface RedisKeyConstants {
|
||||
/**
|
||||
* 支付钱包的分布式锁
|
||||
*
|
||||
* KEY 格式:pay_wallet:lock:%d
|
||||
* KEY 格式:pay_wallet:lock:{tenantId}:{walletOrUserId}
|
||||
* VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构
|
||||
* 过期时间:不固定
|
||||
*/
|
||||
String PAY_WALLET_LOCK = "pay_wallet:lock:%d";
|
||||
String PAY_WALLET_LOCK = "pay_wallet:lock:%d:%d";
|
||||
|
||||
/**
|
||||
* 支付序号的缓存
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.springframework.stereotype.Repository;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder.getRequiredTenantId;
|
||||
import static cn.iocoder.yudao.module.pay.dal.redis.RedisKeyConstants.PAY_WALLET_LOCK;
|
||||
|
||||
/**
|
||||
@@ -36,7 +37,7 @@ public class PayWalletLockRedisDAO {
|
||||
}
|
||||
|
||||
private static String formatKey(Long id) {
|
||||
return String.format(PAY_WALLET_LOCK, id);
|
||||
return String.format(PAY_WALLET_LOCK, getRequiredTenantId(), id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,6 +21,30 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode CHANNEL_IS_DISABLE = new ErrorCode(1_007_001_001, "支付渠道已经禁用");
|
||||
ErrorCode CHANNEL_EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1_007_001_004, "已存在相同的渠道");
|
||||
|
||||
// ========== LEGACY ACCOUNT IMPORT 模块 1-007-001-100 ==========
|
||||
ErrorCode LEGACY_ACCOUNT_IMPORT_UNSUPPORTED_PROVIDER = new ErrorCode(1_007_001_100,
|
||||
"旧支付 Provider 不支持自动迁移:{}");
|
||||
ErrorCode LEGACY_ACCOUNT_IMPORT_UNSUPPORTED_MODE = new ErrorCode(1_007_001_101,
|
||||
"旧收款模式不支持自动迁移:{}");
|
||||
ErrorCode LEGACY_ACCOUNT_IMPORT_CHANNEL_MISMATCH = new ErrorCode(1_007_001_102,
|
||||
"目标渠道 {} 与旧支付 Provider {} 不匹配");
|
||||
ErrorCode LEGACY_ACCOUNT_IMPORT_CONFIG_MISSING = new ErrorCode(1_007_001_103,
|
||||
"旧支付配置缺少迁移必需字段:{}");
|
||||
ErrorCode LEGACY_ACCOUNT_IMPORT_CONFIG_UNSAFE = new ErrorCode(1_007_001_104,
|
||||
"旧支付配置需要人工确认:{}");
|
||||
ErrorCode LEGACY_ACCOUNT_IMPORT_CONFLICT = new ErrorCode(1_007_001_105,
|
||||
"旧支付账号已按不同校验和迁移,禁止覆盖:{}");
|
||||
ErrorCode LEGACY_TRANSACTION_IMPORT_ACCOUNT_INVALID = new ErrorCode(1_007_001_106,
|
||||
"旧交易关联的支付账号导入记录无效:{}");
|
||||
ErrorCode LEGACY_TRANSACTION_IMPORT_CONFLICT = new ErrorCode(1_007_001_107,
|
||||
"旧支付订单已按不同校验和迁移,禁止覆盖:{}");
|
||||
ErrorCode LEGACY_TRANSACTION_IMPORT_UNSAFE_STATE = new ErrorCode(1_007_001_108,
|
||||
"旧支付交易包含不可安全接管的非终态:{}");
|
||||
ErrorCode LEGACY_TRANSACTION_IMPORT_RECONCILIATION = new ErrorCode(1_007_001_109,
|
||||
"旧支付交易金额或状态对账失败:{}");
|
||||
ErrorCode LEGACY_TRANSACTION_IMPORT_PROVIDER_MISMATCH = new ErrorCode(1_007_001_110,
|
||||
"旧支付交易 Provider 与已迁移账号不一致:{}");
|
||||
|
||||
// ========== ORDER 模块 1-007-002-000 ==========
|
||||
ErrorCode PAY_ORDER_NOT_FOUND = new ErrorCode(1_007_002_000, "支付订单不存在");
|
||||
ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_002_001, "支付订单不处于待支付");
|
||||
|
||||
@@ -228,7 +228,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
|
||||
// 成功状态
|
||||
if (PayTransferStatusEnum.isSuccess(transfer.getStatus())) {
|
||||
PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction(
|
||||
String.valueOf(transfer.getId()), PayWalletBizTypeEnum.TRANSFER);
|
||||
transfer.getNo(), PayWalletBizTypeEnum.TRANSFER);
|
||||
Assert.notNull(walletTransaction, "转账单 {} 钱包流水不能为空", outTradeNo);
|
||||
return PayTransferRespDTO.successOf(walletTransaction.getNo(), walletTransaction.getCreateTime(),
|
||||
outTradeNo, walletTransaction);
|
||||
|
||||
@@ -16,6 +16,7 @@ import cn.iocoder.yudao.module.pay.dal.mysql.channel.PayChannelMapper;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.NonePayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Validator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -44,11 +45,15 @@ public class PayChannelServiceImpl implements PayChannelService {
|
||||
@Resource
|
||||
private PayChannelMapper payChannelMapper;
|
||||
|
||||
@Resource
|
||||
private PayAppService payAppService;
|
||||
|
||||
@Resource
|
||||
private Validator validator;
|
||||
|
||||
@Override
|
||||
public Long createChannel(PayChannelCreateReqVO reqVO) {
|
||||
validateAppExists(reqVO.getAppId());
|
||||
// 断言是否有重复的
|
||||
PayChannelDO dbChannel = getChannelByAppIdAndCode(reqVO.getAppId(), reqVO.getCode());
|
||||
if (dbChannel != null) {
|
||||
@@ -66,6 +71,7 @@ public class PayChannelServiceImpl implements PayChannelService {
|
||||
public void updateChannel(PayChannelUpdateReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
PayChannelDO dbChannel = validateChannelExists(updateReqVO.getId());
|
||||
validateAppExists(updateReqVO.getAppId());
|
||||
|
||||
// 更新
|
||||
PayChannelDO channel = PayChannelConvert.INSTANCE.convert(updateReqVO)
|
||||
@@ -113,6 +119,12 @@ public class PayChannelServiceImpl implements PayChannelService {
|
||||
return channel;
|
||||
}
|
||||
|
||||
private void validateAppExists(Long appId) {
|
||||
if (payAppService.getApp(appId) == null) {
|
||||
throw exception(APP_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayChannelDO getChannel(Long id) {
|
||||
return payChannelMapper.selectById(id);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.iocoder.yudao.module.pay.service.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportPageReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportRespVO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
public interface PayLegacyAccountImportService {
|
||||
|
||||
PayLegacyAccountImportRespVO importAccount(@Valid PayLegacyAccountImportReqVO reqVO, Long importedBy);
|
||||
|
||||
PageResult<PayLegacyAccountImportRespVO> getImportPage(PayLegacyAccountImportPageReqVO reqVO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package cn.iocoder.yudao.module.pay.service.legacy;
|
||||
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportPageReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportRespVO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.legacy.PayLegacyAccountImportDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.legacy.PayLegacyAccountImportMapper;
|
||||
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
|
||||
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_CHANNEL_MISMATCH;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_CONFIG_MISSING;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_CONFIG_UNSAFE;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_CONFLICT;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_UNSUPPORTED_MODE;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_UNSUPPORTED_PROVIDER;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
public class PayLegacyAccountImportServiceImpl implements PayLegacyAccountImportService {
|
||||
|
||||
private static final Set<String> ALIPAY_SERVER_URLS = Set.of(
|
||||
"https://openapi.alipay.com/gateway.do",
|
||||
"https://openapi-sandbox.dl.alipaydev.com/gateway.do");
|
||||
|
||||
@Resource
|
||||
private PayLegacyAccountImportMapper importMapper;
|
||||
@Resource
|
||||
private PayAppService appService;
|
||||
@Resource
|
||||
private PayChannelService channelService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PayLegacyAccountImportRespVO importAccount(PayLegacyAccountImportReqVO reqVO, Long importedBy) {
|
||||
PayLegacyAccountImportDO existing = importMapper.selectBySourceAccountId(reqVO.getSourceAccountId());
|
||||
if (existing != null) {
|
||||
if (!existing.getSourceChecksumSha256().equalsIgnoreCase(reqVO.getSourceChecksumSha256())) {
|
||||
throw exception(LEGACY_ACCOUNT_IMPORT_CONFLICT, reqVO.getSourceAccountId());
|
||||
}
|
||||
return toResp(existing, true);
|
||||
}
|
||||
|
||||
if (!"tenant_collect".equals(reqVO.getSourceMode())) {
|
||||
throw exception(LEGACY_ACCOUNT_IMPORT_UNSUPPORTED_MODE, reqVO.getSourceMode());
|
||||
}
|
||||
String provider = normalizeProvider(reqVO.getSourceProvider());
|
||||
validateChannelFamily(provider, reqVO.getTargetChannelCode());
|
||||
|
||||
List<String> notes = new ArrayList<>();
|
||||
if (!provider.equals(reqVO.getSourceProvider().trim().toLowerCase(Locale.ROOT))) {
|
||||
notes.add("Provider 别名已规范化为 " + provider);
|
||||
}
|
||||
notes.add("旧 Provider 回调地址不迁移;Pay App 使用本次显式提供的新业务回调");
|
||||
if (!"active".equals(reqVO.getSourceStatus())) {
|
||||
notes.add("旧状态 " + reqVO.getSourceStatus() + " 已安全映射为禁用");
|
||||
}
|
||||
|
||||
PayClientConfig config = "wechat_pay".equals(provider)
|
||||
? mapWechatConfig(reqVO, notes) : mapAlipayConfig(reqVO, notes);
|
||||
String configJson = JsonUtils.toJsonString(config);
|
||||
String configDigest = DigestUtil.sha256Hex(configJson);
|
||||
Integer targetStatus = "active".equals(reqVO.getSourceStatus())
|
||||
? CommonStatusEnum.ENABLE.getStatus() : CommonStatusEnum.DISABLE.getStatus();
|
||||
|
||||
PayAppCreateReqVO appReqVO = new PayAppCreateReqVO();
|
||||
appReqVO.setAppKey(reqVO.getAppKey());
|
||||
appReqVO.setName(reqVO.getAppName());
|
||||
appReqVO.setStatus(targetStatus);
|
||||
appReqVO.setRemark("由旧支付账号 " + reqVO.getSourceAccountId() + " 审计导入");
|
||||
appReqVO.setOrderNotifyUrl(reqVO.getOrderNotifyUrl());
|
||||
appReqVO.setRefundNotifyUrl(reqVO.getRefundNotifyUrl());
|
||||
appReqVO.setTransferNotifyUrl(reqVO.getTransferNotifyUrl());
|
||||
Long appId = appService.createApp(appReqVO);
|
||||
|
||||
PayChannelCreateReqVO channelReqVO = new PayChannelCreateReqVO();
|
||||
channelReqVO.setCode(reqVO.getTargetChannelCode());
|
||||
channelReqVO.setAppId(appId);
|
||||
channelReqVO.setStatus(targetStatus);
|
||||
channelReqVO.setFeeRate(0D);
|
||||
channelReqVO.setRemark("由旧 Provider " + reqVO.getSourceProvider() + " 审计导入");
|
||||
channelReqVO.setConfig(configJson);
|
||||
Long channelId = channelService.createChannel(channelReqVO);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
PayLegacyAccountImportDO record = PayLegacyAccountImportDO.builder()
|
||||
.sourceTenantId(reqVO.getSourceTenantId().toLowerCase(Locale.ROOT))
|
||||
.sourceAccountId(reqVO.getSourceAccountId().toLowerCase(Locale.ROOT))
|
||||
.sourceChecksumSha256(reqVO.getSourceChecksumSha256().toLowerCase(Locale.ROOT))
|
||||
.sourceProvider(reqVO.getSourceProvider().trim())
|
||||
.normalizedProvider(provider)
|
||||
.sourceMode(reqVO.getSourceMode())
|
||||
.sourceStatus(reqVO.getSourceStatus())
|
||||
.targetAppId(appId)
|
||||
.targetChannelId(channelId)
|
||||
.targetChannelCode(reqVO.getTargetChannelCode())
|
||||
.targetStatus(targetStatus)
|
||||
.normalizedConfigDigest(configDigest)
|
||||
.mappingNotes(List.copyOf(notes))
|
||||
.importedBy(importedBy)
|
||||
.importedAt(now)
|
||||
.build();
|
||||
importMapper.insert(record);
|
||||
return toResp(record, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<PayLegacyAccountImportRespVO> getImportPage(PayLegacyAccountImportPageReqVO reqVO) {
|
||||
PageResult<PayLegacyAccountImportDO> page = importMapper.selectPage(reqVO);
|
||||
return new PageResult<>(page.getList().stream().map(record -> toResp(record, false)).toList(), page.getTotal());
|
||||
}
|
||||
|
||||
private String normalizeProvider(String value) {
|
||||
String normalized = normalizeToken(value);
|
||||
if (Set.of("wechat", "wechatpay", "wxpay", "wx_pay", "wechat_pay").contains(normalized)) {
|
||||
return "wechat_pay";
|
||||
}
|
||||
if (Set.of("alipay", "ali_pay").contains(normalized)) {
|
||||
return "alipay";
|
||||
}
|
||||
throw exception(LEGACY_ACCOUNT_IMPORT_UNSUPPORTED_PROVIDER, value);
|
||||
}
|
||||
|
||||
private String normalizeToken(String value) {
|
||||
return value.trim().toLowerCase(Locale.ROOT).replace('-', '_').replace(' ', '_');
|
||||
}
|
||||
|
||||
private void validateChannelFamily(String provider, String channelCode) {
|
||||
boolean matches = "wechat_pay".equals(provider) ? PayChannelEnum.isWeixin(channelCode)
|
||||
: PayChannelEnum.isAlipay(channelCode);
|
||||
if (!matches) {
|
||||
throw exception(LEGACY_ACCOUNT_IMPORT_CHANNEL_MISMATCH, channelCode, provider);
|
||||
}
|
||||
}
|
||||
|
||||
private WxPayClientConfig mapWechatConfig(PayLegacyAccountImportReqVO reqVO, List<String> notes) {
|
||||
Map<String, Object> publicConfig = reqVO.getConfigPublic();
|
||||
Map<String, Object> secretConfig = reqVO.getSecretJson();
|
||||
WxPayClientConfig config = new WxPayClientConfig();
|
||||
config.setApiVersion(WxPayClientConfig.API_VERSION_V3);
|
||||
config.setAppId(required(publicConfig, "appId"));
|
||||
config.setMchId(required(publicConfig, "merchantId", "mchId"));
|
||||
config.setCertSerialNo(required(publicConfig, "merchantSerialNo", "certSerialNo", "serialNo"));
|
||||
config.setPrivateKeyContent(required(secretConfig, "privateKey", "merchantPrivateKey", "privateKeyContent"));
|
||||
config.setApiV3Key(required(secretConfig, "apiV3Key", "apiv3Key"));
|
||||
|
||||
String publicKey = optional(publicConfig, "wechatpayPublicKey", "platformCertificatePublicKey", "publicKeyContent");
|
||||
String publicKeyId = optional(publicConfig, "wechatpayPublicKeyId", "platformCertificateSerial",
|
||||
"wechatpaySerial", "publicKeyId");
|
||||
Map<String, String> rotatingKeys = stringMap(publicConfig.get("wechatpayPublicKeys"));
|
||||
if (publicKey.isEmpty() && rotatingKeys.size() == 1) {
|
||||
Map.Entry<String, String> entry = rotatingKeys.entrySet().iterator().next();
|
||||
publicKeyId = entry.getKey();
|
||||
publicKey = entry.getValue();
|
||||
notes.add("单个旧微信平台公钥已映射为原生 Pay publicKeyId/publicKeyContent");
|
||||
} else if (publicKey.isEmpty() && rotatingKeys.size() > 1) {
|
||||
throw exception(LEGACY_ACCOUNT_IMPORT_CONFIG_UNSAFE,
|
||||
"微信轮换公钥多于一个,请先显式选择当前 publicKeyId/publicKeyContent");
|
||||
}
|
||||
if (publicKeyId.isEmpty()) {
|
||||
throw exception(LEGACY_ACCOUNT_IMPORT_CONFIG_MISSING, "wechatpayPublicKeyId/publicKeyId");
|
||||
}
|
||||
config.setPublicKeyId(publicKeyId);
|
||||
config.setPublicKeyContent(publicKey.isEmpty() ? null : publicKey);
|
||||
if (publicConfig.containsKey("endpoint") || publicConfig.containsKey("refundEndpoint")) {
|
||||
notes.add("旧微信自定义 endpoint 未迁移;原生 Pay SDK 使用其受支持的官方协议端点");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
private AlipayPayClientConfig mapAlipayConfig(PayLegacyAccountImportReqVO reqVO, List<String> notes) {
|
||||
Map<String, Object> publicConfig = reqVO.getConfigPublic();
|
||||
Map<String, Object> secretConfig = reqVO.getSecretJson();
|
||||
String serverUrl = optional(publicConfig, "endpoint", "serverUrl");
|
||||
if (serverUrl.isEmpty()) {
|
||||
serverUrl = "https://openapi.alipay.com/gateway.do";
|
||||
}
|
||||
if (!ALIPAY_SERVER_URLS.contains(serverUrl)) {
|
||||
throw exception(LEGACY_ACCOUNT_IMPORT_CONFIG_UNSAFE,
|
||||
"支付宝 endpoint 不是原生 Pay 允许的生产或沙箱网关");
|
||||
}
|
||||
AlipayPayClientConfig config = new AlipayPayClientConfig();
|
||||
config.setServerUrl(serverUrl);
|
||||
config.setAppId(required(publicConfig, "appId"));
|
||||
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
|
||||
config.setMode(AlipayPayClientConfig.MODE_PUBLIC_KEY);
|
||||
config.setPrivateKey(required(secretConfig, "privateKey", "appPrivateKey"));
|
||||
config.setAlipayPublicKey(required(secretConfig, "alipayPublicKey", "publicKey"));
|
||||
if (publicConfig.containsKey("sellerId") || publicConfig.containsKey("seller_id")
|
||||
|| publicConfig.containsKey("pid")) {
|
||||
notes.add("旧 sellerId 仅用于旧回调校验,原生 Pay 渠道配置不保存该字段");
|
||||
}
|
||||
if (publicConfig.containsKey("returnUrl") || publicConfig.containsKey("quitUrl")) {
|
||||
notes.add("旧 returnUrl/quitUrl 不属于原生渠道凭据,未迁移");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
private String required(Map<String, Object> values, String... keys) {
|
||||
String value = optional(values, keys);
|
||||
if (value.isEmpty()) {
|
||||
throw exception(LEGACY_ACCOUNT_IMPORT_CONFIG_MISSING, String.join("/", keys));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String optional(Map<String, Object> values, String... keys) {
|
||||
for (String key : keys) {
|
||||
Object value = values.get(key);
|
||||
if (value instanceof String string && !string.isBlank()) {
|
||||
return string.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private Map<String, String> stringMap(Object value) {
|
||||
if (!(value instanceof Map<?, ?> map)) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
map.forEach((key, item) -> {
|
||||
if (key instanceof String stringKey && item instanceof String stringValue
|
||||
&& !stringKey.isBlank() && !stringValue.isBlank()) {
|
||||
result.put(stringKey.trim(), stringValue.trim());
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private PayLegacyAccountImportRespVO toResp(PayLegacyAccountImportDO record, boolean replayed) {
|
||||
return PayLegacyAccountImportRespVO.builder()
|
||||
.id(record.getId())
|
||||
.sourceTenantId(record.getSourceTenantId())
|
||||
.sourceAccountId(record.getSourceAccountId())
|
||||
.normalizedProvider(record.getNormalizedProvider())
|
||||
.sourceMode(record.getSourceMode())
|
||||
.sourceStatus(record.getSourceStatus())
|
||||
.targetAppId(record.getTargetAppId())
|
||||
.targetChannelId(record.getTargetChannelId())
|
||||
.targetChannelCode(record.getTargetChannelCode())
|
||||
.targetStatus(record.getTargetStatus())
|
||||
.normalizedConfigDigest(record.getNormalizedConfigDigest())
|
||||
.mappingNotes(record.getMappingNotes())
|
||||
.importedBy(record.getImportedBy())
|
||||
.importedAt(record.getImportedAt())
|
||||
.replayed(replayed)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cn.iocoder.yudao.module.pay.service.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyTransactionImportPageReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyTransactionImportReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyTransactionImportRespVO;
|
||||
|
||||
public interface PayLegacyTransactionImportService {
|
||||
PayLegacyTransactionImportRespVO importTransaction(PayLegacyTransactionImportReqVO reqVO, Long importedBy);
|
||||
PageResult<PayLegacyTransactionImportRespVO> getImportPage(PayLegacyTransactionImportPageReqVO reqVO);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package cn.iocoder.yudao.module.pay.service.legacy;
|
||||
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.*;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.legacy.*;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.legacy.*;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderExtensionMapper;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
|
||||
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
|
||||
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
public class PayLegacyTransactionImportServiceImpl implements PayLegacyTransactionImportService {
|
||||
|
||||
private static final Set<String> SUCCESS_PAYMENT_STATUSES =
|
||||
Set.of("paid", "partially_refunded", "refunded");
|
||||
|
||||
@Resource private PayLegacyTransactionImportMapper importMapper;
|
||||
@Resource private PayLegacyTransactionPaymentImportMapper paymentImportMapper;
|
||||
@Resource private PayLegacyTransactionRefundImportMapper refundImportMapper;
|
||||
@Resource private PayLegacyAccountImportMapper accountImportMapper;
|
||||
@Resource private PayOrderMapper orderMapper;
|
||||
@Resource private PayOrderExtensionMapper extensionMapper;
|
||||
@Resource private PayRefundMapper refundMapper;
|
||||
@Resource private PayAppService appService;
|
||||
@Resource private PayChannelService channelService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PayLegacyTransactionImportRespVO importTransaction(PayLegacyTransactionImportReqVO req, Long importedBy) {
|
||||
String sourceOrderId = lower(req.getSourceOrderId());
|
||||
PayLegacyTransactionImportDO existing = importMapper.selectBySourceOrderId(sourceOrderId);
|
||||
if (existing != null) {
|
||||
if (!existing.getSourceChecksumSha256().equalsIgnoreCase(req.getSourceChecksumSha256())) {
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_CONFLICT, sourceOrderId);
|
||||
}
|
||||
return toResp(existing, true);
|
||||
}
|
||||
|
||||
PayLegacyAccountImportDO account = accountImportMapper.selectById(req.getSourceAccountImportId());
|
||||
if (account == null || !account.getSourceTenantId().equalsIgnoreCase(req.getSourceTenantId())) {
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_ACCOUNT_INVALID, req.getSourceAccountImportId());
|
||||
}
|
||||
PayAppDO app = appService.getApp(account.getTargetAppId());
|
||||
PayChannelDO channel = channelService.getChannel(account.getTargetChannelId());
|
||||
if (app == null || channel == null || !Objects.equals(channel.getAppId(), app.getId())
|
||||
|| !Objects.equals(channel.getCode(), account.getTargetChannelCode())) {
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_ACCOUNT_INVALID, req.getSourceAccountImportId());
|
||||
}
|
||||
if (app.getOrderNotifyUrl() == null || app.getOrderNotifyUrl().isBlank()
|
||||
|| app.getRefundNotifyUrl() == null || app.getRefundNotifyUrl().isBlank()) {
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_ACCOUNT_INVALID, "目标 Pay App 缺少业务回调");
|
||||
}
|
||||
|
||||
Validation result = validate(req, account.getNormalizedProvider());
|
||||
validateNativeNumberAvailability(req, app.getId());
|
||||
LocalDateTime importedAt = LocalDateTime.now();
|
||||
PayOrderDO order = buildOrder(req, app, channel, result.orderStatus());
|
||||
orderMapper.insert(order);
|
||||
|
||||
PayOrderExtensionDO successfulExtension = null;
|
||||
Map<String, PayOrderExtensionDO> extensions = new LinkedHashMap<>();
|
||||
for (PayLegacyTransactionImportReqVO.Payment payment : req.getPayments()) {
|
||||
int status = SUCCESS_PAYMENT_STATUSES.contains(payment.getSourceStatus())
|
||||
? PayOrderStatusEnum.SUCCESS.getStatus() : PayOrderStatusEnum.CLOSED.getStatus();
|
||||
PayOrderExtensionDO extension = PayOrderExtensionDO.builder()
|
||||
.no(payment.getProviderOutTradeNo()).orderId(order.getId()).channelId(channel.getId())
|
||||
.channelCode(channel.getCode()).userIp("0.0.0.0").status(status)
|
||||
.channelExtras(Map.of()).channelNotifyData(null).build();
|
||||
extension.setCreateTime(payment.getSourceCreatedAt());
|
||||
extensionMapper.insert(extension);
|
||||
extensions.put(lower(payment.getSourcePaymentId()), extension);
|
||||
if (status == PayOrderStatusEnum.SUCCESS.getStatus()) successfulExtension = extension;
|
||||
}
|
||||
if (successfulExtension != null) {
|
||||
PayLegacyTransactionImportReqVO.Payment payment = result.successfulPayment();
|
||||
PayOrderDO update = new PayOrderDO().setExtensionId(successfulExtension.getId())
|
||||
.setNo(successfulExtension.getNo()).setChannelOrderNo(payment.getProviderTradeNo());
|
||||
orderMapper.updateById(update.setId(order.getId()));
|
||||
order.setExtensionId(successfulExtension.getId()).setNo(successfulExtension.getNo())
|
||||
.setChannelOrderNo(payment.getProviderTradeNo());
|
||||
}
|
||||
|
||||
Map<String, PayRefundDO> refunds = new LinkedHashMap<>();
|
||||
for (PayLegacyTransactionImportReqVO.Refund source : req.getRefunds()) {
|
||||
int status = "succeeded".equals(source.getSourceStatus())
|
||||
? PayRefundStatusEnum.SUCCESS.getStatus() : PayRefundStatusEnum.FAILURE.getStatus();
|
||||
PayRefundDO refund = PayRefundDO.builder().no(source.getRefundNo()).appId(app.getId())
|
||||
.channelId(channel.getId()).channelCode(channel.getCode()).orderId(order.getId())
|
||||
.orderNo(order.getNo()).userId(req.getTargetUserId())
|
||||
.userType(req.getTargetUserId() == null ? null : UserTypeEnum.MEMBER.getValue())
|
||||
.merchantOrderId(req.getSourceOrderNo()).merchantRefundId(source.getRefundNo())
|
||||
.notifyUrl(app.getRefundNotifyUrl()).status(status).payPrice(req.getPrice())
|
||||
.refundPrice(source.getAmount()).reason(source.getReason()).userIp("0.0.0.0")
|
||||
.channelOrderNo(order.getChannelOrderNo()).channelRefundNo(source.getProviderRefundNo())
|
||||
.successTime(source.getSourceSucceededAt()).channelNotifyData(null).build();
|
||||
refund.setCreateTime(source.getSourceCreatedAt());
|
||||
refundMapper.insert(refund);
|
||||
refunds.put(lower(source.getSourceRefundId()), refund);
|
||||
}
|
||||
|
||||
List<String> notes = List.of(
|
||||
"仅导入已终结且金额对账通过的交易;未调用渠道 SDK、业务回调或通知任务",
|
||||
"旧 raw_payload、payment_events.payload 与错误原文未复制,仅保留数量和 SHA-256 摘要",
|
||||
"旧模型无可靠用户 IP 与渠道手续费,分别使用 0.0.0.0 与 0");
|
||||
String digest = DigestUtil.sha256Hex(JsonUtils.toJsonString(req));
|
||||
int eventCount = req.getPayments().stream().mapToInt(PayLegacyTransactionImportReqVO.Payment::getSourceEventCount).sum();
|
||||
PayLegacyTransactionImportDO audit = PayLegacyTransactionImportDO.builder()
|
||||
.sourceTenantId(lower(req.getSourceTenantId())).sourceOrderId(sourceOrderId)
|
||||
.sourceChecksumSha256(lower(req.getSourceChecksumSha256())).sourceOrderNo(req.getSourceOrderNo())
|
||||
.sourceOrderStatus(req.getSourceOrderStatus()).sourcePaymentCount(req.getPayments().size())
|
||||
.sourcePaymentEventCount(eventCount).sourceRefundCount(req.getRefunds().size())
|
||||
.sourceRefundedPrice(req.getRefundedPrice()).sourceAccountImportId(account.getId())
|
||||
.targetAppId(app.getId()).targetChannelId(channel.getId()).targetOrderId(order.getId())
|
||||
.normalizedPayloadDigest(digest).mappingNotes(notes).importedBy(importedBy).importedAt(importedAt).build();
|
||||
importMapper.insert(audit);
|
||||
req.getPayments().forEach(source -> paymentImportMapper.insert(PayLegacyTransactionPaymentImportDO.builder()
|
||||
.transactionImportId(audit.getId()).sourcePaymentId(lower(source.getSourcePaymentId()))
|
||||
.sourceStatus(source.getSourceStatus()).sourceProvider(source.getSourceProvider().trim())
|
||||
.sourceEventCount(source.getSourceEventCount())
|
||||
.sourceEventDigest(source.getSourceEventDigest() == null ? null : lower(source.getSourceEventDigest()))
|
||||
.targetExtensionId(extensions.get(lower(source.getSourcePaymentId())).getId()).build()));
|
||||
req.getRefunds().forEach(source -> refundImportMapper.insert(PayLegacyTransactionRefundImportDO.builder()
|
||||
.transactionImportId(audit.getId()).sourceRefundId(lower(source.getSourceRefundId()))
|
||||
.sourceStatus(source.getSourceStatus())
|
||||
.targetRefundId(refunds.get(lower(source.getSourceRefundId())).getId()).build()));
|
||||
return toResp(audit, false);
|
||||
}
|
||||
|
||||
private Validation validate(PayLegacyTransactionImportReqVO req, String expectedProvider) {
|
||||
Set<String> paymentIds = new HashSet<>(), outTradeNos = new HashSet<>(), refundIds = new HashSet<>();
|
||||
PayLegacyTransactionImportReqVO.Payment successful = null;
|
||||
for (PayLegacyTransactionImportReqVO.Payment payment : req.getPayments()) {
|
||||
if (!paymentIds.add(lower(payment.getSourcePaymentId())) || !outTradeNos.add(payment.getProviderOutTradeNo()))
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "支付 ID 或商户支付号重复");
|
||||
requireProvider(expectedProvider, payment.getSourceProvider());
|
||||
if (!Objects.equals(payment.getAmount(), req.getPrice()))
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "支付金额与订单金额不一致");
|
||||
if (payment.getSourceEventCount() == 0 && payment.getSourceEventDigest() != null
|
||||
|| payment.getSourceEventCount() > 0 && payment.getSourceEventDigest() == null)
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "支付事件数量与摘要不一致");
|
||||
if (SUCCESS_PAYMENT_STATUSES.contains(payment.getSourceStatus())) {
|
||||
if (successful != null || payment.getProviderTradeNo() == null || payment.getProviderTradeNo().isBlank()
|
||||
|| payment.getSourcePaidAt() == null)
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "必须且只能有一笔可核验的成功支付");
|
||||
successful = payment;
|
||||
}
|
||||
}
|
||||
boolean paidOrder = Set.of("paid", "partially_refunded", "refunded").contains(req.getSourceOrderStatus());
|
||||
if (paidOrder != (successful != null) || paidOrder && req.getSourcePaidAt() == null)
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "订单状态与成功支付不一致");
|
||||
if (!req.getRefunds().isEmpty() && successful == null)
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "退款记录必须关联已成功支付");
|
||||
int succeededRefund = 0;
|
||||
for (PayLegacyTransactionImportReqVO.Refund refund : req.getRefunds()) {
|
||||
if (!refundIds.add(lower(refund.getSourceRefundId())))
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "退款 ID 重复");
|
||||
requireProvider(expectedProvider, refund.getSourceProvider());
|
||||
if (refund.getAmount() > req.getPrice())
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "退款金额超过订单金额");
|
||||
if ("succeeded".equals(refund.getSourceStatus())) {
|
||||
if (refund.getSourceSucceededAt() == null)
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "成功退款缺少成功时间");
|
||||
succeededRefund += refund.getAmount();
|
||||
}
|
||||
}
|
||||
if (succeededRefund != req.getRefundedPrice())
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "成功退款合计与订单已退金额不一致");
|
||||
int orderStatus = switch (req.getSourceOrderStatus()) {
|
||||
case "paid" -> requireRefund(req, 0, false, PayOrderStatusEnum.SUCCESS.getStatus());
|
||||
case "partially_refunded" -> requireRefund(req, req.getRefundedPrice(), true,
|
||||
PayOrderStatusEnum.SUCCESS.getStatus());
|
||||
case "refunded" -> requireRefund(req, req.getPrice(), false, PayOrderStatusEnum.REFUND.getStatus());
|
||||
case "failed", "closed" -> requireRefund(req, 0, false, PayOrderStatusEnum.CLOSED.getStatus());
|
||||
default -> throw exception(LEGACY_TRANSACTION_IMPORT_UNSAFE_STATE, req.getSourceOrderStatus());
|
||||
};
|
||||
return new Validation(orderStatus, successful);
|
||||
}
|
||||
|
||||
private void validateNativeNumberAvailability(PayLegacyTransactionImportReqVO req, Long appId) {
|
||||
if (orderMapper.selectByAppIdAndMerchantOrderId(appId, req.getSourceOrderNo()) != null)
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "目标应用已存在相同旧订单号");
|
||||
for (PayLegacyTransactionImportReqVO.Payment payment : req.getPayments()) {
|
||||
if (extensionMapper.selectByNo(payment.getProviderOutTradeNo()) != null)
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "目标账本已存在相同商户支付号");
|
||||
}
|
||||
for (PayLegacyTransactionImportReqVO.Refund refund : req.getRefunds()) {
|
||||
if (refundMapper.selectByAppIdAndNo(appId, refund.getRefundNo()) != null)
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "目标应用已存在相同退款号");
|
||||
}
|
||||
}
|
||||
|
||||
private int requireRefund(PayLegacyTransactionImportReqVO req, int expected, boolean partial, int status) {
|
||||
if (req.getRefundedPrice() != expected && !partial
|
||||
|| partial && (req.getRefundedPrice() <= 0 || req.getRefundedPrice() >= req.getPrice()))
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "订单状态与已退金额不一致");
|
||||
return status;
|
||||
}
|
||||
|
||||
private void requireProvider(String expected, String actual) {
|
||||
String normalized = actual.trim().toLowerCase(Locale.ROOT).replace('-', '_');
|
||||
if (Set.of("wechat", "wechatpay", "wxpay", "wx_pay").contains(normalized)) normalized = "wechat_pay";
|
||||
if (!Objects.equals(expected, normalized))
|
||||
throw exception(LEGACY_TRANSACTION_IMPORT_PROVIDER_MISMATCH, actual);
|
||||
}
|
||||
|
||||
private PayOrderDO buildOrder(PayLegacyTransactionImportReqVO req, PayAppDO app,
|
||||
PayChannelDO channel, int status) {
|
||||
PayOrderDO order = PayOrderDO.builder().appId(app.getId()).channelId(channel.getId())
|
||||
.channelCode(channel.getCode()).userId(req.getTargetUserId())
|
||||
.userType(req.getTargetUserId() == null ? null : UserTypeEnum.MEMBER.getValue())
|
||||
.merchantOrderId(req.getSourceOrderNo()).subject(req.getSubject()).body(req.getBody())
|
||||
.notifyUrl(app.getOrderNotifyUrl()).price(req.getPrice()).channelFeeRate(0D)
|
||||
.channelFeePrice(0).status(status).userIp("0.0.0.0").expireTime(req.getSourceCreatedAt())
|
||||
.successTime(req.getSourcePaidAt()).refundPrice(req.getRefundedPrice()).build();
|
||||
order.setCreateTime(req.getSourceCreatedAt());
|
||||
return order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<PayLegacyTransactionImportRespVO> getImportPage(PayLegacyTransactionImportPageReqVO reqVO) {
|
||||
PageResult<PayLegacyTransactionImportDO> page = importMapper.selectPage(reqVO);
|
||||
return new PageResult<>(page.getList().stream().map(row -> toResp(row, false)).toList(), page.getTotal());
|
||||
}
|
||||
|
||||
private PayLegacyTransactionImportRespVO toResp(PayLegacyTransactionImportDO row, boolean replayed) {
|
||||
return PayLegacyTransactionImportRespVO.builder().id(row.getId()).sourceTenantId(row.getSourceTenantId())
|
||||
.sourceOrderId(row.getSourceOrderId()).sourceOrderNo(row.getSourceOrderNo())
|
||||
.sourceOrderStatus(row.getSourceOrderStatus()).sourcePaymentCount(row.getSourcePaymentCount())
|
||||
.sourcePaymentEventCount(row.getSourcePaymentEventCount()).sourceRefundCount(row.getSourceRefundCount())
|
||||
.sourceRefundedPrice(row.getSourceRefundedPrice()).sourceAccountImportId(row.getSourceAccountImportId())
|
||||
.targetAppId(row.getTargetAppId()).targetChannelId(row.getTargetChannelId())
|
||||
.targetOrderId(row.getTargetOrderId()).normalizedPayloadDigest(row.getNormalizedPayloadDigest())
|
||||
.mappingNotes(row.getMappingNotes()).importedBy(row.getImportedBy()).importedAt(row.getImportedAt())
|
||||
.replayed(replayed).build();
|
||||
}
|
||||
|
||||
private String lower(String value) { return value.toLowerCase(Locale.ROOT); }
|
||||
private record Validation(int orderStatus, PayLegacyTransactionImportReqVO.Payment successfulPayment) {}
|
||||
}
|
||||
@@ -293,7 +293,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
|
||||
}
|
||||
// 校验钱包余额是否足够
|
||||
PayWalletDO wallet = payWalletService.getWallet(walletRecharge.getWalletId());
|
||||
Assert.notNull(wallet, "用户钱包({}) 不存在", wallet.getId());
|
||||
Assert.notNull(wallet, "用户钱包({}) 不存在", walletRecharge.getWalletId());
|
||||
if (wallet.getBalance() < walletRecharge.getTotalPrice()) {
|
||||
throw exception(WALLET_RECHARGE_REFUND_BALANCE_NOT_ENOUGH);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,9 @@ public class PayWalletServiceImpl implements PayWalletService {
|
||||
@SneakyThrows
|
||||
public PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId,
|
||||
PayWalletBizTypeEnum bizType, Integer price) {
|
||||
if (price == null || price <= 0) {
|
||||
throw new IllegalArgumentException("扣减钱包余额必须为正数");
|
||||
}
|
||||
// 1. 获取钱包
|
||||
PayWalletDO payWallet = getWallet(walletId);
|
||||
if (payWallet == null) {
|
||||
@@ -164,6 +167,10 @@ public class PayWalletServiceImpl implements PayWalletService {
|
||||
updateCounts = walletMapper.updateWhenRechargeRefund(payWallet.getId(), price);
|
||||
break;
|
||||
}
|
||||
case UPDATE_BALANCE: {
|
||||
updateCounts = walletMapper.updateWhenSubtract(payWallet.getId(), price);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// TODO 其它类型待实现
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
@@ -191,6 +198,9 @@ public class PayWalletServiceImpl implements PayWalletService {
|
||||
@SneakyThrows
|
||||
public PayWalletTransactionDO addWalletBalance(Long walletId, String bizId,
|
||||
PayWalletBizTypeEnum bizType, Integer price) {
|
||||
if (price == null || price <= 0) {
|
||||
throw new IllegalArgumentException("增加钱包余额必须为正数");
|
||||
}
|
||||
// 1. 获取钱包
|
||||
PayWalletDO payWallet = getWallet(walletId);
|
||||
if (payWallet == null) {
|
||||
@@ -229,6 +239,9 @@ public class PayWalletServiceImpl implements PayWalletService {
|
||||
|
||||
@Override
|
||||
public void freezePrice(Long id, Integer price) {
|
||||
if (price == null || price <= 0) {
|
||||
throw new IllegalArgumentException("冻结钱包余额必须为正数");
|
||||
}
|
||||
int updateCounts = walletMapper.freezePrice(id, price);
|
||||
if (updateCounts == 0) {
|
||||
throw exception(WALLET_BALANCE_NOT_ENOUGH);
|
||||
@@ -237,6 +250,9 @@ public class PayWalletServiceImpl implements PayWalletService {
|
||||
|
||||
@Override
|
||||
public void unfreezePrice(Long id, Integer price) {
|
||||
if (price == null || price <= 0) {
|
||||
throw new IllegalArgumentException("解冻钱包余额必须为正数");
|
||||
}
|
||||
int updateCounts = walletMapper.unFreezePrice(id, price);
|
||||
if (updateCounts == 0) {
|
||||
throw exception(WALLET_FREEZE_PRICE_NOT_ENOUGH);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportReqVO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PayLegacyAccountImportControllerContractTest {
|
||||
|
||||
@Test
|
||||
void importRequiresBothWritePermissionsAndDisablesRequestBodyLogging() throws NoSuchMethodException {
|
||||
Method method = PayLegacyAccountImportController.class.getMethod(
|
||||
"importAccount", PayLegacyAccountImportReqVO.class);
|
||||
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("@ss.hasPermission('pay:app:create') and @ss.hasPermission('pay:channel:create')");
|
||||
assertThat(method.getAnnotation(ApiAccessLog.class).requestEnable()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyTransactionImportReqVO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PayLegacyTransactionImportControllerContractTest {
|
||||
@Test
|
||||
void importUsesDedicatedPermissionAndSuppressesRequestLogging() throws NoSuchMethodException {
|
||||
Method method = PayLegacyTransactionImportController.class.getMethod(
|
||||
"importTransaction", PayLegacyTransactionImportReqVO.class);
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("@ss.hasPermission('pay:legacy-transaction:import')");
|
||||
assertThat(method.getAnnotation(ApiAccessLog.class).requestEnable()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package cn.iocoder.yudao.module.pay.controller.admin.wallet;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletUpdateBalanceReqVO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PayWalletControllerTest {
|
||||
|
||||
private final PayWalletService walletService = mock(PayWalletService.class);
|
||||
private final PayWalletController controller = new PayWalletController();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(controller, "payWalletService", walletService);
|
||||
PayWalletDO wallet = new PayWalletDO();
|
||||
wallet.setId(88L);
|
||||
when(walletService.getOrCreateWallet(7L, UserTypeEnum.MEMBER.getValue())).thenReturn(wallet);
|
||||
}
|
||||
|
||||
@Test
|
||||
void positiveAdjustmentUsesAddBalancePath() {
|
||||
PayWalletUpdateBalanceReqVO request = new PayWalletUpdateBalanceReqVO();
|
||||
request.setUserId(7L);
|
||||
request.setBalance(120);
|
||||
|
||||
controller.updateWalletBalance(request);
|
||||
|
||||
verify(walletService).addWalletBalance(88L, "7", PayWalletBizTypeEnum.UPDATE_BALANCE, 120);
|
||||
verify(walletService, never()).reduceWalletBalance(
|
||||
88L, 7L, PayWalletBizTypeEnum.UPDATE_BALANCE, 120);
|
||||
}
|
||||
|
||||
@Test
|
||||
void negativeAdjustmentUsesSafeSubtractPath() {
|
||||
PayWalletUpdateBalanceReqVO request = new PayWalletUpdateBalanceReqVO();
|
||||
request.setUserId(7L);
|
||||
request.setBalance(-120);
|
||||
|
||||
controller.updateWalletBalance(request);
|
||||
|
||||
verify(walletService).reduceWalletBalance(88L, 7L, PayWalletBizTypeEnum.UPDATE_BALANCE, 120);
|
||||
verify(walletService, never()).addWalletBalance(
|
||||
88L, "7", PayWalletBizTypeEnum.UPDATE_BALANCE, -120);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PayTransactionTenantContractTest {
|
||||
|
||||
@Test
|
||||
void orderRefundAndNotifyRecordsParticipateInFrameworkTenantIsolation() {
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayOrderDO.class);
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayOrderExtensionDO.class);
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayRefundDO.class);
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayNotifyTaskDO.class);
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayNotifyLogDO.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.wallet.PayWalletRechargeController;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletRechargeDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
|
||||
import cn.iocoder.yudao.module.pay.job.transfer.PayTransferSyncJob;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PayTransferWalletTenantContractTest {
|
||||
|
||||
@Test
|
||||
void transferAndWalletLedgersParticipateInFrameworkTenantIsolation() {
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayTransferDO.class);
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayWalletDO.class);
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayWalletTransactionDO.class);
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayWalletRechargeDO.class);
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayWalletRechargePackageDO.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferSynchronizationRunsForEveryTenant() throws NoSuchMethodException {
|
||||
Method method = PayTransferSyncJob.class.getMethod("execute", String.class);
|
||||
assertThat(method.getAnnotation(TenantJob.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rechargeRefundRequiresDedicatedPermission() throws NoSuchMethodException {
|
||||
Method method = PayWalletRechargeController.class.getMethod("refundWalletRecharge", Long.class);
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("@ss.hasPermission('pay:wallet-recharge:refund')");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.app;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.app.PayAppController;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PayAppTenantContractTest {
|
||||
|
||||
@Test
|
||||
void payApplicationsParticipateInFrameworkTenantIsolation() {
|
||||
assertThat(TenantBaseDO.class).isAssignableFrom(PayAppDO.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nativeAppListUsesTheAppQueryPermission() throws NoSuchMethodException {
|
||||
Method method = PayAppController.class.getMethod("getAppList");
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("@ss.hasPermission('pay:app:query')");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.redis.wallet;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PayWalletLockRedisDAOTest {
|
||||
|
||||
@AfterEach
|
||||
void clearTenant() {
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void lockKeyContainsTenantAndWalletIdentifiers() throws Exception {
|
||||
RedissonClient redissonClient = mock(RedissonClient.class);
|
||||
RLock lock = mock(RLock.class);
|
||||
when(redissonClient.getLock("pay_wallet:lock:42:99")).thenReturn(lock);
|
||||
PayWalletLockRedisDAO dao = new PayWalletLockRedisDAO();
|
||||
ReflectionTestUtils.setField(dao, "redissonClient", redissonClient);
|
||||
TenantContextHolder.setTenantId(42L);
|
||||
|
||||
String result = dao.lock(99L, 2_000L, () -> "done");
|
||||
|
||||
assertThat(result).isEqualTo("done");
|
||||
verify(redissonClient).getLock("pay_wallet:lock:42:99");
|
||||
verify(lock).lock(2_000L, TimeUnit.MILLISECONDS);
|
||||
verify(lock).unlock();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.wallet;
|
||||
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.NonePayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
|
||||
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletTransactionService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class WalletPayClientTest {
|
||||
|
||||
@Test
|
||||
void successfulTransferLooksUpWalletTransactionByTransferNumber() {
|
||||
PayTransferService transferService = mock(PayTransferService.class);
|
||||
PayWalletTransactionService transactionService = mock(PayWalletTransactionService.class);
|
||||
PayTransferDO transfer = new PayTransferDO();
|
||||
transfer.setId(77L);
|
||||
transfer.setNo("T20260801001");
|
||||
transfer.setStatus(PayTransferStatusEnum.SUCCESS.getStatus());
|
||||
PayWalletTransactionDO transaction = new PayWalletTransactionDO();
|
||||
transaction.setNo("WT20260801001");
|
||||
transaction.setCreateTime(LocalDateTime.of(2026, 8, 1, 6, 0));
|
||||
when(transferService.getTransferByNo("merchant-transfer-1")).thenReturn(transfer);
|
||||
when(transactionService.getWalletTransaction("T20260801001", PayWalletBizTypeEnum.TRANSFER))
|
||||
.thenReturn(transaction);
|
||||
WalletPayClient client = new WalletPayClient(1L, new NonePayClientConfig());
|
||||
ReflectionTestUtils.setField(client, "transferService", transferService);
|
||||
ReflectionTestUtils.setField(client, "walletTransactionService", transactionService);
|
||||
|
||||
PayTransferRespDTO response = client.getTransfer("merchant-transfer-1");
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(PayTransferStatusEnum.SUCCESS.getStatus());
|
||||
assertThat(response.getChannelTransferNo()).isEqualTo("WT20260801001");
|
||||
verify(transactionService).getWalletTransaction("T20260801001", PayWalletBizTypeEnum.TRANSFER);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.channel.PayChannelMapper;
|
||||
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
|
||||
@@ -12,9 +13,11 @@ import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientFactory;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Validator;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
@@ -27,6 +30,7 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -44,6 +48,37 @@ public class PayChannelServiceTest extends BaseDbUnitTest {
|
||||
private PayClientFactory payClientFactory;
|
||||
@MockitoBean
|
||||
private Validator validator;
|
||||
@MockitoBean
|
||||
private PayAppService payAppService;
|
||||
|
||||
@BeforeEach
|
||||
void setUpPayApp() {
|
||||
when(payAppService.getApp(anyLong())).thenReturn(new PayAppDO());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateChannel_appNotFound() {
|
||||
PayChannelCreateReqVO reqVO = randomPojo(PayChannelCreateReqVO.class);
|
||||
when(payAppService.getApp(reqVO.getAppId())).thenReturn(null);
|
||||
|
||||
assertServiceException(() -> channelService.createChannel(reqVO), APP_NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateChannel_appNotFound() {
|
||||
PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> {
|
||||
o.setCode(PayChannelEnum.ALIPAY_APP.getCode());
|
||||
o.setConfig(randomAlipayPayClientConfig());
|
||||
});
|
||||
channelMapper.insert(dbChannel);
|
||||
PayChannelUpdateReqVO reqVO = randomPojo(PayChannelUpdateReqVO.class, o -> {
|
||||
o.setId(dbChannel.getId());
|
||||
o.setConfig(JsonUtils.toJsonString(randomAlipayPayClientConfig()));
|
||||
});
|
||||
when(payAppService.getApp(reqVO.getAppId())).thenReturn(null);
|
||||
|
||||
assertServiceException(() -> channelService.updateChannel(reqVO), APP_NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateChannel_success() {
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
package cn.iocoder.yudao.module.pay.service.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyAccountImportRespVO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.legacy.PayLegacyAccountImportDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.legacy.PayLegacyAccountImportMapper;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig;
|
||||
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
|
||||
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_CHANNEL_MISMATCH;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_CONFIG_MISSING;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_CONFIG_UNSAFE;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_CONFLICT;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_UNSUPPORTED_MODE;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.LEGACY_ACCOUNT_IMPORT_UNSUPPORTED_PROVIDER;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PayLegacyAccountImportServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private PayLegacyAccountImportServiceImpl importService;
|
||||
@Mock
|
||||
private PayLegacyAccountImportMapper importMapper;
|
||||
@Mock
|
||||
private PayAppService appService;
|
||||
@Mock
|
||||
private PayChannelService channelService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(appService.createApp(any())).thenReturn(100L);
|
||||
lenient().when(channelService.createChannel(any())).thenReturn(200L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void importsWechatAliasIntoExplicitNativeChannelWithoutPersistingRawSourcePayload() {
|
||||
PayLegacyAccountImportReqVO reqVO = wechatRequest();
|
||||
reqVO.setSourceProvider("wechat-pay");
|
||||
|
||||
PayLegacyAccountImportRespVO result = importService.importAccount(reqVO, 7L);
|
||||
|
||||
ArgumentCaptor<PayAppCreateReqVO> app = ArgumentCaptor.forClass(PayAppCreateReqVO.class);
|
||||
verify(appService).createApp(app.capture());
|
||||
assertEquals("legacy-pay", app.getValue().getAppKey());
|
||||
assertEquals(CommonStatusEnum.ENABLE.getStatus(), app.getValue().getStatus());
|
||||
assertEquals("https://new.example/order", app.getValue().getOrderNotifyUrl());
|
||||
|
||||
ArgumentCaptor<PayChannelCreateReqVO> channel = ArgumentCaptor.forClass(PayChannelCreateReqVO.class);
|
||||
verify(channelService).createChannel(channel.capture());
|
||||
WxPayClientConfig config = JsonUtils.parseObject2(channel.getValue().getConfig(), WxPayClientConfig.class);
|
||||
assertEquals("wx-app", config.getAppId());
|
||||
assertEquals("mch-1", config.getMchId());
|
||||
assertEquals("serial-1", config.getCertSerialNo());
|
||||
assertEquals("private-key", config.getPrivateKeyContent());
|
||||
assertEquals("api-v3-key", config.getApiV3Key());
|
||||
assertEquals("platform-serial", config.getPublicKeyId());
|
||||
assertEquals("platform-key", config.getPublicKeyContent());
|
||||
|
||||
ArgumentCaptor<PayLegacyAccountImportDO> audit = ArgumentCaptor.forClass(PayLegacyAccountImportDO.class);
|
||||
verify(importMapper).insert(audit.capture());
|
||||
assertEquals("wechat_pay", audit.getValue().getNormalizedProvider());
|
||||
assertFalse(audit.getValue().toString().contains("private-key"));
|
||||
assertFalse(audit.getValue().toString().contains("api-v3-key"));
|
||||
assertNotNull(audit.getValue().getNormalizedConfigDigest());
|
||||
assertTrue(result.getMappingNotes().stream().anyMatch(note -> note.contains("Provider 别名")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importsDisabledAlipayIntoDisabledNativeObjects() {
|
||||
PayLegacyAccountImportReqVO reqVO = alipayRequest();
|
||||
reqVO.setSourceStatus("pending");
|
||||
|
||||
PayLegacyAccountImportRespVO result = importService.importAccount(reqVO, 8L);
|
||||
|
||||
ArgumentCaptor<PayChannelCreateReqVO> channel = ArgumentCaptor.forClass(PayChannelCreateReqVO.class);
|
||||
verify(channelService).createChannel(channel.capture());
|
||||
AlipayPayClientConfig config = JsonUtils.parseObject2(channel.getValue().getConfig(), AlipayPayClientConfig.class);
|
||||
assertEquals("ali-app", config.getAppId());
|
||||
assertEquals(AlipayPayClientConfig.MODE_PUBLIC_KEY, config.getMode());
|
||||
assertEquals(CommonStatusEnum.DISABLE.getStatus(), channel.getValue().getStatus());
|
||||
assertEquals(CommonStatusEnum.DISABLE.getStatus(), result.getTargetStatus());
|
||||
assertThat(result.getMappingNotes()).anyMatch(note -> note.contains("安全映射为禁用"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaysSameSourceChecksumWithoutCreatingAnotherNativeObject() {
|
||||
PayLegacyAccountImportReqVO reqVO = wechatRequest();
|
||||
PayLegacyAccountImportDO existing = existing(reqVO);
|
||||
when(importMapper.selectBySourceAccountId(reqVO.getSourceAccountId())).thenReturn(existing);
|
||||
|
||||
PayLegacyAccountImportRespVO result = importService.importAccount(reqVO, 7L);
|
||||
|
||||
assertTrue(result.getReplayed());
|
||||
verifyNoInteractions(appService, channelService);
|
||||
verify(importMapper, never()).insert(any(PayLegacyAccountImportDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsChecksumChangeForAlreadyImportedSourceAccount() {
|
||||
PayLegacyAccountImportReqVO reqVO = wechatRequest();
|
||||
PayLegacyAccountImportDO existing = existing(reqVO);
|
||||
existing.setSourceChecksumSha256("b".repeat(64));
|
||||
when(importMapper.selectBySourceAccountId(reqVO.getSourceAccountId())).thenReturn(existing);
|
||||
|
||||
assertServiceException(() -> importService.importAccount(reqVO, 7L), LEGACY_ACCOUNT_IMPORT_CONFLICT,
|
||||
reqVO.getSourceAccountId());
|
||||
verifyNoInteractions(appService, channelService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonEquivalentCollectionModes() {
|
||||
PayLegacyAccountImportReqVO reqVO = wechatRequest();
|
||||
reqVO.setSourceMode("service_provider");
|
||||
|
||||
assertServiceException(() -> importService.importAccount(reqVO, 7L),
|
||||
LEGACY_ACCOUNT_IMPORT_UNSUPPORTED_MODE, "service_provider");
|
||||
verifyNoInteractions(appService, channelService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnsupportedLegacyProviders() {
|
||||
PayLegacyAccountImportReqVO reqVO = wechatRequest();
|
||||
reqVO.setSourceProvider("xpay");
|
||||
|
||||
assertServiceException(() -> importService.importAccount(reqVO, 7L),
|
||||
LEGACY_ACCOUNT_IMPORT_UNSUPPORTED_PROVIDER, "xpay");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsImplicitWechatToAlipayChannelMapping() {
|
||||
PayLegacyAccountImportReqVO reqVO = wechatRequest();
|
||||
reqVO.setTargetChannelCode("alipay_wap");
|
||||
|
||||
assertServiceException(() -> importService.importAccount(reqVO, 7L),
|
||||
LEGACY_ACCOUNT_IMPORT_CHANNEL_MISMATCH, "alipay_wap", "wechat_pay");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWechatRotatingKeySetUntilOneKeyIsExplicitlySelected() {
|
||||
PayLegacyAccountImportReqVO reqVO = wechatRequest();
|
||||
reqVO.getConfigPublic().put("wechatpayPublicKeys", Map.of("serial-a", "key-a", "serial-b", "key-b"));
|
||||
|
||||
assertServiceException(() -> importService.importAccount(reqVO, 7L),
|
||||
LEGACY_ACCOUNT_IMPORT_CONFIG_UNSAFE,
|
||||
"微信轮换公钥多于一个,请先显式选择当前 publicKeyId/publicKeyContent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnsafeAlipayEndpointAndMissingRequiredCredential() {
|
||||
PayLegacyAccountImportReqVO reqVO = alipayRequest();
|
||||
reqVO.getConfigPublic().put("endpoint", "http://127.0.0.1:9999/gateway.do");
|
||||
assertServiceException(() -> importService.importAccount(reqVO, 7L),
|
||||
LEGACY_ACCOUNT_IMPORT_CONFIG_UNSAFE, "支付宝 endpoint 不是原生 Pay 允许的生产或沙箱网关");
|
||||
|
||||
reqVO.getConfigPublic().put("endpoint", "https://openapi.alipay.com/gateway.do");
|
||||
reqVO.getSecretJson().remove("alipayPublicKey");
|
||||
assertServiceException(() -> importService.importAccount(reqVO, 7L),
|
||||
LEGACY_ACCOUNT_IMPORT_CONFIG_MISSING, "alipayPublicKey/publicKey");
|
||||
}
|
||||
|
||||
private PayLegacyAccountImportReqVO wechatRequest() {
|
||||
PayLegacyAccountImportReqVO reqVO = baseRequest();
|
||||
reqVO.setSourceProvider("wechat_pay");
|
||||
reqVO.setTargetChannelCode("wx_lite");
|
||||
Map<String, Object> publicConfig = new LinkedHashMap<>();
|
||||
publicConfig.put("appId", "wx-app");
|
||||
publicConfig.put("merchantId", "mch-1");
|
||||
publicConfig.put("merchantSerialNo", "serial-1");
|
||||
publicConfig.put("wechatpayPublicKeys", Map.of("platform-serial", "platform-key"));
|
||||
publicConfig.put("notifyUrl", "https://legacy.example/wechat/notify");
|
||||
reqVO.setConfigPublic(publicConfig);
|
||||
reqVO.setSecretJson(new LinkedHashMap<>(Map.of(
|
||||
"privateKey", "private-key",
|
||||
"apiV3Key", "api-v3-key")));
|
||||
return reqVO;
|
||||
}
|
||||
|
||||
private PayLegacyAccountImportReqVO alipayRequest() {
|
||||
PayLegacyAccountImportReqVO reqVO = baseRequest();
|
||||
reqVO.setSourceProvider("alipay");
|
||||
reqVO.setTargetChannelCode("alipay_wap");
|
||||
reqVO.setConfigPublic(new LinkedHashMap<>(Map.of(
|
||||
"appId", "ali-app",
|
||||
"sellerId", "seller-1",
|
||||
"endpoint", "https://openapi.alipay.com/gateway.do")));
|
||||
reqVO.setSecretJson(new LinkedHashMap<>(Map.of(
|
||||
"privateKey", "ali-private-key",
|
||||
"alipayPublicKey", "ali-public-key")));
|
||||
return reqVO;
|
||||
}
|
||||
|
||||
private PayLegacyAccountImportReqVO baseRequest() {
|
||||
PayLegacyAccountImportReqVO reqVO = new PayLegacyAccountImportReqVO();
|
||||
reqVO.setSourceTenantId("11111111-1111-4111-8111-111111111111");
|
||||
reqVO.setSourceAccountId("22222222-2222-4222-8222-222222222222");
|
||||
reqVO.setSourceChecksumSha256("a".repeat(64));
|
||||
reqVO.setSourceMode("tenant_collect");
|
||||
reqVO.setSourceStatus("active");
|
||||
reqVO.setAppKey("legacy-pay");
|
||||
reqVO.setAppName("旧支付账号");
|
||||
reqVO.setOrderNotifyUrl("https://new.example/order");
|
||||
reqVO.setRefundNotifyUrl("https://new.example/refund");
|
||||
return reqVO;
|
||||
}
|
||||
|
||||
private PayLegacyAccountImportDO existing(PayLegacyAccountImportReqVO reqVO) {
|
||||
return PayLegacyAccountImportDO.builder()
|
||||
.id(9L)
|
||||
.sourceTenantId(reqVO.getSourceTenantId())
|
||||
.sourceAccountId(reqVO.getSourceAccountId())
|
||||
.sourceChecksumSha256(reqVO.getSourceChecksumSha256())
|
||||
.normalizedProvider("wechat_pay")
|
||||
.sourceMode("tenant_collect")
|
||||
.sourceStatus("active")
|
||||
.targetAppId(100L)
|
||||
.targetChannelId(200L)
|
||||
.targetChannelCode("wx_lite")
|
||||
.targetStatus(CommonStatusEnum.ENABLE.getStatus())
|
||||
.normalizedConfigDigest("c".repeat(64))
|
||||
.mappingNotes(List.of("mapped"))
|
||||
.importedBy(7L)
|
||||
.importedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package cn.iocoder.yudao.module.pay.service.legacy;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyTransactionImportReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.legacy.vo.PayLegacyTransactionImportRespVO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.legacy.*;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.legacy.*;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderExtensionMapper;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
|
||||
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
|
||||
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class PayLegacyTransactionImportServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks private PayLegacyTransactionImportServiceImpl service;
|
||||
@Mock private PayLegacyTransactionImportMapper importMapper;
|
||||
@Mock private PayLegacyTransactionPaymentImportMapper paymentImportMapper;
|
||||
@Mock private PayLegacyTransactionRefundImportMapper refundImportMapper;
|
||||
@Mock private PayLegacyAccountImportMapper accountImportMapper;
|
||||
@Mock private PayOrderMapper orderMapper;
|
||||
@Mock private PayOrderExtensionMapper extensionMapper;
|
||||
@Mock private PayRefundMapper refundMapper;
|
||||
@Mock private PayAppService appService;
|
||||
@Mock private PayChannelService channelService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
PayLegacyAccountImportDO account = PayLegacyAccountImportDO.builder().id(9L)
|
||||
.sourceTenantId("11111111-1111-4111-8111-111111111111")
|
||||
.normalizedProvider("wechat_pay").targetAppId(100L).targetChannelId(200L)
|
||||
.targetChannelCode("wx_lite").build();
|
||||
lenient().when(accountImportMapper.selectById(9L)).thenReturn(account);
|
||||
lenient().when(appService.getApp(100L)).thenReturn(PayAppDO.builder().id(100L)
|
||||
.orderNotifyUrl("https://new.example/order").refundNotifyUrl("https://new.example/refund").build());
|
||||
lenient().when(channelService.getChannel(200L)).thenReturn(PayChannelDO.builder().id(200L)
|
||||
.appId(100L).code("wx_lite").build());
|
||||
AtomicLong ids = new AtomicLong(1000);
|
||||
lenient().doAnswer(invocation -> { ((PayOrderDO) invocation.getArgument(0)).setId(ids.incrementAndGet()); return 1; })
|
||||
.when(orderMapper).insert(any(PayOrderDO.class));
|
||||
lenient().doAnswer(invocation -> { ((PayOrderExtensionDO) invocation.getArgument(0)).setId(ids.incrementAndGet()); return 1; })
|
||||
.when(extensionMapper).insert(any(PayOrderExtensionDO.class));
|
||||
lenient().doAnswer(invocation -> { ((PayRefundDO) invocation.getArgument(0)).setId(ids.incrementAndGet()); return 1; })
|
||||
.when(refundMapper).insert(any(PayRefundDO.class));
|
||||
lenient().doAnswer(invocation -> { ((PayLegacyTransactionImportDO) invocation.getArgument(0)).setId(ids.incrementAndGet()); return 1; })
|
||||
.when(importMapper).insert(any(PayLegacyTransactionImportDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importsReconciledTerminalAggregateWithoutRawProviderPayload() {
|
||||
PayLegacyTransactionImportReqVO req = refundedRequest();
|
||||
|
||||
PayLegacyTransactionImportRespVO result = service.importTransaction(req, 7L);
|
||||
|
||||
ArgumentCaptor<PayOrderDO> order = ArgumentCaptor.forClass(PayOrderDO.class);
|
||||
verify(orderMapper).insert(order.capture());
|
||||
assertThat(order.getValue().getStatus()).isEqualTo(PayOrderStatusEnum.REFUND.getStatus());
|
||||
assertThat(order.getValue().getRefundPrice()).isEqualTo(1000);
|
||||
assertThat(order.getValue().getUserIp()).isEqualTo("0.0.0.0");
|
||||
assertThat(order.getValue().getChannelFeePrice()).isZero();
|
||||
|
||||
ArgumentCaptor<PayOrderExtensionDO> extension = ArgumentCaptor.forClass(PayOrderExtensionDO.class);
|
||||
verify(extensionMapper).insert(extension.capture());
|
||||
assertThat(extension.getValue().getNo()).isEqualTo("OLD-OUT-1");
|
||||
assertThat(extension.getValue().getChannelNotifyData()).isNull();
|
||||
ArgumentCaptor<PayRefundDO> refund = ArgumentCaptor.forClass(PayRefundDO.class);
|
||||
verify(refundMapper).insert(refund.capture());
|
||||
assertThat(refund.getValue().getChannelNotifyData()).isNull();
|
||||
|
||||
ArgumentCaptor<PayLegacyTransactionImportDO> audit = ArgumentCaptor.forClass(PayLegacyTransactionImportDO.class);
|
||||
verify(importMapper).insert(audit.capture());
|
||||
assertThat(audit.getValue().getSourcePaymentEventCount()).isEqualTo(2);
|
||||
assertThat(audit.getValue().toString()).doesNotContain("provider raw", "event payload");
|
||||
assertThat(result.getTargetOrderId()).isNotNull();
|
||||
assertThat(result.getReplayed()).isFalse();
|
||||
verify(paymentImportMapper).insert(any(PayLegacyTransactionPaymentImportDO.class));
|
||||
verify(refundImportMapper).insert(any(PayLegacyTransactionRefundImportDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaysSameChecksumWithoutWritingNativeLedgers() {
|
||||
PayLegacyTransactionImportReqVO req = refundedRequest();
|
||||
PayLegacyTransactionImportDO existing = PayLegacyTransactionImportDO.builder().id(88L)
|
||||
.sourceOrderId(req.getSourceOrderId()).sourceChecksumSha256(req.getSourceChecksumSha256())
|
||||
.sourceOrderNo(req.getSourceOrderNo()).sourceOrderStatus(req.getSourceOrderStatus())
|
||||
.sourcePaymentCount(1).sourcePaymentEventCount(2).sourceRefundCount(1).sourceRefundedPrice(1000)
|
||||
.mappingNotes(List.of()).build();
|
||||
when(importMapper.selectBySourceOrderId(req.getSourceOrderId())).thenReturn(existing);
|
||||
|
||||
assertThat(service.importTransaction(req, 7L).getReplayed()).isTrue();
|
||||
verifyNoInteractions(orderMapper, extensionMapper, refundMapper, paymentImportMapper, refundImportMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsChecksumConflictBeforeLoadingAccount() {
|
||||
PayLegacyTransactionImportReqVO req = refundedRequest();
|
||||
when(importMapper.selectBySourceOrderId(req.getSourceOrderId())).thenReturn(
|
||||
PayLegacyTransactionImportDO.builder().sourceChecksumSha256("b".repeat(64)).build());
|
||||
assertServiceException(() -> service.importTransaction(req, 7L),
|
||||
LEGACY_TRANSACTION_IMPORT_CONFLICT, req.getSourceOrderId());
|
||||
verifyNoInteractions(accountImportMapper, orderMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsProviderOutsidePreviouslyAuditedAccountMapping() {
|
||||
PayLegacyTransactionImportReqVO req = refundedRequest();
|
||||
req.getPayments().getFirst().setSourceProvider("alipay");
|
||||
assertServiceException(() -> service.importTransaction(req, 7L),
|
||||
LEGACY_TRANSACTION_IMPORT_PROVIDER_MISMATCH, "alipay");
|
||||
verify(orderMapper, never()).insert(any(PayOrderDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOrderStatusAndRefundAmountMismatch() {
|
||||
PayLegacyTransactionImportReqVO req = refundedRequest();
|
||||
req.setSourceOrderStatus("partially_refunded");
|
||||
assertServiceException(() -> service.importTransaction(req, 7L),
|
||||
LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "订单状态与已退金额不一致");
|
||||
verify(orderMapper, never()).insert(any(PayOrderDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsEventCountWithoutDigest() {
|
||||
PayLegacyTransactionImportReqVO req = refundedRequest();
|
||||
req.getPayments().getFirst().setSourceEventDigest(null);
|
||||
assertServiceException(() -> service.importTransaction(req, 7L),
|
||||
LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "支付事件数量与摘要不一致");
|
||||
verify(orderMapper, never()).insert(any(PayOrderDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsRefundWithoutSuccessfulPaymentBeforeWritingNativeLedgers() {
|
||||
PayLegacyTransactionImportReqVO req = refundedRequest();
|
||||
req.setSourceOrderStatus("failed");
|
||||
req.setRefundedPrice(0);
|
||||
req.setSourcePaidAt(null);
|
||||
req.getPayments().getFirst().setSourceStatus("failed");
|
||||
req.getPayments().getFirst().setProviderTradeNo(null);
|
||||
req.getPayments().getFirst().setSourcePaidAt(null);
|
||||
req.getRefunds().getFirst().setSourceStatus("failed");
|
||||
req.getRefunds().getFirst().setSourceSucceededAt(null);
|
||||
|
||||
assertServiceException(() -> service.importTransaction(req, 7L),
|
||||
LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "退款记录必须关联已成功支付");
|
||||
verify(orderMapper, never()).insert(any(PayOrderDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNativeMerchantNumberCollisionBeforeWritingNativeLedgers() {
|
||||
PayLegacyTransactionImportReqVO req = refundedRequest();
|
||||
when(orderMapper.selectByAppIdAndMerchantOrderId(100L, req.getSourceOrderNo()))
|
||||
.thenReturn(new PayOrderDO().setId(42L));
|
||||
|
||||
assertServiceException(() -> service.importTransaction(req, 7L),
|
||||
LEGACY_TRANSACTION_IMPORT_RECONCILIATION, "目标应用已存在相同旧订单号");
|
||||
verify(orderMapper, never()).insert(any(PayOrderDO.class));
|
||||
verifyNoInteractions(extensionMapper, refundMapper);
|
||||
}
|
||||
|
||||
private PayLegacyTransactionImportReqVO refundedRequest() {
|
||||
LocalDateTime created = LocalDateTime.of(2026, 7, 1, 10, 0);
|
||||
LocalDateTime paid = created.plusMinutes(2);
|
||||
PayLegacyTransactionImportReqVO.Payment payment = new PayLegacyTransactionImportReqVO.Payment();
|
||||
payment.setSourcePaymentId("33333333-3333-4333-8333-333333333333");
|
||||
payment.setSourceProvider("wechat-pay"); payment.setSourceMethod("jsapi");
|
||||
payment.setSourceStatus("refunded"); payment.setAmount(1000);
|
||||
payment.setProviderOutTradeNo("OLD-OUT-1"); payment.setProviderTradeNo("WX-TRADE-1");
|
||||
payment.setSourceCreatedAt(created.plusMinutes(1)); payment.setSourcePaidAt(paid);
|
||||
payment.setSourceEventCount(2); payment.setSourceEventDigest("d".repeat(64));
|
||||
PayLegacyTransactionImportReqVO.Refund refund = new PayLegacyTransactionImportReqVO.Refund();
|
||||
refund.setSourceRefundId("44444444-4444-4444-8444-444444444444");
|
||||
refund.setSourceProvider("wechat_pay"); refund.setSourceStatus("succeeded");
|
||||
refund.setRefundNo("OLD-REFUND-1"); refund.setProviderRefundNo("WX-REFUND-1");
|
||||
refund.setAmount(1000); refund.setReason("requested");
|
||||
refund.setSourceCreatedAt(paid.plusDays(1)); refund.setSourceSucceededAt(paid.plusDays(1).plusMinutes(1));
|
||||
PayLegacyTransactionImportReqVO req = new PayLegacyTransactionImportReqVO();
|
||||
req.setSourceTenantId("11111111-1111-4111-8111-111111111111"); req.setSourceAccountImportId(9L);
|
||||
req.setSourceOrderId("22222222-2222-4222-8222-222222222222");
|
||||
req.setSourceChecksumSha256("a".repeat(64)); req.setSourceOrderNo("OLD-ORDER-1");
|
||||
req.setSourceOrderStatus("refunded"); req.setTargetUserId(77L); req.setSubject("Legacy course");
|
||||
req.setPrice(1000); req.setRefundedPrice(1000); req.setSourceCreatedAt(created); req.setSourcePaidAt(paid);
|
||||
req.setPayments(List.of(payment)); req.setRefunds(List.of(refund));
|
||||
return req;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
|
||||
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
|
||||
import cn.iocoder.yudao.module.pay.service.refund.PayRefundServiceImpl;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.redisson.api.RLock;
|
||||
@@ -47,7 +46,6 @@ import static org.mockito.Mockito.*;
|
||||
*
|
||||
* @author 管理员
|
||||
*/
|
||||
@Disabled // TODO 管理员:后续 fix 补充的单测
|
||||
@Import({PayJobConfiguration.class, PayNotifyServiceImpl.class, PayNotifyLockRedisDAO.class})
|
||||
public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
|
||||
@@ -94,7 +92,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
.containsExactly(type, dataId, PayNotifyStatusEnum.WAITING.getStatus(), 0, 9,
|
||||
order.getAppId(), order.getMerchantOrderId(), order.getNotifyUrl());
|
||||
// 断言,调用
|
||||
verify(payNotifyService).executeNotify0(eq(dbTask));
|
||||
verify(payNotifyService).executeNotifyAsync(any(PayNotifyTaskDO.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +123,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
.containsExactly(type, dataId, PayNotifyStatusEnum.WAITING.getStatus(), 0, 9,
|
||||
refund.getAppId(), refund.getMerchantOrderId(), refund.getNotifyUrl());
|
||||
// 断言,调用
|
||||
verify(payNotifyService).executeNotify0(eq(dbTask));
|
||||
verify(payNotifyService).executeNotifyAsync(any(PayNotifyTaskDO.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,14 +132,17 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
// mock 数据(notify)
|
||||
PayNotifyTaskDO dbTask01 = randomPojo(PayNotifyTaskDO.class,
|
||||
o -> o.setStatus(PayNotifyStatusEnum.WAITING.getStatus())
|
||||
.setType(-1).setNotifyTimes(0).setMaxNotifyTimes(9)
|
||||
.setNextNotifyTime(addTime(Duration.ofMinutes(-1))));
|
||||
notifyTaskMapper.insert(dbTask01);
|
||||
PayNotifyTaskDO dbTask02 = randomPojo(PayNotifyTaskDO.class,
|
||||
o -> o.setStatus(PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus())
|
||||
.setType(-1).setNotifyTimes(0).setMaxNotifyTimes(9)
|
||||
.setNextNotifyTime(addTime(Duration.ofMinutes(-1))));
|
||||
notifyTaskMapper.insert(dbTask02);
|
||||
PayNotifyTaskDO dbTask03 = randomPojo(PayNotifyTaskDO.class,
|
||||
o -> o.setStatus(PayNotifyStatusEnum.REQUEST_FAILURE.getStatus())
|
||||
.setType(-1).setNotifyTimes(0).setMaxNotifyTimes(9)
|
||||
.setNextNotifyTime(addTime(Duration.ofMinutes(-1))));
|
||||
notifyTaskMapper.insert(dbTask03);
|
||||
PayNotifyTaskDO dbTask04 = randomPojo(PayNotifyTaskDO.class, // 不满足状态
|
||||
@@ -171,7 +172,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
public void testExecuteNotify0_exception() {
|
||||
// mock 数据(task)
|
||||
PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, o -> o.setType(-1)
|
||||
.setNotifyTimes(0).setMaxNotifyTimes(9));
|
||||
.setNotifyTimes(0).setMaxNotifyTimes(9).setLastExecuteTime(null));
|
||||
notifyTaskMapper.insert(task);
|
||||
|
||||
// 调用
|
||||
@@ -179,7 +180,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
// 断言,task
|
||||
PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId());
|
||||
assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime());
|
||||
assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime());
|
||||
assertNotNull(dbTask.getLastExecuteTime());
|
||||
assertEquals(dbTask.getNotifyTimes(), 1);
|
||||
assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus());
|
||||
// 断言,log
|
||||
@@ -194,7 +195,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
public void testProcessNotifyResult_success() {
|
||||
// mock 数据(task)
|
||||
PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class,
|
||||
o -> o.setNotifyTimes(0).setMaxNotifyTimes(9));
|
||||
o -> o.setNotifyTimes(0).setMaxNotifyTimes(9).setLastExecuteTime(null));
|
||||
notifyTaskMapper.insert(task);
|
||||
// 准备参数
|
||||
CommonResult<?> invokeResult = CommonResult.success(randomString());
|
||||
@@ -204,7 +205,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
// 断言
|
||||
PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId());
|
||||
assertEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime());
|
||||
assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime());
|
||||
assertNotNull(dbTask.getLastExecuteTime());
|
||||
assertEquals(dbTask.getNotifyTimes(), 1);
|
||||
assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.SUCCESS.getStatus());
|
||||
}
|
||||
@@ -213,7 +214,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
public void testProcessNotifyResult_failure() {
|
||||
// mock 数据(task)
|
||||
PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class,
|
||||
o -> o.setNotifyTimes(8).setMaxNotifyTimes(9));
|
||||
o -> o.setNotifyTimes(8).setMaxNotifyTimes(9).setLastExecuteTime(null));
|
||||
notifyTaskMapper.insert(task);
|
||||
// 准备参数
|
||||
CommonResult<?> invokeResult = CommonResult.error(BAD_REQUEST);
|
||||
@@ -223,7 +224,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
// 断言
|
||||
PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId());
|
||||
assertEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime());
|
||||
assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime());
|
||||
assertNotNull(dbTask.getLastExecuteTime());
|
||||
assertEquals(dbTask.getNotifyTimes(), 9);
|
||||
assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.FAILURE.getStatus());
|
||||
}
|
||||
@@ -232,7 +233,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
public void testProcessNotifyResult_requestFailure() {
|
||||
// mock 数据(task)
|
||||
PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class,
|
||||
o -> o.setNotifyTimes(0).setMaxNotifyTimes(9));
|
||||
o -> o.setNotifyTimes(0).setMaxNotifyTimes(9).setLastExecuteTime(null));
|
||||
notifyTaskMapper.insert(task);
|
||||
// 准备参数
|
||||
CommonResult<?> invokeResult = CommonResult.error(BAD_REQUEST);
|
||||
@@ -242,7 +243,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
// 断言
|
||||
PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId());
|
||||
assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime());
|
||||
assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime());
|
||||
assertNotNull(dbTask.getLastExecuteTime());
|
||||
assertEquals(dbTask.getNotifyTimes(), 1);
|
||||
assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus());
|
||||
}
|
||||
@@ -251,7 +252,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
public void testProcessNotifyResult_requestSuccess() {
|
||||
// mock 数据(task)
|
||||
PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class,
|
||||
o -> o.setNotifyTimes(0).setMaxNotifyTimes(9));
|
||||
o -> o.setNotifyTimes(0).setMaxNotifyTimes(9).setLastExecuteTime(null));
|
||||
notifyTaskMapper.insert(task);
|
||||
// 准备参数
|
||||
CommonResult<?> invokeResult = CommonResult.error(BAD_REQUEST);
|
||||
@@ -262,7 +263,7 @@ public class PayNotifyServiceTest extends BaseDbUnitTest {
|
||||
// 断言
|
||||
PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId());
|
||||
assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime());
|
||||
assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime());
|
||||
assertNotNull(dbTask.getLastExecuteTime());
|
||||
assertEquals(dbTask.getNotifyTimes(), 1);
|
||||
assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package cn.iocoder.yudao.module.pay.service.wallet;
|
||||
|
||||
import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
|
||||
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletRechargeDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletRechargeMapper;
|
||||
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PayWalletRechargeServiceImplTest {
|
||||
|
||||
@Test
|
||||
void successfulRefundUsesRechargeWalletIdWithoutReloadingWallet() {
|
||||
PayWalletRechargeMapper rechargeMapper = mock(PayWalletRechargeMapper.class);
|
||||
PayWalletService walletService = mock(PayWalletService.class);
|
||||
PayRefundApi refundApi = mock(PayRefundApi.class);
|
||||
PayWalletRechargeServiceImpl service = new PayWalletRechargeServiceImpl();
|
||||
ReflectionTestUtils.setField(service, "walletRechargeMapper", rechargeMapper);
|
||||
ReflectionTestUtils.setField(service, "payWalletService", walletService);
|
||||
ReflectionTestUtils.setField(service, "payRefundApi", refundApi);
|
||||
PayWalletRechargeDO recharge = new PayWalletRechargeDO();
|
||||
recharge.setId(10L);
|
||||
recharge.setWalletId(20L);
|
||||
recharge.setTotalPrice(100);
|
||||
recharge.setPayPrice(80);
|
||||
recharge.setBonusPrice(20);
|
||||
recharge.setPayRefundId(30L);
|
||||
when(rechargeMapper.selectById(10L)).thenReturn(recharge);
|
||||
PayRefundRespDTO refund = new PayRefundRespDTO();
|
||||
refund.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
|
||||
refund.setRefundPrice(80);
|
||||
refund.setMerchantRefundId("10-refund");
|
||||
refund.setSuccessTime(LocalDateTime.of(2026, 8, 1, 6, 0));
|
||||
when(refundApi.getRefund(30L)).thenReturn(refund);
|
||||
|
||||
service.updateWalletRechargeRefunded(10L, "10-refund", 30L);
|
||||
|
||||
verify(walletService).reduceWalletBalance(
|
||||
20L, 10L, PayWalletBizTypeEnum.RECHARGE_REFUND, 100);
|
||||
verify(rechargeMapper).updateByIdAndRefunded(
|
||||
org.mockito.ArgumentMatchers.eq(10L),
|
||||
org.mockito.ArgumentMatchers.eq(PayRefundStatusEnum.WAITING.getStatus()),
|
||||
any(PayWalletRechargeDO.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cn.iocoder.yudao.module.pay.service.wallet;
|
||||
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletMapper;
|
||||
import cn.iocoder.yudao.module.pay.dal.redis.wallet.PayWalletLockRedisDAO;
|
||||
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PayWalletServiceImplTest {
|
||||
|
||||
private final PayWalletMapper walletMapper = mock(PayWalletMapper.class);
|
||||
private final PayWalletLockRedisDAO lockRedisDAO = mock(PayWalletLockRedisDAO.class);
|
||||
private final PayWalletTransactionService transactionService = mock(PayWalletTransactionService.class);
|
||||
private final PayWalletServiceImpl service = new PayWalletServiceImpl();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
ReflectionTestUtils.setField(service, "walletMapper", walletMapper);
|
||||
ReflectionTestUtils.setField(service, "lockRedisDAO", lockRedisDAO);
|
||||
ReflectionTestUtils.setField(service, "walletTransactionService", transactionService);
|
||||
when(lockRedisDAO.lock(anyLong(), anyLong(), any())).thenAnswer(invocation -> {
|
||||
Callable<?> callable = invocation.getArgument(2);
|
||||
return callable.call();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void administrativeReductionDoesNotIncreaseExpenseTotal() {
|
||||
PayWalletDO wallet = new PayWalletDO();
|
||||
wallet.setId(9L);
|
||||
wallet.setBalance(500);
|
||||
when(walletMapper.selectById(9L)).thenReturn(wallet);
|
||||
when(walletMapper.updateWhenSubtract(9L, 120)).thenReturn(1);
|
||||
when(transactionService.createWalletTransaction(any(WalletTransactionCreateReqBO.class)))
|
||||
.thenReturn(new PayWalletTransactionDO());
|
||||
|
||||
service.reduceWalletBalance(9L, 7L, PayWalletBizTypeEnum.UPDATE_BALANCE, 120);
|
||||
|
||||
verify(walletMapper).updateWhenSubtract(9L, 120);
|
||||
verify(transactionService).createWalletTransaction(any(WalletTransactionCreateReqBO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAndReduceRejectNonPositiveAmounts() {
|
||||
assertThatThrownBy(() -> service.addWalletBalance(
|
||||
9L, "7", PayWalletBizTypeEnum.UPDATE_BALANCE, 0))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> service.reduceWalletBalance(
|
||||
9L, 7L, PayWalletBizTypeEnum.UPDATE_BALANCE, -1))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,5 +4,9 @@ DELETE FROM pay_order;
|
||||
DELETE FROM pay_order_extension;
|
||||
DELETE FROM pay_refund;
|
||||
DELETE FROM pay_transfer;
|
||||
DELETE FROM pay_wallet_recharge;
|
||||
DELETE FROM pay_wallet_transaction;
|
||||
DELETE FROM pay_wallet_recharge_package;
|
||||
DELETE FROM pay_wallet;
|
||||
DELETE FROM pay_notify_task;
|
||||
DELETE FROM pay_notify_log;
|
||||
|
||||
@@ -6,11 +6,13 @@ CREATE TABLE IF NOT EXISTS "pay_app" (
|
||||
"remark" varchar(255) DEFAULT NULL,
|
||||
`order_notify_url` varchar(1024) NOT NULL,
|
||||
`refund_notify_url` varchar(1024) NOT NULL,
|
||||
`transfer_notify_url` varchar(1024) DEFAULT NULL,
|
||||
"creator" varchar(64) DEFAULT '',
|
||||
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updater" varchar(64) DEFAULT '',
|
||||
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
"deleted" bit(1) NOT NULL DEFAULT FALSE,
|
||||
"tenant_id" bigint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT = '支付应用';
|
||||
|
||||
@@ -31,8 +33,36 @@ CREATE TABLE IF NOT EXISTS "pay_channel" (
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT = '支付渠道';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "pay_legacy_account_import" (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"tenant_id" bigint NOT NULL DEFAULT 0,
|
||||
"source_tenant_id" varchar(36) NOT NULL,
|
||||
"source_account_id" varchar(36) NOT NULL,
|
||||
"source_checksum_sha256" char(64) NOT NULL,
|
||||
"source_provider" varchar(32) NOT NULL,
|
||||
"normalized_provider" varchar(16) NOT NULL,
|
||||
"source_mode" varchar(32) NOT NULL,
|
||||
"source_status" varchar(16) NOT NULL,
|
||||
"target_app_id" bigint NOT NULL,
|
||||
"target_channel_id" bigint NOT NULL,
|
||||
"target_channel_code" varchar(32) NOT NULL,
|
||||
"target_status" tinyint NOT NULL,
|
||||
"normalized_config_digest" char(64) NOT NULL,
|
||||
"mapping_notes" varchar(2048) NOT NULL,
|
||||
"imported_by" bigint NOT NULL,
|
||||
"imported_at" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"creator" varchar(64) DEFAULT '',
|
||||
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updater" varchar(64) DEFAULT '',
|
||||
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
"deleted" bit(1) NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY ("id"),
|
||||
UNIQUE ("tenant_id", "source_account_id")
|
||||
) COMMENT = '旧支付账号导入审计';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_order` (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT 0,
|
||||
`app_id` bigint(20) NOT NULL,
|
||||
`channel_id` bigint(20) DEFAULT NULL,
|
||||
`channel_code` varchar(32) DEFAULT NULL,
|
||||
@@ -65,6 +95,7 @@ CREATE TABLE IF NOT EXISTS `pay_order` (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_order_extension` (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT 0,
|
||||
`no` varchar(64) NOT NULL,
|
||||
`order_id` bigint(20) NOT NULL,
|
||||
`channel_id` bigint(20) NOT NULL,
|
||||
@@ -85,6 +116,7 @@ CREATE TABLE IF NOT EXISTS `pay_order_extension` (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_refund` (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT 0,
|
||||
`no` varchar(64) NOT NULL,
|
||||
`app_id` bigint(20) NOT NULL,
|
||||
`channel_id` bigint(20) NOT NULL,
|
||||
@@ -120,7 +152,9 @@ CREATE TABLE IF NOT EXISTS `pay_notify_task` (
|
||||
`app_id` bigint(20) NOT NULL,
|
||||
`type` tinyint(4) NOT NULL,
|
||||
`data_id` bigint(20) NOT NULL,
|
||||
`merchant_order_id` varchar(64) NOT NULL,
|
||||
`merchant_order_id` varchar(64) NULL,
|
||||
`merchant_refund_id` varchar(64) NULL,
|
||||
`merchant_transfer_id` varchar(64) NULL,
|
||||
`status` tinyint(4) NOT NULL,
|
||||
`next_notify_time` datetime(0) NULL DEFAULT NULL,
|
||||
`last_execute_time` datetime(0) NULL DEFAULT NULL,
|
||||
@@ -138,6 +172,7 @@ CREATE TABLE IF NOT EXISTS `pay_notify_task` (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_notify_log` (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT 0,
|
||||
`task_id` bigint(20) NOT NULL,
|
||||
`notify_times` int NOT NULL,
|
||||
`response` varchar(1024) NOT NULL,
|
||||
@@ -152,6 +187,7 @@ CREATE TABLE IF NOT EXISTS `pay_notify_log` (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_transfer` (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT 0,
|
||||
`no` varchar(64) NOT NULL,
|
||||
`app_id` bigint(20) NOT NULL,
|
||||
`channel_id` bigint(20) NOT NULL,
|
||||
@@ -180,3 +216,79 @@ CREATE TABLE IF NOT EXISTS `pay_transfer` (
|
||||
`deleted` bit(1) NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT = '转账单';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_wallet` (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT 0,
|
||||
`user_id` bigint(20) NOT NULL,
|
||||
`user_type` tinyint(4) NOT NULL,
|
||||
`balance` int NOT NULL DEFAULT 0,
|
||||
`freeze_price` int NOT NULL DEFAULT 0,
|
||||
`total_expense` int NOT NULL DEFAULT 0,
|
||||
`total_recharge` int NOT NULL DEFAULT 0,
|
||||
`creator` varchar(64) NULL DEFAULT '',
|
||||
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updater` varchar(64) NULL DEFAULT '',
|
||||
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`deleted` bit(1) NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT = '支付钱包';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_wallet_transaction` (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT 0,
|
||||
`no` varchar(64) NOT NULL,
|
||||
`wallet_id` bigint(20) NOT NULL,
|
||||
`biz_type` tinyint(4) NOT NULL,
|
||||
`biz_id` varchar(64) NOT NULL,
|
||||
`title` varchar(128) NOT NULL,
|
||||
`price` int NOT NULL,
|
||||
`balance` int NOT NULL,
|
||||
`creator` varchar(64) NULL DEFAULT '',
|
||||
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updater` varchar(64) NULL DEFAULT '',
|
||||
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`deleted` bit(1) NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT = '支付钱包流水';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_wallet_recharge_package` (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT 0,
|
||||
`name` varchar(64) NOT NULL,
|
||||
`pay_price` int NOT NULL,
|
||||
`bonus_price` int NOT NULL DEFAULT 0,
|
||||
`status` tinyint(4) NOT NULL,
|
||||
`creator` varchar(64) NULL DEFAULT '',
|
||||
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updater` varchar(64) NULL DEFAULT '',
|
||||
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`deleted` bit(1) NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT = '支付钱包充值套餐';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pay_wallet_recharge` (
|
||||
"id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
`tenant_id` bigint(20) NOT NULL DEFAULT 0,
|
||||
`wallet_id` bigint(20) NOT NULL,
|
||||
`total_price` int NOT NULL,
|
||||
`pay_price` int NOT NULL,
|
||||
`bonus_price` int NOT NULL DEFAULT 0,
|
||||
`package_id` bigint(20) NULL,
|
||||
`pay_status` bit(1) NOT NULL DEFAULT FALSE,
|
||||
`pay_order_id` bigint(20) NULL,
|
||||
`pay_channel_code` varchar(32) NULL,
|
||||
`pay_time` datetime(0) NULL,
|
||||
`pay_refund_id` bigint(20) NULL,
|
||||
`refund_total_price` int NULL,
|
||||
`refund_pay_price` int NULL,
|
||||
`refund_bonus_price` int NULL,
|
||||
`refund_time` datetime(0) NULL,
|
||||
`refund_status` tinyint(4) NULL,
|
||||
`creator` varchar(64) NULL DEFAULT '',
|
||||
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updater` varchar(64) NULL DEFAULT '',
|
||||
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`deleted` bit(1) NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT = '支付钱包充值';
|
||||
|
||||
Reference in New Issue
Block a user