diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java index 88bc638c..589a9cb4 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java @@ -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 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 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(); diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index f8ea4ef9..0f8ebf35 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -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 requestParams = MapUtil.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)); diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletAddBalanceReqDTO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletAddBalanceReqDTO.java index 300500ba..7ac9fba6 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletAddBalanceReqDTO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/wallet/dto/PayWalletAddBalanceReqDTO.java @@ -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; } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/PayAppController.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/PayAppController.java index a3e6d282..eca2f9c8 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/PayAppController.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/PayAppController.java @@ -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> getAppList() { List appListDO = appService.getAppList(); return success(PayAppConvert.INSTANCE.convertList(appListDO)); diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyAccountImportController.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyAccountImportController.java new file mode 100644 index 00000000..c1ec07f0 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyAccountImportController.java @@ -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 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> getImportPage( + @Valid PayLegacyAccountImportPageReqVO reqVO) { + return success(importService.getImportPage(reqVO)); + } + +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyTransactionImportController.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyTransactionImportController.java new file mode 100644 index 00000000..6832c626 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyTransactionImportController.java @@ -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 importTransaction( + @Valid @RequestBody PayLegacyTransactionImportReqVO reqVO) { + return success(importService.importTransaction(reqVO, getLoginUserId())); + } + + @GetMapping("/page") + @Operation(summary = "获得旧支付交易导入审计分页") + @PreAuthorize("@ss.hasPermission('pay:legacy-transaction:query')") + public CommonResult> getImportPage( + @Valid PayLegacyTransactionImportPageReqVO reqVO) { + return success(importService.getImportPage(reqVO)); + } +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyAccountImportPageReqVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyAccountImportPageReqVO.java new file mode 100644 index 00000000..c3992b2d --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyAccountImportPageReqVO.java @@ -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; + +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyAccountImportReqVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyAccountImportReqVO.java new file mode 100644 index 00000000..b306bfe5 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyAccountImportReqVO.java @@ -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 configPublic; + + @Schema(description = "旧 app_private.tenant_secrets.secret_json;只写入原生 Pay 渠道配置,不写入导入审计", + requiredMode = Schema.RequiredMode.REQUIRED, writeOnly = true) + @NotNull(message = "旧私密配置不能为空") + private Map secretJson; + +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyAccountImportRespVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyAccountImportRespVO.java new file mode 100644 index 00000000..26408b64 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyAccountImportRespVO.java @@ -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 mappingNotes; + private Long importedBy; + private LocalDateTime importedAt; + private Boolean replayed; + +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyTransactionImportPageReqVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyTransactionImportPageReqVO.java new file mode 100644 index 00000000..f1b26c73 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyTransactionImportPageReqVO.java @@ -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; +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyTransactionImportReqVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyTransactionImportReqVO.java new file mode 100644 index 00000000..7d1cccd4 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyTransactionImportReqVO.java @@ -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 payments; + @NotNull @Size(max = 50) @Valid + private List 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; + } +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyTransactionImportRespVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyTransactionImportRespVO.java new file mode 100644 index 00000000..ed03bc63 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/vo/PayLegacyTransactionImportRespVO.java @@ -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 mappingNotes; + private Long importedBy; + private LocalDateTime importedAt; + private Boolean replayed; +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java index 54fa0041..503e3ea8 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletController.java @@ -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); } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java index 26263238..9b73ca13 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java @@ -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 refundWalletRecharge(@RequestParam("id") Long id) { walletRechargeService.refundWalletRecharge(id, getClientIP()); return success(true); diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java index ea6f0c46..f717cc9a 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java @@ -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; } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java index 7569bca7..83568f2a 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java @@ -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; + } + } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java index 48a83ec8..39adeea9 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java @@ -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 = "充值金额和充钱套餐不能同时为空") diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java index 11e2186a..e7d83380 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java @@ -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 { /** * 应用编号,数据库自增 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyAccountImportDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyAccountImportDO.java new file mode 100644 index 00000000..ce571e9d --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyAccountImportDO.java @@ -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 mappingNotes; + + private Long importedBy; + private LocalDateTime importedAt; + +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyTransactionImportDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyTransactionImportDO.java new file mode 100644 index 00000000..546656da --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyTransactionImportDO.java @@ -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 mappingNotes; + private Long importedBy; + private LocalDateTime importedAt; +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyTransactionPaymentImportDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyTransactionPaymentImportDO.java new file mode 100644 index 00000000..f014e91e --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyTransactionPaymentImportDO.java @@ -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; +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyTransactionRefundImportDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyTransactionRefundImportDO.java new file mode 100644 index 00000000..cc06221e --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/legacy/PayLegacyTransactionRefundImportDO.java @@ -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; +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java index b6dc500c..587657f3 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java @@ -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 { /** * 日志编号,自增 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java index 50b141a5..550a2565 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java @@ -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 { /** * 订单编号,数据库自增 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java index c2c7e6e2..81bb1895 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java @@ -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 { /** * 订单拓展编号,数据库自增 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java index d559d94f..37ec4ad9 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java @@ -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 { /** * 退款单编号,数据库自增 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java index 821210d3..a07dd6e5 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java @@ -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; -} \ No newline at end of file +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletDO.java index a3c54c96..9f8dfc9a 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletDO.java @@ -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 { /** * 编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java index a842c95e..cf3a1c7d 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargeDO.java @@ -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 { /** * 编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java index fd328fe3..c3238e25 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java @@ -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 { /** * 编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletTransactionDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletTransactionDO.java index 654e5115..b3b1c3bb 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletTransactionDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/wallet/PayWalletTransactionDO.java @@ -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 { /** * 编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyAccountImportMapper.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyAccountImportMapper.java new file mode 100644 index 00000000..3b90741a --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyAccountImportMapper.java @@ -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 { + + default PayLegacyAccountImportDO selectBySourceAccountId(String sourceAccountId) { + return selectOne(PayLegacyAccountImportDO::getSourceAccountId, sourceAccountId); + } + + default PageResult selectPage(PayLegacyAccountImportPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayLegacyAccountImportDO::getSourceTenantId, reqVO.getSourceTenantId()) + .eqIfPresent(PayLegacyAccountImportDO::getNormalizedProvider, reqVO.getNormalizedProvider()) + .orderByDesc(PayLegacyAccountImportDO::getId)); + } + +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyTransactionImportMapper.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyTransactionImportMapper.java new file mode 100644 index 00000000..6c52e78a --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyTransactionImportMapper.java @@ -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 { + default PayLegacyTransactionImportDO selectBySourceOrderId(String sourceOrderId) { + return selectOne(PayLegacyTransactionImportDO::getSourceOrderId, sourceOrderId); + } + + default PageResult selectPage(PayLegacyTransactionImportPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayLegacyTransactionImportDO::getSourceTenantId, reqVO.getSourceTenantId()) + .likeIfPresent(PayLegacyTransactionImportDO::getSourceOrderNo, reqVO.getSourceOrderNo()) + .eqIfPresent(PayLegacyTransactionImportDO::getSourceOrderStatus, reqVO.getSourceOrderStatus()) + .orderByDesc(PayLegacyTransactionImportDO::getId)); + } +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyTransactionPaymentImportMapper.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyTransactionPaymentImportMapper.java new file mode 100644 index 00000000..7a9c283a --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyTransactionPaymentImportMapper.java @@ -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 { +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyTransactionRefundImportMapper.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyTransactionRefundImportMapper.java new file mode 100644 index 00000000..595a59c3 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/legacy/PayLegacyTransactionRefundImportMapper.java @@ -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 { +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java index 19a76cd6..29cbecb4 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/wallet/PayWalletMapper.java @@ -81,6 +81,16 @@ public interface PayWalletMapper extends BaseMapperX { update(null, lambdaUpdateWrapper); } + /** + * 管理员调减余额,不计入会员消费累计。 + */ + default int updateWhenSubtract(Long id, Integer price) { + return update(null, new LambdaUpdateWrapper() + .setSql(" balance = balance - " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getBalance, price)); + } + /** * 冻结钱包部分余额 * @@ -131,4 +141,3 @@ public interface PayWalletMapper extends BaseMapperX { - diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java index 6d8f48c0..4340c877 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java @@ -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"; /** * 支付序号的缓存 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java index 94547e07..3474be32 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java @@ -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); } } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java index 660cc232..2b56577f 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java @@ -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, "支付订单不处于待支付"); diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java index 8021dc76..31fce920 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClient.java @@ -228,7 +228,7 @@ public class WalletPayClient extends AbstractPayClient { // 成功状态 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); diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java index 234fcafa..57ed0a28 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java @@ -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); diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyAccountImportService.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyAccountImportService.java new file mode 100644 index 00000000..6c8c67f8 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyAccountImportService.java @@ -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 getImportPage(PayLegacyAccountImportPageReqVO reqVO); + +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyAccountImportServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyAccountImportServiceImpl.java new file mode 100644 index 00000000..a8747350 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyAccountImportServiceImpl.java @@ -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 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 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 getImportPage(PayLegacyAccountImportPageReqVO reqVO) { + PageResult 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 notes) { + Map publicConfig = reqVO.getConfigPublic(); + Map 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 rotatingKeys = stringMap(publicConfig.get("wechatpayPublicKeys")); + if (publicKey.isEmpty() && rotatingKeys.size() == 1) { + Map.Entry 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 notes) { + Map publicConfig = reqVO.getConfigPublic(); + Map 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 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 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 stringMap(Object value) { + if (!(value instanceof Map map)) { + return Map.of(); + } + Map 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(); + } + +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyTransactionImportService.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyTransactionImportService.java new file mode 100644 index 00000000..3827ed46 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyTransactionImportService.java @@ -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 getImportPage(PayLegacyTransactionImportPageReqVO reqVO); +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyTransactionImportServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyTransactionImportServiceImpl.java new file mode 100644 index 00000000..729c5ec5 --- /dev/null +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyTransactionImportServiceImpl.java @@ -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 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 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 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 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 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 getImportPage(PayLegacyTransactionImportPageReqVO reqVO) { + PageResult 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) {} +} diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java index e189018d..892412c7 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java @@ -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); } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java index 445f06a4..718c8edc 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java @@ -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); diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyAccountImportControllerContractTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyAccountImportControllerContractTest.java new file mode 100644 index 00000000..6c46e7f9 --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyAccountImportControllerContractTest.java @@ -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(); + } + +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyTransactionImportControllerContractTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyTransactionImportControllerContractTest.java new file mode 100644 index 00000000..51f66109 --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/controller/admin/legacy/PayLegacyTransactionImportControllerContractTest.java @@ -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(); + } +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletControllerTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletControllerTest.java new file mode 100644 index 00000000..065918fd --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletControllerTest.java @@ -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); + } + +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/dataobject/PayTransactionTenantContractTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/dataobject/PayTransactionTenantContractTest.java new file mode 100644 index 00000000..2e884844 --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/dataobject/PayTransactionTenantContractTest.java @@ -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); + } + +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/dataobject/PayTransferWalletTenantContractTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/dataobject/PayTransferWalletTenantContractTest.java new file mode 100644 index 00000000..2694d8b3 --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/dataobject/PayTransferWalletTenantContractTest.java @@ -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')"); + } + +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppTenantContractTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppTenantContractTest.java new file mode 100644 index 00000000..a7a39ba5 --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppTenantContractTest.java @@ -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')"); + } +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAOTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAOTest.java new file mode 100644 index 00000000..2f8f2547 --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAOTest.java @@ -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(); + } + +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClientTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClientTest.java new file mode 100644 index 00000000..33d861cb --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/wallet/WalletPayClientTest.java @@ -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); + } + +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java index eb8f6947..31cf9675 100644 --- a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java @@ -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() { diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyAccountImportServiceImplTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyAccountImportServiceImplTest.java new file mode 100644 index 00000000..e8ff586a --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyAccountImportServiceImplTest.java @@ -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 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 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 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 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 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(); + } + +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyTransactionImportServiceImplTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyTransactionImportServiceImplTest.java new file mode 100644 index 00000000..1202f128 --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/legacy/PayLegacyTransactionImportServiceImplTest.java @@ -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 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 extension = ArgumentCaptor.forClass(PayOrderExtensionDO.class); + verify(extensionMapper).insert(extension.capture()); + assertThat(extension.getValue().getNo()).isEqualTo("OLD-OUT-1"); + assertThat(extension.getValue().getChannelNotifyData()).isNull(); + ArgumentCaptor refund = ArgumentCaptor.forClass(PayRefundDO.class); + verify(refundMapper).insert(refund.capture()); + assertThat(refund.getValue().getChannelNotifyData()).isNull(); + + ArgumentCaptor 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; + } +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceTest.java index 6be7a547..d71b8293 100644 --- a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceTest.java +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceTest.java @@ -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()); } diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImplTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImplTest.java new file mode 100644 index 00000000..e140f4b2 --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImplTest.java @@ -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)); + } + +} diff --git a/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImplTest.java b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImplTest.java new file mode 100644 index 00000000..e9164a92 --- /dev/null +++ b/yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImplTest.java @@ -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); + } + +} diff --git a/yudao-module-pay/src/test/resources/sql/clean.sql b/yudao-module-pay/src/test/resources/sql/clean.sql index e7bf56bf..6dfae837 100644 --- a/yudao-module-pay/src/test/resources/sql/clean.sql +++ b/yudao-module-pay/src/test/resources/sql/clean.sql @@ -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; diff --git a/yudao-module-pay/src/test/resources/sql/create_tables.sql b/yudao-module-pay/src/test/resources/sql/create_tables.sql index a3df280b..2b70013c 100644 --- a/yudao-module-pay/src/test/resources/sql/create_tables.sql +++ b/yudao-module-pay/src/test/resources/sql/create_tables.sql @@ -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 = '支付钱包充值';