diff --git a/yudao-module-pay/pom.xml b/yudao-module-pay/pom.xml new file mode 100644 index 000000000..45e8b639a --- /dev/null +++ b/yudao-module-pay/pom.xml @@ -0,0 +1,25 @@ + + + + cn.iocoder.cloud + yudao + ${revision} + + 4.0.0 + yudao-module-pay + pom + + yudao-module-pay-api + yudao-module-pay-biz + + + ${project.artifactId} + + pay 模块,我们放支付业务,提供业务的支付能力。 + 例如说:商户、应用、支付、退款等等 + + + + diff --git a/yudao-module-pay/yudao-module-pay-api/pom.xml b/yudao-module-pay/yudao-module-pay-api/pom.xml new file mode 100644 index 000000000..21d9bf351 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/pom.xml @@ -0,0 +1,47 @@ + + + + yudao-module-pay + cn.iocoder.cloud + ${revision} + + 4.0.0 + yudao-module-pay-api + jar + + ${project.artifactId} + + pay 模块 API,暴露给其它模块调用 + + + + + cn.iocoder.cloud + yudao-common + + + + + io.swagger.core.v3 + swagger-annotations + provided + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + true + + + + diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayOrderNotifyReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayOrderNotifyReqDTO.java new file mode 100644 index 000000000..291b364d9 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayOrderNotifyReqDTO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.pay.api.notify.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "RPC 服务 - 支付单的通知 Request DTO") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderNotifyReqDTO { + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "M101") + @NotEmpty(message = "商户订单号不能为空") + private String merchantOrderId; + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "P202") + @NotNull(message = "支付订单编号不能为空") + private Long payOrderId; + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java new file mode 100644 index 000000000..d92ad994f --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.pay.api.notify.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "RPC 服务 - 退款单的通知 Request DTO") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayRefundNotifyReqDTO { + + @Schema(description = "商户退款单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "MR101") + @NotEmpty(message = "商户退款单编号不能为空") + private String merchantOrderId; + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "R303") + @NotNull(message = "支付退款编号不能为空") + private Long payRefundId; + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/package-info.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/package-info.java new file mode 100644 index 000000000..60c4ca883 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位符,无特殊作用 + */ +package cn.iocoder.yudao.module.pay.api.notify; diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java new file mode 100644 index 000000000..c75631fb6 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApi.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pay.api.order; + +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; +import cn.iocoder.yudao.module.pay.enums.ApiConstants; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import javax.validation.Valid; + +@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory = +@Tag(name = "RPC 服务 - 支付单") +public interface PayOrderApi { + + String PREFIX = ApiConstants.PREFIX + "/order"; + + @PostMapping(PREFIX + "/create") + @Operation(summary = "创建支付单") + Long createOrder(@Valid @RequestBody PayOrderCreateReqDTO reqDTO); + + @PostMapping(PREFIX + "/get") + @Operation(summary = "获得支付单") + @Parameter(name = "id", description = "支付单编号", example = "1", required = true) + PayOrderRespDTO getOrder(Long id); + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java new file mode 100644 index 000000000..656a2f1d8 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.pay.api.order.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.time.LocalDateTime; + +@Schema(description = "RPC 服务 - 支付单创建 Request DTO") +@Data +public class PayOrderCreateReqDTO implements Serializable { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long appId; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "M101") // 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一 + @NotEmpty(message = "商户订单编号不能为空") + private String merchantOrderId; + + @Schema(description = "商品标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是标题") + @NotEmpty(message = "商品标题不能为空") + @Length(max = 32, message = "商品标题不能超过 32") + private String subject; + + @Schema(description = "商品描述", example = "我是描述") + @Length(max = 128, message = "商品描述信息长度不能超过128") + private String body; + + // ========== 订单相关字段 ========== + + @Schema(description = "支付金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "80") + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer price; + + @Schema(description = "支付过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "支付过期时间不能为空") + private LocalDateTime expireTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderRespDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderRespDTO.java new file mode 100644 index 000000000..7bf9e6167 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderRespDTO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.pay.api.order.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "RPC 服务 - 支付单信息 Response DTO") +@Data +public class PayOrderRespDTO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_pub") // PayChannelEnum 枚举 + private String channelCode; + + // ========== 商户相关字段 ========== + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "M101") // 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一 + private String merchantOrderId; + + // ========== 订单相关字段 ========== + + @Schema(description = "支付金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "101") + private Integer price; + + @Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") // PayOrderStatusEnum 枚举 + private Integer status; + + // ========== 渠道相关字段 ========== + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java new file mode 100644 index 000000000..524895bab --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pay.api.refund; + +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO; +import cn.iocoder.yudao.module.pay.enums.ApiConstants; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import javax.validation.Valid; + +@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory = +@Tag(name = "RPC 服务 - 退款单") +public interface PayRefundApi { + + String PREFIX = ApiConstants.PREFIX + "/refund"; + + @PostMapping(PREFIX + "/create") + @Operation(summary = "创建退款单") + Long createRefund(@Valid @RequestBody PayRefundCreateReqDTO reqDTO); + + @PostMapping(PREFIX + "/get") + @Operation(summary = "获得退款单") + @Parameter(name = "id", description = "退款单编号", example = "1", required = true) + PayRefundRespDTO getRefund(Long id); + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java new file mode 100644 index 000000000..c03bc5368 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.pay.api.refund.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "RPC 服务 - 退款单创建 Request DTO") +@Data +public class PayRefundCreateReqDTO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long appId; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "M101") // 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一 + @NotEmpty(message = "商户订单编号不能为空") + private String merchantOrderId; + + @Schema(description = "商户退款编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "MR101") + @NotEmpty(message = "商户退款编号不能为空") + private String merchantRefundId; + + @Schema(description = "退款描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是退款描述") + @NotEmpty(message = "退款描述不能为空") + @Length(max = 128, message = "退款描述长度不能超过 128") + private String reason; + + // ========== 订单相关字段 ========== + + @Schema(description = "退款金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "60") + @NotNull(message = "退款金额不能为空") + @Min(value = 1, message = "退款金额必须大于零") + private Integer price; + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundRespDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundRespDTO.java new file mode 100644 index 000000000..a562f581e --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundRespDTO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.pay.api.refund.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "RPC 服务 - 退款单信息 Response DTO") +@Data +public class PayRefundRespDTO { + + @Schema(description = "退款单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + // ========== 退款相关字段 ========== + + @Schema(description = "退款状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") // PayRefundStatusEnum 枚举 + private Integer status; + + @Schema(description = "退款金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "101") + private Integer refundPrice; + + // ========== 商户相关字段 ========== + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "M101") // 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一 + private String merchantOrderId; + + @Schema(description = "退款成功时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime successTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ApiConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ApiConstants.java new file mode 100644 index 000000000..72b040248 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ApiConstants.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.pay.enums; + +import cn.iocoder.yudao.framework.common.enums.RpcConstants; + +/** + * API 相关的枚举 + * + * @author 芋道源码 + */ +public class ApiConstants { + + /** + * 服务名 + * + * 注意,需要保证和 spring.application.name 保持一致 + */ + public static final String NAME = "pay-server"; + + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/pay"; + + public static final String VERSION = "1.0.0"; + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/DictTypeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/DictTypeConstants.java new file mode 100644 index 000000000..8f0d9b718 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/DictTypeConstants.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.pay.enums; + +/** + * Pay 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String CHANNEL_CODE = "pay_channel_code"; // 支付渠道编码 + + String ORDER_STATUS = "pay_order_status"; // 支付渠道 + + String REFUND_STATUS = "pay_order_status"; // 退款状态 + + String NOTIFY_STATUS = "pay_notify_status"; // 回调状态 + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java new file mode 100644 index 000000000..8e2f935fc --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.pay.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * Pay 错误码 Core 枚举类 + * + * pay 系统,使用 1-007-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== APP 模块 1007000000 ========== + ErrorCode APP_NOT_FOUND = new ErrorCode(1007000000, "App 不存在"); + ErrorCode APP_IS_DISABLE = new ErrorCode(1007000002, "App 已经被禁用"); + ErrorCode APP_EXIST_ORDER_CANT_DELETE = new ErrorCode(1007000003, "支付应用存在支付订单,无法删除"); + ErrorCode APP_EXIST_REFUND_CANT_DELETE = new ErrorCode(1007000004, "支付应用存在退款订单,无法删除"); + + // ========== CHANNEL 模块 1007001000 ========== + ErrorCode CHANNEL_NOT_FOUND = new ErrorCode(1007001000, "支付渠道的配置不存在"); + ErrorCode CHANNEL_IS_DISABLE = new ErrorCode(1007001001, "支付渠道已经禁用"); + ErrorCode CHANNEL_EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1007001004, "已存在相同的渠道"); + + // ========== ORDER 模块 1007002000 ========== + ErrorCode ORDER_NOT_FOUND = new ErrorCode(1007002000, "支付订单不存在"); + ErrorCode ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付"); + ErrorCode ORDER_STATUS_IS_SUCCESS = new ErrorCode(1007002002, "订单已支付,请刷新页面"); + ErrorCode ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期"); + ErrorCode ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}"); + ErrorCode ORDER_REFUND_FAIL_STATUS_ERROR = new ErrorCode(1007002005, "支付订单退款失败,原因:状态不是已支付或已退款"); + + // ========== ORDER 模块(拓展单) 1007003000 ========== + ErrorCode ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在"); + ErrorCode ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付"); + ErrorCode ORDER_EXTENSION_IS_PAID = new ErrorCode(1007003002, "订单已支付,请等待支付结果"); + + // ========== 支付模块(退款) 1007006000 ========== + ErrorCode REFUND_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额"); + ErrorCode REFUND_HAS_REFUNDING = new ErrorCode(1007006002, "已经有退款在处理中"); + ErrorCode REFUND_EXISTS = new ErrorCode(1007006003, "已经存在退款单"); + ErrorCode REFUND_NOT_FOUND = new ErrorCode(1007006004, "支付退款单不存在"); + ErrorCode REFUND_STATUS_IS_NOT_WAITING = new ErrorCode(1007006005, "支付退款单不处于待退款"); + + // ========== 示例订单 1007900000 ========== + ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1007900000, "示例订单不存在"); + ErrorCode DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1007900001, "示例订单更新支付状态失败,订单不是【未支付】状态"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR = new ErrorCode(1007900002, "示例订单更新支付状态失败,支付单编号不匹配"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS = new ErrorCode(1007900003, "示例订单更新支付状态失败,支付单状态不是【支付成功】状态"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH = new ErrorCode(1007900004, "示例订单更新支付状态失败,支付单金额不匹配"); + ErrorCode DEMO_ORDER_REFUND_FAIL_NOT_PAID = new ErrorCode(1007900005, "发起退款失败,示例订单未支付"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUNDED = new ErrorCode(1007900006, "发起退款失败,示例订单已退款"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND = new ErrorCode(1007900007, "发起退款失败,退款订单不存在"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS = new ErrorCode(1007900008, "发起退款失败,退款订单未退款成功"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1007900009, "发起退款失败,退款单编号不匹配"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1007900010, "发起退款失败,退款单金额不匹配"); + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/member/WalletOperateTypeEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/member/WalletOperateTypeEnum.java new file mode 100644 index 000000000..896e90ef9 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/member/WalletOperateTypeEnum.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.pay.enums.member; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 钱包操作类型枚举 + * + * @author jason + */ +@AllArgsConstructor +@Getter +public enum WalletOperateTypeEnum { + TOP_UP_INC(1, "充值增加"), + ORDER_DEC(2, "订单消费扣除"); + // TODO 其它类型 + + private final Integer type; + + private final String desc; +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/member/WalletTransactionGategoryEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/member/WalletTransactionGategoryEnum.java new file mode 100644 index 000000000..fe950991d --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/member/WalletTransactionGategoryEnum.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.pay.enums.member; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 钱包交易大类枚举 + * + * @author jason + */ +@AllArgsConstructor +@Getter +public enum WalletTransactionGategoryEnum { + TOP_UP(1, "充值"), + SPENDING(2, "支出"); + + /** + * 分类 + */ + private final Integer category; + + /** + * 说明 + */ + private final String desc; +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyStatusEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyStatusEnum.java new file mode 100644 index 000000000..b735598d5 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyStatusEnum.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.pay.enums.notify; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付通知状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayNotifyStatusEnum { + + WAITING(0, "等待通知"), + SUCCESS(10, "通知成功"), + FAILURE(20, "通知失败"), // 多次尝试,彻底失败 + REQUEST_SUCCESS(21, "请求成功,但是结果失败"), + REQUEST_FAILURE(22, "请求失败"), + + ; + + /** + * 状态 + */ + private final Integer status; + /** + * 名字 + */ + private final String name; + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java new file mode 100644 index 000000000..8c259d93c --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.pay.enums.notify; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付通知类型 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayNotifyTypeEnum { + + ORDER(1, "支付单"), + REFUND(2, "退款单"), + ; + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderStatusEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderStatusEnum.java new file mode 100644 index 000000000..86a9e1704 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderStatusEnum.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.pay.enums.order; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 支付订单的状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayOrderStatusEnum implements IntArrayValuable { + + WAITING(0, "未支付"), + SUCCESS(10, "支付成功"), + REFUND(20, "已退款"), + CLOSED(30, "支付关闭"), // 注意:全部退款后,还是 REFUND 状态 + ; + + private final Integer status; + private final String name; + + @Override + public int[] array() { + return new int[0]; + } + + /** + * 判断是否支付成功 + * + * @param status 状态 + * @return 是否支付成功 + */ + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + /** + * 判断是否支付成功或者已退款 + * + * @param status 状态 + * @return 是否支付成功或者已退款 + */ + public static boolean isSuccessOrRefund(Integer status) { + return ObjectUtils.equalsAny(status, + SUCCESS.getStatus(), REFUND.getStatus()); + } + + /** + * 判断是否支付关闭 + * + * @param status 状态 + * @return 是否支付关闭 + */ + public static boolean isClosed(Integer status) { + return Objects.equals(status, CLOSED.getStatus()); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundStatusEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundStatusEnum.java new file mode 100644 index 000000000..4ae14cc67 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundStatusEnum.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.pay.enums.refund; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 渠道的退款状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayRefundStatusEnum { + + WAITING(0, "未退款"), + SUCCESS(10, "退款成功"), + FAILURE(20, "退款失败"); + + private final Integer status; + private final String name; + + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + public static boolean isFailure(Integer status) { + return Objects.equals(status, FAILURE.getStatus()); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/pom.xml b/yudao-module-pay/yudao-module-pay-biz/pom.xml new file mode 100644 index 000000000..1e0850252 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/pom.xml @@ -0,0 +1,118 @@ + + + + cn.iocoder.cloud + yudao-module-pay + ${revision} + + 4.0.0 + yudao-module-pay-biz + jar + + ${project.artifactId} + + pay 模块,我们放支付业务,提供业务的支付能力。 + 例如说:商户、应用、支付、退款等等 + + + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + + + cn.iocoder.cloud + yudao-module-pay-api + ${revision} + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-banner + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-operatelog + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-tenant + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-error-code + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-pay + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-rpc + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-job + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-monitor + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-excel + + + + + diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/PayServerApplication.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/PayServerApplication.java new file mode 100644 index 000000000..dc67f2da3 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/PayServerApplication.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pay; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 项目的启动类 + * + * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + * + * @author 芋道源码 + */ +@SpringBootApplication +public class PayServerApplication { + + public static void main(String[] args) { + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + + SpringApplication.run(PayServerApplication.class, args); + + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java new file mode 100644 index 000000000..8b4ae4354 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pay.api.order; + +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; +import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert; +import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import org.apache.dubbo.config.annotation.DubboService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.module.system.enums.ApiConstants.VERSION; + +@Service +@RestController // 提供 RESTful API 接口,给 Feign 调用 +@DubboService(version = VERSION) // 提供 Dubbo RPC 接口,给 Dubbo Consumer 调用 +@Validated +public class PayOrderApiImpl implements PayOrderApi { + + @Resource + private PayOrderService payOrderService; + + @Override + public Long createOrder(PayOrderCreateReqDTO reqDTO) { + return payOrderService.createOrder(reqDTO); + } + + @Override + public PayOrderRespDTO getOrder(Long id) { + PayOrderDO order = payOrderService.getOrder(id); + return PayOrderConvert.INSTANCE.convert2(order); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java new file mode 100644 index 000000000..5bb633f37 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.pay.api.refund; + +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO; +import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert; +import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; +import org.apache.dubbo.config.annotation.DubboService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.module.system.enums.ApiConstants.VERSION; + +@Service +@RestController // 提供 RESTful API 接口,给 Feign 调用 +@DubboService(version = VERSION) // 提供 Dubbo RPC 接口,给 Dubbo Consumer 调用 +@Validated +public class PayRefundApiImpl implements PayRefundApi { + + @Resource + private PayRefundService payRefundService; + + @Override + public Long createRefund(PayRefundCreateReqDTO reqDTO) { + return payRefundService.createPayRefund(reqDTO); + } + + @Override + public PayRefundRespDTO getRefund(Long id) { + return PayRefundConvert.INSTANCE.convert02(payRefundService.getRefund(id)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/PayAppController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/PayAppController.java new file mode 100644 index 000000000..b0d80eac3 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/PayAppController.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.module.pay.controller.admin.app; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.*; +import cn.iocoder.yudao.module.pay.convert.app.PayAppConvert; +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.service.app.PayAppService; +import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Slf4j +@Tag(name = "管理后台 - 支付应用信息") +@RestController +@RequestMapping("/pay/app") +@Validated +public class PayAppController { + + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + + @PostMapping("/create") + @Operation(summary = "创建支付应用信息") + @PreAuthorize("@ss.hasPermission('pay:app:create')") + public CommonResult createApp(@Valid @RequestBody PayAppCreateReqVO createReqVO) { + return success(appService.createApp(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新支付应用信息") + @PreAuthorize("@ss.hasPermission('pay:app:update')") + public CommonResult updateApp(@Valid @RequestBody PayAppUpdateReqVO updateReqVO) { + appService.updateApp(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新支付应用状态") + @PreAuthorize("@ss.hasPermission('pay:app:update')") + public CommonResult updateAppStatus(@Valid @RequestBody PayAppUpdateStatusReqVO updateReqVO) { + appService.updateAppStatus(updateReqVO.getId(), updateReqVO.getStatus()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除支付应用信息") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('pay:app:delete')") + public CommonResult deleteApp(@RequestParam("id") Long id) { + appService.deleteApp(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得支付应用信息") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:app:query')") + public CommonResult getApp(@RequestParam("id") Long id) { + PayAppDO app = appService.getApp(id); + return success(PayAppConvert.INSTANCE.convert(app)); + } + + @GetMapping("/page") + @Operation(summary = "获得支付应用信息分页") + @PreAuthorize("@ss.hasPermission('pay:app:query')") + public CommonResult> getAppPage(@Valid PayAppPageReqVO pageVO) { + // 得到应用分页列表 + PageResult pageResult = appService.getAppPage(pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 得到所有的应用编号,查出所有的渠道 + Collection appIds = convertList(pageResult.getList(), PayAppDO::getId); + List channels = channelService.getChannelListByAppIds(appIds); + + // 拼接后返回 + return success(PayAppConvert.INSTANCE.convertPage(pageResult, channels)); + } + + @GetMapping("/list") + @Operation(summary = "获得应用列表") + @PreAuthorize("@ss.hasPermission('pay:merchant:query')") + public CommonResult> getAppList() { + List appListDO = appService.getAppList(); + return success(PayAppConvert.INSTANCE.convertList(appListDO)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java new file mode 100644 index 000000000..45f29360f --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.pay.controller.admin.app.vo; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.*; + +/** +* 支付应用信息 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayAppBaseVO { + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小豆") + @NotNull(message = "应用名不能为空") + private String name; + + @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "开启状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "备注", example = "我是一个测试应用") + private String remark; + + @Schema(description = "支付结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay-callback") + @NotNull(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的回调地址必须为 URL 格式") + private String orderNotifyUrl; + + @Schema(description = "退款结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback") + @NotNull(message = "退款结果的回调地址不能为空") + @URL(message = "退款结果的回调地址必须为 URL 格式") + private String refundNotifyUrl; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java new file mode 100644 index 000000000..03cab7d3e --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java @@ -0,0 +1,11 @@ +package cn.iocoder.yudao.module.pay.controller.admin.app.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(description = "管理后台 - 支付应用信息创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppCreateReqVO extends PayAppBaseVO { + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java new file mode 100644 index 000000000..76b62003c --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.Set; + +@Schema(description = "管理后台 - 支付应用信息分页查询 Response VO,相比于支付信息,还会多出应用渠道的开关信息") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppPageItemRespVO extends PayAppBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "已配置的支付渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "[alipay_pc, alipay_wap]") + private Set channelCodes; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java new file mode 100644 index 000000000..94ade7ce6 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pay.controller.admin.app.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 支付应用信息分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppPageReqVO extends PageParam { + + @Schema(description = "应用名", example = "小豆") + private String name; + + @Schema(description = "开启状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppRespVO.java new file mode 100644 index 000000000..9471a2f01 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.pay.controller.admin.app.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付应用信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppRespVO extends PayAppBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java new file mode 100644 index 000000000..5edf2290a --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.pay.controller.admin.app.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 支付应用信息更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppUpdateReqVO extends PayAppBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long id; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java new file mode 100644 index 000000000..9b8dad6e1 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 应用更新状态 Request VO") +@Data +public class PayAppUpdateStatusReqVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long id; + + @Schema(description = "状态,见 SysCommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/PayChannelController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/PayChannelController.java new file mode 100644 index 000000000..37927a799 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/PayChannelController.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.pay.controller.admin.channel; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelRespVO; +import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import cn.iocoder.yudao.module.pay.convert.channel.PayChannelConvert; +import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - 支付渠道") +@RestController +@RequestMapping("/pay/channel") +@Validated +public class PayChannelController { + + @Resource + private PayChannelService channelService; + + @PostMapping("/create") + @Operation(summary = "创建支付渠道 ") + @PreAuthorize("@ss.hasPermission('pay:channel:create')") + public CommonResult createChannel(@Valid @RequestBody PayChannelCreateReqVO createReqVO) { + return success(channelService.createChannel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新支付渠道 ") + @PreAuthorize("@ss.hasPermission('pay:channel:update')") + public CommonResult updateChannel(@Valid @RequestBody PayChannelUpdateReqVO updateReqVO) { + channelService.updateChannel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除支付渠道 ") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('pay:channel:delete')") + public CommonResult deleteChannel(@RequestParam("id") Long id) { + channelService.deleteChannel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得支付渠道") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:channel:query')") + public CommonResult getChannel(@RequestParam(value = "id", required = false) Long id, + @RequestParam(value = "appId", required = false) Long appId, + @RequestParam(value = "code", required = false) String code) { + PayChannelDO channel = null; + if (id != null) { + channel = channelService.getChannel(id); + } else if (appId != null && code != null) { + channel = channelService.getChannelByAppIdAndCode(appId, code); + } + return success(PayChannelConvert.INSTANCE.convert(channel)); + } + + @GetMapping("/get-enable-code-list") + @Operation(summary = "获得指定应用的开启的支付渠道编码列表") + @Parameter(name = "appId", description = "应用编号", required = true, example = "1") + public CommonResult> getEnableChannelCodeList(@RequestParam("appId") Long appId) { + List channels = channelService.getEnableChannelList(appId); + return success(convertSet(channels, PayChannelDO::getCode)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelBaseVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelBaseVO.java new file mode 100644 index 000000000..1416c9790 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelBaseVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.pay.controller.admin.channel.vo; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +/** +* 支付渠道 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayChannelBaseVO { + + @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "开启状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "备注", example = "我是小备注") + private String remark; + + @Schema(description = "渠道费率,单位:百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "渠道费率,单位:百分比不能为空") + private Double feeRate; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long appId; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java new file mode 100644 index 000000000..08073827a --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pay.controller.admin.channel.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 支付渠道 创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayChannelCreateReqVO extends PayChannelBaseVO { + + @Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_pc") + @NotNull(message = "渠道编码不能为空") + private String code; + + @Schema(description = "渠道配置的 json 字符串") + @NotBlank(message = "渠道配置不能为空") + private String config; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelRespVO.java new file mode 100644 index 000000000..dafd29ec9 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pay.controller.admin.channel.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付渠道 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayChannelRespVO extends PayChannelBaseVO { + + @Schema(description = "商户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private LocalDateTime createTime; + + @Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_pc") + private String code; + + @Schema(description = "配置", requiredMode = Schema.RequiredMode.REQUIRED) + private String config; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java new file mode 100644 index 000000000..39bf83eb0 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pay.controller.admin.channel.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 支付渠道 更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayChannelUpdateReqVO extends PayChannelBaseVO { + + @Schema(description = "商户编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "商户编号不能为空") + private Long id; + + @Schema(description = "渠道配置的json字符串") + @NotBlank(message = "渠道配置不能为空") + private String config; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java new file mode 100644 index 000000000..1e3a61eec --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.pay.controller.admin.demo; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO; +import cn.iocoder.yudao.module.pay.convert.demo.PayDemoOrderConvert; +import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import cn.iocoder.yudao.module.pay.service.demo.PayDemoOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 示例订单") +@RestController +@RequestMapping("/pay/demo-order") +@Validated +public class PayDemoOrderController { + + @Resource + private PayDemoOrderService payDemoOrderService; + + @PostMapping("/create") + @Operation(summary = "创建示例订单") + public CommonResult createDemoOrder(@Valid @RequestBody PayDemoOrderCreateReqVO createReqVO) { + return success(payDemoOrderService.createDemoOrder(getLoginUserId(), createReqVO)); + } + + @GetMapping("/page") + @Operation(summary = "获得示例订单分页") + public CommonResult> getDemoOrderPage(@Valid PageParam pageVO) { + PageResult pageResult = payDemoOrderService.getDemoOrderPage(pageVO); + return success(PayDemoOrderConvert.INSTANCE.convertPage(pageResult)); + } + + @PostMapping("/update-paid") + @Operation(summary = "更新示例订单为已支付") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现 + @OperateLog(enable = false) // 禁用操作日志,因为没有操作人 + public CommonResult updateDemoOrderPaid(@RequestBody PayOrderNotifyReqDTO notifyReqDTO) { + payDemoOrderService.updateDemoOrderPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getPayOrderId()); + return success(true); + } + + @PutMapping("/refund") + @Operation(summary = "发起示例订单的退款") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult refundDemoOrder(@RequestParam("id") Long id) { + payDemoOrderService.refundDemoOrder(id, getClientIP()); + return success(true); + } + + @PostMapping("/update-refunded") + @Operation(summary = "更新示例订单为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现 + @OperateLog(enable = false) // 禁用操作日志,因为没有操作人 + public CommonResult updateDemoOrderRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { + payDemoOrderService.updateDemoOrderRefunded(Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getPayRefundId()); + return success(true); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java new file mode 100644 index 000000000..9960ada48 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.pay.controller.admin.demo.vo; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 示例订单创建 Request VO") +@Data +public class PayDemoOrderCreateReqVO { + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17682") + @NotNull(message = "商品编号不能为空") + private Long spuId; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java new file mode 100644 index 000000000..3404844dc --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.pay.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +/** +* 示例订单 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayDemoOrderRespVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23199") + private Long userId; + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17682") + private Long spuId; + + @Schema(description = "商家备注", example = "李四") + private String spuName; + + @Schema(description = "价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "30381") + private Integer price; + + @Schema(description = "是否已支付", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean payStatus; + + @Schema(description = "支付订单编号", example = "16863") + private Long payOrderId; + + @Schema(description = "订单支付时间") + private LocalDateTime payTime; + + @Schema(description = "支付渠道", example = "alipay_qr") + private String payChannelCode; + + @Schema(description = "支付退款编号", example = "23366") + private Long payRefundId; + + @Schema(description = "退款金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "14039") + private Integer refundPrice; + + @Schema(description = "退款时间") + private LocalDateTime refundTime; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java new file mode 100644 index 000000000..9db3bba30 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.pay.controller.admin.notify; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO; +import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO; +import cn.iocoder.yudao.module.pay.convert.notify.PayNotifyTaskConvert; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +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.service.app.PayAppService; +import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.CHANNEL_NOT_FOUND; + +@Tag(name = "管理后台 - 回调通知") +@RestController +@RequestMapping("/pay/notify") +@Validated +@Slf4j +public class PayNotifyController { + + @Resource + private PayOrderService orderService; + @Resource + private PayRefundService refundService; + @Resource + private PayNotifyService notifyService; + @Resource + private PayAppService appService; + + @Resource + private PayClientFactory payClientFactory; + + @PostMapping(value = "/order/{channelId}") + @Operation(summary = "支付渠道的统一【支付】回调") + @PermitAll + @OperateLog(enable = false) // 回调地址,无需记录操作日志 + public String notifyOrder(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body) { + log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body); + // 1. 校验支付渠道是否存在 + PayClient payClient = payClientFactory.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId); + throw exception(CHANNEL_NOT_FOUND); + } + + // 2. 解析通知数据 + PayOrderRespDTO notify = payClient.parseOrderNotify(params, body); + orderService.notifyOrder(channelId, notify); + return "success"; + } + + @PostMapping(value = "/refund/{channelId}") + @Operation(summary = "支付渠道的统一【退款】回调") + @PermitAll + @OperateLog(enable = false) // 回调地址,无需记录操作日志 + public String notifyRefund(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body) { + log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body); + // 1. 校验支付渠道是否存在 + PayClient payClient = payClientFactory.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId); + throw exception(CHANNEL_NOT_FOUND); + } + + // 2. 解析通知数据 + PayRefundRespDTO notify = payClient.parseRefundNotify(params, body); + refundService.notifyRefund(channelId, notify); + return "success"; + } + + @GetMapping("/get-detail") + @Operation(summary = "获得回调通知的明细") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:notify:query')") + public CommonResult getNotifyTaskDetail(@RequestParam("id") Long id) { + PayNotifyTaskDO task = notifyService.getNotifyTask(id); + if (task == null) { + return success(null); + } + // 拼接返回 + PayAppDO app = appService.getApp(task.getAppId()); + List logs = notifyService.getNotifyLogList(id); + return success(PayNotifyTaskConvert.INSTANCE.convert(task, app, logs)); + } + + @GetMapping("/page") + @Operation(summary = "获得回调通知分页") + @PreAuthorize("@ss.hasPermission('pay:notify:query')") + public CommonResult> getNotifyTaskPage(@Valid PayNotifyTaskPageReqVO pageVO) { + PageResult pageResult = notifyService.getNotifyTaskPage(pageVO); + // 拼接返回 + Map appMap = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTaskDO::getAppId)); + return success(PayNotifyTaskConvert.INSTANCE.convertPage(pageResult, appMap)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskBaseVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskBaseVO.java new file mode 100644 index 000000000..1e623751b --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskBaseVO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.pay.controller.admin.notify.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 回调通知 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class PayNotifyTaskBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10636") + private Long appId; + + @Schema(description = "通知类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Byte type; + + @Schema(description = "数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6722") + private Long dataId; + + @Schema(description = "通知状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Byte status; + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26697") + private String merchantOrderId; + + @Schema(description = "下一次通知时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime nextNotifyTime; + + @Schema(description = "最后一次执行时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime lastExecuteTime; + + @Schema(description = "当前通知次数", requiredMode = Schema.RequiredMode.REQUIRED) + private Byte notifyTimes; + + @Schema(description = "最大可通知次数", requiredMode = Schema.RequiredMode.REQUIRED) + private Byte maxNotifyTimes; + + @Schema(description = "异步通知地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn") + private String notifyUrl; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java new file mode 100644 index 000000000..7c75613e2 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java @@ -0,0 +1,54 @@ + +package cn.iocoder.yudao.module.pay.controller.admin.notify.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 回调通知的明细 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayNotifyTaskDetailRespVO extends PayNotifyTaskBaseVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3380") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + @Schema(description = "应用名称", example = "wx_pay") + private String appName; + + @Schema(description = "回调日志列表") + private List logs; + + @Schema(description = "管理后台 - 回调日志") + @Data + public static class Log { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8848") + private Long id; + + @Schema(description = "通知状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Byte status; + + @Schema(description = "当前通知次数", requiredMode = Schema.RequiredMode.REQUIRED) + private Byte notifyTimes; + + @Schema(description = "HTTP 响应结果", requiredMode = Schema.RequiredMode.REQUIRED) + private String response; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java new file mode 100644 index 000000000..003d2fb33 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.pay.controller.admin.notify.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 回调通知分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayNotifyTaskPageReqVO extends PageParam { + + @Schema(description = "应用编号", example = "10636") + private Long appId; + + @Schema(description = "通知类型", example = "2") + private Integer type; + + @Schema(description = "数据编号", example = "6722") + private Long dataId; + + @Schema(description = "通知状态", example = "1") + private Integer status; + + @Schema(description = "商户订单编号", example = "26697") + private String merchantOrderId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java new file mode 100644 index 000000000..d7f7fe6fb --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.pay.controller.admin.notify.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 回调通知 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayNotifyTaskRespVO extends PayNotifyTaskBaseVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3380") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "应用名称", example = "wx_pay") + private String appName; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java new file mode 100755 index 000000000..9a2afbe66 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java @@ -0,0 +1,111 @@ +package cn.iocoder.yudao.module.pay.controller.admin.order; + +import cn.hutool.core.collection.CollectionUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*; +import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +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.service.app.PayAppService; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 支付订单") +@RestController +@RequestMapping("/pay/order") +@Validated +public class PayOrderController { + + @Resource + private PayOrderService orderService; + @Resource + private PayAppService appService; + + @GetMapping("/get") + @Operation(summary = "获得支付订单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:order:query')") + public CommonResult getOrder(@RequestParam("id") Long id) { + return success(PayOrderConvert.INSTANCE.convert(orderService.getOrder(id))); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得支付订单详情") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:order:query')") + public CommonResult getOrderDetail(@RequestParam("id") Long id) { + PayOrderDO order = orderService.getOrder(id); + if (order == null) { + return success(null); + } + + // 拼接返回 + PayAppDO app = appService.getApp(order.getAppId()); + PayOrderExtensionDO orderExtension = orderService.getOrderExtension(order.getExtensionId()); + return success(PayOrderConvert.INSTANCE.convert(order, orderExtension, app)); + } + + @PostMapping("/submit") + @Operation(summary = "提交支付订单") + public CommonResult submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) { + PayOrderSubmitRespVO respVO = orderService.submitOrder(reqVO, getClientIP()); + return success(respVO); + } + + @GetMapping("/page") + @Operation(summary = "获得支付订单分页") + @PreAuthorize("@ss.hasPermission('pay:order:query')") + public CommonResult> getOrderPage(@Valid PayOrderPageReqVO pageVO) { + PageResult pageResult = orderService.getOrderPage(pageVO); + if (CollectionUtil.isEmpty(pageResult.getList())) { + return success(new PageResult<>(pageResult.getTotal())); + } + + // 拼接返回 + Map appMap = appService.getAppMap(convertList(pageResult.getList(), PayOrderDO::getAppId)); + return success(PayOrderConvert.INSTANCE.convertPage(pageResult, appMap)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出支付订单 Excel") + @PreAuthorize("@ss.hasPermission('pay:order:export')") + @OperateLog(type = EXPORT) + public void exportOrderExcel(@Valid PayOrderExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = orderService.getOrderList(exportReqVO); + if (CollectionUtil.isEmpty(list)) { + ExcelUtils.write(response, "支付订单.xls", "数据", + PayOrderExcelVO.class, new ArrayList<>()); + return; + } + + // 拼接返回 + Map appMap = appService.getAppMap(convertList(list, PayOrderDO::getAppId)); + List excelList = PayOrderConvert.INSTANCE.convertList(list, appMap); + // 导出 Excel + ExcelUtils.write(response, "支付订单.xls", "数据", PayOrderExcelVO.class, excelList); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderBaseVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderBaseVO.java new file mode 100755 index 000000000..568f84459 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderBaseVO.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.pay.controller.admin.order.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * 支付订单 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + * + * @author aquan + */ +@Data +public class PayOrderBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long appId; + + @Schema(description = "渠道编号", example = "2048") + private Long channelId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "888") + @NotNull(message = "商户订单编号不能为空") + private String merchantOrderId; + + @Schema(description = "商品标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + @NotNull(message = "商品标题不能为空") + private String subject; + + @Schema(description = "商品描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是土豆") + @NotNull(message = "商品描述不能为空") + private String body; + + @Schema(description = "异步通知地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay/notify") + @NotNull(message = "异步通知地址不能为空") + private String notifyUrl; + + @Schema(description = "支付金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "支付金额,单位:分不能为空") + private Long price; + + @Schema(description = "渠道手续费,单位:百分比", example = "10") + private Double channelFeeRate; + + @Schema(description = "渠道手续金额,单位:分", example = "100") + private Integer channelFeePrice; + + @Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "支付状态不能为空") + private Integer status; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @NotNull(message = "用户 IP不能为空") + private String userIp; + + @Schema(description = "订单失效时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "订单失效时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime expireTime; + + @Schema(description = "订单支付成功时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime successTime; + + @Schema(description = "支付成功的订单拓展单编号", example = "50") + private Long extensionId; + + @Schema(description = "支付订单号", example = "2048888") + private String no; + + @Schema(description = "退款总金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "退款总金额,单位:分不能为空") + private Long refundPrice; + + @Schema(description = "渠道用户编号", example = "2048") + private String channelUserId; + + @Schema(description = "渠道订单号", example = "4096") + private String channelOrderNo; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java new file mode 100644 index 000000000..17644fd60 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付订单详细信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderDetailsRespVO extends PayOrderBaseVO { + + @Schema(description = "支付订单编号", required = true, example = "1024") + private Long id; + + @Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String appName; + + @Schema(description = "创建时间", required = true) + private LocalDateTime createTime; + + @Schema(description = "更新时间", required = true) + private LocalDateTime updateTime; + + /** + * 支付订单扩展 + */ + private PayOrderExtension extension; + + @Data + @Schema(description = "支付订单扩展") + public static class PayOrderExtension { + + @Schema(description = "支付订单号", required = true, example = "1024") + private String no; + + @Schema(description = "支付异步通知的内容") + private String channelNotifyData; + + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java new file mode 100755 index 000000000..5dc17a0f0 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.pay.controller.admin.order.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert; +import cn.iocoder.yudao.module.pay.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 支付订单 Excel VO + * + * @author aquan + */ +@Data +public class PayOrderExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @ExcelProperty(value = "支付金额", converter = MoneyConvert.class) + private Integer price; + + @ExcelProperty(value = "退款金额", converter = MoneyConvert.class) + private Integer refundPrice; + + @ExcelProperty(value = "手续金额", converter = MoneyConvert.class) + private Integer channelFeePrice; + + @ExcelProperty("商户单号") + private String merchantOrderId; + + @ExcelProperty(value = "支付单号") + private String no; + + @ExcelProperty("渠道单号") + private String channelOrderNo; + + @ExcelProperty(value = "支付状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.ORDER_STATUS) + private Integer status; + + @ExcelProperty(value = "渠道编号名称", converter = DictConvert.class) + @DictFormat(DictTypeConstants.CHANNEL_CODE) + private String channelCode; + + @ExcelProperty("订单支付成功时间") + private LocalDateTime successTime; + + @ExcelProperty("订单失效时间") + private LocalDateTime expireTime; + + @ExcelProperty(value = "应用名称") + private String appName; + + @ExcelProperty("商品标题") + private String subject; + + @ExcelProperty("商品描述") + private String body; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExportReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExportReqVO.java new file mode 100755 index 000000000..9e4d20b91 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExportReqVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 支付订单 Excel 导出 Request VO,参数和 PayOrderPageReqVO 是一致的") +@Data +public class PayOrderExportReqVO { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户订单编号", example = "4096") + private String merchantOrderId; + + @Schema(description = "渠道编号", example = "1888") + private String channelOrderNo; + + @Schema(description = "支付单号", example = "2014888") + private String no; + + @Schema(description = "支付状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java new file mode 100755 index 000000000..05411c913 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付订单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderPageItemRespVO extends PayOrderBaseVO { + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "应用名称", example = "wx_pay") + private String appName; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderPageReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderPageReqVO.java new file mode 100755 index 000000000..f7ff801cb --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderPageReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.pay.controller.admin.order.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 支付订单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderPageReqVO extends PageParam { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户订单编号", example = "4096") + private String merchantOrderId; + + @Schema(description = "渠道编号", example = "1888") + private String channelOrderNo; + + @Schema(description = "支付单号", example = "2014888") + private String no; + + @Schema(description = "支付状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderRespVO.java new file mode 100644 index 000000000..d48a4db08 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderRespVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付订单 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderRespVO extends PayOrderBaseVO { + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java new file mode 100644 index 000000000..ffda105b7 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 支付订单提交 Request VO") +@Data +public class PayOrderSubmitReqVO { + + @Schema(description = "支付单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "支付单编号不能为空") + private Long id; + + @Schema(description = "支付渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_pub") + @NotEmpty(message = "支付渠道不能为空") + private String channelCode; + + @Schema(description = "支付渠道的额外参数,例如说,微信公众号需要传递 openid 参数") + private Map channelExtras; + + @Schema(description = "展示模式", example = "url") // 参见 {@link PayDisplayModeEnum} 枚举。如果不传递,则每个支付渠道使用默认的方式 + private String displayMode; + + @Schema(description = "回跳地址") + @URL(message = "回跳地址的格式必须是 URL") + private String returnUrl; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java new file mode 100644 index 000000000..8dcd9df2f --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 支付订单提交 Response VO") +@Data +public class PayOrderSubmitRespVO { + + @Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") // 参见 PayOrderStatusEnum 枚举 + private Integer status; + + @Schema(description = "展示模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "url") // 参见 PayDisplayModeEnum 枚举 + private String displayMode; + @Schema(description = "展示内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String displayContent; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java new file mode 100755 index 000000000..df131ddd3 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/PayRefundController.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.pay.controller.admin.refund; + +import cn.hutool.core.collection.CollectionUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.*; +import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.iocoder.yudao.module.pay.service.app.PayAppService; +import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 退款订单") +@RestController +@RequestMapping("/pay/refund") +@Validated +public class PayRefundController { + + @Resource + private PayRefundService refundService; + @Resource + private PayAppService appService; + + @GetMapping("/get") + @Operation(summary = "获得退款订单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:refund:query')") + public CommonResult getRefund(@RequestParam("id") Long id) { + PayRefundDO refund = refundService.getRefund(id); + if (refund == null) { + return success(new PayRefundDetailsRespVO()); + } + + // 拼接数据 + PayAppDO app = appService.getApp(refund.getAppId()); + return success(PayRefundConvert.INSTANCE.convert(refund, app)); + } + + @GetMapping("/page") + @Operation(summary = "获得退款订单分页") + @PreAuthorize("@ss.hasPermission('pay:refund:query')") + public CommonResult> getRefundPage(@Valid PayRefundPageReqVO pageVO) { + PageResult pageResult = refundService.getRefundPage(pageVO); + if (CollectionUtil.isEmpty(pageResult.getList())) { + return success(new PageResult<>(pageResult.getTotal())); + } + + // 处理应用ID数据 + Map appMap = appService.getAppMap(convertList(pageResult.getList(), PayRefundDO::getAppId)); + return success(PayRefundConvert.INSTANCE.convertPage(pageResult, appMap)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出退款订单 Excel") + @PreAuthorize("@ss.hasPermission('pay:refund:export')") + @OperateLog(type = EXPORT) + public void exportRefundExcel(@Valid PayRefundExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = refundService.getRefundList(exportReqVO); + if (CollectionUtil.isEmpty(list)) { + ExcelUtils.write(response, "退款订单.xls", "数据", + PayRefundExcelVO.class, new ArrayList<>()); + return; + } + + // 拼接返回 + Map appMap = appService.getAppMap(convertList(list, PayRefundDO::getAppId)); + List excelList = PayRefundConvert.INSTANCE.convertList(list, appMap); + // 导出 Excel + ExcelUtils.write(response, "退款订单.xls", "数据", PayRefundExcelVO.class, excelList); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundBaseVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundBaseVO.java new file mode 100755 index 000000000..25cadf478 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundBaseVO.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** +* 退款订单 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayRefundBaseVO { + + @Schema(description = "外部退款号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110") + private String no; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long appId; + + @Schema(description = "渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long channelId; + + @Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_app") + private String channelCode; + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long orderId; + + // ========== 商户相关字段 ========== + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "225") + private String merchantOrderId; + + @Schema(description = "商户退款订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "512") + private String merchantRefundId; + + @Schema(description = "异步通知地址", requiredMode = Schema.RequiredMode.REQUIRED) + private String notifyUrl; + + // ========== 退款相关字段 ========== + + @Schema(description = "退款状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "支付金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long payPrice; + + @Schema(description = "退款金额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Long refundPrice; + + @Schema(description = "退款原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "我要退了") + private String reason; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + private String userIp; + + // ========== 渠道相关字段 ========== + + @Schema(description = "渠道订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "233") + private String channelOrderNo; + + @Schema(description = "渠道退款单号", example = "2022") + private String channelRefundNo; + + @Schema(description = "退款成功时间") + private LocalDateTime successTime; + + @Schema(description = "调用渠道的错误码") + private String channelErrorCode; + + @Schema(description = "调用渠道的错误提示") + private String channelErrorMsg; + + @Schema(description = "支付渠道的额外参数") + private String channelNotifyData; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java new file mode 100755 index 000000000..8f50a3a13 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 退款订单详情 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayRefundDetailsRespVO extends PayRefundBaseVO { + + @Schema(description = "支付退款编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + + @Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是芋艿") + private String appName; + + @Schema(description = "支付订单", requiredMode = Schema.RequiredMode.REQUIRED) + private Order order; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "管理后台 - 支付订单") + @Data + public static class Order { + + @Schema(description = "商品标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + private String subject; + + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java new file mode 100755 index 000000000..758b6b6b0 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.pay.controller.admin.refund.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert; +import cn.iocoder.yudao.module.pay.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 退款订单 Excel VO + * + * @author aquan + */ +@Data +public class PayRefundExcelVO { + + @ExcelProperty("支付退款编号") + private Long id; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @ExcelProperty(value = "支付金额", converter = MoneyConvert.class) + private Integer payPrice; + + @ExcelProperty(value = "退款金额", converter = MoneyConvert.class) + private Integer refundPrice; + + @ExcelProperty("商户退款单号") + private String merchantRefundId; + @ExcelProperty("退款单号") + private String no; + @ExcelProperty("渠道退款单号") + private String channelRefundNo; + + @ExcelProperty("商户支付单号") + private String merchantOrderId; + @ExcelProperty("渠道支付单号") + private String channelOrderNo; + + @ExcelProperty(value = "退款状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.REFUND_STATUS) + private Integer status; + + @ExcelProperty(value = "退款渠道", converter = DictConvert.class) + @DictFormat(DictTypeConstants.CHANNEL_CODE) + private String channelCode; + + @ExcelProperty("成功时间") + private LocalDateTime successTime; + + @ExcelProperty(value = "支付应用") + private String appName; + + @ExcelProperty("退款原因") + private String reason; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExportReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExportReqVO.java new file mode 100755 index 000000000..645816ee2 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExportReqVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 退款订单 Excel 导出 Request VO,参数和 PayRefundPageReqVO 是一致的") +@Data +public class PayRefundExportReqVO { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户支付单号", example = "10") + private String merchantOrderId; + + @Schema(description = "商户退款单号", example = "20") + private String merchantRefundId; + + @Schema(description = "渠道支付单号", example = "30") + private String channelOrderNo; + + @Schema(description = "渠道退款单号", example = "40") + private String channelRefundNo; + + @Schema(description = "退款状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java new file mode 100755 index 000000000..27c1285cc --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 退款订单分页查询 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayRefundPageItemRespVO extends PayRefundBaseVO { + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是芋艿") + private String appName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundPageReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundPageReqVO.java new file mode 100755 index 000000000..7ff1530f0 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundPageReqVO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.pay.controller.admin.refund.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 退款订单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayRefundPageReqVO extends PageParam { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户支付单号", example = "10") + private String merchantOrderId; + + @Schema(description = "商户退款单号", example = "20") + private String merchantRefundId; + + @Schema(description = "渠道支付单号", example = "30") + private String channelOrderNo; + + @Schema(description = "渠道退款单号", example = "40") + private String channelRefundNo; + + @Schema(description = "退款状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/channel/AppPayChannelController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/channel/AppPayChannelController.java new file mode 100644 index 000000000..12323a7d5 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/channel/AppPayChannelController.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.pay.controller.app.channel; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "用户 App - 支付渠道") +@RestController +@RequestMapping("/pay/channel") +@Validated +public class AppPayChannelController { + + @Resource + private PayChannelService channelService; + + @GetMapping("/get-enable-code-list") + @Operation(summary = "获得指定应用的开启的支付渠道编码列表") + @Parameter(name = "appId", description = "应用编号", required = true, example = "1") + public CommonResult> getEnableChannelCodeList(@RequestParam("appId") Long appId) { + List channels = channelService.getEnableChannelList(appId); + return success(convertSet(channels, PayChannelDO::getCode)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.http b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.http new file mode 100644 index 000000000..14ce54ef9 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.http @@ -0,0 +1,63 @@ +### /pay/create 提交支付订单【alipay_pc】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "id": 174, + "channelCode": "alipay_pc" +} + +### /pay/create 提交支付订单【wx_bar】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "id": 202, + "channelCode": "wx_bar", + "channelExtras": { + "authCode": "134042110834344848" + } +} + +### /pay/create 提交支付订单【wx_pub】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "id": 202, + "channelCode": "wx_pub", + "channelExtras": { + "openid": "ockUAwIZ-0OeMZl9ogcZ4ILrGba0" + } +} + +### /pay/create 提交支付订单【wx_lite】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "id": 202, + "channelCode": "wx_lite", + "channelExtras": { + "openid": "oLefc4g5GjKWHJjLjMSXB3wX0fD0" + } +} + +### /pay/create 提交支付订单【wx_native】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "id": 202, + "channelCode": "wx_native" +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java new file mode 100644 index 000000000..9100d3498 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.pay.controller.app.order; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderRespVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO; +import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; +import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import io.swagger.v3.oas.annotations.Operation; +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.web.bind.annotation.*; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; + +@Tag(name = "用户 APP - 支付订单") +@RestController +@RequestMapping("/pay/order") +@Validated +@Slf4j +public class AppPayOrderController { + + @Resource + private PayOrderService payOrderService; + + // TODO 芋艿:临时 demo,技术打样。 + @GetMapping("/get") + @Operation(summary = "获得支付订单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult getOrder(@RequestParam("id") Long id) { + return success(PayOrderConvert.INSTANCE.convert(payOrderService.getOrder(id))); + } + + @PostMapping("/submit") + @Operation(summary = "提交支付订单") + public CommonResult submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) { + PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP()); + return success(PayOrderConvert.INSTANCE.convert3(respVO)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java new file mode 100644 index 000000000..8f18c6cec --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.pay.controller.app.order.vo; + +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "用户 APP - 支付订单提交 Request VO") +@Data +public class AppPayOrderSubmitReqVO extends PayOrderSubmitReqVO { +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java new file mode 100644 index 000000000..106535efd --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.pay.controller.app.order.vo; + +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Schema(description = "用户 APP - 支付订单提交 Response VO") +@Data +public class AppPayOrderSubmitRespVO extends PayOrderSubmitRespVO { + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/refund/package-info.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/refund/package-info.java new file mode 100644 index 000000000..ee2004e1a --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/refund/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:占个位置,没啥用 + */ +package cn.iocoder.yudao.module.pay.controller.app.refund; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/package-info.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/package-info.java new file mode 100644 index 000000000..652122752 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.iocoder.yudao.module.pay.controller; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/app/PayAppConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/app/PayAppConvert.java new file mode 100644 index 000000000..4853d07f7 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/app/PayAppConvert.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.pay.convert.app; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageItemRespVO; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppRespVO; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +/** + * 支付应用信息 Convert + * + * @author 芋艿 + */ +@Mapper +public interface PayAppConvert { + + PayAppConvert INSTANCE = Mappers.getMapper(PayAppConvert.class); + + PayAppPageItemRespVO pageConvert (PayAppDO bean); + + PayAppDO convert(PayAppCreateReqVO bean); + + PayAppDO convert(PayAppUpdateReqVO bean); + + PayAppRespVO convert(PayAppDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default PageResult convertPage(PageResult pageResult, List channels) { + PageResult voPageResult = convertPage(pageResult); + // 处理 channel 关系 + Map> appIdChannelMap = CollectionUtils.convertMultiMap2(channels, PayChannelDO::getAppId, PayChannelDO::getCode); + voPageResult.getList().forEach(app -> app.setChannelCodes(appIdChannelMap.get(app.getId()))); + return voPageResult; + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/channel/PayChannelConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/channel/PayChannelConvert.java new file mode 100644 index 000000000..5baf598fd --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/channel/PayChannelConvert.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.pay.convert.channel; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelRespVO; +import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PayChannelConvert { + + PayChannelConvert INSTANCE = Mappers.getMapper(PayChannelConvert.class); + + @Mapping(target = "config",ignore = true) + PayChannelDO convert(PayChannelCreateReqVO bean); + + @Mapping(target = "config",ignore = true) + PayChannelDO convert(PayChannelUpdateReqVO bean); + + @Mapping(target = "config",expression = "java(cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString(bean.getConfig()))") + PayChannelRespVO convert(PayChannelDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java new file mode 100644 index 000000000..313e5d266 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.pay.convert.demo; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 示例订单 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface PayDemoOrderConvert { + + PayDemoOrderConvert INSTANCE = Mappers.getMapper(PayDemoOrderConvert.class); + + PayDemoOrderDO convert(PayDemoOrderCreateReqVO bean); + + PayDemoOrderRespVO convert(PayDemoOrderDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/notify/PayNotifyTaskConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/notify/PayNotifyTaskConvert.java new file mode 100644 index 000000000..d0b8e36ff --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/notify/PayNotifyTaskConvert.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pay.convert.notify; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO; +import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +/** + * 支付通知 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface PayNotifyTaskConvert { + + PayNotifyTaskConvert INSTANCE = Mappers.getMapper(PayNotifyTaskConvert.class); + + PayNotifyTaskRespVO convert(PayNotifyTaskDO bean); + + default PageResult convertPage(PageResult page, Map appMap){ + PageResult result = convertPage(page); + result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName()))); + return result; + } + PageResult convertPage(PageResult page); + + default PayNotifyTaskDetailRespVO convert(PayNotifyTaskDO task, PayAppDO app, List logs) { + PayNotifyTaskDetailRespVO respVO = convert(task, logs); + if (app != null) { + respVO.setAppName(app.getName()); + } + return respVO; + } + PayNotifyTaskDetailRespVO convert(PayNotifyTaskDO task, List logs); +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java new file mode 100755 index 000000000..682006041 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.pay.convert.order; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*; +import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +/** + * 支付订单 Convert + * + * @author aquan + */ +@Mapper +public interface PayOrderConvert { + + PayOrderConvert INSTANCE = Mappers.getMapper(PayOrderConvert.class); + + PayOrderRespVO convert(PayOrderDO bean); + + PayOrderRespDTO convert2(PayOrderDO order); + + default PayOrderDetailsRespVO convert(PayOrderDO order, PayOrderExtensionDO orderExtension, PayAppDO app) { + PayOrderDetailsRespVO respVO = convertDetail(order); + respVO.setExtension(convert(orderExtension)); + if (app != null) { + respVO.setAppName(app.getName()); + } + return respVO; + } + PayOrderDetailsRespVO convertDetail(PayOrderDO bean); + PayOrderDetailsRespVO.PayOrderExtension convert(PayOrderExtensionDO bean); + + default PageResult convertPage(PageResult page, Map appMap) { + PageResult result = convertPage(page); + result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName()))); + return result; + } + PageResult convertPage(PageResult page); + + default List convertList(List list, Map appMap) { + return CollectionUtils.convertList(list, order -> { + PayOrderExcelVO excelVO = convertExcel(order); + MapUtils.findAndThen(appMap, order.getAppId(), app -> excelVO.setAppName(app.getName())); + return excelVO; + }); + } + PayOrderExcelVO convertExcel(PayOrderDO bean); + + PayOrderDO convert(PayOrderCreateReqDTO bean); + + @Mapping(target = "id", ignore = true) + PayOrderExtensionDO convert(PayOrderSubmitReqVO bean, String userIp); + + PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqVO reqVO, String userIp); + + @Mapping(source = "order.status", target = "status") + PayOrderSubmitRespVO convert(PayOrderDO order, cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO respDTO); + + AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/package-info.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/package-info.java new file mode 100644 index 000000000..df43d5ac3 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package cn.iocoder.yudao.module.pay.convert; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java new file mode 100755 index 000000000..9f087f702 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.pay.convert.refund; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundDetailsRespVO; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExcelVO; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageItemRespVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface PayRefundConvert { + + PayRefundConvert INSTANCE = Mappers.getMapper(PayRefundConvert.class); + + + default PayRefundDetailsRespVO convert(PayRefundDO refund, PayAppDO app) { + PayRefundDetailsRespVO respVO = convert(refund); + if (app != null) { + respVO.setAppName(app.getName()); + } + return respVO; + } + PayRefundDetailsRespVO convert(PayRefundDO bean); + PayRefundDetailsRespVO.Order convert(PayOrderDO bean); + + default PageResult convertPage(PageResult page, Map appMap) { + PageResult result = convertPage(page); + result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName()))); + return result; + } + PageResult convertPage(PageResult page); + + PayRefundDO convert(PayRefundCreateReqDTO bean); + + PayRefundRespDTO convert02(PayRefundDO bean); + + default List convertList(List list, Map appMap) { + return CollectionUtils.convertList(list, order -> { + PayRefundExcelVO excelVO = convertExcel(order); + MapUtils.findAndThen(appMap, order.getAppId(), app -> excelVO.setAppName(app.getName())); + return excelVO; + }); + } + PayRefundExcelVO convertExcel(PayRefundDO bean); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 000000000..8153487b7 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java new file mode 100644 index 000000000..977eff93a --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java @@ -0,0 +1,57 @@ +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 com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 支付应用 DO + * 一个商户下,可能会有多个支付应用。例如说,京东有京东商城、京东到家等等 + * 不过一般来说,一个商户,只有一个应用哈~ + * + * 即 PayMerchantDO : PayAppDO = 1 : n + * + * @author 芋道源码 + */ +@TableName("pay_app") +@KeySequence("pay_app_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayAppDO extends BaseDO { + + /** + * 应用编号,数据库自增 + */ + @TableId + private Long id; + /** + * 应用名 + */ + private String name; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 支付结果的回调地址 + */ + private String orderNotifyUrl; + /** + * 退款结果的回调地址 + */ + private String refundNotifyUrl; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/channel/PayChannelDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/channel/PayChannelDO.java new file mode 100644 index 000000000..72387eb1c --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/channel/PayChannelDO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.pay.dal.dataobject.channel; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +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.*; + +/** + * 支付渠道 DO + * 一个应用下,会有多种支付渠道,例如说微信支付、支付宝支付等等 + * + * 即 PayAppDO : PayChannelDO = 1 : n + * + * @author 芋道源码 + */ +@TableName(value = "pay_channel", autoResultMap = true) +@KeySequence("pay_channel_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayChannelDO extends TenantBaseDO { + + /** + * 渠道编号,数据库自增 + */ + private Long id; + /** + * 渠道编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String code; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 渠道费率,单位:百分比 + */ + private Double feeRate; + /** + * 备注 + */ + private String remark; + + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 支付渠道配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private PayClientConfig config; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/demo/PayDemoOrderDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/demo/PayDemoOrderDO.java new file mode 100644 index 000000000..a22f7c1b8 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/demo/PayDemoOrderDO.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.pay.dal.dataobject.demo; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 示例订单 + * + * 演示业务系统的订单,如何接入 pay 系统的支付与退款 + * + * @author 芋道源码 + */ +@TableName("pay_demo_order") +@KeySequence("pay_demo_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayDemoOrderDO extends BaseDO { + + /** + * 订单编号,自增 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 商品编号 + */ + private Long spuId; + /** + * 商品名称 + */ + private String spuName; + /** + * 价格,单位:分 + */ + private Integer price; + + // ========== 支付相关字段 ========== + + /** + * 是否支付 + */ + private Boolean payStatus; + /** + * 支付订单编号 + * + * 对接 pay-module-biz 支付服务的支付订单编号,即 PayOrderDO 的 id 编号 + */ + private Long payOrderId; + /** + * 付款时间 + */ + private LocalDateTime payTime; + /** + * 支付渠道 + * + * 对应 PayChannelEnum 枚举 + */ + private String payChannelCode; + + // ========== 退款相关字段 ========== + /** + * 支付退款单号 + */ + private Long payRefundId; + /** + * 退款金额,单位:分 + */ + private Integer refundPrice; + /** + * 退款完成时间 + */ + private LocalDateTime refundTime; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/member/MemberWalletDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/member/MemberWalletDO.java new file mode 100644 index 000000000..e6e92bf82 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/member/MemberWalletDO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.pay.dal.dataobject.member; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +// TODO @jason:修改 MemberWalletDO 为 PayWalletDO +/** + * 支付 - 会员钱包 DO + * + * @author jason + */ +@TableName(value ="pay_member_wallet") +@KeySequence("pay_member_wallet_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class MemberWalletDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + // TODO @jaosn:增加 userType 字段; + /** + * 用户 id + * + * 关联 MemberUserDO 的 id 编号 + * 关联 AdminUserDO 的 id 编号 + */ + private Long userId; + + /** + * 余额, 单位分 + */ + private Integer balance; + + /** + * 累计支出, 单位分 + */ + private Integer totalSpending; + + /** + * 累计充值, 单位分 + */ + private Integer totalTopUp; +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/member/MemberWalletTransactionDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/member/MemberWalletTransactionDO.java new file mode 100644 index 000000000..82ede2915 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/member/MemberWalletTransactionDO.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.pay.dal.dataobject.member; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pay.enums.member.WalletOperateTypeEnum; +import cn.iocoder.yudao.module.pay.enums.member.WalletTransactionGategoryEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 支付-会员钱包明细 DO + * + * @author jason + */ +@TableName(value ="pay_member_wallet_transaction") +@KeySequence("pay_member_wallet_transaction_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class MemberWalletTransactionDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 会员钱包 id + * + * 关联 {@link MemberWalletDO#getId()} + */ + private Long walletId; + + /** + * 用户 id + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + + /** + * 交易单号 @芋艿 这里是关联交易单号, 还是订单号 , 退款单号! ?? + */ + private String tradeNo; + + /** + * 交易分类 + * + * 枚举 {@link WalletTransactionGategoryEnum#getCategory()} + */ + private Integer category; + + /** + * 操作分类 + * + * 枚举 {@link WalletOperateTypeEnum#getType()} + */ + private Integer operateType; + + /** + * 操作详情 + */ + private String operateDesc; + + /** + * 交易金额, 单位分 + */ + private Integer price; + + /** + * 余额, 单位分 + */ + private Integer balance; + + /** + * 备注 + */ + private String mark; + + /** + * 交易时间 + */ + private LocalDateTime transactionTime; +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java new file mode 100644 index 000000000..a482605d5 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.pay.dal.dataobject.notify; + +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.*; + +/** + * 商户支付、退款等的通知 Log + * 每次通知时,都会在该表中,记录一次 Log,方便排查问题 + * + * @author 芋道源码 + */ +@TableName("pay_notify_log") +@KeySequence("pay_notify_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayNotifyLogDO extends BaseDO { + + /** + * 日志编号,自增 + */ + private Long id; + /** + * 通知任务编号 + * + * 关联 {@link PayNotifyTaskDO#getId()} + */ + private Long taskId; + /** + * 第几次被通知 + * + * 对应到 {@link PayNotifyTaskDO#getNotifyTimes()} + */ + private Integer notifyTimes; + /** + * HTTP 响应结果 + */ + private String response; + /** + * 支付通知状态 + * + * 外键 {@link PayNotifyStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java new file mode 100644 index 000000000..181a32802 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.pay.dal.dataobject.notify; + +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.order.PayOrderDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 支付通知 + * 在支付系统收到支付渠道的支付、退款的结果后,需要不断的通知到业务系统,直到成功。 + * + * @author 芋道源码 + */ +@TableName("pay_notify_task") +@KeySequence("pay_notify_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class PayNotifyTaskDO extends TenantBaseDO { + + /** + * 通知频率,单位为秒。 + * + * 算上首次的通知,实际是一共 1 + 8 = 9 次。 + */ + public static final Integer[] NOTIFY_FREQUENCY = new Integer[]{ + 15, 15, 30, 180, + 1800, 1800, 1800, 3600 + }; + + /** + * 编号,自增 + */ + @TableId + private Long id; + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 通知类型 + * + * 外键 {@link PayNotifyTypeEnum} + */ + private Integer type; + /** + * 数据编号,根据不同 type 进行关联: + * + * 1. {@link PayNotifyTypeEnum#ORDER} 时,关联 {@link PayOrderDO#getId()} + * 2. {@link PayNotifyTypeEnum#REFUND} 时,关联 {@link PayRefundDO#getId()} + */ + private Long dataId; + /** + * 商户订单编号 + */ + private String merchantOrderId; + /** + * 通知状态 + * + * 外键 {@link PayNotifyStatusEnum} + */ + private Integer status; + /** + * 下一次通知时间 + */ + private LocalDateTime nextNotifyTime; + /** + * 最后一次执行时间 + */ + private LocalDateTime lastExecuteTime; + /** + * 当前通知次数 + */ + private Integer notifyTimes; + /** + * 最大可通知次数 + */ + private Integer maxNotifyTimes; + /** + * 通知地址 + */ + private String notifyUrl; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java new file mode 100644 index 000000000..a51b875fd --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java @@ -0,0 +1,138 @@ +package cn.iocoder.yudao.module.pay.dal.dataobject.order; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +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.order.PayOrderStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 支付订单 DO + * + * @author 芋道源码 + */ +@TableName("pay_order") +@KeySequence("pay_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderDO extends BaseDO { + + /** + * 订单编号,数据库自增 + */ + private Long id; + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + /** + * 渠道编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String channelCode; + + // ========== 商户相关字段 ========== + + /** + * 商户订单编号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantOrderId; + /** + * 商品标题 + */ + private String subject; + /** + * 商品描述信息 + */ + private String body; + /** + * 异步通知地址 + */ + private String notifyUrl; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + private Integer price; + /** + * 渠道手续费,单位:百分比 + * + * 冗余 {@link PayChannelDO#getFeeRate()} + */ + private Double channelFeeRate; + /** + * 渠道手续金额,单位:分 + */ + private Integer channelFeePrice; + /** + * 支付状态 + * + * 枚举 {@link PayOrderStatusEnum} + */ + private Integer status; + /** + * 用户 IP + */ + private String userIp; + /** + * 订单失效时间 + */ + private LocalDateTime expireTime; + /** + * 订单支付成功时间 + */ + private LocalDateTime successTime; + /** + * 支付成功的订单拓展单编号 + * + * 关联 {@link PayOrderExtensionDO#getId()} + */ + private Long extensionId; + /** + * 支付成功的外部订单号 + * + * 关联 {@link PayOrderExtensionDO#getNo()} + */ + private String no; + + // ========== 退款相关字段 ========== + /** + * 退款总金额,单位:分 + */ + private Integer refundPrice; + + // ========== 渠道相关字段 ========== + /** + * 渠道用户编号 + * + * 例如说,微信 openid、支付宝账号 + */ + private String channelUserId; + /** + * 渠道订单号 + */ + private String channelOrderNo; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java new file mode 100644 index 000000000..9466243e1 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.pay.dal.dataobject.order; + +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +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 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.util.Map; + +/** + * 支付订单拓展 DO + * + * 每次调用支付渠道,都会生成一条对应记录 + * + * @author 芋道源码 + */ +@TableName(value = "pay_order_extension",autoResultMap = true) +@KeySequence("pay_order_extension_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderExtensionDO extends BaseDO { + + /** + * 订单拓展编号,数据库自增 + */ + private Long id; + /** + * 外部订单号,根据规则生成 + * + * 调用支付渠道时,使用该字段作为对接的订单号: + * 1. 微信支付:对应 JSAPI 支付 的 out_trade_no 字段 + * 2. 支付宝支付:对应 电脑网站支付 的 out_trade_no 字段 + * + * 例如说,P202110132239124200055 + */ + private String no; + /** + * 订单号 + * + * 关联 {@link PayOrderDO#getId()} + */ + private Long orderId; + /** + * 渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + /** + * 渠道编码 + */ + private String channelCode; + /** + * 用户 IP + */ + private String userIp; + /** + * 支付状态 + * + * 枚举 {@link PayOrderStatusEnum} + */ + private Integer status; + /** + * 支付渠道的额外参数 + * + * 参见 参数说明 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map channelExtras; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + /** + * 支付渠道的同步/异步通知的内容 + * + * 对应 {@link PayOrderRespDTO#getRawData()} + */ + private String channelNotifyData; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java new file mode 100644 index 000000000..5d9c612a3 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.module.pay.dal.dataobject.refund; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +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.refund.PayRefundStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 支付退款单 DO + * 一个支付订单,可以拥有多个支付退款单 + * + * 即 PayOrderDO : PayRefundDO = 1 : n + * + * @author 芋道源码 + */ +@TableName("pay_refund") +@KeySequence("pay_refund_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayRefundDO extends BaseDO { + + /** + * 退款单编号,数据库自增 + */ + @TableId + private Long id; + /** + * 外部退款号,根据规则生成 + * + * 调用支付渠道时,使用该字段作为对接的退款号: + * 1. 微信退款:对应 申请退款 的 out_refund_no 字段 + * 2. 支付宝退款:对应 的 out_request_no 字段 + */ + private String no; + + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + /** + * 商户编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String channelCode; + /** + * 订单编号 + * + * 关联 {@link PayOrderDO#getId()} + */ + private Long orderId; + /** + * 支付订单编号 + * + * 冗余 {@link PayOrderDO#getNo()} + */ + private String orderNo; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantOrderId; + /** + * 商户退款订单号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantRefundId; + /** + * 异步通知地址 + */ + private String notifyUrl; + + // ========== 退款相关字段 ========== + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusEnum} + */ + private Integer status; + + /** + * 支付金额,单位:分 + */ + private Integer payPrice; + /** + * 退款金额,单位:分 + */ + private Integer refundPrice; + + /** + * 退款原因 + */ + private String reason; + + /** + * 用户 IP + */ + private String userIp; + + // ========== 渠道相关字段 ========== + /** + * 渠道订单号 + * + * 冗余 {@link PayOrderDO#getChannelOrderNo()} + */ + private String channelOrderNo; + /** + * 渠道退款单号 + * + * 1. 微信退款:对应 申请退款 的 refund_id 字段 + * 2. 支付宝退款:没有字段 + */ + private String channelRefundNo; + /** + * 退款成功时间 + */ + private LocalDateTime successTime; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道的错误提示 + */ + private String channelErrorMsg; + + /** + * 支付渠道的同步/异步通知的内容 + * + * 对应 {@link PayRefundRespDTO#getRawData()} + */ + private String channelNotifyData; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/app/PayAppMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/app/PayAppMapper.java new file mode 100644 index 000000000..c31dba551 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/app/PayAppMapper.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.app; + +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.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PayAppMapper extends BaseMapperX { + + default PageResult selectPage(PayAppPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(PayAppDO::getName, reqVO.getName()) + .eqIfPresent(PayAppDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayAppDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayAppDO::getId)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/channel/PayChannelMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/channel/PayChannelMapper.java new file mode 100644 index 000000000..acc28eeb6 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/channel/PayChannelMapper.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.channel; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PayChannelMapper extends BaseMapperX { + + default PayChannelDO selectByAppIdAndCode(Long appId, String code) { + return selectOne(PayChannelDO::getAppId, appId, PayChannelDO::getCode, code); + } + + default List selectListByAppIds(Collection appIds){ + return selectList(PayChannelDO::getAppId, appIds); + } + + default List selectListByAppId(Long appId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(PayChannelDO::getAppId, appId) + .eq(PayChannelDO::getStatus, status)); + } + + @Select("SELECT COUNT(*) FROM pay_channel WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/demo/PayDemoOrderMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/demo/PayDemoOrderMapper.java new file mode 100644 index 000000000..0a92c6b76 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/demo/PayDemoOrderMapper.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.demo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +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.dal.dataobject.demo.PayDemoOrderDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 示例订单 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface PayDemoOrderMapper extends BaseMapperX { + + default PageResult selectPage(PageParam reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .orderByDesc(PayDemoOrderDO::getId)); + } + + default int updateByIdAndPayed(Long id, boolean wherePayed, PayDemoOrderDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(PayDemoOrderDO::getId, id).eq(PayDemoOrderDO::getPayStatus, wherePayed)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/member/MemberWalletMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/member/MemberWalletMapper.java new file mode 100644 index 000000000..46e80df8e --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/member/MemberWalletMapper.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.member; + + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.pay.dal.dataobject.member.MemberWalletDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MemberWalletMapper extends BaseMapperX { + +} + + + + diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/member/MemberWalletTransactionMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/member/MemberWalletTransactionMapper.java new file mode 100644 index 000000000..a6cb541d1 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/member/MemberWalletTransactionMapper.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.member; + + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.pay.dal.dataobject.member.MemberWalletTransactionDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MemberWalletTransactionMapper extends BaseMapperX { + +} + + + + diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/notify/PayNotifyLogMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/notify/PayNotifyLogMapper.java new file mode 100644 index 000000000..8b586dff2 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/notify/PayNotifyLogMapper.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.notify; + +import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PayNotifyLogMapper extends BaseMapperX { + + default List selectListByTaskId(Long taskId) { + return selectList(PayNotifyLogDO::getTaskId, taskId); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java new file mode 100644 index 000000000..cc7701271 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.notify; + +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.notify.vo.PayNotifyTaskPageReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface PayNotifyTaskMapper extends BaseMapperX { + + /** + * 获得需要通知的 PayNotifyTaskDO 记录。需要满足如下条件: + * + * 1. status 非成功 + * 2. nextNotifyTime 小于当前时间 + * + * @return PayTransactionNotifyTaskDO 数组 + */ + default List selectListByNotify() { + return selectList(new LambdaQueryWrapper() + .in(PayNotifyTaskDO::getStatus, PayNotifyStatusEnum.WAITING.getStatus(), + PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()) + .le(PayNotifyTaskDO::getNextNotifyTime, LocalDateTime.now())); + } + + default PageResult selectPage(PayNotifyTaskPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayNotifyTaskDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayNotifyTaskDO::getType, reqVO.getType()) + .eqIfPresent(PayNotifyTaskDO::getDataId, reqVO.getDataId()) + .eqIfPresent(PayNotifyTaskDO::getStatus, reqVO.getStatus()) + .eqIfPresent(PayNotifyTaskDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .betweenIfPresent(PayNotifyTaskDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayNotifyTaskDO::getId)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java new file mode 100755 index 000000000..8513c4b31 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderExtensionMapper.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.order; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface PayOrderExtensionMapper extends BaseMapperX { + + default PayOrderExtensionDO selectByNo(String no) { + return selectOne(PayOrderExtensionDO::getNo, no); + } + + default int updateByIdAndStatus(Long id, Integer status, PayOrderExtensionDO update) { + return update(update, new LambdaQueryWrapper() + .eq(PayOrderExtensionDO::getId, id).eq(PayOrderExtensionDO::getStatus, status)); + } + + default List selectListByOrderId(Long orderId) { + return selectList(PayOrderExtensionDO::getOrderId, orderId); + } + + default List selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime) { + return selectList(new LambdaQueryWrapper() + .eq(PayOrderExtensionDO::getStatus, status) + .ge(PayOrderExtensionDO::getCreateTime, minCreateTime)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderMapper.java new file mode 100755 index 000000000..95510d5f7 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/order/PayOrderMapper.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.order; + +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.order.vo.PayOrderExportReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface PayOrderMapper extends BaseMapperX { + + default PageResult selectPage(PayOrderPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayOrderDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayOrderDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayOrderDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayOrderDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(PayOrderDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayOrderDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayOrderDO::getId)); + } + + default List selectList(PayOrderExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(PayOrderDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayOrderDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayOrderDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayOrderDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(PayOrderDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayOrderDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayOrderDO::getId)); + } + + default Long selectCountByAppId(Long appId) { + return selectCount(PayOrderDO::getAppId, appId); + } + + default PayOrderDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) { + return selectOne(PayOrderDO::getAppId, appId, + PayOrderDO::getMerchantOrderId, merchantOrderId); + } + + default int updateByIdAndStatus(Long id, Integer status, PayOrderDO update) { + return update(update, new LambdaQueryWrapper() + .eq(PayOrderDO::getId, id).eq(PayOrderDO::getStatus, status)); + } + + default List selectListByStatusAndExpireTimeLt(Integer status, LocalDateTime expireTime) { + return selectList(new LambdaQueryWrapper() + .eq(PayOrderDO::getStatus, status) + .lt(PayOrderDO::getExpireTime, expireTime)); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java new file mode 100755 index 000000000..af5fad77b --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.refund; + +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.refund.vo.PayRefundExportReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PayRefundMapper extends BaseMapperX { + + default Long selectCountByAppId(Long appId) { + return selectCount(PayRefundDO::getAppId, appId); + } + + default PayRefundDO selectByAppIdAndMerchantRefundId(Long appId, String merchantRefundId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getMerchantRefundId, merchantRefundId)); + } + + default Long selectCountByAppIdAndOrderId(Long appId, Long orderId, Integer status) { + return selectCount(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getOrderId, orderId) + .eq(PayRefundDO::getStatus, status)); + } + + default PayRefundDO selectByAppIdAndNo(Long appId, String no) { + return selectOne(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getNo, no)); + } + + default int updateByIdAndStatus(Long id, Integer status, PayRefundDO update) { + return update(update, new LambdaQueryWrapper() + .eq(PayRefundDO::getId, id).eq(PayRefundDO::getStatus, status)); + } + + default PageResult selectPage(PayRefundPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayRefundDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayRefundDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayRefundDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayRefundDO::getMerchantRefundId, reqVO.getMerchantRefundId()) + .likeIfPresent(PayRefundDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayRefundDO::getChannelRefundNo, reqVO.getChannelRefundNo()) + .eqIfPresent(PayRefundDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayRefundDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayRefundDO::getId)); + } + + default List selectList(PayRefundExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(PayRefundDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayRefundDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayRefundDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayRefundDO::getMerchantRefundId, reqVO.getMerchantRefundId()) + .likeIfPresent(PayRefundDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayRefundDO::getChannelRefundNo, reqVO.getChannelRefundNo()) + .eqIfPresent(PayRefundDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayRefundDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayRefundDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(PayRefundDO::getStatus, status); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java new file mode 100644 index 000000000..f4abe7032 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pay.dal.redis; + +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import org.redisson.api.RLock; + +/** + * 支付 Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + RedisKeyDefine PAY_NOTIFY_LOCK = new RedisKeyDefine("通知任务的分布式锁", + "pay_notify:lock:%d", // 参数来自 DefaultLockKeyBuilder 类 + RedisKeyDefine.KeyTypeEnum.HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构 + + /** + * 支付序号的缓存 + * + * KEY 格式:pay_no:{prefix} + * VALUE 数据格式:编号自增 + */ + String PAY_NO = "pay_no"; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/no/PayNoRedisDAO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/no/PayNoRedisDAO.java new file mode 100644 index 000000000..7ce6caadc --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/no/PayNoRedisDAO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.pay.dal.redis.no; + +import cn.hutool.core.date.DatePattern;import cn.hutool.core.date.DateUtil;import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource;import java.time.LocalDateTime; + +/** + * 支付序号的 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class PayNoRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 生成序号 + * + * @param prefix 前缀 + * @return 序号 + */ + public String generate(String prefix) { + String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN); + Long no = stringRedisTemplate.opsForValue().increment(noPrefix); + return noPrefix + no; + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/notify/PayNotifyLockRedisDAO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/notify/PayNotifyLockRedisDAO.java new file mode 100644 index 000000000..2d1f13f16 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/notify/PayNotifyLockRedisDAO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.pay.dal.redis.notify; + +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.module.pay.dal.redis.RedisKeyConstants.PAY_NOTIFY_LOCK; + +/** + * 支付通知的锁 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class PayNotifyLockRedisDAO { + + @Resource + private RedissonClient redissonClient; + + public void lock(Long id, Long timeoutMillis, Runnable runnable) { + String lockKey = formatKey(id); + RLock lock = redissonClient.getLock(lockKey); + try { + lock.lock(timeoutMillis, TimeUnit.MILLISECONDS); + // 执行逻辑 + runnable.run(); + } finally { + lock.unlock(); + } + } + + private static String formatKey(Long id) { + return String.format(PAY_NOTIFY_LOCK.getKeyTemplate(), id); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/job/config/PayJobConfiguration.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/job/config/PayJobConfiguration.java new file mode 100644 index 000000000..cd6bd0bb7 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/job/config/PayJobConfiguration.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.pay.framework.job.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ThreadPoolExecutor; + +@Configuration(proxyBeanMethods = false) +public class PayJobConfiguration { + + public static final String NOTIFY_THREAD_POOL_TASK_EXECUTOR = "NOTIFY_THREAD_POOL_TASK_EXECUTOR"; + + @Bean(NOTIFY_THREAD_POOL_TASK_EXECUTOR) + public ThreadPoolTaskExecutor notifyThreadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(8); // 设置核心线程数 + executor.setMaxPoolSize(16); // 设置最大线程数 + executor.setKeepAliveSeconds(60); // 设置空闲时间 + executor.setQueueCapacity(100); // 设置队列大小 + executor.setThreadNamePrefix("notify-task-"); // 配置线程池的前缀 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 进行加载 + executor.initialize(); + return executor; + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/job/core/package-info.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/job/core/package-info.java new file mode 100644 index 000000000..5a5a689f8 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/job/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.pay.framework.job.core; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/package-info.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/package-info.java new file mode 100644 index 000000000..a1286828e --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 pay 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.pay.framework; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayConfiguration.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayConfiguration.java new file mode 100644 index 000000000..376cd0acb --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayConfiguration.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.module.pay.framework.pay.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(PayProperties.class) +public class PayConfiguration { +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java new file mode 100644 index 000000000..d422b3528 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.pay.framework.pay.config; + +import lombok.Data; +import org.hibernate.validator.constraints.URL; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; + +@ConfigurationProperties(prefix = "yudao.pay") +@Validated +@Data +public class PayProperties { + + private static final String ORDER_NO_PREFIX = "P"; + private static final String REFUND_NO_PREFIX = "R"; + + /** + * 支付回调地址 + * + * 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL + * + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址 + */ + @NotEmpty(message = "支付回调地址不能为空") + @URL(message = "支付回调地址的格式必须是 URL") + private String orderNotifyUrl; + + /** + * 退款回调地址 + * + * 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL + * + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址 + */ + @NotEmpty(message = "支付回调地址不能为空") + @URL(message = "支付回调地址的格式必须是 URL") + private String refundNotifyUrl; + + /** + * 支付订单 no 的前缀 + */ + @NotEmpty(message = "支付订单 no 的前缀不能为空") + private String orderNoPrefix = ORDER_NO_PREFIX; + + /** + * 退款订单 no 的前缀 + */ + @NotEmpty(message = "退款订单 no 的前缀不能为空") + private String refundNoPrefix = REFUND_NO_PREFIX; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/package-info.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/package-info.java new file mode 100644 index 000000000..07f837ff5 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,无实际作用 + */ +package cn.iocoder.yudao.module.pay.framework.pay.core; \ No newline at end of file diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/security/config/SecurityConfiguration.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/security/config/SecurityConfiguration.java new file mode 100644 index 000000000..b9c06cb9c --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.pay.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * Pay 模块的 Security 配置 + */ +@Configuration("paySecurityConfiguration") +public class SecurityConfiguration { + + @Bean("payAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // Swagger 接口文档 + registry.antMatchers("/v3/api-docs/**").permitAll() // 元数据 + .antMatchers("/swagger-ui.html").permitAll(); // Swagger UI + // Spring Boot Actuator 的安全配置 + registry.antMatchers("/actuator").anonymous() + .antMatchers("/actuator/**").anonymous(); + // Druid 监控 + registry.antMatchers("/druid/**").anonymous(); + } + + }; + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/security/core/package-info.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/security/core/package-info.java new file mode 100644 index 000000000..12c1c533d --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.pay.framework.security.core; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/web/config/PayWebConfiguration.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/web/config/PayWebConfiguration.java new file mode 100644 index 000000000..febc2bc8a --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/web/config/PayWebConfiguration.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.pay.framework.web.config; + +import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * pay 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class PayWebConfiguration { + + /** + * pay 模块的 API 分组 + */ + @Bean + public GroupedOpenApi payGroupedOpenApi() { + return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("pay"); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/web/package-info.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/web/package-info.java new file mode 100644 index 000000000..03bfad978 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * pay 模块的 web 配置 + */ +package cn.iocoder.yudao.module.pay.framework.web; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java new file mode 100644 index 000000000..5a961ea57 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.pay.job.notify; + +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 支付通知 Job + * 通过不断扫描待通知的 PayNotifyTaskDO 记录,回调业务线的回调接口 + * + * @author 芋道源码 + */ +@Component +@TenantJob // 多租户 +@Slf4j +public class PayNotifyJob { + + @Resource + private PayNotifyService payNotifyService; + + @XxlJob("payNotifyJob") + public void execute() throws Exception { + int notifyCount = payNotifyService.executeNotify(); + log.info("[execute][执行支付通知 ({}) 个]", notifyCount); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderExpireJob.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderExpireJob.java new file mode 100644 index 000000000..39fbc323d --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderExpireJob.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.pay.job.order; + +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 支付订单的过期 Job + * + * 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。 + * + * @author 芋道源码 + */ +@Component +@TenantJob +@Slf4j +public class PayOrderExpireJob { + + @Resource + private PayOrderService orderService; + + @XxlJob("payOrderExpireJob") + public void execute(String param) { + int count = orderService.expireOrder(); + log.info("[execute][支付过期 ({}) 个]", count); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderSyncJob.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderSyncJob.java new file mode 100644 index 000000000..7cff73fcd --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/order/PayOrderSyncJob.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pay.job.order; + +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * 支付订单的同步 Job + * + * 由于支付订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 + * + * @author 芋道源码 + */ +@Component +@TenantJob +@Slf4j +public class PayOrderSyncJob { + + /** + * 同步创建时间在 N 分钟之前的订单 + * + * 为什么同步 10 分钟之前的订单? + * 因为一个订单发起支付,到支付成功,大多数在 10 分钟内,需要保证轮询到。 + * 如果设置为 30、60 或者更大时间范围,会导致轮询的订单太多,影响性能。当然,你也可以根据自己的业务情况来处理。 + */ + private static final Duration CREATE_TIME_DURATION_BEFORE = Duration.ofMinutes(10); + + @Resource + private PayOrderService orderService; + + @XxlJob("payOrderSyncJob") + public void execute() { + LocalDateTime minCreateTime = LocalDateTime.now().minus(CREATE_TIME_DURATION_BEFORE); + int count = orderService.syncOrder(minCreateTime); + log.info("[execute][同步支付订单 ({}) 个]", count); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/refund/PayRefundSyncJob.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/refund/PayRefundSyncJob.java new file mode 100644 index 000000000..38a113de1 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/refund/PayRefundSyncJob.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.pay.job.refund; + +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 退款订单的同步 Job + * + * 由于退款订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 + * + * @author 芋道源码 + */ +@Component +@TenantJob +@Slf4j +public class PayRefundSyncJob { + + @Resource + private PayRefundService refundService; + + @XxlJob("payRefundSyncJob") + public void execute() { + int count = refundService.syncRefund(); + log.info("[execute][同步退款订单 ({}) 个]", count); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/package-info.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/package-info.java new file mode 100644 index 000000000..40ab66da0 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/package-info.java @@ -0,0 +1,10 @@ +/** + * pay 模块,我们放支付业务,提供业务的支付能力。 + * 例如说:商户、应用、支付、退款等等 + * + * 1. Controller URL:以 /pay/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 pay_ 开头,方便在数据库中区分 + * + * 注意,由于 Pay 模块和 Trade 模块,容易重名,所以类名都加载 Pay 的前缀~ + */ +package cn.iocoder.yudao.module.pay; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java new file mode 100644 index 000000000..4f9e289d6 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.module.pay.service.app; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 支付应用 Service 接口 + * + * @author 芋艿 + */ +public interface PayAppService { + + /** + * 创建支付应用 + * + * @param createReqVO 创建 + * @return 编号 + */ + Long createApp(@Valid PayAppCreateReqVO createReqVO); + + /** + * 更新支付应用 + * + * @param updateReqVO 更新 + */ + void updateApp(@Valid PayAppUpdateReqVO updateReqVO); + + /** + * 修改应用状态 + * + * @param id 应用编号 + * @param status 状态 + */ + void updateAppStatus(Long id, Integer status); + + /** + * 删除支付应用 + * + * @param id 编号 + */ + void deleteApp(Long id); + + /** + * 获得支付应用 + * + * @param id 编号 + * @return 支付应用 + */ + PayAppDO getApp(Long id); + + /** + * 获得支付应用列表 + * + * @param ids 编号 + * @return 支付应用列表 + */ + List getAppList(Collection ids); + + /** + * 获得支付应用列表 + * + * @return 支付应用列表 + */ + List getAppList(); + + /** + * 获得支付应用分页 + * + * @param pageReqVO 分页查询 + * @return 支付应用分页 + */ + PageResult getAppPage(PayAppPageReqVO pageReqVO); + + /** + * 获得指定编号的商户 Map + * + * @param ids 应用编号集合 + * @return 商户 Map + */ + default Map getAppMap(Collection ids) { + List list = getAppList(ids); + return CollectionUtils.convertMap(list, PayAppDO::getId); + } + + /** + * 支付应用的合法性 + * + * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param id 应用编号 + * @return 应用 + */ + PayAppDO validPayApp(Long id); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java new file mode 100644 index 000000000..56087ef13 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.pay.service.app; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import cn.iocoder.yudao.module.pay.convert.app.PayAppConvert; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +import cn.iocoder.yudao.module.pay.dal.mysql.app.PayAppMapper; +import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; + +/** + * 支付应用 Service 实现类 + * + * @author aquan + */ +@Service +@Validated +public class PayAppServiceImpl implements PayAppService { + + @Resource + private PayAppMapper appMapper; + + @Resource + @Lazy // 延迟加载,避免循环依赖报错 + private PayOrderService orderService; + @Resource + @Lazy // 延迟加载,避免循环依赖报错 + private PayRefundService refundService; + + @Override + public Long createApp(PayAppCreateReqVO createReqVO) { + // 插入 + PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO); + appMapper.insert(app); + // 返回 + return app.getId(); + } + + @Override + public void updateApp(PayAppUpdateReqVO updateReqVO) { + // 校验存在 + validateAppExists(updateReqVO.getId()); + // 更新 + PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO); + appMapper.updateById(updateObj); + } + + @Override + public void updateAppStatus(Long id, Integer status) { + // 校验商户存在 + validateAppExists(id); + // 更新状态 + appMapper.updateById(new PayAppDO().setId(id).setStatus(status)); + } + + @Override + public void deleteApp(Long id) { + // 校验存在 + validateAppExists(id); + // 校验关联数据是否存在 + if (orderService.getOrderCountByAppId(id) > 0) { + throw exception(APP_EXIST_ORDER_CANT_DELETE); + } + if (refundService.getRefundCountByAppId(id) > 0) { + throw exception(APP_EXIST_REFUND_CANT_DELETE); + } + + // 删除 + appMapper.deleteById(id); + } + + private void validateAppExists(Long id) { + if (appMapper.selectById(id) == null) { + throw exception(APP_NOT_FOUND); + } + } + + @Override + public PayAppDO getApp(Long id) { + return appMapper.selectById(id); + } + + @Override + public List getAppList(Collection ids) { + return appMapper.selectBatchIds(ids); + } + + @Override + public List getAppList() { + return appMapper.selectList(); + } + + @Override + public PageResult getAppPage(PayAppPageReqVO pageReqVO) { + return appMapper.selectPage(pageReqVO); + } + + @Override + public PayAppDO validPayApp(Long id) { + PayAppDO app = appMapper.selectById(id); + // 校验是否存在 + if (app == null) { + throw exception(ErrorCodeConstants.APP_NOT_FOUND); + } + // 校验是否禁用 + if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) { + throw exception(ErrorCodeConstants.APP_IS_DISABLE); + } + return app; + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelService.java new file mode 100644 index 000000000..1ec50e989 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelService.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.pay.service.channel; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +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.channel.PayChannelDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 支付渠道 Service 接口 + * + * @author aquan + */ +public interface PayChannelService { + + /** + * 创建支付渠道 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createChannel(@Valid PayChannelCreateReqVO createReqVO); + + /** + * 更新支付渠道 + * + * @param updateReqVO 更新信息 + */ + void updateChannel(@Valid PayChannelUpdateReqVO updateReqVO); + + /** + * 删除支付渠道 + * + * @param id 编号 + */ + void deleteChannel(Long id); + + /** + * 获得支付渠道 + * + * @param id 编号 + * @return 支付渠道 + */ + PayChannelDO getChannel(Long id); + + /** + * 根据支付应用 ID 集合,获得支付渠道列表 + * + * @param appIds 应用编号集合 + * @return 支付渠道列表 + */ + List getChannelListByAppIds(Collection appIds); + + /** + * 根据条件获取渠道 + * + * @param appId 应用编号 + * @param code 渠道编码 + * @return 数量 + */ + PayChannelDO getChannelByAppIdAndCode(Long appId, String code); + + /** + * 支付渠道的合法性 + * + * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param id 渠道编号 + * @return 渠道信息 + */ + PayChannelDO validPayChannel(Long id); + + /** + * 支付渠道的合法性 + * + * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param appId 应用编号 + * @param code 支付渠道 + * @return 渠道信息 + */ + PayChannelDO validPayChannel(Long appId, String code); + + /** + * 获得指定应用的开启的渠道列表 + * + * @param appId 应用编号 + * @return 渠道列表 + */ + List getEnableChannelList(Long appId); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java new file mode 100644 index 000000000..3eb996c4a --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java @@ -0,0 +1,216 @@ +package cn.iocoder.yudao.module.pay.service.channel; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +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.convert.channel.PayChannelConvert; +import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; +import cn.iocoder.yudao.module.pay.dal.mysql.channel.PayChannelMapper; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import javax.validation.Validator; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; + +/** + * 支付渠道 Service 实现类 + * + * @author aquan + */ +@Service +@Slf4j +@Validated +public class PayChannelServiceImpl implements PayChannelService { + + @Getter // 为了方便测试,这里提供 getter 方法 + @Setter + private volatile List channelCache; + + @Resource + private PayClientFactory payClientFactory; + + @Resource + private PayChannelMapper channelMapper; + + @Resource + private Validator validator; + + /** + * 初始化 {@link #payClientFactory} 缓存 + */ + @PostConstruct + public void initLocalCache() { + // 注意:忽略自动多租户,因为要全局初始化缓存 + TenantUtils.executeIgnore(() -> { + // 第一步:查询数据 + List channels = channelMapper.selectList(); + log.info("[initLocalCache][缓存支付渠道,数量为:{}]", channels.size()); + + // 第二步:构建缓存:创建或更新支付 Client + channels.forEach(payChannel -> payClientFactory.createOrUpdatePayClient(payChannel.getId(), + payChannel.getCode(), payChannel.getConfig())); + this.channelCache = channels; + }); + } + + /** + * 通过定时任务轮询,刷新缓存 + * + * 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新 + */ + @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) + public void refreshLocalCache() { + // 注意:忽略自动多租户,因为要全局初始化缓存 + TenantUtils.executeIgnore(() -> { + // 情况一:如果缓存里没有数据,则直接刷新缓存 + if (CollUtil.isEmpty(channelCache)) { + initLocalCache(); + return; + } + + // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存 + LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime); + if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) { + initLocalCache(); + } + }); + } + + @Override + public Long createChannel(PayChannelCreateReqVO reqVO) { + // 断言是否有重复的 + PayChannelDO dbChannel = getChannelByAppIdAndCode(reqVO.getAppId(), reqVO.getCode()); + if (dbChannel != null) { + throw exception(CHANNEL_EXIST_SAME_CHANNEL_ERROR); + } + + // 新增渠道 + PayChannelDO channel = PayChannelConvert.INSTANCE.convert(reqVO) + .setConfig(parseConfig(reqVO.getCode(), reqVO.getConfig())); + channelMapper.insert(channel); + + // 刷新缓存 + initLocalCache(); + return channel.getId(); + } + + @Override + public void updateChannel(PayChannelUpdateReqVO updateReqVO) { + // 校验存在 + PayChannelDO dbChannel = validateChannelExists(updateReqVO.getId()); + + // 更新 + PayChannelDO channel = PayChannelConvert.INSTANCE.convert(updateReqVO) + .setConfig(parseConfig(dbChannel.getCode(), updateReqVO.getConfig())); + channelMapper.updateById(channel); + + // 刷新缓存 + initLocalCache(); + } + + /** + * 解析并校验配置 + * + * @param code 渠道编码 + * @param configStr 配置 + * @return 支付配置 + */ + private PayClientConfig parseConfig(String code, String configStr) { + // 解析配置 + Class payClass = PayChannelEnum.getByCode(code).getConfigClass(); + if (ObjectUtil.isNull(payClass)) { + throw exception(CHANNEL_NOT_FOUND); + } + PayClientConfig config = JsonUtils.parseObject2(configStr, payClass); + Assert.notNull(config); + + // 验证参数 + config.validate(validator); + return config; + } + + @Override + public void deleteChannel(Long id) { + // 校验存在 + validateChannelExists(id); + + // 删除 + channelMapper.deleteById(id); + + // 刷新缓存 + initLocalCache(); + } + + private PayChannelDO validateChannelExists(Long id) { + PayChannelDO channel = channelMapper.selectById(id); + if (channel == null) { + throw exception(CHANNEL_NOT_FOUND); + } + return channel; + } + + @Override + public PayChannelDO getChannel(Long id) { + return channelMapper.selectById(id); + } + + @Override + public List getChannelListByAppIds(Collection appIds) { + return channelMapper.selectListByAppIds(appIds); + } + + @Override + public PayChannelDO getChannelByAppIdAndCode(Long appId, String code) { + return channelMapper.selectByAppIdAndCode(appId, code); + } + + @Override + public PayChannelDO validPayChannel(Long id) { + PayChannelDO channel = channelMapper.selectById(id); + validPayChannel(channel); + return channel; + } + + @Override + public PayChannelDO validPayChannel(Long appId, String code) { + PayChannelDO channel = channelMapper.selectByAppIdAndCode(appId, code); + validPayChannel(channel); + return channel; + } + + private void validPayChannel(PayChannelDO channel) { + if (channel == null) { + throw exception(CHANNEL_NOT_FOUND); + } + if (CommonStatusEnum.DISABLE.getStatus().equals(channel.getStatus())) { + throw exception(CHANNEL_IS_DISABLE); + } + } + + @Override + public List getEnableChannelList(Long appId) { + return channelMapper.selectListByAppId(appId, CommonStatusEnum.ENABLE.getStatus()); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java new file mode 100644 index 000000000..e6822e626 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.pay.service.demo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO; + +import javax.validation.Valid; + +/** + * 示例订单 Service 接口 + * + * @author 芋道源码 + */ +public interface PayDemoOrderService { + + /** + * 创建示例订单 + * + * @param userId 用户编号 + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemoOrder(Long userId, @Valid PayDemoOrderCreateReqVO createReqVO); + + /** + * 获得示例订单 + * + * @param id 编号 + * @return 示例订单 + */ + PayDemoOrderDO getDemoOrder(Long id); + + /** + * 获得示例订单分页 + * + * @param pageReqVO 分页查询 + * @return 示例订单分页 + */ + PageResult getDemoOrderPage(PageParam pageReqVO); + + /** + * 更新示例订单为已支付 + * + * @param id 编号 + * @param payOrderId 支付订单号 + */ + void updateDemoOrderPaid(Long id, Long payOrderId); + + /** + * 发起示例订单的退款 + * + * @param id 编号 + * @param userIp 用户编号 + */ + void refundDemoOrder(Long id, String userIp); + + /** + * 更新示例订单为已退款 + * + * @param id 编号 + * @param payRefundId 退款订单号 + */ + void updateDemoOrderRefunded(Long id, Long payRefundId); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java new file mode 100644 index 000000000..f5ebf7961 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java @@ -0,0 +1,265 @@ +package cn.iocoder.yudao.module.pay.service.demo; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; +import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO; +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoOrderMapper; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static cn.hutool.core.util.ObjectUtil.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; + +/** + * 示例订单 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class PayDemoOrderServiceImpl implements PayDemoOrderService { + + /** + * 接入的实力应用编号 + * + * 从 [支付管理 -> 应用信息] 里添加 + */ + private static final Long PAY_APP_ID = 7L; + + /** + * 商品信息 Map + * + * key:商品编号 + * value:[商品名、商品价格] + */ + private final Map spuNames = new HashMap<>(); + + @Resource + private PayOrderApi payOrderApi; + @Resource + private PayRefundApi payRefundApi; + + @Resource + private PayDemoOrderMapper payDemoOrderMapper; + + public PayDemoOrderServiceImpl() { + spuNames.put(1L, new Object[]{"华为手机", 1}); + spuNames.put(2L, new Object[]{"小米电视", 10}); + spuNames.put(3L, new Object[]{"苹果手表", 100}); + spuNames.put(4L, new Object[]{"华硕笔记本", 1000}); + spuNames.put(5L, new Object[]{"蔚来汽车", 200000}); + } + + @Override + public Long createDemoOrder(Long userId, PayDemoOrderCreateReqVO createReqVO) { + // 1.1 获得商品 + Object[] spu = spuNames.get(createReqVO.getSpuId()); + Assert.notNull(spu, "商品({}) 不存在", createReqVO.getSpuId()); + String spuName = (String) spu[0]; + Integer price = (Integer) spu[1]; + // 1.2 插入 demo 订单 + PayDemoOrderDO demoOrder = new PayDemoOrderDO().setUserId(userId) + .setSpuId(createReqVO.getSpuId()).setSpuName(spuName) + .setPrice(price).setPayStatus(false).setRefundPrice(0); + payDemoOrderMapper.insert(demoOrder); + + // 2.1 创建支付单 + Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO() + .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用 + .setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号 + .setSubject(spuName).setBody("").setPrice(price) // 价格信息 + .setExpireTime(addTime(Duration.ofHours(2L)))); // 支付的过期时间 + // 2.2 更新支付单到 demo 订单 + payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(demoOrder.getId()) + .setPayOrderId(payOrderId)); + // 返回 + return demoOrder.getId(); + } + + @Override + public PayDemoOrderDO getDemoOrder(Long id) { + return payDemoOrderMapper.selectById(id); + } + + @Override + public PageResult getDemoOrderPage(PageParam pageReqVO) { + return payDemoOrderMapper.selectPage(pageReqVO); + } + + @Override + public void updateDemoOrderPaid(Long id, Long payOrderId) { + // 校验并获得支付订单(可支付) + PayOrderRespDTO payOrder = validateDemoOrderCanPaid(id, payOrderId); + + // 更新 PayDemoOrderDO 状态为已支付 + int updateCount = payDemoOrderMapper.updateByIdAndPayed(id, false, + new PayDemoOrderDO().setPayStatus(true).setPayTime(LocalDateTime.now()) + .setPayChannelCode(payOrder.getChannelCode())); + if (updateCount == 0) { + throw exception(DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID); + } + } + + /** + * 校验交易订单满足被支付的条件 + * + * 1. 交易订单未支付 + * 2. 支付单已支付 + * + * @param id 交易订单编号 + * @param payOrderId 支付订单编号 + * @return 交易订单 + */ + private PayOrderRespDTO validateDemoOrderCanPaid(Long id, Long payOrderId) { + // 1.1 校验订单是否存在 + PayDemoOrderDO order = payDemoOrderMapper.selectById(id); + if (order == null) { + throw exception(DEMO_ORDER_NOT_FOUND); + } + // 1.2 校验订单未支付 + if (order.getPayStatus()) { + log.error("[validateDemoOrderCanPaid][order({}) 不处于待支付状态,请进行处理!order 数据是:{}]", + id, toJsonString(order)); + throw exception(DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID); + } + // 1.3 校验支付订单匹配 + if (notEqual(order.getPayOrderId(), payOrderId)) { // 支付单号 + log.error("[validateDemoOrderCanPaid][order({}) 支付单不匹配({}),请进行处理!order 数据是:{}]", + id, payOrderId, toJsonString(order)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + + // 2.1 校验支付单是否存在 + PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId); + if (payOrder == null) { + log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 不存在,请进行处理!]", id, payOrderId); + throw exception(ORDER_NOT_FOUND); + } + // 2.2 校验支付单已支付 + if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) { + log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 未支付,请进行处理!payOrder 数据是:{}]", + id, payOrderId, toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS); + } + // 2.3 校验支付金额一致 + if (notEqual(payOrder.getPrice(), order.getPrice())) { + log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 支付金额不匹配,请进行处理!order 数据是:{},payOrder 数据是:{}]", + id, payOrderId, toJsonString(order), toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH); + } + // 2.4 校验支付订单匹配(二次) + if (notEqual(payOrder.getMerchantOrderId(), id.toString())) { + log.error("[validateDemoOrderCanPaid][order({}) 支付单不匹配({}),请进行处理!payOrder 数据是:{}]", + id, payOrderId, toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + return payOrder; + } + + @Override + public void refundDemoOrder(Long id, String userIp) { + // 1. 校验订单是否可以退款 + PayDemoOrderDO order = validateDemoOrderCanRefund(id); + + // 2.1 生成退款单号 + // 一般来说,用户发起退款的时候,都会单独插入一个售后维权表,然后使用该表的 id 作为 refundId + // 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id + "-refund" 来演示 + String refundId = order.getId() + "-refund"; + // 2.2 创建退款单 + Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO() + .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用 + .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 + .setMerchantRefundId(refundId) + .setReason("想退钱").setPrice(order.getPrice()));// 价格信息 + // 2.3 更新退款单到 demo 订单 + payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id) + .setPayRefundId(payRefundId).setRefundPrice(order.getPrice())); + } + + private PayDemoOrderDO validateDemoOrderCanRefund(Long id) { + // 校验订单是否存在 + PayDemoOrderDO order = payDemoOrderMapper.selectById(id); + if (order == null) { + throw exception(DEMO_ORDER_NOT_FOUND); + } + // 校验订单是否支付 + if (!order.getPayStatus()) { + throw exception(DEMO_ORDER_REFUND_FAIL_NOT_PAID); + } + // 校验订单是否已退款 + if (order.getPayRefundId() != null) { + throw exception(DEMO_ORDER_REFUND_FAIL_REFUNDED); + } + return order; + } + + @Override + public void updateDemoOrderRefunded(Long id, Long payRefundId) { + // 1. 校验并获得退款订单(可退款) + PayRefundRespDTO payRefund = validateDemoOrderCanRefunded(id, payRefundId); + // 2.2 更新退款单到 demo 订单 + payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id) + .setRefundTime(payRefund.getSuccessTime())); + } + + private PayRefundRespDTO validateDemoOrderCanRefunded(Long id, Long payRefundId) { + // 1.1 校验示例订单 + PayDemoOrderDO order = payDemoOrderMapper.selectById(id); + if (order == null) { + throw exception(DEMO_ORDER_NOT_FOUND); + } + // 1.2 校验退款订单匹配 + if (Objects.equals(order.getPayOrderId(), payRefundId)) { + log.error("[validateDemoOrderCanRefunded][order({}) 退款单不匹配({}),请进行处理!order 数据是:{}]", + id, payRefundId, toJsonString(order)); + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + + // 2.1 校验退款订单 + PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId); + if (payRefund == null) { + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND); + } + // 2.2 + if (!PayRefundStatusEnum.isSuccess(payRefund.getStatus())) { + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS); + } + // 2.3 校验退款金额一致 + if (notEqual(payRefund.getRefundPrice(), order.getPrice())) { + log.error("[validateDemoOrderCanRefunded][order({}) payRefund({}) 退款金额不匹配,请进行处理!order 数据是:{},payRefund 数据是:{}]", + id, payRefundId, toJsonString(order), toJsonString(payRefund)); + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH); + } + // 2.4 校验退款订单匹配(二次) + if (notEqual(payRefund.getMerchantOrderId(), id.toString())) { + log.error("[validateDemoOrderCanRefunded][order({}) 退款单不匹配({}),请进行处理!payRefund 数据是:{}]", + id, payRefundId, toJsonString(payRefund)); + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + return payRefund; + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyService.java new file mode 100644 index 000000000..ed9399772 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyService.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.pay.service.notify; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO; + +import java.util.List; + +/** + * 回调通知 Service 接口 + * + * @author 芋道源码 + */ +public interface PayNotifyService { + + /** + * 创建回调通知任务 + * + * @param type 类型 + * @param dataId 数据编号 + */ + void createPayNotifyTask(Integer type, Long dataId); + + /** + * 执行回调通知 + * + * 注意,该方法提供给定时任务调用。目前是 yudao-server 进行调用 + * @return 通知数量 + */ + int executeNotify() throws InterruptedException; + + /** + * 获得回调通知 + * + * @param id 编号 + * @return 回调通知 + */ + PayNotifyTaskDO getNotifyTask(Long id); + + /** + * 获得回调通知分页 + * + * @param pageReqVO 分页查询 + * @return 回调通知分页 + */ + PageResult getNotifyTaskPage(PayNotifyTaskPageReqVO pageReqVO); + + /** + * 获得回调日志列表 + * + * @param taskId 任务编号 + * @return 日志列表 + */ + List getNotifyLogList(Long taskId); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java new file mode 100644 index 000000000..c85876ee7 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java @@ -0,0 +1,294 @@ +package cn.iocoder.yudao.module.pay.service.notify; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +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.refund.PayRefundDO; +import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyLogMapper; +import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper; +import cn.iocoder.yudao.module.pay.dal.redis.notify.PayNotifyLockRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime; +import static cn.iocoder.yudao.module.pay.framework.job.config.PayJobConfiguration.NOTIFY_THREAD_POOL_TASK_EXECUTOR; + +/** + * 支付通知 Core Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Valid +@Slf4j +public class PayNotifyServiceImpl implements PayNotifyService { + + /** + * 通知超时时间,单位:秒 + */ + public static final int NOTIFY_TIMEOUT = 120; + /** + * {@link #NOTIFY_TIMEOUT} 的毫秒 + */ + public static final long NOTIFY_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS; + + @Resource + @Lazy // 循环依赖,避免报错 + private PayOrderService orderService; + @Resource + @Lazy // 循环依赖,避免报错 + private PayRefundService refundService; + + @Resource + private PayNotifyTaskMapper notifyTaskMapper; + @Resource + private PayNotifyLogMapper notifyLogMapper; + + @Resource(name = NOTIFY_THREAD_POOL_TASK_EXECUTOR) + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + @Resource + private PayNotifyLockRedisDAO notifyLockCoreRedisDAO; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createPayNotifyTask(Integer type, Long dataId) { + PayNotifyTaskDO task = new PayNotifyTaskDO().setType(type).setDataId(dataId); + task.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setNextNotifyTime(LocalDateTime.now()) + .setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1); + // 补充 appId + notifyUrl 字段 + if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) { + PayOrderDO order = orderService.getOrder(task.getDataId()); // 不进行非空判断,有问题直接异常 + task.setAppId(order.getAppId()). + setMerchantOrderId(order.getMerchantOrderId()).setNotifyUrl(order.getNotifyUrl()); + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) { + PayRefundDO refundDO = refundService.getRefund(task.getDataId()); + task.setAppId(refundDO.getAppId()) + .setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl()); + } + + // 执行插入 + notifyTaskMapper.insert(task); + + // 必须在事务提交后,在发起任务,否则 PayNotifyTaskDO 还没入库,就提前回调接入的业务 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + executeNotify(task); + } + }); + } + + @Override + public int executeNotify() throws InterruptedException { + // 获得需要通知的任务 + List tasks = notifyTaskMapper.selectListByNotify(); + if (CollUtil.isEmpty(tasks)) { + return 0; + } + + // 遍历,逐个通知 + CountDownLatch latch = new CountDownLatch(tasks.size()); + tasks.forEach(task -> threadPoolTaskExecutor.execute(() -> { + try { + executeNotify(task); + } finally { + latch.countDown(); + } + })); + // 等待完成 + awaitExecuteNotify(latch); + // 返回执行完成的任务数(成功 + 失败) + return tasks.size(); + } + + /** + * 等待全部支付通知的完成 + * 每 1 秒会打印一次剩余任务数量 + * + * @param latch Latch + * @throws InterruptedException 如果被打断 + */ + private void awaitExecuteNotify(CountDownLatch latch) throws InterruptedException { + long size = latch.getCount(); + for (int i = 0; i < NOTIFY_TIMEOUT; i++) { + if (latch.await(1L, TimeUnit.SECONDS)) { + return; + } + log.info("[awaitExecuteNotify][任务处理中, 总任务数({}) 剩余任务数({})]", size, latch.getCount()); + } + log.error("[awaitExecuteNotify][任务未处理完,总任务数({}) 剩余任务数({})]", size, latch.getCount()); + } + + /** + * 同步执行单个支付通知 + * + * @param task 通知任务 + */ + public void executeNotify(PayNotifyTaskDO task) { + // 分布式锁,避免并发问题 + notifyLockCoreRedisDAO.lock(task.getId(), NOTIFY_TIMEOUT_MILLIS, () -> { + // 校验,当前任务是否已经被通知过 + // 虽然已经通过分布式加锁,但是可能同时满足通知的条件,然后都去获得锁。此时,第一个执行完后,第二个还是能拿到锁,然后会再执行一次。 + // 因此,此处我们通过第 notifyTimes 通知次数是否匹配来判断 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + if (ObjectUtil.notEqual(task.getNotifyTimes(), dbTask.getNotifyTimes())) { + log.warn("[executeNotifySync][task({}) 任务被忽略,原因是它的通知不是第 ({}) 次,可能是因为并发执行了]", + JsonUtils.toJsonString(task), dbTask.getNotifyTimes()); + return; + } + + // 执行通知 + getSelf().executeNotify0(dbTask); + }); + } + + @Transactional(rollbackFor = Exception.class) + public void executeNotify0(PayNotifyTaskDO task) { + // 发起回调 + CommonResult invokeResult = null; + Throwable invokeException = null; + try { + invokeResult = executeNotifyInvoke(task); + } catch (Throwable e) { + invokeException = e; + } + + // 处理结果 + Integer newStatus = processNotifyResult(task, invokeResult, invokeException); + + // 记录 PayNotifyLog 日志 + String response = invokeException != null ? ExceptionUtil.getRootCauseMessage(invokeException) : + JsonUtils.toJsonString(invokeResult); + notifyLogMapper.insert(PayNotifyLogDO.builder().taskId(task.getId()) + .notifyTimes(task.getNotifyTimes() + 1).status(newStatus).response(response).build()); + } + + /** + * 执行单个支付任务的 HTTP 调用 + * + * @param task 通知任务 + * @return HTTP 响应 + */ + private CommonResult executeNotifyInvoke(PayNotifyTaskDO task) { + // 拼接 body 参数 + Object request; + if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) { + request = PayOrderNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId()) + .payOrderId(task.getDataId()).build(); + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) { + request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId()) + .payRefundId(task.getDataId()).build(); + } else { + throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task)); + } + // 拼接 header 参数 + Map headers = new HashMap<>(); + TenantUtils.addTenantHeader(headers, task.getTenantId()); + + // 发起请求 + try (HttpResponse response = HttpUtil.createPost(task.getNotifyUrl()) + .body(JsonUtils.toJsonString(request)).addHeaders(headers) + .timeout((int) NOTIFY_TIMEOUT_MILLIS).execute()) { + // 解析结果 + return JsonUtils.parseObject(response.body(), CommonResult.class); + } + } + + /** + * 处理并更新通知结果 + * + * @param task 通知任务 + * @param invokeResult 通知结果 + * @param invokeException 通知异常 + * @return 最终任务的状态 + */ + @VisibleForTesting + Integer processNotifyResult(PayNotifyTaskDO task, CommonResult invokeResult, Throwable invokeException) { + // 设置通用的更新 PayNotifyTaskDO 的字段 + PayNotifyTaskDO updateTask = new PayNotifyTaskDO() + .setId(task.getId()) + .setLastExecuteTime(LocalDateTime.now()) + .setNotifyTimes(task.getNotifyTimes() + 1); + + // 情况一:调用成功 + if (invokeResult != null && invokeResult.isSuccess()) { + updateTask.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()); + notifyTaskMapper.updateById(updateTask); + return updateTask.getStatus(); + } + // 情况二:调用失败、调用异常 + // 2.1 超过最大回调次数 + if (updateTask.getNotifyTimes() >= PayNotifyTaskDO.NOTIFY_FREQUENCY.length) { + updateTask.setStatus(PayNotifyStatusEnum.FAILURE.getStatus()); + notifyTaskMapper.updateById(updateTask); + return updateTask.getStatus(); + } + // 2.2 未超过最大回调次数 + updateTask.setNextNotifyTime(addTime(Duration.ofSeconds(PayNotifyTaskDO.NOTIFY_FREQUENCY[updateTask.getNotifyTimes()]))); + updateTask.setStatus(invokeException != null ? PayNotifyStatusEnum.REQUEST_FAILURE.getStatus() + : PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus()); + notifyTaskMapper.updateById(updateTask); + return updateTask.getStatus(); + } + + @Override + public PayNotifyTaskDO getNotifyTask(Long id) { + return notifyTaskMapper.selectById(id); + } + + @Override + public PageResult getNotifyTaskPage(PayNotifyTaskPageReqVO pageReqVO) { + return notifyTaskMapper.selectPage(pageReqVO); + } + + @Override + public List getNotifyLogList(Long taskId) { + return notifyLogMapper.selectListByTaskId(taskId); + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayNotifyServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java new file mode 100755 index 000000000..12618c287 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java @@ -0,0 +1,124 @@ +package cn.iocoder.yudao.module.pay.service.order; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; +import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 支付订单 Service 接口 + * + * @author aquan + */ +public interface PayOrderService { + + /** + * 获得支付订单 + * + * @param id 编号 + * @return 支付订单 + */ + PayOrderDO getOrder(Long id); + + /** + * 获得支付订单 + * + * @param appId 应用编号 + * @param merchantOrderId 商户订单编号 + * @return 支付订单 + */ + PayOrderDO getOrder(Long appId, String merchantOrderId); + + /** + * 获得指定应用的订单数量 + * + * @param appId 应用编号 + * @return 订单数量 + */ + Long getOrderCountByAppId(Long appId); + + /** + * 获得支付订单分页 + * + * @param pageReqVO 分页查询 + * @return 支付订单分页 + */ + PageResult getOrderPage(PayOrderPageReqVO pageReqVO); + + /** + * 获得支付订单列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 支付订单列表 + */ + List getOrderList(PayOrderExportReqVO exportReqVO); + + /** + * 创建支付单 + * + * @param reqDTO 创建请求 + * @return 支付单编号 + */ + Long createOrder(@Valid PayOrderCreateReqDTO reqDTO); + + /** + * 提交支付 + * 此时,会发起支付渠道的调用 + * + * @param reqVO 提交请求 + * @param userIp 提交 IP + * @return 提交结果 + */ + PayOrderSubmitRespVO submitOrder(@Valid PayOrderSubmitReqVO reqVO, + @NotEmpty(message = "提交 IP 不能为空") String userIp); + + /** + * 通知支付单成功 + * + * @param channelId 渠道编号 + * @param notify 通知 + */ + void notifyOrder(Long channelId, PayOrderRespDTO notify); + + /** + * 更新支付订单的退款金额 + * + * @param id 编号 + * @param incrRefundPrice 增加的退款金额 + */ + void updateOrderRefundPrice(Long id, Integer incrRefundPrice); + + /** + * 获得支付订单 + * + * @param id 编号 + * @return 支付订单 + */ + PayOrderExtensionDO getOrderExtension(Long id); + + /** + * 同步订单的支付状态 + * + * @param minCreateTime 最小创建时间 + * @return 同步到已支付的订单数量 + */ + int syncOrder(LocalDateTime minCreateTime); + + /** + * 将已过期的订单,状态修改为已关闭 + * + * @return 过期的订单数量 + */ + int expireOrder(); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java new file mode 100755 index 000000000..20bb26087 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java @@ -0,0 +1,549 @@ +package cn.iocoder.yudao.module.pay.service.order; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert; +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.dal.dataobject.order.PayOrderExtensionDO; +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.redis.no.PayNoRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties; +import cn.iocoder.yudao.module.pay.service.app.PayAppService; +import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; +import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; +import cn.iocoder.yudao.module.pay.util.MoneyUtils; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; + +/** + * 支付订单 Service 实现类 + * + * @author aquan + */ +@Service +@Validated +@Slf4j +public class PayOrderServiceImpl implements PayOrderService { + + @Resource + private PayProperties payProperties; + + @Resource + private PayClientFactory payClientFactory; + + @Resource + private PayOrderMapper orderMapper; + @Resource + private PayOrderExtensionMapper orderExtensionMapper; + @Resource + private PayNoRedisDAO noRedisDAO; + + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + @Resource + private PayNotifyService notifyService; + + @Override + public PayOrderDO getOrder(Long id) { + return orderMapper.selectById(id); + } + + @Override + public PayOrderDO getOrder(Long appId, String merchantOrderId) { + return orderMapper.selectByAppIdAndMerchantOrderId(appId, merchantOrderId); + } + + @Override + public Long getOrderCountByAppId(Long appId) { + return orderMapper.selectCountByAppId(appId); + } + + @Override + public PageResult getOrderPage(PayOrderPageReqVO pageReqVO) { + return orderMapper.selectPage(pageReqVO); + } + + @Override + public List getOrderList(PayOrderExportReqVO exportReqVO) { + return orderMapper.selectList(exportReqVO); + } + + @Override + public Long createOrder(PayOrderCreateReqDTO reqDTO) { + // 校验 App + PayAppDO app = appService.validPayApp(reqDTO.getAppId()); + + // 查询对应的支付交易单是否已经存在。如果是,则直接返回 + PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId( + reqDTO.getAppId(), reqDTO.getMerchantOrderId()); + if (order != null) { + log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(), + order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况 + return order.getId(); + } + + // 创建支付交易单 + order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId()) + // 商户相关字段 + .setNotifyUrl(app.getOrderNotifyUrl()) + // 订单相关字段 + .setStatus(PayOrderStatusEnum.WAITING.getStatus()) + // 退款相关字段 + .setRefundPrice(0); + orderMapper.insert(order); + return order.getId(); + } + + @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了 + public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) { + // 1.1 获得 PayOrderDO ,并校验其是否存在 + PayOrderDO order = validateOrderCanSubmit(reqVO.getId()); + // 1.32 校验支付渠道是否有效 + PayChannelDO channel = validateChannelCanSubmit(order.getAppId(), reqVO.getChannelCode()); + PayClient client = payClientFactory.getPayClient(channel.getId()); + + // 2. 插入 PayOrderExtensionDO + String no = noRedisDAO.generate(payProperties.getOrderNoPrefix()); + PayOrderExtensionDO orderExtension = PayOrderConvert.INSTANCE.convert(reqVO, userIp) + .setOrderId(order.getId()).setNo(no) + .setChannelId(channel.getId()).setChannelCode(channel.getCode()) + .setStatus(PayOrderStatusEnum.WAITING.getStatus()); + orderExtensionMapper.insert(orderExtension); + + // 3. 调用三方接口 + PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp) + // 商户相关的字段 + .setOutTradeNo(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性! + .setSubject(order.getSubject()).setBody(order.getBody()) + .setNotifyUrl(genChannelOrderNotifyUrl(channel)) + .setReturnUrl(reqVO.getReturnUrl()) + // 订单相关字段 + .setPrice(order.getPrice()).setExpireTime(order.getExpireTime()); + PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO); + + // 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功 + if (unifiedOrderResp != null) { + getSelf().notifyOrder(channel, unifiedOrderResp); + // 如有渠道错误码,则抛出业务异常,提示用户 + if (StrUtil.isNotEmpty(unifiedOrderResp.getChannelErrorCode())) { + throw exception(ORDER_SUBMIT_CHANNEL_ERROR, unifiedOrderResp.getChannelErrorCode(), + unifiedOrderResp.getChannelErrorMsg()); + } + // 此处需要读取最新的状态 + order = orderMapper.selectById(order.getId()); + } + return PayOrderConvert.INSTANCE.convert(order, unifiedOrderResp); + } + + private PayOrderDO validateOrderCanSubmit(Long id) { + PayOrderDO order = orderMapper.selectById(id); + if (order == null) { // 是否存在 + throw exception(ORDER_NOT_FOUND); + } + if (PayOrderStatusEnum.isSuccess(order.getStatus())) { // 校验状态,发现已支付 + throw exception(ORDER_STATUS_IS_SUCCESS); + } + if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 + throw exception(ORDER_STATUS_IS_NOT_WAITING); + } + if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期 + throw exception(ORDER_IS_EXPIRED); + } + + // 【重要】校验是否支付拓展单已支付,只是没有回调、或者数据不正常 + validateOrderActuallyPaid(id); + return order; + } + + /** + * 校验支付订单实际已支付 + * + * @param id 支付编号 + */ + @VisibleForTesting + void validateOrderActuallyPaid(Long id) { + List orderExtensions = orderExtensionMapper.selectListByOrderId(id); + orderExtensions.forEach(orderExtension -> { + // 情况一:校验数据库中的 orderExtension 是不是已支付 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + log.warn("[validateOrderCanSubmit][order({}) 的 extension({}) 已支付,可能是数据不一致]", + id, orderExtension.getId()); + throw exception(ORDER_EXTENSION_IS_PAID); + } + // 情况二:调用三方接口,查询支付单状态,是不是已支付 + PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId()); + if (payClient == null) { + log.error("[validateOrderCanSubmit][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId()); + return; + } + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo()); + if (respDTO != null && PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) { + log.warn("[validateOrderCanSubmit][order({}) 的 PayOrderRespDTO({}) 已支付,可能是回调延迟]", + id, toJsonString(respDTO)); + throw exception(ORDER_EXTENSION_IS_PAID); + } + }); + } + + private PayChannelDO validateChannelCanSubmit(Long appId, String channelCode) { + // 校验 App + appService.validPayApp(appId); + // 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(appId, channelCode); + PayClient client = payClientFactory.getPayClient(channel.getId()); + if (client == null) { + log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); + throw exception(CHANNEL_NOT_FOUND); + } + return channel; + } + + /** + * 根据支付渠道的编码,生成支付渠道的回调地址 + * + * @param channel 支付渠道 + * @return 支付渠道的回调地址 配置地址 + "/" + channel id + */ + private String genChannelOrderNotifyUrl(PayChannelDO channel) { + return payProperties.getOrderNotifyUrl() + "/" + channel.getId(); + } + + @Override + public void notifyOrder(Long channelId, PayOrderRespDTO notify) { + // 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(channelId); + // 更新支付订单为已支付 + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyOrder(channel, notify)); + } + + /** + * 通知并更新订单的支付结果 + * + * @param channel 支付渠道 + * @param notify 通知 + */ + @Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyPayOrder(channel, notify) 调用,否则事务不生效 + public void notifyOrder(PayChannelDO channel, PayOrderRespDTO notify) { + // 情况一:支付成功的回调 + if (PayOrderStatusRespEnum.isSuccess(notify.getStatus())) { + notifyOrderSuccess(channel, notify); + return; + } + // 情况二:支付失败的回调 + if (PayOrderStatusRespEnum.isClosed(notify.getStatus())) { + notifyOrderClosed(channel, notify); + } + // 情况三:WAITING:无需处理 + // 情况四:REFUND:通过退款回调处理 + } + + private void notifyOrderSuccess(PayChannelDO channel, PayOrderRespDTO notify) { + // 1. 更新 PayOrderExtensionDO 支付成功 + PayOrderExtensionDO orderExtension = updateOrderSuccess(notify); + // 2. 更新 PayOrderDO 支付成功 + Boolean paid = updateOrderSuccess(channel, orderExtension, notify); + if (paid) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调 + return; + } + + // 3. 插入支付通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.ORDER.getType(), + orderExtension.getOrderId()); + } + + /** + * 更新 PayOrderExtensionDO 支付成功 + * + * @param notify 通知 + * @return PayOrderExtensionDO 对象 + */ + private PayOrderExtensionDO updateOrderSuccess(PayOrderRespDTO notify) { + // 1. 查询 PayOrderExtensionDO + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo()); + if (orderExtension == null) { + throw exception(ORDER_EXTENSION_NOT_FOUND); + } + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 + log.info("[updateOrderExtensionSuccess][orderExtension({}) 已经是已支付,无需更新]", orderExtension.getId()); + return orderExtension; + } + if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付 + throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + // 2. 更新 PayOrderExtensionDO + int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(), + PayOrderExtensionDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(toJsonString(notify)).build()); + if (updateCounts == 0) { // 校验状态,必须是待支付 + throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + log.info("[updateOrderExtensionSuccess][orderExtension({}) 更新为已支付]", orderExtension.getId()); + return orderExtension; + } + + /** + * 更新 PayOrderDO 支付成功 + * + * @param channel 支付渠道 + * @param orderExtension 支付拓展单 + * @param notify 通知回调 + * @return 是否之前已经成功回调 + */ + private Boolean updateOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension, + PayOrderRespDTO notify) { + // 1. 判断 PayOrderDO 是否处于待支付 + PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId()); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新 + && Objects.equals(order.getExtensionId(), orderExtension.getId())) { + log.info("[updateOrderExtensionSuccess][order({}) 已经是已支付,无需更新]", order.getId()); + return true; + } + if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 + throw exception(ORDER_STATUS_IS_NOT_WAITING); + } + + // 2. 更新 PayOrderDO + int updateCounts = orderMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(), + PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()) + .channelId(channel.getId()).channelCode(channel.getCode()) + .successTime(notify.getSuccessTime()).extensionId(orderExtension.getId()).no(orderExtension.getNo()) + .channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId()) + .channelFeeRate(channel.getFeeRate()).channelFeePrice(MoneyUtils.calculateRatePrice(order.getPrice(), channel.getFeeRate())) + .build()); + if (updateCounts == 0) { // 校验状态,必须是待支付 + throw exception(ORDER_STATUS_IS_NOT_WAITING); + } + log.info("[updateOrderExtensionSuccess][order({}) 更新为已支付]", order.getId()); + return false; + } + + private void notifyOrderClosed(PayChannelDO channel, PayOrderRespDTO notify) { + updateOrderExtensionClosed(channel, notify); + } + + private void updateOrderExtensionClosed(PayChannelDO channel, PayOrderRespDTO notify) { + // 1. 查询 PayOrderExtensionDO + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo()); + if (orderExtension == null) { + throw exception(ORDER_EXTENSION_NOT_FOUND); + } + if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭,直接返回,不用重复更新 + log.info("[updateOrderExtensionClosed][orderExtension({}) 已经是支付关闭,无需更新]", orderExtension.getId()); + return; + } + // 一般出现先是支付成功,然后支付关闭,都是全部退款导致关闭的场景。这个情况,我们不更新支付拓展单,只通过退款流程,更新支付单 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + log.info("[updateOrderExtensionClosed][orderExtension({}) 是已支付,无需更新为支付关闭]", orderExtension.getId()); + return; + } + if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付 + throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + // 2. 更新 PayOrderExtensionDO + int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(), + PayOrderExtensionDO.builder().status(PayOrderStatusEnum.CLOSED.getStatus()).channelNotifyData(toJsonString(notify)) + .channelErrorCode(notify.getChannelErrorCode()).channelErrorMsg(notify.getChannelErrorMsg()).build()); + if (updateCounts == 0) { // 校验状态,必须是待支付 + throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + log.info("[updateOrderExtensionClosed][orderExtension({}) 更新为支付关闭]", orderExtension.getId()); + } + + @Override + public void updateOrderRefundPrice(Long id, Integer incrRefundPrice) { + PayOrderDO order = orderMapper.selectById(id); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) { + throw exception(ORDER_REFUND_FAIL_STATUS_ERROR); + } + if (order.getRefundPrice() + incrRefundPrice > order.getPrice()) { + throw exception(REFUND_PRICE_EXCEED); + } + + // 更新订单 + PayOrderDO updateObj = new PayOrderDO() + .setRefundPrice(order.getRefundPrice() + incrRefundPrice) + .setStatus(PayOrderStatusEnum.REFUND.getStatus()); + int updateCount = orderMapper.updateByIdAndStatus(id, order.getStatus(), updateObj); + if (updateCount == 0) { + throw exception(ORDER_REFUND_FAIL_STATUS_ERROR); + } + } + + @Override + public PayOrderExtensionDO getOrderExtension(Long id) { + return orderExtensionMapper.selectById(id); + } + + @Override + public int syncOrder(LocalDateTime minCreateTime) { + // 1. 查询指定创建时间内的待支付订单 + List orderExtensions = orderExtensionMapper.selectListByStatusAndCreateTimeGe( + PayOrderStatusEnum.WAITING.getStatus(), minCreateTime); + if (CollUtil.isEmpty(orderExtensions)) { + return 0; + } + // 2. 遍历执行 + int count = 0; + for (PayOrderExtensionDO orderExtension : orderExtensions) { + count += syncOrder(orderExtension) ? 1 : 0; + } + return count; + } + + /** + * 同步单个支付拓展单 + * + * @param orderExtension 支付拓展单 + * @return 是否已支付 + */ + private boolean syncOrder(PayOrderExtensionDO orderExtension) { + try { + // 1.1 查询支付订单信息 + PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId()); + if (payClient == null) { + log.error("[syncOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId()); + return false; + } + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo()); + // 1.2 回调支付结果 + notifyOrder(orderExtension.getChannelId(), respDTO); + + // 2. 如果是已支付,则返回 true + return PayOrderStatusRespEnum.isSuccess(respDTO.getStatus()); + } catch (Throwable e) { + log.error("[syncOrder][orderExtension({}) 同步支付状态异常]", orderExtension.getId(), e); + return false; + } + } + + @Override + public int expireOrder() { + // 1. 查询过期的待支付订单 + List orders = orderMapper.selectListByStatusAndExpireTimeLt( + PayOrderStatusEnum.WAITING.getStatus(), LocalDateTime.now()); + if (CollUtil.isEmpty(orders)) { + return 0; + } + + // 2. 遍历执行 + int count = 0; + for (PayOrderDO order : orders) { + count += expireOrder(order) ? 1 : 0; + } + return count; + } + + /** + * 同步单个支付单 + * + * @param order 支付单 + * @return 是否已过期 + */ + private boolean expireOrder(PayOrderDO order) { + try { + // 1. 需要先处理关联的支付拓展单,避免错误的过期已支付 or 已退款的订单 + List orderExtensions = orderExtensionMapper.selectListByOrderId(order.getId()); + for (PayOrderExtensionDO orderExtension : orderExtensions) { + if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { + continue; + } + // 情况一:校验数据库中的 orderExtension 是不是已支付 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + log.error("[expireOrder][order({}) 的 extension({}) 已支付,可能是数据不一致]", + order.getId(), orderExtension.getId()); + return false; + } + // 情况二:调用三方接口,查询支付单状态,是不是已支付/已退款 + PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId()); + if (payClient == null) { + log.error("[expireOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId()); + return false; + } + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo()); + if (PayOrderStatusRespEnum.isRefund(respDTO.getStatus())) { + // 补充说明:按道理,应该是 WAITING => SUCCESS => REFUND 状态,如果直接 WAITING => REFUND 状态,说明中间丢了过程 + // 此时,需要人工介入,手工补齐数据,保持 WAITING => SUCCESS => REFUND 的过程 + log.error("[expireOrder][extension({}) 的 PayOrderRespDTO({}) 已退款,可能是回调延迟]", + orderExtension.getId(), toJsonString(respDTO)); + return false; + } + if (PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) { + notifyOrder(orderExtension.getChannelId(), respDTO); + return false; + } + // 兜底逻辑:将支付拓展单更新为已关闭 + PayOrderExtensionDO updateObj = new PayOrderExtensionDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setChannelNotifyData(toJsonString(respDTO)); + if (orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), PayOrderStatusEnum.WAITING.getStatus(), + updateObj) == 0) { + log.error("[expireOrder][extension({}) 更新为支付关闭失败]", orderExtension.getId()); + return false; + } + log.info("[expireOrder][extension({}) 更新为支付关闭成功]", orderExtension.getId()); + } + + // 2. 都没有上述情况,可以安心更新为已关闭 + PayOrderDO updateObj = new PayOrderDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus()); + if (orderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), updateObj) == 0) { + log.error("[expireOrder][order({}) 更新为支付关闭失败]", order.getId()); + return false; + } + log.info("[expireOrder][order({}) 更新为支付关闭失败]", order.getId()); + return true; + } catch (Throwable e) { + log.error("[expireOrder][order({}) 过期订单异常]", order.getId(), e); + return false; + } + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayOrderServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java new file mode 100755 index 000000000..8a93ec4da --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.pay.service.refund; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; + +import java.util.List; + +/** + * 退款订单 Service 接口 + * + * @author aquan + */ +public interface PayRefundService { + + /** + * 获得退款订单 + * + * @param id 编号 + * @return 退款订单 + */ + PayRefundDO getRefund(Long id); + + /** + * 获得指定应用的退款数量 + * + * @param appId 应用编号 + * @return 退款数量 + */ + Long getRefundCountByAppId(Long appId); + + /** + * 获得退款订单分页 + * + * @param pageReqVO 分页查询 + * @return 退款订单分页 + */ + PageResult getRefundPage(PayRefundPageReqVO pageReqVO); + + /** + * 获得退款订单列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 退款订单列表 + */ + List getRefundList(PayRefundExportReqVO exportReqVO); + + /** + * 创建退款申请 + * + * @param reqDTO 退款申请信息 + * @return 退款单号 + */ + Long createPayRefund(PayRefundCreateReqDTO reqDTO); + + /** + * 渠道的退款通知 + * + * @param channelId 渠道编号 + * @param notify 通知 + */ + void notifyRefund(Long channelId, PayRefundRespDTO notify); + + /** + * 同步渠道退款的退款状态 + * + * @return 同步到状态的退款数量,包括退款成功、退款失败 + */ + int syncRefund(); + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java new file mode 100755 index 000000000..4ab692b19 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java @@ -0,0 +1,330 @@ +package cn.iocoder.yudao.module.pay.service.refund; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert; +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.dal.dataobject.refund.PayRefundDO; +import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper; +import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; +import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties; +import cn.iocoder.yudao.module.pay.service.app.PayAppService; +import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; +import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; + +/** + * 退款订单 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +@Validated +public class PayRefundServiceImpl implements PayRefundService { + + @Resource + private PayProperties payProperties; + + @Resource + private PayClientFactory payClientFactory; + + @Resource + private PayRefundMapper refundMapper; + @Resource + private PayNoRedisDAO noRedisDAO; + + @Resource + private PayOrderService orderService; + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + @Resource + private PayNotifyService notifyService; + + @Override + public PayRefundDO getRefund(Long id) { + return refundMapper.selectById(id); + } + + @Override + public Long getRefundCountByAppId(Long appId) { + return refundMapper.selectCountByAppId(appId); + } + + @Override + public PageResult getRefundPage(PayRefundPageReqVO pageReqVO) { + return refundMapper.selectPage(pageReqVO); + } + + @Override + public List getRefundList(PayRefundExportReqVO exportReqVO) { + return refundMapper.selectList(exportReqVO); + } + + @Override + public Long createPayRefund(PayRefundCreateReqDTO reqDTO) { + // 1.1 校验 App + PayAppDO app = appService.validPayApp(reqDTO.getAppId()); + // 1.2 校验支付订单 + PayOrderDO order = validatePayOrderCanRefund(reqDTO); + // 1.3 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(order.getChannelId()); + PayClient client = payClientFactory.getPayClient(channel.getId()); + if (client == null) { + log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); + throw exception(CHANNEL_NOT_FOUND); + } + // 1.4 校验退款订单是否已经存在 + PayRefundDO refund = refundMapper.selectByAppIdAndMerchantRefundId( + app.getId(), reqDTO.getMerchantRefundId()); + if (refund != null) { + throw exception(REFUND_EXISTS); + } + + // 2.1 插入退款单 + String no = noRedisDAO.generate(payProperties.getRefundNoPrefix()); + refund = PayRefundConvert.INSTANCE.convert(reqDTO) + .setNo(no).setOrderId(order.getId()).setOrderNo(order.getNo()) + .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode()) + // 商户相关的字段 + .setNotifyUrl(app.getRefundNotifyUrl()) + // 渠道相关字段 + .setChannelOrderNo(order.getChannelOrderNo()) + // 退款相关字段 + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setPayPrice(order.getPrice()).setRefundPrice(reqDTO.getPrice()); + refundMapper.insert(refund); + try { + // 2.2 向渠道发起退款申请 + PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO() + .setPayPrice(order.getPrice()) + .setRefundPrice(reqDTO.getPrice()) + .setOutTradeNo(order.getNo()) + .setOutRefundNo(refund.getNo()) + .setNotifyUrl(genChannelRefundNotifyUrl(channel)) + .setReason(reqDTO.getReason()); + PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); + // 2.3 处理退款返回 + getSelf().notifyRefund(channel, refundRespDTO); + } catch (Throwable e) { + // 注意:这里仅打印异常,不进行抛出。 + // 原因是:虽然调用支付渠道进行退款发生异常(网络请求超时),实际退款成功。这个结果,后续通过退款回调、或者退款轮询补偿可以拿到。 + // 最终,在异常的情况下,支付中心会异步回调业务的退款回调接口,提供退款结果 + log.error("[createPayRefund][退款 id({}) requestDTO({}) 发生异常]", + refund.getId(), reqDTO, e); + } + + // 返回退款编号 + return refund.getId(); + } + + /** + * 校验支付订单是否可以退款 + * + * @param reqDTO 退款申请信息 + * @return 支付订单 + */ + private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) { + PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId()); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 校验状态,必须是已支付、或者已退款 + if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) { + throw exception(ORDER_REFUND_FAIL_STATUS_ERROR); + } + + // 校验金额,退款金额不能大于原定的金额 + if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){ + throw exception(REFUND_PRICE_EXCEED); + } + // 是否有退款中的订单 + if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(), + PayRefundStatusEnum.WAITING.getStatus()) > 0) { + throw exception(REFUND_HAS_REFUNDING); + } + return order; + } + + /** + * 根据支付渠道的编码,生成支付渠道的回调地址 + * + * @param channel 支付渠道 + * @return 支付渠道的回调地址 配置地址 + "/" + channel id + */ + private String genChannelRefundNotifyUrl(PayChannelDO channel) { + return payProperties.getRefundNotifyUrl() + "/" + channel.getId(); + } + + @Override + public void notifyRefund(Long channelId, PayRefundRespDTO notify) { + // 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(channelId); + // 更新退款订单 + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyRefund(channel, notify)); + } + + /** + * 通知并更新订单的退款结果 + * + * @param channel 支付渠道 + * @param notify 通知 + */ + @Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效 + public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) { + // 情况一:退款成功 + if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) { + notifyRefundSuccess(channel, notify); + return; + } + // 情况二:退款失败 + if (PayRefundStatusRespEnum.isFailure(notify.getStatus())) { + notifyRefundFailure(channel, notify); + } + } + + private void notifyRefundSuccess(PayChannelDO channel, PayRefundRespDTO notify) { + // 1.1 查询 PayRefundDO + PayRefundDO refund = refundMapper.selectByAppIdAndNo( + channel.getAppId(), notify.getOutRefundNo()); + if (refund == null) { + throw exception(REFUND_NOT_FOUND); + } + if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 + log.info("[notifyRefundSuccess][退款订单({}) 已经是退款成功,无需更新]", refund.getId()); + return; + } + if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) { + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + // 1.2 更新 PayRefundDO + PayRefundDO updateRefundObj = new PayRefundDO() + .setSuccessTime(notify.getSuccessTime()) + .setChannelRefundNo(notify.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus()) + .setChannelNotifyData(toJsonString(notify)); + int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj); + if (updateCounts == 0) { // 校验状态,必须是等待状态 + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + log.info("[notifyRefundSuccess][退款订单({}) 更新为退款成功]", refund.getId()); + + // 2. 更新订单 + orderService.updateOrderRefundPrice(refund.getOrderId(), refund.getRefundPrice()); + + // 3. 插入退款通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.REFUND.getType(), + refund.getId()); + } + + private void notifyRefundFailure(PayChannelDO channel, PayRefundRespDTO notify) { + // 1.1 查询 PayRefundDO + PayRefundDO refund = refundMapper.selectByAppIdAndNo( + channel.getAppId(), notify.getOutRefundNo()); + if (refund == null) { + throw exception(REFUND_NOT_FOUND); + } + if (PayRefundStatusEnum.isFailure(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 + log.info("[notifyRefundSuccess][退款订单({}) 已经是退款关闭,无需更新]", refund.getId()); + return; + } + if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) { + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + // 1.2 更新 PayRefundDO + PayRefundDO updateRefundObj = new PayRefundDO() + .setChannelRefundNo(notify.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.FAILURE.getStatus()) + .setChannelNotifyData(toJsonString(notify)) + .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()); + int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj); + if (updateCounts == 0) { // 校验状态,必须是等待状态 + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + log.info("[notifyRefundFailure][退款订单({}) 更新为退款失败]", refund.getId()); + + // 2. 插入退款通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.REFUND.getType(), + refund.getId()); + } + + @Override + public int syncRefund() { + // 1. 查询指定创建时间内的待退款订单 + List refunds = refundMapper.selectListByStatus(PayRefundStatusEnum.WAITING.getStatus()); + if (CollUtil.isEmpty(refunds)) { + return 0; + } + // 2. 遍历执行 + int count = 0; + for (PayRefundDO refund : refunds) { + count += syncRefund(refund) ? 1 : 0; + } + return count; + } + + /** + * 同步单个退款订单 + * + * @param refund 退款订单 + * @return 是否同步到 + */ + private boolean syncRefund(PayRefundDO refund) { + try { + // 1.1 查询退款订单信息 + PayClient payClient = payClientFactory.getPayClient(refund.getChannelId()); + if (payClient == null) { + log.error("[syncRefund][渠道编号({}) 找不到对应的支付客户端]", refund.getChannelId()); + return false; + } + PayRefundRespDTO respDTO = payClient.getRefund(refund.getOrderNo(), refund.getNo()); + // 1.2 回调退款结果 + notifyRefund(refund.getChannelId(), respDTO); + + // 2. 如果同步到,则返回 true + return PayRefundStatusEnum.isSuccess(respDTO.getStatus()) + || PayRefundStatusEnum.isFailure(respDTO.getStatus()); + } catch (Throwable e) { + log.error("[syncRefund][refund({}) 同步退款状态异常]", refund.getId(), e); + return false; + } + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayRefundServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/util/MoneyUtils.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/util/MoneyUtils.java new file mode 100644 index 000000000..76cf5bfa3 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/util/MoneyUtils.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.pay.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 金额工具类 + * + * @author 芋道源码 + */ +public class MoneyUtils { + + /** + * 计算百分比金额,四舍五入 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @return 百分比金额 + */ + public static Integer calculateRatePrice(Integer price, Double rate) { + return new BigDecimal(price) + .multiply(BigDecimal.valueOf(rate)) // 乘以 + .setScale(0, RoundingMode.HALF_UP) // 四舍五入 + .intValue(); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/util/PaySeqUtils.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/util/PaySeqUtils.java new file mode 100644 index 000000000..3882a2fd3 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/util/PaySeqUtils.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.pay.util; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; + +import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 支付相关编号的生产 + */ +// TODO @jason:需要改造,基于 db; +public class PaySeqUtils { + + private static final AtomicLong REFUND_REQ_NO_SEQ = new AtomicLong(0L); + + private static final AtomicLong MER_REFUND_NO_SEQ = new AtomicLong(0L); + + private static final AtomicLong MER_ORDER_NO_SEQ = new AtomicLong(0L); + + // TODO 芋艿:需要看看 + /** + * 生成商户退款单号,用于测试,应该由商户系统生成 + * @return 商户退款单 + */ + public static String genMerchantRefundNo() { + return String.format("%s%s%04d", "MR", + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_MS_PATTERN), + (int) MER_REFUND_NO_SEQ.getAndIncrement() % 10000); + } + + // TODO 芋艿:需要看看 + + /** + * 生成退款请求号 + * @return 退款请求号 + */ + public static String genRefundReqNo() { + return String.format("%s%s%04d", "RR", + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_MS_PATTERN), + (int) REFUND_REQ_NO_SEQ.getAndIncrement() % 10000); + } + + /** + * 生成商户订单编号号 用于测试,应该由商户系统生成 + * @return 商户订单编号 + */ + public static String genMerchantOrderNo() { + return String.format("%s%s%04d", "MO", + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_MS_PATTERN), + (int) MER_ORDER_NO_SEQ.getAndIncrement() % 10000); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/resources/application-dev.yaml b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/application-dev.yaml new file mode 100644 index 000000000..71849d27e --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/application-dev.yaml @@ -0,0 +1,107 @@ +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + name: ruoyi-vue-pro + url: jdbc:mysql://400-infra.server.iocoder.cn:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT&nullCatalogMeansCurrent=true + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 3WLiVUBEwTbvAfsh + slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 + name: ruoyi-vue-pro + url: jdbc:mysql://400-infra.server.iocoder.cn:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT&nullCatalogMeansCurrent=true + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 3WLiVUBEwTbvAfsh + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 400-infra.server.iocoder.cn # 地址 + port: 6379 # 端口 + database: 1 # 数据库索引 +# password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + access-log: # 访问日志的配置项 + enable: false + error-code: # 错误码相关配置项 + enable: false + pay: + order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 + refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 + demo: false # 关闭演示模式 diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/resources/application-local.yaml b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/application-local.yaml new file mode 100644 index 000000000..a0ce01f8c --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/application-local.yaml @@ -0,0 +1,132 @@ +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + # url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例 + # url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.master.name} # PostgreSQL 连接的示例 + # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 + # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例 + username: root + password: 123456 + # username: sa + # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W + slave: # 模拟从库,可根据自己需要修改 + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + # url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例 + # url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例 + # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 + # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例 + username: root + password: 123456 + # username: sa + # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 +# password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### +xxl: + job: + enabled: true # 是否开启调度中心,默认为 true 开启 + admin: + addresses: http://127.0.0.1:8000/xxl-job-admin # 调度中心部署跟地址 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + +# 日志文件配置 +logging: + level: + # 配置自己写的 MyBatis Mapper 打印日志 + cn.iocoder.yudao.module.pay.dal.mysql: debug + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + env: # 多环境的配置项 + tag: ${HOSTNAME} + security: + mock-enable: true + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + access-log: # 访问日志的配置项 + enable: false + error-code: # 错误码相关配置项 + enable: false + pay: + order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 + refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 + demo: false # 关闭演示模式 diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/resources/application.yaml b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/application.yaml new file mode 100644 index 000000000..0db5aca2a --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/application.yaml @@ -0,0 +1,95 @@ +spring: + main: + allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 + allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Dubbo 或者 Feign 等会存在重复定义的服务 + + # Servlet 配置 + servlet: + # 文件上传相关配置项 + multipart: + max-file-size: 16MB # 单个文件大小 + max-request-size: 32MB # 设置总上传的文件大小 + mvc: + pathmatch: + matching-strategy: ANT_PATH_MATCHER # 解决 SpringFox 与 SpringBoot 2.6.x 不兼容的问题,参见 SpringFoxHandlerProviderBeanPostProcessor 类 + + # Jackson 配置项 + jackson: + serialization: + write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳 + write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 + write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳 + fail-on-empty-beans: false # 允许序列化无属性的 Bean + + # Cache 配置项 + cache: + type: REDIS + redis: + time-to-live: 1h # 设置过期时间为 1 小时 + +--- #################### 接口文档配置 #################### + +springdoc: + api-docs: + enabled: true # 1. 是否开启 Swagger 接文档的元数据 + path: /v3/api-docs + swagger-ui: + enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面 + path: /swagger-ui.html + +knife4j: + enable: true # 2.2 是否开启 Swagger 文档的 Knife4j UI 界面 + setting: + language: zh_cn + +# MyBatis Plus 的配置项 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 + global-config: + db-config: + # 重要说明:如果将配置放到 Nacos 时,请注意将 id-type 设置为对应 DB 的类型,否则会报错;详细见 https://gitee.com/zhijiantianya/yudao-cloud/issues/I5W2N0 讨论 + id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 +# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库 +# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 +# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + type-aliases-package: ${yudao.info.base-package}.dal.dataobject + encryptor: + password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### + +xxl: + job: + executor: + appname: ${spring.application.name} # 执行器 AppName + logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径 + accessToken: default_token # 执行器通讯TOKEN + +--- #################### 芋道相关配置 #################### + +yudao: + info: + version: 1.0.0 + base-package: cn.iocoder.yudao.module.pay + web: + admin-ui: + url: http://dashboard.yudao.iocoder.cn # Admin 管理后台 UI 的地址 + swagger: + title: 管理后台 + description: 提供管理员管理的所有功能 + version: ${yudao.info.version} + base-package: ${yudao.info.base-package} + error-code: # 错误码相关配置项 + constants-class-list: + - cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants + tenant: # 多租户相关配置项 + enable: true + ignore-urls: + - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号 + +debug: false diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/resources/bootstrap-local.yaml b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/bootstrap-local.yaml new file mode 100644 index 000000000..2de0efbf7 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/bootstrap-local.yaml @@ -0,0 +1,23 @@ +--- #################### 注册中心相关配置 #################### + +spring: + cloud: + nacos: + server-addr: 127.0.0.1:8848 + discovery: + namespace: dev # 命名空间。这里使用 dev 开发环境 + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 + +--- #################### 配置中心相关配置 #################### + +spring: + cloud: + nacos: + # Nacos Config 配置项,对应 NacosConfigProperties 配置属性类 + config: + server-addr: 127.0.0.1:8848 # Nacos 服务器地址 + namespace: dev # 命名空间 dev 的ID,不能直接使用 dev 名称。创建命名空间的时候需要指定ID为 dev,这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + name: ${spring.application.name} # 使用的 Nacos 配置集的 dataId,默认为 spring.application.name + file-extension: yaml # 使用的 Nacos 配置集的 dataId 的文件拓展名,同时也是 Nacos 配置集的配置格式,默认为 properties diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/resources/bootstrap.yaml b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/bootstrap.yaml new file mode 100644 index 000000000..9f25acdb8 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/bootstrap.yaml @@ -0,0 +1,14 @@ +spring: + application: + name: pay-server + + profiles: + active: local + +server: + port: 48085 + +# 日志文件配置。注意,如果 logging.file.name 不放在 bootstrap.yaml 配置文件,而是放在 application.yaml 中,会导致出现 LOG_FILE_IS_UNDEFINED 文件 +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/resources/logback-spring.xml b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..5f4b30d04 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/resources/logback-spring.xml @@ -0,0 +1,76 @@ + + + + + + + + + +       + + + ${PATTERN_DEFAULT} + + + + + + + + + + ${PATTERN_DEFAULT} + + + + ${LOG_FILE} + + + ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz} + + ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} + + ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} + + ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} + + ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30} + + + + + + 0 + + 256 + + + + + + + + ${PATTERN_DEFAULT} + + + + + + + + + + + + + + + + + + + + + + diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/dal/dataobject/merchant/PayChannelDOTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/dal/dataobject/merchant/PayChannelDOTest.java new file mode 100644 index 000000000..73b4f6f82 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/dal/dataobject/merchant/PayChannelDOTest.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.pay.dal.dataobject.merchant; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WXPayClientConfig; +import org.junit.jupiter.api.Test; + +public class PayChannelDOTest { + + @Test + public void testSerialization() { + PayChannelDO payChannelDO = new PayChannelDO(); + // 创建配置 + WXPayClientConfig config = new WXPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WXPayClientConfig.API_VERSION_V2); + config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); + payChannelDO.setConfig(config); + + // 序列化 + String text = JsonUtils.toJsonString(payChannelDO); + System.out.println(text); + + // 反序列化 + payChannelDO = JsonUtils.parseObject(text, PayChannelDO.class); + System.out.println(payChannelDO.getConfig().getClass()); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/dal/mysql/merchant/PayChannelMapperIntegrationTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/dal/mysql/merchant/PayChannelMapperIntegrationTest.java new file mode 100644 index 000000000..2150a8a15 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/dal/mysql/merchant/PayChannelMapperIntegrationTest.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.pay.dal.mysql.merchant; + +import cn.hutool.core.io.IoUtil; +import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WXPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; +import cn.iocoder.yudao.module.pay.test.BaseDbIntegrationTest; +import org.junit.jupiter.api.Test; + +import javax.annotation.Resource; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.List; + +@Resource +public class PayChannelMapperIntegrationTest extends BaseDbIntegrationTest { + + @Resource + private PayChannelMapper payChannelMapper; + + /** + * 插入 {@link PayChannelEnum#WX_PUB} 初始配置 + */ + @Test + public void testInsertWxPub() throws FileNotFoundException { + PayChannelDO payChannelDO = new PayChannelDO(); + payChannelDO.setCode(PayChannelEnum.WX_PUB.getCode()); + payChannelDO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + payChannelDO.setFeeRate(1D); + payChannelDO.setAppId(6L); + // 配置 + WXPayClientConfig config = new WXPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WXPayClientConfig.API_VERSION_V2); + config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); + config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"))); + config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"))); + config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase"); + payChannelDO.setConfig(config); + // 执行插入 + payChannelMapper.insert(payChannelDO); + } + + // TODO @ouyang:Zfb 改成 AlipayQr + /** + * 插入 {@link PayChannelEnum#ALIPAY_QR} 初始配置 + */ + @Test + public void testInsertZfb() { + PayChannelDO payChannelDO = new PayChannelDO(); + payChannelDO.setCode(PayChannelEnum.ALIPAY_QR.getCode()); + payChannelDO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + payChannelDO.setFeeRate(1D); + payChannelDO.setAppId(6L); + // 配置 + AlipayPayClientConfig config = new AlipayPayClientConfig(); + config.setAppId("2021000118634035"); + config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX); + config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); + config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); + config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); + // 创建客户端 + payChannelDO.setConfig(config); + // 执行插入 + payChannelMapper.insert(payChannelDO); + } + + /** + * 查询所有支付配置,看看是否都是 ok 的 + */ + @Test + public void testSelectList() { + List payChannels = payChannelMapper.selectList(); + System.out.println(payChannels.size()); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceIntegrationTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceIntegrationTest.java new file mode 100644 index 000000000..55a37ad1a --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceIntegrationTest.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.pay.service.order; + +import cn.iocoder.yudao.module.pay.service.channel.PayAppServiceImpl; +import cn.iocoder.yudao.module.pay.service.channel.PayChannelServiceImpl; +import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderSubmitReqDTO; +import cn.iocoder.yudao.module.pay.test.BaseDbIntegrationTest; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; + +@Import({PayOrderServiceImpl.class, PayAppServiceImpl.class, + PayChannelServiceImpl.class, YudaoPayAutoConfiguration.class}) +public class PayOrderServiceIntegrationTest extends BaseDbIntegrationTest { + + @Resource + private PayOrderService payOrderService; + + @Test + public void testCreatePayOrder() { + // 构造请求 + PayOrderCreateReqDTO reqDTO = new PayOrderCreateReqDTO(); + reqDTO.setAppId(6L); + reqDTO.setUserIp("127.0.0.1"); + reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis())); + reqDTO.setSubject("标题"); + reqDTO.setBody("内容"); + reqDTO.setPrice(100); + reqDTO.setExpireTime(DateUtils.addTime(Duration.ofDays(1))); + // 发起请求 + payOrderService.createPayOrder(reqDTO); + } + + @Test + public void testSubmitPayOrder() { + // 构造请求 + PayOrderSubmitReqDTO reqDTO = new PayOrderSubmitReqDTO(); + reqDTO.setId(10L); + reqDTO.setAppId(6L); + reqDTO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqDTO.setUserIp("127.0.0.1"); + // 发起请求 + payOrderService.submitPayOrder(reqDTO); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/service/package-info.java b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/service/package-info.java new file mode 100644 index 000000000..2cad91eba --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/service/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.module.pay.service; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/test/BaseDbAndRedisIntegrationTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/test/BaseDbAndRedisIntegrationTest.java new file mode 100644 index 000000000..2ee19ebf9 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/test/BaseDbAndRedisIntegrationTest.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.pay.test; + +import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; +import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseDbAndRedisIntegrationTest { + + @Import({ + // DB 配置类 + DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类 + YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + // MyBatis 配置类 + YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/test/BaseDbIntegrationTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/test/BaseDbIntegrationTest.java new file mode 100644 index 000000000..380efa3f4 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/test/BaseDbIntegrationTest.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pay.test; + +import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; +import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseDbIntegrationTest { + + @Import({ + // DB 配置类 + DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类 + YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + // MyBatis 配置类 + YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + }) + public static class Application { + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/test/BaseRedisIntegrationTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/test/BaseRedisIntegrationTest.java new file mode 100644 index 000000000..d01353d72 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/java/cn/iocoder/yudao/module/pay/test/BaseRedisIntegrationTest.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.pay.test; + +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseRedisIntegrationTest { + + @Import({ + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test-integration/resources/application-integration-test.yaml b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/resources/application-integration-test.yaml new file mode 100644 index 000000000..c8e34e476 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test-integration/resources/application-integration-test.yaml @@ -0,0 +1,93 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 123456 + slave: # 模拟从库,可根据自己需要修改 + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印日志 + global-config: + db-config: + id-type: AUTO # 自增 ID + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + mapper-locations: classpath*:mapper/*.xml + type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 +resilience4j: + ratelimiter: + instances: + backendA: + limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50 + limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500 + timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s + register-health-indicator: true # 是否注册到健康监测 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +yudao: + info: + version: 1.0.0 + base-package: cn.iocoder.yudao.module + pay: + pay-notify-url: http://niubi.natapp1.cc/api/pay/order/notify + refund-notify-url: http://niubi.natapp1.cc/api/pay/refund/notify diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceTest.java new file mode 100644 index 000000000..1934cfe97 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceTest.java @@ -0,0 +1,258 @@ +package cn.iocoder.yudao.module.pay.service.app; + +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; +import cn.iocoder.yudao.module.pay.dal.mysql.app.PayAppMapper; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; +import static java.util.Collections.singleton; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link PayAppServiceImpl} 的单元测试 + * + * @author aquan + */ +@Import(PayAppServiceImpl.class) +public class PayAppServiceTest extends BaseDbUnitTest { + + @Resource + private PayAppServiceImpl appService; + + @Resource + private PayAppMapper appMapper; + + @MockBean + private PayOrderService orderService; + @MockBean + private PayRefundService refundService; + + @Test + public void testCreateApp_success() { + // 准备参数 + PayAppCreateReqVO reqVO = randomPojo(PayAppCreateReqVO.class, o -> + o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus())) + .setOrderNotifyUrl(randomURL()) + .setRefundNotifyUrl(randomURL())); + + // 调用 + Long appId = appService.createApp(reqVO); + // 断言 + assertNotNull(appId); + PayAppDO app = appMapper.selectById(appId); + assertPojoEquals(reqVO, app); + } + + @Test + public void testUpdateApp_success() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setOrderNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL()); + o.setId(dbApp.getId()); // 设置更新的 ID + }); + + // 调用 + appService.updateApp(reqVO); + // 校验是否更新正确 + PayAppDO app = appMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, app); + } + + @Test + public void testUpdateApp_notExists() { + // 准备参数 + PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> + o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus()))); + // 调用, 并断言异常 + assertServiceException(() -> appService.updateApp(reqVO), APP_NOT_FOUND); + } + + @Test + public void testUpdateAppStatus() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, o -> + o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + + // 准备参数 + Long id = dbApp.getId(); + Integer status = CommonStatusEnum.ENABLE.getStatus(); + // 调用 + appService.updateAppStatus(id, status); + // 断言 + PayAppDO app = appMapper.selectById(id); // 获取最新的 + assertEquals(status, app.getStatus()); + } + + @Test + public void testDeleteApp_success() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用 + appService.deleteApp(id); + // 校验数据不存在了 + assertNull(appMapper.selectById(id)); + } + + @Test + public void testDeleteApp_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> appService.deleteApp(id), APP_NOT_FOUND); + } + + @Test + public void testDeleteApp_existOrder() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + // mock 订单有订单 + when(orderService.getOrderCountByAppId(eq(id))).thenReturn(10L); + + // 调用, 并断言异常 + assertServiceException(() -> appService.deleteApp(id), APP_EXIST_ORDER_CANT_DELETE); + } + + @Test + public void testDeleteApp_existRefund() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + // mock 订单有订单 + when(refundService.getRefundCountByAppId(eq(id))).thenReturn(10L); + + // 调用, 并断言异常 + assertServiceException(() -> appService.deleteApp(id), APP_EXIST_REFUND_CANT_DELETE); + } + + @Test + public void testApp() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用 + PayAppDO app = appService.getApp(id); + // 校验数据一致 + assertPojoEquals(app, dbApp); + } + + @Test + public void testAppMap() { + // mock 数据 + PayAppDO dbApp01 = randomPojo(PayAppDO.class); + appMapper.insert(dbApp01);// @Sql: 先插入出一条存在的数据 + PayAppDO dbApp02 = randomPojo(PayAppDO.class); + appMapper.insert(dbApp02);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp01.getId(); + + // 调用 + Map appMap = appService.getAppMap(singleton(id)); + // 校验数据一致 + assertEquals(1, appMap.size()); + assertPojoEquals(dbApp01, appMap.get(id)); + } + + @Test + public void testGetAppPage() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, o -> { // 等会查询到 + o.setName("灿灿姐的杂货铺"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2021,11,20)); + }); + + appMapper.insert(dbApp); + // 测试 name 不匹配 + appMapper.insert(cloneIgnoreId(dbApp, o -> o.setName("敏敏姐的杂货铺"))); + // 测试 status 不匹配 + appMapper.insert(cloneIgnoreId(dbApp, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + appMapper.insert(cloneIgnoreId(dbApp, o -> o.setCreateTime(buildTime(2021,12,21)))); + // 准备参数 + PayAppPageReqVO reqVO = new PayAppPageReqVO(); + reqVO.setName("灿灿姐的杂货铺"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2021, 11, 19, 2021, 11, 21)); + + // 调用 + PageResult pageResult = appService.getAppPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbApp, pageResult.getList().get(0)); + } + + @Test + public void testValidPayApp_success() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, + o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用 + PayAppDO app = appService.validPayApp(id); + // 校验数据一致 + assertPojoEquals(app, dbApp); + } + + @Test + public void testValidPayApp_notFound() { + assertServiceException(() -> appService.validPayApp(randomLongId()), APP_NOT_FOUND); + } + + @Test + public void testValidPayApp_disable() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, + o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用,并断言异常 + assertServiceException(() -> appService.validPayApp(id), APP_IS_DISABLE); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java new file mode 100644 index 000000000..932f5fbc6 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java @@ -0,0 +1,365 @@ +package cn.iocoder.yudao.module.pay.service.channel; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +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.channel.PayChannelDO; +import cn.iocoder.yudao.module.pay.dal.mysql.channel.PayChannelMapper; +import com.alibaba.fastjson.JSON; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import javax.validation.Validator; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +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({PayChannelServiceImpl.class}) +public class PayChannelServiceTest extends BaseDbUnitTest { + + private static final String ALIPAY_SERVER_URL = "https://openapi.alipay.com/gateway.do"; + + @Resource + private PayChannelServiceImpl channelService; + + @Resource + private PayChannelMapper channelMapper; + + @MockBean + private PayClientFactory payClientFactory; + @MockBean + private Validator validator; + + @BeforeEach + public void setUp() { + channelService.setChannelCache(null); + } + + @Test + public void testInitLocalCache() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, + o -> o.setConfig(randomWxPayClientConfig())); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + + // 调用 + channelService.initLocalCache(); + // 校验缓存 + assertEquals(1, channelService.getChannelCache().size()); + assertEquals(dbChannel, channelService.getChannelCache().get(0)); + } + + @Test + public void testRefreshLocalCache() { + // mock 数据 01 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, + o -> o.setConfig(randomWxPayClientConfig()).setUpdateTime(addTime(Duration.ofMinutes(-2)))); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + channelService.initLocalCache(); + // mock 数据 02 + PayChannelDO dbChannel02 = randomPojo(PayChannelDO.class, + o -> o.setConfig(randomWxPayClientConfig())); + channelMapper.insert(dbChannel02);// @Sql: 先插入出一条存在的数据 + + // 调用 + channelService.refreshLocalCache(); + // 校验缓存 + assertEquals(2, channelService.getChannelCache().size()); + assertEquals(dbChannel, channelService.getChannelCache().get(0)); + assertEquals(dbChannel02, channelService.getChannelCache().get(1)); + } + + @Test + public void testCreateChannel_success() { + // 准备参数 + WxPayClientConfig config = randomWxPayClientConfig(); + PayChannelCreateReqVO reqVO = randomPojo(PayChannelCreateReqVO.class, o -> { + o.setStatus(randomCommonStatus()); + o.setCode(PayChannelEnum.WX_PUB.getCode()); + o.setConfig(JsonUtils.toJsonString(config)); + }); + + // 调用 + Long channelId = channelService.createChannel(reqVO); + // 校验记录的属性是否正确 + PayChannelDO channel = channelMapper.selectById(channelId); + assertPojoEquals(reqVO, channel, "config"); + assertPojoEquals(config, channel.getConfig()); + // 校验缓存 + assertEquals(1, channelService.getChannelCache().size()); + assertEquals(channel, channelService.getChannelCache().get(0)); + } + + @Test + public void testCreateChannel_exists() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, + o -> o.setConfig(randomWxPayClientConfig())); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + PayChannelCreateReqVO reqVO = randomPojo(PayChannelCreateReqVO.class, o -> { + o.setAppId(dbChannel.getAppId()); + o.setCode(dbChannel.getCode()); + }); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.createChannel(reqVO), CHANNEL_EXIST_SAME_CHANNEL_ERROR); + } + + @Test + public void testUpdateChannel_success() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + AlipayPayClientConfig config = randomAlipayPayClientConfig(); + PayChannelUpdateReqVO reqVO = randomPojo(PayChannelUpdateReqVO.class, o -> { + o.setId(dbChannel.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + o.setConfig(JsonUtils.toJsonString(config)); + }); + + // 调用 + channelService.updateChannel(reqVO); + // 校验是否更新正确 + PayChannelDO channel = channelMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, channel, "config"); + assertPojoEquals(config, channel.getConfig()); + // 校验缓存 + assertEquals(1, channelService.getChannelCache().size()); + assertEquals(channel, channelService.getChannelCache().get(0)); + } + + @Test + public void testUpdateChannel_notExists() { + // 准备参数 + AlipayPayClientConfig payClientPublicKeyConfig = randomAlipayPayClientConfig(); + PayChannelUpdateReqVO reqVO = randomPojo(PayChannelUpdateReqVO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setConfig(JSON.toJSONString(payClientPublicKeyConfig)); + }); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.updateChannel(reqVO), CHANNEL_NOT_FOUND); + } + + @Test + public void testDeleteChannel_success() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用 + channelService.deleteChannel(id); + // 校验数据不存在了 + assertNull(channelMapper.selectById(id)); + // 校验缓存 + assertEquals(0, channelService.getChannelCache().size()); + } + + @Test + public void testDeleteChannel_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.deleteChannel(id), CHANNEL_NOT_FOUND); + } + + @Test + public void testGetChannel() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用 + PayChannelDO channel = channelService.getChannel(id); + // 校验是否更新正确 + assertPojoEquals(dbChannel, channel); + } + + @Test + public void testGetChannelListByAppIds() { + // mock 数据 + PayChannelDO dbChannel01 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel01);// @Sql: 先插入出一条存在的数据 + PayChannelDO dbChannel02 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.WX_PUB.getCode()); + o.setConfig(randomWxPayClientConfig()); + }); + channelMapper.insert(dbChannel02);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long appId = dbChannel01.getAppId(); + + // 调用 + List channels = channelService.getChannelListByAppIds(Collections.singleton(appId)); + // 校验是否更新正确 + assertEquals(1, channels.size()); + assertPojoEquals(dbChannel01, channels.get(0)); + } + + @Test + public void testGetChannelByAppIdAndCode() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long appId = dbChannel.getAppId(); + String code = dbChannel.getCode();; + + // 调用 + PayChannelDO channel = channelService.getChannelByAppIdAndCode(appId, code); + // 断言 + assertPojoEquals(channel, dbChannel); + } + + @Test + public void testValidPayChannel_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.validPayChannel(id), CHANNEL_NOT_FOUND); + } + + @Test + public void testValidPayChannel_isDisable() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.validPayChannel(id), CHANNEL_IS_DISABLE); + } + + @Test + public void testValidPayChannel_success() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用 + PayChannelDO channel = channelService.validPayChannel(id); + // 断言异常 + assertPojoEquals(channel, dbChannel); + } + + @Test + public void testValidPayChannel_appIdAndCode() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long appId = dbChannel.getAppId(); + String code = dbChannel.getCode(); + + // 调用 + PayChannelDO channel = channelService.validPayChannel(appId, code); + // 断言异常 + assertPojoEquals(channel, dbChannel); + } + + @Test + public void testGetEnableChannelList() { + // 准备参数 + Long appId = randomLongId(); + // mock 数据 01(enable 不匹配) + PayChannelDO dbChannel01 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + }); + channelMapper.insert(dbChannel01);// @Sql: 先插入出一条存在的数据 + // mock 数据 02(appId 不匹配) + PayChannelDO dbChannel02 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel02);// @Sql: 先插入出一条存在的数据 + // mock 数据 03 + PayChannelDO dbChannel03 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setAppId(appId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel03);// @Sql: 先插入出一条存在的数据 + + // 调用 + List channel = channelService.getEnableChannelList(appId); + // 断言异常 + assertPojoEquals(channel, dbChannel03); + } + + public WxPayClientConfig randomWxPayClientConfig() { + return new WxPayClientConfig() + .setAppId(randomString()) + .setMchId(randomString()) + .setApiVersion(WxPayClientConfig.API_VERSION_V2) + .setMchKey(randomString()); + } + + public AlipayPayClientConfig randomAlipayPayClientConfig() { + return new AlipayPayClientConfig() + .setServerUrl(randomURL()) + .setAppId(randomString()) + .setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT) + .setMode(AlipayPayClientConfig.MODE_PUBLIC_KEY) + .setPrivateKey(randomString()) + .setAlipayPublicKey(randomString()); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceTest.java new file mode 100644 index 000000000..eb931f4fb --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceTest.java @@ -0,0 +1,351 @@ +package cn.iocoder.yudao.module.pay.service.notify; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +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.refund.PayRefundDO; +import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyLogMapper; +import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper; +import cn.iocoder.yudao.module.pay.dal.redis.notify.PayNotifyLockRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.iocoder.yudao.module.pay.framework.job.config.PayJobConfiguration; +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 org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * {@link PayRefundServiceImpl} 的单元测试类 + * + * @author 芋艿 + */ +@Import({PayJobConfiguration.class, PayNotifyServiceImpl.class, PayNotifyLockRedisDAO.class}) +public class PayNotifyServiceTest extends BaseDbUnitTest { + + @Resource + private PayNotifyServiceImpl notifyService; + + @MockBean + private PayOrderService orderService; + @MockBean + private PayRefundService refundService; + + @Resource + private PayNotifyTaskMapper notifyTaskMapper; + @Resource + private PayNotifyLogMapper notifyLogMapper; + + @MockBean + private RedissonClient redissonClient; + + @Test + public void testCreatePayNotifyTask_order() { + PayNotifyServiceImpl payNotifyService = mock(PayNotifyServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayNotifyServiceImpl.class))) + .thenReturn(payNotifyService); + + // 准备参数 + Integer type = PayNotifyTypeEnum.ORDER.getType(); + Long dataId = 1L; + // mock 方法(order) + PayOrderDO order = randomPojo(PayOrderDO.class); + when(orderService.getOrder(eq(1L))).thenReturn(order); + // mock 方法(lock) + mockLock(null); // null 的原因,是咱没办法拿到 taskId 新增 + + // 调用 + notifyService.createPayNotifyTask(type, dataId); + // 断言,task + PayNotifyTaskDO dbTask = notifyTaskMapper.selectOne(null); + assertNotNull(dbTask.getNextNotifyTime()); + assertThat(dbTask) + .extracting("type", "dataId", "status", "notifyTimes", "maxNotifyTimes", + "appId", "merchantOrderId", "notifyUrl") + .containsExactly(type, dataId, PayNotifyStatusEnum.WAITING.getStatus(), 0, 9, + order.getAppId(), order.getMerchantOrderId(), order.getNotifyUrl()); + // 断言,调用 + verify(payNotifyService).executeNotify0(eq(dbTask)); + } + } + + @Test + public void testCreatePayNotifyTask_refund() { + PayNotifyServiceImpl payNotifyService = mock(PayNotifyServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayNotifyServiceImpl.class))) + .thenReturn(payNotifyService); + + // 准备参数 + Integer type = PayNotifyTypeEnum.REFUND.getType(); + Long dataId = 1L; + // mock 方法(refund) + PayRefundDO refund = randomPojo(PayRefundDO.class); + when(refundService.getRefund(eq(1L))).thenReturn(refund); + // mock 方法(lock) + mockLock(null); // null 的原因,是咱没办法拿到 taskId 新增 + + // 调用 + notifyService.createPayNotifyTask(type, dataId); + // 断言,task + PayNotifyTaskDO dbTask = notifyTaskMapper.selectOne(null); + assertNotNull(dbTask.getNextNotifyTime()); + assertThat(dbTask) + .extracting("type", "dataId", "status", "notifyTimes", "maxNotifyTimes", + "appId", "merchantOrderId", "notifyUrl") + .containsExactly(type, dataId, PayNotifyStatusEnum.WAITING.getStatus(), 0, 9, + refund.getAppId(), refund.getMerchantOrderId(), refund.getNotifyUrl()); + // 断言,调用 + verify(payNotifyService).executeNotify0(eq(dbTask)); + } + } + + @Test + public void testExecuteNotify() throws InterruptedException { + // mock 数据(notify) + PayNotifyTaskDO dbTask01 = randomPojo(PayNotifyTaskDO.class, + o -> o.setStatus(PayNotifyStatusEnum.WAITING.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask01); + PayNotifyTaskDO dbTask02 = randomPojo(PayNotifyTaskDO.class, + o -> o.setStatus(PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask02); + PayNotifyTaskDO dbTask03 = randomPojo(PayNotifyTaskDO.class, + o -> o.setStatus(PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask03); + PayNotifyTaskDO dbTask04 = randomPojo(PayNotifyTaskDO.class, // 不满足状态 + o -> o.setStatus(PayNotifyStatusEnum.FAILURE.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask04); + PayNotifyTaskDO dbTask05 = randomPojo(PayNotifyTaskDO.class, // 不满足状态 + o -> o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask05); + PayNotifyTaskDO dbTask06 = randomPojo(PayNotifyTaskDO.class, // 不满足时间 + o -> o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(1)))); + notifyTaskMapper.insert(dbTask06); + // mock 方法(lock) + mockLock(dbTask01.getId()); + mockLock(dbTask02.getId()); + mockLock(dbTask03.getId()); + + // 调用 + int count = notifyService.executeNotify(); + // 断言,数量 + assertEquals(count, 3); + } + + @Test // 由于 HttpUtil 不好 mock,所以只测试异常的情况 + public void testExecuteNotify0_exception() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, o -> o.setType(-1) + .setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + + // 调用 + notifyService.executeNotify0(task); + // 断言,task + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()); + // 断言,log + PayNotifyLogDO dbLog = notifyLogMapper.selectOne(null); + assertEquals(dbLog.getTaskId(), task.getId()); + assertEquals(dbLog.getNotifyTimes(), 1); + assertTrue(dbLog.getResponse().contains("未知的通知任务类型:")); + assertEquals(dbLog.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()); + } + + @Test + public void testProcessNotifyResult_success() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.success(randomString()); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, null); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.SUCCESS.getStatus()); + } + + @Test + public void testProcessNotifyResult_failure() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(8).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.error(BAD_REQUEST); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, null); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 9); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.FAILURE.getStatus()); + } + + @Test + public void testProcessNotifyResult_requestFailure() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.error(BAD_REQUEST); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, null); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus()); + } + + @Test + public void testProcessNotifyResult_requestSuccess() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.error(BAD_REQUEST); + RuntimeException invokeException = new RuntimeException(); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, invokeException); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()); + } + + @Test + public void testGetNotifyTask() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class); + notifyTaskMapper.insert(task); + // 准备参数 + Long id = task.getId(); + + // 调用 + PayNotifyTaskDO dbTask = notifyService.getNotifyTask(id); + // 断言 + assertPojoEquals(dbTask, task); + } + + @Test + public void testGetNotifyTaskPage() { + // mock 数据 + PayNotifyTaskDO dbTask = randomPojo(PayNotifyTaskDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setType(PayNotifyTypeEnum.REFUND.getType()); + o.setDataId(100L); + o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()); + o.setMerchantOrderId("P110"); + o.setCreateTime(buildTime(2023, 2, 3)); + }); + notifyTaskMapper.insert(dbTask); + // 测试 appId 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setAppId(2L))); + // 测试 type 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setType(PayNotifyTypeEnum.ORDER.getType()))); + // 测试 dataId 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setDataId(200L))); + // 测试 status 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setStatus(PayNotifyStatusEnum.FAILURE.getStatus()))); + // 测试 merchantOrderId 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setMerchantOrderId(randomString()))); + // 测试 createTime 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setCreateTime(buildTime(2023, 1, 1)))); + // 准备参数 + PayNotifyTaskPageReqVO reqVO = new PayNotifyTaskPageReqVO(); + reqVO.setAppId(1L); + reqVO.setType(PayNotifyTypeEnum.REFUND.getType()); + reqVO.setDataId(100L); + reqVO.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()); + reqVO.setMerchantOrderId("P110"); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = notifyService.getNotifyTaskPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbTask, pageResult.getList().get(0)); + } + + @Test + public void testGetNotifyLogList() { + // mock 数据 + PayNotifyLogDO dbLog = randomPojo(PayNotifyLogDO.class); + notifyLogMapper.insert(dbLog); + PayNotifyLogDO dbLog02 = randomPojo(PayNotifyLogDO.class); + notifyLogMapper.insert(dbLog02); + // 准备参数 + Long taskId = dbLog.getTaskId(); + + // 调用 + List logList = notifyService.getNotifyLogList(taskId); + // 断言 + assertEquals(logList.size(), 1); + assertPojoEquals(dbLog, logList.get(0)); + } + + private void mockLock(Long id) { + RLock lock = mock(RLock.class); + if (id == null) { + when(redissonClient.getLock(anyString())) + .thenReturn(lock); + } else { + when(redissonClient.getLock(eq("pay_notify:lock:" + id))) + .thenReturn(lock); + } + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java new file mode 100755 index 000000000..6a5f15ce3 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java @@ -0,0 +1,1105 @@ +package cn.iocoder.yudao.module.pay.service.order; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +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.dal.dataobject.order.PayOrderExtensionDO; +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.redis.no.PayNoRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties; +import cn.iocoder.yudao.module.pay.service.app.PayAppService; +import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; +import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * {@link PayOrderServiceImpl} 的单元测试类 + * + * @author 芋艿 + */ +@Import({PayOrderServiceImpl.class, PayNoRedisDAO.class}) +public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { + + @Resource + private PayOrderServiceImpl orderService; + + @Resource + private PayOrderMapper orderMapper; + @Resource + private PayOrderExtensionMapper orderExtensionMapper; + + @MockBean + private PayClientFactory payClientFactory; + @MockBean + private PayProperties properties; + @MockBean + private PayAppService appService; + @MockBean + private PayChannelService channelService; + @MockBean + private PayNotifyService notifyService; + + @BeforeEach + public void setUp() { + when(properties.getOrderNotifyUrl()).thenReturn("http://127.0.0.1"); + } + + @Test + public void testGetOrder_id() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + + // 调用 + PayOrderDO dbOrder = orderService.getOrder(id); + // 断言 + assertPojoEquals(dbOrder, order); + } + + @Test + public void testGetOrder_appIdAndMerchantOrderId() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class); + orderMapper.insert(order); + // 准备参数 + Long appId = order.getAppId(); + String merchantOrderId = order.getMerchantOrderId(); + + // 调用 + PayOrderDO dbOrder = orderService.getOrder(appId, merchantOrderId); + // 断言 + assertPojoEquals(dbOrder, order); + } + + @Test + public void testGetOrderCountByAppId() { + // mock 数据(PayOrderDO) + PayOrderDO order01 = randomPojo(PayOrderDO.class); + orderMapper.insert(order01); + PayOrderDO order02 = randomPojo(PayOrderDO.class); + orderMapper.insert(order02); + // 准备参数 + Long appId = order01.getAppId(); + + // 调用 + Long count = orderService.getOrderCountByAppId(appId); + // 断言 + assertEquals(count, 1L); + } + + @Test + public void testGetOrderPage() { + // mock 数据 + PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("110"); + o.setChannelOrderNo("220"); + o.setNo("330"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setCreateTime(buildTime(2018, 1, 15)); + }); + orderMapper.insert(dbOrder); + // 测试 appId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setMerchantOrderId(randomString()))); + // 测试 channelOrderNo 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelOrderNo(randomString()))); + // 测试 no 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setNo(randomString()))); + // 测试 status 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()))); + // 测试 createTime 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(buildTime(2019, 1, 1)))); + // 准备参数 + PayOrderPageReqVO reqVO = new PayOrderPageReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("11"); + reqVO.setChannelOrderNo("22"); + reqVO.setNo("33"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2018, 1, 10, 2018, 1, 30)); + + // 调用 + PageResult pageResult = orderService.getOrderPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbOrder, pageResult.getList().get(0)); + } + + @Test + public void testGetOrderList() { + // mock 数据 + PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("110"); + o.setChannelOrderNo("220"); + o.setNo("330"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setCreateTime(buildTime(2018, 1, 15)); + }); + orderMapper.insert(dbOrder); + // 测试 appId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setMerchantOrderId(randomString()))); + // 测试 channelOrderNo 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelOrderNo(randomString()))); + // 测试 no 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setNo(randomString()))); + // 测试 status 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()))); + // 测试 createTime 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(buildTime(2019, 1, 1)))); + // 准备参数 + PayOrderExportReqVO reqVO = new PayOrderExportReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("11"); + reqVO.setChannelOrderNo("22"); + reqVO.setNo("33"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2018, 1, 10, 2018, 1, 30)); + + // 调用 + List list = orderService.getOrderList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbOrder, list.get(0)); + } + + @Test + public void testCreateOrder_success() { + // mock 参数 + PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("10") + .setSubject(randomString()).setBody(randomString())); + // mock 方法 + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1")); + when(appService.validPayApp(eq(reqDTO.getAppId()))).thenReturn(app); + + // 调用 + Long orderId = orderService.createOrder(reqDTO); + // 断言 + PayOrderDO order = orderMapper.selectById(orderId); + assertPojoEquals(order, reqDTO); + assertEquals(order.getAppId(), 1L); + assertEquals(order.getNotifyUrl(), "http://127.0.0.1"); + assertEquals(order.getStatus(), PayOrderStatusEnum.WAITING.getStatus()); + assertEquals(order.getRefundPrice(), 0); + } + + @Test + public void testCreateOrder_exists() { + // mock 参数 + PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("10")); + // mock 数据 + PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> o.setAppId(1L).setMerchantOrderId("10")); + orderMapper.insert(dbOrder); + + // 调用 + Long orderId = orderService.createOrder(reqDTO); + // 断言 + PayOrderDO order = orderMapper.selectById(orderId); + assertPojoEquals(dbOrder, order); + } + + @Test + public void testSubmitOrder_notFound() { + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_NOT_FOUND); + } + + @Test + public void testSubmitOrder_notWaiting() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus())); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_STATUS_IS_NOT_WAITING); + } + + @Test + public void testSubmitOrder_isSuccess() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_STATUS_IS_SUCCESS); + } + + @Test + public void testSubmitOrder_expired() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofDays(-1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_IS_EXPIRED); + } + + @Test + public void testSubmitOrder_channelNotFound() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()) + .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + String userIp = randomString(); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode()))) + .thenReturn(channel); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), CHANNEL_NOT_FOUND); + } + + @Test // 调用 unifiedOrder 接口,返回存在渠道错误 + public void testSubmitOrder_channelError() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()) + .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + String userIp = randomString(); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode()))) + .thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法() + PayOrderRespDTO unifiedOrderResp = randomPojo(PayOrderRespDTO.class, o -> + o.setChannelErrorCode("001").setChannelErrorMsg("模拟异常")); + when(client.unifiedOrder(argThat(payOrderUnifiedReqDTO -> { + assertNotNull(payOrderUnifiedReqDTO.getOutTradeNo()); + assertThat(payOrderUnifiedReqDTO) + .extracting("subject", "body", "notifyUrl", "returnUrl", "price", "expireTime") + .containsExactly(order.getSubject(), order.getBody(), "http://127.0.0.1/10", + reqVO.getReturnUrl(), order.getPrice(), order.getExpireTime()); + return true; + }))).thenReturn(unifiedOrderResp); + + // 调用,并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), + ORDER_SUBMIT_CHANNEL_ERROR, "001", "模拟异常"); + // 断言,数据记录(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectOne(null); + assertNotNull(orderExtension); + assertThat(orderExtension).extracting("no", "orderId").isNotNull(); + assertThat(orderExtension) + .extracting("channelId", "channelCode","userIp" ,"status", "channelExtras", + "channelErrorCode", "channelErrorMsg", "channelNotifyData") + .containsExactly(10L, PayChannelEnum.ALIPAY_APP.getCode(), userIp, + PayOrderStatusEnum.WAITING.getStatus(), reqVO.getChannelExtras(), + null, null, null); + } + } + + @Test + public void testSubmitOrder_success() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()) + .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + String userIp = randomString(); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode()))) + .thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(支付渠道的调用) + PayOrderRespDTO unifiedOrderResp = randomPojo(PayOrderRespDTO.class, o -> o.setChannelErrorCode(null).setChannelErrorMsg(null) + .setDisplayMode(PayOrderDisplayModeEnum.URL.getMode()).setDisplayContent("tudou")); + when(client.unifiedOrder(argThat(payOrderUnifiedReqDTO -> { + assertNotNull(payOrderUnifiedReqDTO.getOutTradeNo()); + assertThat(payOrderUnifiedReqDTO) + .extracting("subject", "body", "notifyUrl", "returnUrl", "price", "expireTime") + .containsExactly(order.getSubject(), order.getBody(), "http://127.0.0.1/10", + reqVO.getReturnUrl(), order.getPrice(), order.getExpireTime()); + return true; + }))).thenReturn(unifiedOrderResp); + + // 调用 + PayOrderSubmitRespVO result = orderService.submitOrder(reqVO, userIp); + // 断言,数据记录(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectOne(null); + assertNotNull(orderExtension); + assertThat(orderExtension).extracting("no", "orderId").isNotNull(); + assertThat(orderExtension) + .extracting("channelId", "channelCode","userIp" ,"status", "channelExtras", + "channelErrorCode", "channelErrorMsg", "channelNotifyData") + .containsExactly(10L, PayChannelEnum.ALIPAY_APP.getCode(), userIp, + PayOrderStatusEnum.WAITING.getStatus(), reqVO.getChannelExtras(), + null, null, null); + // 断言,返回(PayOrderSubmitRespVO) + assertThat(result) + .extracting("status", "displayMode", "displayContent") + .containsExactly(PayOrderStatusEnum.WAITING.getStatus(), PayOrderDisplayModeEnum.URL.getMode(), "tudou"); + // 断言,调用 + verify(payOrderServiceImpl).notifyOrder(same(channel), same(unifiedOrderResp)); + } + } + + @Test + public void testValidateOrderActuallyPaid_dbPaid() { + // 准备参数 + Long id = randomLongId(); + // mock 方法(OrderExtension 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + orderExtensionMapper.insert(orderExtension); + + // 调用,并断言异常 + assertServiceException(() -> orderService.validateOrderActuallyPaid(id), + ORDER_EXTENSION_IS_PAID); + } + + @Test + public void testValidateOrderActuallyPaid_remotePaid() { + // 准备参数 + Long id = randomLongId(); + // mock 方法(OrderExtension 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient 已支付) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client); + when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()))); + + // 调用,并断言异常 + assertServiceException(() -> orderService.validateOrderActuallyPaid(id), + ORDER_EXTENSION_IS_PAID); + } + + @Test + public void testValidateOrderActuallyPaid_success() { + // 准备参数 + Long id = randomLongId(); + // mock 方法(OrderExtension 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient 已支付) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client); + when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); + + // 调用,并断言异常 + orderService.validateOrderActuallyPaid(id); + } + + @Test + public void testNotifyOrder_channelId() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + // 准备参数 + Long channelId = 10L; + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + orderService.notifyOrder(channelId, notify); + // 断言 + verify(payOrderServiceImpl).notifyOrder(same(channel), same(notify)); + } + } + + @Test + public void testNotifyOrderSuccess_orderExtension_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_EXTENSION_NOT_FOUND); + } + + @Test + public void testNotifyOrderSuccess_orderExtension_closed() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyOrderSuccess_order_notFound() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_NOT_FOUND); + // 断言 PayOrderExtensionDO :数据更新被回滚 + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderSuccess_order_closed() { + testNotifyOrderSuccess_order_closedOrRefund(PayOrderStatusEnum.CLOSED.getStatus()); + } + + @Test + public void testNotifyOrderSuccess_order_refund() { + testNotifyOrderSuccess_order_closedOrRefund(PayOrderStatusEnum.REFUND.getStatus()); + } + + private void testNotifyOrderSuccess_order_closedOrRefund(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status)); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110") + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_STATUS_IS_NOT_WAITING); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderSuccess_order_paid() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110") + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // 重要:需要将 order 的 extensionId 更新下 + order.setExtensionId(orderExtension.getId()); + orderMapper.updateById(order); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + // 断言 PayOrderDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(order, orderMapper.selectOne(null)); + // 断言,调用 + verify(notifyService, never()).createPayNotifyTask(anyInt(), anyLong()); + } + + @Test + public void testNotifyOrderSuccess_order_waiting() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setPrice(10)); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setNo("P110") + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setFeeRate(0.1D)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + orderExtension.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setChannelNotifyData(toJsonString(notify)); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + // 断言 PayOrderDO :数据未更新,因为它是 SUCCESS + order.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setChannelId(10L).setChannelCode(channel.getCode()) + .setSuccessTime(notify.getSuccessTime()).setExtensionId(orderExtension.getId()).setNo(orderExtension.getNo()) + .setChannelOrderNo(notify.getChannelOrderNo()).setChannelUserId(notify.getChannelUserId()) + .setChannelFeeRate(0.1D).setChannelFeePrice(1); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + // 断言,调用 + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.ORDER.getType()), + eq(orderExtension.getOrderId())); + } + + @Test + public void testNotifyOrderClosed_orderExtension_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_EXTENSION_NOT_FOUND); + } + + @Test + public void testNotifyOrderClosed_orderExtension_closed() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 CLOSED + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderClosed_orderExtension_paid() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderClosed_orderExtension_refund() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyOrderClosed_orderExtension_waiting() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO + orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus()).setChannelNotifyData(toJsonString(notify)) + .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + } + + @Test + public void testUpdateOrderRefundPrice_notFound() { + // 准备参数 + Long id = randomLongId(); + Integer incrRefundPrice = randomInteger(); + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + ORDER_NOT_FOUND); + } + + @Test + public void testUpdateOrderRefundPrice_waiting() { + testUpdateOrderRefundPrice_waitingOrClosed(PayOrderStatusEnum.WAITING.getStatus()); + } + + @Test + public void testUpdateOrderRefundPrice_closed() { + testUpdateOrderRefundPrice_waitingOrClosed(PayOrderStatusEnum.CLOSED.getStatus()); + } + + private void testUpdateOrderRefundPrice_waitingOrClosed(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(status)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = randomInteger(); + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + ORDER_REFUND_FAIL_STATUS_ERROR); + } + + @Test + public void testUpdateOrderRefundPrice_priceExceed() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setRefundPrice(1).setPrice(10)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = 10; + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + REFUND_PRICE_EXCEED); + } + + @Test + public void testUpdateOrderRefundPrice_refund() { + testUpdateOrderRefundPrice_refundOrSuccess(PayOrderStatusEnum.REFUND.getStatus()); + } + + @Test + public void testUpdateOrderRefundPrice_success() { + testUpdateOrderRefundPrice_refundOrSuccess(PayOrderStatusEnum.SUCCESS.getStatus()); + } + + private void testUpdateOrderRefundPrice_refundOrSuccess(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(status).setRefundPrice(1).setPrice(10)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = 8; + + // 调用 + orderService.updateOrderRefundPrice(id, incrRefundPrice); + // 断言 + order.setRefundPrice(9).setStatus(PayOrderStatusEnum.REFUND.getStatus()); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + } + + @Test + public void testGetOrderExtension() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + Long id = orderExtension.getId(); + + // 调用 + PayOrderExtensionDO dbOrderExtension = orderService.getOrderExtension(id); + // 断言 + assertPojoEquals(dbOrderExtension, orderExtension); + } + + @Test + public void testSyncOrder_payClientNotFound() { + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 0); + } + + @Test + public void testSyncOrder_exception() { + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setChannelId(10L) + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 异常) + when(client.getOrder(any())).thenThrow(new RuntimeException()); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 0); + } + + @Test + public void testSyncOrder_orderSuccess() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setChannelId(10L).setNo("P110") + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 成功返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + // mock 方法(PayChannelDO) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 1); + verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO)); + } + } + + @Test + public void testSyncOrder_orderClosed() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setChannelId(10L).setNo("P110") + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 成功返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + // mock 方法(PayChannelDO) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 0); + verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO)); + } + } + + @Test + public void testExpireOrder_orderExtension_isSuccess() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + } + + @Test + public void testExpireOrder_payClient_notFound() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()) + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + } + + @Test + public void testExpireOrder_getOrder_isRefund() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 退款返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + } + + @Test + public void testExpireOrder_getOrder_isSuccess() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 成功返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + // mock 方法(PayChannelDO) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO)); + } + } + + @Test + public void testExpireOrder_success() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 关闭返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 1); + // 断言 extension 变化 + orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setChannelNotifyData(toJsonString(respDTO)); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + // 断言 order 变化 + order.setStatus(PayOrderStatusEnum.CLOSED.getStatus()); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java new file mode 100755 index 000000000..b0b31fbc4 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java @@ -0,0 +1,703 @@ +package cn.iocoder.yudao.module.pay.service.refund; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +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.dal.dataobject.refund.PayRefundDO; +import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper; +import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; +import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties; +import cn.iocoder.yudao.module.pay.service.app.PayAppService; +import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; +import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; +import cn.iocoder.yudao.module.pay.service.order.PayOrderService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * {@link PayRefundServiceImpl} 的单元测试类 + * + * @author 芋艿 + */ +@Import({PayRefundServiceImpl.class, PayNoRedisDAO.class}) +public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { + + @Resource + private PayRefundServiceImpl refundService; + + @Resource + private PayRefundMapper refundMapper; + + @MockBean + private PayProperties payProperties; + @MockBean + private PayClientFactory payClientFactory; + @MockBean + private PayOrderService orderService; + @MockBean + private PayAppService appService; + @MockBean + private PayChannelService channelService; + @MockBean + private PayNotifyService notifyService; + + @BeforeEach + public void setUp() { + when(payProperties.getRefundNotifyUrl()).thenReturn("http://127.0.0.1"); + } + + @Test + public void testGetRefund() { + // mock 数据 + PayRefundDO refund = randomPojo(PayRefundDO.class); + refundMapper.insert(refund); + // 准备参数 + Long id = refund.getId(); + + // 调用 + PayRefundDO dbRefund = refundService.getRefund(id); + // 断言 + assertPojoEquals(dbRefund, refund); + } + + @Test + public void testGetRefundCountByAppId() { + // mock 数据 + PayRefundDO refund01 = randomPojo(PayRefundDO.class); + refundMapper.insert(refund01); + PayRefundDO refund02 = randomPojo(PayRefundDO.class); + refundMapper.insert(refund02); + // 准备参数 + Long appId = refund01.getAppId(); + + // 调用 + Long count = refundService.getRefundCountByAppId(appId); + // 断言 + assertEquals(count, 1); + } + + @Test + public void testGetRefundPage() { + // mock 数据 + PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("MOT0000001"); + o.setMerchantRefundId("MRF0000001"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setChannelOrderNo("CH0000001"); + o.setChannelRefundNo("CHR0000001"); + o.setCreateTime(buildTime(2021, 1, 10)); + }); + refundMapper.insert(dbRefund); + // 测试 appId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantOrderId(randomString()))); + // 测试 merchantRefundId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId(randomString()))); + // 测试 channelOrderNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelOrderNo(randomString()))); + // 测试 channelRefundNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelRefundNo(randomString()))); + // 测试 status 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); + // 测试 createTime 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + PayRefundPageReqVO reqVO = new PayRefundPageReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("MOT0000001"); + reqVO.setMerchantRefundId("MRF0000001"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setChannelOrderNo("CH0000001"); + reqVO.setChannelRefundNo("CHR0000001"); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 9, 2021, 1, 11)); + + // 调用 + PageResult pageResult = refundService.getRefundPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbRefund, pageResult.getList().get(0)); + } + + @Test + public void testGetRefundList() { + // mock 数据 + PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("MOT0000001"); + o.setMerchantRefundId("MRF0000001"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setChannelOrderNo("CH0000001"); + o.setChannelRefundNo("CHR0000001"); + o.setCreateTime(buildTime(2021, 1, 10)); + }); + refundMapper.insert(dbRefund); + // 测试 appId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantOrderId(randomString()))); + // 测试 merchantRefundId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId(randomString()))); + // 测试 channelOrderNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelOrderNo(randomString()))); + // 测试 channelRefundNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelRefundNo(randomString()))); + // 测试 status 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); + // 测试 createTime 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + PayRefundExportReqVO reqVO = new PayRefundExportReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("MOT0000001"); + reqVO.setMerchantRefundId("MRF0000001"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setChannelOrderNo("CH0000001"); + reqVO.setChannelRefundNo("CHR0000001"); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 9, 2021, 1, 11)); + + // 调用 + List list = refundService.getRefundList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbRefund, list.get(0)); + } + + @Test + public void testCreateRefund_orderNotFound() { + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + ORDER_NOT_FOUND); + } + + @Test + public void testCreateRefund_orderWaiting() { + testCreateRefund_orderWaitingOrClosed(PayOrderStatusEnum.WAITING.getStatus()); + } + + @Test + public void testCreateRefund_orderClosed() { + testCreateRefund_orderWaitingOrClosed(PayOrderStatusEnum.CLOSED.getStatus()); + } + + private void testCreateRefund_orderWaitingOrClosed(Integer status) { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + ORDER_REFUND_FAIL_STATUS_ERROR); + } + + @Test + public void testCreateRefund_refundPriceExceed() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + REFUND_PRICE_EXCEED); + } + + @Test + public void testCreateRefund_orderHasRefunding() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 数据(refund 在退款中) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> + o.setOrderId(order.getId()).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + REFUND_PRICE_EXCEED); + } + + @Test + public void testCreateRefund_channelNotFound() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(1L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L))).thenReturn(channel); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + CHANNEL_NOT_FOUND); + } + + @Test + public void testCreateRefund_refundExists() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(1L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L))).thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 数据(refund 已存在) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> + o.setAppId(1L).setMerchantRefundId("200")); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + REFUND_EXISTS); + } + + @Test + public void testCreateRefund_invokeException() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(10L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 调用发生异常) + when(client.unifiedRefund(any(PayRefundUnifiedReqDTO.class))).thenThrow(new RuntimeException()); + + // 调用 + Long refundId = refundService.createPayRefund(reqDTO); + // 断言 + PayRefundDO refundDO = refundMapper.selectById(refundId); + assertPojoEquals(reqDTO, refundDO); + assertNotNull(refundDO.getNo()); + assertThat(refundDO) + .extracting("orderId", "orderNo", "channelId", "channelCode", + "notifyUrl", "channelOrderNo", "status", "payPrice", "refundPrice") + .containsExactly(order.getId(), order.getNo(), channel.getId(), channel.getCode(), + app.getRefundNotifyUrl(), order.getChannelOrderNo(), PayRefundStatusEnum.WAITING.getStatus(), + order.getPrice(), reqDTO.getPrice()); + } + + @Test + public void testCreateRefund_invokeSuccess() { + PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class))) + .thenReturn(payRefundServiceImpl); + + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(10L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 成功) + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class); + when(client.unifiedRefund(argThat(unifiedReqDTO -> { + assertNotNull(unifiedReqDTO.getOutRefundNo()); + assertThat(unifiedReqDTO) + .extracting("payPrice", "refundPrice", "outTradeNo", + "notifyUrl", "reason") + .containsExactly(order.getPrice(), reqDTO.getPrice(), order.getNo(), + "http://127.0.0.1/10", reqDTO.getReason()); + return true; + }))).thenReturn(refundRespDTO); + + // 调用 + Long refundId = refundService.createPayRefund(reqDTO); + // 断言 + PayRefundDO refundDO = refundMapper.selectById(refundId); + assertPojoEquals(reqDTO, refundDO); + assertNotNull(refundDO.getNo()); + assertThat(refundDO) + .extracting("orderId", "orderNo", "channelId", "channelCode", + "notifyUrl", "channelOrderNo", "status", "payPrice", "refundPrice") + .containsExactly(order.getId(), order.getNo(), channel.getId(), channel.getCode(), + app.getRefundNotifyUrl(), order.getChannelOrderNo(), PayRefundStatusEnum.WAITING.getStatus(), + order.getPrice(), reqDTO.getPrice()); + // 断言调用 + verify(payRefundServiceImpl).notifyRefund(same(channel), same(refundRespDTO)); + } + } + + @Test + public void testNotifyRefund() { + PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class))) + .thenReturn(payRefundServiceImpl); + + // 准备参数 + Long channelId = 10L; + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + refundService.notifyRefund(channelId, refundRespDTO); + // 断言 + verify(payRefundServiceImpl).notifyRefund(same(channel), same(refundRespDTO)); + } + } + + @Test + public void testNotifyRefundSuccess_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_NOT_FOUND); + } + + @Test + public void testNotifyRefundSuccess_isSuccess() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund 没有更新,因为已经退款成功 + assertPojoEquals(refund, refundMapper.selectById(refund.getId())); + } + + @Test + public void testNotifyRefundSuccess_failure() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.FAILURE.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyRefundSuccess_updateOrderException() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderId(100L).setRefundPrice(23)); + refundMapper.insert(refund); + // mock 方法(order + 更新异常) + doThrow(new RuntimeException()).when(orderService) + .updateOrderRefundPrice(eq(100L), eq(23)); + + // 调用,并断言异常 + assertThrows(RuntimeException.class, () -> refundService.notifyRefund(channel, refundRespDTO)); + // 断言,refund 没有更新,因为事务回滚了 + assertPojoEquals(refund, refundMapper.selectById(refund.getId())); + } + + @Test + public void testNotifyRefundSuccess_success() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderId(100L).setRefundPrice(23)); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund + refund.setSuccessTime(refundRespDTO.getSuccessTime()) + .setChannelRefundNo(refundRespDTO.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus()) + .setChannelNotifyData(toJsonString(refundRespDTO)); + assertPojoEquals(refund, refundMapper.selectById(refund.getId()), + "updateTime", "updater"); + // 断言,调用 + verify(orderService).updateOrderRefundPrice(eq(100L), eq(23)); + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.REFUND.getType()), + eq(refund.getId())); + } + + @Test + public void testNotifyRefundFailure_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_NOT_FOUND); + } + + @Test + public void testNotifyRefundFailure_isFailure() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 退款失败) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.FAILURE.getStatus())); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund 没有更新,因为已经退款失败 + assertPojoEquals(refund, refundMapper.selectById(refund.getId())); + } + + @Test + public void testNotifyRefundFailure_isSuccess() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyRefundFailure_success() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderId(100L).setRefundPrice(23)); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund + refund.setChannelRefundNo(refundRespDTO.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.FAILURE.getStatus()) + .setChannelNotifyData(toJsonString(refundRespDTO)) + .setChannelErrorCode(refundRespDTO.getChannelErrorCode()) + .setChannelErrorMsg(refundRespDTO.getChannelErrorMsg()); + assertPojoEquals(refund, refundMapper.selectById(refund.getId()), + "updateTime", "updater"); + // 断言,调用 + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.REFUND.getType()), + eq(refund.getId())); + } + + @Test + public void testSyncRefund_notFound() { + // 准备参数 + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L) + .setStatus(PayRefundStatusEnum.WAITING.getStatus())); + refundMapper.insert(refund); + + // 调用 + int count = refundService.syncRefund(); + // 断言 + assertEquals(count, 0); + } + + @Test + public void testSyncRefund_waiting() { + assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.WAITING.getStatus()), 0); + } + + @Test + public void testSyncRefund_success() { + assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.SUCCESS.getStatus()), 1); + } + + @Test + public void testSyncRefund_failure() { + assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.FAILURE.getStatus()), 1); + } + + private int testSyncRefund_waitingOrSuccessOrFailure(Integer status) { + PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class))) + .thenReturn(payRefundServiceImpl); + + // 准备参数 + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setChannelId(10L) + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderNo("P110").setNo("R220")); + refundMapper.insert(refund); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 返回指定状态) + PayRefundRespDTO respDTO = randomPojo(PayRefundRespDTO.class, o -> o.setStatus(status)); + when(client.getRefund(eq("P110"), eq("R220"))).thenReturn(respDTO); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + return refundService.syncRefund(); + } + } + + @Test + public void testSyncRefund_exception() { + // 准备参数 + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setChannelId(10L) + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderNo("P110").setNo("R220")); + refundMapper.insert(refund); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 抛出异常) + when(client.getRefund(eq("P110"), eq("R220"))).thenThrow(new RuntimeException()); + + // 调用 + int count = refundService.syncRefund(); + // 断言 + assertEquals(count, 0); + } + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/resources/application-unit-test.yaml b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 000000000..d9e2f811e --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,54 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +mybatis-plus: + global-config: + db-config: + id-type: AUTO # H2 主键递增 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + info: + base-package: cn.iocoder.yudao.module diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/resources/logback.xml b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/logback.xml new file mode 100644 index 000000000..daf756bff --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/clean.sql b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/clean.sql new file mode 100644 index 000000000..91fff0cae --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,7 @@ +DELETE FROM pay_app; +DELETE FROM pay_channel; +DELETE FROM pay_order; +DELETE FROM pay_order_extension; +DELETE FROM pay_refund; +DELETE FROM pay_notify_task; +DELETE FROM pay_notify_log; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 000000000..6ae2ce2d4 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,146 @@ +CREATE TABLE IF NOT EXISTS "pay_app" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(64) NOT NULL, + "status" tinyint NOT NULL, + "remark" varchar(255) DEFAULT NULL, + `order_notify_url` varchar(1024) NOT NULL, + `refund_notify_url` varchar(1024) NOT 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, + PRIMARY KEY ("id") +) COMMENT = '支付应用'; + +CREATE TABLE IF NOT EXISTS "pay_channel" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(32) NOT NULL, + "status" tinyint(4) NOT NULL, + "remark" varchar(255) DEFAULT NULL, + "fee_rate" double NOT NULL DEFAULT 0, + "app_id" bigint(20) NOT NULL, + "config" varchar(10240) NOT NULL, + "creator" varchar(64) NULL DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) NULL 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 = '支付渠道'; + +CREATE TABLE IF NOT EXISTS `pay_order` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `app_id` bigint(20) NOT NULL, + `channel_id` bigint(20) DEFAULT NULL, + `channel_code` varchar(32) DEFAULT NULL, + `merchant_order_id` varchar(64) NOT NULL, + `subject` varchar(32) NOT NULL, + `body` varchar(128) NOT NULL, + `notify_url` varchar(1024) NOT NULL, + `price` bigint(20) NOT NULL, + `channel_fee_rate` double DEFAULT 0, + `channel_fee_price` bigint(20) DEFAULT 0, + `status` tinyint(4) NOT NULL, + `user_ip` varchar(50) NOT NULL, + `expire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `success_time` datetime(0) DEFAULT CURRENT_TIMESTAMP, + `notify_time` datetime(0) DEFAULT CURRENT_TIMESTAMP, + `extension_id` bigint(20) DEFAULT NULL, + `no` varchar(64) NULL, + `refund_price` bigint(20) NOT NULL, + `channel_user_id` varchar(255) DEFAULT NULL, + `channel_order_no` varchar(64) DEFAULT NULL, + `creator` varchar(64) DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) 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_order_extension` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `no` varchar(64) NOT NULL, + `order_id` bigint(20) NOT NULL, + `channel_id` bigint(20) NOT NULL, + `channel_code` varchar(32) NOT NULL, + `user_ip` varchar(50) NULL DEFAULT NULL, + `status` tinyint(4) NOT NULL, + `channel_extras` varchar(1024) NULL DEFAULT NULL, + `channel_error_code` varchar(64) NULL, + `channel_error_msg` varchar(64) NULL, + `channel_notify_data` varchar(1024) 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_refund` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `no` varchar(64) NOT NULL, + `app_id` bigint(20) NOT NULL, + `channel_id` bigint(20) NOT NULL, + `channel_code` varchar(32) NOT NULL, + `order_id` bigint(20) NOT NULL, + `order_no` varchar(64) NOT NULL, + `merchant_order_id` varchar(64) NOT NULL, + `merchant_refund_id` varchar(64) NOT NULL, + `notify_url` varchar(1024) NOT NULL, + `status` tinyint(4) NOT NULL, + `pay_price` bigint(20) NOT NULL, + `refund_price` bigint(20) NOT NULL, + `reason` varchar(256) NOT NULL, + `user_ip` varchar(50) NULL DEFAULT NULL, + `channel_order_no` varchar(64) NOT NULL, + `channel_refund_no` varchar(64) NULL DEFAULT NULL, + `success_time` datetime(0) NULL DEFAULT NULL, + `channel_error_code` varchar(128) NULL DEFAULT NULL, + `channel_error_msg` varchar(256) NULL DEFAULT NULL, + `channel_notify_data` varchar(1024) 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_notify_task` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `app_id` bigint(20) NOT NULL, + `type` tinyint(4) NOT NULL, + `data_id` bigint(20) NOT NULL, + `merchant_order_id` varchar(64) NOT NULL, + `status` tinyint(4) NOT NULL, + `next_notify_time` datetime(0) NULL DEFAULT NULL, + `last_execute_time` datetime(0) NULL DEFAULT NULL, + `notify_times` int NOT NULL, + `max_notify_times` int NOT NULL, + `notify_url` varchar(1024) 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, + `tenant_id` bigint(20) NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +) COMMENT = '支付通知任务'; + +CREATE TABLE IF NOT EXISTS `pay_notify_log` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `task_id` bigint(20) NOT NULL, + `notify_times` int NOT NULL, + `response` varchar(1024) NOT NULL, + `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 = '支付通知日志';