增加 pay 支付服务

This commit is contained in:
YunaiV
2023-07-27 19:55:15 +08:00
parent 9ba06ec07e
commit c92f1c44a6
152 changed files with 10914 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-module-pay</artifactId>
<groupId>cn.iocoder.cloud</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-pay-api</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
pay 模块 API暴露给其它模块调用
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>io.swagger.core.v3</groupId> <!-- 接口文档:使用最新版本的 Swagger 模型 -->
<artifactId>swagger-annotations</artifactId>
<scope>provided</scope>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<optional>true</optional>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
/**
* 占位符,无特殊作用
*/
package cn.iocoder.yudao.module.pay.api.notify;

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
// ========== 渠道相关字段 ==========
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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";
}

View File

@@ -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"; // 回调状态
}

View File

@@ -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, "发起退款失败,退款单金额不匹配");
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}