diff --git a/logs/app-server.log.2025-06-04.0.gz b/logs/app-server.log.2025-06-04.0.gz new file mode 100644 index 0000000..1e64eb5 Binary files /dev/null and b/logs/app-server.log.2025-06-04.0.gz differ diff --git a/tashow-dependencies/pom.xml b/tashow-dependencies/pom.xml index c427091..516a5eb 100644 --- a/tashow-dependencies/pom.xml +++ b/tashow-dependencies/pom.xml @@ -151,6 +151,32 @@ ${revision} + + com.tashow.cloud + tashow-trade-api + ${revision} + + + com.tashow.cloud + tashow-module-trade + ${revision} + + + com.tashow.cloud + tashow-pay-api + ${revision} + + + com.tashow.cloud + tashow-module-pay + ${revision} + + + com.tashow.cloud + tashow-sdk-payment + ${revision} + + diff --git a/tashow-feign/pom.xml b/tashow-feign/pom.xml index feb8d49..51f81e6 100644 --- a/tashow-feign/pom.xml +++ b/tashow-feign/pom.xml @@ -13,6 +13,8 @@ tashow-infra-api tashow-system-api + tashow-trade-api + tashow-pay-api diff --git a/tashow-feign/tashow-pay-api/pom.xml b/tashow-feign/tashow-pay-api/pom.xml new file mode 100644 index 0000000..4474ffc --- /dev/null +++ b/tashow-feign/tashow-pay-api/pom.xml @@ -0,0 +1,44 @@ + + + + com.tashow.cloud + tashow-feign + ${revision} + + 4.0.0 + tashow-pay-api + jar + + ${project.artifactId} + + pay 模块 API,暴露给其它模块调用 + + + + + com.tashow.cloud + tashow-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + true + + + com.tashow.cloud + tashow-sdk-payment + + + + diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/dto/PayOrderNotifyReqDTO.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/dto/PayOrderNotifyReqDTO.java new file mode 100644 index 0000000..7bb6dd3 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/dto/PayOrderNotifyReqDTO.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.payapi.api.notify.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 支付单的通知 Request DTO + * + * @author 芋道源码 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderNotifyReqDTO { + + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单号不能为空") + private String merchantOrderId; + + /** + * 支付订单编号 + */ + @NotNull(message = "支付订单编号不能为空") + private Long payOrderId; + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/dto/PayRefundNotifyReqDTO.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/dto/PayRefundNotifyReqDTO.java new file mode 100644 index 0000000..f5c4ed3 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/dto/PayRefundNotifyReqDTO.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.payapi.api.notify.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 退款单的通知 Request DTO + * + * @author 芋道源码 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayRefundNotifyReqDTO { + + /** + * 商户退款单编号 + */ + @NotEmpty(message = "商户退款单编号不能为空") + private String merchantOrderId; + + /** + * 支付退款编号 + */ + @NotNull(message = "支付退款编号不能为空") + private Long payRefundId; + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/dto/PayTransferNotifyReqDTO.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/dto/PayTransferNotifyReqDTO.java new file mode 100644 index 0000000..f9e8baf --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/dto/PayTransferNotifyReqDTO.java @@ -0,0 +1,26 @@ +package com.tashow.cloud.payapi.api.notify.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 转账单的通知 Request DTO + * + * @author jason + */ +@Data +public class PayTransferNotifyReqDTO { + + /** + * 商户转账单号 + */ + @NotEmpty(message = "商户转账单号不能为空") + private String merchantTransferId; + + /** + * 转账订单编号 + */ + @NotNull(message = "转账订单编号不能为空") + private Long payTransferId; +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/package-info.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/notify/package-info.java new file mode 100644 index 0000000..e69de29 diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/order/PayOrderApi.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/order/PayOrderApi.java new file mode 100644 index 0000000..638cb62 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/order/PayOrderApi.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.payapi.api.order; + +import com.tashow.cloud.common.pojo.CommonResult; +import com.tashow.cloud.payapi.api.order.dto.PayOrderCreateReqDTO; +import com.tashow.cloud.payapi.enums.ApiConstants; +import com.tashow.cloud.sdk.payment.dto.order.PayOrderRespDTO; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +/** + * RPC 服务 - 支付单 + */ +@FeignClient(name = ApiConstants.NAME) // +public interface PayOrderApi { + + String PREFIX = ApiConstants.PREFIX + "/order"; + + /*创建支付单*/ + @PostMapping(PREFIX + "/create") + CommonResult createOrder(@Valid @RequestBody PayOrderCreateReqDTO reqDTO); + + /*获得支付单*/ + @GetMapping(PREFIX + "/get") + @PermitAll + CommonResult getOrder(@RequestParam("id") Long id); + + /** + * 更新支付订单价格 + * @param id 支付单编号 + * @param payPrice 支付单价格 + * @return + */ + @PutMapping(PREFIX + "/update-price") + CommonResult updatePayOrderPrice(@RequestParam("id") Long id ,//支付单编号 + @RequestParam("payPrice") Integer payPrice); + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/order/dto/PayOrderCreateReqDTO.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/order/dto/PayOrderCreateReqDTO.java new file mode 100644 index 0000000..368720d --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/order/dto/PayOrderCreateReqDTO.java @@ -0,0 +1,65 @@ +package com.tashow.cloud.payapi.api.order.dto; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 支付单创建 Request DTO + */ +@Data +public class PayOrderCreateReqDTO implements Serializable { + + public static final int SUBJECT_MAX_LENGTH = 32; + + /** + * 应用标识 + */ + @NotNull(message = "应用标识不能为空") + private String appKey; + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单编号不能为空") + private String merchantOrderId; + /** + * 商品标题 + */ + @NotEmpty(message = "商品标题不能为空") + @Length(max = SUBJECT_MAX_LENGTH, message = "商品标题不能超过 32") + private String subject; + /** + * 商品描述 + */ + @Length(max = 128, message = "商品描述信息长度不能超过128") + private String body; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer price; + + /** + * 支付过期时间 + */ + @NotNull(message = "支付过期时间不能为空") + private LocalDateTime expireTime; + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/order/dto/PayOrderRespDTO.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/order/dto/PayOrderRespDTO.java new file mode 100644 index 0000000..f77af33 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/order/dto/PayOrderRespDTO.java @@ -0,0 +1,46 @@ +package com.tashow.cloud.payapi.api.order.dto; + +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import lombok.Data; + +/** + * 支付单信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PayOrderRespDTO { + + /** + * 订单编号,数据库自增 + */ + private Long id; + /** + * 渠道编码 + * + * 枚举 PayChannelEnum + */ + private String channelCode; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + * 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一 + */ + private String merchantOrderId; + + // ========== 订单相关字段 ========== + /** + * 支付金额,单位:分 + */ + private Integer price; + /** + * 支付状态 + * + * 枚举 {@link PayOrderStatusEnum} + */ + private Integer status; + + // ========== 渠道相关字段 ========== + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/refund/PayRefundApi.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/refund/PayRefundApi.java new file mode 100644 index 0000000..e3ed7ea --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/refund/PayRefundApi.java @@ -0,0 +1,36 @@ +package com.tashow.cloud.payapi.api.refund; + +import com.tashow.cloud.common.pojo.CommonResult; +import com.tashow.cloud.payapi.enums.ApiConstants; +import jakarta.validation.Valid; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * RPC 服务 - 退款单 + */ +@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory = +public interface PayRefundApi { + + String PREFIX = ApiConstants.PREFIX + "/refund"; + + /** + * 创建退款单 + * @param reqDTO + * @return + */ + @PostMapping(PREFIX + "/create") + CommonResult createRefund(@Valid @RequestBody PayRefundCreateReqDTO reqDTO); + + /** + * 获得退款单 + * @param id 退款单编号 + * @return + */ + @GetMapping(PREFIX + "/get") + CommonResult getRefund(@RequestParam("id") Long id); + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/refund/dto/PayRefundCreateReqDTO.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/refund/dto/PayRefundCreateReqDTO.java new file mode 100644 index 0000000..9936e56 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/refund/dto/PayRefundCreateReqDTO.java @@ -0,0 +1,57 @@ +package com.tashow.cloud.payapi.api.refund.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +/** + * 退款单创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class PayRefundCreateReqDTO { + + /** + * 应用标识 + */ + @NotNull(message = "应用标识不能为空") + private String appKey; + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单编号不能为空") + private String merchantOrderId; + + /** + * 商户退款编号 + */ + @NotEmpty(message = "商户退款编号不能为空") + private String merchantRefundId; + + /** + * 退款描述 + */ + @NotEmpty(message = "退款描述不能为空") + @Length(max = 128, message = "退款描述长度不能超过 128") + private String reason; + + // ========== 订单相关字段 ========== + + /** + * 退款金额,单位:分 + */ + @NotNull(message = "退款金额不能为空") + @Min(value = 1, message = "退款金额必须大于零") + private Integer price; + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/refund/dto/PayRefundRespDTO.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/refund/dto/PayRefundRespDTO.java new file mode 100644 index 0000000..9d2228c --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/refund/dto/PayRefundRespDTO.java @@ -0,0 +1,43 @@ +package com.tashow.cloud.payapi.api.refund.dto; + +import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 退款单信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PayRefundRespDTO { + + /** + * 退款单编号 + */ + private Long id; + + // ========== 退款相关字段 ========== + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusEnum} + */ + private Integer status; + /** + * 退款金额,单位:分 + */ + private Integer refundPrice; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + */ + private String merchantOrderId; + /** + * 退款成功时间 + */ + private LocalDateTime successTime; + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/transfer/PayTransferApi.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/transfer/PayTransferApi.java new file mode 100644 index 0000000..01646bf --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/transfer/PayTransferApi.java @@ -0,0 +1,36 @@ +package com.tashow.cloud.payapi.api.transfer; + +import com.tashow.cloud.common.pojo.CommonResult; +import com.tashow.cloud.payapi.enums.ApiConstants; +import jakarta.validation.Valid; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * RPC 服务 - 转账单 + */ +@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory = +public interface PayTransferApi { + + String PREFIX = ApiConstants.PREFIX + "/transfer"; + + /** + * 创建转账单 + * @param reqDTO + * @return + */ + @PostMapping(PREFIX + "/create") + CommonResult createTransfer(@Valid @RequestBody PayTransferCreateReqDTO reqDTO); + + /** + * 获得转账单 + * @param id 转账单编号 + * @return + */ + @GetMapping(PREFIX + "/get") + CommonResult getTransfer(@RequestParam("id") Long id); + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/transfer/dto/PayTransferCreateReqDTO.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/transfer/dto/PayTransferCreateReqDTO.java new file mode 100644 index 0000000..5f2d9db --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/transfer/dto/PayTransferCreateReqDTO.java @@ -0,0 +1,77 @@ +package com.tashow.cloud.payapi.api.transfer.dto; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Map; + +/** + * 转账单创建 Request DTO + * + * @author jason + */ +@Data +public class PayTransferCreateReqDTO { + + /** + * 应用标识 + */ + @NotNull(message = "应用标识不能为空") + private String appKey; + + @NotEmpty(message = "转账渠道不能为空") + private String channelCode; + + /** + * 转账渠道的额外参数 + */ + private Map channelExtras; + + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + /** + * 类型 + */ + @NotNull(message = "转账类型不能为空") + @InEnum(PayTransferTypeEnum.class) + private Integer type; + + + /** + * 商户转账单编号 + */ + @NotEmpty(message = "商户转账单编号能为空") + private String merchantTransferId; + + /** + * 转账金额,单位:分 + */ + @Min(value = 1, message = "转账金额必须大于零") + @NotNull(message = "转账金额不能为空") + private Integer price; + + /** + * 转账标题 + */ + @NotEmpty(message = "转账标题不能为空") + private String subject; + + /** + * 收款人姓名 + */ + @NotBlank(message = "收款人姓名不能为空", groups = {PayTransferTypeEnum.Alipay.class}) + private String userName; + + @NotBlank(message = "支付宝登录号不能为空", groups = {PayTransferTypeEnum.Alipay.class}) + private String alipayLogonId; + + // ========== 微信转账相关字段 ========== + @NotBlank(message = "微信 openId 不能为空", groups = {PayTransferTypeEnum.WxPay.class}) + private String openid; +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/transfer/dto/PayTransferRespDTO.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/transfer/dto/PayTransferRespDTO.java new file mode 100644 index 0000000..330fdff --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/transfer/dto/PayTransferRespDTO.java @@ -0,0 +1,31 @@ +package com.tashow.cloud.payapi.api.transfer.dto; + +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum; +import lombok.Data; + +@Data +public class PayTransferRespDTO { + + /** + * 编号 + */ + private Long id; + + /** + * 转账单号 + */ + private String no; + + /** + * 转账金额,单位:分 + */ + private Integer price; + + /** + * 转账状态 + * + * 枚举 {@link PayTransferStatusEnum} + */ + private Integer status; + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/wallet/PayWalletApi.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/wallet/PayWalletApi.java new file mode 100644 index 0000000..05b685d --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/wallet/PayWalletApi.java @@ -0,0 +1,26 @@ +package com.tashow.cloud.payapi.api.wallet; + +import com.tashow.cloud.common.pojo.CommonResult; +import com.tashow.cloud.payapi.enums.ApiConstants; +import jakarta.validation.Valid; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * RPC 服务 - 钱包 + */ +@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory = +public interface PayWalletApi { + + String PREFIX = ApiConstants.PREFIX + "/wallet"; + + /** + * 添加钱包余额 + * @param reqDTO + * @return + */ + @PostMapping(PREFIX + "/add-balance") + CommonResult addWalletBalance(@Valid @RequestBody PayWalletAddBalanceReqDTO reqDTO); + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/wallet/dto/PayWalletAddBalanceReqDTO.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/wallet/dto/PayWalletAddBalanceReqDTO.java new file mode 100644 index 0000000..d644cba --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/api/wallet/dto/PayWalletAddBalanceReqDTO.java @@ -0,0 +1,49 @@ +package com.tashow.cloud.payapi.api.wallet.dto; + +import com.tashow.cloud.common.enums.UserTypeEnum; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 钱包余额增加 Request DTO + * + * @author 芋道源码 + */ +@Data +public class PayWalletAddBalanceReqDTO { + + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 关联业务分类 + */ + @NotNull(message = "关联业务分类不能为空") + private Integer bizType; + /** + * 关联业务编号 + */ + @NotNull(message = "关联业务编号不能为空") + private String bizId; + + /** + * 交易金额,单位分 + * + * 正值表示余额增加,负值表示余额减少 + */ + @NotNull(message = "交易金额不能为空") + private Integer price; + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/ApiConstants.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/ApiConstants.java new file mode 100644 index 0000000..2019c34 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/ApiConstants.java @@ -0,0 +1,24 @@ +package com.tashow.cloud.payapi.enums; + + +import com.tashow.cloud.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/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/DictTypeConstants.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/DictTypeConstants.java new file mode 100644 index 0000000..9c553e7 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/DictTypeConstants.java @@ -0,0 +1,18 @@ +package com.tashow.cloud.payapi.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/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/ErrorCodeConstants.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..8dc4225 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/ErrorCodeConstants.java @@ -0,0 +1,94 @@ +package com.tashow.cloud.payapi.enums; + + +import com.tashow.cloud.common.exception.ErrorCode; + +/** + * Pay 错误码 Core 枚举类 + * + * pay 系统,使用 1-007-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== APP 模块 1-007-000-000 ========== + ErrorCode APP_NOT_FOUND = new ErrorCode(1_007_000_000, "App 不存在"); + ErrorCode APP_IS_DISABLE = new ErrorCode(1_007_000_002, "App 已经被禁用"); + ErrorCode APP_EXIST_ORDER_CANT_DELETE = new ErrorCode(1_007_000_003, "支付应用存在支付订单,无法删除"); + ErrorCode APP_EXIST_REFUND_CANT_DELETE = new ErrorCode(1_007_000_004, "支付应用存在退款订单,无法删除"); + ErrorCode APP_KEY_EXISTS = new ErrorCode(1_007_000_005, "支付应用标识已经存在"); + + // ========== CHANNEL 模块 1-007-001-000 ========== + ErrorCode CHANNEL_NOT_FOUND = new ErrorCode(1_007_001_000, "支付渠道的配置不存在"); + ErrorCode CHANNEL_IS_DISABLE = new ErrorCode(1_007_001_001, "支付渠道已经禁用"); + ErrorCode CHANNEL_EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1_007_001_004, "已存在相同的渠道"); + + // ========== ORDER 模块 1-007-002-000 ========== + ErrorCode PAY_ORDER_NOT_FOUND = new ErrorCode(1_007_002_000, "支付订单不存在"); + ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_002_001, "支付订单不处于待支付"); + ErrorCode PAY_ORDER_STATUS_IS_SUCCESS = new ErrorCode(1_007_002_002, "订单已支付,请刷新页面"); + ErrorCode PAY_ORDER_IS_EXPIRED = new ErrorCode(1_007_002_003, "支付订单已经过期"); + ErrorCode PAY_ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_002_004, "发起支付报错,错误码:{},错误提示:{}"); + ErrorCode PAY_ORDER_REFUND_FAIL_STATUS_ERROR = new ErrorCode(1_007_002_005, "支付订单退款失败,原因:状态不是已支付或已退款"); + + // ========== ORDER 模块(拓展单) 1-007-003-000 ========== + ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1_007_003_000, "支付交易拓展单不存在"); + ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_003_001, "支付交易拓展单不处于待支付"); + ErrorCode PAY_ORDER_EXTENSION_IS_PAID = new ErrorCode(1_007_003_002, "订单已支付,请等待支付结果"); + + // ========== 支付模块(退款) 1-007-006-000 ========== + ErrorCode REFUND_PRICE_EXCEED = new ErrorCode(1_007_006_000, "退款金额超过订单可退款金额"); + ErrorCode REFUND_HAS_REFUNDING = new ErrorCode(1_007_006_002, "已经有退款在处理中"); + ErrorCode REFUND_EXISTS = new ErrorCode(1_007_006_003, "已经存在退款单"); + ErrorCode REFUND_NOT_FOUND = new ErrorCode(1_007_006_004, "支付退款单不存在"); + ErrorCode REFUND_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_006_005, "支付退款单不处于待退款"); + + // ========== 钱包模块 1-007-007-000 ========== + ErrorCode WALLET_NOT_FOUND = new ErrorCode(1_007_007_000, "用户钱包不存在"); + ErrorCode WALLET_BALANCE_NOT_ENOUGH = new ErrorCode(1_007_007_001, "钱包余额不足"); + ErrorCode WALLET_TRANSACTION_NOT_FOUND = new ErrorCode(1_007_007_002, "未找到对应的钱包交易"); + ErrorCode WALLET_REFUND_EXIST = new ErrorCode(1_007_007_003, "已经存在钱包退款"); + ErrorCode WALLET_FREEZE_PRICE_NOT_ENOUGH = new ErrorCode(1_007_007_004, "钱包冻结余额不足"); + + // ========== 钱包充值模块 1-007-008-000 ========== + ErrorCode WALLET_RECHARGE_NOT_FOUND = new ErrorCode(1_007_008_000, "钱包充值记录不存在"); + ErrorCode WALLET_RECHARGE_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1_007_008_001, "钱包充值更新支付状态失败,钱包充值记录不是【未支付】状态"); + ErrorCode WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_ID_ERROR = new ErrorCode(1_007_008_002, "钱包充值更新支付状态失败,支付单编号不匹配"); + ErrorCode WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_STATUS_NOT_SUCCESS = new ErrorCode(1_007_008_003, "钱包充值更新支付状态失败,支付单状态不是【支付成功】状态"); + ErrorCode WALLET_RECHARGE_UPDATE_PAID_PAY_PRICE_NOT_MATCH = new ErrorCode(1_007_008_004, "钱包充值更新支付状态失败,支付单金额不匹配"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_NOT_PAID = new ErrorCode(1_007_008_005, "钱包发起退款失败,钱包充值订单未支付"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUNDED = new ErrorCode(1_007_008_006, "钱包发起退款失败,钱包充值订单已退款"); + ErrorCode WALLET_RECHARGE_REFUND_BALANCE_NOT_ENOUGH = new ErrorCode(1_007_008_007, "钱包发起退款失败,钱包余额不足"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1_007_008_008, "钱包退款更新失败,钱包退款单编号不匹配"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_007_008_009, "钱包退款更新失败,退款订单不存在"); + ErrorCode WALLET_RECHARGE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1_007_008_010, "钱包退款更新失败,退款单金额不匹配"); + ErrorCode WALLET_RECHARGE_PACKAGE_NOT_FOUND = new ErrorCode(1_007_008_011, "钱包充值套餐不存在"); + ErrorCode WALLET_RECHARGE_PACKAGE_IS_DISABLE = new ErrorCode(1_007_008_012, "钱包充值套餐已禁用"); + ErrorCode WALLET_RECHARGE_PACKAGE_NAME_EXISTS = new ErrorCode(1_007_008_013, "钱包充值套餐名称已存在"); + + // ========== 转账模块 1-007-009-000 ========== + ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_000, "发起转账报错,错误码:{},错误提示:{}"); + ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在"); + ErrorCode PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH = new ErrorCode(1_007_009_002, "两次相同转账请求的类型不匹配"); + ErrorCode PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "两次相同转账请求的金额不匹配"); + ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经发起,请查询转账订单相关状态"); + ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_005, "转账单不处于待转账"); + ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中"); + + // ========== 示例订单 1-007-900-000 ========== + ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1_007_900_000, "示例订单不存在"); + ErrorCode DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1_007_900_001, "示例订单更新支付状态失败,订单不是【未支付】状态"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR = new ErrorCode(1_007_900_002, "示例订单更新支付状态失败,支付单编号不匹配"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS = new ErrorCode(1_007_900_003, "示例订单更新支付状态失败,支付单状态不是【支付成功】状态"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH = new ErrorCode(1_007_900_004, "示例订单更新支付状态失败,支付单金额不匹配"); + ErrorCode DEMO_ORDER_REFUND_FAIL_NOT_PAID = new ErrorCode(1_007_900_005, "发起退款失败,示例订单未支付"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUNDED = new ErrorCode(1_007_900_006, "发起退款失败,示例订单已退款"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_007_900_007, "发起退款失败,退款订单不存在"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS = new ErrorCode(1_007_900_008, "发起退款失败,退款订单未退款成功"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1_007_900_009, "发起退款失败,退款单编号不匹配"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1_007_900_010, "发起退款失败,退款单金额不匹配"); + + // ========== 示例转账订单 1-007-901-001 ========== + ErrorCode DEMO_TRANSFER_NOT_FOUND = new ErrorCode(1_007_901_001, "示例转账单不存在"); + ErrorCode DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR = new ErrorCode(1_007_901_002, "转账失败,转账单编号不匹配"); + ErrorCode DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH = new ErrorCode(1_007_901_003, "转账失败,转账单金额不匹配"); +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/MessageTemplateConstants.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/MessageTemplateConstants.java new file mode 100644 index 0000000..2723f09 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/MessageTemplateConstants.java @@ -0,0 +1,14 @@ +package com.tashow.cloud.payapi.enums; + +/** + * 通知模板枚举类 + * + * @author HUIHUI + */ +public interface MessageTemplateConstants { + + // ======================= 小程序订阅消息 ======================= + + String WXA_WALLET_RECHARGER_PAID = "充值成功通知"; + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/notify/PayNotifyStatusEnum.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/notify/PayNotifyStatusEnum.java new file mode 100644 index 0000000..7bf09a8 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/notify/PayNotifyStatusEnum.java @@ -0,0 +1,32 @@ +package com.tashow.cloud.payapi.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/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/notify/PayNotifyTypeEnum.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/notify/PayNotifyTypeEnum.java new file mode 100644 index 0000000..bd0cc60 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/notify/PayNotifyTypeEnum.java @@ -0,0 +1,29 @@ +package com.tashow.cloud.payapi.enums.notify; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付通知类型 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayNotifyTypeEnum { + + ORDER(1, "支付单"), + REFUND(2, "退款单"), + TRANSFER(3, "转账单") + ; + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/order/PayOrderStatusEnum.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/order/PayOrderStatusEnum.java new file mode 100644 index 0000000..e296926 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/order/PayOrderStatusEnum.java @@ -0,0 +1,74 @@ +package com.tashow.cloud.payapi.enums.order; + +import com.tashow.cloud.common.core.ArrayValuable; +import com.tashow.cloud.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 支付订单的状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayOrderStatusEnum implements ArrayValuable { + + WAITING(0, "未支付"), + SUCCESS(10, "支付成功"), + REFUND(20, "已退款"), + CLOSED(30, "支付关闭"), // 注意:全部退款后,还是 REFUND 状态 + ; + + private final Integer status; + private final String name; + + @Override + public Integer[] array() { + return new Integer[0]; + } + + /** + * 判断是否等待支付 + * + * @param status 状态 + * @return 是否等待支付 + */ + public static boolean isWaiting(Integer status) { + return Objects.equals(status, WAITING.getStatus()); + } + + /** + * 判断是否支付成功 + * + * @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/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/refund/PayRefundStatusEnum.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/refund/PayRefundStatusEnum.java new file mode 100644 index 0000000..a4a2b51 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/refund/PayRefundStatusEnum.java @@ -0,0 +1,32 @@ +package com.tashow.cloud.payapi.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/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/transfer/PayTransferStatusEnum.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/transfer/PayTransferStatusEnum.java new file mode 100644 index 0000000..ef181d6 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/transfer/PayTransferStatusEnum.java @@ -0,0 +1,59 @@ +package com.tashow.cloud.payapi.enums.transfer; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * @author jason + */ +@Getter +@AllArgsConstructor +public enum PayTransferStatusEnum { + + WAITING(0, "等待转账"), + /** + * TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现 + */ + IN_PROGRESS(10, "转账进行中"), + + SUCCESS(20, "转账成功"), + /** + * 转账关闭 (失败,或者其它情况) // TODO 改成 转账失败状态 + */ + CLOSED(30, "转账关闭"); + + /** + * 状态 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + public static boolean isClosed(Integer status) { + return Objects.equals(status, CLOSED.getStatus()); + } + + public static boolean isWaiting(Integer status) { + return Objects.equals(status, WAITING.getStatus()); + } + public static boolean isInProgress(Integer status) { + return Objects.equals(status, IN_PROGRESS.getStatus()); + } + + /** + * 是否处于待转账或者转账中的状态 + * @param status 状态 + */ + public static boolean isPendingStatus(Integer status) { + return Objects.equals(status, WAITING.getStatus()) || Objects.equals(status, IN_PROGRESS.getStatus()); + } + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/transfer/PayTransferTypeEnum.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/transfer/PayTransferTypeEnum.java new file mode 100644 index 0000000..6b00742 --- /dev/null +++ b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/transfer/PayTransferTypeEnum.java @@ -0,0 +1,44 @@ +package com.tashow.cloud.payapi.enums.transfer; + +import cn.hutool.core.util.ArrayUtil; +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 转账类型枚举 + * + * @author jason + */ +@AllArgsConstructor +@Getter +public enum PayTransferTypeEnum implements ArrayValuable { + + ALIPAY_BALANCE(1, "支付宝余额"), + WX_BALANCE(2, "微信余额"), + BANK_CARD(3, "银行卡"), + WALLET_BALANCE(4, "钱包余额"); + + public interface WxPay { + } + + public interface Alipay { + } + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(PayTransferTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PayTransferTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/wallet/PayWalletBizTypeEnum.java b/tashow-feign/tashow-pay-api/src/main/java/com/tashow/cloud/payapi/enums/wallet/PayWalletBizTypeEnum.java new file mode 100644 index 0000000..e69de29 diff --git a/tashow-feign/tashow-trade-api/pom.xml b/tashow-feign/tashow-trade-api/pom.xml new file mode 100644 index 0000000..3e8d53a --- /dev/null +++ b/tashow-feign/tashow-trade-api/pom.xml @@ -0,0 +1,40 @@ + + + + com.tashow.cloud + tashow-feign + ${revision} + + 4.0.0 + tashow-trade-api + jar + + ${project.artifactId} + + trade 模块 API,暴露给其它模块调用 + + + + + com.tashow.cloud + tashow-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + true + + + + diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/api/order/TradeOrderApi.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/api/order/TradeOrderApi.java new file mode 100644 index 0000000..57d0e30 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/api/order/TradeOrderApi.java @@ -0,0 +1,32 @@ +package com.tashow.cloud.tradeapi.api.order; + +import com.tashow.cloud.common.pojo.CommonResult; +import com.tashow.cloud.tradeapi.api.order.dto.TradeOrderRespDTO; +import com.tashow.cloud.tradeapi.enums.ApiConstants; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.Collection; +import java.util.List; + +@FeignClient(name = ApiConstants.NAME) +public interface TradeOrderApi { + + String PREFIX = ApiConstants.PREFIX + "/order"; + + /*获得订单列表*/ + @GetMapping(PREFIX + "/order/list") + CommonResult> getOrderList(@RequestParam("ids") Collection ids); + + /*获得订单*/ + @GetMapping(PREFIX + "/get") + CommonResult getOrder(@RequestParam("id") Long id); + + @PutMapping(PREFIX + "/cancel-paid") + CommonResult cancelPaidOrder(@RequestParam("userId") Long userId, + @RequestParam("orderId") Long orderId, + @RequestParam("cancelType") Integer cancelType); + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/api/order/dto/TradeOrderRespDTO.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/api/order/dto/TradeOrderRespDTO.java new file mode 100644 index 0000000..4dfab03 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/api/order/dto/TradeOrderRespDTO.java @@ -0,0 +1,67 @@ +package com.tashow.cloud.tradeapi.api.order.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 订单信息 Response DTO + * RPC 服务 - 订单信息 Response DTO + * @author HUIHUI + */ +@Data +public class TradeOrderRespDTO { + + // ========== 订单基本信息 ========== + + //订单编号 + private Long id; + + //订单流水号 + private String no; + + //订单类型 + private Integer type; // 参见 TradeOrderTypeEnum 枚举 + + //订单来源 + private Integer terminal; // 参见 TerminalEnum 枚举 + + //用户编号 + private Long userId; + + //用户 IP + private String userIp; + + //用户备注 + private String userRemark; + + //订单状态 + private Integer status; // 参见 TradeOrderStatusEnum 枚举 + + //购买的商品数量 + private Integer productCount; + + //订单完成时间 + private LocalDateTime finishTime; + + //订单取消时间 + private LocalDateTime cancelTime; + + //取消类型 + private Integer cancelType; // 参见 TradeOrderCancelTypeEnum 枚举 + + //商家备注 + private String remark; + + //是否评价 + private Boolean commentStatus; + + // ========== 价格 + 支付基本信息 ========== + + //支付订单编号 + private Long payOrderId; + + //是否已支付 + private Boolean payStatus; + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/api/package-info.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/api/package-info.java new file mode 100644 index 0000000..33ff7ee --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/api/package-info.java @@ -0,0 +1 @@ +package com.tashow.cloud.tradeapi.api; \ No newline at end of file diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/ApiConstants.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/ApiConstants.java new file mode 100644 index 0000000..2082491 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/ApiConstants.java @@ -0,0 +1,24 @@ +package com.tashow.cloud.tradeapi.enums; + + +import com.tashow.cloud.common.enums.RpcConstants; + +/** + * API 相关的枚举 + * + * @author 芋道源码 + */ +public class ApiConstants { + + /** + * 服务名 + * + * 注意,需要保证和 spring.application.name 保持一致 + */ + public static final String NAME = "trade-server"; + + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/trade"; + + public static final String VERSION = "1.0.0"; + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/DictTypeConstants.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/DictTypeConstants.java new file mode 100644 index 0000000..cec7a1a --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/DictTypeConstants.java @@ -0,0 +1,12 @@ +package com.tashow.cloud.tradeapi.enums; + +/** + * Trade 字典类型的枚举类 + * + * @author owen + */ +public interface DictTypeConstants { + + String BROKERAGE_WITHDRAW_STATUS = "brokerage_withdraw_status"; // 佣金提现状态 + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/ErrorCodeConstants.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..3f0a983 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/ErrorCodeConstants.java @@ -0,0 +1,106 @@ +package com.tashow.cloud.tradeapi.enums; + + +import com.tashow.cloud.common.exception.ErrorCode; + +/** + * Trade 错误码枚举类 + * trade 系统,使用 1-011-000-000 段 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface ErrorCodeConstants { + + // ========== Order 模块 1-011-000-000 ========== + ErrorCode ORDER_ITEM_NOT_FOUND = new ErrorCode(1_011_000_010, "交易订单项不存在"); + ErrorCode ORDER_NOT_FOUND = new ErrorCode(1_011_000_011, "交易订单不存在"); + ErrorCode ORDER_ITEM_UPDATE_AFTER_SALE_STATUS_FAIL = new ErrorCode(1_011_000_012, "交易订单项更新售后状态失败,请重试"); + ErrorCode ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1_011_000_013, "交易订单更新支付状态失败,订单不是【未支付】状态"); + ErrorCode ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR = new ErrorCode(1_011_000_014, "交易订单更新支付状态失败,支付单编号不匹配"); + ErrorCode ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_015, "交易订单更新支付状态失败,支付单状态不是【支付成功】状态"); + ErrorCode ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH = new ErrorCode(1_011_000_016, "交易订单更新支付状态失败,支付单金额不匹配"); + ErrorCode ORDER_DELIVERY_FAIL_STATUS_NOT_UNDELIVERED = new ErrorCode(1_011_000_017, "交易订单发货失败,订单不是【待发货】状态"); + ErrorCode ORDER_RECEIVE_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_018, "交易订单收货失败,订单不是【待收货】状态"); + ErrorCode ORDER_COMMENT_FAIL_STATUS_NOT_COMPLETED = new ErrorCode(1_011_000_019, "创建交易订单项的评价失败,订单不是【已完成】状态"); + ErrorCode ORDER_COMMENT_STATUS_NOT_FALSE = new ErrorCode(1_011_000_020, "创建交易订单项的评价失败,订单已评价"); + ErrorCode ORDER_DELIVERY_FAIL_REFUND_STATUS_NOT_NONE = new ErrorCode(1_011_000_021, "交易订单发货失败,订单已退款或部分退款"); + ErrorCode ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_022, "交易订单发货失败,拼团未成功"); + ErrorCode ORDER_DELIVERY_FAIL_BARGAIN_RECORD_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_023, "交易订单发货失败,砍价未成功"); + ErrorCode ORDER_DELIVERY_FAIL_DELIVERY_TYPE_NOT_EXPRESS = new ErrorCode(1_011_000_024, "交易订单发货失败,发货类型不是快递"); + ErrorCode ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID = new ErrorCode(1_011_000_025, "交易订单取消失败,订单不是【待支付】状态"); + ErrorCode ORDER_UPDATE_PRICE_FAIL_PAID = new ErrorCode(1_011_000_026, "支付订单调价失败,原因:支付订单已付款,不能调价"); + ErrorCode ORDER_UPDATE_PRICE_FAIL_ALREADY = new ErrorCode(1_011_000_027, "支付订单调价失败,原因:已经修改过价格"); + ErrorCode ORDER_UPDATE_PRICE_FAIL_PRICE_ERROR = new ErrorCode(1_011_000_028, "支付订单调价失败,原因:调整后支付价格不能小于 0.01 元"); + ErrorCode ORDER_DELETE_FAIL_STATUS_NOT_CANCEL = new ErrorCode(1_011_000_029, "交易订单删除失败,订单不是【已取消】状态"); + ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】"); + ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态"); + ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单"); + ErrorCode ORDER_CANCEL_PAID_FAIL = new ErrorCode(1_011_000_033, "交易订单取消支付失败,原因:订单不是【{}】状态"); + ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_034, "交易订单自提失败,原因:你没有核销该门店订单的权限"); + ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_035, "交易订单创建失败,原因:用户积分不足"); + + // ========== After Sale 模块 1-011-000-100 ========== + ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在"); + ErrorCode AFTER_SALE_CREATE_FAIL_REFUND_PRICE_ERROR = new ErrorCode(1_011_000_101, "申请退款金额错误"); + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_CANCELED = new ErrorCode(1_011_000_102, "订单已关闭,无法申请售后"); + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_PAID = new ErrorCode(1_011_000_103, "订单未支付,无法申请售后"); + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_DELIVERED = new ErrorCode(1_011_000_104, "订单未发货,无法申请【退货退款】售后"); + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_ITEM_APPLIED = new ErrorCode(1_011_000_105, "订单项已申请售后,无法重复申请"); + ErrorCode AFTER_SALE_AUDIT_FAIL_STATUS_NOT_APPLY = new ErrorCode(1_011_000_106, "审批失败,售后状态不处于审批中"); + ErrorCode AFTER_SALE_UPDATE_STATUS_FAIL = new ErrorCode(1_011_000_107, "操作售后单失败,请刷新后重试"); + ErrorCode AFTER_SALE_DELIVERY_FAIL_STATUS_NOT_SELLER_AGREE = new ErrorCode(1_011_000_108, "退货失败,售后单状态不处于【待买家退货】"); + ErrorCode AFTER_SALE_CONFIRM_FAIL_STATUS_NOT_BUYER_DELIVERY = new ErrorCode(1_011_000_109, "确认收货失败,售后单状态不处于【待确认收货】"); + ErrorCode AFTER_SALE_REFUND_FAIL_STATUS_NOT_WAIT_REFUND = new ErrorCode(1_011_000_110, "退款失败,售后单状态不是【待退款】"); + ErrorCode AFTER_SALE_CANCEL_FAIL_STATUS_NOT_APPLY_OR_AGREE_OR_BUYER_DELIVERY = + new ErrorCode(1_011_000_111, "取消售后单失败,售后单状态不是【待审核】或【卖家同意】或【商家待收货】"); + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS = new ErrorCode(1_011_000_112, "订单拼团中,无法申请售后"); + + // ========== Cart 模块 1-011-002-000 ========== + ErrorCode CARD_ITEM_NOT_FOUND = new ErrorCode(1_011_002_000, "购物车项不存在"); + + // ========== Price 相关 1-011-003-000 ============ + ErrorCode PRICE_CALCULATE_PAY_PRICE_ILLEGAL = new ErrorCode(1_011_003_000, "支付价格计算异常,原因:价格小于等于 0"); + ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_001, "计算快递运费异常,找不到对应的运费模板"); + ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_002, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵"); + ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_003, "参与秒杀的商品,超过了秒杀总限购数量"); + ErrorCode PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_004, "参与积分活动的商品,超过了积分活动商品总限购数量"); + ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_005, "计算快递运费异常,配送方式不匹配"); + ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_006, "该优惠劵无法使用,原因:{}」"); + + // ========== 物流 Express 模块 1-011-004-000 ========== + ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在"); + ErrorCode EXPRESS_CODE_DUPLICATE = new ErrorCode(1_011_004_001, "已经存在该编码的快递公司"); + ErrorCode EXPRESS_CLIENT_NOT_PROVIDE = new ErrorCode(1_011_004_002, "需要接入快递服务商,比如【快递100】"); + ErrorCode EXPRESS_STATUS_NOT_ENABLE = new ErrorCode(1_011_004_003, "快递公司未启用"); + + ErrorCode EXPRESS_API_QUERY_ERROR = new ErrorCode(1_011_004_101, "快递查询接口异常"); + ErrorCode EXPRESS_API_QUERY_FAILED = new ErrorCode(1_011_004_102, "快递查询返回失败,原因:{}"); + + // ========== 物流 Template 模块 1-011-005-000 ========== + ErrorCode EXPRESS_TEMPLATE_NAME_DUPLICATE = new ErrorCode(1_011_005_000, "已经存在该运费模板名"); + ErrorCode EXPRESS_TEMPLATE_NOT_EXISTS = new ErrorCode(1_011_005_001, "运费模板不存在"); + + // ========== 物流 PICK_UP 模块 1-011-006-000 ========== + ErrorCode PICK_UP_STORE_NOT_EXISTS = new ErrorCode(1_011_006_000, "自提门店不存在"); + ErrorCode PICK_UP_STORE_STAFF_NOT_EXISTS = new ErrorCode(1_011_006_000, "自提门店店员不存在"); + + // ========== 分销用户 模块 1-011-007-000 ========== + ErrorCode BROKERAGE_USER_NOT_EXISTS = new ErrorCode(1_011_007_000, "分销用户不存在"); + ErrorCode BROKERAGE_USER_FROZEN_PRICE_NOT_ENOUGH = new ErrorCode(1_011_007_001, "用户冻结佣金({})数量不足"); + ErrorCode BROKERAGE_BIND_SELF = new ErrorCode(1_011_007_002, "不能绑定自己"); + ErrorCode BROKERAGE_BIND_USER_NOT_ENABLED = new ErrorCode(1_011_007_003, "绑定用户没有推广资格"); + ErrorCode BROKERAGE_BIND_CONDITION_ADMIN = new ErrorCode(1_011_007_004, "仅可在后台绑定推广员"); + ErrorCode BROKERAGE_BIND_MODE_REGISTER = new ErrorCode(1_011_007_005, "只有在注册时可以绑定"); + ErrorCode BROKERAGE_BIND_OVERRIDE = new ErrorCode(1_011_007_006, "已绑定了推广人"); + ErrorCode BROKERAGE_BIND_LOOP = new ErrorCode(1_011_007_007, "下级不能绑定自己的上级"); + ErrorCode BROKERAGE_USER_LEVEL_NOT_SUPPORT = new ErrorCode(1_011_007_008, "目前只支持 level 小于等于 2"); + ErrorCode BROKERAGE_CREATE_USER_EXISTS = new ErrorCode(1_011_007_009, "分销用户已存在"); + + // ========== 分销提现 模块 1-011-008-000 ========== + ErrorCode BROKERAGE_WITHDRAW_NOT_EXISTS = new ErrorCode(1_011_008_000, "佣金提现记录不存在"); + ErrorCode BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING = new ErrorCode(1_011_008_001, "佣金提现记录状态不是审核中"); + ErrorCode BROKERAGE_WITHDRAW_MIN_PRICE = new ErrorCode(1_011_008_002, "提现金额不能低于 {} 元"); + ErrorCode BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH = new ErrorCode(1_011_008_003, "您当前最多可提现 {} 元"); + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/MessageTemplateConstants.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/MessageTemplateConstants.java new file mode 100644 index 0000000..337ca78 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/MessageTemplateConstants.java @@ -0,0 +1,18 @@ +package com.tashow.cloud.tradeapi.enums; + +/** + * 通知模板枚举类 + * + * @author HUIHUI + */ +public interface MessageTemplateConstants { + + // ======================= 短信消息模版 ======================= + + String SMS_ORDER_DELIVERY = "order_delivery"; // 短信模版编号 + + // ======================= 小程序订阅消息模版 ======================= + + String WXA_ORDER_DELIVERY = "订单发货通知"; + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleOperateTypeEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleOperateTypeEnum.java new file mode 100644 index 0000000..54183cb --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleOperateTypeEnum.java @@ -0,0 +1,35 @@ +package com.tashow.cloud.tradeapi.enums.aftersale; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 售后操作类型的枚举 + * + * @author 陈賝 + * @since 2023/6/13 13:53 + */ +@RequiredArgsConstructor +@Getter +public enum AfterSaleOperateTypeEnum { + + MEMBER_CREATE(10, "会员申请退款"), + ADMIN_AGREE_APPLY(11, "商家同意退款"), + ADMIN_DISAGREE_APPLY(12, "商家拒绝退款"), + MEMBER_DELIVERY(20, "会员填写退货物流信息,快递公司:{deliveryName},快递单号:{logisticsNo}"), + ADMIN_AGREE_RECEIVE(21, "商家收货"), + ADMIN_DISAGREE_RECEIVE(22, "商家拒绝收货,原因:{reason}"), + ADMIN_REFUND(30, "商家退款"), + MEMBER_CANCEL(40, "会员取消退款"), + ; + + /** + * 操作类型 + */ + private final Integer type; + /** + * 操作描述 + */ + private final String content; + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleStatusEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleStatusEnum.java new file mode 100644 index 0000000..91834b3 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleStatusEnum.java @@ -0,0 +1,95 @@ +package com.tashow.cloud.tradeapi.enums.aftersale; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Collection; + +import static cn.hutool.core.util.ArrayUtil.firstMatch; + +/** + * 售后状态的枚举 + * + * 状态流转 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum AfterSaleStatusEnum implements ArrayValuable { + + /** + * 【申请售后】 + */ + APPLY(10,"申请中", "会员申请退款"), // 有赞的状态提示:退款申请待商家处理 + /** + * 卖家通过售后;【商品待退货】 + */ + SELLER_AGREE(20, "卖家通过", "商家同意退款"), // 有赞的状态提示:请退货并填写物流信息 + /** + * 买家已退货,等待卖家收货;【商家待收货】 + */ + BUYER_DELIVERY(30,"待卖家收货", "会员填写退货物流信息"), // 有赞的状态提示:退货退款申请待商家处理 + /** + * 卖家已收货,等待平台退款;等待退款【等待退款】 + */ + WAIT_REFUND(40, "等待平台退款", "商家收货"), // 有赞的状态提示:无(有赞无该状态) + /** + * 完成退款【退款成功】 + */ + COMPLETE(50, "完成", "商家确认退款"), // 有赞的状态提示:退款成功 + /** + * 【买家取消】 + */ + BUYER_CANCEL(61, "买家取消售后", "会员取消退款"), // 有赞的状态提示:退款关闭 + /** + * 卖家拒绝售后;商家拒绝【商家拒绝】 + */ + SELLER_DISAGREE(62,"卖家拒绝", "商家拒绝退款"), // 有赞的状态提示:商家不同意退款申请 + /** + * 卖家拒绝收货,终止售后;【商家拒收货】 + */ + SELLER_REFUSE(63,"卖家拒绝收货", "商家拒绝收货"), // 有赞的状态提示:商家拒绝收货,不同意退款 + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 进行中的售后状态 + * + * 不包括已经结束的状态 + */ + public static final Collection APPLYING_STATUSES = Arrays.asList( + APPLY.getStatus(), + SELLER_AGREE.getStatus(), + BUYER_DELIVERY.getStatus(), + WAIT_REFUND.getStatus() + ); + + /** + * 状态 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + /** + * 操作内容 + * + * 目的:记录售后日志的内容 + */ + private final String content; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static AfterSaleStatusEnum valueOf(Integer status) { + return firstMatch(value -> value.getStatus().equals(status), values()); + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleTypeEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleTypeEnum.java new file mode 100644 index 0000000..e3bb43d --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleTypeEnum.java @@ -0,0 +1,37 @@ +package com.tashow.cloud.tradeapi.enums.aftersale; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易售后 - 类型 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum AfterSaleTypeEnum implements ArrayValuable { + + IN_SALE(10, "售中退款"), // 交易完成前买家申请退款 + AFTER_SALE(20, "售后退款"); // 交易完成后买家申请退款 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleWayEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleWayEnum.java new file mode 100644 index 0000000..f3e7016 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/aftersale/AfterSaleWayEnum.java @@ -0,0 +1,37 @@ +package com.tashow.cloud.tradeapi.enums.aftersale; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易售后 - 方式 + * + * @author Sin + */ +@RequiredArgsConstructor +@Getter +public enum AfterSaleWayEnum implements ArrayValuable { + + REFUND(10, "仅退款"), + RETURN_AND_REFUND(20, "退货退款"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(AfterSaleWayEnum::getWay).toArray(Integer[]::new); + + /** + * 方式 + */ + private final Integer way; + /** + * 方式名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageBindModeEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageBindModeEnum.java new file mode 100644 index 0000000..9d0353f --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageBindModeEnum.java @@ -0,0 +1,48 @@ +package com.tashow.cloud.tradeapi.enums.brokerage; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 分销关系绑定模式枚举 + * + * @author owen + */ +@AllArgsConstructor +@Getter +public enum BrokerageBindModeEnum implements ArrayValuable { + + /** + * 只要用户没有推广人,随时都可以绑定分销关系 + */ + ANYTIME(1, "首次绑定"), + /** + * 仅新用户注册时才能绑定推广关系 + */ + REGISTER(2, "注册绑定"), + /** + * 每次扫码都覆盖 + */ + OVERRIDE(3, "覆盖绑定"), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BrokerageBindModeEnum::getMode).toArray(Integer[]::new); + + /** + * 模式 + */ + private final Integer mode; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageEnabledConditionEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageEnabledConditionEnum.java new file mode 100644 index 0000000..5b58c2f --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageEnabledConditionEnum.java @@ -0,0 +1,44 @@ +package com.tashow.cloud.tradeapi.enums.brokerage; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 分佣模式枚举 + * + * @author owen + */ +@AllArgsConstructor +@Getter +public enum BrokerageEnabledConditionEnum implements ArrayValuable { + + /** + * 所有用户都可以分销 + */ + ALL(1, "人人分销"), + /** + * 仅可后台手动设置推广员 + */ + ADMIN(2, "指定分销"), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BrokerageEnabledConditionEnum::getCondition).toArray(Integer[]::new); + + /** + * 模式 + */ + private final Integer condition; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageRecordBizTypeEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageRecordBizTypeEnum.java new file mode 100644 index 0000000..d10221a --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageRecordBizTypeEnum.java @@ -0,0 +1,47 @@ +package com.tashow.cloud.tradeapi.enums.brokerage; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 佣金记录业务类型枚举 + * + * @author owen + */ +@AllArgsConstructor +@Getter +public enum BrokerageRecordBizTypeEnum implements ArrayValuable { + + ORDER(1, "获得推广佣金", "获得推广佣金 {}", true), + WITHDRAW(2, "提现申请", "提现申请扣除佣金 {}", false), + WITHDRAW_REJECT(3, "提现申请驳回", "提现申请驳回,返还佣金 {}", true), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BrokerageRecordBizTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 标题 + */ + private final String title; + /** + * 描述 + */ + private final String description; + /** + * 是否为增加佣金 + */ + private final boolean add; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageRecordStatusEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageRecordStatusEnum.java new file mode 100644 index 0000000..22cdf1d --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageRecordStatusEnum.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.tradeapi.enums.brokerage; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 佣金记录状态枚举 + * + * @author owen + */ +@AllArgsConstructor +@Getter +public enum BrokerageRecordStatusEnum implements ArrayValuable { + + WAIT_SETTLEMENT(0, "待结算"), + SETTLEMENT(1, "已结算"), + CANCEL(2, "已取消"), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BrokerageRecordStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer status; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageWithdrawStatusEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageWithdrawStatusEnum.java new file mode 100644 index 0000000..6293f38 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageWithdrawStatusEnum.java @@ -0,0 +1,42 @@ +package com.tashow.cloud.tradeapi.enums.brokerage; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +// TODO 芋艿:提现的打通,在纠结下; +/** + * 佣金提现状态枚举 + * + * @author owen + */ +@AllArgsConstructor +@Getter +public enum BrokerageWithdrawStatusEnum implements ArrayValuable { + + AUDITING(0, "审核中"), + AUDIT_SUCCESS(10, "审核通过"), + WITHDRAW_SUCCESS(11, "提现成功"), + AUDIT_FAIL(20, "审核不通过"), + WITHDRAW_FAIL(21, "提现失败"), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BrokerageWithdrawStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer status; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageWithdrawTypeEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageWithdrawTypeEnum.java new file mode 100644 index 0000000..87a234e --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/brokerage/BrokerageWithdrawTypeEnum.java @@ -0,0 +1,51 @@ +package com.tashow.cloud.tradeapi.enums.brokerage; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 佣金提现类型枚举 + * + * @author owen + */ +@AllArgsConstructor +@Getter +public enum BrokerageWithdrawTypeEnum implements ArrayValuable { + + WALLET(1, "钱包"), + BANK(2, "银行卡"), + WECHAT(3, "微信"), // 手动打款 + ALIPAY(4, "支付宝"), + WECHAT_API(5, "微信零钱"), // 自动打款,通过微信转账 API + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BrokerageWithdrawTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + /** + * 是否通过支付平台的 API 打款 + * + * @param type 类型 + * @return 是否 + */ + public static boolean isApi(Integer type) { + return WECHAT_API.getType().equals(type); + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/delivery/DeliveryExpressChargeModeEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/delivery/DeliveryExpressChargeModeEnum.java new file mode 100644 index 0000000..be36fc0 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/delivery/DeliveryExpressChargeModeEnum.java @@ -0,0 +1,43 @@ +package com.tashow.cloud.tradeapi.enums.delivery; + +import cn.hutool.core.util.ArrayUtil; +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 快递配送计费方式枚举 + * + * @author jason + */ +@AllArgsConstructor +@Getter +public enum DeliveryExpressChargeModeEnum implements ArrayValuable { + + COUNT(1, "按件"), + WEIGHT(2,"按重量"), + VOLUME(3, "按体积"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(DeliveryExpressChargeModeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 描述 + */ + private final String desc; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static DeliveryExpressChargeModeEnum valueOf(Integer value) { + return ArrayUtil.firstMatch(chargeMode -> chargeMode.getType().equals(value), DeliveryExpressChargeModeEnum.values()); + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/delivery/DeliveryTypeEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/delivery/DeliveryTypeEnum.java new file mode 100644 index 0000000..121cf7d --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/delivery/DeliveryTypeEnum.java @@ -0,0 +1,37 @@ +package com.tashow.cloud.tradeapi.enums.delivery; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 配送方式枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum DeliveryTypeEnum implements ArrayValuable { + + EXPRESS(1, "快递发货"), + PICK_UP(2, "用户自提"),; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(DeliveryTypeEnum::getType).toArray(Integer[]::new); + + /** + * 配送方式 + */ + private final Integer type; + /** + * 状态名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/notify/TradeNotifyEnums.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/notify/TradeNotifyEnums.java new file mode 100644 index 0000000..d7c1eae --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/notify/TradeNotifyEnums.java @@ -0,0 +1,5 @@ +package com.tashow.cloud.tradeapi.enums.notify; + +// TODO @芋艿:这个枚举的作用? +public interface TradeNotifyEnums { +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderCancelTypeEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderCancelTypeEnum.java new file mode 100644 index 0000000..99fec84 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderCancelTypeEnum.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.tradeapi.enums.order; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易订单 - 关闭类型 + * + * @author Sin + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderCancelTypeEnum implements ArrayValuable { + + PAY_TIMEOUT(10, "超时未支付"), + AFTER_SALE_CLOSE(20, "退款关闭"), + MEMBER_CANCEL(30, "买家取消"), + COMBINATION_CLOSE(40, "拼团关闭"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(TradeOrderCancelTypeEnum::getType).toArray(Integer[]::new); + + /** + * 关闭类型 + */ + private final Integer type; + /** + * 关闭类型名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderItemAfterSaleStatusEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderItemAfterSaleStatusEnum.java new file mode 100644 index 0000000..5a7f6a9 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderItemAfterSaleStatusEnum.java @@ -0,0 +1,49 @@ +package com.tashow.cloud.tradeapi.enums.order; + +import cn.hutool.core.util.ObjectUtil; +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易订单项 - 售后状态 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderItemAfterSaleStatusEnum implements ArrayValuable { + + NONE(0, "未售后"), + APPLY(10, "售后中"), + SUCCESS(20, "售后成功"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(TradeOrderItemAfterSaleStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + /** + * 判断指定状态,是否正处于【未申请】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isNone(Integer status) { + return ObjectUtil.equals(status, NONE.getStatus()); + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderOperateTypeEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderOperateTypeEnum.java new file mode 100644 index 0000000..44cbd88 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderOperateTypeEnum.java @@ -0,0 +1,42 @@ +package com.tashow.cloud.tradeapi.enums.order; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 订单操作类型的枚举 + * + * @author 陈賝 + * @since 2023/7/6 15:31 + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderOperateTypeEnum { + + MEMBER_CREATE(1, "用户下单"), + ADMIN_UPDATE_PRICE(2, "订单价格 {oldPayPrice} 修改,调整价格 {adjustPrice},实际支付金额为 {newPayPrice} 元"), + MEMBER_PAY(10, "用户付款成功"), + ADMIN_UPDATE_ADDRESS(11, "收货地址修改"), + ADMIN_DELIVERY(20, "已发货,快递公司:{expressName},快递单号:{logisticsNo}"), + MEMBER_RECEIVE(30, "用户已收货"), + SYSTEM_RECEIVE(31, "到期未收货,系统自动确认收货"), + ADMIN_PICK_UP_RECEIVE(32, "管理员自提收货"), + MEMBER_COMMENT(33, "用户评价"), + SYSTEM_COMMENT(34, "到期未评价,系统自动评价"), + MEMBER_CANCEL(40, "取消订单"), + SYSTEM_CANCEL(41, "到期未支付,系统自动取消订单"), + // 42 预留:管理员取消订单 + ADMIN_CANCEL_AFTER_SALE(43, "订单全部售后,管理员自动取消订单"), + MEMBER_DELETE(49, "删除订单"), + ; + + /** + * 操作类型 + */ + private final Integer type; + /** + * 操作描述 + */ + private final String content; + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderRefundStatusEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderRefundStatusEnum.java new file mode 100644 index 0000000..b7702e6 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderRefundStatusEnum.java @@ -0,0 +1,38 @@ +package com.tashow.cloud.tradeapi.enums.order; + +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易订单 - 退款状态 + * + * @author Sin + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderRefundStatusEnum implements ArrayValuable { + + NONE(0, "未退款"), + PART(10, "部分退款"), + ALL(20, "全部退款"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(TradeOrderRefundStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderStatusEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderStatusEnum.java new file mode 100644 index 0000000..fa1a997 --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderStatusEnum.java @@ -0,0 +1,116 @@ +package com.tashow.cloud.tradeapi.enums.order; + +import cn.hutool.core.util.ObjectUtil; +import com.tashow.cloud.common.core.ArrayValuable; +import com.tashow.cloud.common.util.object.ObjectUtils; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易订单 - 状态 + * + * @author Sin + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderStatusEnum implements ArrayValuable { + + UNPAID(0, "待支付"), + UNDELIVERED(10, "待发货"), + DELIVERED(20, "已发货"), + COMPLETED(30, "已完成"), + CANCELED(40, "已取消"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(TradeOrderStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + // ========== 问:为什么写了很多 isXXX 和 haveXXX 的判断逻辑呢? ========== + // ========== 答:方便找到某一类判断,哪些业务正在使用 ========== + + /** + * 判断指定状态,是否正处于【未付款】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isUnpaid(Integer status) { + return ObjectUtil.equal(UNPAID.getStatus(), status); + } + + /** + * 判断指定状态,是否正处于【待发货】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isUndelivered(Integer status) { + return ObjectUtil.equal(UNDELIVERED.getStatus(), status); + } + + /** + * 判断指定状态,是否正处于【已发货】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isDelivered(Integer status) { + return ObjectUtil.equals(status, DELIVERED.getStatus()); + } + + /** + * 判断指定状态,是否正处于【已取消】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isCanceled(Integer status) { + return ObjectUtil.equals(status, CANCELED.getStatus()); + } + + /** + * 判断指定状态,是否正处于【已完成】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isCompleted(Integer status) { + return ObjectUtil.equals(status, COMPLETED.getStatus()); + } + + /** + * 判断指定状态,是否有过【已付款】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean havePaid(Integer status) { + return ObjectUtils.equalsAny(status, UNDELIVERED.getStatus(), + DELIVERED.getStatus(), COMPLETED.getStatus()); + } + + /** + * 判断指定状态,是否有过【已发货】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean haveDelivered(Integer status) { + return ObjectUtils.equalsAny(status, DELIVERED.getStatus(), COMPLETED.getStatus()); + } + +} diff --git a/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderTypeEnum.java b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderTypeEnum.java new file mode 100644 index 0000000..531f3cb --- /dev/null +++ b/tashow-feign/tashow-trade-api/src/main/java/com/tashow/cloud/tradeapi/enums/order/TradeOrderTypeEnum.java @@ -0,0 +1,62 @@ +package com.tashow.cloud.tradeapi.enums.order; + +import cn.hutool.core.util.ObjectUtil; +import com.tashow.cloud.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易订单 - 类型 + * + * @author Sin + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderTypeEnum implements ArrayValuable { + + NORMAL(0, "普通订单"), + SECKILL(1, "秒杀订单"), + BARGAIN(2, "砍价订单"), + COMBINATION(3, "拼团订单"), + POINT(4, "积分商城"), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(TradeOrderTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static boolean isNormal(Integer type) { + return ObjectUtil.equal(type, NORMAL.getType()); + } + + public static boolean isSeckill(Integer type) { + return ObjectUtil.equal(type, SECKILL.getType()); + } + + public static boolean isBargain(Integer type) { + return ObjectUtil.equal(type, BARGAIN.getType()); + } + + public static boolean isCombination(Integer type) { + return ObjectUtil.equal(type, COMBINATION.getType()); + } + + public static boolean isPoint(Integer type) { + return ObjectUtil.equal(type, POINT.getType()); + } + +} diff --git a/tashow-module/pom.xml b/tashow-module/pom.xml index 37c811b..0ec0ea6 100644 --- a/tashow-module/pom.xml +++ b/tashow-module/pom.xml @@ -14,6 +14,8 @@ tashow-module-system tashow-module-infra tashow-module-app + tashow-module-pay + tashow-module-trade diff --git a/tashow-module/tashow-module-infra/src/main/resources/application-local.yaml b/tashow-module/tashow-module-infra/src/main/resources/application-local.yaml index 15b2156..da745cc 100644 --- a/tashow-module/tashow-module-infra/src/main/resources/application-local.yaml +++ b/tashow-module/tashow-module-infra/src/main/resources/application-local.yaml @@ -7,11 +7,11 @@ spring: username: nacos # Nacos 账号 password: nacos # Nacos 密码 discovery: # 【配置中心】配置项 - namespace: 76667956-2ac2-4e05-906b-4bca4ebcc5f0 # 命名空间。这里使用 dev 开发环境 + namespace: liwq # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP metadata: version: 1.0.0 # 服务实例的版本号,可用于灰度发布 config: # 【注册中心】配置项 - namespace: 76667956-2ac2-4e05-906b-4bca4ebcc5f0 # 命名空间。这里使用 dev 开发环境 + namespace: liwq # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP diff --git a/tashow-module/tashow-module-infra/src/main/resources/application.yaml b/tashow-module/tashow-module-infra/src/main/resources/application.yaml index e4e14f6..2fdefa0 100644 --- a/tashow-module/tashow-module-infra/src/main/resources/application.yaml +++ b/tashow-module/tashow-module-infra/src/main/resources/application.yaml @@ -10,4 +10,4 @@ spring: - optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置 - optional:nacos:application.yaml # 加载【Nacos】的配置 - optional:nacos:tenant.yaml # 加载【Nacos】的配置 - - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置 + - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置 \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/controller.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/controller.vm deleted file mode 100644 index e47b61e..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/controller.vm +++ /dev/null @@ -1,253 +0,0 @@ -package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}; - -import org.springframework.web.bind.annotation.*; -import ${jakartaPackage}.annotation.Resource; -import org.springframework.validation.annotation.Validated; -#if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end - -import ${jakartaPackage}.validation.constraints.*; -import ${jakartaPackage}.validation.*; -import ${jakartaPackage}.servlet.http.*; -import java.util.*; -import java.io.IOException; - -import ${PageParamClassName}; -import ${PageResultClassName}; -import ${CommonResultClassName}; -import ${BeanUtils}; -import static ${CommonResultClassName}.success; - -import ${ExcelUtilsClassName}; - -import ${ApiAccessLogClassName}; -import static ${OperateTypeEnumClassName}.*; - -import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; -import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) -import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; -#end -import ${basePackage}.module.${table.moduleName}.service.${table.businessName}.${table.className}Service; - -/** - * ${sceneEnum.name} - ${table.classComment} - */ -@RestController -##二级的 businessName 暂时不算在 HTTP 路径上,可以根据需要写 -@RequestMapping("/${table.moduleName}/${simpleClassName_strikeCase}") -@Validated -public class ${sceneEnum.prefixClass}${table.className}Controller { - - @Resource - private ${table.className}Service ${classNameVar}Service; - - /** - * 创建${table.classComment} - */ - @PostMapping("/create") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')") -#end - public CommonResult<${primaryColumn.javaType}> create${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO) { - return success(${classNameVar}Service.create${simpleClassName}(createReqVO)); - } - - /** - * 更新${table.classComment} - */ - @PutMapping("/update") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')") -#end - public CommonResult update${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO) { - ${classNameVar}Service.update${simpleClassName}(updateReqVO); - return success(true); - } - - /** - * 删除${table.classComment} - */ - @DeleteMapping("/delete") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')") -#end - public CommonResult delete${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { - ${classNameVar}Service.delete${simpleClassName}(id); - return success(true); - } - - /** - * 获得${table.classComment} - */ - @GetMapping("/get") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") -#end - public CommonResult<${sceneEnum.prefixClass}${table.className}RespVO> get${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { - ${table.className}DO ${classNameVar} = ${classNameVar}Service.get${simpleClassName}(id); - return success(BeanUtils.toBean(${classNameVar}, ${sceneEnum.prefixClass}${table.className}RespVO.class)); - } - -#if ( $table.templateType != 2 ) - - /** - * 获得${table.classComment}分页 - */ - @GetMapping("/page") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") -#end - public CommonResult> get${simpleClassName}Page(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) { - PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(pageReqVO); - return success(BeanUtils.toBean(pageResult, ${sceneEnum.prefixClass}${table.className}RespVO.class)); - } - -## 特殊:树表专属逻辑(树不需要分页接口) -#else - /** - * 获得${table.classComment}列表 - */ - @GetMapping("/list") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") -#end - public CommonResult> get${simpleClassName}List(@Valid ${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO) { - List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(listReqVO); - return success(BeanUtils.toBean(list, ${sceneEnum.prefixClass}${table.className}RespVO.class)); - } - -#end - /** - * 导出${table.classComment} Excel - */ - @GetMapping("/export-excel") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:export')") -#end - @ApiAccessLog(operateType = EXPORT) -#if ( $table.templateType != 2 ) - public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO, - HttpServletResponse response) throws IOException { - pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); - List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}Page(pageReqVO).getList(); - // 导出 Excel - ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${sceneEnum.prefixClass}${table.className}RespVO.class, - BeanUtils.toBean(list, ${sceneEnum.prefixClass}${table.className}RespVO.class)); - } -## 特殊:树表专属逻辑(树不需要分页接口) -#else - public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO, - HttpServletResponse response) throws IOException { - List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(listReqVO); - // 导出 Excel - ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${table.className}RespVO.class, - BeanUtils.toBean(list, ${table.className}RespVO.class)); - } -#end - -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) -#set ($subSimpleClassName = $subSimpleClassNames.get($index)) -#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 -#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 -#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) -#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) -#set ($subClassNameVar = $subClassNameVars.get($index)) - // ==================== 子表($subTable.classComment) ==================== - -## 情况一:MASTER_ERP 时,需要分查询页子表 -#if ( $table.templateType == 11 ) - /** - * 获得${subTable.classComment}分页 - */ - @GetMapping("/${subSimpleClassName_strikeCase}/page") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") -#end - public CommonResult> get${subSimpleClassName}Page(PageParam pageReqVO, - @RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return success(${classNameVar}Service.get${subSimpleClassName}Page(pageReqVO, ${subJoinColumn.javaField})); - } - -## 情况二:非 MASTER_ERP 时,需要列表查询子表 -#else - #if ( $subTable.subJoinMany ) - /** - * 获得${subTable.classComment}列表 - */ - @GetMapping("/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") -#end - public CommonResult> get${subSimpleClassName}ListBy${SubJoinColumnName}(@RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return success(${classNameVar}Service.get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField})); - } - - #else - /** - * 获得${subTable.classComment} - */ - @GetMapping("/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") -#end - public CommonResult<${subTable.className}DO> get${subSimpleClassName}By${SubJoinColumnName}(@RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return success(${classNameVar}Service.get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField})); - } - - #end -#end -## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 -#if ( $table.templateType == 11 ) - /** - * 创建${subTable.classComment} - */ - @PostMapping("/${subSimpleClassName_strikeCase}/create") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')") -#end - public CommonResult<${subPrimaryColumn.javaType}> create${subSimpleClassName}(@Valid @RequestBody ${subTable.className}DO ${subClassNameVar}) { - return success(${classNameVar}Service.create${subSimpleClassName}(${subClassNameVar})); - } - - /** - * 更新${subTable.classComment} - */ - @PutMapping("/${subSimpleClassName_strikeCase}/update") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')") -#end - public CommonResult update${subSimpleClassName}(@Valid @RequestBody ${subTable.className}DO ${subClassNameVar}) { - ${classNameVar}Service.update${subSimpleClassName}(${subClassNameVar}); - return success(true); - } - - /** - * 删除${subTable.classComment} - */ - @DeleteMapping("/${subSimpleClassName_strikeCase}/delete") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')") -#end - public CommonResult delete${subSimpleClassName}(@RequestParam("id") ${subPrimaryColumn.javaType} id) { - ${classNameVar}Service.delete${subSimpleClassName}(id); - return success(true); - } - - /** - * 获得${subTable.classComment} - */ - @GetMapping("/${subSimpleClassName_strikeCase}/get") -#if ($sceneEnum.scene == 1) - @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") -#end - public CommonResult<${subTable.className}DO> get${subSimpleClassName}(@RequestParam("id") ${subPrimaryColumn.javaType} id) { - return success(${classNameVar}Service.get${subSimpleClassName}(id)); - } - -#end -#end -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/listReqVO.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/listReqVO.vm deleted file mode 100644 index 4797c0b..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/listReqVO.vm +++ /dev/null @@ -1,51 +0,0 @@ -package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; - -import lombok.*; -import java.util.*; - -import ${PageParamClassName}; -#foreach ($column in $columns) -#if (${column.javaType} == "BigDecimal") -import java.math.BigDecimal; -#break -#end -#end -## 处理 LocalDateTime 字段的引入 -#foreach ($column in $columns) -#if (${column.listOperation} && ${column.javaType} == "LocalDateTime") -import java.time.LocalDateTime; -import org.springframework.format.annotation.DateTimeFormat; - -import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; -#break -#end -#end -## 字段模板 -#macro(columnTpl $prefix $prefixStr) - /** - * ${prefixStr}${column.columnComment}"#if ("$!column.example" != " - */, example = "${column.example}"#end) - private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; -#end - -/** - * ${sceneEnum.name} - ${table.classComment}列表 Request VO - */ -@Data -public class ${sceneEnum.prefixClass}${table.className}ListReqVO { - -#foreach ($column in $columns) -#if (${column.listOperation})##查询操作 -#if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 - /** - * ${column.columnComment}"#if ("$!column.example" != " - */, example = "${column.example}"#end) - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private ${column.javaType}[] ${column.javaField}; -#else##情况二,非 Between 的时间 - #columnTpl('', '') -#end - -#end -#end -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/pageReqVO.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/pageReqVO.vm deleted file mode 100644 index f7841c0..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/pageReqVO.vm +++ /dev/null @@ -1,53 +0,0 @@ -package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; - -import lombok.*; -import java.util.*; - -import ${PageParamClassName}; -#foreach ($column in $columns) -#if (${column.javaType} == "BigDecimal") -import java.math.BigDecimal; -#break -#end -#end -## 处理 LocalDateTime 字段的引入 -#foreach ($column in $columns) -#if (${column.listOperationCondition} && ${column.javaType} == "LocalDateTime") -import org.springframework.format.annotation.DateTimeFormat; -import java.time.LocalDateTime; - -import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; -#break -#end -#end -## 字段模板 -#macro(columnTpl $prefix $prefixStr) - /** - * ${prefixStr}${column.columnComment}"#if ("$!column.example" != " - */, example = "${column.example}"#end) - private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; -#end - -/** - * ${sceneEnum.name} - ${table.classComment}分页 Request VO - */ -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class ${sceneEnum.prefixClass}${table.className}PageReqVO extends PageParam { - -#foreach ($column in $columns) -#if (${column.listOperation})##查询操作 -#if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 - /** - * ${column.columnComment}"#if ("$!column.example" != " - */, example = "${column.example}"#end) - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private ${column.javaType}[] ${column.javaField}; -#else##情况二,非 Between 的时间 - #columnTpl('', '') -#end - -#end -#end -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm deleted file mode 100644 index e4fcf6f..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm +++ /dev/null @@ -1,57 +0,0 @@ -package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; - - -import lombok.*; -import java.util.*; -## 处理 BigDecimal 字段的引入 -#foreach ($column in $columns) -#if (${column.javaType} == "BigDecimal") -import java.math.BigDecimal; -#break -#end -#end -## 处理 LocalDateTime 字段的引入 -#foreach ($column in $columns) -#if (${column.listOperationResult} && ${column.javaType} == "LocalDateTime") -import org.springframework.format.annotation.DateTimeFormat; -import java.time.LocalDateTime; -#break -#end -#end -## 处理 Excel 导出 -import com.alibaba.excel.annotation.*; -#foreach ($column in $columns) -#if ("$!column.dictType" != "")## 有设置数据字典 -import ${DictFormatClassName}; -import ${DictConvertClassName}; -#break -#end -#end - -/** - * ${sceneEnum.name} - ${table.classComment} Response VO - */ -@Data -@ExcelIgnoreUnannotated -public class ${sceneEnum.prefixClass}${table.className}RespVO { - -## 逐个处理字段 -#foreach ($column in $columns) -#if (${column.listOperationResult}) -## 1. 处理 Swagger 注解 - /** - * ${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != " - */, example = "${column.example}"#end) -## 2. 处理 Excel 导出 -#if ("$!column.dictType" != "")##处理枚举值 - @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class) - @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 -#else - @ExcelProperty("${column.columnComment}") -#end -## 3. 处理字段定义 - private ${column.javaType} ${column.javaField}; - -#end -#end -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/saveReqVO.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/saveReqVO.vm deleted file mode 100644 index ba32472..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/controller/vo/saveReqVO.vm +++ /dev/null @@ -1,72 +0,0 @@ -package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; - - -import lombok.*; -import java.util.*; -import ${jakartaPackage}.validation.constraints.*; -## 处理 BigDecimal 字段的引入 -#foreach ($column in $columns) -#if (${column.javaType} == "BigDecimal") -import java.math.BigDecimal; -#break -#end -#end -## 处理 LocalDateTime 字段的引入 -#foreach ($column in $columns) -#if ((${column.createOperation} || ${column.updateOperation}) && ${column.javaType} == "LocalDateTime") -import org.springframework.format.annotation.DateTimeFormat; -import java.time.LocalDateTime; -#break -#end -#end -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) -import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; -#end - -/** - * ${sceneEnum.name} - ${table.classComment}新增/修改 Request VO - */ -@Data -public class ${sceneEnum.prefixClass}${table.className}SaveReqVO { - -## 逐个处理字段 -#foreach ($column in $columns) -#if (${column.createOperation} || ${column.updateOperation}) -## 1. 处理 Swagger 注解 - /** - * ${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != " - */, example = "${column.example}"#end) -## 2. 处理 Validator 参数校验 -#if (!${column.nullable} && !${column.primaryKey}) -#if (${column.javaType} == 'String') - @NotEmpty(message = "${column.columnComment}不能为空") -#else - @NotNull(message = "${column.columnComment}不能为空") -#end -#end -## 3. 处理字段定义 - private ${column.javaType} ${column.javaField}; - -#end -#end -## 特殊:主子表专属逻辑(非 ERP 模式) -#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) - #if ( $subTable.subJoinMany) - /** - * ${subTable.classComment}列表 - */ - private List<${subTable.className}DO> ${subClassNameVars.get($index)}s; - - #else - /** - * ${subTable.classComment} - */ - private ${subTable.className}DO ${subClassNameVars.get($index)}; - - #end -#end -#end -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/do.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/do.vm deleted file mode 100644 index b019d6e..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/do.vm +++ /dev/null @@ -1,52 +0,0 @@ -package ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}; - -import lombok.*; -import java.util.*; -#foreach ($column in $columns) -#if (${column.javaType} == "BigDecimal") -import java.math.BigDecimal; -#end -#if (${column.javaType} == "LocalDateTime") -import java.time.LocalDateTime; -#end -#end -import com.baomidou.mybatisplus.annotation.*; -import ${BaseDOClassName}; - -/** - * ${table.classComment} DO - * - * @author ${table.author} - */ -@TableName("${table.tableName.toLowerCase()}") -@KeySequence("${table.tableName.toLowerCase()}_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ${table.className}DO extends BaseDO { - -## 特殊:树表专属逻辑 -#if ( $table.templateType == 2 ) - public static final Long ${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT = 0L; - -#end -#foreach ($column in $columns) -#if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段 - /** - * ${column.columnComment} - #if ("$!column.dictType" != "")##处理枚举值 - * - * 枚举 {@link TODO ${column.dictType} 对应的类} - #end - */ - #if (${column.primaryKey})##处理主键 - @TableId#if (${column.javaType} == 'String')(type = IdType.INPUT)#end - #end - private ${column.javaType} ${column.javaField}; -#end -#end - -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/do_sub.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/do_sub.vm deleted file mode 100644 index 16be55e..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/do_sub.vm +++ /dev/null @@ -1,49 +0,0 @@ -#set ($subTable = $subTables.get($subIndex))##当前表 -#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 -package ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}; - -import lombok.*; -import java.util.*; -#foreach ($column in $subColumns) -#if (${column.javaType} == "BigDecimal") -import java.math.BigDecimal; -#end -#if (${column.javaType} == "LocalDateTime") -import java.time.LocalDateTime; -#end -#end -import com.baomidou.mybatisplus.annotation.*; -import ${BaseDOClassName}; - -/** - * ${subTable.classComment} DO - * - * @author ${subTable.author} - */ -@TableName("${subTable.tableName.toLowerCase()}") -@KeySequence("${subTable.tableName.toLowerCase()}_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ${subTable.className}DO extends BaseDO { - -#foreach ($column in $subColumns) -#if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段 - /** - * ${column.columnComment} - #if ("$!column.dictType" != "")##处理枚举值 - * - * 枚举 {@link TODO ${column.dictType} 对应的类} - #end - */ - #if (${column.primaryKey})##处理主键 - @TableId#if (${column.javaType} == 'String')(type = IdType.INPUT)#end - #end - private ${column.javaType} ${column.javaField}; -#end -#end - -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/mapper.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/mapper.vm deleted file mode 100644 index b98b471..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/mapper.vm +++ /dev/null @@ -1,82 +0,0 @@ -package ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}; - -import java.util.*; - -import ${PageResultClassName}; -import ${QueryWrapperClassName}; -import ${BaseMapperClassName}; -import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; -import org.apache.ibatis.annotations.Mapper; -import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; - -## 字段模板 -#macro(listCondition) -#foreach ($column in $columns) -#if (${column.listOperation}) -#set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 -#if (${column.listOperationCondition} == "=")##情况一,= 的时候 - .eqIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) -#end -#if (${column.listOperationCondition} == "!=")##情况二,!= 的时候 - .neIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) -#end -#if (${column.listOperationCondition} == ">")##情况三,> 的时候 - .gtIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) -#end -#if (${column.listOperationCondition} == ">=")##情况四,>= 的时候 - .geIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) -#end -#if (${column.listOperationCondition} == "<")##情况五,< 的时候 - .ltIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) -#end -#if (${column.listOperationCondition} == "<=")##情况五,<= 的时候 - .leIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) -#end -#if (${column.listOperationCondition} == "LIKE")##情况七,Like 的时候 - .likeIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) -#end -#if (${column.listOperationCondition} == "BETWEEN")##情况八,Between 的时候 - .betweenIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) -#end -#end -#end -#end -/** - * ${table.classComment} Mapper - * - * @author ${table.author} - */ -@Mapper -public interface ${table.className}Mapper extends BaseMapperX<${table.className}DO> { - -## 特殊:树表专属逻辑(树不需要分页接口) -#if ( $table.templateType != 2 ) - default PageResult<${table.className}DO> selectPage(${sceneEnum.prefixClass}${table.className}PageReqVO reqVO) { - return selectPage(reqVO, new LambdaQueryWrapperX<${table.className}DO>() - #listCondition() - .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序 - - } -#else - default List<${table.className}DO> selectList(${sceneEnum.prefixClass}${table.className}ListReqVO reqVO) { - return selectList(new LambdaQueryWrapperX<${table.className}DO>() - #listCondition() - .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序 - - } -#end - -## 特殊:树表专属逻辑 -#if ( $table.templateType == 2 ) -#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 -#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 - default ${table.className}DO selectBy${TreeParentJavaField}And${TreeNameJavaField}(Long ${treeParentColumn.javaField}, String ${treeNameColumn.javaField}) { - return selectOne(${table.className}DO::get${TreeParentJavaField}, ${treeParentColumn.javaField}, ${table.className}DO::get${TreeNameJavaField}, ${treeNameColumn.javaField}); - } - - default Long selectCountBy${TreeParentJavaField}(${treeParentColumn.javaType} ${treeParentColumn.javaField}) { - return selectCount(${table.className}DO::get${TreeParentJavaField}, ${treeParentColumn.javaField}); - } - -#end -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/mapper.xml.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/mapper.xml.vm deleted file mode 100644 index 290378d..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/mapper.xml.vm +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/mapper_sub.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/mapper_sub.vm deleted file mode 100644 index 6ccaea7..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/dal/mapper_sub.vm +++ /dev/null @@ -1,57 +0,0 @@ -#set ($subTable = $subTables.get($subIndex))##当前表 -#set ($subColumns = $subJoinColumnsList.get($subIndex))##当前字段数组 -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 -package ${basePackage}.module.${subTable.moduleName}.dal.mysql.${subTable.businessName}; - -import java.util.*; - -import ${PageResultClassName}; -import ${PageParamClassName}; -import ${QueryWrapperClassName}; -import ${BaseMapperClassName}; -import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; -import org.apache.ibatis.annotations.Mapper; - -/** - * ${subTable.classComment} Mapper - * - * @author ${subTable.author} - */ -@Mapper -public interface ${subTable.className}Mapper extends BaseMapperX<${subTable.className}DO> { - -## 情况一:MASTER_ERP 时,需要分查询页子表 -#if ( $table.templateType == 11 ) - default PageResult<${subTable.className}DO> selectPage(PageParam reqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return selectPage(reqVO, new LambdaQueryWrapperX<${subTable.className}DO>() - .eq(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}) - .orderByDesc(${subTable.className}DO::getId));## 大多数情况下,id 倒序 - - } -## 主表与子表是一对一时 - #if (!$subTable.subJoinMany) - default ${subTable.className}DO selectBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return selectOne(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); - } - #end - -## 情况二:非 MASTER_ERP 时,需要列表查询子表 -#else - #if ( $subTable.subJoinMany) - default List<${subTable.className}DO> selectListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return selectList(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); - } - - #else - default ${subTable.className}DO selectBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return selectOne(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); - } - - #end - #end - default int deleteBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return delete(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); - } - -} diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/enums/errorcode.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/enums/errorcode.vm deleted file mode 100644 index b7e21e6..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/enums/errorcode.vm +++ /dev/null @@ -1,22 +0,0 @@ -// TODO 待办:请将下面的错误码复制到 yudao-module-${table.moduleName}-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!! -// ========== ${table.classComment} TODO 补充编号 ========== -ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS = new ErrorCode(TODO 补充编号, "${table.classComment}不存在"); -## 特殊:树表专属逻辑 -#if ( $table.templateType == 2 ) -ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_EXITS_CHILDREN = new ErrorCode(TODO 补充编号, "存在存在子${table.classComment},无法删除"); -ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_NOT_EXITS = new ErrorCode(TODO 补充编号,"父级${table.classComment}不存在"); -ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_ERROR = new ErrorCode(TODO 补充编号, "不能设置自己为父${table.classComment}"); -ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE = new ErrorCode(TODO 补充编号, "已经存在该${treeNameColumn.columnComment}的${table.classComment}"); -ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_IS_CHILD = new ErrorCode(TODO 补充编号, "不能设置自己的子${table.className}为父${table.className}"); -#end -## 特殊:主子表专属逻辑 -#if ( $table.templateType == 11 )## 特殊:ERP 情况 -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) -#set ($simpleClassNameUnderlineCase = $simpleClassNameUnderlineCases.get($index)) -ErrorCode ${simpleClassNameUnderlineCase.toUpperCase()}_NOT_EXISTS = new ErrorCode(TODO 补充编号, "${subTable.classComment}不存在"); -#if ( !$subTable.subJoinMany ) -ErrorCode ${simpleClassNameUnderlineCase.toUpperCase()}_EXISTS = new ErrorCode(TODO 补充编号, "${subTable.classComment}已存在"); -#end -#end -#end \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/service/service.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/service/service.vm deleted file mode 100644 index c4ee4f0..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/service/service.vm +++ /dev/null @@ -1,147 +0,0 @@ -package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; - -import java.util.*; -import ${jakartaPackage}.validation.*; -import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; -import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) -import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; -#end -import ${PageResultClassName}; -import ${PageParamClassName}; - -/** - * ${table.classComment} Service 接口 - * - * @author ${table.author} - */ -public interface ${table.className}Service { - - /** - * 创建${table.classComment} - * - * @param createReqVO 创建信息 - * @return 编号 - */ - ${primaryColumn.javaType} create${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO); - - /** - * 更新${table.classComment} - * - * @param updateReqVO 更新信息 - */ - void update${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO); - - /** - * 删除${table.classComment} - * - * @param id 编号 - */ - void delete${simpleClassName}(${primaryColumn.javaType} id); - - /** - * 获得${table.classComment} - * - * @param id 编号 - * @return ${table.classComment} - */ - ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id); - -## 特殊:树表专属逻辑(树不需要分页接口) -#if ( $table.templateType != 2 ) - /** - * 获得${table.classComment}分页 - * - * @param pageReqVO 分页查询 - * @return ${table.classComment}分页 - */ - PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO); -#else - /** - * 获得${table.classComment}列表 - * - * @param listReqVO 查询条件 - * @return ${table.classComment}列表 - */ - List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO); -#end - -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) -#set ($subSimpleClassName = $subSimpleClassNames.get($index)) -#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 -#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 -#set ($subClassNameVar = $subClassNameVars.get($index)) - // ==================== 子表($subTable.classComment) ==================== - -## 情况一:MASTER_ERP 时,需要分查询页子表 -#if ( $table.templateType == 11 ) - /** - * 获得${subTable.classComment}分页 - * - * @param pageReqVO 分页查询 - * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment} - * @return ${subTable.classComment}分页 - */ - PageResult<${subTable.className}DO> get${subSimpleClassName}Page(PageParam pageReqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}); - -## 情况二:非 MASTER_ERP 时,需要列表查询子表 -#else - #if ( $subTable.subJoinMany ) - /** - * 获得${subTable.classComment}列表 - * - * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment} - * @return ${subTable.classComment}列表 - */ - List<${subTable.className}DO> get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}); - - #else - /** - * 获得${subTable.classComment} - * - * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment} - * @return ${subTable.classComment} - */ - ${subTable.className}DO get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}); - - #end -#end -## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 -#if ( $table.templateType == 11 ) - /** - * 创建${subTable.classComment} - * - * @param ${subClassNameVar} 创建信息 - * @return 编号 - */ - ${subPrimaryColumn.javaType} create${subSimpleClassName}(@Valid ${subTable.className}DO ${subClassNameVar}); - - /** - * 更新${subTable.classComment} - * - * @param ${subClassNameVar} 更新信息 - */ - void update${subSimpleClassName}(@Valid ${subTable.className}DO ${subClassNameVar}); - - /** - * 删除${subTable.classComment} - * - * @param id 编号 - */ - void delete${subSimpleClassName}(${subPrimaryColumn.javaType} id); - - /** - * 获得${subTable.classComment} - * - * @param id 编号 - * @return ${subTable.classComment} - */ - ${subTable.className}DO get${subSimpleClassName}(${subPrimaryColumn.javaType} id); - -#end -#end -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm deleted file mode 100644 index 80bc71b..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm +++ /dev/null @@ -1,351 +0,0 @@ -package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; - -import org.springframework.stereotype.Service; -import ${jakartaPackage}.annotation.Resource; -import org.springframework.validation.annotation.Validated; -import org.springframework.transaction.annotation.Transactional; - -import java.util.*; -import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; -import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) -import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; -#end -import ${PageResultClassName}; -import ${PageParamClassName}; -import ${BeanUtils}; - -import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper; -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) -import ${basePackage}.module.${subTable.moduleName}.dal.mysql.${subTable.businessName}.${subTable.className}Mapper; -#end - -import static ${ServiceExceptionUtilClassName}.exception; -import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*; - -/** - * ${table.classComment} Service 实现类 - * - * @author ${table.author} - */ -@Service -@Validated -public class ${table.className}ServiceImpl implements ${table.className}Service { - - @Resource - private ${table.className}Mapper ${classNameVar}Mapper; -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) - @Resource - private ${subTable.className}Mapper ${subClassNameVars.get($index)}Mapper; -#end - - @Override -## 特殊:主子表专属逻辑(非 ERP 模式) -#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) - @Transactional(rollbackFor = Exception.class) -#end - public ${primaryColumn.javaType} create${simpleClassName}(${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO) { -## 特殊:树表专属逻辑 -#if ( $table.templateType == 2 ) -#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 -#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 - // 校验${treeParentColumn.columnComment}的有效性 - validateParent${simpleClassName}(null, createReqVO.get${TreeParentJavaField}()); - // 校验${treeNameColumn.columnComment}的唯一性 - validate${simpleClassName}${TreeNameJavaField}Unique(null, createReqVO.get${TreeParentJavaField}(), createReqVO.get${TreeNameJavaField}()); - -#end - // 插入 - ${table.className}DO ${classNameVar} = BeanUtils.toBean(createReqVO, ${table.className}DO.class); - ${classNameVar}Mapper.insert(${classNameVar}); -## 特殊:主子表专属逻辑(非 ERP 模式) -#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) - - // 插入子表 -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) -#set ($subSimpleClassName = $subSimpleClassNames.get($index)) -#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 - #if ( $subTable.subJoinMany) - create${subSimpleClassName}List(${classNameVar}.getId(), createReqVO.get${subSimpleClassNames.get($index)}s()); - #else - create${subSimpleClassName}(${classNameVar}.getId(), createReqVO.get${subSimpleClassNames.get($index)}()); - #end -#end -#end - // 返回 - return ${classNameVar}.getId(); - } - - @Override -## 特殊:主子表专属逻辑(非 ERP 模式) -#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) - @Transactional(rollbackFor = Exception.class) -#end - public void update${simpleClassName}(${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO) { - // 校验存在 - validate${simpleClassName}Exists(updateReqVO.getId()); -## 特殊:树表专属逻辑 -#if ( $table.templateType == 2 ) -#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 -#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 - // 校验${treeParentColumn.columnComment}的有效性 - validateParent${simpleClassName}(updateReqVO.getId(), updateReqVO.get${TreeParentJavaField}()); - // 校验${treeNameColumn.columnComment}的唯一性 - validate${simpleClassName}${TreeNameJavaField}Unique(updateReqVO.getId(), updateReqVO.get${TreeParentJavaField}(), updateReqVO.get${TreeNameJavaField}()); - -#end - // 更新 - ${table.className}DO updateObj = BeanUtils.toBean(updateReqVO, ${table.className}DO.class); - ${classNameVar}Mapper.updateById(updateObj); -## 特殊:主子表专属逻辑(非 ERP 模式) -#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11) - - // 更新子表 -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) -#set ($subSimpleClassName = $subSimpleClassNames.get($index)) -#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 - #if ( $subTable.subJoinMany) - update${subSimpleClassName}List(updateReqVO.getId(), updateReqVO.get${subSimpleClassNames.get($index)}s()); - #else - update${subSimpleClassName}(updateReqVO.getId(), updateReqVO.get${subSimpleClassNames.get($index)}()); - #end -#end -#end - } - - @Override -## 特殊:主子表专属逻辑 -#if ( $subTables && $subTables.size() > 0) - @Transactional(rollbackFor = Exception.class) -#end - public void delete${simpleClassName}(${primaryColumn.javaType} id) { - // 校验存在 - validate${simpleClassName}Exists(id); -## 特殊:树表专属逻辑 -#if ( $table.templateType == 2 ) -#set ($ParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 - // 校验是否有子${table.classComment} - if (${classNameVar}Mapper.selectCountBy${ParentJavaField}(id) > 0) { - throw exception(${simpleClassName_underlineCase.toUpperCase()}_EXITS_CHILDREN); - } -#end - // 删除 - ${classNameVar}Mapper.deleteById(id); -## 特殊:主子表专属逻辑 -#if ( $subTables && $subTables.size() > 0) - - // 删除子表 -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) -#set ($subSimpleClassName = $subSimpleClassNames.get($index)) -#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 - delete${subSimpleClassName}By${SubJoinColumnName}(id); -#end -#end - } - - private void validate${simpleClassName}Exists(${primaryColumn.javaType} id) { - if (${classNameVar}Mapper.selectById(id) == null) { - throw exception(${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); - } - } - -## 特殊:树表专属逻辑 -#if ( $table.templateType == 2 ) -#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 -#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 - private void validateParent${simpleClassName}(Long id, Long ${treeParentColumn.javaField}) { - if (${treeParentColumn.javaField} == null || ${simpleClassName}DO.${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT.equals(${treeParentColumn.javaField})) { - return; - } - // 1. 不能设置自己为父${table.classComment} - if (Objects.equals(id, ${treeParentColumn.javaField})) { - throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_ERROR); - } - // 2. 父${table.classComment}不存在 - ${simpleClassName}DO parent${simpleClassName} = ${classNameVar}Mapper.selectById(${treeParentColumn.javaField}); - if (parent${simpleClassName} == null) { - throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_NOT_EXITS); - } - // 3. 递归校验父${table.classComment},如果父${table.classComment}是自己的子${table.classComment},则报错,避免形成环路 - if (id == null) { // id 为空,说明新增,不需要考虑环路 - return; - } - for (int i = 0; i < Short.MAX_VALUE; i++) { - // 3.1 校验环路 - ${treeParentColumn.javaField} = parent${simpleClassName}.get${TreeParentJavaField}(); - if (Objects.equals(id, ${treeParentColumn.javaField})) { - throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_IS_CHILD); - } - // 3.2 继续递归下一级父${table.classComment} - if (${treeParentColumn.javaField} == null || ${simpleClassName}DO.${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT.equals(${treeParentColumn.javaField})) { - break; - } - parent${simpleClassName} = ${classNameVar}Mapper.selectById(${treeParentColumn.javaField}); - if (parent${simpleClassName} == null) { - break; - } - } - } - - private void validate${simpleClassName}${TreeNameJavaField}Unique(Long id, Long ${treeParentColumn.javaField}, String ${treeNameColumn.javaField}) { - ${simpleClassName}DO ${classNameVar} = ${classNameVar}Mapper.selectBy${TreeParentJavaField}And${TreeNameJavaField}(${treeParentColumn.javaField}, ${treeNameColumn.javaField}); - if (${classNameVar} == null) { - return; - } - // 如果 id 为空,说明不用比较是否为相同 id 的${table.classComment} - if (id == null) { - throw exception(${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE); - } - if (!Objects.equals(${classNameVar}.getId(), id)) { - throw exception(${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE); - } - } - -#end - @Override - public ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id) { - return ${classNameVar}Mapper.selectById(id); - } - -## 特殊:树表专属逻辑(树不需要分页接口) -#if ( $table.templateType != 2 ) - @Override - public PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) { - return ${classNameVar}Mapper.selectPage(pageReqVO); - } -#else - @Override - public List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO) { - return ${classNameVar}Mapper.selectList(listReqVO); - } -#end - -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) -#set ($subSimpleClassName = $subSimpleClassNames.get($index)) -#set ($simpleClassNameUnderlineCase = $simpleClassNameUnderlineCases.get($index)) -#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 -#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 -#set ($subClassNameVar = $subClassNameVars.get($index)) - // ==================== 子表($subTable.classComment) ==================== - -## 情况一:MASTER_ERP 时,需要分查询页子表 -#if ( $table.templateType == 11 ) - @Override - public PageResult<${subTable.className}DO> get${subSimpleClassName}Page(PageParam pageReqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return ${subClassNameVars.get($index)}Mapper.selectPage(pageReqVO, ${subJoinColumn.javaField}); - } - -## 情况二:非 MASTER_ERP 时,需要列表查询子表 -#else - #if ( $subTable.subJoinMany ) - @Override - public List<${subTable.className}DO> get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return ${subClassNameVars.get($index)}Mapper.selectListBy${SubJoinColumnName}(${subJoinColumn.javaField}); - } - - #else - @Override - public ${subTable.className}DO get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { - return ${subClassNameVars.get($index)}Mapper.selectBy${SubJoinColumnName}(${subJoinColumn.javaField}); - } - - #end -#end -## 情况一:MASTER_ERP 时,支持单个的新增、修改、删除操作 -#if ( $table.templateType == 11 ) - @Override - public ${subPrimaryColumn.javaType} create${subSimpleClassName}(${subTable.className}DO ${subClassNameVar}) { -## 特殊:一对一时,需要保证只有一条,不能重复插入 -#if ( !$subTable.subJoinMany) - // 校验是否已经存在 - if (${subClassNameVars.get($index)}Mapper.selectBy${SubJoinColumnName}(${subClassNameVar}.get${SubJoinColumnName}()) != null) { - throw exception(${simpleClassNameUnderlineCase.toUpperCase()}_EXISTS); - } - // 插入 -#end - ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar}); - return ${subClassNameVar}.getId(); - } - - @Override - public void update${subSimpleClassName}(${subTable.className}DO ${subClassNameVar}) { - // 校验存在 - validate${subSimpleClassName}Exists(${subClassNameVar}.getId()); - // 更新 - ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新 - ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar}); - } - - @Override - public void delete${subSimpleClassName}(${subPrimaryColumn.javaType} id) { - // 校验存在 - validate${subSimpleClassName}Exists(id); - // 删除 - ${subClassNameVars.get($index)}Mapper.deleteById(id); - } - - @Override - public ${subTable.className}DO get${subSimpleClassName}(${subPrimaryColumn.javaType} id) { - return ${subClassNameVars.get($index)}Mapper.selectById(id); - } - - private void validate${subSimpleClassName}Exists(${subPrimaryColumn.javaType} id) { - if (${subClassNameVar}Mapper.selectById(id) == null) { - throw exception(${simpleClassNameUnderlineCase.toUpperCase()}_NOT_EXISTS); - } - } - -## 情况二:非 MASTER_ERP 时,支持批量的新增、修改操作 -#else - #if ( $subTable.subJoinMany) - private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) { - list.forEach(o -> o.set$SubJoinColumnName(${subJoinColumn.javaField})); - ${subClassNameVars.get($index)}Mapper.insertBatch(list); - } - - private void update${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) { - delete${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}); - list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新 - create${subSimpleClassName}List(${subJoinColumn.javaField}, list); - } - - #else - private void create${subSimpleClassName}(${primaryColumn.javaType} ${subJoinColumn.javaField}, ${subTable.className}DO ${subClassNameVar}) { - if (${subClassNameVar} == null) { - return; - } - ${subClassNameVar}.set$SubJoinColumnName(${subJoinColumn.javaField}); - ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar}); - } - - private void update${subSimpleClassName}(${primaryColumn.javaType} ${subJoinColumn.javaField}, ${subTable.className}DO ${subClassNameVar}) { - if (${subClassNameVar} == null) { - return; - } - ${subClassNameVar}.set$SubJoinColumnName(${subJoinColumn.javaField}); - ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新 - ${subClassNameVars.get($index)}Mapper.insertOrUpdate(${subClassNameVar}); - } - - #end -#end - private void delete${subSimpleClassName}By${SubJoinColumnName}(${primaryColumn.javaType} ${subJoinColumn.javaField}) { - ${subClassNameVars.get($index)}Mapper.deleteBy${SubJoinColumnName}(${subJoinColumn.javaField}); - } - -#end -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/test/serviceTest.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/java/test/serviceTest.vm deleted file mode 100644 index bfd4600..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/java/test/serviceTest.vm +++ /dev/null @@ -1,168 +0,0 @@ -package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; - -import ${jakartaPackage}.annotation.Resource; - -import ${baseFrameworkPackage}.test.core.ut.BaseDbUnitTest; - -import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; -import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; -import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper; -import ${PageResultClassName}; - -import ${jakartaPackage}.annotation.Resource; -import org.springframework.context.annotation.Import; -import java.util.*; -import java.time.LocalDateTime; - -import static cn.hutool.core.util.RandomUtil.*; -import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*; -import static ${baseFrameworkPackage}.test.core.util.AssertUtils.*; -import static ${baseFrameworkPackage}.test.core.util.RandomUtils.*; -import static ${LocalDateTimeUtilsClassName}.*; -import static ${ObjectUtilsClassName}.*; -import static ${DateUtilsClassName}.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -## 字段模板 -#macro(getPageCondition $VO) - // mock 数据 - ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class, o -> { // 等会查询到 - #foreach ($column in $columns) - #if (${column.listOperation}) - #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 - o.set$JavaField(null); - #end - #end - }); - ${classNameVar}Mapper.insert(db${simpleClassName}); - #foreach ($column in $columns) - #if (${column.listOperation}) - #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 - // 测试 ${column.javaField} 不匹配 - ${classNameVar}Mapper.insert(cloneIgnoreId(db${simpleClassName}, o -> o.set$JavaField(null))); - #end - #end - // 准备参数 - ${sceneEnum.prefixClass}${table.className}${VO} reqVO = new ${sceneEnum.prefixClass}${table.className}${VO}(); - #foreach ($column in $columns) - #if (${column.listOperation}) - #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 - #if (${column.listOperationCondition} == "BETWEEN")## BETWEEN 的情况 - reqVO.set${JavaField}(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); - #else - reqVO.set$JavaField(null); - #end - #end - #end -#end -/** - * {@link ${table.className}ServiceImpl} 的单元测试类 - * - * @author ${table.author} - */ -@Import(${table.className}ServiceImpl.class) -public class ${table.className}ServiceImplTest extends BaseDbUnitTest { - - @Resource - private ${table.className}ServiceImpl ${classNameVar}Service; - - @Resource - private ${table.className}Mapper ${classNameVar}Mapper; - - @Test - public void testCreate${simpleClassName}_success() { - // 准备参数 - ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class).setId(null); - - // 调用 - ${primaryColumn.javaType} ${classNameVar}Id = ${classNameVar}Service.create${simpleClassName}(createReqVO); - // 断言 - assertNotNull(${classNameVar}Id); - // 校验记录的属性是否正确 - ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(${classNameVar}Id); - assertPojoEquals(createReqVO, ${classNameVar}, "id"); - } - - @Test - public void testUpdate${simpleClassName}_success() { - // mock 数据 - ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class); - ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据 - // 准备参数 - ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class, o -> { - o.setId(db${simpleClassName}.getId()); // 设置更新的 ID - }); - - // 调用 - ${classNameVar}Service.update${simpleClassName}(updateReqVO); - // 校验是否更新正确 - ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(updateReqVO.getId()); // 获取最新的 - assertPojoEquals(updateReqVO, ${classNameVar}); - } - - @Test - public void testUpdate${simpleClassName}_notExists() { - // 准备参数 - ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class); - - // 调用, 并断言异常 - assertServiceException(() -> ${classNameVar}Service.update${simpleClassName}(updateReqVO), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); - } - - @Test - public void testDelete${simpleClassName}_success() { - // mock 数据 - ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class); - ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据 - // 准备参数 - ${primaryColumn.javaType} id = db${simpleClassName}.getId(); - - // 调用 - ${classNameVar}Service.delete${simpleClassName}(id); - // 校验数据不存在了 - assertNull(${classNameVar}Mapper.selectById(id)); - } - - @Test - public void testDelete${simpleClassName}_notExists() { - // 准备参数 - ${primaryColumn.javaType} id = random${primaryColumn.javaType}Id(); - - // 调用, 并断言异常 - assertServiceException(() -> ${classNameVar}Service.delete${simpleClassName}(id), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); - } - -## 特殊:树表专属逻辑(树不需要分页接口) -#if ( $table.templateType != 2 ) - @Test - @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 - public void testGet${simpleClassName}Page() { - #getPageCondition("PageReqVO") - - // 调用 - PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(reqVO); - // 断言 - assertEquals(1, pageResult.getTotal()); - assertEquals(1, pageResult.getList().size()); - assertPojoEquals(db${simpleClassName}, pageResult.getList().get(0)); - } -#else - @Test - @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 - public void testGet${simpleClassName}List() { - #getPageCondition("ListReqVO") - - // 调用 - List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(reqVO); - // 断言 - assertEquals(1, list.size()); - assertPojoEquals(db${simpleClassName}, list.get(0)); - } -#end - -} \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/sql/h2.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/sql/h2.vm deleted file mode 100644 index a073fdb..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/sql/h2.vm +++ /dev/null @@ -1,37 +0,0 @@ --- 将该建表 SQL 语句,添加到 yudao-module-${table.moduleName}-biz 模块的 test/resources/sql/create_tables.sql 文件里 -CREATE TABLE IF NOT EXISTS "${table.tableName.toLowerCase()}" ( -#foreach ($column in $columns) -#if (${column.javaType} == 'Long') - #set ($dataType='bigint') -#elseif (${column.javaType} == 'Integer') - #set ($dataType='int') -#elseif (${column.javaType} == 'Boolean') - #set ($dataType='bit') -#elseif (${column.javaType} == 'Date') - #set ($dataType='datetime') -#else - #set ($dataType='varchar') -#end - #if (${column.primaryKey})##处理主键 - "${column.javaField}"#if (${column.javaType} == 'String') ${dataType} NOT NULL#else ${dataType} NOT NULL GENERATED BY DEFAULT AS IDENTITY#end, - #else - #if (${column.columnName} == 'create_time') - "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - #elseif (${column.columnName} == 'update_time') - "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - #elseif (${column.columnName} == 'creator' || ${column.columnName} == 'updater') - "${column.columnName}" ${dataType} DEFAULT '', - #elseif (${column.columnName} == 'deleted') - "deleted" bit NOT NULL DEFAULT FALSE, - #elseif (${column.columnName} == 'tenant_id') - "tenant_id" bigint NOT NULL DEFAULT 0, - #else - "${column.columnName.toLowerCase()}" ${dataType}#if (${column.nullable} == false) NOT NULL#end, - #end - #end -#end - PRIMARY KEY ("${primaryColumn.columnName.toLowerCase()}") -) COMMENT '${table.tableComment}'; - --- 将该删表 SQL 语句,添加到 yudao-module-${table.moduleName}-biz 模块的 test/resources/sql/clean.sql 文件里 -DELETE FROM "${table.tableName}"; \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/sql/sql.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/sql/sql.vm deleted file mode 100644 index 41b107d..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/sql/sql.vm +++ /dev/null @@ -1,28 +0,0 @@ --- 菜单 SQL -INSERT INTO system_menu( - name, permission, type, sort, parent_id, - path, icon, component, status, component_name -) -VALUES ( - '${table.classComment}管理', '', 2, 0, ${table.parentMenuId}, - '${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}' -); - --- 按钮父菜单ID --- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码 -SELECT @parentId := LAST_INSERT_ID(); - --- 按钮 SQL -#set ($functionNames = ['查询', '创建', '更新', '删除', '导出']) -#set ($functionOps = ['query', 'create', 'update', 'delete', 'export']) -#foreach ($functionName in $functionNames) -#set ($index = $foreach.count - 1) -INSERT INTO system_menu( - name, permission, type, sort, parent_id, - path, icon, component, status -) -VALUES ( - '${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId, - '', '', '', 0 -); -#end \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/api/api.js.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/api/api.js.vm deleted file mode 100644 index 835c019..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/api/api.js.vm +++ /dev/null @@ -1,141 +0,0 @@ -import request from '@/utils/request' -#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") - -// 创建${table.classComment} -export function create${simpleClassName}(data) { - return request({ - url: '${baseURL}/create', - method: 'post', - data: data - }) -} - -// 更新${table.classComment} -export function update${simpleClassName}(data) { - return request({ - url: '${baseURL}/update', - method: 'put', - data: data - }) -} - -// 删除${table.classComment} -export function delete${simpleClassName}(id) { - return request({ - url: '${baseURL}/delete?id=' + id, - method: 'delete' - }) -} - -// 获得${table.classComment} -export function get${simpleClassName}(id) { - return request({ - url: '${baseURL}/get?id=' + id, - method: 'get' - }) -} - -#if ( $table.templateType != 2 ) -// 获得${table.classComment}分页 -export function get${simpleClassName}Page(params) { - return request({ - url: '${baseURL}/page', - method: 'get', - params - }) -} -#else -// 获得${table.classComment}列表 -export function get${simpleClassName}List(params) { - return request({ - url: '${baseURL}/list', - method: 'get', - params - }) -} -#end -// 导出${table.classComment} Excel -export function export${simpleClassName}Excel(params) { - return request({ - url: '${baseURL}/export-excel', - method: 'get', - params, - responseType: 'blob' - }) -} -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) - #set ($index = $foreach.count - 1) - #set ($subSimpleClassName = $subSimpleClassNames.get($index)) - #set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 - #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 - #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 - #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) - #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) - #set ($subClassNameVar = $subClassNameVars.get($index)) - -// ==================== 子表($subTable.classComment) ==================== - ## 情况一:MASTER_ERP 时,需要分查询页子表 - #if ($table.templateType == 11) - // 获得${subTable.classComment}分页 - export function get${subSimpleClassName}Page(params) { - return request({ - url: '${baseURL}/${subSimpleClassName_strikeCase}/page', - method: 'get', - params - }) - } - ## 情况二:非 MASTER_ERP 时,需要列表查询子表 - #else - #if ($subTable.subJoinMany) - // 获得${subTable.classComment}列表 - export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}) { - return request({ - url: '${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=' + ${subJoinColumn.javaField}, - method: 'get' - }) - } - #else - // 获得${subTable.classComment} - export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}) { - return request({ - url: '${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=' + ${subJoinColumn.javaField}, - method: 'get' - }) - } - #end - #end - ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 - #if ($table.templateType == 11) - // 新增${subTable.classComment} - export function create${subSimpleClassName}(data) { - return request({ - url: '${baseURL}/${subSimpleClassName_strikeCase}/create', - method: 'post', - data - }) - } - // 修改${subTable.classComment} - export function update${subSimpleClassName}(data) { - return request({ - url: '${baseURL}/${subSimpleClassName_strikeCase}/update', - method: 'post', - data - }) - } - // 删除${subTable.classComment} - export function delete${subSimpleClassName}(id) { - return request({ - url: '${baseURL}/${subSimpleClassName_strikeCase}/delete?id=' + id, - method: 'delete' - }) - } - // 获得${subTable.classComment} - export function get${subSimpleClassName}(id) { - return request({ - url: '${baseURL}/${subSimpleClassName_strikeCase}/get?id=' + id, - method: 'get' - }) - } - #end -#end \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm deleted file mode 100644 index 99aa91a..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm +++ /dev/null @@ -1,205 +0,0 @@ -#set ($subTable = $subTables.get($subIndex))##当前表 -#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 -#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 - - - diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm deleted file mode 100644 index ca266be..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm +++ /dev/null @@ -1,2 +0,0 @@ -## 主表的 normal 和 inner 使用相同的 form 表单 -#parse("codegen/vue/views/components/form_sub_normal.vue.vm") \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm deleted file mode 100644 index 48a404a..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm +++ /dev/null @@ -1,347 +0,0 @@ -#set ($subTable = $subTables.get($subIndex))##当前表 -#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 -#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 - - - diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm deleted file mode 100644 index 589736b..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm +++ /dev/null @@ -1,165 +0,0 @@ -#set ($subTable = $subTables.get($subIndex))##当前表 -#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 -#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 - - - diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm deleted file mode 100644 index 90b8e41..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm +++ /dev/null @@ -1,4 +0,0 @@ -## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点: -## 1)inner 使用 list 不分页,erp 使用 page 分页 -## 2)erp 支持单个子表的新增、修改、删除,inner 不支持 -#parse("codegen/vue/views/components/list_sub_erp.vue.vm") \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/form.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/form.vue.vm deleted file mode 100644 index 634d05d..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/form.vue.vm +++ /dev/null @@ -1,320 +0,0 @@ - - - diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/index.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/index.vue.vm deleted file mode 100644 index 9c1e124..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue/views/index.vue.vm +++ /dev/null @@ -1,340 +0,0 @@ - - - diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm deleted file mode 100644 index c3044fb..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm +++ /dev/null @@ -1,115 +0,0 @@ -import request from '@/config/axios' -#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") - -// ${table.classComment} VO -export interface ${simpleClassName}VO { -#foreach ($column in $columns) -#if ($column.createOperation || $column.updateOperation) -#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") - ${column.javaField}: number // ${column.columnComment} -#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime") - ${column.javaField}: Date // ${column.columnComment} -#else - ${column.javaField}: ${column.javaType.toLowerCase()} // ${column.columnComment} -#end -#end -#end -} - -// ${table.classComment} API -export const ${simpleClassName}Api = { -#if ( $table.templateType != 2 ) - // 查询${table.classComment}分页 - get${simpleClassName}Page: async (params: any) => { - return await request.get({ url: `${baseURL}/page`, params }) - }, -#else - // 查询${table.classComment}列表 - get${simpleClassName}List: async (params) => { - return await request.get({ url: `${baseURL}/list`, params }) - }, -#end - - // 查询${table.classComment}详情 - get${simpleClassName}: async (id: number) => { - return await request.get({ url: `${baseURL}/get?id=` + id }) - }, - - // 新增${table.classComment} - create${simpleClassName}: async (data: ${simpleClassName}VO) => { - return await request.post({ url: `${baseURL}/create`, data }) - }, - - // 修改${table.classComment} - update${simpleClassName}: async (data: ${simpleClassName}VO) => { - return await request.put({ url: `${baseURL}/update`, data }) - }, - - // 删除${table.classComment} - delete${simpleClassName}: async (id: number) => { - return await request.delete({ url: `${baseURL}/delete?id=` + id }) - }, - - // 导出${table.classComment} Excel - export${simpleClassName}: async (params) => { - return await request.download({ url: `${baseURL}/export-excel`, params }) - }, -## 特殊:主子表专属逻辑 -#foreach ($subTable in $subTables) -#set ($index = $foreach.count - 1) -#set ($subSimpleClassName = $subSimpleClassNames.get($index)) -#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 -#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 -#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) -#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) -#set ($subClassNameVar = $subClassNameVars.get($index)) - -// ==================== 子表($subTable.classComment) ==================== -## 情况一:MASTER_ERP 时,需要分查询页子表 -#if ( $table.templateType == 11 ) - - // 获得${subTable.classComment}分页 - get${subSimpleClassName}Page: async (params) => { - return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/page`, params }) - }, -## 情况二:非 MASTER_ERP 时,需要列表查询子表 -#else - #if ( $subTable.subJoinMany ) - - // 获得${subTable.classComment}列表 - get${subSimpleClassName}ListBy${SubJoinColumnName}: async (${subJoinColumn.javaField}) => { - return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} }) - }, - #else - - // 获得${subTable.classComment} - get${subSimpleClassName}By${SubJoinColumnName}: async (${subJoinColumn.javaField}) => { - return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} }) - }, - #end -#end -## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 -#if ( $table.templateType == 11 ) - // 新增${subTable.classComment} - create${subSimpleClassName}: async (data) => { - return await request.post({ url: `${baseURL}/${subSimpleClassName_strikeCase}/create`, data }) - }, - - // 修改${subTable.classComment} - update${subSimpleClassName}: async (data) => { - return await request.put({ url: `${baseURL}/${subSimpleClassName_strikeCase}/update`, data }) - }, - - // 删除${subTable.classComment} - delete${subSimpleClassName}: async (id: number) => { - return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id }) - }, - - // 获得${subTable.classComment} - get${subSimpleClassName}: async (id: number) => { - return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id }) - }, -#end -#end -} diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm deleted file mode 100644 index 81cd977..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm +++ /dev/null @@ -1,204 +0,0 @@ -#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 -#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 - - \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_inner.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_inner.vue.vm deleted file mode 100644 index d8542c3..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_inner.vue.vm +++ /dev/null @@ -1,2 +0,0 @@ -## 主表的 normal 和 inner 使用相同的 form 表单 -#parse("codegen/vue3/views/components/form_sub_normal.vue.vm") \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm deleted file mode 100644 index 3fa1eff..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm +++ /dev/null @@ -1,360 +0,0 @@ -#set ($subTable = $subTables.get($subIndex))##当前表 -#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 -#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 - - \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm deleted file mode 100644 index 3f0710e..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm +++ /dev/null @@ -1,184 +0,0 @@ -#set ($subTable = $subTables.get($subIndex))##当前表 -#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 -#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) -#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 -#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 - - \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_inner.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_inner.vue.vm deleted file mode 100644 index 3fe6488..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_inner.vue.vm +++ /dev/null @@ -1,4 +0,0 @@ -## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点: -## 1)inner 使用 list 不分页,erp 使用 page 分页 -## 2)erp 支持单个子表的新增、修改、删除,inner 不支持 -#parse("codegen/vue3/views/components/list_sub_erp.vue.vm") \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/form.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/form.vue.vm deleted file mode 100644 index e37474b..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/form.vue.vm +++ /dev/null @@ -1,300 +0,0 @@ - - \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm deleted file mode 100644 index 399b58e..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm +++ /dev/null @@ -1,374 +0,0 @@ - - - \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/api/api.ts.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/api/api.ts.vm deleted file mode 100644 index b7f2651..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/api/api.ts.vm +++ /dev/null @@ -1,32 +0,0 @@ -import { defHttp } from '@/utils/http/axios' -#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") - -// 查询${table.classComment}列表 -export function get${simpleClassName}Page(params) { - return defHttp.get({ url: '${baseURL}/page', params }) -} - -// 查询${table.classComment}详情 -export function get${simpleClassName}(id: number) { - return defHttp.get({ url: `${baseURL}/get?id=${id}` }) -} - -// 新增${table.classComment} -export function create${simpleClassName}(data) { - return defHttp.post({ url: '${baseURL}/create', data }) -} - -// 修改${table.classComment} -export function update${simpleClassName}(data) { - return defHttp.put({ url: '${baseURL}/update', data }) -} - -// 删除${table.classComment} -export function delete${simpleClassName}(id: number) { - return defHttp.delete({ url: `${baseURL}/delete?id=${id}` }) -} - -// 导出${table.classComment} Excel -export function export${simpleClassName}(params) { - return defHttp.download({ url: '${baseURL}/export-excel', params }, '${table.classComment}.xls') -} diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/views/data.ts.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/views/data.ts.vm deleted file mode 100644 index 56f4e82..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/views/data.ts.vm +++ /dev/null @@ -1,260 +0,0 @@ -import type {BasicColumn, FormSchema} from '@/components/Table' -import {useRender} from '@/components/Table' -import {DICT_TYPE, getDictOptions} from '@/utils/dict' - -export const columns: BasicColumn[] = [ -#foreach($column in $columns) -#if ($column.listOperationResult) - #set ($dictType=$column.dictType) - #set ($javaField = $column.javaField) - #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) - #set ($comment=$column.columnComment) -#if ($column.javaType == "LocalDateTime")## 时间类型 - { - title: '${comment}', - dataIndex: '${javaField}', - width: 180, - customRender: ({ text }) => { - return useRender.renderDate(text) - }, - }, -#elseif("" != $column.dictType)## 数据字典 - { - title: '${comment}', - dataIndex: '${javaField}', - width: 180, - customRender: ({ text }) => { - return useRender.renderDict(text, DICT_TYPE.$dictType.toUpperCase()) - }, - }, -#else - { - title: '${comment}', - dataIndex: '${javaField}', - width: 160, - }, -#end -#end -#end -] - -export const searchFormSchema: FormSchema[] = [ -#foreach($column in $columns) -#if ($column.listOperation) - #set ($dictType=$column.dictType) - #set ($javaType = $column.javaType) - #set ($javaField = $column.javaField) - #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) - #set ($comment=$column.columnComment) - #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") - #set ($dictMethod = "number") - #elseif ($javaType == "String") - #set ($dictMethod = "string") - #elseif ($javaType == "Boolean") - #set ($dictMethod = "boolean") - #end - { - label: '${comment}', - field: '${javaField}', - #if ($column.htmlType == "input") - component: 'Input', - #elseif ($column.htmlType == "select") - component: 'Select', - componentProps: { - #if ("" != $dictType)## 设置了 dictType 数据字典的情况 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), - #else## 未设置 dictType 数据字典的情况 - options: [], - #end - }, - #elseif ($column.htmlType == "radio") - component: 'RadioButtonGroup', - componentProps: { - #if ("" != $dictType)## 设置了 dictType 数据字典的情况 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), - #else## 未设置 dictType 数据字典的情况 - options: [], - #end - }, - #elseif($column.htmlType == "datetime") - component: 'RangePicker', - #end - colProps: { span: 8 }, - }, -#end -#end -] - -export const createFormSchema: FormSchema[] = [ - { - label: '编号', - field: 'id', - show: false, - component: 'Input', - }, -#foreach($column in $columns) -#if ($column.createOperation) - #set ($dictType = $column.dictType) - #set ($javaType = $column.javaType) - #set ($javaField = $column.javaField) - #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) - #set ($comment = $column.columnComment) - #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") - #set ($dictMethod = "number") - #elseif ($javaType == "String") - #set ($dictMethod = "string") - #elseif ($javaType == "Boolean") - #set ($dictMethod = "boolean") - #end -#if (!$column.primaryKey)## 忽略主键,不用在表单里 - { - label: '${comment}', - field: '${javaField}', - #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 - required: true, - #end - #if ($column.htmlType == "input") - component: 'Input', - #elseif($column.htmlType == "imageUpload")## 图片上传 - component: 'FileUpload', - componentProps: { - fileType: 'image', - maxCount: 1, - }, - #elseif($column.htmlType == "fileUpload")## 文件上传 - component: 'FileUpload', - componentProps: { - fileType: 'file', - maxCount: 1, - }, - #elseif($column.htmlType == "editor")## 文本编辑器 - component: 'Editor', - #elseif($column.htmlType == "select")## 下拉框 - component: 'Select', - componentProps: { - #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), - #else##没数据字典 - options:[], - #end - }, - #elseif($column.htmlType == "checkbox")## 多选框 - component: 'Checkbox', - componentProps: { - #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), - #else##没数据字典 - options:[], - #end - }, - #elseif($column.htmlType == "radio")## 单选框 - component: 'RadioButtonGroup', - componentProps: { - #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), - #else##没数据字典 - options:[], - #end - }, - #elseif($column.htmlType == "datetime")## 时间框 - component: 'DatePicker', - componentProps: { - showTime: true, - format: 'YYYY-MM-DD HH:mm:ss', - valueFormat: 'x', - }, - #elseif($column.htmlType == "textarea")## 文本域 - component: 'InputTextArea', - #end - }, -#end -#end -#end -] - -export const updateFormSchema: FormSchema[] = [ - { - label: '编号', - field: 'id', - show: false, - component: 'Input', - }, -#foreach($column in $columns) -#if ($column.updateOperation) -#set ($dictType = $column.dictType) -#set ($javaType = $column.javaType) -#set ($javaField = $column.javaField) -#set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) -#set ($comment = $column.columnComment) -#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") - #set ($dictMethod = "number") -#elseif ($javaType == "String") - #set ($dictMethod = "string") -#elseif ($javaType == "Boolean") - #set ($dictMethod = "boolean") -#end - #if (!$column.primaryKey)## 忽略主键,不用在表单里 - { - label: '${comment}', - field: '${javaField}', - #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 - required: true, - #end - #if ($column.htmlType == "input") - component: 'Input', - #elseif($column.htmlType == "imageUpload")## 图片上传 - component: 'FileUpload', - componentProps: { - fileType: 'image', - maxCount: 1, - }, - #elseif($column.htmlType == "fileUpload")## 文件上传 - component: 'FileUpload', - componentProps: { - fileType: 'file', - maxCount: 1, - }, - #elseif($column.htmlType == "editor")## 文本编辑器 - component: 'Editor', - #elseif($column.htmlType == "select")## 下拉框 - component: 'Select', - componentProps: { - #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), - #else##没数据字典 - options:[], - #end - }, - #elseif($column.htmlType == "checkbox")## 多选框 - component: 'Checkbox', - componentProps: { - #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), - #else##没数据字典 - options:[], - #end - }, - #elseif($column.htmlType == "radio")## 单选框 - component: 'RadioButtonGroup', - componentProps: { - #if ("" != $dictType)## 有数据字典 - options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), - #else##没数据字典 - options:[], - #end - }, - #elseif($column.htmlType == "datetime")## 时间框 - component: 'DatePicker', - componentProps: { - showTime: true, - format: 'YYYY-MM-DD HH:mm:ss', - valueFormat: 'x', - }, - #elseif($column.htmlType == "textarea")## 文本域 - component: 'InputTextArea', - #end - }, - #end -#end -#end -] \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/views/form.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/views/form.vue.vm deleted file mode 100644 index 1804365..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/views/form.vue.vm +++ /dev/null @@ -1,58 +0,0 @@ - - \ No newline at end of file diff --git a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/views/index.vue.vm b/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/views/index.vue.vm deleted file mode 100644 index 9e59b12..0000000 --- a/tashow-module/tashow-module-infra/src/main/resources/codegen/vue3_vben/views/index.vue.vm +++ /dev/null @@ -1,92 +0,0 @@ - - diff --git a/tashow-module/tashow-module-pay/Dockerfile b/tashow-module/tashow-module-pay/Dockerfile new file mode 100644 index 0000000..97ad4a0 --- /dev/null +++ b/tashow-module/tashow-module-pay/Dockerfile @@ -0,0 +1,20 @@ +## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性 +## 感谢复旦核博士的建议!灰子哥,牛皮! +FROM eclipse-temurin:21-jre + + +## 创建目录,并使用它作为工作目录 +RUN mkdir -p /yudao-module-pay-biz +WORKDIR /yudao-module-pay-biz +## 将后端项目的 Jar 文件,复制到镜像中 +COPY ./target/yudao-module-pay-biz.jar app.jar + +## 设置 TZ 时区 +## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖 +ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx512m" + +## 暴露后端项目的 48080 端口 +EXPOSE 48085 + +## 启动后端项目 +CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar diff --git a/tashow-module/tashow-module-pay/pom.xml b/tashow-module/tashow-module-pay/pom.xml new file mode 100644 index 0000000..4b7c884 --- /dev/null +++ b/tashow-module/tashow-module-pay/pom.xml @@ -0,0 +1,97 @@ + + + + com.tashow.cloud + tashow-module + ${revision} + + 4.0.0 + tashow-module-pay + jar + + ${project.artifactId} + + pay 模块,我们放支付业务,提供业务的支付能力。 + 例如说:商户、应用、支付、退款等等 + + + + + + com.tashow.cloud + tashow-framework-env + + + + + com.tashow.cloud + tashow-pay-api + + + + + com.tashow.cloud + tashow-framework-security + + + + + com.tashow.cloud + tashow-data-mybatis + + + + com.tashow.cloud + tashow-data-redis + + + + + com.tashow.cloud + tashow-framework-rpc + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + + com.tashow.cloud + tashow-data-excel + + + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + + diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/PayServerApplication.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/PayServerApplication.java new file mode 100644 index 0000000..fe1defd --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/PayServerApplication.java @@ -0,0 +1,30 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/order/PayOrderApiImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/order/PayOrderApiImpl.java new file mode 100644 index 0000000..2cb7d25 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/order/PayOrderApiImpl.java @@ -0,0 +1,40 @@ +package com.tashow.cloud.pay.api.order; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; +import com.tashow.cloud.pay.convert.order.PayOrderConvert; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.service.order.PayOrderService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.annotation.Resource; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@RestController // 提供 RESTful API 接口,给 Feign 调用 +@Validated +public class PayOrderApiImpl implements PayOrderApi { + + @Resource + private PayOrderService payOrderService; + + @Override + public CommonResult createOrder(PayOrderCreateReqDTO reqDTO) { + return success(payOrderService.createOrder(reqDTO)); + } + + @Override + public CommonResult getOrder(Long id) { + PayOrderDO order = payOrderService.getOrder(id); + return success(PayOrderConvert.INSTANCE.convert2(order)); + } + + @Override + public CommonResult updatePayOrderPrice(Long id, Integer payPrice) { + payOrderService.updatePayOrderPrice(id, payPrice); + return success(true); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/refund/PayRefundApiImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/refund/PayRefundApiImpl.java new file mode 100644 index 0000000..4a890bc --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/refund/PayRefundApiImpl.java @@ -0,0 +1,32 @@ +package com.tashow.cloud.pay.api.refund; + +import com.tashow.cloud.common.pojo.CommonResult; +import com.tashow.cloud.pay.convert.refund.PayRefundConvert; +import com.tashow.cloud.pay.service.refund.PayRefundService; +import com.tashow.cloud.payapi.api.refund.PayRefundApi; +import com.tashow.cloud.sdk.payment.dto.refund.PayRefundRespDTO; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import static com.tashow.cloud.common.pojo.CommonResult.success; + + +@RestController // 提供 RESTful API 接口,给 Feign 调用 +@Validated +public class PayRefundApiImpl implements PayRefundApi { + + @Resource + private PayRefundService payRefundService; + + @Override + public CommonResult createRefund(PayRefundCreateReqDTO reqDTO) { + return success(payRefundService.createPayRefund(reqDTO)); + } + + @Override + public CommonResult getRefund(Long id) { + return success(PayRefundConvert.INSTANCE.convert02(payRefundService.getRefund(id))); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/transfer/PayTransferApiImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/transfer/PayTransferApiImpl.java new file mode 100644 index 0000000..019e1fb --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/transfer/PayTransferApiImpl.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.pay.api.transfer; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO; +import com.tashow.cloud.pay.dal.dataobject.transfer.PayTransferDO; +import com.tashow.cloud.pay.service.transfer.PayTransferService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.annotation.Resource; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * 转账单 API 实现类 + * + * @author jason + */ +@RestController // 提供 RESTful API 接口,给 Feign 调用 +@Validated +public class PayTransferApiImpl implements PayTransferApi { + + @Resource + private PayTransferService payTransferService; + + @Override + public CommonResult createTransfer(PayTransferCreateReqDTO reqDTO) { + return success(payTransferService.createTransfer(reqDTO)); + } + + @Override + public CommonResult getTransfer(Long id) { + PayTransferDO transfer = payTransferService.getTransfer(id); + return success(BeanUtils.toBean(transfer, PayTransferRespDTO.class)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/wallet/PayWalletApiImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/wallet/PayWalletApiImpl.java new file mode 100644 index 0000000..959ccce --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/api/wallet/PayWalletApiImpl.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.pay.api.wallet; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletDO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import com.tashow.cloud.pay.service.wallet.PayWalletService; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; + +/** + * 钱包 API 实现类 + * + * @author jason + */ +@RestController // 提供 RESTful API 接口,给 Feign 调用 +@Validated +public class PayWalletApiImpl implements PayWalletApi { + + @Resource + private PayWalletService payWalletService; + + @Override + public CommonResult addWalletBalance(PayWalletAddBalanceReqDTO reqDTO) { + // 创建或获取钱包 + PayWalletDO wallet = payWalletService.getOrCreateWallet(reqDTO.getUserId(), reqDTO.getUserType()); + Assert.notNull(wallet, "钱包({}/{})不存在", reqDTO.getUserId(), reqDTO.getUserType()); + + // 增加余额 + PayWalletBizTypeEnum bizType = PayWalletBizTypeEnum.valueOf(reqDTO.getBizType()); + payWalletService.addWalletBalance(wallet.getId(), reqDTO.getBizId(), bizType, reqDTO.getPrice()); + return success(true); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/PayAppController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/PayAppController.java new file mode 100644 index 0000000..31dd2ec --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/PayAppController.java @@ -0,0 +1,110 @@ +package com.tashow.cloud.pay.controller.admin.app; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pay.controller.admin.app.vo.*; +import com.tashow.cloud.pay.controller.admin.app.vo.*; +import com.tashow.cloud.payapi.controller.admin.app.vo.*; +import com.tashow.cloud.pay.convert.app.PayAppConvert; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.pay.service.app.PayAppService; +import com.tashow.cloud.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 jakarta.annotation.Resource; +import jakarta.validation.Valid; +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 java.util.List; + +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()); + } + + // 得到所有的应用编号,查出所有的渠道,并移除未启用的渠道 + List channels = channelService.getChannelListByAppIds( + convertList(pageResult.getList(), PayAppDO::getId)); + channels.removeIf(channel -> !CommonStatusEnum.ENABLE.getStatus().equals(channel.getStatus())); + + // 拼接后返回 + 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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppBaseVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppBaseVO.java new file mode 100644 index 0000000..75ed259 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppBaseVO.java @@ -0,0 +1,47 @@ +package com.tashow.cloud.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 jakarta.validation.constraints.*; + +/** + * 支付应用信息 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class PayAppBaseVO { + + @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + @NotEmpty(message = "应用标识不能为空") + private String appKey; + + @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; + + @Schema(description = "转账结果的回调地址", example = "http://127.0.0.1:48080/transfer-callback") + @URL(message = "转账结果的回调地址必须为 URL 格式") + private String transferNotifyUrl; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppCreateReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppCreateReqVO.java new file mode 100644 index 0000000..aa9e4f3 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppCreateReqVO.java @@ -0,0 +1,14 @@ +package com.tashow.cloud.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 支付应用信息创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppCreateReqVO extends PayAppBaseVO { + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppPageItemRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppPageItemRespVO.java new file mode 100644 index 0000000..ef684f5 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppPageItemRespVO.java @@ -0,0 +1,26 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppPageReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppPageReqVO.java new file mode 100644 index 0000000..fb4b066 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppPageReqVO.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.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 = "yudao") + private String appKey; + + @Schema(description = "开启状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppRespVO.java new file mode 100644 index 0000000..464386c --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppRespVO.java @@ -0,0 +1,25 @@ +package com.tashow.cloud.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; + +@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, example = "yudao") + private String appKey; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppUpdateReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppUpdateReqVO.java new file mode 100644 index 0000000..f82d469 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppUpdateReqVO.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java new file mode 100644 index 0000000..5c8ebcf --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/PayChannelController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/PayChannelController.java new file mode 100644 index 0000000..d7c9493 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/PayChannelController.java @@ -0,0 +1,82 @@ +package com.tashow.cloud.pay.controller.admin.channel; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelRespVO; +import com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import com.tashow.cloud.pay.convert.channel.PayChannelConvert; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.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 jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import 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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelBaseVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelBaseVO.java new file mode 100644 index 0000000..45aa07a --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelBaseVO.java @@ -0,0 +1,32 @@ +package com.tashow.cloud.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 jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** +* 支付渠道 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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java new file mode 100644 index 0000000..45b5a23 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java @@ -0,0 +1,24 @@ +package com.tashow.cloud.pay.controller.admin.channel.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelRespVO.java new file mode 100644 index 0000000..ca32181 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelRespVO.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.pay.controller.admin.channel.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 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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java new file mode 100644 index 0000000..88da502 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java @@ -0,0 +1,24 @@ +package com.tashow.cloud.pay.controller.admin.channel.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/PayDemoOrderController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/PayDemoOrderController.java new file mode 100644 index 0000000..e2b30a8 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/PayDemoOrderController.java @@ -0,0 +1,74 @@ +package com.tashow.cloud.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.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import com.tashow.cloud.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO; +import com.tashow.cloud.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO; +import com.tashow.cloud.pay.convert.demo.PayDemoOrderConvert; +import com.tashow.cloud.pay.dal.dataobject.demo.PayDemoOrderDO; +import com.tashow.cloud.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 jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.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 内部校验实现 + 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 内部校验实现 + public CommonResult updateDemoOrderRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { + payDemoOrderService.updateDemoOrderRefunded(Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getPayRefundId()); + return success(true); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/PayDemoTransferController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/PayDemoTransferController.java new file mode 100644 index 0000000..3711c7b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/PayDemoTransferController.java @@ -0,0 +1,51 @@ +package com.tashow.cloud.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.module.pay.api.notify.dto.PayTransferNotifyReqDTO; +import com.tashow.cloud.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO; +import com.tashow.cloud.pay.controller.admin.demo.vo.transfer.PayDemoTransferRespVO; +import com.tashow.cloud.pay.convert.demo.PayDemoTransferConvert; +import com.tashow.cloud.pay.dal.dataobject.demo.PayDemoTransferDO; +import com.tashow.cloud.pay.service.demo.PayDemoTransferService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 示例转账单") +@RestController +@RequestMapping("/pay/demo-transfer") +@Validated +public class PayDemoTransferController { + @Resource + private PayDemoTransferService demoTransferService; + + @PostMapping("/create") + @Operation(summary = "创建示例转账订单") + public CommonResult createDemoTransfer(@Valid @RequestBody PayDemoTransferCreateReqVO createReqVO) { + return success(demoTransferService.createDemoTransfer(createReqVO)); + } + + @GetMapping("/page") + @Operation(summary = "获得示例转账订单分页") + public CommonResult> getDemoTransferPage(@Valid PageParam pageVO) { + PageResult pageResult = demoTransferService.getDemoTransferPage(pageVO); + return success(PayDemoTransferConvert.INSTANCE.convertPage(pageResult)); + } + + @PostMapping("/update-status") + @Operation(summary = "更新示例转账订单的转账状态") // 由 pay-module 转账服务,进行回调 + @PermitAll // 无需登录,安全由 PayDemoTransferService 内部校验实现 + public CommonResult updateDemoTransferStatus(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) { + demoTransferService.updateDemoTransferStatus(Long.valueOf(notifyReqDTO.getMerchantTransferId()), + notifyReqDTO.getPayTransferId()); + return success(true); + } +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java new file mode 100644 index 0000000..8391d84 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java @@ -0,0 +1,15 @@ +package com.tashow.cloud.pay.controller.admin.demo.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 示例订单创建 Request VO") +@Data +public class PayDemoOrderCreateReqVO { + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17682") + @NotNull(message = "商品编号不能为空") + private Long spuId; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java new file mode 100644 index 0000000..24bd088 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java @@ -0,0 +1,54 @@ +package com.tashow.cloud.pay.controller.admin.demo.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/transfer/PayDemoTransferCreateReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/transfer/PayDemoTransferCreateReqVO.java new file mode 100644 index 0000000..e39908d --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/transfer/PayDemoTransferCreateReqVO.java @@ -0,0 +1,67 @@ +package com.tashow.cloud.pay.controller.admin.demo.vo.transfer; + +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Validator; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum.Alipay; +import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum.WxPay; + +/** + * @author jason + */ +@Schema(description = "管理后台 - 示例转账单创建 Request VO") +@Data +public class PayDemoTransferCreateReqVO { + + @Schema(description = "转账类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "转账类型不能为空") + @InEnum(PayTransferTypeEnum.class) + private Integer type; + + @Schema(description = "转账金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "转账金额不能为空") + @Min(value = 1, message = "转账金额必须大于零") + private Integer price; + + @Schema(description = "收款人姓名", example = "test1") + @NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class}) + private String userName; + + // ========== 支付宝转账相关字段 ========== + @Schema(description = "支付宝登录号,支持邮箱和手机号格式", example = "test1@@sandbox.com") + @NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class}) + private String alipayLogonId; + + // ========== 微信转账相关字段 ========== + @Schema(description = "微信 openId", example = "oLefc4g5Gxx") + @NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class}) + private String openid; + + + // ========== 转账到银行卡和钱包相关字段 待补充 ========== + + public void validate(Validator validator) { + PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(type); + switch (transferType) { + case ALIPAY_BALANCE: { + ValidationUtils.validate(validator, this, Alipay.class); + break; + } + case WX_BALANCE: { + ValidationUtils.validate(validator, this, WxPay.class); + break; + } + default: { + throw new UnsupportedOperationException("待实现"); + } + } + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/transfer/PayDemoTransferRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/transfer/PayDemoTransferRespVO.java new file mode 100644 index 0000000..d8876f4 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/demo/vo/transfer/PayDemoTransferRespVO.java @@ -0,0 +1,47 @@ +package com.tashow.cloud.pay.controller.admin.demo.vo.transfer; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 示例业务转账订单 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class PayDemoTransferRespVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long appId; + + @Schema(description = "转账金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "22338") + private Integer price; + + @Schema(description = "转账类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "收款人姓名", example = "test") + private String userName; + + @Schema(description = "支付宝登录号", example = "32167") + private String alipayLogonId; + + @Schema(description = "微信 openId", example = "31139") + private String openid; + + @Schema(description = "转账状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer transferStatus; + + @Schema(description = "转账订单编号", example = "23695") + private Long payTransferId; + + @Schema(description = "转账支付成功渠道") + private String payChannelCode; + + @Schema(description = "转账支付时间") + private LocalDateTime transferTime; +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/PayNotifyController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/PayNotifyController.java new file mode 100644 index 0000000..5335ec5 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/PayNotifyController.java @@ -0,0 +1,150 @@ +package com.tashow.cloud.pay.controller.admin.notify; + +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.pay.core.client.PayClient; +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.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import com.tashow.cloud.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO; +import com.tashow.cloud.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import com.tashow.cloud.pay.controller.admin.notify.vo.PayNotifyTaskRespVO; +import com.tashow.cloud.pay.convert.notify.PayNotifyTaskConvert; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.tashow.cloud.pay.dal.dataobject.notify.PayNotifyTaskDO; +import com.tashow.cloud.pay.service.app.PayAppService; +import com.tashow.cloud.pay.service.channel.PayChannelService; +import com.tashow.cloud.pay.service.notify.PayNotifyService; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.tashow.cloud.pay.service.refund.PayRefundService; +import com.tashow.cloud.pay.service.transfer.PayTransferService; +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 jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.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 PayTransferService payTransferService; + @Resource + private PayNotifyService notifyService; + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + + @PostMapping(value = "/order/{channelId}") + @Operation(summary = "支付渠道的统一【支付】回调") + @PermitAll + 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 = channelService.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyOrder][渠道编号({}) 找不到对应的支付客户端]", 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 + 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 = channelService.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyRefund][渠道编号({}) 找不到对应的支付客户端]", channelId); + throw exception(CHANNEL_NOT_FOUND); + } + + // 2. 解析通知数据 + PayRefundRespDTO notify = payClient.parseRefundNotify(params, body); + refundService.notifyRefund(channelId, notify); + return "success"; + } + + @PostMapping(value = "/transfer/{channelId}") + @Operation(summary = "支付渠道的统一【转账】回调") + @PermitAll + public String notifyTransfer(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body) { + log.info("[notifyTransfer][channelId({}) 回调数据({}/{})]", channelId, params, body); + // 1. 校验支付渠道是否存在 + PayClient payClient = channelService.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyTransfer][渠道编号({}) 找不到对应的支付客户端]", channelId); + throw exception(CHANNEL_NOT_FOUND); + } + + // 2. 解析通知数据 + PayTransferRespDTO notify = payClient.parseTransferNotify(params, body); + payTransferService.notifyTransfer(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); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + // 拼接返回 + Map appMap = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTaskDO::getAppId)); + return success(PayNotifyTaskConvert.INSTANCE.convertPage(pageResult, appMap)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskBaseVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskBaseVO.java new file mode 100644 index 0000000..c204898 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskBaseVO.java @@ -0,0 +1,45 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java new file mode 100644 index 0000000..b8467a1 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java @@ -0,0 +1,54 @@ + +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java new file mode 100644 index 0000000..bc275bc --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java new file mode 100644 index 0000000..3000767 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java @@ -0,0 +1,25 @@ +package com.tashow.cloud.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; + +@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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/PayOrderController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/PayOrderController.java new file mode 100644 index 0000000..9345f15 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/PayOrderController.java @@ -0,0 +1,143 @@ +package com.tashow.cloud.pay.controller.admin.order; + +import cn.hutool.core.collection.CollectionUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*; +import com.tashow.cloud.pay.controller.admin.order.vo.*; +import com.tashow.cloud.payapi.controller.admin.order.vo.*; +import com.tashow.cloud.pay.convert.order.PayOrderConvert; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderExtensionDO; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import com.tashow.cloud.pay.framework.pay.core.WalletPayClient; +import com.tashow.cloud.pay.service.app.PayAppService; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.google.common.collect.Maps; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +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.web.core.util.WebFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserType; + +@Tag(name = "管理后台 - 支付订单") +@RestController +@RequestMapping("/pay/order") +@Validated +public class PayOrderController { + + @Resource + private PayOrderService orderService; + @Resource + private PayAppService appService; + + @GetMapping("/get") + @Operation(summary = "获得支付订单") + @Parameters({ + @Parameter(name = "id", description = "编号", required = true, example = "1024"), + @Parameter(name = "sync", description = "是否同步", example = "true") + }) + @PreAuthorize("@ss.hasPermission('pay:order:query')") + public CommonResult getOrder(@RequestParam("id") Long id, + @RequestParam(value = "sync", required = false) Boolean sync) { + PayOrderDO order = orderService.getOrder(id); + // sync 仅在等待支付 + if (Boolean.TRUE.equals(sync) && PayOrderStatusEnum.isWaiting(order.getStatus())) { + orderService.syncOrderQuietly(order.getId()); + // 重新查询,因为同步后,可能会有变化 + order = orderService.getOrder(id); + } + return success(BeanUtils.toBean(order, PayOrderRespVO.class)); + } + + @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) { + // 1. 钱包支付事,需要额外传 user_id 和 user_type + if (Objects.equals(reqVO.getChannelCode(), PayChannelEnum.WALLET.getCode())) { + Map channelExtras = reqVO.getChannelExtras() == null ? + Maps.newHashMapWithExpectedSize(2) : reqVO.getChannelExtras(); + channelExtras.put(WalletPayClient.USER_ID_KEY, String.valueOf(getLoginUserId())); + channelExtras.put(WalletPayClient.USER_TYPE_KEY, String.valueOf(getLoginUserType())); + reqVO.setChannelExtras(channelExtras); + } + + // 2. 提交支付 + 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')") + @ApiAccessLog(operateType = 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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderBaseVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderBaseVO.java new file mode 100644 index 0000000..9279961 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderBaseVO.java @@ -0,0 +1,90 @@ +package com.tashow.cloud.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +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; + +/** + * 支付订单 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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java new file mode 100644 index 0000000..0cda935 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java @@ -0,0 +1,45 @@ +package com.tashow.cloud.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 = "支付订单编号", 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; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + /** + * 支付订单扩展 + */ + private PayOrderExtension extension; + + @Data + @Schema(description = "支付订单扩展") + public static class PayOrderExtension { + + @Schema(description = "支付订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String no; + + @Schema(description = "支付异步通知的内容") + private String channelNotifyData; + + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderExcelVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderExcelVO.java new file mode 100644 index 0000000..8f9b422 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderExcelVO.java @@ -0,0 +1,67 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderExportReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderExportReqVO.java new file mode 100644 index 0000000..566fdd8 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderExportReqVO.java @@ -0,0 +1,37 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java new file mode 100644 index 0000000..ae35a7c --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java @@ -0,0 +1,25 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderPageReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderPageReqVO.java new file mode 100644 index 0000000..d400287 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderPageReqVO.java @@ -0,0 +1,42 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderRespVO.java new file mode 100644 index 0000000..3e1fbc1 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderRespVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java new file mode 100644 index 0000000..963d14f --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java new file mode 100644 index 0000000..dd19139 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java @@ -0,0 +1,18 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/PayRefundController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/PayRefundController.java new file mode 100644 index 0000000..37e894b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/PayRefundController.java @@ -0,0 +1,98 @@ +package com.tashow.cloud.pay.controller.admin.refund; + +import cn.hutool.core.collection.CollectionUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.*; +import com.tashow.cloud.pay.controller.admin.refund.vo.*; +import com.tashow.cloud.payapi.controller.admin.refund.vo.*; +import com.tashow.cloud.pay.convert.refund.PayRefundConvert; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.refund.PayRefundDO; +import com.tashow.cloud.pay.service.app.PayAppService; +import com.tashow.cloud.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 jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@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')") + @ApiAccessLog(operateType = 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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundBaseVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundBaseVO.java new file mode 100644 index 0000000..12b4084 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundBaseVO.java @@ -0,0 +1,78 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java new file mode 100644 index 0000000..ab0ca7d --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java @@ -0,0 +1,40 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundExcelVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundExcelVO.java new file mode 100644 index 0000000..9bf9a71 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundExcelVO.java @@ -0,0 +1,61 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundExportReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundExportReqVO.java new file mode 100644 index 0000000..9aee454 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundExportReqVO.java @@ -0,0 +1,40 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java new file mode 100644 index 0000000..3d652a4 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java @@ -0,0 +1,25 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundPageReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundPageReqVO.java new file mode 100644 index 0000000..4f573cc --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/refund/vo/PayRefundPageReqVO.java @@ -0,0 +1,45 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/PayTransferController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/PayTransferController.java new file mode 100644 index 0000000..e523b1e --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/PayTransferController.java @@ -0,0 +1,53 @@ +package com.tashow.cloud.pay.controller.admin.transfer; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.*; +import com.tashow.cloud.pay.controller.admin.transfer.vo.*; +import com.tashow.cloud.payapi.controller.admin.transfer.vo.*; +import com.tashow.cloud.pay.convert.transfer.PayTransferConvert; +import com.tashow.cloud.pay.dal.dataobject.transfer.PayTransferDO; +import com.tashow.cloud.pay.service.transfer.PayTransferService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; + +@Tag(name = "管理后台 - 转账单") +@RestController +@RequestMapping("/pay/transfer") +@Validated +public class PayTransferController { + + @Resource + private PayTransferService payTransferService; + + @PostMapping("/create") + @Operation(summary = "创建转账单,发起转账") + @PreAuthorize("@ss.hasPermission('pay:transfer:create')") + public CommonResult createPayTransfer(@Valid @RequestBody PayTransferCreateReqVO reqVO) { + PayTransferDO payTransfer = payTransferService.createTransfer(reqVO, getClientIP()); + return success(new PayTransferCreateRespVO().setId(payTransfer.getId()).setStatus(payTransfer.getStatus())); + } + + @GetMapping("/get") + @Operation(summary = "获得转账订单") + @PreAuthorize("@ss.hasPermission('pay:transfer:query')") + public CommonResult getTransfer(@RequestParam("id") Long id) { + return success(PayTransferConvert.INSTANCE.convert(payTransferService.getTransfer(id))); + } + + @GetMapping("/page") + @Operation(summary = "获得转账订单分页") + @PreAuthorize("@ss.hasPermission('pay:transfer:query')") + public CommonResult> getTransferPage(@Valid PayTransferPageReqVO pageVO) { + PageResult pageResult = payTransferService.getTransferPage(pageVO); + return success(PayTransferConvert.INSTANCE.convertPage(pageResult)); + } +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferCreateReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferCreateReqVO.java new file mode 100644 index 0000000..809bedc --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferCreateReqVO.java @@ -0,0 +1,95 @@ +package com.tashow.cloud.pay.controller.admin.transfer.vo; + +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Validator; +import jakarta.validation.constraints.*; +import lombok.Data; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum.*; + +@Schema(description = "管理后台 - 发起转账 Request VO") +@Data +public class PayTransferCreateReqVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "应用编号不能为空") + private Long appId; + + @Schema(description = "商户转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商户转账单编号不能为空") + private String merchantTransferId; + + @Schema(description = "转账类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "转账类型不能为空") + @InEnum(PayTransferTypeEnum.class) + private Integer type; + + @Schema(description = "转账渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_pc") + @NotEmpty(message = "转账渠道不能为空") + private String channelCode; + + @Min(value = 1, message = "转账金额必须大于零") + @NotNull(message = "转账金额不能为空") + private Integer price; + + @Schema(description = "转账标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "示例转账") + @NotEmpty(message = "转账标题不能为空") + private String subject; + + @Schema(description = "收款人姓名", example = "test1") + @NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class}) + private String userName; + + @Schema(description = "支付宝登录号", example = "test1@sandbox.com") + @NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class}) + private String alipayLogonId; + + @Schema(description = "微信 openId", example = "oLefc4g5Gxx") + @NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class}) + private String openid; + + @Schema(description = "转账渠道的额外参数") + private Map channelExtras; + + public void validate(Validator validator) { + PayTransferTypeEnum transferType = typeOf(type); + switch (transferType) { + case ALIPAY_BALANCE: { + ValidationUtils.validate(validator, this, Alipay.class); + break; + } + case WX_BALANCE: { + ValidationUtils.validate(validator, this, WxPay.class); + break; + } + default: { + throw new UnsupportedOperationException("待实现"); + } + } + } + + @AssertTrue(message = "转账类型和转账渠道不匹配") + public boolean isValidChannelCode() { + PayTransferTypeEnum transferType = typeOf(type); + switch (transferType) { + case ALIPAY_BALANCE: { + return PayChannelEnum.isAlipay(channelCode); + } + case WX_BALANCE: + case BANK_CARD: + case WALLET_BALANCE: { + throw exception(NOT_IMPLEMENTED); + } + } + return Boolean.FALSE; + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferCreateRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferCreateRespVO.java new file mode 100644 index 0000000..b2ef0e9 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferCreateRespVO.java @@ -0,0 +1,16 @@ +package com.tashow.cloud.pay.controller.admin.transfer.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 发起转账 Response VO") +@Data +public class PayTransferCreateRespVO { + + @Schema(description = "转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "转账状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") // 参见 PayTransferStatusEnum 枚举 + private Integer status; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferPageItemRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferPageItemRespVO.java new file mode 100644 index 0000000..4e67881 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferPageItemRespVO.java @@ -0,0 +1,62 @@ +package com.tashow.cloud.pay.controller.admin.transfer.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @author jason + */ +@Schema(description = "管理后台 - 转账单分页项 Response VO") +@Data +public class PayTransferPageItemRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2931") + private Long id; + + @Schema(description = "转账单号", requiredMode = Schema.RequiredMode.REQUIRED) + private String no; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12831") + private Long appId; + + @Schema(description = "转账渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24833") + private Long channelId; + + @Schema(description = "转账渠道编码", requiredMode = Schema.RequiredMode.REQUIRED) + private String channelCode; + + @Schema(description = "商户转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17481") + private String merchantTransferId; + + @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer type; + + @Schema(description = "转账状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer status; + + @Schema(description = "转账成功时间") + private LocalDateTime successTime; + + @Schema(description = "转账金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "964") + private Integer price; + + @Schema(description = "转账标题", requiredMode = Schema.RequiredMode.REQUIRED) + private String subject; + + @Schema(description = "收款人姓名", example = "王五") + private String userName; + + @Schema(description = "支付宝登录号", example = "29245") + private String alipayLogonId; + + @Schema(description = "微信 openId", example = "26589") + private String openid; + + @Schema(description = "渠道转账单号") + private String channelTransferNo; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferPageReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferPageReqVO.java new file mode 100644 index 0000000..c5f0494 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferPageReqVO.java @@ -0,0 +1,48 @@ +package com.tashow.cloud.pay.controller.admin.transfer.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 PayTransferPageReqVO extends PageParam { + + @Schema(description = "转账单号") + private String no; + + @Schema(description = "应用编号", example = "12831") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户转账单编号", example = "17481") + private String merchantTransferId; + + @Schema(description = "类型", example = "2") + private Integer type; + + @Schema(description = "转账状态", example = "2") + private Integer status; + + @Schema(description = "收款人姓名", example = "王五") + private String userName; + + @Schema(description = "渠道转账单号") + private String channelTransferNo; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferRespVO.java new file mode 100644 index 0000000..fc7fb8d --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/transfer/vo/PayTransferRespVO.java @@ -0,0 +1,79 @@ +package com.tashow.cloud.pay.controller.admin.transfer.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +@Schema(description = "管理后台 - 转账单 Response VO") +@Data +public class PayTransferRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2931") + private Long id; + + @Schema(description = "转账单号", requiredMode = Schema.RequiredMode.REQUIRED) + private String no; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12831") + private Long appId; + + @Schema(description = "转账渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24833") + private Long channelId; + + @Schema(description = "转账渠道编码", requiredMode = Schema.RequiredMode.REQUIRED) + private String channelCode; + + @Schema(description = "商户转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17481") + private String merchantTransferId; + + @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer type; + + @Schema(description = "转账状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer status; + + @Schema(description = "转账成功时间") + private LocalDateTime successTime; + + @Schema(description = "转账金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "964") + private Integer price; + + @Schema(description = "转账标题", requiredMode = Schema.RequiredMode.REQUIRED) + private String subject; + + @Schema(description = "收款人姓名", example = "王五") + private String userName; + + @Schema(description = "支付宝登录号", example = "29245") + private String alipayLogonId; + + @Schema(description = "微信 openId", example = "26589") + private String openid; + + @Schema(description = "异步通知商户地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn") + private String notifyUrl; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED) + private String userIp; + + @Schema(description = "渠道的额外参数") + private Map channelExtras; + + @Schema(description = "渠道转账单号") + private String channelTransferNo; + + @Schema(description = "调用渠道的错误码") + private String channelErrorCode; + + @Schema(description = "调用渠道的错误提示") + private String channelErrorMsg; + + @Schema(description = "渠道的同步/异步通知的内容") + private String channelNotifyData; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletController.java new file mode 100644 index 0000000..c11581b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletController.java @@ -0,0 +1,70 @@ +package com.tashow.cloud.pay.controller.admin.wallet; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.wallet.PayWalletUpdateBalanceReqVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.wallet.PayWalletUserReqVO; +import com.tashow.cloud.pay.convert.wallet.PayWalletConvert; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletDO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import com.tashow.cloud.pay.service.wallet.PayWalletService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +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 static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.MEMBER; +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.module.pay.enums.ErrorCodeConstants.WALLET_NOT_FOUND; + +@Tag(name = "管理后台 - 用户钱包") +@RestController +@RequestMapping("/pay/wallet") +@Validated +@Slf4j +public class PayWalletController { + + @Resource + private PayWalletService payWalletService; + + @GetMapping("/get") + @PreAuthorize("@ss.hasPermission('pay:wallet:query')") + @Operation(summary = "获得用户钱包明细") + public CommonResult getWallet(PayWalletUserReqVO reqVO) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(reqVO.getUserId(), MEMBER.getValue()); + return success(PayWalletConvert.INSTANCE.convert02(wallet)); + } + + @GetMapping("/page") + @Operation(summary = "获得会员钱包分页") + @PreAuthorize("@ss.hasPermission('pay:wallet:query')") + public CommonResult> getWalletPage(@Valid PayWalletPageReqVO pageVO) { + PageResult pageResult = payWalletService.getWalletPage(pageVO); + return success(PayWalletConvert.INSTANCE.convertPage(pageResult)); + } + + @PutMapping("/update-balance") + @Operation(summary = "更新会员用户余额") + @PreAuthorize("@ss.hasPermission('pay:wallet:update-balance')") + public CommonResult updateWalletBalance(@Valid @RequestBody PayWalletUpdateBalanceReqVO updateReqVO) { + // 获得用户钱包 + PayWalletDO wallet = payWalletService.getOrCreateWallet(updateReqVO.getUserId(), MEMBER.getValue()); + if (wallet == null) { + log.error("[updateWalletBalance],updateReqVO({}) 用户钱包不存在.", updateReqVO); + throw exception(WALLET_NOT_FOUND); + } + + // 更新钱包余额 + payWalletService.addWalletBalance(wallet.getId(), String.valueOf(updateReqVO.getUserId()), + PayWalletBizTypeEnum.UPDATE_BALANCE, updateReqVO.getBalance()); + return success(true); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletRechargeController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletRechargeController.java new file mode 100644 index 0000000..cb10401 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletRechargeController.java @@ -0,0 +1,57 @@ +package com.tashow.cloud.pay.controller.admin.wallet; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import com.tashow.cloud.pay.service.wallet.PayWalletRechargeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; + +@Tag(name = "管理后台 - 钱包充值") +@RestController +@RequestMapping("/pay/wallet-recharge") +@Validated +@Slf4j +public class PayWalletRechargeController { + + @Resource + private PayWalletRechargeService walletRechargeService; + + @PostMapping("/update-paid") + @Operation(summary = "更新钱包充值为已充值") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录, 内部校验实现 + public CommonResult updateWalletRechargerPaid(@Valid @RequestBody PayOrderNotifyReqDTO notifyReqDTO) { + walletRechargeService.updateWalletRechargerPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getPayOrderId()); + return success(true); + } + + // TODO @jason:发起退款,要 post 操作哈; + @GetMapping("/refund") + @Operation(summary = "发起钱包充值退款") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult refundWalletRecharge(@RequestParam("id") Long id) { + walletRechargeService.refundWalletRecharge(id, getClientIP()); + return success(true); + } + + @PostMapping("/update-refunded") + @Operation(summary = "更新钱包充值为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录, 内部校验实现 + public CommonResult updateWalletRechargeRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { + walletRechargeService.updateWalletRechargeRefunded( + Long.valueOf(notifyReqDTO.getMerchantOrderId()), notifyReqDTO.getPayRefundId()); + return success(true); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletRechargePackageController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletRechargePackageController.java new file mode 100644 index 0000000..0b081d1 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletRechargePackageController.java @@ -0,0 +1,74 @@ +package com.tashow.cloud.pay.controller.admin.wallet; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageCreateReqVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackagePageReqVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageRespVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageUpdateReqVO; +import com.tashow.cloud.pay.convert.wallet.PayWalletRechargePackageConvert; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import com.tashow.cloud.pay.service.wallet.PayWalletRechargePackageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + + +@Tag(name = "管理后台 - 钱包充值套餐") +@RestController +@RequestMapping("/pay/wallet-recharge-package") +@Validated +public class PayWalletRechargePackageController { + + @Resource + private PayWalletRechargePackageService walletRechargePackageService; + + @PostMapping("/create") + @Operation(summary = "创建钱包充值套餐") + @PreAuthorize("@ss.hasPermission('pay:wallet-recharge-package:create')") + public CommonResult createWalletRechargePackage(@Valid @RequestBody WalletRechargePackageCreateReqVO createReqVO) { + return success(walletRechargePackageService.createWalletRechargePackage(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新钱包充值套餐") + @PreAuthorize("@ss.hasPermission('pay:wallet-recharge-package:update')") + public CommonResult updateWalletRechargePackage(@Valid @RequestBody WalletRechargePackageUpdateReqVO updateReqVO) { + walletRechargePackageService.updateWalletRechargePackage(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除钱包充值套餐") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('pay:wallet-recharge-package:delete')") + public CommonResult deleteWalletRechargePackage(@RequestParam("id") Long id) { + walletRechargePackageService.deleteWalletRechargePackage(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得钱包充值套餐") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:wallet-recharge-package:query')") + public CommonResult getWalletRechargePackage(@RequestParam("id") Long id) { + PayWalletRechargePackageDO walletRechargePackage = walletRechargePackageService.getWalletRechargePackage(id); + return success(PayWalletRechargePackageConvert.INSTANCE.convert(walletRechargePackage)); + } + + @GetMapping("/page") + @Operation(summary = "获得钱包充值套餐分页") + @PreAuthorize("@ss.hasPermission('pay:wallet-recharge-package:query')") + public CommonResult> getWalletRechargePackagePage(@Valid WalletRechargePackagePageReqVO pageVO) { + PageResult pageResult = walletRechargePackageService.getWalletRechargePackagePage(pageVO); + return success(PayWalletRechargePackageConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletTransactionController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletTransactionController.java new file mode 100644 index 0000000..dc8dd6d --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/PayWalletTransactionController.java @@ -0,0 +1,42 @@ +package com.tashow.cloud.pay.controller.admin.wallet; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionRespVO; +import com.tashow.cloud.pay.convert.wallet.PayWalletTransactionConvert; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import com.tashow.cloud.pay.service.wallet.PayWalletTransactionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +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.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 钱包余额明细") +@RestController +@RequestMapping("/pay/wallet-transaction") +@Validated +@Slf4j +public class PayWalletTransactionController { + + @Resource + private PayWalletTransactionService payWalletTransactionService; + + @GetMapping("/page") + @Operation(summary = "获得钱包流水分页") + @PreAuthorize("@ss.hasPermission('pay:wallet:query')") + public CommonResult> getWalletTransactionPage( + @Valid PayWalletTransactionPageReqVO pageReqVO) { + PageResult result = payWalletTransactionService.getWalletTransactionPage(pageReqVO); + return success(PayWalletTransactionConvert.INSTANCE.convertPage2(result)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java new file mode 100644 index 0000000..08da20b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageBaseVO.java @@ -0,0 +1,30 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 充值套餐 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class WalletRechargePackageBaseVO { + + @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotNull(message = "套餐名不能为空") + private String name; + + @Schema(description = "支付金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "16454") + @NotNull(message = "支付金额不能为空") + private Integer payPrice; + + @Schema(description = "赠送金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20887") + @NotNull(message = "赠送金额不能为空") + private Integer bonusPrice; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "状态不能为空") + private Byte status; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageCreateReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageCreateReqVO.java new file mode 100644 index 0000000..affba6f --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageCreateReqVO.java @@ -0,0 +1,14 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 充值套餐创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WalletRechargePackageCreateReqVO extends WalletRechargePackageBaseVO { + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackagePageReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackagePageReqVO.java new file mode 100644 index 0000000..79667d0 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackagePageReqVO.java @@ -0,0 +1,30 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage; + +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 WalletRechargePackagePageReqVO extends PageParam { + + @Schema(description = "套餐名", example = "李四") + private String name; + + @Schema(description = "状态", example = "2") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageRespVO.java new file mode 100644 index 0000000..0ec7484 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageRespVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage; + +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 WalletRechargePackageRespVO extends WalletRechargePackageBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9032") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageUpdateReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageUpdateReqVO.java new file mode 100644 index 0000000..3f848fc --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/rechargepackage/WalletRechargePackageUpdateReqVO.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 充值套餐更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WalletRechargePackageUpdateReqVO extends WalletRechargePackageBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9032") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java new file mode 100644 index 0000000..eea1b62 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java @@ -0,0 +1,23 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.transaction; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 钱包流水分页 Request VO") +@Data +public class PayWalletTransactionPageReqVO extends PageParam { + + @Schema(description = "钱包编号", example = "888") + private Long walletId; + + @Schema(description = "用户编号", example = "1024") + private Long userId; + + @Schema(description = "用户类型", example = "1") + @InEnum(UserTypeEnum.class) + private Integer userType; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionRespVO.java new file mode 100644 index 0000000..7db9a5c --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionRespVO.java @@ -0,0 +1,35 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.transaction; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 APP - 钱包流水分页 Response VO") +@Data +public class PayWalletTransactionRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "钱包编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Long walletId; + + @Schema(description = "业务分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer bizType; + + @Schema(description = "交易金额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long price; + + @Schema(description = "流水标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆土豆") + private String title; + + @Schema(description = "交易后的余额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long balance; + + @Schema(description = "交易时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + // TODO @jason:merchantOrderId 字段,需要在 PayWalletTransaction 存储下;然后,前端也返回下这个字段,界面也展示下商户名 + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletBaseVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletBaseVO.java new file mode 100644 index 0000000..dc6cd80 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletBaseVO.java @@ -0,0 +1,38 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 用户钱包 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class PayWalletBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20020") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @Schema(description = "余额,单位分", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "余额,单位分不能为空") + private Integer balance; + + @Schema(description = "累计支出,单位分", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "累计支出,单位分不能为空") + private Integer totalExpense; + + @Schema(description = "累计充值,单位分", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "累计充值,单位分不能为空") + private Integer totalRecharge; + + @Schema(description = "冻结金额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "20737") + @NotNull(message = "冻结金额,单位分不能为空") + private Integer freezePrice; + +} \ No newline at end of file diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletPageReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletPageReqVO.java new file mode 100644 index 0000000..f892763 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletPageReqVO.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.wallet; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +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 PayWalletPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "1024") + private Long userId; + + @Schema(description = "用户类型", example = "1") + @InEnum(value = UserTypeEnum.class) + private Integer userType; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletRespVO.java new file mode 100644 index 0000000..7cc2d7f --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletRespVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.wallet; + +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 PayWalletRespVO extends PayWalletBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29528") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java new file mode 100644 index 0000000..2de68f5 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletUpdateBalanceReqVO.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 修改钱包余额 Request VO") +@Data +public class PayWalletUpdateBalanceReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "变动余额,正数为增加,负数为减少", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "变动余额不能为空") + private Integer balance; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletUserReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletUserReqVO.java new file mode 100644 index 0000000..c4b548d --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/admin/wallet/vo/wallet/PayWalletUserReqVO.java @@ -0,0 +1,15 @@ +package com.tashow.cloud.pay.controller.admin.wallet.vo.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 用户钱包明细 Request VO") +@Data +public class PayWalletUserReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "用户编号不能为空") + private Long userId; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/channel/AppPayChannelController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/channel/AppPayChannelController.java new file mode 100644 index 0000000..afea122 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/channel/AppPayChannelController.java @@ -0,0 +1,42 @@ +package com.tashow.cloud.pay.controller.app.channel; + +import com.tashow.cloud.common.pojo.CommonResult; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.pay.service.channel.PayChannelService; +import jakarta.annotation.Resource; +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 java.util.List; +import java.util.Set; + +import static com.tashow.cloud.common.pojo.CommonResult.success; +import static com.tashow.cloud.common.util.collection.CollectionUtils.convertSet; + + +/** + * 用户 App - 支付渠道 + */ +@RestController +@RequestMapping("/pay/channel") +@Validated +public class AppPayChannelController { + + @Resource + private PayChannelService channelService; + + /** + * 获得指定应用的开启的支付渠道编码列表 + * @param appId 应用编号 + * @return + */ + @GetMapping("/get-enable-code-list") + public CommonResult> getEnableChannelCodeList(@RequestParam("appId") Long appId) { + List channels = channelService.getEnableChannelList(appId); + return success(convertSet(channels, PayChannelDO::getCode)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/AppPayOrderController.http b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/AppPayOrderController.http new file mode 100644 index 0000000..4fe1898 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/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: {{appTenantId}} + +{ + "id": 174, + "channelCode": "alipay_pc" +} + +### /pay/create 提交支付订单【wx_bar】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +{ + "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: {{appTenantId}} + +{ + "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: {{appTenantId}} + +{ + "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: {{appTenantId}} + +{ + "id": 202, + "channelCode": "wx_native" +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/AppPayOrderController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/AppPayOrderController.java new file mode 100644 index 0000000..553ac6e --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/AppPayOrderController.java @@ -0,0 +1,74 @@ +package com.tashow.cloud.pay.controller.app.order; + +import com.tashow.cloud.common.pojo.CommonResult; +import com.tashow.cloud.common.util.object.BeanUtils; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderRespVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import com.tashow.cloud.pay.controller.app.order.vo.AppPayOrderSubmitReqVO; +import com.tashow.cloud.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; +import com.tashow.cloud.pay.convert.order.PayOrderConvert; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.tashow.cloud.payapi.enums.order.PayOrderStatusEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static com.tashow.cloud.common.pojo.CommonResult.success; +import static com.tashow.cloud.common.util.servlet.ServletUtils.getClientIP; + + +/** + * 用户 APP - 支付订单 + */ +@RestController +@RequestMapping("/pay/order") +@Validated +@Slf4j +public class AppPayOrderController { + + @Resource + private PayOrderService payOrderService; + + /** + * 获得支付订单 + * @param id 编号 + * @param sync 是否同步 + * @return + */ + @GetMapping("/get") + public CommonResult getOrder(@RequestParam("id") Long id, + @RequestParam(value = "sync", required = false) Boolean sync) { + PayOrderDO order = payOrderService.getOrder(id); + // sync 仅在等待支付 + if (Boolean.TRUE.equals(sync) && PayOrderStatusEnum.isWaiting(order.getStatus())) { + payOrderService.syncOrderQuietly(order.getId()); + // 重新查询,因为同步后,可能会有变化 + order = payOrderService.getOrder(id); + } + return success(BeanUtils.toBean(order, PayOrderRespVO.class)); + } + + /** + * 提交支付订单 + * @param reqVO + * @return + */ + @PostMapping("/submit") + public CommonResult submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) { + // 1. 钱包支付事,需要额外传 user_id 和 user_type +// if (Objects.equals(reqVO.getChannelCode(), PayChannelEnum.WALLET.getCode())) { +// Map channelExtras = reqVO.getChannelExtras() == null ? +// Maps.newHashMapWithExpectedSize(2) : reqVO.getChannelExtras(); +// channelExtras.put(WalletPayClient.USER_ID_KEY, String.valueOf(getLoginUserId())); +// channelExtras.put(WalletPayClient.USER_TYPE_KEY, String.valueOf(getLoginUserType())); +// reqVO.setChannelExtras(channelExtras); +// } + + // 2. 提交支付 + PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP()); + return success(PayOrderConvert.INSTANCE.convert3(respVO)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java new file mode 100644 index 0000000..a03a051 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java @@ -0,0 +1,10 @@ +package com.tashow.cloud.pay.controller.app.order.vo; + +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 支付订单提交 Request VO") +@Data +public class AppPayOrderSubmitReqVO extends PayOrderSubmitReqVO { +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java new file mode 100644 index 0000000..e7185f7 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java @@ -0,0 +1,11 @@ +package com.tashow.cloud.pay.controller.app.order.vo; + +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 支付订单提交 Response VO") +@Data +public class AppPayOrderSubmitRespVO extends PayOrderSubmitRespVO { + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletController.java new file mode 100644 index 0000000..71fbcc2 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletController.java @@ -0,0 +1,41 @@ +package com.tashow.cloud.pay.controller.app.wallet; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.tashow.cloud.pay.controller.app.wallet.vo.wallet.AppPayWalletRespVO; +import com.tashow.cloud.pay.convert.wallet.PayWalletConvert; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletDO; +import com.tashow.cloud.pay.service.wallet.PayWalletService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +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.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * @author jason + */ +@Tag(name = "用户 APP - 钱包") +@RestController +@RequestMapping("/pay/wallet") +@Validated +@Slf4j +public class AppPayWalletController { + + @Resource + private PayWalletService payWalletService; + + @GetMapping("/get") + @Operation(summary = "获取钱包") + public CommonResult getPayWallet() { + PayWalletDO wallet = payWalletService.getOrCreateWallet(getLoginUserId(), UserTypeEnum.MEMBER.getValue()); + return success(PayWalletConvert.INSTANCE.convert(wallet)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletRechargeController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletRechargeController.java new file mode 100644 index 0000000..61cb25f --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletRechargeController.java @@ -0,0 +1,67 @@ +package com.tashow.cloud.pay.controller.app.wallet; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +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 com.tashow.cloud.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateReqVO; +import com.tashow.cloud.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateRespVO; +import com.tashow.cloud.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeRespVO; +import com.tashow.cloud.pay.convert.wallet.PayWalletRechargeConvert; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargeDO; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.tashow.cloud.pay.service.wallet.PayWalletRechargeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +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.web.core.util.WebFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserType; + +@Tag(name = "用户 APP - 钱包充值") +@RestController +@RequestMapping("/pay/wallet-recharge") +@Validated +@Slf4j +public class AppPayWalletRechargeController { + + @Resource + private PayWalletRechargeService walletRechargeService; + @Resource + private PayOrderService payOrderService; + + @PostMapping("/create") + @Operation(summary = "创建钱包充值记录(发起充值)") + public CommonResult createWalletRecharge( + @Valid @RequestBody AppPayWalletRechargeCreateReqVO reqVO) { + PayWalletRechargeDO walletRecharge = walletRechargeService.createWalletRecharge( + getLoginUserId(), getLoginUserType(), getClientIP(), reqVO); + return success(PayWalletRechargeConvert.INSTANCE.convert(walletRecharge)); + } + + @GetMapping("/page") + @Operation(summary = "获得钱包充值记录分页") + public CommonResult> getWalletRechargePage(@Valid PageParam pageReqVO) { + PageResult pageResult = walletRechargeService.getWalletRechargePackagePage( + getLoginUserId(), UserTypeEnum.MEMBER.getValue(), pageReqVO, true); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + // 拼接数据 + List payOrderList = payOrderService.getOrderList( + convertList(pageResult.getList(), PayWalletRechargeDO::getPayOrderId)); + return success(PayWalletRechargeConvert.INSTANCE.convertPage(pageResult, payOrderList)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletRechargePackageController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletRechargePackageController.java new file mode 100644 index 0000000..49f46f5 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletRechargePackageController.java @@ -0,0 +1,42 @@ +package com.tashow.cloud.pay.controller.app.wallet; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import com.tashow.cloud.pay.controller.app.wallet.vo.recharge.AppPayWalletPackageRespVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import com.tashow.cloud.pay.service.wallet.PayWalletRechargePackageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +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.RestController; + +import java.util.Comparator; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 APP - 钱包充值套餐") +@RestController +@RequestMapping("/pay/wallet-recharge-package") +@Validated +@Slf4j +public class AppPayWalletRechargePackageController { + + @Resource + private PayWalletRechargePackageService walletRechargePackageService; + + @GetMapping("/list") + @Operation(summary = "获得钱包充值套餐列表") + public CommonResult> getWalletRechargePackageList() { + List list = walletRechargePackageService.getWalletRechargePackageList( + CommonStatusEnum.ENABLE.getStatus()); + list.sort(Comparator.comparingInt(PayWalletRechargePackageDO::getPayPrice)); + return success(BeanUtils.toBean(list, AppPayWalletPackageRespVO.class)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletTransactionController.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletTransactionController.java new file mode 100644 index 0000000..6c3dc8c --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/AppPayWalletTransactionController.java @@ -0,0 +1,60 @@ +package com.tashow.cloud.pay.controller.app.wallet; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO; +import com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionRespVO; +import com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionSummaryRespVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import com.tashow.cloud.pay.service.wallet.PayWalletTransactionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +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 java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 APP - 钱包余额明细") +@RestController +@RequestMapping("/pay/wallet-transaction") +@Validated +@Slf4j +public class AppPayWalletTransactionController { + + @Resource + private PayWalletTransactionService payWalletTransactionService; + + @GetMapping("/page") + @Operation(summary = "获得钱包流水分页") + public CommonResult> getWalletTransactionPage( + @Valid AppPayWalletTransactionPageReqVO pageReqVO) { + PageResult pageResult = payWalletTransactionService.getWalletTransactionPage( + getLoginUserId(), UserTypeEnum.MEMBER.getValue(), pageReqVO); + return success(BeanUtils.toBean(pageResult, AppPayWalletTransactionRespVO.class)); + } + + @GetMapping("/get-summary") + @Operation(summary = "获得钱包流水统计") + @Parameter(name = "times", description = "时间段", required = true) + public CommonResult getWalletTransactionSummary( + @RequestParam("createTime") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) LocalDateTime[] createTime) { + AppPayWalletTransactionSummaryRespVO summary = payWalletTransactionService.getWalletTransactionSummary( + getLoginUserId(), UserTypeEnum.MEMBER.getValue(), createTime); + return success(summary); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletPackageRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletPackageRespVO.java new file mode 100644 index 0000000..46f95a0 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletPackageRespVO.java @@ -0,0 +1,20 @@ +package com.tashow.cloud.pay.controller.app.wallet.vo.recharge; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 用户充值套餐 Response VO") +@Data +public class AppPayWalletPackageRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小套餐") + private String name; + + @Schema(description = "支付金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer payPrice; + @Schema(description = "赠送金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer bonusPrice; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java new file mode 100644 index 0000000..85cc681 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateReqVO.java @@ -0,0 +1,26 @@ +package com.tashow.cloud.pay.controller.app.wallet.vo.recharge; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Min; +import lombok.Data; + +import java.util.Objects; + +@Schema(description = "用户 APP - 创建钱包充值 Request VO") +@Data +public class AppPayWalletRechargeCreateReqVO { + + @Schema(description = "支付金额", example = "1000") + @Min(value = 1, message = "支付金额必须大于零") + private Integer payPrice; + + @Schema(description = "充值套餐编号", example = "1024") + private Long packageId; + + @AssertTrue(message = "充值金额和充钱套餐不能同时为空") + public boolean isValidPayPriceAndPackageId() { + return Objects.nonNull(payPrice) || Objects.nonNull(packageId); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateRespVO.java new file mode 100644 index 0000000..15d7e5b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeCreateRespVO.java @@ -0,0 +1,16 @@ +package com.tashow.cloud.pay.controller.app.wallet.vo.recharge; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 创建钱包充值 Resp VO") +@Data +public class AppPayWalletRechargeCreateRespVO { + + @Schema(description = "钱包充值编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long payOrderId; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeRespVO.java new file mode 100644 index 0000000..da26eb4 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/recharge/AppPayWalletRechargeRespVO.java @@ -0,0 +1,42 @@ +package com.tashow.cloud.pay.controller.app.wallet.vo.recharge; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 APP - 钱包充值记录 Resp VO") +@Data +public class AppPayWalletRechargeRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户实际到账余额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer totalPrice; + + @Schema(description = "实际支付金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer payPrice; + + @Schema(description = "钱包赠送金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "80") + private Integer bonusPrice; + + @Schema(description = "支付成功的支付渠道", requiredMode = Schema.RequiredMode.REQUIRED) + private String payChannelCode; + + @Schema(description = "支付渠道名", example = "微信小程序支付") + private String payChannelName; + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long payOrderId; + + @Schema(description = "支付成功的外部订单号", requiredMode = Schema.RequiredMode.REQUIRED) + private String payOrderChannelOrderNo; // 从 PayOrderDO 的 channelOrderNo 字段 + + @Schema(description = "订单支付时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime payTime; + + @Schema(description = "退款状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer refundStatus; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionPageReqVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionPageReqVO.java new file mode 100644 index 0000000..daba89b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionPageReqVO.java @@ -0,0 +1,31 @@ +package com.tashow.cloud.pay.controller.app.wallet.vo.transaction; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +@Schema(description = "用户 APP - 钱包流水分页 Request VO") +@Data +public class AppPayWalletTransactionPageReqVO extends PageParam { + + /** + * 类型 - 收入 + */ + public static final Integer TYPE_INCOME = 1; + /** + * 类型 - 支出 + */ + public static final Integer TYPE_EXPENSE = 2; + + @Schema(description = "类型", example = "1") + private Integer type; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionRespVO.java new file mode 100644 index 0000000..bd26b34 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionRespVO.java @@ -0,0 +1,24 @@ +package com.tashow.cloud.pay.controller.app.wallet.vo.transaction; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 APP - 钱包流水分页 Response VO") +@Data +public class AppPayWalletTransactionRespVO { + + @Schema(description = "业务分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer bizType; + + @Schema(description = "交易金额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long price; + + @Schema(description = "流水标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆土豆") + private String title; + + @Schema(description = "交易时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionSummaryRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionSummaryRespVO.java new file mode 100644 index 0000000..5c3c685 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/transaction/AppPayWalletTransactionSummaryRespVO.java @@ -0,0 +1,16 @@ +package com.tashow.cloud.pay.controller.app.wallet.vo.transaction; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 钱包流水统计 Request VO") +@Data +public class AppPayWalletTransactionSummaryRespVO { + + @Schema(description = "累计支出,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer totalExpense; + + @Schema(description = "累计收入,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000") + private Integer totalIncome; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/wallet/AppPayWalletRespVO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/wallet/AppPayWalletRespVO.java new file mode 100644 index 0000000..e5f6f75 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/app/wallet/vo/wallet/AppPayWalletRespVO.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.pay.controller.app.wallet.vo.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 APP - 用户钱包 Response VO") +@Data +public class AppPayWalletRespVO { + + @Schema(description = "钱包余额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer balance; + + @Schema(description = "累计支出,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer totalExpense; + + @Schema(description = "累计充值,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000") + private Integer totalRecharge; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/package-info.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/controller/package-info.java new file mode 100644 index 0000000..3b9527b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/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 com.tashow.cloud.pay.controller; diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/app/PayAppConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/app/PayAppConvert.java new file mode 100644 index 0000000..930c3a4 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/app/PayAppConvert.java @@ -0,0 +1,48 @@ +package com.tashow.cloud.pay.convert.app; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppCreateReqVO; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppPageItemRespVO; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppRespVO; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.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; + +/** + * 支付应用信息 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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/channel/PayChannelConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/channel/PayChannelConvert.java new file mode 100644 index 0000000..2458dab --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/channel/PayChannelConvert.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.pay.convert.channel; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelRespVO; +import com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/demo/PayDemoOrderConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/demo/PayDemoOrderConvert.java new file mode 100644 index 0000000..e48872f --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/demo/PayDemoOrderConvert.java @@ -0,0 +1,26 @@ +package com.tashow.cloud.pay.convert.demo; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO; +import com.tashow.cloud.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO; +import com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/demo/PayDemoTransferConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/demo/PayDemoTransferConvert.java new file mode 100644 index 0000000..a0e9f3c --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/demo/PayDemoTransferConvert.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.pay.convert.demo; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO; +import com.tashow.cloud.pay.controller.admin.demo.vo.transfer.PayDemoTransferRespVO; +import com.tashow.cloud.pay.dal.dataobject.demo.PayDemoTransferDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author jason + */ +@Mapper +public interface PayDemoTransferConvert { + + PayDemoTransferConvert INSTANCE = Mappers.getMapper(PayDemoTransferConvert.class); + + PayDemoTransferDO convert(PayDemoTransferCreateReqVO bean); + + PageResult convertPage(PageResult pageResult); +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/notify/PayNotifyTaskConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/notify/PayNotifyTaskConvert.java new file mode 100644 index 0000000..c1842f0 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/notify/PayNotifyTaskConvert.java @@ -0,0 +1,43 @@ +package com.tashow.cloud.pay.convert.notify; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import com.tashow.cloud.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO; +import com.tashow.cloud.pay.controller.admin.notify.vo.PayNotifyTaskRespVO; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/order/PayOrderConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/order/PayOrderConvert.java new file mode 100644 index 0000000..21be540 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/order/PayOrderConvert.java @@ -0,0 +1,76 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.order.vo.*; +import com.tashow.cloud.payapi.controller.admin.order.vo.*; +import com.tashow.cloud.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/package-info.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/package-info.java new file mode 100644 index 0000000..303126f --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package com.tashow.cloud.pay.convert; diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/refund/PayRefundConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/refund/PayRefundConvert.java new file mode 100644 index 0000000..dee47b6 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/refund/PayRefundConvert.java @@ -0,0 +1,56 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundDetailsRespVO; +import com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundExcelVO; +import com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundPageItemRespVO; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/transfer/PayTransferConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/transfer/PayTransferConvert.java new file mode 100644 index 0000000..85c2f84 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/transfer/PayTransferConvert.java @@ -0,0 +1,31 @@ +package com.tashow.cloud.pay.convert.transfer; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import com.tashow.cloud.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO; +import com.tashow.cloud.pay.controller.admin.transfer.vo.PayTransferCreateReqVO; +import com.tashow.cloud.pay.controller.admin.transfer.vo.PayTransferPageItemRespVO; +import com.tashow.cloud.pay.controller.admin.transfer.vo.PayTransferRespVO; +import com.tashow.cloud.pay.dal.dataobject.transfer.PayTransferDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PayTransferConvert { + + PayTransferConvert INSTANCE = Mappers.getMapper(PayTransferConvert.class); + + PayTransferDO convert(PayTransferCreateReqDTO dto); + + PayTransferUnifiedReqDTO convert2(PayTransferDO dto); + + PayTransferCreateReqDTO convert(PayTransferCreateReqVO vo); + + PayTransferCreateReqDTO convert(PayDemoTransferCreateReqVO vo); + + PayTransferRespVO convert(PayTransferDO bean); + + PageResult convertPage(PageResult pageResult); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletConvert.java new file mode 100644 index 0000000..5dd663b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletConvert.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.pay.convert.wallet; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.wallet.PayWalletRespVO; +import com.tashow.cloud.pay.controller.app.wallet.vo.wallet.AppPayWalletRespVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PayWalletConvert { + + PayWalletConvert INSTANCE = Mappers.getMapper(PayWalletConvert.class); + + AppPayWalletRespVO convert(PayWalletDO bean); + + PayWalletRespVO convert02(PayWalletDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletRechargeConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletRechargeConvert.java new file mode 100644 index 0000000..1556660 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletRechargeConvert.java @@ -0,0 +1,43 @@ +package com.tashow.cloud.pay.convert.wallet; + +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.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import com.tashow.cloud.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateRespVO; +import com.tashow.cloud.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeRespVO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargeDO; +import cn.iocoder.yudao.module.pay.enums.DictTypeConstants; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface PayWalletRechargeConvert { + + PayWalletRechargeConvert INSTANCE = Mappers.getMapper(PayWalletRechargeConvert.class); + + @Mapping(target = "totalPrice", expression = "java( payPrice + bonusPrice)") + PayWalletRechargeDO convert(Long walletId, Integer payPrice, Integer bonusPrice, Long packageId); + + AppPayWalletRechargeCreateRespVO convert(PayWalletRechargeDO bean); + + default PageResult convertPage(PageResult pageResult, + List payOrderList) { + PageResult voPageResult = BeanUtils.toBean(pageResult, AppPayWalletRechargeRespVO.class); + Map payOrderMap = CollectionUtils.convertMap(payOrderList, PayOrderDO::getId); + voPageResult.getList().forEach(recharge -> { + recharge.setPayChannelName(DictFrameworkUtils.getDictDataLabel( + DictTypeConstants.CHANNEL_CODE, recharge.getPayChannelCode())); + MapUtils.findAndThen(payOrderMap, recharge.getPayOrderId(), + order -> recharge.setPayOrderChannelOrderNo(order.getChannelOrderNo())); + }); + return voPageResult; + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletRechargePackageConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletRechargePackageConvert.java new file mode 100644 index 0000000..2a2bcb7 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletRechargePackageConvert.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.pay.convert.wallet; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageCreateReqVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageRespVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageUpdateReqVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface PayWalletRechargePackageConvert { + + PayWalletRechargePackageConvert INSTANCE = Mappers.getMapper(PayWalletRechargePackageConvert.class); + + PayWalletRechargePackageDO convert(WalletRechargePackageCreateReqVO bean); + + PayWalletRechargePackageDO convert(WalletRechargePackageUpdateReqVO bean); + + WalletRechargePackageRespVO convert(PayWalletRechargePackageDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletTransactionConvert.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletTransactionConvert.java new file mode 100644 index 0000000..7c181d4 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/wallet/PayWalletTransactionConvert.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.pay.convert.wallet; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionRespVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import com.tashow.cloud.pay.service.wallet.bo.WalletTransactionCreateReqBO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PayWalletTransactionConvert { + + PayWalletTransactionConvert INSTANCE = Mappers.getMapper(PayWalletTransactionConvert.class); + + PageResult convertPage2(PageResult page); + + PayWalletTransactionDO convert(WalletTransactionCreateReqBO bean); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 0000000..8153487 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/app/PayAppDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/app/PayAppDO.java new file mode 100644 index 0000000..a4d439e --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/app/PayAppDO.java @@ -0,0 +1,66 @@ +package com.tashow.cloud.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 appKey; + /** + * 应用名 + */ + private String name; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 支付结果的回调地址 + */ + private String orderNotifyUrl; + /** + * 退款结果的回调地址 + */ + private String refundNotifyUrl; + + /** + * 转账结果的回调地址 + */ + private String transferNotifyUrl; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/channel/PayChannelDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/channel/PayChannelDO.java new file mode 100644 index 0000000..3828922 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/channel/PayChannelDO.java @@ -0,0 +1,69 @@ +package com.tashow.cloud.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 com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/demo/PayDemoOrderDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/demo/PayDemoOrderDO.java new file mode 100644 index 0000000..e29388c --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/demo/PayDemoOrderDO.java @@ -0,0 +1,87 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/demo/PayDemoTransferDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/demo/PayDemoTransferDO.java new file mode 100644 index 0000000..e25adca --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/demo/PayDemoTransferDO.java @@ -0,0 +1,84 @@ +package com.tashow.cloud.pay.dal.dataobject.demo; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +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; + +// TODO 芋艿:需要详细 review +/** + * 示例转账订单 + * + * 演示业务系统的转账业务 + */ +@TableName(value ="pay_demo_transfer", autoResultMap = true) +@KeySequence("pay_demo_transfer_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayDemoTransferDO extends BaseDO { + + /** + * 订单编号 + */ + @TableId + private Long id; + + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + + /** + * 转账类型 + *

+ * 枚举 {@link PayTransferTypeEnum} + */ + private Integer type; + + /** + * 转账金额,单位:分 + */ + private Integer price; + + /** + * 收款人姓名 + */ + private String userName; + + /** + * 支付宝登录号 + */ + private String alipayLogonId; + + /** + * 微信 openId + */ + private String openid; + + /** + * 转账状态 + */ + private Integer transferStatus; + + /** + * 转账单编号 + */ + private Long payTransferId; + + /** + * 转账支付成功渠道 + */ + private String payChannelCode; + + /** + * 转账支付时间 + */ + private LocalDateTime transferTime; + +} \ No newline at end of file diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/notify/PayNotifyLogDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/notify/PayNotifyLogDO.java new file mode 100644 index 0000000..84188c0 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/notify/PayNotifyLogDO.java @@ -0,0 +1,51 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/notify/PayNotifyTaskDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/notify/PayNotifyTaskDO.java new file mode 100644 index 0000000..fa82763 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/notify/PayNotifyTaskDO.java @@ -0,0 +1,100 @@ +package com.tashow.cloud.pay.dal.dataobject.notify; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.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; + /** + * 商户转账单编号 + */ + private String merchantTransferId; + /** + * 通知状态 + * + * 枚举 {@link PayNotifyStatusEnum} + */ + private Integer status; + /** + * 下一次通知时间 + */ + private LocalDateTime nextNotifyTime; + /** + * 最后一次执行时间 + */ + private LocalDateTime lastExecuteTime; + /** + * 当前通知次数 + */ + private Integer notifyTimes; + /** + * 最大可通知次数 + */ + private Integer maxNotifyTimes; + /** + * 通知地址 + */ + private String notifyUrl; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/order/PayOrderDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/order/PayOrderDO.java new file mode 100644 index 0000000..61f28bc --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/order/PayOrderDO.java @@ -0,0 +1,138 @@ +package com.tashow.cloud.pay.dal.dataobject.order; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/order/PayOrderExtensionDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/order/PayOrderExtensionDO.java new file mode 100644 index 0000000..9191db7 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/order/PayOrderExtensionDO.java @@ -0,0 +1,96 @@ +package com.tashow.cloud.pay.dal.dataobject.order; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.tashow.cloud.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.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/refund/PayRefundDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/refund/PayRefundDO.java new file mode 100644 index 0000000..546cd32 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/refund/PayRefundDO.java @@ -0,0 +1,160 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/transfer/PayTransferDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/transfer/PayTransferDO.java new file mode 100644 index 0000000..dda157e --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/transfer/PayTransferDO.java @@ -0,0 +1,158 @@ +package com.tashow.cloud.pay.dal.dataobject.transfer; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +// TODO 芋艿:需要详细 review +/** + * 转账单 DO + * + * @author jason + */ +@TableName(value ="pay_transfer", autoResultMap = true) +@KeySequence("pay_transfer_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayTransferDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 转账单号 + * + */ + private String no; + + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + + /** + * 转账渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + + /** + * 转账渠道编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String channelCode; + + // ========== 商户相关字段 ========== + /** + * 商户转账单编号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantTransferId; + + // ========== 转账相关字段 ========== + + /** + * 类型 + * + * 枚举 {@link PayTransferTypeEnum} + */ + private Integer type; + + /** + * 转账标题 + */ + private String subject; + + /** + * 转账金额,单位:分 + */ + private Integer price; + + /** + * 收款人姓名 + */ + private String userName; + + /** + * 转账状态 + * + * 枚举 {@link PayTransferStatusRespEnum} + */ + private Integer status; + + /** + * 订单转账成功时间 + */ + private LocalDateTime successTime; + + // ========== 支付宝转账相关字段 ========== + /** + * 支付宝登录号 + */ + private String alipayLogonId; + + + // ========== 微信转账相关字段 ========== + /** + * 微信 openId + */ + private String openid; + + // ========== 其它字段 ========== + + /** + * 异步通知地址 + */ + private String notifyUrl; + + /** + * 用户 IP + */ + private String userIp; + + /** + * 渠道的额外参数 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map channelExtras; + + /** + * 渠道转账单号 + */ + private String channelTransferNo; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道的错误提示 + */ + private String channelErrorMsg; + + /** + * 渠道的同步/异步通知的内容 + * + */ + private String channelNotifyData; + +} \ No newline at end of file diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletDO.java new file mode 100644 index 0000000..df0f5a2 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletDO.java @@ -0,0 +1,59 @@ +package com.tashow.cloud.pay.dal.dataobject.wallet; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +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; + +/** + * 会员钱包 DO + * + * @author jason + */ +@TableName(value ="pay_wallet") +@KeySequence("pay_wallet_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayWalletDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 用户 id + * + * 关联 MemberUserDO 的 id 编号 + * 关联 AdminUserDO 的 id 编号 + */ + private Long userId; + /** + * 用户类型, 预留 多商户转帐可能需要用到 + * + * 关联 {@link UserTypeEnum} + */ + private Integer userType; + + /** + * 余额,单位分 + */ + private Integer balance; + + /** + * 冻结金额,单位分 + */ + private Integer freezePrice; + + /** + * 累计支出,单位分 + */ + private Integer totalExpense; + /** + * 累计充值,单位分 + */ + private Integer totalRecharge; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletRechargeDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletRechargeDO.java new file mode 100644 index 0000000..df089f5 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletRechargeDO.java @@ -0,0 +1,116 @@ +package com.tashow.cloud.pay.dal.dataobject.wallet; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.refund.PayRefundDO; +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.Data; + +import java.time.LocalDateTime; + +/** + * 会员钱包充值 + */ +@TableName(value ="pay_wallet_recharge") +@KeySequence("pay_wallet_recharge_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayWalletRechargeDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 钱包编号 + * + * 关联 {@link PayWalletDO#getId()} + */ + private Long walletId; + + /** + * 用户实际到账余额 + * + * 例如充 100 送 20,则该值是 120 + */ + private Integer totalPrice; + /** + * 实际支付金额 + */ + private Integer payPrice; + /** + * 钱包赠送金额 + */ + private Integer bonusPrice; + + /** + * 充值套餐编号 + * + * 关联 {@link PayWalletRechargeDO#getPackageId()} 字段 + */ + private Long packageId; + + /** + * 是否已支付 + * + * true - 已支付 + * false - 未支付 + */ + private Boolean payStatus; + + /** + * 支付订单编号 + * + * 关联 {@link PayOrderDO#getId()} + */ + private Long payOrderId; + + /** + * 支付成功的支付渠道 + * + * 冗余 {@link PayOrderDO#getChannelCode()} + */ + private String payChannelCode; + /** + * 订单支付时间 + */ + private LocalDateTime payTime; + + /** + * 支付退款单编号 + * + * 关联 {@link PayRefundDO#getId()} + */ + private Long payRefundId; + + /** + * 退款金额,包含赠送金额 + */ + private Integer refundTotalPrice; + /** + * 退款支付金额 + */ + private Integer refundPayPrice; + + /** + * 退款钱包赠送金额 + */ + private Integer refundBonusPrice; + + /** + * 退款时间 + */ + private LocalDateTime refundTime; + + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusEnum} + */ + private Integer refundStatus; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java new file mode 100644 index 0000000..44790e7 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletRechargePackageDO.java @@ -0,0 +1,47 @@ +package com.tashow.cloud.pay.dal.dataobject.wallet; + +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; + +/** + * 会员钱包充值套餐 DO + * + * 通过充值套餐时,可以赠送一定金额; + * + * @author 芋道源码 + */ +@TableName(value ="pay_wallet_recharge_package") +@KeySequence("pay_wallet_recharge_package_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayWalletRechargePackageDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 套餐名 + */ + private String name; + + /** + * 支付金额 + */ + private Integer payPrice; + /** + * 赠送金额 + */ + private Integer bonusPrice; + + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletTransactionDO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletTransactionDO.java new file mode 100644 index 0000000..d8064d9 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/dataobject/wallet/PayWalletTransactionDO.java @@ -0,0 +1,66 @@ +package com.tashow.cloud.pay.dal.dataobject.wallet; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 会员钱包流水 DO + * + * @author jason + */ +@TableName(value ="pay_wallet_transaction") +@KeySequence("pay_wallet_transaction_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class PayWalletTransactionDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 流水号 + */ + private String no; + + /** + * 钱包编号 + * + * 关联 {@link PayWalletDO#getId()} + */ + private Long walletId; + + /** + * 关联业务分类 + * + * 枚举 {@link PayWalletBizTypeEnum#getType()} + */ + private Integer bizType; + + /** + * 关联业务编号 + */ + private String bizId; + + /** + * 流水说明 + */ + private String title; + + /** + * 交易金额,单位分 + * + * 正值表示余额增加,负值表示余额减少 + */ + private Integer price; + + /** + * 交易后余额,单位分 + */ + private Integer balance; +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/app/PayAppMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/app/PayAppMapper.java new file mode 100644 index 0000000..0200ef6 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/app/PayAppMapper.java @@ -0,0 +1,26 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.app.vo.PayAppPageReqVO; +import com.tashow.cloud.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()) + .likeIfPresent(PayAppDO::getAppKey, reqVO.getAppKey()) + .eqIfPresent(PayAppDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayAppDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayAppDO::getId)); + } + + default PayAppDO selectByAppKey(String appKey) { + return selectOne(PayAppDO::getAppKey, appKey); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/channel/PayChannelMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/channel/PayChannelMapper.java new file mode 100644 index 0000000..e1b99c0 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/channel/PayChannelMapper.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.pay.dal.mysql.channel; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import org.apache.ibatis.annotations.Mapper; + +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)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/demo/PayDemoOrderMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/demo/PayDemoOrderMapper.java new file mode 100644 index 0000000..c49ac37 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/demo/PayDemoOrderMapper.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.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 com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/demo/PayDemoTransferMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/demo/PayDemoTransferMapper.java new file mode 100644 index 0000000..ea6102a --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/demo/PayDemoTransferMapper.java @@ -0,0 +1,17 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.dal.dataobject.demo.PayDemoTransferDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PayDemoTransferMapper extends BaseMapperX { + + default PageResult selectPage(PageParam pageParam){ + return selectPage(pageParam, new LambdaQueryWrapperX() + .orderByDesc(PayDemoTransferDO::getId)); + } +} \ No newline at end of file diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/notify/PayNotifyLogMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/notify/PayNotifyLogMapper.java new file mode 100644 index 0000000..dabdefd --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/notify/PayNotifyLogMapper.java @@ -0,0 +1,16 @@ +package com.tashow.cloud.pay.dal.mysql.notify; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.tashow.cloud.pay.dal.dataobject.notify.PayNotifyLogDO; +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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/notify/PayNotifyTaskMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/notify/PayNotifyTaskMapper.java new file mode 100644 index 0000000..fdb8cb9 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/notify/PayNotifyTaskMapper.java @@ -0,0 +1,44 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/order/PayOrderExtensionMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/order/PayOrderExtensionMapper.java new file mode 100644 index 0000000..ff9b66d --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/order/PayOrderExtensionMapper.java @@ -0,0 +1,38 @@ +package com.tashow.cloud.pay.dal.mysql.order; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.tashow.cloud.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 selectListByOrderIdAndStatus(Long orderId, Integer status) { + return selectList(PayOrderExtensionDO::getOrderId, orderId, + PayOrderExtensionDO::getStatus, status); + } + + default List selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime) { + return selectList(new LambdaQueryWrapper() + .eq(PayOrderExtensionDO::getStatus, status) + .ge(PayOrderExtensionDO::getCreateTime, minCreateTime)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/order/PayOrderMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/order/PayOrderMapper.java new file mode 100644 index 0000000..e949908 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/order/PayOrderMapper.java @@ -0,0 +1,62 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.order.vo.PayOrderExportReqVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderPageReqVO; +import com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/refund/PayRefundMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/refund/PayRefundMapper.java new file mode 100644 index 0000000..a8e383c --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/refund/PayRefundMapper.java @@ -0,0 +1,78 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import com.tashow.cloud.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 PayRefundDO selectByNo(String no) { + return selectOne(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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/transfer/PayTransferMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/transfer/PayTransferMapper.java new file mode 100644 index 0000000..b6f8a8a --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/transfer/PayTransferMapper.java @@ -0,0 +1,53 @@ +package com.tashow.cloud.pay.dal.mysql.transfer; + +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 com.tashow.cloud.pay.controller.admin.transfer.vo.PayTransferPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.transfer.PayTransferDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PayTransferMapper extends BaseMapperX { + + default int updateByIdAndStatus(Long id, List status, PayTransferDO updateObj) { + return update(updateObj, new LambdaQueryWrapper() + .eq(PayTransferDO::getId, id).in(PayTransferDO::getStatus, status)); + } + + default PayTransferDO selectByAppIdAndMerchantTransferId(Long appId, String merchantTransferId){ + return selectOne(PayTransferDO::getAppId, appId, + PayTransferDO::getMerchantTransferId, merchantTransferId); + } + + default PageResult selectPage(PayTransferPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayTransferDO::getNo, reqVO.getNo()) + .eqIfPresent(PayTransferDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayTransferDO::getChannelCode, reqVO.getChannelCode()) + .eqIfPresent(PayTransferDO::getMerchantTransferId, reqVO.getMerchantTransferId()) + .eqIfPresent(PayTransferDO::getType, reqVO.getType()) + .eqIfPresent(PayTransferDO::getStatus, reqVO.getStatus()) + .likeIfPresent(PayTransferDO::getUserName, reqVO.getUserName()) + .eqIfPresent(PayTransferDO::getChannelTransferNo, reqVO.getChannelTransferNo()) + .betweenIfPresent(PayTransferDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayTransferDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(PayTransferDO::getStatus, status); + } + + default PayTransferDO selectByAppIdAndNo(Long appId, String no) { + return selectOne(PayTransferDO::getAppId, appId, + PayTransferDO::getNo, no); + } + +} + + + + diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletMapper.java new file mode 100644 index 0000000..76e75ba --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletMapper.java @@ -0,0 +1,134 @@ +package com.tashow.cloud.pay.dal.mysql.wallet; + + +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 com.tashow.cloud.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PayWalletMapper extends BaseMapperX { + + default PayWalletDO selectByUserIdAndType(Long userId, Integer userType) { + return selectOne(PayWalletDO::getUserId, userId, + PayWalletDO::getUserType, userType); + } + + default PageResult selectPage(PayWalletPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayWalletDO::getUserId, reqVO.getUserId()) + .eqIfPresent(PayWalletDO::getUserType, reqVO.getUserType()) + .betweenIfPresent(PayWalletDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayWalletDO::getId)); + } + + /** + * 当消费退款时候, 更新钱包 + * + * @param id 钱包 id + * @param price 消费金额 + */ + default int updateWhenConsumptionRefund(Long id, Integer price) { + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance + " + price + + ", total_expense = total_expense - " + price) + .eq(PayWalletDO::getId, id); + return update(null, lambdaUpdateWrapper); + } + + /** + * 当消费时候, 更新钱包 + * + * @param price 消费金额 + * @param id 钱包 id + */ + default int updateWhenConsumption(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance - " + price + + ", total_expense = total_expense + " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getBalance, price); // cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + + /** + * 当充值的时候,更新钱包 + * + * @param id 钱包 id + * @param price 钱包金额 + */ + default int updateWhenRecharge(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance + " + price + + ", total_recharge = total_recharge + " + price) + .eq(PayWalletDO::getId, id); + return update(null, lambdaUpdateWrapper); + } + + /** + * 增加余额的时候,更新钱包 + * + * @param id 钱包 id + * @param price 钱包金额 + */ + default void updateWhenAdd(Long id, Integer price) { + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance + " + price) + .eq(PayWalletDO::getId, id); + update(null, lambdaUpdateWrapper); + } + + /** + * 冻结钱包部分余额 + * + * @param id 钱包 id + * @param price 冻结金额 + */ + default int freezePrice(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance - " + price + + ", freeze_price = freeze_price + " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getBalance, price); // cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + + /** + * 解冻钱包余额 + * + * @param id 钱包 id + * @param price 解冻金额 + */ + default int unFreezePrice(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" balance = balance + " + price + + ", freeze_price = freeze_price - " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getFreezePrice, price); // cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + + /** + * 当充值退款时, 更新钱包 + * + * @param id 钱包 id + * @param price 退款金额 + */ + default int updateWhenRechargeRefund(Long id, Integer price){ + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" freeze_price = freeze_price - " + price + + ", total_recharge = total_recharge - " + price) + .eq(PayWalletDO::getId, id) + .ge(PayWalletDO::getFreezePrice, price) + .ge(PayWalletDO::getTotalRecharge, price);// cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + +} + + + + diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletRechargeMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletRechargeMapper.java new file mode 100644 index 0000000..174d6da --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletRechargeMapper.java @@ -0,0 +1,30 @@ +package com.tashow.cloud.pay.dal.mysql.wallet; + +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 com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PayWalletRechargeMapper extends BaseMapperX { + + default int updateByIdAndPaid(Long id, boolean wherePayStatus, PayWalletRechargeDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(PayWalletRechargeDO::getId, id).eq(PayWalletRechargeDO::getPayStatus, wherePayStatus)); + } + + default int updateByIdAndRefunded(Long id, Integer whereRefundStatus, PayWalletRechargeDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(PayWalletRechargeDO::getId, id).eq(PayWalletRechargeDO::getRefundStatus, whereRefundStatus)); + } + + default PageResult selectPage(PageParam pageReqVO, Long walletId, Boolean payStatus) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eq(PayWalletRechargeDO::getWalletId, walletId) + .eq(PayWalletRechargeDO::getPayStatus, payStatus) + .orderByDesc(PayWalletRechargeDO::getId)); + } + +} \ No newline at end of file diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletRechargePackageMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletRechargePackageMapper.java new file mode 100644 index 0000000..ebd17c8 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletRechargePackageMapper.java @@ -0,0 +1,32 @@ +package com.tashow.cloud.pay.dal.mysql.wallet; + + +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 com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackagePageReqVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PayWalletRechargePackageMapper extends BaseMapperX { + + default PageResult selectPage(WalletRechargePackagePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(PayWalletRechargePackageDO::getName, reqVO.getName()) + .eqIfPresent(PayWalletRechargePackageDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayWalletRechargePackageDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayWalletRechargePackageDO::getPayPrice)); + } + + default PayWalletRechargePackageDO selectByName(String name) { + return selectOne(PayWalletRechargePackageDO::getName, name); + } + + default List selectListByStatus(Integer status) { + return selectList(PayWalletRechargePackageDO::getStatus, status); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletTransactionMapper.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletTransactionMapper.java new file mode 100644 index 0000000..0d9210e --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/mysql/wallet/PayWalletTransactionMapper.java @@ -0,0 +1,65 @@ +package com.tashow.cloud.pay.dal.mysql.wallet; + + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +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.framework.mybatis.core.query.QueryWrapperX; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO.TYPE_EXPENSE; +import static com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO.TYPE_INCOME; + +@Mapper +public interface PayWalletTransactionMapper extends BaseMapperX { + + default PageResult selectPage(Long walletId, Integer type, + PageParam pageParam, LocalDateTime[] createTime) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .eqIfPresent(PayWalletTransactionDO::getWalletId, walletId); + if (Objects.equals(type, TYPE_INCOME)) { + query.gt(PayWalletTransactionDO::getPrice, 0); + } else if (Objects.equals(type, TYPE_EXPENSE)) { + query.lt(PayWalletTransactionDO::getPrice, 0); + } + query.betweenIfPresent(PayWalletTransactionDO::getCreateTime, createTime); + query.orderByDesc(PayWalletTransactionDO::getId); + return selectPage(pageParam, query); + } + + default Integer selectPriceSum(Long walletId, Integer type, LocalDateTime[] createTime) { + // SQL sum 查询 + List> result = selectMaps(new QueryWrapperX() + .select("SUM(price) AS priceSum") + .gt(Objects.equals(type, TYPE_INCOME), "price", 0) // 收入 + .lt(Objects.equals(type, TYPE_EXPENSE), "price", 0) // 支出 + .eq("wallet_id", walletId) + .between("create_time", createTime[0], createTime[1])); + // 获得 sum 结果 + Map first = CollUtil.getFirst(result); + return MapUtil.getInt(first, "priceSum", 0); + } + + default PayWalletTransactionDO selectByNo(String no) { + return selectOne(PayWalletTransactionDO::getNo, no); + } + + default PayWalletTransactionDO selectByBiz(String bizId, Integer bizType) { + return selectOne(PayWalletTransactionDO::getBizId, bizId, + PayWalletTransactionDO::getBizType, bizType); + } + +} + + + + diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/RedisKeyConstants.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/RedisKeyConstants.java new file mode 100644 index 0000000..168eb8f --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/RedisKeyConstants.java @@ -0,0 +1,36 @@ +package com.tashow.cloud.pay.dal.redis; + +/** + * 支付 Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 通知任务的分布式锁 + * + * KEY 格式:pay_notify:lock:%d // 参数来自 DefaultLockKeyBuilder 类 + * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构 + * 过期时间:不固定 + */ + String PAY_NOTIFY_LOCK = "pay_notify:lock:%d"; + + /** + * 支付钱包的分布式锁 + * + * KEY 格式:pay_wallet:lock:%d + * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构 + * 过期时间:不固定 + */ + String PAY_WALLET_LOCK = "pay_wallet:lock:%d"; + + /** + * 支付序号的缓存 + * + * KEY 格式:pay_no:{prefix} + * VALUE 数据格式:编号自增 + */ + String PAY_NO = "pay_no:"; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/no/PayNoRedisDAO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/no/PayNoRedisDAO.java new file mode 100644 index 0000000..77083b3 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/no/PayNoRedisDAO.java @@ -0,0 +1,40 @@ +package com.tashow.cloud.pay.dal.redis.no; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import com.tashow.cloud.pay.dal.redis.RedisKeyConstants; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +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); + String key = RedisKeyConstants.PAY_NO + noPrefix; + Long no = stringRedisTemplate.opsForValue().increment(key); + // 设置过期时间 + stringRedisTemplate.expire(key, Duration.ofMinutes(1L)); + return noPrefix + no; + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/notify/PayNotifyLockRedisDAO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/notify/PayNotifyLockRedisDAO.java new file mode 100644 index 0000000..affec36 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/notify/PayNotifyLockRedisDAO.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.pay.dal.redis.notify; + +import jakarta.annotation.Resource; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Repository; + +import java.util.concurrent.TimeUnit; + +import static com.tashow.cloud.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, id); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/wallet/PayWalletLockRedisDAO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/wallet/PayWalletLockRedisDAO.java new file mode 100644 index 0000000..33d5a7f --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/dal/redis/wallet/PayWalletLockRedisDAO.java @@ -0,0 +1,42 @@ +package com.tashow.cloud.pay.dal.redis.wallet; + +import jakarta.annotation.Resource; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Repository; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static com.tashow.cloud.pay.dal.redis.RedisKeyConstants.PAY_WALLET_LOCK; + +/** + * 支付钱包的锁 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class PayWalletLockRedisDAO { + + @Resource + private RedissonClient redissonClient; + + public V lock(Long id, Long timeoutMillis, Callable callable) throws Exception { + String lockKey = formatKey(id); + RLock lock = redissonClient.getLock(lockKey); + try { + lock.lock(timeoutMillis, TimeUnit.MILLISECONDS); + // 执行逻辑 + return callable.call(); + } catch (Exception e) { + throw e; + } finally { + lock.unlock(); + } + } + + private static String formatKey(Long id) { + return String.format(PAY_WALLET_LOCK, id); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/job/config/PayJobConfiguration.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/job/config/PayJobConfiguration.java new file mode 100644 index 0000000..e849b22 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/job/config/PayJobConfiguration.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/job/core/package-info.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/job/core/package-info.java new file mode 100644 index 0000000..279fbaf --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/job/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.tashow.cloud.pay.framework.job.core; diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/package-info.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/package-info.java new file mode 100644 index 0000000..d8641a4 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 pay 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.tashow.cloud.pay.framework; diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/pay/config/PayConfiguration.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/pay/config/PayConfiguration.java new file mode 100644 index 0000000..49ad8ab --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/pay/config/PayConfiguration.java @@ -0,0 +1,9 @@ +package com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/pay/config/PayProperties.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/pay/config/PayProperties.java new file mode 100644 index 0000000..4dc1b68 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/pay/config/PayProperties.java @@ -0,0 +1,69 @@ +package com.tashow.cloud.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 jakarta.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"; + + private static final String WALLET_PAY_APP_KEY_DEFAULT = "wallet"; + + /** + * 支付回调地址 + * + * 实际上,对应的 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; + + /** + * 转账回调地址 + * + * 实际上,对应的 PayNotifyController 的 notifyTransfer 方法的 URL + * + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 transferNotifyUrl 地址 => 业务的 PayAppDO.transferNotifyUrl 地址 + */ + private String transferNotifyUrl; + + /** + * 支付订单 no 的前缀 + */ + @NotEmpty(message = "支付订单 no 的前缀不能为空") + private String orderNoPrefix = ORDER_NO_PREFIX; + + /** + * 退款订单 no 的前缀 + */ + @NotEmpty(message = "退款订单 no 的前缀不能为空") + private String refundNoPrefix = REFUND_NO_PREFIX; + + /** + * 钱包支付应用 AppKey + */ + @NotEmpty(message = "钱包支付应用 AppKey 不能为空") + private String walletPayAppKey = WALLET_PAY_APP_KEY_DEFAULT; + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/pay/core/WalletPayClient.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/pay/core/WalletPayClient.java new file mode 100644 index 0000000..ba32bbd --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/pay/core/WalletPayClient.java @@ -0,0 +1,195 @@ +package com.tashow.cloud.pay.framework.pay.core; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +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.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig; +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.pay.core.enums.transfer.PayTransferTypeEnum; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderExtensionDO; +import com.tashow.cloud.pay.dal.dataobject.refund.PayRefundDO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.tashow.cloud.pay.service.refund.PayRefundService; +import com.tashow.cloud.pay.service.wallet.PayWalletService; +import com.tashow.cloud.pay.service.wallet.PayWalletTransactionService; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.REFUND_NOT_FOUND; + +/** + * 钱包支付的 PayClient 实现类 + * + * @author jason + */ +@Slf4j +public class WalletPayClient extends AbstractPayClient { + + public static final String USER_ID_KEY = "user_id"; + public static final String USER_TYPE_KEY = "user_type"; + + private PayWalletService wallService; + private PayWalletTransactionService walletTransactionService; + private PayOrderService orderService; + private PayRefundService refundService; + + public WalletPayClient(Long channelId, NonePayClientConfig config) { + super(channelId, PayChannelEnum.WALLET.getCode(), config); + } + + @Override + protected void doInit() { + if (wallService == null) { + wallService = SpringUtil.getBean(PayWalletService.class); + } + if (walletTransactionService == null) { + walletTransactionService = SpringUtil.getBean(PayWalletTransactionService.class); + } + } + + @Override + protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + try { + Long userId = MapUtil.getLong(reqDTO.getChannelExtras(), USER_ID_KEY); + Integer userType = MapUtil.getInt(reqDTO.getChannelExtras(), USER_TYPE_KEY); + Assert.notNull(userId, "用户 id 不能为空"); + Assert.notNull(userType, "用户类型不能为空"); + PayWalletTransactionDO transaction = wallService.orderPay(userId, userType, reqDTO.getOutTradeNo(), + reqDTO.getPrice()); + return PayOrderRespDTO.successOf(transaction.getNo(), transaction.getCreator(), + transaction.getCreateTime(), + reqDTO.getOutTradeNo(), transaction); + } catch (Throwable ex) { + log.error("[doUnifiedOrder] 失败", ex); + Integer errorCode = INTERNAL_SERVER_ERROR.getCode(); + String errorMsg = INTERNAL_SERVER_ERROR.getMsg(); + if (ex instanceof ServiceException) { + ServiceException serviceException = (ServiceException) ex; + errorCode = serviceException.getCode(); + errorMsg = serviceException.getMessage(); + } + return PayOrderRespDTO.closedOf(String.valueOf(errorCode), errorMsg, + reqDTO.getOutTradeNo(), ""); + } + } + + @Override + protected PayOrderRespDTO doParseOrderNotify(Map params, String body) { + throw new UnsupportedOperationException("钱包支付无支付回调"); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) { + if (orderService == null) { + orderService = SpringUtil.getBean(PayOrderService.class); + } + PayOrderExtensionDO orderExtension = orderService.getOrderExtensionByNo(outTradeNo); + // 支付交易拓展单不存在, 返回关闭状态 + if (orderExtension == null) { + return PayOrderRespDTO.closedOf(String.valueOf(PAY_ORDER_EXTENSION_NOT_FOUND.getCode()), + PAY_ORDER_EXTENSION_NOT_FOUND.getMsg(), outTradeNo, ""); + } + // 关闭状态 + if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { + return PayOrderRespDTO.closedOf(orderExtension.getChannelErrorCode(), + orderExtension.getChannelErrorMsg(), outTradeNo, ""); + } + // 成功状态 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction( + String.valueOf(orderExtension.getOrderId()), PayWalletBizTypeEnum.PAYMENT); + Assert.notNull(walletTransaction, "支付单 {} 钱包流水不能为空", outTradeNo); + return PayOrderRespDTO.successOf(walletTransaction.getNo(), walletTransaction.getCreator(), + walletTransaction.getCreateTime(), outTradeNo, walletTransaction); + } + // 其它状态为无效状态 + log.error("[doGetOrder] 支付单 {} 的状态不正确", outTradeNo); + throw new IllegalStateException(String.format("支付单[%s] 状态不正确", outTradeNo)); + } + + @Override + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + try { + PayWalletTransactionDO payWalletTransaction = wallService.orderRefund(reqDTO.getOutRefundNo(), + reqDTO.getRefundPrice(), reqDTO.getReason()); + return PayRefundRespDTO.successOf(payWalletTransaction.getNo(), payWalletTransaction.getCreateTime(), + reqDTO.getOutRefundNo(), payWalletTransaction); + } catch (Throwable ex) { + log.error("[doUnifiedRefund] 失败", ex); + Integer errorCode = INTERNAL_SERVER_ERROR.getCode(); + String errorMsg = INTERNAL_SERVER_ERROR.getMsg(); + if (ex instanceof ServiceException) { + ServiceException serviceException = (ServiceException) ex; + errorCode = serviceException.getCode(); + errorMsg = serviceException.getMessage(); + } + return PayRefundRespDTO.failureOf(String.valueOf(errorCode), errorMsg, + reqDTO.getOutRefundNo(), ""); + } + } + + @Override + protected PayRefundRespDTO doParseRefundNotify(Map params, String body) { + throw new UnsupportedOperationException("钱包支付无退款回调"); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) { + if (refundService == null) { + refundService = SpringUtil.getBean(PayRefundService.class); + } + PayRefundDO payRefund = refundService.getRefundByNo(outRefundNo); + // 支付退款单不存在, 返回退款失败状态 + if (payRefund == null) { + return PayRefundRespDTO.failureOf(String.valueOf(REFUND_NOT_FOUND), REFUND_NOT_FOUND.getMsg(), + outRefundNo, ""); + } + // 退款失败 + if (PayRefundStatusRespEnum.isFailure(payRefund.getStatus())) { + return PayRefundRespDTO.failureOf(payRefund.getChannelErrorCode(), payRefund.getChannelErrorMsg(), + outRefundNo, ""); + } + // 退款成功 + if (PayRefundStatusRespEnum.isSuccess(payRefund.getStatus())) { + PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction( + String.valueOf(payRefund.getId()), PayWalletBizTypeEnum.PAYMENT_REFUND); + Assert.notNull(walletTransaction, "支付退款单 {} 钱包流水不能为空", outRefundNo); + return PayRefundRespDTO.successOf(walletTransaction.getNo(), walletTransaction.getCreateTime(), + outRefundNo, walletTransaction); + } + // 其它状态为无效状态 + log.error("[doGetRefund] 支付退款单 {} 的状态不正确", outRefundNo); + throw new IllegalStateException(String.format("支付退款单[%s] 状态不正确", outRefundNo)); + } + + @Override + protected PayTransferRespDTO doParseTransferNotify(Map params, String body) throws Throwable { + throw new UnsupportedOperationException("未实现"); + } + + @Override + public PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { + throw new UnsupportedOperationException("待实现"); + } + + @Override + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) { + throw new UnsupportedOperationException("待实现"); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/rpc/config/RpcConfiguration.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/rpc/config/RpcConfiguration.java new file mode 100644 index 0000000..0aa0715 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/rpc/config/RpcConfiguration.java @@ -0,0 +1,10 @@ +package com.tashow.cloud.pay.framework.rpc.config; + +import cn.iocoder.yudao.module.system.api.social.SocialClientApi; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableFeignClients(clients = {SocialClientApi.class}) +public class RpcConfiguration { +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/rpc/package-info.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/rpc/package-info.java new file mode 100644 index 0000000..849d767 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/rpc/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.tashow.cloud.pay.framework.rpc; diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/security/config/SecurityConfiguration.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/security/config/SecurityConfiguration.java new file mode 100644 index 0000000..54fc80b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.pay.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import cn.iocoder.yudao.module.pay.enums.ApiConstants; +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.AuthorizeHttpRequestsConfigurer; + +/** + * Pay 模块的 Security 配置 + */ +@Configuration("paySecurityConfiguration") +public class SecurityConfiguration { + + @Bean("payAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + // Swagger 接口文档 + registry.requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers("/webjars/**").permitAll() + .requestMatchers("/swagger-ui").permitAll() + .requestMatchers("/swagger-ui/**").permitAll(); + // Spring Boot Actuator 的安全配置 + registry.requestMatchers("/actuator").permitAll() + .requestMatchers("/actuator/**").permitAll(); + // Druid 监控 + registry.requestMatchers("/druid/**").permitAll(); + // RPC 服务的安全配置 + registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); + } + + }; + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/security/core/package-info.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/security/core/package-info.java new file mode 100644 index 0000000..2336c92 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.tashow.cloud.pay.framework.security.core; diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/notify/PayNotifyJob.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/notify/PayNotifyJob.java new file mode 100644 index 0000000..ff983e0 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/notify/PayNotifyJob.java @@ -0,0 +1,32 @@ +package com.tashow.cloud.pay.job.notify; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import com.tashow.cloud.pay.service.notify.PayNotifyService; +import com.xxl.job.core.handler.annotation.XxlJob; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 支付通知 Job + * 通过不断扫描待通知的 PayNotifyTaskDO 记录,回调业务线的回调接口 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class PayNotifyJob { + + @Resource + private PayNotifyService payNotifyService; + + @XxlJob("payNotifyJob") + @TenantJob // 多租户 + public String execute() throws Exception { + int notifyCount = payNotifyService.executeNotify(); + log.info("[execute][执行支付通知 ({}) 个]", notifyCount); + return StrUtil.format("执行支付通知 ({}) 个",notifyCount); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/order/PayOrderExpireJob.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/order/PayOrderExpireJob.java new file mode 100644 index 0000000..2df0ca3 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/order/PayOrderExpireJob.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.pay.job.order; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.xxl.job.core.handler.annotation.XxlJob; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 支付订单的过期 Job + * + * 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class PayOrderExpireJob { + + @Resource + private PayOrderService orderService; + + @XxlJob("payOrderExpireJob") + @TenantJob // 多租户 + public String execute(String param) { + int count = orderService.expireOrder(); + log.info("[execute][支付过期 ({}) 个]", count); + return StrUtil.format("支付过期 ({}) 个",count); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/order/PayOrderSyncJob.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/order/PayOrderSyncJob.java new file mode 100644 index 0000000..6291bbd --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/order/PayOrderSyncJob.java @@ -0,0 +1,46 @@ +package com.tashow.cloud.pay.job.order; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.xxl.job.core.handler.annotation.XxlJob; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * 支付订单的同步 Job + * + * 由于支付订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 + * + * @author 芋道源码 + */ +@Component +@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") + @TenantJob // 多租户 + public String execute() { + LocalDateTime minCreateTime = LocalDateTime.now().minus(CREATE_TIME_DURATION_BEFORE); + int count = orderService.syncOrder(minCreateTime); + log.info("[execute][同步支付订单 ({}) 个]", count); + return StrUtil.format("同步支付订单 ({}) 个",count); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/refund/PayRefundSyncJob.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/refund/PayRefundSyncJob.java new file mode 100644 index 0000000..fd45031 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/refund/PayRefundSyncJob.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.pay.job.refund; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import com.tashow.cloud.pay.service.refund.PayRefundService; +import com.xxl.job.core.handler.annotation.XxlJob; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 退款订单的同步 Job + * + * 由于退款订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class PayRefundSyncJob { + + @Resource + private PayRefundService refundService; + + @XxlJob("payRefundSyncJob") + @TenantJob // 多租户 + public String execute() { + int count = refundService.syncRefund(); + log.info("[execute][同步退款订单 ({}) 个]", count); + return StrUtil.format("同步退款订单 ({}) 个",count); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/transfer/PayTransferSyncJob.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/transfer/PayTransferSyncJob.java new file mode 100644 index 0000000..420b108 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/job/transfer/PayTransferSyncJob.java @@ -0,0 +1,32 @@ +package com.tashow.cloud.pay.job.transfer; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import com.tashow.cloud.pay.service.transfer.PayTransferService; +import com.xxl.job.core.handler.annotation.XxlJob; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 转账订单的同步 Job + * + * 由于转账订单的转账结果,有些渠道是异步通知进行同步的,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 + * + * @author jason + */ +@Component +@Slf4j +public class PayTransferSyncJob { + + @Resource + private PayTransferService transferService; + + @XxlJob("payTransferSyncJob") + @TenantJob // 多租户 + public String execute(String param) { + int count = transferService.syncTransfer(); + log.info("[execute][同步转账订单 ({}) 个]", count); + return StrUtil.format("同步转账订单 ({}) 个",count); + } +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/package-info.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/package-info.java new file mode 100644 index 0000000..e7fbd6d --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/package-info.java @@ -0,0 +1,10 @@ +/** + * pay 模块,我们放支付业务,提供业务的支付能力。 + * 例如说:商户、应用、支付、退款等等 + * + * 1. Controller URL:以 /pay/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 pay_ 开头,方便在数据库中区分 + * + * 注意,由于 Pay 模块和 Trade 模块,容易重名,所以类名都加载 Pay 的前缀~ + */ +package com.tashow.cloud.pay; diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/app/PayAppService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/app/PayAppService.java new file mode 100644 index 0000000..013ffc5 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/app/PayAppService.java @@ -0,0 +1,115 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.app.vo.PayAppCreateReqVO; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppPageReqVO; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import jakarta.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); + + /** + * 支付应用的合法性 + *

+ * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param appKey 应用标识 + * @return 应用 + */ + PayAppDO validPayApp(String appKey); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/app/PayAppServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/app/PayAppServiceImpl.java new file mode 100644 index 0000000..a9d4c27 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/app/PayAppServiceImpl.java @@ -0,0 +1,162 @@ +package com.tashow.cloud.pay.service.app; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppCreateReqVO; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppPageReqVO; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import com.tashow.cloud.pay.convert.app.PayAppConvert; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.mysql.app.PayAppMapper; +import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.tashow.cloud.pay.service.refund.PayRefundService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +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) { + // 验证 appKey 是否重复 + validateAppKeyUnique(null, createReqVO.getAppKey()); + + // 插入 + PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO); + appMapper.insert(app); + // 返回 + return app.getId(); + } + + @Override + public void updateApp(PayAppUpdateReqVO updateReqVO) { + // 校验存在 + validateAppExists(updateReqVO.getId()); + // 验证 appKey 是否重复 + validateAppKeyUnique(updateReqVO.getId(), updateReqVO.getAppKey()); + + // 更新 + PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO); + appMapper.updateById(updateObj); + } + + void validateAppKeyUnique(Long id, String appKey) { + PayAppDO app = appMapper.selectByAppKey(appKey); + if (app == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 appKey 的应用 + if (id == null) { + throw exception(APP_KEY_EXISTS); + } + if (!app.getId().equals(id)) { + throw exception(APP_KEY_EXISTS); + } + } + + @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 appId) { + PayAppDO app = appMapper.selectById(appId); + return validatePayApp(app); + } + + @Override + public PayAppDO validPayApp(String appKey) { + PayAppDO app = appMapper.selectByAppKey(appKey); + return validatePayApp(app); + } + + /** + * 校验支付应用实体的有效性:存在 + 开启 + * + * @param app 待校验的支付应用实体 + * @return 校验通过的支付应用实体 + */ + private PayAppDO validatePayApp(PayAppDO app) { + // 校验是否存在 + if (app == null) { + throw exception(ErrorCodeConstants.APP_NOT_FOUND); + } + // 校验是否禁用 + if (CommonStatusEnum.isDisable(app.getStatus())) { + throw exception(ErrorCodeConstants.APP_IS_DISABLE); + } + return app; + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/channel/PayChannelService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/channel/PayChannelService.java new file mode 100644 index 0000000..9b92d48 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/channel/PayChannelService.java @@ -0,0 +1,104 @@ +package com.tashow.cloud.pay.service.channel; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import jakarta.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); + + /** + * 获得指定编号的支付客户端 + * + * @param id 编号 + * @return 支付客户端 + */ + PayClient getPayClient(Long id); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/channel/PayChannelServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/channel/PayChannelServiceImpl.java new file mode 100644 index 0000000..f5e5fe2 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/channel/PayChannelServiceImpl.java @@ -0,0 +1,170 @@ +package com.tashow.cloud.pay.service.channel; + +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.json.JsonUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +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 com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import com.tashow.cloud.pay.convert.channel.PayChannelConvert; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.pay.dal.mysql.channel.PayChannelMapper; +import com.tashow.cloud.pay.framework.pay.core.WalletPayClient; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import jakarta.validation.Validator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +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 +@Slf4j +@Validated +public class PayChannelServiceImpl implements PayChannelService { + + @Resource + private PayClientFactory payClientFactory; + + @Resource + private PayChannelMapper payChannelMapper; + + @Resource + private Validator validator; + + /** + * 初始化,为了注册钱包 + */ + @PostConstruct + public void init() { + payClientFactory.registerPayClientClass(PayChannelEnum.WALLET, WalletPayClient.class); + } + + @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())); + payChannelMapper.insert(channel); + 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())); + payChannelMapper.updateById(channel); + } + + /** + * 解析并校验配置 + * + * @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); + + // 删除 + payChannelMapper.deleteById(id); + } + + private PayChannelDO validateChannelExists(Long id) { + PayChannelDO channel = payChannelMapper.selectById(id); + if (channel == null) { + throw exception(CHANNEL_NOT_FOUND); + } + return channel; + } + + @Override + public PayChannelDO getChannel(Long id) { + return payChannelMapper.selectById(id); + } + + @Override + public List getChannelListByAppIds(Collection appIds) { + return payChannelMapper.selectListByAppIds(appIds); + } + + @Override + public PayChannelDO getChannelByAppIdAndCode(Long appId, String code) { + return payChannelMapper.selectByAppIdAndCode(appId, code); + } + + @Override + public PayChannelDO validPayChannel(Long id) { + PayChannelDO channel = payChannelMapper.selectById(id); + validPayChannel(channel); + return channel; + } + + @Override + public PayChannelDO validPayChannel(Long appId, String code) { + PayChannelDO channel = payChannelMapper.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 payChannelMapper.selectListByAppId(appId, CommonStatusEnum.ENABLE.getStatus()); + } + + @Override + public PayClient getPayClient(Long id) { + PayChannelDO channel = validPayChannel(id); + return payClientFactory.createOrUpdatePayClient(id, channel.getCode(), channel.getConfig()); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoOrderService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoOrderService.java new file mode 100644 index 0000000..8648e34 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoOrderService.java @@ -0,0 +1,65 @@ +package com.tashow.cloud.pay.service.demo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO; +import com.tashow.cloud.pay.dal.dataobject.demo.PayDemoOrderDO; +import jakarta.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoOrderServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoOrderServiceImpl.java new file mode 100644 index 0000000..4d0aff6 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoOrderServiceImpl.java @@ -0,0 +1,264 @@ +package com.tashow.cloud.pay.service.demo; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +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 com.tashow.cloud.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO; +import com.tashow.cloud.pay.dal.dataobject.demo.PayDemoOrderDO; +import com.tashow.cloud.pay.dal.mysql.demo.PayDemoOrderMapper; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +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.notEqual; +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 String PAY_APP_KEY = "demo"; + + /** + * 商品信息 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() + .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用 + .setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号 + .setSubject(spuName).setBody("").setPrice(price) // 价格信息 + .setExpireTime(addTime(Duration.ofHours(2L)))).getCheckedData(); // 支付的过期时间 + // 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) { + // 1.1 校验订单是否存在 + PayDemoOrderDO order = payDemoOrderMapper.selectById(id); + if (order == null) { + log.error("[updateDemoOrderPaid][order({}) payOrder({}) 不存在订单,请进行处理!]", id, payOrderId); + throw exception(DEMO_ORDER_NOT_FOUND); + } + // 1.2 校验订单已支付 + if (order.getPayStatus()) { + // 特殊:如果订单已支付,且支付单号相同,直接返回,说明重复回调 + if (ObjectUtil.equals(order.getPayOrderId(), payOrderId)) { + log.warn("[updateDemoOrderPaid][order({}) 已支付,且支付单号相同({}),直接返回]", order, payOrderId); + return; + } + // 异常:支付单号不同,说明支付单号错误 + log.error("[updateDemoOrderPaid][order({}) 支付单不匹配({}),请进行处理!order 数据是:{}]", + order, payOrderId, toJsonString(order)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + + // 2. 校验支付订单的合法性 + PayOrderRespDTO payOrder = validatePayOrderPaid(order, payOrderId); + + // 3. 更新 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); + } + } + + /** + * 校验支付订单的合法性 + * + * @param order 交易订单 + * @param payOrderId 支付订单编号 + * @return 支付订单 + */ + private PayOrderRespDTO validatePayOrderPaid(PayDemoOrderDO order, Long payOrderId) { + // 1. 校验支付单是否存在 + PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId).getCheckedData(); + if (payOrder == null) { + log.error("[validatePayOrderPaid][order({}) payOrder({}) 不存在,请进行处理!]", order.getId(), payOrderId); + throw exception(PAY_ORDER_NOT_FOUND); + } + // 2.1 校验支付单已支付 + if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) { + log.error("[validatePayOrderPaid][order({}) payOrder({}) 未支付,请进行处理!payOrder 数据是:{}]", + order.getId(), payOrderId, toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS); + } + // 2.1 校验支付金额一致 + if (notEqual(payOrder.getPrice(), order.getPrice())) { + log.error("[validatePayOrderPaid][order({}) payOrder({}) 支付金额不匹配,请进行处理!order 数据是:{},payOrder 数据是:{}]", + order.getId(), payOrderId, toJsonString(order), toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH); + } + // 2.2 校验支付订单匹配(二次) + if (notEqual(payOrder.getMerchantOrderId(), order.getId().toString())) { + log.error("[validatePayOrderPaid][order({}) 支付单不匹配({}),请进行处理!payOrder 数据是:{}]", + order.getId(), 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() + .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用 + .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 + .setMerchantRefundId(refundId) + .setReason("想退钱").setPrice(order.getPrice())).getCheckedData();// 价格信息 + // 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.getPayRefundId(), 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).getCheckedData(); + 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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoTransferService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoTransferService.java new file mode 100644 index 0000000..91bf40a --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoTransferService.java @@ -0,0 +1,38 @@ +package com.tashow.cloud.pay.service.demo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO; +import com.tashow.cloud.pay.dal.dataobject.demo.PayDemoTransferDO; +import jakarta.validation.Valid; + +/** + * 示例转账业务 Service 接口 + * + * @author jason + */ +public interface PayDemoTransferService { + + /** + * 创建转账业务示例订单 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemoTransfer(@Valid PayDemoTransferCreateReqVO createReqVO); + + /** + * 获得转账业务示例订单分页 + * + * @param pageVO 分页查询参数 + */ + PageResult getDemoTransferPage(PageParam pageVO); + + /** + * 更新转账业务示例订单的转账状态 + * + * @param id 编号 + * @param payTransferId 转账单编号 + */ + void updateDemoTransferStatus(Long id, Long payTransferId); +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoTransferServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoTransferServiceImpl.java new file mode 100644 index 0000000..e075533 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/demo/PayDemoTransferServiceImpl.java @@ -0,0 +1,99 @@ +package com.tashow.cloud.pay.service.demo; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO; +import com.tashow.cloud.pay.convert.demo.PayDemoTransferConvert; +import com.tashow.cloud.pay.dal.dataobject.demo.PayDemoTransferDO; +import com.tashow.cloud.pay.dal.dataobject.transfer.PayTransferDO; +import com.tashow.cloud.pay.dal.mysql.demo.PayDemoTransferMapper; +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum; +import com.tashow.cloud.pay.service.transfer.PayTransferService; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.WAITING; + +/** + * 示例转账业务 Service 实现类 + * + * @author jason + */ +@Service +@Validated +public class PayDemoTransferServiceImpl implements PayDemoTransferService { + + /** + * 接入的实力应用编号 + + * 从 [支付管理 -> 应用信息] 里添加 + */ + private static final Long TRANSFER_APP_ID = 8L; + @Resource + private PayDemoTransferMapper demoTransferMapper; + @Resource + private PayTransferService payTransferService; + @Resource + private Validator validator; + + @Override + public Long createDemoTransfer(@Valid PayDemoTransferCreateReqVO vo) { + // 1 校验参数 + vo.validate(validator); + // 2 保存示例转账业务表 + PayDemoTransferDO demoTransfer = PayDemoTransferConvert.INSTANCE.convert(vo) + .setAppId(TRANSFER_APP_ID).setTransferStatus(WAITING.getStatus()); + demoTransferMapper.insert(demoTransfer); + return demoTransfer.getId(); + } + + @Override + public PageResult getDemoTransferPage(PageParam pageVO) { + return demoTransferMapper.selectPage(pageVO); + } + + @Override + public void updateDemoTransferStatus(Long id, Long payTransferId) { + PayTransferDO payTransfer = validateDemoTransferStatusCanUpdate(id, payTransferId); + // 更新示例订单状态 + if (payTransfer != null) { + demoTransferMapper.updateById(new PayDemoTransferDO().setId(id) + .setPayTransferId(payTransferId) + .setPayChannelCode(payTransfer.getChannelCode()) + .setTransferStatus(payTransfer.getStatus()) + .setTransferTime(payTransfer.getSuccessTime())); + } + } + + private PayTransferDO validateDemoTransferStatusCanUpdate(Long id, Long payTransferId) { + PayDemoTransferDO demoTransfer = demoTransferMapper.selectById(id); + if (demoTransfer == null) { + throw exception(DEMO_TRANSFER_NOT_FOUND); + } + if (PayTransferStatusEnum.isSuccess(demoTransfer.getTransferStatus()) + || PayTransferStatusEnum.isClosed(demoTransfer.getTransferStatus())) { + // 无需更新返回 null + return null; + } + PayTransferDO transfer = payTransferService.getTransfer(payTransferId); + if (transfer == null) { + throw exception(PAY_TRANSFER_NOT_FOUND); + } + if (!Objects.equals(demoTransfer.getPrice(), transfer.getPrice())) { + throw exception(DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH); + } + if (ObjectUtil.notEqual(transfer.getMerchantTransferId(), id.toString())) { + throw exception(DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR); + } + // TODO 校验账号 + return transfer; + } +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/notify/PayNotifyService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/notify/PayNotifyService.java new file mode 100644 index 0000000..f25f13a --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/notify/PayNotifyService.java @@ -0,0 +1,57 @@ +package com.tashow.cloud.pay.service.notify; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.tashow.cloud.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/notify/PayNotifyServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/notify/PayNotifyServiceImpl.java new file mode 100644 index 0000000..26d3495 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/notify/PayNotifyServiceImpl.java @@ -0,0 +1,308 @@ +package com.tashow.cloud.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.api.notify.dto.PayTransferNotifyReqDTO; +import com.tashow.cloud.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.tashow.cloud.pay.dal.dataobject.notify.PayNotifyTaskDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.refund.PayRefundDO; +import com.tashow.cloud.pay.dal.dataobject.transfer.PayTransferDO; +import com.tashow.cloud.pay.dal.mysql.notify.PayNotifyLogMapper; +import com.tashow.cloud.pay.dal.mysql.notify.PayNotifyTaskMapper; +import com.tashow.cloud.pay.dal.redis.notify.PayNotifyLockRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.tashow.cloud.pay.service.refund.PayRefundService; +import com.tashow.cloud.pay.service.transfer.PayTransferService; +import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +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 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 com.tashow.cloud.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 + @Lazy // 循环依赖,避免报错 + private PayTransferService transferService; + + @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()); + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) { + PayTransferDO transfer = transferService.getTransfer(task.getDataId()); + task.setAppId(transfer.getAppId()).setMerchantTransferId(transfer.getMerchantTransferId()) + .setNotifyUrl(transfer.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 if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) { + request = new PayTransferNotifyReqDTO().setMerchantTransferId(task.getMerchantTransferId()) + .setPayTransferId(task.getDataId()); + } 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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/order/PayOrderService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/order/PayOrderService.java new file mode 100644 index 0000000..b4d2085 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/order/PayOrderService.java @@ -0,0 +1,159 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.order.vo.PayOrderExportReqVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderPageReqVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderExtensionDO; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; + +import java.time.LocalDateTime; +import java.util.Collection; +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 ids 编号数组 + * @return 支付订单列表 + */ + List getOrderList(Collection ids); + + /** + * 获得指定应用的订单数量 + * + * @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 支付单编号 + * @param payPrice 支付单价格 + */ + void updatePayOrderPrice(Long id, Integer payPrice); + + /** + * 获得支付订单 + * + * @param id 编号 + * @return 支付订单 + */ + PayOrderExtensionDO getOrderExtension(Long id); + + /** + * 获得支付订单 + * + * @param no 支付订单 no + * @return 支付订单 + */ + PayOrderExtensionDO getOrderExtensionByNo(String no); + + /** + * 同步订单的支付状态 + * + * @param minCreateTime 最小创建时间 + * @return 同步到已支付的订单数量 + */ + int syncOrder(LocalDateTime minCreateTime); + + /** + * 同步订单的支付状态 + * + * 1. Quietly 表示,即使同步失败,也不会抛出异常 + * 2. 什么时候回出现异常?因为是主动同步,可能和支付渠道的异步回调存在并发冲突,导致抛出异常 + * + * @param id 订单编号 + */ + void syncOrderQuietly(Long id); + + /** + * 将已过期的订单,状态修改为已关闭 + * + * @return 过期的订单数量 + */ + int expireOrder(); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/order/PayOrderServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/order/PayOrderServiceImpl.java new file mode 100644 index 0000000..8d555ef --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/order/PayOrderServiceImpl.java @@ -0,0 +1,605 @@ +package com.tashow.cloud.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.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +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 com.tashow.cloud.pay.controller.admin.order.vo.PayOrderExportReqVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderPageReqVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import com.tashow.cloud.pay.convert.order.PayOrderConvert; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderExtensionDO; +import com.tashow.cloud.pay.dal.mysql.order.PayOrderExtensionMapper; +import com.tashow.cloud.pay.dal.mysql.order.PayOrderMapper; +import com.tashow.cloud.pay.dal.redis.no.PayNoRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import com.tashow.cloud.pay.framework.pay.config.PayProperties; +import com.tashow.cloud.pay.service.app.PayAppService; +import com.tashow.cloud.pay.service.channel.PayChannelService; +import com.tashow.cloud.pay.service.notify.PayNotifyService; +import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +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 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 List getOrderList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return orderMapper.selectBatchIds(ids); + } + + @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.getAppKey()); + + // 查询对应的支付交易单是否已经存在。如果是,则直接返回 + PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId( + app.getId(), 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 = channelService.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) { + try { + getSelf().notifyOrder(channel, unifiedOrderResp); + } catch (Exception e) { + // 兼容 https://gitee.com/zhijiantianya/yudao-cloud/issues/I8SM9H 场景 + // 支付宝或微信扫码之后时,由于 PayClient 是直接返回支付成功,而支付也会有回调,导致存在并发更新问题,此时一般是可以 try catch 直接忽略 + log.warn("[submitOrder][order({}) channel({}) 支付结果({}) 通知时发生异常,可能是并发问题]", + order, channel, unifiedOrderResp, e); + } + // 如有渠道错误码,则抛出业务异常,提示用户 + if (StrUtil.isNotEmpty(unifiedOrderResp.getChannelErrorCode())) { + throw exception(PAY_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(PAY_ORDER_NOT_FOUND); + } + if (PayOrderStatusEnum.isSuccess(order.getStatus())) { // 校验状态,发现已支付 + throw exception(PAY_ORDER_STATUS_IS_SUCCESS); + } + if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 + throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING); + } + if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期 + throw exception(PAY_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(PAY_ORDER_EXTENSION_IS_PAID); + } + // 情况二:调用三方接口,查询支付单状态,是不是已支付 + PayClient payClient = channelService.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(PAY_ORDER_EXTENSION_IS_PAID); + } + }); + } + + private PayChannelDO validateChannelCanSubmit(Long appId, String channelCode) { + // 校验 App + appService.validPayApp(appId); + // 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(appId, channelCode); + PayClient client = channelService.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(PAY_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(PAY_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(PAY_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(PAY_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(PAY_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(PAY_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(PAY_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(PAY_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(PAY_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(PAY_ORDER_NOT_FOUND); + } + if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) { + throw exception(PAY_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(PAY_ORDER_REFUND_FAIL_STATUS_ERROR); + } + } + + @Override + public void updatePayOrderPrice(Long id, Integer payPrice) { + PayOrderDO order = orderMapper.selectById(id); + if (order == null) { + throw exception(PAY_ORDER_NOT_FOUND); + } + if (ObjectUtil.notEqual(PayOrderStatusEnum.WAITING.getStatus(), order.getStatus())) { + throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING); + } + if (ObjectUtil.equal(order.getPrice(), payPrice)) { + return; + } + + orderMapper.updateById(new PayOrderDO().setId(order.getId()).setPrice(payPrice)); + } + + @Override + public PayOrderExtensionDO getOrderExtension(Long id) { + return orderExtensionMapper.selectById(id); + } + + @Override + public PayOrderExtensionDO getOrderExtensionByNo(String no) { + return orderExtensionMapper.selectByNo(no); + } + + @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; + } + + @Override + public void syncOrderQuietly(Long id) { + // 1. 查询待支付订单 + List orderExtensions = orderExtensionMapper.selectListByOrderIdAndStatus(id, + PayOrderStatusEnum.WAITING.getStatus()); + + // 2. 遍历执行 + for (PayOrderExtensionDO orderExtension : orderExtensions) { + syncOrder(orderExtension); + } + } + + /** + * 同步单个支付拓展单 + * + * @param orderExtension 支付拓展单 + * @return 是否已支付 + */ + private boolean syncOrder(PayOrderExtensionDO orderExtension) { + try { + // 1.1 查询支付订单信息 + PayClient payClient = channelService.getPayClient(orderExtension.getChannelId()); + if (payClient == null) { + log.error("[syncOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId()); + return false; + } + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo()); + // 如果查询到订单不存在,PayClient 返回的状态为关闭。但此时不能关闭订单。存在以下一种场景: + // 拉起渠道支付后,短时间内用户未及时完成支付,但是该订单同步定时任务恰巧自动触发了,主动查询结果为订单不存在。 + // 当用户支付成功之后,该订单状态在渠道的回调中无法从已关闭改为已支付,造成重大影响。 + // 考虑此定时任务是异常场景的兜底操作,因此这里不做变更,优先以回调为准。 + // 让订单自动随着支付渠道那边一起等到过期,确保渠道先过期关闭支付入口,而后通过订单过期定时任务关闭自己的订单。 + if (PayOrderStatusRespEnum.isClosed(respDTO.getStatus())) { + return false; + } + // 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 = channelService.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/refund/PayRefundService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/refund/PayRefundService.java new file mode 100644 index 0000000..463b006 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/refund/PayRefundService.java @@ -0,0 +1,82 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.refund.PayRefundDO; + +import java.util.List; + +/** + * 退款订单 Service 接口 + * + * @author aquan + */ +public interface PayRefundService { + + /** + * 获得退款订单 + * + * @param id 编号 + * @return 退款订单 + */ + PayRefundDO getRefund(Long id); + + /** + * 获得退款订单 + * + * @param no 外部退款单号 + * @return 退款订单 + */ + PayRefundDO getRefundByNo(String no); + + /** + * 获得指定应用的退款数量 + * + * @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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/refund/PayRefundServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/refund/PayRefundServiceImpl.java new file mode 100644 index 0000000..d1bdce7 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/refund/PayRefundServiceImpl.java @@ -0,0 +1,332 @@ +package com.tashow.cloud.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.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 com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import com.tashow.cloud.pay.convert.refund.PayRefundConvert; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.refund.PayRefundDO; +import com.tashow.cloud.pay.dal.mysql.refund.PayRefundMapper; +import com.tashow.cloud.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 com.tashow.cloud.pay.framework.pay.config.PayProperties; +import com.tashow.cloud.pay.service.app.PayAppService; +import com.tashow.cloud.pay.service.channel.PayChannelService; +import com.tashow.cloud.pay.service.notify.PayNotifyService; +import com.tashow.cloud.pay.service.order.PayOrderService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +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 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 PayRefundDO getRefundByNo(String no) { + return refundMapper.selectByNo(no); + } + + @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.getAppKey()); + // 1.2 校验支付订单 + PayOrderDO order = validatePayOrderCanRefund(reqDTO, app.getId()); + // 1.3 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(order.getChannelId()); + PayClient client = channelService.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).setAppId(app.getId()).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, Long appId) { + PayOrderDO order = orderService.getOrder(appId, reqDTO.getMerchantOrderId()); + if (order == null) { + throw exception(PAY_ORDER_NOT_FOUND); + } + // 校验状态,必须是已支付、或者已退款 + if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) { + throw exception(PAY_ORDER_REFUND_FAIL_STATUS_ERROR); + } + + // 校验金额,退款金额不能大于原定的金额 + if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()) { + throw exception(REFUND_PRICE_EXCEED); + } + // 是否有退款中的订单 + if (refundMapper.selectCountByAppIdAndOrderId(appId, 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 通知 + */ + // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效 + @Transactional(rollbackFor = Exception.class) + 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 = channelService.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/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/transfer/PayTransferService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/transfer/PayTransferService.java new file mode 100644 index 0000000..1ec738f --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/transfer/PayTransferService.java @@ -0,0 +1,66 @@ +package com.tashow.cloud.pay.service.transfer; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import com.tashow.cloud.pay.controller.admin.transfer.vo.PayTransferCreateReqVO; +import com.tashow.cloud.pay.controller.admin.transfer.vo.PayTransferPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.transfer.PayTransferDO; +import jakarta.validation.Valid; + +/** + * 转账 Service 接口 + * + * @author jason + */ +public interface PayTransferService { + + /** + * 创建转账单,并发起转账 + * + * 此时,会发起转账渠道的调用 + * + * @param reqVO 请求 + * @param userIp 用户 ip + * @return 渠道的返回结果 + */ + PayTransferDO createTransfer(@Valid PayTransferCreateReqVO reqVO, String userIp); + + /** + * 创建转账单,并发起转账 + * + * @param reqDTO 创建请求 + * @return 转账单编号 + */ + Long createTransfer(@Valid PayTransferCreateReqDTO reqDTO); + + /** + * 获取转账单 + * @param id 转账单编号 + */ + PayTransferDO getTransfer(Long id); + + /** + * 获得转账单分页 + * + * @param pageReqVO 分页查询 + * @return 转账单分页 + */ + PageResult getTransferPage(PayTransferPageReqVO pageReqVO); + + /** + * 同步渠道转账单状态 + * + * @return 同步到状态的转账数量,包括转账成功、转账失败、转账中的 + */ + int syncTransfer(); + + /** + * 渠道的转账通知 + * + * @param channelId 渠道编号 + * @param notify 通知 + */ + void notifyTransfer(Long channelId, PayTransferRespDTO notify); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/transfer/PayTransferServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/transfer/PayTransferServiceImpl.java new file mode 100644 index 0000000..f6892e7 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/transfer/PayTransferServiceImpl.java @@ -0,0 +1,313 @@ +package com.tashow.cloud.pay.service.transfer; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import com.tashow.cloud.pay.controller.admin.transfer.vo.PayTransferCreateReqVO; +import com.tashow.cloud.pay.controller.admin.transfer.vo.PayTransferPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.pay.dal.dataobject.transfer.PayTransferDO; +import com.tashow.cloud.pay.dal.mysql.transfer.PayTransferMapper; +import com.tashow.cloud.pay.dal.redis.no.PayNoRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum; +import com.tashow.cloud.pay.framework.pay.config.PayProperties; +import com.tashow.cloud.pay.service.app.PayAppService; +import com.tashow.cloud.pay.service.channel.PayChannelService; +import com.tashow.cloud.pay.service.notify.PayNotifyService; +import jakarta.annotation.Resource; +import jakarta.validation.Validator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.tashow.cloud.pay.convert.transfer.PayTransferConvert.INSTANCE; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.*; + +// TODO @jason:等彻底实现完,单测写写; + +/** + * 转账 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +public class PayTransferServiceImpl implements PayTransferService { + + private static final String TRANSFER_NO_PREFIX = "T"; + + @Resource + private PayProperties payProperties; + + @Resource + private PayTransferMapper transferMapper; + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + @Resource + private PayNotifyService notifyService; + @Resource + private PayNoRedisDAO noRedisDAO; + @Resource + private Validator validator; + + @Override + public PayTransferDO createTransfer(PayTransferCreateReqVO reqVO, String userIp) { + // 1. 校验参数 + reqVO.validate(validator); + + // 2. 创建转账单,发起转账 + PayTransferCreateReqDTO req = INSTANCE.convert(reqVO).setUserIp(userIp); + Long transferId = createTransfer(req); + + // 3. 返回转账单 + return getTransfer(transferId); + } + + @Override + public Long createTransfer(PayTransferCreateReqDTO reqDTO) { + // 1.1 校验 App + PayAppDO payApp = appService.validPayApp(reqDTO.getAppKey()); + // 1.2 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(payApp.getId(), reqDTO.getChannelCode()); + PayClient client = channelService.getPayClient(channel.getId()); + if (client == null) { + log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); + throw exception(CHANNEL_NOT_FOUND); + } + // 1.3 校验转账单已经发起过转账。 + PayTransferDO transfer = validateTransferCanCreate(reqDTO, payApp.getId()); + + if (transfer == null) { + // 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账 + String no = noRedisDAO.generate(TRANSFER_NO_PREFIX); + transfer = INSTANCE.convert(reqDTO) + .setChannelId(channel.getId()) + .setNo(no).setStatus(WAITING.getStatus()) + .setNotifyUrl(payApp.getTransferNotifyUrl()) + .setAppId(channel.getAppId()); + transferMapper.insert(transfer); + } + try { + // 3. 调用三方渠道发起转账 + PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer) + .setOutTransferNo(transfer.getNo()); + transferUnifiedReq.setNotifyUrl(genChannelTransferNotifyUrl(channel)); + PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq); + // 4. 通知转账结果 + getSelf().notifyTransfer(channel, unifiedTransferResp); + } catch (Throwable e) { + // 注意这里仅打印异常,不进行抛出。 + // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续转账轮询可以拿到。 + // 或者使用相同 no 再次发起转账请求 + log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e); + } + + return transfer.getId(); + } + + /** + * 根据支付渠道的编码,生成支付渠道的回调地址 + * + * @param channel 支付渠道 + * @return 支付渠道的回调地址 配置地址 + "/" + channel id + */ + private String genChannelTransferNotifyUrl(PayChannelDO channel) { + return payProperties.getTransferNotifyUrl() + "/" + channel.getId(); + } + + private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto, Long appId) { + PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, dto.getMerchantTransferId()); + if (transfer != null) { + // 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果. + if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) { + throw exception(PAY_MERCHANT_TRANSFER_EXISTS); + } + if (ObjectUtil.notEqual(dto.getPrice(), transfer.getPrice())) { + throw exception(PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH); + } + if (ObjectUtil.notEqual(dto.getType(), transfer.getType())) { + throw exception(PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH); + } + } + // 如果状态为等待状态。不知道渠道转账是否发起成功。 允许使用相同的 no 再次发起转账,渠道会保证幂等 + return transfer; + } + + @Transactional(rollbackFor = Exception.class) + // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyTransfer(channel, notify) 调用,否则事务不生效 + public void notifyTransfer(PayChannelDO channel, PayTransferRespDTO notify) { + // 转账成功的回调 + if (PayTransferStatusRespEnum.isSuccess(notify.getStatus())) { + notifyTransferSuccess(channel, notify); + } + // 转账关闭的回调 + if (PayTransferStatusRespEnum.isClosed(notify.getStatus())) { + notifyTransferClosed(channel, notify); + } + // 转账处理中的回调 + if (PayTransferStatusRespEnum.isInProgress(notify.getStatus())) { + notifyTransferInProgress(channel, notify); + } + // WAITING 状态无需处理 + } + + private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) { + // 1.校验 + PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo()); + if (transfer == null) { + throw exception(PAY_TRANSFER_NOT_FOUND); + } + if (isInProgress(transfer.getStatus())) { // 如果已经是转账中,直接返回,不用重复更新 + return; + } + if (!isWaiting(transfer.getStatus())) { + throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING); + } + // 2.更新 + int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(), + CollUtil.newArrayList(WAITING.getStatus()), + new PayTransferDO().setStatus(IN_PROGRESS.getStatus())); + if (updateCounts == 0) { + throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING); + } + log.info("[notifyTransferInProgress][transfer({}) 更新为转账进行中状态]", transfer.getId()); + } + + + private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) { + // 1.校验 + PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo()); + if (transfer == null) { + throw exception(PAY_TRANSFER_NOT_FOUND); + } + if (isSuccess(transfer.getStatus())) { // 如果已成功,直接返回,不用重复更新 + return; + } + if (!isPendingStatus(transfer.getStatus())) { + throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING); + } + // 2.更新 + int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(), + CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()), + new PayTransferDO().setStatus(SUCCESS.getStatus()).setSuccessTime(notify.getSuccessTime()) + .setChannelTransferNo(notify.getChannelTransferNo()) + .setChannelId(channel.getId()).setChannelCode(channel.getCode()) + .setChannelNotifyData(JsonUtils.toJsonString(notify))); + if (updateCounts == 0) { + throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING); + } + log.info("[updateTransferSuccess][transfer({}) 更新为已转账]", transfer.getId()); + + // 3. 插入转账通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(), + transfer.getId()); + } + + private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) { + // 1.校验 + PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo()); + if (transfer == null) { + throw exception(PAY_TRANSFER_NOT_FOUND); + } + if (isClosed(transfer.getStatus())) { // 如果已是关闭状态,直接返回,不用重复更新 + log.info("[updateTransferClosed][transfer({}) 已经是关闭状态,无需更新]", transfer.getId()); + return; + } + if (!isPendingStatus(transfer.getStatus())) { + throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING); + } + + // 2.更新 + int updateCount = transferMapper.updateByIdAndStatus(transfer.getId(), + CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()), + new PayTransferDO().setStatus(CLOSED.getStatus()).setChannelId(channel.getId()) + .setChannelCode(channel.getCode()).setChannelTransferNo(notify.getChannelTransferNo()) + .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()) + .setChannelNotifyData(JsonUtils.toJsonString(notify))); + if (updateCount == 0) { + throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING); + } + log.info("[updateTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId()); + + // 3. 插入转账通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(), + transfer.getId()); + + } + + @Override + public PayTransferDO getTransfer(Long id) { + return transferMapper.selectById(id); + } + + @Override + public PageResult getTransferPage(PayTransferPageReqVO pageReqVO) { + return transferMapper.selectPage(pageReqVO); + } + + @Override + public int syncTransfer() { + List list = transferMapper.selectListByStatus(WAITING.getStatus()); + if (CollUtil.isEmpty(list)) { + return 0; + } + int count = 0; + for (PayTransferDO transfer : list) { + count += syncTransfer(transfer) ? 1 : 0; + } + return count; + } + + private boolean syncTransfer(PayTransferDO transfer) { + try { + // 1. 查询转账订单信息 + PayClient payClient = channelService.getPayClient(transfer.getChannelId()); + if (payClient == null) { + log.error("[syncTransfer][渠道编号({}) 找不到对应的支付客户端]", transfer.getChannelId()); + return false; + } + PayTransferRespDTO resp = payClient.getTransfer(transfer.getNo(), + PayTransferTypeEnum.typeOf(transfer.getType())); + + // 2. 回调转账结果 + notifyTransfer(transfer.getChannelId(), resp); + return true; + } catch (Throwable ex) { + log.error("[syncTransfer][transfer({}) 同步转账单状态异常]", transfer.getId(), ex); + return false; + } + } + + public void notifyTransfer(Long channelId, PayTransferRespDTO notify) { + // 校验渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(channelId); + // 通知转账结果给对应的业务 + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyTransfer(channel, notify)); + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayTransferServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargePackageService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargePackageService.java new file mode 100644 index 0000000..e2affae --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargePackageService.java @@ -0,0 +1,70 @@ +package com.tashow.cloud.pay.service.wallet; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageCreateReqVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackagePageReqVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageUpdateReqVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 钱包充值套餐 Service 接口 + * + * @author jason + */ +public interface PayWalletRechargePackageService { + + /** + * 获取钱包充值套餐 + * @param packageId 充值套餐编号 + */ + PayWalletRechargePackageDO getWalletRechargePackage(Long packageId); + + /** + * 校验钱包充值套餐的有效性, 无效的话抛出 ServiceException 异常 + * + * @param packageId 充值套餐编号 + */ + PayWalletRechargePackageDO validWalletRechargePackage(Long packageId); + + /** + * 创建充值套餐 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createWalletRechargePackage(@Valid WalletRechargePackageCreateReqVO createReqVO); + + /** + * 更新充值套餐 + * + * @param updateReqVO 更新信息 + */ + void updateWalletRechargePackage(@Valid WalletRechargePackageUpdateReqVO updateReqVO); + + /** + * 删除充值套餐 + * + * @param id 编号 + */ + void deleteWalletRechargePackage(Long id); + + /** + * 获得充值套餐分页 + * + * @param pageReqVO 分页查询 + * @return 充值套餐分页 + */ + PageResult getWalletRechargePackagePage(WalletRechargePackagePageReqVO pageReqVO); + + /** + * 获得充值套餐列表 + * + * @param status 状态 + * @return 充值套餐列表 + */ + List getWalletRechargePackageList(Integer status); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargePackageServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargePackageServiceImpl.java new file mode 100644 index 0000000..35211ca --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargePackageServiceImpl.java @@ -0,0 +1,112 @@ +package com.tashow.cloud.pay.service.wallet; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageCreateReqVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackagePageReqVO; +import com.tashow.cloud.pay.controller.admin.wallet.vo.rechargepackage.WalletRechargePackageUpdateReqVO; +import com.tashow.cloud.pay.convert.wallet.PayWalletRechargePackageConvert; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import com.tashow.cloud.pay.dal.mysql.wallet.PayWalletRechargePackageMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +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 jason + */ +@Service +public class PayWalletRechargePackageServiceImpl implements PayWalletRechargePackageService { + + @Resource + private PayWalletRechargePackageMapper walletRechargePackageMapper; + + @Override + public PayWalletRechargePackageDO getWalletRechargePackage(Long packageId) { + return walletRechargePackageMapper.selectById(packageId); + } + + @Override + public PayWalletRechargePackageDO validWalletRechargePackage(Long packageId) { + PayWalletRechargePackageDO rechargePackageDO = walletRechargePackageMapper.selectById(packageId); + if (rechargePackageDO == null) { + throw exception(WALLET_RECHARGE_PACKAGE_NOT_FOUND); + } + if (CommonStatusEnum.DISABLE.getStatus().equals(rechargePackageDO.getStatus())) { + throw exception(WALLET_RECHARGE_PACKAGE_IS_DISABLE); + } + return rechargePackageDO; + } + + @Override + public Long createWalletRechargePackage(WalletRechargePackageCreateReqVO createReqVO) { + // 校验套餐名是否唯一 + validateRechargePackageNameUnique(null, createReqVO.getName()); + + // 插入 + PayWalletRechargePackageDO walletRechargePackage = PayWalletRechargePackageConvert.INSTANCE.convert(createReqVO); + walletRechargePackageMapper.insert(walletRechargePackage); + // 返回 + return walletRechargePackage.getId(); + } + + @Override + public void updateWalletRechargePackage(WalletRechargePackageUpdateReqVO updateReqVO) { + // 校验存在 + validateWalletRechargePackageExists(updateReqVO.getId()); + // 校验套餐名是否唯一 + validateRechargePackageNameUnique(updateReqVO.getId(), updateReqVO.getName()); + + // 更新 + PayWalletRechargePackageDO updateObj = PayWalletRechargePackageConvert.INSTANCE.convert(updateReqVO); + walletRechargePackageMapper.updateById(updateObj); + } + + private void validateRechargePackageNameUnique(Long id, String name) { + if (StrUtil.isBlank(name)) { + return; + } + PayWalletRechargePackageDO rechargePackage = walletRechargePackageMapper.selectByName(name); + if (rechargePackage == null) { + return ; + } + if (id == null) { + throw exception(WALLET_RECHARGE_PACKAGE_NAME_EXISTS); + } + if (!id.equals(rechargePackage.getId())) { + throw exception(WALLET_RECHARGE_PACKAGE_NAME_EXISTS); + } + } + + @Override + public void deleteWalletRechargePackage(Long id) { + // 校验存在 + validateWalletRechargePackageExists(id); + // 删除 + walletRechargePackageMapper.deleteById(id); + } + + private void validateWalletRechargePackageExists(Long id) { + if (walletRechargePackageMapper.selectById(id) == null) { + throw exception(WALLET_RECHARGE_PACKAGE_NOT_FOUND); + } + } + + @Override + public PageResult getWalletRechargePackagePage(WalletRechargePackagePageReqVO pageReqVO) { + return walletRechargePackageMapper.selectPage(pageReqVO); + } + + @Override + public List getWalletRechargePackageList(Integer status) { + return walletRechargePackageMapper.selectListByStatus(status); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargeService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargeService.java new file mode 100644 index 0000000..3375e4e --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargeService.java @@ -0,0 +1,63 @@ +package com.tashow.cloud.pay.service.wallet; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateReqVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargeDO; + +/** + * 钱包充值 Service 接口 + * + * @author jason + */ +public interface PayWalletRechargeService { + + /** + * 创建钱包充值记录(发起充值) + * + * @param userId 用户 id + * @param userType 用户类型 + * @param createReqVO 钱包充值请求 VO + * @param userIp 用户Ip + * @return 钱包充值记录 + */ + PayWalletRechargeDO createWalletRecharge(Long userId, Integer userType, String userIp, + AppPayWalletRechargeCreateReqVO createReqVO); + + /** + * 获得钱包充值记录分页 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param pageReqVO 分页请求 + * @param payStatus 是否支付 + * @return 钱包充值记录分页 + */ + PageResult getWalletRechargePackagePage(Long userId, Integer userType, + PageParam pageReqVO, Boolean payStatus); + + /** + * 更新钱包充值成功 + * + * @param id 钱包充值记录 id + * @param payOrderId 支付订单 id + */ + void updateWalletRechargerPaid(Long id, Long payOrderId); + + /** + * 发起钱包充值退款 + * + * @param id 钱包充值编号 + * @param userIp 用户 ip 地址 + */ + void refundWalletRecharge(Long id, String userIp); + + /** + * 更新钱包充值记录为已退款 + * + * @param id 钱包充值 id + * @param payRefundId 退款单id + */ + void updateWalletRechargeRefunded(Long id, Long payRefundId); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargeServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargeServiceImpl.java new file mode 100644 index 0000000..d621121 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletRechargeServiceImpl.java @@ -0,0 +1,328 @@ +package com.tashow.cloud.pay.service.wallet; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import com.tashow.cloud.pay.controller.app.wallet.vo.recharge.AppPayWalletRechargeCreateReqVO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.refund.PayRefundDO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletDO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargeDO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletRechargePackageDO; +import com.tashow.cloud.pay.dal.mysql.wallet.PayWalletRechargeMapper; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import com.tashow.cloud.pay.framework.pay.config.PayProperties; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.tashow.cloud.pay.service.refund.PayRefundService; +import cn.iocoder.yudao.module.system.api.social.SocialClientApi; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Objects; + +import static cn.hutool.core.util.ObjectUtil.notEqual; +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.number.MoneyUtils.fenToYuanStr; +import static com.tashow.cloud.pay.convert.wallet.PayWalletRechargeConvert.INSTANCE; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.pay.enums.MessageTemplateConstants.WXA_WALLET_RECHARGER_PAID; +import static cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum.*; + +/** + * 钱包充值 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { + + private static final String WALLET_RECHARGE_ORDER_SUBJECT = "钱包余额充值"; + + @Resource + private PayWalletRechargeMapper walletRechargeMapper; + @Resource + private PayWalletService payWalletService; + @Resource + private PayOrderService payOrderService; + @Resource + private PayRefundService payRefundService; + @Resource + private PayWalletRechargePackageService payWalletRechargePackageService; + + @Resource + public SocialClientApi socialClientApi; + + @Resource + private PayProperties payProperties; + + @Override + @Transactional(rollbackFor = Exception.class) + public PayWalletRechargeDO createWalletRecharge(Long userId, Integer userType, String userIp, + AppPayWalletRechargeCreateReqVO reqVO) { + // 1.1 计算充值金额 + int payPrice; + int bonusPrice = 0; + if (Objects.nonNull(reqVO.getPackageId())) { + PayWalletRechargePackageDO rechargePackage = payWalletRechargePackageService.validWalletRechargePackage(reqVO.getPackageId()); + payPrice = rechargePackage.getPayPrice(); + bonusPrice = rechargePackage.getBonusPrice(); + } else { + payPrice = reqVO.getPayPrice(); + } + // 1.2 插入充值记录 + PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType); + PayWalletRechargeDO recharge = INSTANCE.convert(wallet.getId(), payPrice, bonusPrice, reqVO.getPackageId()); + walletRechargeMapper.insert(recharge); + + // 2.1 创建支付单 + Long payOrderId = payOrderService.createOrder(new PayOrderCreateReqDTO() + .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp) + .setMerchantOrderId(recharge.getId().toString()) // 业务的订单编号 + .setSubject(WALLET_RECHARGE_ORDER_SUBJECT).setBody("") + .setPrice(recharge.getPayPrice()) + .setExpireTime(addTime(Duration.ofHours(2L)))); // TODO @芋艿:支付超时时间 + // 2.2 更新钱包充值记录中支付订单 + walletRechargeMapper.updateById(new PayWalletRechargeDO().setId(recharge.getId()).setPayOrderId(payOrderId)); + recharge.setPayOrderId(payOrderId); + return recharge; + } + + @Override + public PageResult getWalletRechargePackagePage(Long userId, Integer userType, + PageParam pageReqVO, Boolean payStatus) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType); + return walletRechargeMapper.selectPage(pageReqVO, wallet.getId(), payStatus); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWalletRechargerPaid(Long id, Long payOrderId) { + // 1.1 校验钱包充值是否存在 + PayWalletRechargeDO recharge = walletRechargeMapper.selectById(id); + if (recharge == null) { + log.error("[updateWalletRechargerPaid][recharge({}) payOrder({}) 不存在充值订单,请进行处理!]", id, payOrderId); + throw exception(WALLET_RECHARGE_NOT_FOUND); + } + // 1.2 校验钱包充值是否可以支付 + if (recharge.getPayStatus()) { + // 特殊:如果订单已支付,且支付单号相同,直接返回,说明重复回调 + if (ObjectUtil.equals(recharge.getPayOrderId(), payOrderId)) { + log.warn("[updateWalletRechargerPaid][recharge({}) 已支付,且支付单号相同({}),直接返回]", recharge, payOrderId); + return; + } + // 异常:支付单号不同,说明支付单号错误 + log.error("[updateWalletRechargerPaid][recharge({}) 已支付,但是支付单号不同({}),请进行处理!]", recharge, payOrderId); + throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_ID_ERROR); + } + + // 2. 校验支付订单的合法性 + PayOrderDO payOrderDO = validatePayOrderPaid(recharge, payOrderId); + + // 3. 更新钱包充值的支付状态 + int updateCount = walletRechargeMapper.updateByIdAndPaid(id, false, + new PayWalletRechargeDO().setId(id).setPayStatus(true).setPayTime(LocalDateTime.now()) + .setPayChannelCode(payOrderDO.getChannelCode())); + if (updateCount == 0) { + throw exception(WALLET_RECHARGE_UPDATE_PAID_STATUS_NOT_UNPAID); + } + + // 4. 更新钱包余额 + // TODO @jason:这样的话,未来提现会不会把充值的,也提现走哈。类似先充 100,送 110;然后提现 110; + // TODO 需要钱包中加个可提现余额 + payWalletService.addWalletBalance(recharge.getWalletId(), String.valueOf(id), + PayWalletBizTypeEnum.RECHARGE, recharge.getTotalPrice()); + + // 5. 发送订阅消息 + getSelf().sendWalletRechargerPaidMessage(payOrderId, recharge); + } + + @Async + public void sendWalletRechargerPaidMessage(Long payOrderId, PayWalletRechargeDO walletRecharge) { + // 1. 获得会员钱包信息 + PayWalletDO wallet = payWalletService.getWallet(walletRecharge.getWalletId()); + // 2. 构建并发送模版消息 + socialClientApi.sendWxaSubscribeMessage(new SocialWxaSubscribeMessageSendReqDTO() + .setUserId(wallet.getUserId()).setUserType(wallet.getUserType()) + .setTemplateTitle(WXA_WALLET_RECHARGER_PAID) + .setPage("pages/user/wallet/money") // 钱包详情界面 + .addMessage("character_string1", String.valueOf(payOrderId)) // 支付单编号 + .addMessage("amount2", fenToYuanStr(walletRecharge.getTotalPrice())) // 充值金额 + .addMessage("time3", LocalDateTimeUtil.formatNormal(walletRecharge.getCreateTime())) // 充值时间 + .addMessage("phrase4", "充值成功")).checkError(); // 充值状态 + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void refundWalletRecharge(Long id, String userIp) { + // 1.1 获取钱包充值记录 + PayWalletRechargeDO walletRecharge = walletRechargeMapper.selectById(id); + if (walletRecharge == null) { + log.error("[refundWalletRecharge][钱包充值记录不存在,钱包充值记录 id({})]", id); + throw exception(WALLET_RECHARGE_NOT_FOUND); + } + // 1.2 校验钱包充值是否可以发起退款 + PayWalletDO wallet = validateWalletRechargeCanRefund(walletRecharge); + + // 2. 冻结退款的余额,暂时只处理赠送的余额也全部退回 + payWalletService.freezePrice(wallet.getId(), walletRecharge.getTotalPrice()); + + // 3. 创建退款单 + String walletRechargeId = String.valueOf(id); + String refundId = walletRechargeId + "-refund"; + Long payRefundId = payRefundService.createPayRefund(new PayRefundCreateReqDTO() + .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp) + .setMerchantOrderId(walletRechargeId) + .setMerchantRefundId(refundId) + .setReason("想退钱").setPrice(walletRecharge.getPayPrice())); + + // 4. 更新充值记录退款单号 + walletRechargeMapper.updateById(new PayWalletRechargeDO().setPayRefundId(payRefundId) + .setRefundStatus(WAITING.getStatus()).setId(walletRecharge.getId())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWalletRechargeRefunded(Long id, Long payRefundId) { + // 1.1 获取钱包充值记录 + PayWalletRechargeDO walletRecharge = walletRechargeMapper.selectById(id); + if (walletRecharge == null) { + log.error("[updateWalletRechargerPaid][钱包充值记录不存在,钱包充值记录 id({})]", id); + throw exception(WALLET_RECHARGE_NOT_FOUND); + } + // 1.2 校验钱包充值是否可以更新已退款 + PayRefundDO payRefund = validateWalletRechargeCanRefunded(walletRecharge, payRefundId); + + PayWalletRechargeDO updateObj = new PayWalletRechargeDO().setId(id); + // 退款成功 + if (PayRefundStatusEnum.isSuccess(payRefund.getStatus())) { + // 2.1 更新钱包余额 + payWalletService.reduceWalletBalance(walletRecharge.getWalletId(), id, + PayWalletBizTypeEnum.RECHARGE_REFUND, walletRecharge.getTotalPrice()); + + updateObj.setRefundStatus(SUCCESS.getStatus()).setRefundTime(payRefund.getSuccessTime()) + .setRefundTotalPrice(walletRecharge.getTotalPrice()).setRefundPayPrice(walletRecharge.getPayPrice()) + .setRefundBonusPrice(walletRecharge.getBonusPrice()); + } + // 退款失败 + if (PayRefundStatusRespEnum.isFailure(payRefund.getStatus())) { + // 2.2 解冻余额 + payWalletService.unfreezePrice(walletRecharge.getWalletId(), walletRecharge.getTotalPrice()); + + updateObj.setRefundStatus(FAILURE.getStatus()); + } + // 3. 更新钱包充值的退款字段 + walletRechargeMapper.updateByIdAndRefunded(id, WAITING.getStatus(), updateObj); + } + + private PayRefundDO validateWalletRechargeCanRefunded(PayWalletRechargeDO walletRecharge, Long payRefundId) { + // 1. 校验退款订单匹配 + if (notEqual(walletRecharge.getPayRefundId(), payRefundId)) { + log.error("[validateWalletRechargeCanRefunded][钱包充值({}) 退款单不匹配({}),请进行处理!钱包充值的数据是:{}]", + walletRecharge.getId(), payRefundId, toJsonString(walletRecharge)); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + + // 2.1 校验退款订单 + PayRefundDO payRefund = payRefundService.getRefund(payRefundId); + if (payRefund == null) { + log.error("[validateWalletRechargeCanRefunded][payRefund({})不存在]", payRefundId); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_NOT_FOUND); + } + // 2.2 校验退款金额一致 + if (notEqual(payRefund.getRefundPrice(), walletRecharge.getPayPrice())) { + log.error("[validateWalletRechargeCanRefunded][钱包({}) payRefund({}) 退款金额不匹配,请进行处理!钱包数据是:{},payRefund 数据是:{}]", + walletRecharge.getId(), payRefundId, toJsonString(walletRecharge), toJsonString(payRefund)); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH); + } + // 2.3 校验退款订单商户订单是否匹配 + if (notEqual(payRefund.getMerchantOrderId(), walletRecharge.getId().toString())) { + log.error("[validateWalletRechargeCanRefunded][钱包({}) 退款单不匹配({}),请进行处理!payRefund 数据是:{}]", + walletRecharge.getId(), payRefundId, toJsonString(payRefund)); + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + return payRefund; + } + + private PayWalletDO validateWalletRechargeCanRefund(PayWalletRechargeDO walletRecharge) { + // 校验充值订单是否支付 + if (!walletRecharge.getPayStatus()) { + throw exception(WALLET_RECHARGE_REFUND_FAIL_NOT_PAID); + } + // 校验充值订单是否已退款 + if (walletRecharge.getPayRefundId() != null) { + throw exception(WALLET_RECHARGE_REFUND_FAIL_REFUNDED); + } + // 校验钱包余额是否足够 + PayWalletDO wallet = payWalletService.getWallet(walletRecharge.getWalletId()); + Assert.notNull(wallet, "用户钱包({}) 不存在", wallet.getId()); + if (wallet.getBalance() < walletRecharge.getTotalPrice()) { + throw exception(WALLET_RECHARGE_REFUND_BALANCE_NOT_ENOUGH); + } + // TODO @芋艿:需要考虑下,赠送的金额,会不会导致提现超过; + return wallet; + } + + /** + * 校验支付订单的合法性 + * + * @param recharge 充值订单 + * @param payOrderId 支付订单编号 + * @return 支付订单 + */ + private PayOrderDO validatePayOrderPaid(PayWalletRechargeDO recharge, Long payOrderId) { + // 1. 校验支付单是否存在 + PayOrderDO payOrder = payOrderService.getOrder(payOrderId); + if (payOrder == null) { + log.error("[validatePayOrderPaid][充值订单({}) payOrder({}) 不存在,请进行处理!]", + recharge.getId(), payOrderId); + throw exception(PAY_ORDER_NOT_FOUND); + } + + // 2.1 校验支付单已支付 + if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) { + log.error("[validatePayOrderPaid][充值订单({}) payOrder({}) 未支付,请进行处理!payOrder 数据是:{}]", + recharge.getId(), payOrderId, toJsonString(payOrder)); + throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_STATUS_NOT_SUCCESS); + } + // 2.2 校验支付金额一致 + if (notEqual(payOrder.getPrice(), recharge.getPayPrice())) { + log.error("[validatePayOrderPaid][充值订单({}) payOrder({}) 支付金额不匹配,请进行处理!钱包 数据是:{},payOrder 数据是:{}]", + recharge.getId(), payOrderId, toJsonString(recharge), toJsonString(payOrder)); + throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_PRICE_NOT_MATCH); + } + // 2.3 校验支付订单的商户订单匹配 + if (notEqual(payOrder.getMerchantOrderId(), recharge.getId().toString())) { + log.error("[validatePayOrderPaid][充值订单({}) 支付单不匹配({}),请进行处理!payOrder 数据是:{}]", + recharge.getId(), payOrderId, toJsonString(payOrder)); + throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_ID_ERROR); + } + return payOrder; + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayWalletRechargeServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletService.java new file mode 100644 index 0000000..214c2c6 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletService.java @@ -0,0 +1,100 @@ +package com.tashow.cloud.pay.service.wallet; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletDO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; + +/** + * 钱包 Service 接口 + * + * @author jason + */ +public interface PayWalletService { + + /** + * 获取钱包信息 + *

+ * 如果不存在,则创建钱包。由于用户注册时候不会创建钱包 + * + * @param userId 用户编号 + * @param userType 用户类型 + */ + PayWalletDO getOrCreateWallet(Long userId, Integer userType); + + /** + * 获取钱包信息 + * + * @param walletId 钱包 id + */ + PayWalletDO getWallet(Long walletId); + + /** + * 获得会员钱包分页 + * + * @param pageReqVO 分页查询 + * @return 会员钱包分页 + */ + PageResult getWalletPage(PayWalletPageReqVO pageReqVO); + + /** + * 钱包订单支付 + * + * @param userId 用户 id + * @param userType 用户类型 + * @param outTradeNo 外部订单号 + * @param price 金额 + */ + PayWalletTransactionDO orderPay(Long userId, Integer userType, String outTradeNo, Integer price); + + /** + * 钱包订单支付退款 + * + * @param outRefundNo 外部退款号 + * @param refundPrice 退款金额 + * @param reason 退款原因 + */ + PayWalletTransactionDO orderRefund(String outRefundNo, Integer refundPrice, String reason); + + /** + * 扣减钱包余额 + * + * @param walletId 钱包 id + * @param bizId 业务关联 id + * @param bizType 业务关联分类 + * @param price 扣减金额 + * @return 钱包流水 + */ + PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId, + PayWalletBizTypeEnum bizType, Integer price); + + /** + * 增加钱包余额 + * + * @param walletId 钱包 id + * @param bizId 业务关联 id + * @param bizType 业务关联分类 + * @param price 增加金额 + * @return 钱包流水 + */ + PayWalletTransactionDO addWalletBalance(Long walletId, String bizId, + PayWalletBizTypeEnum bizType, Integer price); + + /** + * 冻结钱包部分余额 + * + * @param id 钱包编号 + * @param price 冻结金额 + */ + void freezePrice(Long id, Integer price); + + /** + * 解冻钱包余额 + * + * @param id 钱包编号 + * @param price 解冻金额 + */ + void unfreezePrice(Long id, Integer price); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletServiceImpl.java new file mode 100644 index 0000000..839c572 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletServiceImpl.java @@ -0,0 +1,233 @@ +package com.tashow.cloud.pay.service.wallet; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import com.tashow.cloud.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderExtensionDO; +import com.tashow.cloud.pay.dal.dataobject.refund.PayRefundDO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletDO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import com.tashow.cloud.pay.dal.mysql.wallet.PayWalletMapper; +import com.tashow.cloud.pay.dal.redis.wallet.PayWalletLockRedisDAO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.tashow.cloud.pay.service.refund.PayRefundService; +import com.tashow.cloud.pay.service.wallet.bo.WalletTransactionCreateReqBO; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYMENT; +import static cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYMENT_REFUND; + +/** + * 钱包 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +public class PayWalletServiceImpl implements PayWalletService { + + /** + * 通知超时时间,单位:毫秒 + */ + public static final long UPDATE_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS; + + @Resource + private PayWalletMapper walletMapper; + @Resource + private PayWalletLockRedisDAO lockRedisDAO; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PayWalletTransactionService walletTransactionService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PayOrderService orderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PayRefundService refundService; + + @Override + public PayWalletDO getOrCreateWallet(Long userId, Integer userType) { + PayWalletDO wallet = walletMapper.selectByUserIdAndType(userId, userType); + if (wallet == null) { + wallet = new PayWalletDO().setUserId(userId).setUserType(userType) + .setBalance(0).setTotalExpense(0).setTotalRecharge(0); + wallet.setCreateTime(LocalDateTime.now()); + walletMapper.insert(wallet); + } + return wallet; + } + + @Override + public PayWalletDO getWallet(Long walletId) { + return walletMapper.selectById(walletId); + } + + @Override + public PageResult getWalletPage(PayWalletPageReqVO pageReqVO) { + return walletMapper.selectPage(pageReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public PayWalletTransactionDO orderPay(Long userId, Integer userType, String outTradeNo, Integer price) { + // 1. 判断支付交易拓展单是否存 + PayOrderExtensionDO orderExtension = orderService.getOrderExtensionByNo(outTradeNo); + if (orderExtension == null) { + throw exception(PAY_ORDER_EXTENSION_NOT_FOUND); + } + PayWalletDO wallet = getOrCreateWallet(userId, userType); + // 2. 扣减余额 + return reduceWalletBalance(wallet.getId(), orderExtension.getOrderId(), PAYMENT, price); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public PayWalletTransactionDO orderRefund(String outRefundNo, Integer refundPrice, String reason) { + // 1.1 判断退款单是否存在 + PayRefundDO payRefund = refundService.getRefundByNo(outRefundNo); + if (payRefund == null) { + throw exception(REFUND_NOT_FOUND); + } + // 1.2 校验是否可以退款 + Long walletId = validateWalletCanRefund(payRefund.getId(), payRefund.getChannelOrderNo()); + PayWalletDO wallet = walletMapper.selectById(walletId); + Assert.notNull(wallet, "钱包 {} 不存在", walletId); + + // 2. 增加余额 + return addWalletBalance(walletId, String.valueOf(payRefund.getId()), PAYMENT_REFUND, refundPrice); + } + + /** + * 校验是否能退款 + * + * @param refundId 支付退款单 id + * @param walletPayNo 钱包支付 no + */ + private Long validateWalletCanRefund(Long refundId, String walletPayNo) { + // 1. 校验钱包支付交易存在 + PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransactionByNo(walletPayNo); + if (walletTransaction == null) { + throw exception(WALLET_TRANSACTION_NOT_FOUND); + } + // 2. 校验退款是否存在 + PayWalletTransactionDO refundTransaction = walletTransactionService.getWalletTransaction( + String.valueOf(refundId), PAYMENT_REFUND); + if (refundTransaction != null) { + throw exception(WALLET_REFUND_EXIST); + } + return walletTransaction.getWalletId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @SneakyThrows + public PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId, + PayWalletBizTypeEnum bizType, Integer price) { + // 1. 获取钱包 + PayWalletDO payWallet = getWallet(walletId); + if (payWallet == null) { + log.error("[reduceWalletBalance][用户钱包({})不存在]", walletId); + throw exception(WALLET_NOT_FOUND); + } + + // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯) + return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> { + // 2. 扣除余额 + int updateCounts; + switch (bizType) { + case PAYMENT: { + updateCounts = walletMapper.updateWhenConsumption(payWallet.getId(), price); + break; + } + case RECHARGE_REFUND: { + updateCounts = walletMapper.updateWhenRechargeRefund(payWallet.getId(), price); + break; + } + default: { + // TODO 其它类型待实现 + throw new UnsupportedOperationException("待实现"); + } + } + if (updateCounts == 0) { + throw exception(WALLET_BALANCE_NOT_ENOUGH); + } + + // 3. 生成钱包流水 + Integer afterBalance = payWallet.getBalance() - price; + WalletTransactionCreateReqBO bo = new WalletTransactionCreateReqBO().setWalletId(payWallet.getId()) + .setPrice(-price).setBalance(afterBalance).setBizId(String.valueOf(bizId)) + .setBizType(bizType.getType()).setTitle(bizType.getDescription()); + return walletTransactionService.createWalletTransaction(bo); + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @SneakyThrows + public PayWalletTransactionDO addWalletBalance(Long walletId, String bizId, + PayWalletBizTypeEnum bizType, Integer price) { + // 1. 获取钱包 + PayWalletDO payWallet = getWallet(walletId); + if (payWallet == null) { + log.error("[addWalletBalance][用户钱包({})不存在]", walletId); + throw exception(WALLET_NOT_FOUND); + } + + // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯) + return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> { + // 3. 更新钱包金额 + switch (bizType) { + case PAYMENT_REFUND: { // 退款更新 + walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price); + break; + } + case RECHARGE: { // 充值更新 + walletMapper.updateWhenRecharge(payWallet.getId(), price); + break; + } + case UPDATE_BALANCE: // 更新余额 + case BROKERAGE_WITHDRAW: // 分佣提现 + walletMapper.updateWhenAdd(payWallet.getId(), price); + break; + default: { + throw new UnsupportedOperationException("待实现:" + bizType); + } + } + + // 4. 生成钱包流水 + WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO() + .setWalletId(payWallet.getId()).setPrice(price).setBalance(payWallet.getBalance() + price) + .setBizId(bizId).setBizType(bizType.getType()).setTitle(bizType.getDescription()); + return walletTransactionService.createWalletTransaction(transactionCreateReqBO); + }); + } + + @Override + public void freezePrice(Long id, Integer price) { + int updateCounts = walletMapper.freezePrice(id, price); + if (updateCounts == 0) { + throw exception(WALLET_BALANCE_NOT_ENOUGH); + } + } + + @Override + public void unfreezePrice(Long id, Integer price) { + int updateCounts = walletMapper.unFreezePrice(id, price); + if (updateCounts == 0) { + throw exception(WALLET_FREEZE_PRICE_NOT_ENOUGH); + } + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletTransactionService.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletTransactionService.java new file mode 100644 index 0000000..848a05b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletTransactionService.java @@ -0,0 +1,73 @@ +package com.tashow.cloud.pay.service.wallet; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO; +import com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO; +import com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionSummaryRespVO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import com.tashow.cloud.pay.service.wallet.bo.WalletTransactionCreateReqBO; +import jakarta.validation.Valid; + +import java.time.LocalDateTime; + +/** + * 钱包余额流水 Service 接口 + * + * @author jason + */ +public interface PayWalletTransactionService { + + /** + * 查询钱包余额流水分页 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param pageVO 分页查询参数 + */ + PageResult getWalletTransactionPage(Long userId, Integer userType, + AppPayWalletTransactionPageReqVO pageVO); + + /** + * 查询钱包余额流水分页 + * + * @param pageVO 分页查询参数 + */ + PageResult getWalletTransactionPage(PayWalletTransactionPageReqVO pageVO); + + /** + * 新增钱包余额流水 + * + * @param bo 创建钱包流水 bo + * @return 新建的钱包 do + */ + PayWalletTransactionDO createWalletTransaction(@Valid WalletTransactionCreateReqBO bo); + + /** + * 根据 no,获取钱包余流水 + * + * @param no 流水号 + */ + PayWalletTransactionDO getWalletTransactionByNo(String no); + + /** + * 获取钱包流水 + * + * @param bizId 业务编号 + * @param type 业务类型 + * @return 钱包流水 + */ + PayWalletTransactionDO getWalletTransaction(String bizId, PayWalletBizTypeEnum type); + + /** + * 获得钱包流水统计 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param createTime 时间段 + * @return 钱包流水统计 + */ + AppPayWalletTransactionSummaryRespVO getWalletTransactionSummary(Long userId, Integer userType, + LocalDateTime[] createTime); + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletTransactionServiceImpl.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletTransactionServiceImpl.java new file mode 100644 index 0000000..e8022fe --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/PayWalletTransactionServiceImpl.java @@ -0,0 +1,95 @@ +package com.tashow.cloud.pay.service.wallet; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO; +import com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO; +import com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionSummaryRespVO; +import com.tashow.cloud.pay.convert.wallet.PayWalletTransactionConvert; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletDO; +import com.tashow.cloud.pay.dal.dataobject.wallet.PayWalletTransactionDO; +import com.tashow.cloud.pay.dal.mysql.wallet.PayWalletTransactionMapper; +import com.tashow.cloud.pay.dal.redis.no.PayNoRedisDAO; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import com.tashow.cloud.pay.service.wallet.bo.WalletTransactionCreateReqBO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; + +import static com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO.TYPE_EXPENSE; +import static com.tashow.cloud.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO.TYPE_INCOME; + +/** + * 钱包流水 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +@Validated +public class PayWalletTransactionServiceImpl implements PayWalletTransactionService { + + /** + * 钱包流水的 no 前缀 + */ + private static final String WALLET_NO_PREFIX = "W"; + + @Resource + private PayWalletService payWalletService; + @Resource + private PayWalletTransactionMapper payWalletTransactionMapper; + @Resource + private PayNoRedisDAO noRedisDAO; + + @Override + public PageResult getWalletTransactionPage(Long userId, Integer userType, + AppPayWalletTransactionPageReqVO pageVO) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType); + return payWalletTransactionMapper.selectPage(wallet.getId(), pageVO.getType(), pageVO, pageVO.getCreateTime()); + } + + @Override + public PageResult getWalletTransactionPage(PayWalletTransactionPageReqVO pageVO) { + // 基于 userId + userType 查询钱包 + if (pageVO.getWalletId() == null + && ObjectUtil.isAllNotEmpty(pageVO.getUserId(), pageVO.getUserType())) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(pageVO.getUserId(), pageVO.getUserType()); + if (wallet != null) { + pageVO.setWalletId(wallet.getId()); + } + } + + // 查询分页 + return payWalletTransactionMapper.selectPage(pageVO.getWalletId(), null, pageVO, null); + } + + @Override + public PayWalletTransactionDO createWalletTransaction(WalletTransactionCreateReqBO bo) { + PayWalletTransactionDO transaction = PayWalletTransactionConvert.INSTANCE.convert(bo) + .setNo(noRedisDAO.generate(WALLET_NO_PREFIX)); + payWalletTransactionMapper.insert(transaction); + return transaction; + } + + @Override + public PayWalletTransactionDO getWalletTransactionByNo(String no) { + return payWalletTransactionMapper.selectByNo(no); + } + + @Override + public PayWalletTransactionDO getWalletTransaction(String bizId, PayWalletBizTypeEnum type) { + return payWalletTransactionMapper.selectByBiz(bizId, type.getType()); + } + + @Override + public AppPayWalletTransactionSummaryRespVO getWalletTransactionSummary(Long userId, Integer userType, LocalDateTime[] createTime) { + PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType); + return new AppPayWalletTransactionSummaryRespVO() + .setTotalExpense(payWalletTransactionMapper.selectPriceSum(wallet.getId(), TYPE_EXPENSE, createTime)) + .setTotalIncome(payWalletTransactionMapper.selectPriceSum(wallet.getId(), TYPE_INCOME, createTime)); + } + +} diff --git a/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/bo/WalletTransactionCreateReqBO.java b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/bo/WalletTransactionCreateReqBO.java new file mode 100644 index 0000000..cdadac6 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/java/com/tashow/cloud/pay/service/wallet/bo/WalletTransactionCreateReqBO.java @@ -0,0 +1,58 @@ +package com.tashow.cloud.pay.service.wallet.bo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 创建钱包流水 BO + * + * @author jason + */ +@Data +public class WalletTransactionCreateReqBO { + + /** + * 钱包编号 + * + */ + @NotNull(message = "钱包编号不能为空") + private Long walletId; + + /** + * 交易金额,单位分 + * + * 正值表示余额增加,负值表示余额减少 + */ + @NotNull(message = "交易金额不能为空") + private Integer price; + + /** + * 交易后余额,单位分 + */ + @NotNull(message = "交易后余额不能为空") + private Integer balance; + + /** + * 关联业务分类 + * + * 枚举 {@link PayWalletBizTypeEnum#getType()} + */ + @NotNull(message = "关联业务分类不能为空") + @InEnum(PayWalletBizTypeEnum.class) + private Integer bizType; + + /** + * 关联业务编号 + */ + @NotEmpty(message = "关联业务编号不能为空") + private String bizId; + + /** + * 流水说明 + */ + @NotEmpty(message = "流水说明不能为空") + private String title; +} diff --git a/tashow-module/tashow-module-pay/src/main/resources/application-dev.yaml b/tashow-module/tashow-module-pay/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..17c1e15 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/resources/application-dev.yaml @@ -0,0 +1,113 @@ +--- #################### 注册中心 + 配置中心相关配置 #################### + +spring: + cloud: + nacos: + server-addr: 127.0.0.1:8848 # Nacos 服务器地址 + username: # Nacos 账号 + password: # Nacos 密码 + discovery: # 【配置中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 + config: # 【注册中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + 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: + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + username: root + password: 123456 + slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + data: + 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: + 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 # 支付渠道的【退款】回调地址 + transfer-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址 + demo: true # 关闭演示模式 diff --git a/tashow-module/tashow-module-pay/src/main/resources/application-local.yaml b/tashow-module/tashow-module-pay/src/main/resources/application-local.yaml new file mode 100644 index 0000000..97082b3 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/resources/application-local.yaml @@ -0,0 +1,141 @@ +--- #################### 注册中心 + 配置中心相关配置 #################### + +spring: + cloud: + nacos: + server-addr: 127.0.0.1:8848 # Nacos 服务器地址 + username: # Nacos 账号 + password: # Nacos 密码 + discovery: # 【配置中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 + config: # 【注册中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 + 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: 1 # 初始连接数 + min-idle: 1 # 最小连接池数量 + 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: + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例 + # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 + # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 + # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例 + # url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例 + username: root + password: 123456 + # username: sa # SQL Server 连接的示例 + # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例 + # username: SYSDBA # DM 连接的示例 + # password: SYSDBA # DM 连接的示例 + slave: # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + data: + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 + # password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### +xxl: + job: + enabled: false # 是否开启调度中心,默认为 true 开启 + admin: + addresses: http://127.0.0.1:9090/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 + cn.iocoder.yudao.module.pay.dal.mysql.notify.com.tashow.cloud.payapi.dal.mysql.notify.PayNotifyTaskMapper: INFO # 配置 PayNotifyTaskMapper 的日志级别为 info + org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示 + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + env: # 多环境的配置项 + tag: ${HOSTNAME} + security: + mock-enable: true + access-log: # 访问日志的配置项 + 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 # 支付渠道的【退款】回调地址 + transfer-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址 \ No newline at end of file diff --git a/tashow-module/tashow-module-pay/src/main/resources/application.yaml b/tashow-module/tashow-module-pay/src/main/resources/application.yaml new file mode 100644 index 0000000..c3f0203 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/main/resources/application.yaml @@ -0,0 +1,125 @@ +spring: + application: + name: pay-server + + profiles: + active: local + + main: + allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 + allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务 + + config: + import: + - optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置 + - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置 + + # Servlet 配置 + servlet: + # 文件上传相关配置项 + multipart: + max-file-size: 16MB # 单个文件大小 + max-request-size: 32MB # 设置总上传的文件大小 + + # 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 小时 + +server: + port: 48085 + +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + +--- #################### 接口文档配置 #################### + +springdoc: + api-docs: + enabled: true # 1. 是否开启 Swagger 接文档的元数据 + path: /v3/api-docs + swagger-ui: + enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面 + path: /swagger-ui + default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档 + +knife4j: + enable: false # TODO 芋艿:需要关闭增强,具体原因见:https://github.com/xiaoymin/knife4j/issues/874 + setting: + language: zh_cn + +# MyBatis Plus 的配置项 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 + global-config: + db-config: + 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) + banner: false # 关闭控制台的 Banner 打印 + type-aliases-package: ${yudao.info.base-package}.dal.dataobject + encryptor: + password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成 + +mybatis-plus-join: + banner: false # 关闭控制台的 Banner 打印 + +# Spring Data Redis 配置 +spring: + data: + redis: + repositories: + enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度 + +# VO 转换(数据翻译)相关 +easy-trans: + is-enable-global: true # 启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口 + +--- #################### 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 的地址 + xss: + enable: false + exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + swagger: + title: 管理后台 + description: 提供管理员管理的所有功能 + version: ${yudao.info.version} + tenant: # 多租户相关配置项 + enable: true + ignore-urls: + - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号 + +debug: false diff --git a/tashow-module/tashow-module-pay/src/main/resources/logback-spring.xml b/tashow-module/tashow-module-pay/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b1b9f3f --- /dev/null +++ b/tashow-module/tashow-module-pay/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/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/app/PayAppServiceTest.java b/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/app/PayAppServiceTest.java new file mode 100644 index 0000000..9a94c61 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/app/PayAppServiceTest.java @@ -0,0 +1,260 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.app.vo.PayAppCreateReqVO; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppPageReqVO; +import com.tashow.cloud.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.mysql.app.PayAppMapper; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.tashow.cloud.pay.service.refund.PayRefundService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +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 + */ +@Disabled // TODO 芋艿:后续 fix 补充的单测 +@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/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/channel/PayChannelServiceTest.java b/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/channel/PayChannelServiceTest.java new file mode 100644 index 0000000..af289f7 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/channel/PayChannelServiceTest.java @@ -0,0 +1,337 @@ +package com.tashow.cloud.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.PayClient; +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 com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import com.tashow.cloud.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.pay.dal.mysql.channel.PayChannelMapper; +import com.alibaba.fastjson.JSON; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import jakarta.annotation.Resource; +import jakarta.validation.Validator; +import java.util.Collections; +import java.util.List; + +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 static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@Import({PayChannelServiceImpl.class}) +public class PayChannelServiceTest extends BaseDbUnitTest { + + @Resource + private PayChannelServiceImpl channelService; + + @Resource + private PayChannelMapper channelMapper; + + @MockBean + private PayClientFactory payClientFactory; + @MockBean + private Validator validator; + + @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()); + } + + @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()); + } + + @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)); + } + + @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); + } + + @Test + public void testGetPayClient() { + // mock 数据 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(channel); + // mock 参数 + Long id = channel.getId(); + // mock 方法 + PayClient mockClient = mock(PayClient.class); + when(payClientFactory.createOrUpdatePayClient(eq(id), eq(channel.getCode()), eq(channel.getConfig()))) + .thenReturn(mockClient); + + // 调用 + PayClient client = channelService.getPayClient(id); + // 断言 + assertSame(client, mockClient); + } + + 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/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/notify/PayNotifyServiceTest.java b/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/notify/PayNotifyServiceTest.java new file mode 100644 index 0000000..93717c7 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/notify/PayNotifyServiceTest.java @@ -0,0 +1,353 @@ +package com.tashow.cloud.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 com.tashow.cloud.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.tashow.cloud.pay.dal.dataobject.notify.PayNotifyTaskDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.refund.PayRefundDO; +import com.tashow.cloud.pay.dal.mysql.notify.PayNotifyLogMapper; +import com.tashow.cloud.pay.dal.mysql.notify.PayNotifyTaskMapper; +import com.tashow.cloud.pay.dal.redis.notify.PayNotifyLockRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import com.tashow.cloud.pay.framework.job.config.PayJobConfiguration; +import com.tashow.cloud.pay.service.order.PayOrderService; +import com.tashow.cloud.pay.service.refund.PayRefundService; +import com.tashow.cloud.pay.service.refund.PayRefundServiceImpl; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +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 芋艿 + */ +@Disabled // TODO 芋艿:后续 fix 补充的单测 +@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/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/order/PayOrderServiceTest.java b/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/order/PayOrderServiceTest.java new file mode 100644 index 0000000..3ea404f --- /dev/null +++ b/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/order/PayOrderServiceTest.java @@ -0,0 +1,1105 @@ +package com.tashow.cloud.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.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 com.tashow.cloud.pay.controller.admin.order.vo.PayOrderExportReqVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderPageReqVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import com.tashow.cloud.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderExtensionDO; +import com.tashow.cloud.pay.dal.mysql.order.PayOrderExtensionMapper; +import com.tashow.cloud.pay.dal.mysql.order.PayOrderMapper; +import com.tashow.cloud.pay.dal.redis.no.PayNoRedisDAO; +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; +import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; +import com.tashow.cloud.pay.framework.pay.config.PayProperties; +import com.tashow.cloud.pay.service.app.PayAppService; +import com.tashow.cloud.pay.service.channel.PayChannelService; +import com.tashow.cloud.pay.service.notify.PayNotifyService; +import jakarta.annotation.Resource; +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 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 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.setAppKey("demo").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.getAppKey()))).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.setAppKey("demo").setMerchantOrderId("10")); + // mock 数据 + PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> o.setAppId(1L).setMerchantOrderId("10")); + orderMapper.insert(dbOrder); + // mock 方法 + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1")); + when(appService.validPayApp(eq(reqDTO.getAppKey()))).thenReturn(app); + + // 调用 + 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), PAY_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), PAY_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), PAY_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), PAY_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(channelService.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), + PAY_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(channelService.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), + PAY_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(channelService.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), + PAY_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(channelService.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), + PAY_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), + PAY_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), + PAY_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), + PAY_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(10D)); + 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(10D).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), + PAY_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), + PAY_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), + PAY_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), + PAY_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(channelService.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(channelService.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(channelService.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, never()).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(channelService.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(channelService.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(channelService.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(channelService.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/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/refund/PayRefundServiceTest.java b/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/refund/PayRefundServiceTest.java new file mode 100644 index 0000000..3451db5 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/test/java/com/tashow/cloud/pay/service/refund/PayRefundServiceTest.java @@ -0,0 +1,700 @@ +package com.tashow.cloud.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.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 com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import com.tashow.cloud.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import com.tashow.cloud.pay.dal.dataobject.app.PayAppDO; +import com.tashow.cloud.pay.dal.dataobject.channel.PayChannelDO; +import com.tashow.cloud.pay.dal.dataobject.order.PayOrderDO; +import com.tashow.cloud.pay.dal.dataobject.refund.PayRefundDO; +import com.tashow.cloud.pay.dal.mysql.refund.PayRefundMapper; +import com.tashow.cloud.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 com.tashow.cloud.pay.framework.pay.config.PayProperties; +import com.tashow.cloud.pay.service.app.PayAppService; +import com.tashow.cloud.pay.service.channel.PayChannelService; +import com.tashow.cloud.pay.service.notify.PayNotifyService; +import com.tashow.cloud.pay.service.order.PayOrderService; +import jakarta.annotation.Resource; +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 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 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.setAppKey("demo")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).thenReturn(app); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + PAY_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.setAppKey("demo").setMerchantOrderId("100")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).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), + PAY_ORDER_REFUND_FAIL_STATUS_ERROR); + } + + @Test + public void testCreateRefund_refundPriceExceed() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(10)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).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.setAppKey("demo").setMerchantOrderId("100").setPrice(10)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).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.setAppKey("demo").setMerchantOrderId("100").setPrice(9)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).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.setAppKey("demo").setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).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(channelService.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.setAppKey("demo").setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).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(channelService.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.setAppKey("demo").setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq("demo"))).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(channelService.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(channelService.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(channelService.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/tashow-module/tashow-module-pay/src/test/resources/application-unit-test.yaml b/tashow-module/tashow-module-pay/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..93023bd --- /dev/null +++ b/tashow-module/tashow-module-pay/src/test/resources/application-unit-test.yaml @@ -0,0 +1,53 @@ +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 默认的配置足够使用,一般不需要进行调优 + data: + 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) + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + info: + base-package: cn.iocoder.yudao.module diff --git a/tashow-module/tashow-module-pay/src/test/resources/logback.xml b/tashow-module/tashow-module-pay/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/tashow-module/tashow-module-pay/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tashow-module/tashow-module-pay/src/test/resources/sql/clean.sql b/tashow-module/tashow-module-pay/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..91fff0c --- /dev/null +++ b/tashow-module/tashow-module-pay/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/tashow-module/tashow-module-pay/src/test/resources/sql/create_tables.sql b/tashow-module/tashow-module-pay/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..3f9f764 --- /dev/null +++ b/tashow-module/tashow-module-pay/src/test/resources/sql/create_tables.sql @@ -0,0 +1,147 @@ +CREATE TABLE IF NOT EXISTS "pay_app" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "app_key" varchar(64) NOT NULL, + "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 = '支付通知日志'; diff --git a/tashow-module/tashow-module-system/src/main/resources/application-local.yaml b/tashow-module/tashow-module-system/src/main/resources/application-local.yaml index 03bada2..04c956f 100644 --- a/tashow-module/tashow-module-system/src/main/resources/application-local.yaml +++ b/tashow-module/tashow-module-system/src/main/resources/application-local.yaml @@ -5,11 +5,11 @@ spring: username: nacos # Nacos 账号 password: nacos # Nacos 密码 discovery: # 【配置中心】配置项 - namespace: dev # 命名空间。这里使用 dev 开发环境 + namespace: liwq # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP metadata: version: 1.0.0 # 服务实例的版本号,可用于灰度发布 config: # 【注册中心】配置项 - namespace: dev # 命名空间。这里使用 dev 开发环境 + namespace: liwq # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP diff --git a/tashow-module/tashow-module-trade/Dockerfile b/tashow-module/tashow-module-trade/Dockerfile new file mode 100644 index 0000000..b6aef79 --- /dev/null +++ b/tashow-module/tashow-module-trade/Dockerfile @@ -0,0 +1,19 @@ +## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性 +## 感谢复旦核博士的建议!灰子哥,牛皮! +FROM eclipse-temurin:21-jre + +## 创建目录,并使用它作为工作目录 +RUN mkdir -p /yudao-module-trade-biz +WORKDIR /yudao-module-trade-biz +## 将后端项目的 Jar 文件,复制到镜像中 +COPY ./target/yudao-module-trade-biz.jar app.jar + +## 设置 TZ 时区 +## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖 +ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx512m" + +## 暴露后端项目的 48080 端口 +EXPOSE 48102 + +## 启动后端项目 +CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar diff --git a/tashow-module/tashow-module-trade/pom.xml b/tashow-module/tashow-module-trade/pom.xml new file mode 100644 index 0000000..4a27f22 --- /dev/null +++ b/tashow-module/tashow-module-trade/pom.xml @@ -0,0 +1,100 @@ + + + + com.tashow.cloud + tashow-module + ${revision} + + 4.0.0 + tashow-module-trade + jar + + ${project.artifactId} + + trade 模块,主要实现交易相关功能 + 例如:订单、退款、购物车等功能。 + + + + + + com.tashow.cloud + tashow-framework-env + + + + + com.tashow.cloud + tashow-trade-api + + + + + com.tashow.cloud + tashow-framework-web + + + + com.tashow.cloud + tashow-framework-security + + + + + com.tashow.cloud + tashow-data-mybatis + + + + com.tashow.cloud + tashow-data-redis + + + + + com.tashow.cloud + tashow-framework-rpc + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.tashow.cloud + tashow-data-excel + + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + + diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/TradeServerApplication.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/TradeServerApplication.java new file mode 100644 index 0000000..82289a9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/TradeServerApplication.java @@ -0,0 +1,30 @@ +package com.tashow.cloud.trade; + +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 TradeServerApplication { + + 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(TradeServerApplication.class, args); + + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/api/order/TradeOrderApiImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/api/order/TradeOrderApiImpl.java new file mode 100644 index 0000000..7ac927e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/api/order/TradeOrderApiImpl.java @@ -0,0 +1,47 @@ +package com.tashow.cloud.trade.api.order; + +import com.tashow.cloud.common.pojo.CommonResult; +import com.tashow.cloud.trade.convert.order.TradeOrderConvert; +import com.tashow.cloud.trade.service.order.TradeOrderQueryService; +import com.tashow.cloud.trade.service.order.TradeOrderUpdateService; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collection; +import java.util.List; + +import static com.tashow.cloud.common.pojo.CommonResult.success; + + +/** + * 订单 API 接口实现类 + * + * @author HUIHUI + */ +@RestController // 提供 RESTful API 接口,给 Feign 调用 +@Validated +public class TradeOrderApiImpl implements TradeOrderApi { + + @Resource + private TradeOrderUpdateService tradeOrderUpdateService; + @Resource + private TradeOrderQueryService tradeOrderQueryService; + + @Override + public CommonResult> getOrderList(Collection ids) { + return success(TradeOrderConvert.INSTANCE.convertList04(tradeOrderQueryService.getOrderList(ids))); + } + + @Override + public CommonResult getOrder(Long id) { + return success(TradeOrderConvert.INSTANCE.convert(tradeOrderQueryService.getOrder(id))); + } + + @Override + public CommonResult cancelPaidOrder(Long userId, Long orderId, Integer cancelType) { + tradeOrderUpdateService.cancelPaidOrder(userId, orderId, cancelType); + return success(true); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/api/package-info.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/api/package-info.java new file mode 100644 index 0000000..88efea9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/api/package-info.java @@ -0,0 +1 @@ +package com.tashow.cloud.trade.api; \ No newline at end of file diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/AfterSaleController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/AfterSaleController.java new file mode 100644 index 0000000..5d0fcd6 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/AfterSaleController.java @@ -0,0 +1,145 @@ +package com.tashow.cloud.trade.controller.admin.aftersale; + +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.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.*; +import com.tashow.cloud.trade.controller.admin.aftersale.vo.*; +import com.tashow.cloud.trade.convert.aftersale.AfterSaleConvert; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleDO; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleLogDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import com.tashow.cloud.trade.service.aftersale.AfterSaleLogService; +import com.tashow.cloud.trade.service.aftersale.AfterSaleService; +import com.tashow.cloud.trade.service.order.TradeOrderQueryService; +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 jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +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.convertSet; +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("/trade/after-sale") +@Validated +@Slf4j +public class AfterSaleController { + + @Resource + private AfterSaleService afterSaleService; + @Resource + private TradeOrderQueryService tradeOrderQueryService; + @Resource + private AfterSaleLogService afterSaleLogService; + @Resource + private MemberUserApi memberUserApi; + + @GetMapping("/page") + @Operation(summary = "获得售后订单分页") + @PreAuthorize("@ss.hasPermission('trade:after-sale:query')") + public CommonResult> getAfterSalePage(@Valid AfterSalePageReqVO pageVO) { + // 查询售后 + PageResult pageResult = afterSaleService.getAfterSalePage(pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 查询会员 + Map memberUsers = memberUserApi.getUserMap( + convertSet(pageResult.getList(), AfterSaleDO::getUserId)); + return success(AfterSaleConvert.INSTANCE.convertPage(pageResult, memberUsers)); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得售后订单详情") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:after-sale:query')") + public CommonResult getOrderDetail(@RequestParam("id") Long id) { + // 查询订单 + AfterSaleDO afterSale = afterSaleService.getAfterSale(id); + if (afterSale == null) { + return success(null); + } + + // 查询订单 + TradeOrderDO order = tradeOrderQueryService.getOrder(afterSale.getOrderId()); + // 查询订单项 + TradeOrderItemDO orderItem = tradeOrderQueryService.getOrderItem(afterSale.getOrderItemId()); + // 拼接数据 + MemberUserRespDTO user = memberUserApi.getUser(afterSale.getUserId()).getCheckedData(); + List logs = afterSaleLogService.getAfterSaleLogList(afterSale.getId()); + return success(AfterSaleConvert.INSTANCE.convert(afterSale, order, orderItem, user, logs)); + } + + @PutMapping("/agree") + @Operation(summary = "同意售后") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:after-sale:agree')") + public CommonResult agreeAfterSale(@RequestParam("id") Long id) { + afterSaleService.agreeAfterSale(getLoginUserId(), id); + return success(true); + } + + @PutMapping("/disagree") + @Operation(summary = "拒绝售后") + @PreAuthorize("@ss.hasPermission('trade:after-sale:disagree')") + public CommonResult disagreeAfterSale(@RequestBody AfterSaleDisagreeReqVO confirmReqVO) { + afterSaleService.disagreeAfterSale(getLoginUserId(), confirmReqVO); + return success(true); + } + + @PutMapping("/receive") + @Operation(summary = "确认收货") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:after-sale:receive')") + public CommonResult receiveAfterSale(@RequestParam("id") Long id) { + afterSaleService.receiveAfterSale(getLoginUserId(), id); + return success(true); + } + + @PutMapping("/refuse") + @Operation(summary = "拒绝收货") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:after-sale:receive')") + public CommonResult refuseAfterSale(AfterSaleRefuseReqVO refuseReqVO) { + afterSaleService.refuseAfterSale(getLoginUserId(), refuseReqVO); + return success(true); + } + + @PutMapping("/refund") + @Operation(summary = "确认退款") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:after-sale:refund')") + public CommonResult refundAfterSale(@RequestParam("id") Long id) { + afterSaleService.refundAfterSale(getLoginUserId(), getClientIP(), id); + return success(true); + } + + @PostMapping("/update-refunded") + @Operation(summary = "更新售后订单为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现 + public CommonResult updateAfterRefund(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { + // 目前业务逻辑,不需要做任何事情 + // 当然,退款会有小概率会失败的情况,可以监控失败状态,进行告警 + log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO); + return success(true); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/TradeAfterSaleController.http b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/TradeAfterSaleController.http new file mode 100644 index 0000000..f342e94 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/TradeAfterSaleController.http @@ -0,0 +1,33 @@ +### 获得交易售后分页 => 成功 +GET {{baseUrl}}/trade/after-sale/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +### 同意售后 => 成功 +PUT {{baseUrl}}/trade/after-sale/agree?id=7 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} +Content-Type: application/json + +### 拒绝售后 => 成功 +PUT {{baseUrl}}/trade/after-sale/disagree +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} +Content-Type: application/json + +{ + "id": 6, + "auditReason": "阿巴巴" +} + +### 确认退款 => 成功 +PUT {{baseUrl}}/trade/after-sale/refund?id=6 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} +Content-Type: application/json + +### 确认收货 => 成功 +PUT {{baseUrl}}/trade/after-sale/receive?id=7 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} +Content-Type: application/json diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleBaseVO.java new file mode 100644 index 0000000..9e5a37e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleBaseVO.java @@ -0,0 +1,119 @@ +package com.tashow.cloud.trade.controller.admin.aftersale.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* 交易售后 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class AfterSaleBaseVO { + + @Schema(description = "售后流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "202211190847450020500077") + @NotNull(message = "售后流水号不能为空") + private String no; + + @Schema(description = "售后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "售后状态不能为空") + private Integer status; + + @Schema(description = "售后类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + @NotNull(message = "售后类型不能为空") + private Integer type; + + @Schema(description = "售后方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "售后方式不能为空") + private Integer way; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30337") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "申请原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "不喜欢") + @NotNull(message = "申请原因不能为空") + private String applyReason; + + @Schema(description = "补充描述", example = "你说的对") + private String applyDescription; + + @Schema(description = "补充凭证图片", example = "https://www.iocoder.cn/1.png") + private List applyPicUrls; + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18078") + @NotNull(message = "订单编号不能为空") + private Long orderId; + + @Schema(description = "订单流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022111917190001") + @NotNull(message = "订单流水号不能为空") + private String orderNo; + + @Schema(description = "订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "572") + @NotNull(message = "订单项编号不能为空") + private Long orderItemId; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2888") + @NotNull(message = "商品 SPU 编号不能为空") + private Long spuId; + + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotNull(message = "商品 SPU 名称不能为空") + private String spuName; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15657") + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + @Schema(description = "商品图片", example = "https://www.iocoder.cn/2.png") + private String picUrl; + + @Schema(description = "购买数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20012") + @NotNull(message = "购买数量不能为空") + private Integer count; + + @Schema(description = "审批时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime auditTime; + + @Schema(description = "审批人", example = "30835") + private Long auditUserId; + + @Schema(description = "审批备注", example = "不香") + private String auditReason; + + @Schema(description = "退款金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "18077") + @NotNull(message = "退款金额,单位:分不能为空") + private Integer refundPrice; + + @Schema(description = "支付退款编号", example = "10271") + private Long payRefundId; + + @Schema(description = "退款时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime refundTime; + + @Schema(description = "退货物流公司编号", example = "10") + private Long logisticsId; + + @Schema(description = "退货物流单号", example = "610003952009") + private String logisticsNo; + + @Schema(description = "退货时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime deliveryTime; + + @Schema(description = "收货时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime receiveTime; + + @Schema(description = "收货备注", example = "不喜欢") + private String receiveReason; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleDetailRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleDetailRespVO.java new file mode 100644 index 0000000..b7d37c6 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleDetailRespVO.java @@ -0,0 +1,52 @@ +package com.tashow.cloud.trade.controller.admin.aftersale.vo; + +import com.tashow.cloud.trade.controller.admin.aftersale.vo.log.AfterSaleLogRespVO; +import com.tashow.cloud.trade.controller.admin.base.member.user.MemberUserRespVO; +import com.tashow.cloud.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderBaseVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderItemBaseVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 售后订单的详情 Response VO") +@Data +public class AfterSaleDetailRespVO extends AfterSaleBaseVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + + + /** + * 订单基本信息 + */ + private TradeOrderBaseVO order; + /** + * 订单项列表 + */ + private OrderItem orderItem; + + /** + * 用户信息 + */ + private MemberUserRespVO user; + + /** + * 售后日志 + */ + private List logs; + + @Schema(description = "管理后台 - 交易订单的详情的订单项目") + @Data + public static class OrderItem extends TradeOrderItemBaseVO { + + /** + * 属性数组 + */ + private List properties; + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleDisagreeReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleDisagreeReqVO.java new file mode 100644 index 0000000..f68b5f3 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleDisagreeReqVO.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.trade.controller.admin.aftersale.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 交易售后拒绝 Request VO") +@Data +public class AfterSaleDisagreeReqVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "售后编号不能为空") + private Long id; + + @Schema(description = "审批备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + @NotEmpty(message = "审批备注不能为空") + private String auditReason; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java new file mode 100644 index 0000000..d20058f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java @@ -0,0 +1,52 @@ +package com.tashow.cloud.trade.controller.admin.aftersale.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleStatusEnum; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleTypeEnum; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleWayEnum; +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 AfterSalePageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "1024") + private Long userId; + + @Schema(description = "售后流水号", example = "202211190847450020500077") + private String no; + + @Schema(description = "售后状态", example = "10") + @InEnum(value = AfterSaleStatusEnum.class, message = "售后状态必须是 {value}") + private Integer status; + + @Schema(description = "售后类型", example = "20") + @InEnum(value = AfterSaleTypeEnum.class, message = "售后类型必须是 {value}") + private Integer type; + + @Schema(description = "售后方式", example = "10") + @InEnum(value = AfterSaleWayEnum.class, message = "售后方式必须是 {value}") + private Integer way; + + @Schema(description = "订单编号", example = "18078") + private String orderNo; + + @Schema(description = "商品 SPU 名称", example = "李四") + private String spuName; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleRefuseReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleRefuseReqVO.java new file mode 100644 index 0000000..1790b73 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleRefuseReqVO.java @@ -0,0 +1,20 @@ +package com.tashow.cloud.trade.controller.admin.aftersale.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 交易售后拒绝收货 Request VO") +@Data +public class AfterSaleRefuseReqVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "售后编号不能为空") + private Long id; + + @Schema(description = "收货备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + @NotNull(message = "收货备注不能为空") + private String refuseMemo; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleRespPageItemVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleRespPageItemVO.java new file mode 100644 index 0000000..93b6c94 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/AfterSaleRespPageItemVO.java @@ -0,0 +1,35 @@ +package com.tashow.cloud.trade.controller.admin.aftersale.vo; + +import com.tashow.cloud.trade.controller.admin.base.member.user.MemberUserRespVO; +import com.tashow.cloud.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +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 AfterSaleRespPageItemVO extends AfterSaleBaseVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "27630") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + /** + * 商品属性数组 + */ + private List properties; + + /** + * 用户信息 + */ + private MemberUserRespVO user; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/log/AfterSaleLogRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/log/AfterSaleLogRespVO.java new file mode 100644 index 0000000..a537f4b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/aftersale/vo/log/AfterSaleLogRespVO.java @@ -0,0 +1,37 @@ +package com.tashow.cloud.trade.controller.admin.aftersale.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 交易售后日志 Response VO") +@Data +public class AfterSaleLogRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20669") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22634") + private Long userId; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer userType; + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3023") + private Long afterSaleId; + + @Schema(description = "售后状态(之前)", example = "2") + private Integer beforeStatus; + + @Schema(description = "售后状态(之后)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer afterStatus; + + @Schema(description = "操作明细", requiredMode = Schema.RequiredMode.REQUIRED, example = "维权完成,退款金额:¥37776.00") + private String content; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/member/package-info.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/member/package-info.java new file mode 100644 index 0000000..bd77464 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/member/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位符,可忽略 + */ +package com.tashow.cloud.trade.controller.admin.base.member; diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/member/user/MemberUserRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/member/user/MemberUserRespVO.java new file mode 100644 index 0000000..445c67c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/member/user/MemberUserRespVO.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.trade.controller.admin.base.member.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 会员用户 Response VO") +@Data +public class MemberUserRespVO { + + @Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String nickname; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/package-info.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/package-info.java new file mode 100644 index 0000000..9f93ed2 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/package-info.java @@ -0,0 +1,4 @@ +/** + * 放置该模块通用的 VO 类 + */ +package com.tashow.cloud.trade.controller.admin.base; diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/product/property/ProductPropertyValueDetailRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/product/property/ProductPropertyValueDetailRespVO.java new file mode 100644 index 0000000..ba4e20c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/product/property/ProductPropertyValueDetailRespVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.admin.base.product.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 商品属性值的明细 Response VO") +@Data +public class ProductPropertyValueDetailRespVO { + + @Schema(description = "属性的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long propertyId; + + @Schema(description = "属性的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "颜色") + private String propertyName; + + @Schema(description = "属性值的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long valueId; + + @Schema(description = "属性值的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "红色") + private String valueName; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/system/package-info.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/system/package-info.java new file mode 100644 index 0000000..694594d --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/system/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位符,可忽略 + */ +package com.tashow.cloud.trade.controller.admin.base.system; diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/system/user/UserSimpleBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/system/user/UserSimpleBaseVO.java new file mode 100644 index 0000000..758b358 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/base/system/user/UserSimpleBaseVO.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.trade.controller.admin.base.system.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户精简信息 VO") +@Data +public class UserSimpleBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String nickname; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png") + private String avatar; + +} \ No newline at end of file diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/BrokerageRecordController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/BrokerageRecordController.java new file mode 100644 index 0000000..c3383f1 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/BrokerageRecordController.java @@ -0,0 +1,66 @@ +package com.tashow.cloud.trade.controller.admin.brokerage; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.record.BrokerageRecordRespVO; +import com.tashow.cloud.trade.convert.brokerage.BrokerageRecordConvert; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageRecordDO; +import com.tashow.cloud.trade.service.brokerage.BrokerageRecordService; +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 jakarta.annotation.Resource; +import jakarta.validation.Valid; +import java.util.Map; +import java.util.Set; + +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.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - 佣金记录") +@RestController +@RequestMapping("/trade/brokerage-record") +@Validated +public class BrokerageRecordController { + + @Resource + private BrokerageRecordService brokerageRecordService; + + @Resource + private MemberUserApi memberUserApi; + + @GetMapping("/get") + @Operation(summary = "获得佣金记录") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('trade:brokerage-record:query')") + public CommonResult getBrokerageRecord(@RequestParam("id") Long id) { + BrokerageRecordDO brokerageRecord = brokerageRecordService.getBrokerageRecord(id); + return success(BrokerageRecordConvert.INSTANCE.convert(brokerageRecord)); + } + + @GetMapping("/page") + @Operation(summary = "获得佣金记录分页") + @PreAuthorize("@ss.hasPermission('trade:brokerage-record:query')") + public CommonResult> getBrokerageRecordPage(@Valid BrokerageRecordPageReqVO pageVO) { + PageResult pageResult = brokerageRecordService.getBrokerageRecordPage(pageVO); + + // 查询用户信息 + Set userIds = convertSet(pageResult.getList(), BrokerageRecordDO::getUserId); + userIds.addAll(convertList(pageResult.getList(), BrokerageRecordDO::getSourceUserId)); + Map userMap = memberUserApi.getUserMap(userIds); + // 拼接数据 + return success(BrokerageRecordConvert.INSTANCE.convertPage(pageResult, userMap)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/BrokerageUserController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/BrokerageUserController.java new file mode 100644 index 0000000..f5e7118 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/BrokerageUserController.java @@ -0,0 +1,121 @@ +package com.tashow.cloud.trade.controller.admin.brokerage; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.user.*; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.user.*; +import com.tashow.cloud.trade.convert.brokerage.BrokerageUserConvert; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageUserDO; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; +import com.tashow.cloud.trade.service.brokerage.BrokerageRecordService; +import com.tashow.cloud.trade.service.brokerage.BrokerageUserService; +import com.tashow.cloud.trade.service.brokerage.BrokerageWithdrawService; +import com.tashow.cloud.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO; +import com.tashow.cloud.trade.service.brokerage.bo.UserBrokerageSummaryRespBO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static java.util.Arrays.asList; + +@Tag(name = "管理后台 - 分销用户") +@RestController +@RequestMapping("/trade/brokerage-user") +@Validated +public class BrokerageUserController { + + @Resource + private BrokerageUserService brokerageUserService; + @Resource + private BrokerageRecordService brokerageRecordService; + @Resource + private BrokerageWithdrawService brokerageWithdrawService; + + @Resource + private MemberUserApi memberUserApi; + + @PostMapping("/create") + @Operation(summary = "创建分销用户") + @PreAuthorize("@ss.hasPermission('trade:brokerage-user:create')") + public CommonResult createBrokerageUser(@Valid @RequestBody BrokerageUserCreateReqVO createReqVO) { + return success(brokerageUserService.createBrokerageUser(createReqVO)); + } + + @PutMapping("/update-bind-user") + @Operation(summary = "修改推广员") + @PreAuthorize("@ss.hasPermission('trade:brokerage-user:update-bind-user')") + public CommonResult updateBindUser(@Valid @RequestBody BrokerageUserUpdateBrokerageUserReqVO updateReqVO) { + brokerageUserService.updateBrokerageUserId(updateReqVO.getId(), updateReqVO.getBindUserId()); + return success(true); + } + + @PutMapping("/clear-bind-user") + @Operation(summary = "清除推广员") + @PreAuthorize("@ss.hasPermission('trade:brokerage-user:clear-bind-user')") + public CommonResult clearBindUser(@Valid @RequestBody BrokerageUserClearBrokerageUserReqVO updateReqVO) { + brokerageUserService.updateBrokerageUserId(updateReqVO.getId(), null); + return success(true); + } + + @PutMapping("/update-brokerage-enable") + @Operation(summary = "修改推广资格") + @PreAuthorize("@ss.hasPermission('trade:brokerage-user:update-brokerage-enable')") + public CommonResult updateBrokerageEnabled(@Valid @RequestBody BrokerageUserUpdateBrokerageEnabledReqVO updateReqVO) { + brokerageUserService.updateBrokerageUserEnabled(updateReqVO.getId(), updateReqVO.getEnabled()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得分销用户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('trade:brokerage-user:query')") + public CommonResult getBrokerageUser(@RequestParam("id") Long id) { + BrokerageUserDO brokerageUser = brokerageUserService.getBrokerageUser(id); + // TODO @疯狂:是不是搞成一个统一的 convert? + BrokerageUserRespVO respVO = BrokerageUserConvert.INSTANCE.convert(brokerageUser); + return success(BrokerageUserConvert.INSTANCE.copyTo(memberUserApi.getUser(id).getCheckedData(), respVO)); + } + + @GetMapping("/page") + @Operation(summary = "获得分销用户分页") + @PreAuthorize("@ss.hasPermission('trade:brokerage-user:query')") + public CommonResult> getBrokerageUserPage(@Valid BrokerageUserPageReqVO pageVO) { + // 分页查询 + PageResult pageResult = brokerageUserService.getBrokerageUserPage(pageVO); + + // 查询用户信息 + Set userIds = convertSet(pageResult.getList(), BrokerageUserDO::getId); + Map userMap = memberUserApi.getUserMap(userIds); + // 合计分佣的推广订单 + Map brokerageOrderSummaryMap = brokerageRecordService.getUserBrokerageSummaryMapByUserId( + userIds, BrokerageRecordBizTypeEnum.ORDER.getType(), BrokerageRecordStatusEnum.SETTLEMENT.getStatus()); + // 合计分佣的推广用户 + // TODO @疯狂:转成 map 批量读取 + Map brokerageUserCountMap = convertMap(userIds, + userId -> userId, + userId -> brokerageUserService.getBrokerageUserCountByBindUserId(userId, null)); + // 合计分佣的提现 + // TODO @疯狂:如果未来支持了打款这个动作,可能 status 会不对; + Map withdrawMap = brokerageWithdrawService.getWithdrawSummaryMapByUserId( + userIds, asList(BrokerageWithdrawStatusEnum.AUDIT_SUCCESS, BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS)); + // 拼接返回 + return success(BrokerageUserConvert.INSTANCE.convertPage(pageResult, userMap, brokerageUserCountMap, + brokerageOrderSummaryMap, withdrawMap)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/BrokerageWithdrawController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/BrokerageWithdrawController.java new file mode 100644 index 0000000..b70ada5 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/BrokerageWithdrawController.java @@ -0,0 +1,96 @@ +package com.tashow.cloud.trade.controller.admin.brokerage; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRejectReqVO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRespVO; +import com.tashow.cloud.trade.convert.brokerage.BrokerageWithdrawConvert; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageWithdrawDO; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; +import com.tashow.cloud.trade.service.brokerage.BrokerageWithdrawService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.security.PermitAll; +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 jakarta.annotation.Resource; +import jakarta.validation.Valid; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; + +@Tag(name = "管理后台 - 佣金提现") +@RestController +@RequestMapping("/trade/brokerage-withdraw") +@Validated +@Slf4j +public class BrokerageWithdrawController { + + @Resource + private BrokerageWithdrawService brokerageWithdrawService; + + @Resource + private MemberUserApi memberUserApi; + + @PutMapping("/approve") + @Operation(summary = "通过申请") + @PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:audit')") + public CommonResult approveBrokerageWithdraw(@RequestParam("id") Long id) { + brokerageWithdrawService.auditBrokerageWithdraw(id, + BrokerageWithdrawStatusEnum.AUDIT_SUCCESS, "", getClientIP()); + return success(true); + } + + @PutMapping("/reject") + @Operation(summary = "驳回申请") + @PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:audit')") + public CommonResult rejectBrokerageWithdraw(@Valid @RequestBody BrokerageWithdrawRejectReqVO reqVO) { + brokerageWithdrawService.auditBrokerageWithdraw(reqVO.getId(), + BrokerageWithdrawStatusEnum.AUDIT_FAIL, reqVO.getAuditReason(), getClientIP()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得佣金提现") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:query')") + public CommonResult getBrokerageWithdraw(@RequestParam("id") Long id) { + BrokerageWithdrawDO brokerageWithdraw = brokerageWithdrawService.getBrokerageWithdraw(id); + return success(BrokerageWithdrawConvert.INSTANCE.convert(brokerageWithdraw)); + } + + @GetMapping("/page") + @Operation(summary = "获得佣金提现分页") + @PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:query')") + public CommonResult> getBrokerageWithdrawPage(@Valid BrokerageWithdrawPageReqVO pageVO) { + // 分页查询 + PageResult pageResult = brokerageWithdrawService.getBrokerageWithdrawPage(pageVO); + + // 拼接信息 + Map userMap = memberUserApi.getUserMap( + convertSet(pageResult.getList(), BrokerageWithdrawDO::getUserId)); + return success(BrokerageWithdrawConvert.INSTANCE.convertPage(pageResult, userMap)); + } + + // TODO @luchi:update-transferred,url 改成这个。和 update-paid 、update-refunded 保持一致 + @PostMapping("/update-transfer") + @Operation(summary = "更新转账订单为转账成功") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现 + public CommonResult updateBrokerageWithdrawTransferred(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) { + log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO); + brokerageWithdrawService.updateBrokerageWithdrawTransferred( + Long.parseLong(notifyReqDTO.getMerchantTransferId()), notifyReqDTO.getPayTransferId()); + return success(true); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/record/BrokerageRecordBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/record/BrokerageRecordBaseVO.java new file mode 100644 index 0000000..5f8eed3 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/record/BrokerageRecordBaseVO.java @@ -0,0 +1,65 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +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; + +/** + * 佣金记录 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class BrokerageRecordBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25973") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23353") + @NotEmpty(message = "业务编号不能为空") + private String bizId; + + @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "业务类型不能为空") + private Integer bizType; + + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "标题不能为空") + private String title; + + @Schema(description = "金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "28731") + @NotNull(message = "金额不能为空") + private Integer price; + + @Schema(description = "当前总佣金", requiredMode = Schema.RequiredMode.REQUIRED, example = "13226") + @NotNull(message = "当前总佣金不能为空") + private Integer totalPrice; + + @Schema(description = "说明", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对") + @NotNull(message = "说明不能为空") + private String description; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "冻结时间(天)", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "冻结时间(天)不能为空") + private Integer frozenDays; + + @Schema(description = "解冻时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime unfreezeTime; + + @Schema(description = "来源用户等级") + private Integer sourceUserLevel; + + @Schema(description = "来源用户编号") + private Long sourceUserId; +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/record/BrokerageRecordPageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/record/BrokerageRecordPageReqVO.java new file mode 100644 index 0000000..c525a2f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/record/BrokerageRecordPageReqVO.java @@ -0,0 +1,36 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.record; + +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 BrokerageRecordPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "25973") + private Long userId; + + @Schema(description = "业务类型", example = "1") + private Integer bizType; + + @Schema(description = "状态", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "用户类型", example = "1") + private Integer sourceUserLevel; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/record/BrokerageRecordRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/record/BrokerageRecordRespVO.java new file mode 100644 index 0000000..cd8b024 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/record/BrokerageRecordRespVO.java @@ -0,0 +1,37 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.record; + +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 BrokerageRecordRespVO extends BrokerageRecordBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28896") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + + // ========== 用户信息 ========== + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String userAvatar; + @Schema(description = "用户昵称", example = "李四") + private String userNickname; + + + // ========== 来源用户信息 ========== + + @Schema(description = "来源用户头像", example = "https://www.iocoder.cn/xxx.png") + private String sourceUserAvatar; + @Schema(description = "来源用户昵称", example = "李四") + private String sourceUserNickname; +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserBaseVO.java new file mode 100644 index 0000000..b1c93a9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserBaseVO.java @@ -0,0 +1,43 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +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; + +/** + * 分销用户 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class BrokerageUserBaseVO { + + @Schema(description = "推广员编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4587") + @NotNull(message = "推广员编号不能为空") + private Long bindUserId; + + @Schema(description = "推广员绑定时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime bindUserTime; + + @Schema(description = "推广资格", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "推广资格不能为空") + private Boolean brokerageEnabled; + + @Schema(description = "成为分销员时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime brokerageTime; + + @Schema(description = "可用佣金", requiredMode = Schema.RequiredMode.REQUIRED, example = "11089") + @NotNull(message = "可用佣金不能为空") + private Integer price; + + @Schema(description = "冻结佣金", requiredMode = Schema.RequiredMode.REQUIRED, example = "30916") + @NotNull(message = "冻结佣金不能为空") + private Integer frozenPrice; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserClearBrokerageUserReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserClearBrokerageUserReqVO.java new file mode 100644 index 0000000..1f8a24c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserClearBrokerageUserReqVO.java @@ -0,0 +1,17 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "管理后台 - 分销用户 - 清除推广员 Request VO") +@Data +@ToString(callSuper = true) +public class BrokerageUserClearBrokerageUserReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20019") + @NotNull(message = "用户编号不能为空") + private Long id; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserCreateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserCreateReqVO.java new file mode 100644 index 0000000..48ca055 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserCreateReqVO.java @@ -0,0 +1,18 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 分销用户创建 Request VO") +@Data +public class BrokerageUserCreateReqVO { + + @Schema(description = "分销用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "分销用户编号不能为空") + private Long userId; + + @Schema(description = "推广员编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4587") + private Long bindUserId; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserPageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserPageReqVO.java new file mode 100644 index 0000000..5e4a5a2 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserPageReqVO.java @@ -0,0 +1,37 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.user; + +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 BrokerageUserPageReqVO extends PageParam { + + @Schema(description = "推广员编号", example = "4587") + private Long bindUserId; + + @Schema(description = "推广资格", example = "true") + private Boolean brokerageEnabled; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "用户等级", example = "1") // 注意,这了不是用户的会员等级,而是过滤推广的层级 + private Integer level; + + @Schema(description = "绑定时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] bindUserTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserRespVO.java new file mode 100644 index 0000000..07fd62a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserRespVO.java @@ -0,0 +1,45 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.user; + +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 BrokerageUserRespVO extends BrokerageUserBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20019") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + // ========== 用户信息 ========== + + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xxx.png") + private String avatar; + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + private String nickname; + + // ========== 推广信息 ========== 注意:是包括 1 + 2 级的数据 + + @Schema(description = "推广用户数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20019") + private Integer brokerageUserCount; + @Schema(description = "推广订单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20019") + private Integer brokerageOrderCount; + @Schema(description = "推广订单金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20019") + private Integer brokerageOrderPrice; + + // ========== 提现信息 ========== + + @Schema(description = "已提现金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20019") + private Integer withdrawPrice; + @Schema(description = "已提现次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20019") + private Integer withdrawCount; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserUpdateBrokerageEnabledReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserUpdateBrokerageEnabledReqVO.java new file mode 100644 index 0000000..70f36a7 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserUpdateBrokerageEnabledReqVO.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "管理后台 - 分销用户 - 修改推广员 Request VO") +@Data +@ToString(callSuper = true) +public class BrokerageUserUpdateBrokerageEnabledReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20019") + @NotNull(message = "用户编号不能为空") + private Long id; + + @Schema(description = "推广资格", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "推广资格不能为空") + private Boolean enabled; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserUpdateBrokerageUserReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserUpdateBrokerageUserReqVO.java new file mode 100644 index 0000000..57fbc5e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/user/BrokerageUserUpdateBrokerageUserReqVO.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "管理后台 - 分销用户 - 修改推广员 Request VO") +@Data +@ToString(callSuper = true) +public class BrokerageUserUpdateBrokerageUserReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20019") + @NotNull(message = "用户编号不能为空") + private Long id; + + @Schema(description = "推广员编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4587") + @NotNull(message = "推广员编号不能为空") + private Long bindUserId; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawBaseVO.java new file mode 100644 index 0000000..b27f83c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawBaseVO.java @@ -0,0 +1,68 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +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; + +/** + * 佣金提现 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class BrokerageWithdrawBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11436") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "提现金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "18781") + @NotNull(message = "提现金额不能为空") + private Integer price; + + @Schema(description = "提现手续费", requiredMode = Schema.RequiredMode.REQUIRED, example = "11417") + @NotNull(message = "提现手续费不能为空") + private Integer feePrice; + + @Schema(description = "当前总佣金", requiredMode = Schema.RequiredMode.REQUIRED, example = "18576") + @NotNull(message = "当前总佣金不能为空") + private Integer totalPrice; + + @Schema(description = "提现类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "提现类型不能为空") + private Integer type; + + @Schema(description = "真实姓名", example = "赵六") + private String name; + + @Schema(description = "账号", example = "88677912132") + private String accountNo; + + @Schema(description = "银行名称", example = "1") + private String bankName; + + @Schema(description = "开户地址", example = "海淀支行") + private String bankAddress; + + @Schema(description = "收款码", example = "https://www.iocoder.cn") + private String accountQrCodeUrl; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "审核驳回原因", example = "不对") + private String auditReason; + + @Schema(description = "审核时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime auditTime; + + @Schema(description = "备注", example = "随便") + private String remark; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawPageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawPageReqVO.java new file mode 100644 index 0000000..4534350 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawPageReqVO.java @@ -0,0 +1,47 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum; +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 BrokerageWithdrawPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "11436") + private Long userId; + + @Schema(description = "提现类型", example = "1") + @InEnum(value = BrokerageWithdrawTypeEnum.class, message = "提现类型必须是 {value}") + private Integer type; + + @Schema(description = "真实姓名", example = "赵六") + private String name; + + @Schema(description = "账号", example = "886779132") + private String accountNo; + + @Schema(description = "银行名称", example = "1") + private String bankName; + + @Schema(description = "状态", example = "1") + @InEnum(value = BrokerageWithdrawStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRejectReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRejectReqVO.java new file mode 100644 index 0000000..63aa0fb --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRejectReqVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "管理后台 - 驳回申请 Request VO") +@Data +@ToString(callSuper = true) +public class BrokerageWithdrawRejectReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7161") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "审核驳回原因", example = "不对") + @NotEmpty(message = "审核驳回原因不能为空") + private String auditReason; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRespVO.java new file mode 100644 index 0000000..b752636 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/brokerage/vo/withdraw/BrokerageWithdrawRespVO.java @@ -0,0 +1,25 @@ +package com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw; + +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 BrokerageWithdrawRespVO extends BrokerageWithdrawBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7161") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String userNickname; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/TradeConfigController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/TradeConfigController.java new file mode 100644 index 0000000..69d5682 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/TradeConfigController.java @@ -0,0 +1,53 @@ +package com.tashow.cloud.trade.controller.admin.config; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.tashow.cloud.trade.controller.admin.config.vo.TradeConfigRespVO; +import com.tashow.cloud.trade.controller.admin.config.vo.TradeConfigSaveReqVO; +import com.tashow.cloud.trade.convert.config.TradeConfigConvert; +import com.tashow.cloud.trade.dal.dataobject.config.TradeConfigDO; +import com.tashow.cloud.trade.service.config.TradeConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import jakarta.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 交易中心配置") +@RestController +@RequestMapping("/trade/config") +@Validated +public class TradeConfigController { + + @Resource + private TradeConfigService tradeConfigService; + + @Value("${yudao.tencent-lbs-key}") + private String tencentLbsKey; + + @PutMapping("/save") + @Operation(summary = "更新交易中心配置") + @PreAuthorize("@ss.hasPermission('trade:config:save')") + public CommonResult updateConfig(@Valid @RequestBody TradeConfigSaveReqVO updateReqVO) { + tradeConfigService.saveTradeConfig(updateReqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得交易中心配置") + @PreAuthorize("@ss.hasPermission('trade:config:query')") + public CommonResult getConfig() { + TradeConfigDO config = tradeConfigService.getTradeConfig(); + TradeConfigRespVO configVO = TradeConfigConvert.INSTANCE.convert(config); + if (configVO != null) { + configVO.setTencentLbsKey(tencentLbsKey); + } + return success(configVO); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/vo/TradeConfigBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/vo/TradeConfigBaseVO.java new file mode 100644 index 0000000..d39226f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/vo/TradeConfigBaseVO.java @@ -0,0 +1,100 @@ +package com.tashow.cloud.trade.controller.admin.config.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageBindModeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageEnabledConditionEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import java.util.List; + +/** + * 交易中心配置 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class TradeConfigBaseVO { + + // ========== 售后相关 ========== + + @Schema(description = "售后的退款理由", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "售后的退款理由不能为空") + private List afterSaleRefundReasons; + + @Schema(description = "售后的退货理由", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "售后的退货理由不能为空") + private List afterSaleReturnReasons; + + // ========== 配送相关 ========== + + /** + * 是否启用全场包邮 + */ + @Schema(description = "是否启用全场包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否启用全场包邮不能为空") + private Boolean deliveryExpressFreeEnabled; + + @Schema(description = "全场包邮的最小金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + @NotNull(message = "全场包邮的最小金额不能为空") + @PositiveOrZero(message = "全场包邮的最小金额不能是负数") + private Integer deliveryExpressFreePrice; + + @Schema(description = "是否开启自提", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否开启自提不能为空") + private Boolean deliveryPickUpEnabled; + + // ========== 分销相关 ========== + + @Schema(description = "是否启用分佣", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否启用分佣不能为空") + private Boolean brokerageEnabled; + + @Schema(description = "分佣模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "分佣模式不能为空") + @InEnum(value = BrokerageEnabledConditionEnum.class, message = "分佣模式必须是 {value}") + private Integer brokerageEnabledCondition; + + @Schema(description = "分销关系绑定模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "分销关系绑定模式不能为空") + @InEnum(value = BrokerageBindModeEnum.class, message = "分销关系绑定模式必须是 {value}") + private Integer brokerageBindMode; + + @Schema(description = "分销海报图地址数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[https://www.iocoder.cn/yudao.jpg]") + private List brokeragePosterUrls; + + @Schema(description = "一级返佣比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "一级返佣比例不能为空") + @Range(min = 0, max = 100, message = "一级返佣比例必须在 0 - 100 之间") + private Integer brokerageFirstPercent; + + @Schema(description = "二级返佣比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "二级返佣比例不能为空") + @Range(min = 0, max = 100, message = "二级返佣比例必须在 0 - 100 之间") + private Integer brokerageSecondPercent; + + @Schema(description = "用户提现最低金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + @NotNull(message = "用户提现最低金额不能为空") + @PositiveOrZero(message = "用户提现最低金额不能是负数") + private Integer brokerageWithdrawMinPrice; + + @Schema(description = "用户提现手续费百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + @NotNull(message = "用户提现手续费百分比不能为空") + @PositiveOrZero(message = "用户提现手续费百分比不能是负数") + private Integer brokerageWithdrawFeePercent; + + @Schema(description = "佣金冻结时间(天)", requiredMode = Schema.RequiredMode.REQUIRED, example = "7") + @NotNull(message = "佣金冻结时间(天)不能为空") + @PositiveOrZero(message = "佣金冻结时间不能是负数") + private Integer brokerageFrozenDays; + + @Schema(description = "提现方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "[0, 1]") + @NotEmpty(message = "提现方式不能为空") + @InEnum(value = BrokerageWithdrawTypeEnum.class, message = "提现方式必须是 {value}") + private List brokerageWithdrawTypes; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/vo/TradeConfigRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/vo/TradeConfigRespVO.java new file mode 100644 index 0000000..a55e641 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/vo/TradeConfigRespVO.java @@ -0,0 +1,20 @@ +package com.tashow.cloud.trade.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 交易中心配置 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TradeConfigRespVO extends TradeConfigBaseVO { + + @Schema(description = "自增主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "腾讯地图 KEY", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + private String tencentLbsKey; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/vo/TradeConfigSaveReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/vo/TradeConfigSaveReqVO.java new file mode 100644 index 0000000..d0b7b02 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/config/vo/TradeConfigSaveReqVO.java @@ -0,0 +1,14 @@ +package com.tashow.cloud.trade.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 交易中心配置更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TradeConfigSaveReqVO extends TradeConfigBaseVO { + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/DeliveryExpressController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/DeliveryExpressController.java new file mode 100644 index 0000000..91e59a5 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/DeliveryExpressController.java @@ -0,0 +1,97 @@ +package com.tashow.cloud.trade.controller.admin.delivery; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +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.module.trade.controller.admin.delivery.vo.express.*; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.*; +import com.tashow.cloud.trade.convert.delivery.DeliveryExpressConvert; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.tashow.cloud.trade.service.delivery.DeliveryExpressService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 快递公司") +@RestController +@RequestMapping("/trade/delivery/express") +@Validated +public class DeliveryExpressController { + + @Resource + private DeliveryExpressService deliveryExpressService; + + @PostMapping("/create") + @Operation(summary = "创建快递公司") + @PreAuthorize("@ss.hasPermission('trade:delivery:express:create')") + public CommonResult createDeliveryExpress(@Valid @RequestBody DeliveryExpressCreateReqVO createReqVO) { + return success(deliveryExpressService.createDeliveryExpress(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新快递公司") + @PreAuthorize("@ss.hasPermission('trade:delivery:express:update')") + public CommonResult updateDeliveryExpress(@Valid @RequestBody DeliveryExpressUpdateReqVO updateReqVO) { + deliveryExpressService.updateDeliveryExpress(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除快递公司") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('trade:delivery:express:delete')") + public CommonResult deleteDeliveryExpress(@RequestParam("id") Long id) { + deliveryExpressService.deleteDeliveryExpress(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得快递公司") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('trade:delivery:express:query')") + public CommonResult getDeliveryExpress(@RequestParam("id") Long id) { + DeliveryExpressDO deliveryExpress = deliveryExpressService.getDeliveryExpress(id); + return success(DeliveryExpressConvert.INSTANCE.convert(deliveryExpress)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取快递公司精简信息列表", description = "主要用于前端的下拉选项") + public CommonResult> getSimpleDeliveryExpressList() { + List list = deliveryExpressService.getDeliveryExpressListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(DeliveryExpressConvert.INSTANCE.convertList1(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得快递公司分页") + @PreAuthorize("@ss.hasPermission('trade:delivery:express:query')") + public CommonResult> getDeliveryExpressPage(@Valid DeliveryExpressPageReqVO pageVO) { + PageResult pageResult = deliveryExpressService.getDeliveryExpressPage(pageVO); + return success(DeliveryExpressConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出快递公司 Excel") + @PreAuthorize("@ss.hasPermission('trade:delivery:express:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportDeliveryExpressExcel(@Valid DeliveryExpressExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = deliveryExpressService.getDeliveryExpressList(exportReqVO); + // 导出 Excel + List dataList = DeliveryExpressConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "快递公司.xls", "数据", DeliveryExpressExcelVO.class, dataList); + } +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/DeliveryExpressTemplateController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/DeliveryExpressTemplateController.java new file mode 100644 index 0000000..c106810 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/DeliveryExpressTemplateController.java @@ -0,0 +1,91 @@ +package com.tashow.cloud.trade.controller.admin.delivery; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.expresstemplate.*; +import com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate.*; +import com.tashow.cloud.trade.convert.delivery.DeliveryExpressTemplateConvert; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO; +import com.tashow.cloud.trade.service.delivery.DeliveryExpressTemplateService; +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 jakarta.annotation.Resource; +import jakarta.validation.Valid; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 快递运费模板") +@RestController +@RequestMapping("/trade/delivery/express-template") +@Validated +public class DeliveryExpressTemplateController { + + @Resource + private DeliveryExpressTemplateService deliveryExpressTemplateService; + + @PostMapping("/create") + @Operation(summary = "创建快递运费模板") + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:create')") + public CommonResult createDeliveryExpressTemplate(@Valid @RequestBody DeliveryExpressTemplateCreateReqVO createReqVO) { + return success(deliveryExpressTemplateService.createDeliveryExpressTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新快递运费模板") + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:update')") + public CommonResult updateDeliveryExpressTemplate(@Valid @RequestBody DeliveryExpressTemplateUpdateReqVO updateReqVO) { + deliveryExpressTemplateService.updateDeliveryExpressTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除快递运费模板") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:delete')") + public CommonResult deleteDeliveryExpressTemplate(@RequestParam("id") Long id) { + deliveryExpressTemplateService.deleteDeliveryExpressTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得快递运费模板") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:query')") + public CommonResult getDeliveryExpressTemplate(@RequestParam("id") Long id) { + return success(deliveryExpressTemplateService.getDeliveryExpressTemplate(id)); + } + + @GetMapping("/list") + @Operation(summary = "获得快递运费模板列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:query')") + public CommonResult> getDeliveryExpressTemplateList(@RequestParam("ids") Collection ids) { + List list = deliveryExpressTemplateService.getDeliveryExpressTemplateList(ids); + return success(DeliveryExpressTemplateConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取快递模版精简信息列表", description = "主要用于前端的下拉选项") + public CommonResult> getSimpleTemplateList() { + // 获取运费模版列表,只要开启状态的 + List list = deliveryExpressTemplateService.getDeliveryExpressTemplateList(); + // 排序后,返回给前端 + return success(DeliveryExpressTemplateConvert.INSTANCE.convertList1(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得快递运费模板分页") + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:query')") + public CommonResult> getDeliveryExpressTemplatePage(@Valid DeliveryExpressTemplatePageReqVO pageVO) { + PageResult pageResult = deliveryExpressTemplateService.getDeliveryExpressTemplatePage(pageVO); + return success(DeliveryExpressTemplateConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/DeliveryPickUpStoreController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/DeliveryPickUpStoreController.java new file mode 100644 index 0000000..b6981b4 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/DeliveryPickUpStoreController.java @@ -0,0 +1,114 @@ +package com.tashow.cloud.trade.controller.admin.delivery; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.tashow.cloud.trade.controller.admin.base.system.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.pickup.*; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.*; +import com.tashow.cloud.trade.convert.delivery.DeliveryPickUpStoreConvert; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import com.tashow.cloud.trade.service.delivery.DeliveryPickUpStoreService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 自提门店") +@RestController +@RequestMapping("/trade/delivery/pick-up-store") +@Validated +public class DeliveryPickUpStoreController { + + @Resource + private DeliveryPickUpStoreService deliveryPickUpStoreService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建自提门店") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:create')") + public CommonResult createDeliveryPickUpStore(@Valid @RequestBody DeliveryPickUpStoreCreateReqVO createReqVO) { + return success(deliveryPickUpStoreService.createDeliveryPickUpStore(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新自提门店") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:update')") + public CommonResult updateDeliveryPickUpStore(@Valid @RequestBody DeliveryPickUpStoreUpdateReqVO updateReqVO) { + deliveryPickUpStoreService.updateDeliveryPickUpStore(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除自提门店") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:delete')") + public CommonResult deleteDeliveryPickUpStore(@RequestParam("id") Long id) { + deliveryPickUpStoreService.deleteDeliveryPickUpStore(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得自提门店") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:query')") + public CommonResult getDeliveryPickUpStore(@RequestParam("id") Long id) { + DeliveryPickUpStoreDO deliveryPickUpStore = deliveryPickUpStoreService.getDeliveryPickUpStore(id); + if (deliveryPickUpStore == null) { + return success(null); + } + List verifyUsers = CollUtil.isNotEmpty(deliveryPickUpStore.getVerifyUserIds()) ? + adminUserApi.getUserList(deliveryPickUpStore.getVerifyUserIds()).getCheckedData() : null; + return success(BeanUtils.toBean(deliveryPickUpStore, DeliveryPickUpStoreRespVO.class) + .setVerifyUsers(BeanUtils.toBean(verifyUsers, UserSimpleBaseVO.class))); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得自提门店精简信息列表") + public CommonResult> getSimpleDeliveryPickUpStoreList() { + List list = deliveryPickUpStoreService.getDeliveryPickUpStoreListByStatus( + CommonStatusEnum.ENABLE.getStatus()); + return success(DeliveryPickUpStoreConvert.INSTANCE.convertList1(list)); + } + + @GetMapping("/list") + @Operation(summary = "获得自提门店列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:query')") + public CommonResult> getDeliveryPickUpStoreList(@RequestParam("ids") Collection ids) { + List list = deliveryPickUpStoreService.getDeliveryPickUpStoreList(ids); + return success(DeliveryPickUpStoreConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得自提门店分页") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:query')") + public CommonResult> getDeliveryPickUpStorePage(@Valid DeliveryPickUpStorePageReqVO pageVO) { + PageResult pageResult = deliveryPickUpStoreService.getDeliveryPickUpStorePage(pageVO); + return success(DeliveryPickUpStoreConvert.INSTANCE.convertPage(pageResult)); + } + + @PostMapping("/bind") + @Operation(summary = "绑定自提店员") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:create')") + public CommonResult bindDeliveryPickUpStore(@Valid @RequestBody DeliveryPickUpBindReqVO bindReqVO) { + deliveryPickUpStoreService.bindDeliveryPickUpStore(bindReqVO); + return success(true); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressBaseVO.java new file mode 100644 index 0000000..a1cc0b8 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressBaseVO.java @@ -0,0 +1,34 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.express; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +/** +* 快递公司 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class DeliveryExpressBaseVO { + + @Schema(description = "快递公司编码", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "快递公司编码不能为空") + private String code; + + @Schema(description = "快递公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotNull(message = "快递公司名称不能为空") + private String name; + + @Schema(description = "快递公司logo") + private String logo; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "排序不能为空") + private Integer sort; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressCreateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressCreateReqVO.java new file mode 100644 index 0000000..0af5173 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressCreateReqVO.java @@ -0,0 +1,12 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.express; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "管理后台 - 快递公司创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressCreateReqVO extends DeliveryExpressBaseVO { + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java new file mode 100644 index 0000000..3b020c1 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.express; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 快递公司 Excel VO + */ +@Data +public class DeliveryExpressExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("快递公司编码") + private String code; + + @ExcelProperty("快递公司名称") + private String name; + + @ExcelProperty("快递公司 logo") + private String logo; + + @ExcelProperty("排序") + private Integer sort; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressExportReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressExportReqVO.java new file mode 100644 index 0000000..4e14e5e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressExportReqVO.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.express; + +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") +@Data +public class DeliveryExpressExportReqVO { + + @Schema(description = "快递公司编码") + private String code; + + @Schema(description = "快递公司名称", example = "李四") + private String name; + + @Schema(description = "状态(0正常 1停用)", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressPageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressPageReqVO.java new file mode 100644 index 0000000..262f495 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressPageReqVO.java @@ -0,0 +1,31 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.express; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +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 DeliveryExpressPageReqVO extends PageParam { + + @Schema(description = "快递公司编码") + private String code; + + @Schema(description = "快递公司名称", example = "李四") + private String name; + + @Schema(description = "状态(0正常 1停用)", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressRespVO.java new file mode 100644 index 0000000..6acb9c0 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressRespVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.express; + +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 DeliveryExpressRespVO extends DeliveryExpressBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6592") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressSimpleRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressSimpleRespVO.java new file mode 100644 index 0000000..d23df33 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressSimpleRespVO.java @@ -0,0 +1,24 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.express; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 快递公司精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeliveryExpressSimpleRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6592") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "快递公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "顺丰速运") + @NotNull(message = "快递公司名称不能为空") + private String name; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressUpdateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressUpdateReqVO.java new file mode 100644 index 0000000..d3f4a01 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/express/DeliveryExpressUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.express; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 快递公司更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressUpdateReqVO extends DeliveryExpressBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6592") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateBaseVO.java new file mode 100644 index 0000000..83cd958 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateBaseVO.java @@ -0,0 +1,27 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +/** +* 快递运费模板 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class DeliveryExpressTemplateBaseVO { + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @NotNull(message = "模板名称不能为空") + private String name; + + @Schema(description = "配送计费方式 1:按件 2:按重量 3:按体积", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "配送计费方式 1:按件 2:按重量 3:按体积不能为空") + private Integer chargeMode; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "排序不能为空") + private Integer sort; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateChargeBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateChargeBaseVO.java new file mode 100644 index 0000000..819ee12 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateChargeBaseVO.java @@ -0,0 +1,38 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +/** + * 快递运费模板运费设置 Base VO,提供给添加运费模板使用 + */ +@Data +public class DeliveryExpressTemplateChargeBaseVO { + + @Schema(description = "编号", example = "6592", hidden = true) // 由于想简单一点,复用这个 VO 在更新操作,所以 hidden 为 false + private Long id; + + @Schema(description = "区域编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,120000]") + @NotEmpty(message = "区域编号列表不能为空") + private List areaIds; + + @Schema(description = "首件数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "首件数量不能为空") + private Double startCount; + + @Schema(description = "起步价", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + @NotNull(message = "起步价不能为空") + private Integer startPrice; + + @Schema(description = "续件数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "续件数量不能为空") + private Double extraCount; + + @Schema(description = "额外价", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000") + @NotNull(message = "额外价不能为空") + private Integer extraPrice; +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateCreateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateCreateReqVO.java new file mode 100644 index 0000000..17b0984 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateCreateReqVO.java @@ -0,0 +1,25 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import jakarta.validation.Valid; +import java.util.List; + +@Schema(description = "管理后台 - 快递运费模板创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressTemplateCreateReqVO extends DeliveryExpressTemplateBaseVO { + + @Schema(description = "区域运费列表") + @Valid + private List charges; + + @Schema(description = "包邮区域列表") + @Valid + private List frees; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateDetailRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateDetailRespVO.java new file mode 100644 index 0000000..dd2e05c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateDetailRespVO.java @@ -0,0 +1,25 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - 快递运费模板的详细 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressTemplateDetailRespVO extends DeliveryExpressTemplateBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "371") + private Long id; + + @Schema(description = "运费模板运费设置", requiredMode = Schema.RequiredMode.REQUIRED) + private List charges; + + @Schema(description = "运费模板包邮区域", requiredMode = Schema.RequiredMode.REQUIRED) + private List frees; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateFreeBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateFreeBaseVO.java new file mode 100644 index 0000000..b9b39fb --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateFreeBaseVO.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +/** + * 快递运费模板包邮 Base VO,提供给添加运费模板使用 + */ +@Data +public class DeliveryExpressTemplateFreeBaseVO { + + @Schema(description = "区域编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,120000]") + @NotEmpty(message = "区域编号列表不能为空") + private List areaIds; + + @Schema(description = "包邮金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "5000") + @NotNull(message = "包邮金额不能为空") + private Integer freePrice; + + @Schema(description = "包邮件数", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "包邮件数不能为空") + private Integer freeCount; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplatePageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplatePageReqVO.java new file mode 100644 index 0000000..76bb307 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplatePageReqVO.java @@ -0,0 +1,30 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate; + +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 DeliveryExpressTemplatePageReqVO extends PageParam { + + @Schema(description = "模板名称", example = "王五") + private String name; + + @Schema(description = "配送计费方式 1:按件 2:按重量 3:按体积") + private Integer chargeMode; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateRespVO.java new file mode 100644 index 0000000..2908291 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateRespVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate; + +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 DeliveryExpressTemplateRespVO extends DeliveryExpressTemplateBaseVO { + + @Schema(description = "编号,自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "371") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateSimpleRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateSimpleRespVO.java new file mode 100644 index 0000000..4080140 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateSimpleRespVO.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Schema(description = "管理后台 - 模版精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeliveryExpressTemplateSimpleRespVO { + + @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试模版") + private String name; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateUpdateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateUpdateReqVO.java new file mode 100644 index 0000000..66d0686 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateUpdateReqVO.java @@ -0,0 +1,30 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 快递运费模板更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressTemplateUpdateReqVO extends DeliveryExpressTemplateBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "371") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "区域运费列表") + @Valid + private List charges; + + @Schema(description = "包邮区域列表") + @Valid + private List frees; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpBindReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpBindReqVO.java new file mode 100644 index 0000000..ed62424 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpBindReqVO.java @@ -0,0 +1,24 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.pickup; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - 自提门店绑定核销人 Request VO") +@Data +@ToString(callSuper = true) +public class DeliveryPickUpBindReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "绑定用户编号组数", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128") + @NotEmpty(message = "绑定用户编号组数不能未空") + private List verifyUserIds; + +} \ No newline at end of file diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreBaseVO.java new file mode 100644 index 0000000..e268757 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreBaseVO.java @@ -0,0 +1,68 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.pickup; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalTime; + +/** +* 自提门店 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class DeliveryPickUpStoreBaseVO { + + @Schema(description = "门店名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotBlank(message = "门店名称不能为空") + private String name; + + @Schema(description = "门店简介", example = "我是门店简介") + private String introduction; + + @Schema(description = "门店手机", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601892312") + @NotBlank(message = "门店手机不能为空") + @Mobile + private String phone; + + @Schema(description = "区域编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18733") + @NotNull(message = "区域编号不能为空") + private Integer areaId; + + @Schema(description = "门店详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "复旦大学路 188 号") + @NotBlank(message = "门店详细地址不能为空") + private String detailAddress; + + @Schema(description = "门店 logo", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @NotBlank(message = "门店 logo 不能为空") + private String logo; + + @Schema(description = "营业开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "营业开始时间不能为空") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") + private LocalTime openingTime; + + @Schema(description = "营业结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "营业结束时间不能为空") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") + private LocalTime closingTime; + + @Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED, example = "5.88") + @NotNull(message = "纬度不能为空") + private Double latitude; + + @Schema(description = "经度", requiredMode = Schema.RequiredMode.REQUIRED, example = "6.99") + @NotNull(message = "经度不能为空") + private Double longitude; + + @Schema(description = "门店状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "门店状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreCreateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreCreateReqVO.java new file mode 100644 index 0000000..86a47f9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreCreateReqVO.java @@ -0,0 +1,14 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.pickup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 自提门店创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryPickUpStoreCreateReqVO extends DeliveryPickUpStoreBaseVO { + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStorePageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStorePageReqVO.java new file mode 100644 index 0000000..03050bf --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStorePageReqVO.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.pickup; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +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 DeliveryPickUpStorePageReqVO extends PageParam { + + @Schema(description = "门店名称", example = "李四") + private String name; + + @Schema(description = "门店手机") + private String phone; + + @Schema(description = "区域编号", example = "18733") + private Integer areaId; + + @Schema(description = "门店状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreRespVO.java new file mode 100644 index 0000000..d02f7ff --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreRespVO.java @@ -0,0 +1,27 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.pickup; + +import com.tashow.cloud.trade.controller.admin.base.system.user.UserSimpleBaseVO; +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 DeliveryPickUpStoreRespVO extends DeliveryPickUpStoreBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "核销用户数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List verifyUsers; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreSimpleRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreSimpleRespVO.java new file mode 100644 index 0000000..d1630d6 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreSimpleRespVO.java @@ -0,0 +1,37 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.pickup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - 自提门店精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeliveryPickUpStoreSimpleRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128") + private Long id; + + @Schema(description = "门店名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + private String name; + + @Schema(description = "门店手机", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601892312") + private String phone; + + @Schema(description = "区域编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18733") + private Integer areaId; + + @Schema(description = "区域名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "xx市") + private String areaName; + + @Schema(description = "门店详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "复旦大学路 188 号") + private String detailAddress; + + @Schema(description = "绑定用户编号组数", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128") + private List verifyUserIds; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreUpdateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreUpdateReqVO.java new file mode 100644 index 0000000..b0acbe3 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreUpdateReqVO.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.trade.controller.admin.delivery.vo.pickup; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 自提门店更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryPickUpStoreUpdateReqVO extends DeliveryPickUpStoreBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/TradeOrderController.http b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/TradeOrderController.http new file mode 100644 index 0000000..5255d1b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/TradeOrderController.http @@ -0,0 +1,14 @@ +### 获得交易订单分页 => 成功 +GET {{baseUrl}}/trade/order/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +### 获得交易订单分页 => 成功 +GET {{baseUrl}}/trade/order/get-detail?id=21 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +### 获得交易订单的物流轨迹 => 成功 +GET {{baseUrl}}/trade/order/get-express-track-list?id=21 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} \ No newline at end of file diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/TradeOrderController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/TradeOrderController.java new file mode 100644 index 0000000..8f3d04f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/TradeOrderController.java @@ -0,0 +1,170 @@ +package com.tashow.cloud.trade.controller.admin.order; + +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.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.trade.controller.admin.order.vo.*; +import com.tashow.cloud.trade.controller.admin.order.vo.*; +import com.tashow.cloud.trade.convert.order.TradeOrderConvert; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderLogDO; +import com.tashow.cloud.trade.service.order.TradeOrderLogService; +import com.tashow.cloud.trade.service.order.TradeOrderQueryService; +import com.tashow.cloud.trade.service.order.TradeOrderUpdateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +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 java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +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.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 交易订单") +@RestController +@RequestMapping("/trade/order") +@Validated +@Slf4j +public class TradeOrderController { + + @Resource + private TradeOrderUpdateService tradeOrderUpdateService; + @Resource + private TradeOrderQueryService tradeOrderQueryService; + @Resource + private TradeOrderLogService tradeOrderLogService; + + @Resource + private MemberUserApi memberUserApi; + + @GetMapping("/page") + @Operation(summary = "获得交易订单分页") + @PreAuthorize("@ss.hasPermission('trade:order:query')") + public CommonResult> getOrderPage(TradeOrderPageReqVO reqVO) { + // 查询订单 + PageResult pageResult = tradeOrderQueryService.getOrderPage(reqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 查询用户信息 + Set userIds = CollUtil.unionDistinct(convertList(pageResult.getList(), TradeOrderDO::getUserId), + convertList(pageResult.getList(), TradeOrderDO::getBrokerageUserId, Objects::nonNull)); + Map userMap = memberUserApi.getUserMap(userIds); + // 查询订单项 + List orderItems = tradeOrderQueryService.getOrderItemListByOrderId( + convertSet(pageResult.getList(), TradeOrderDO::getId)); + // 最终组合 + return success(TradeOrderConvert.INSTANCE.convertPage(pageResult, orderItems, userMap)); + } + + @GetMapping("/summary") + @Operation(summary = "获得交易订单统计") + @PreAuthorize("@ss.hasPermission('trade:order:query')") + public CommonResult getOrderSummary(TradeOrderPageReqVO reqVO) { + return success(tradeOrderQueryService.getOrderSummary(reqVO)); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得交易订单详情") + @Parameter(name = "id", description = "订单编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:order:query')") + public CommonResult getOrderDetail(@RequestParam("id") Long id) { + // 查询订单 + TradeOrderDO order = tradeOrderQueryService.getOrder(id); + if (order == null) { + return success(null); + } + // 查询订单项 + List orderItems = tradeOrderQueryService.getOrderItemListByOrderId(id); + + // 拼接数据 + MemberUserRespDTO user = memberUserApi.getUser(order.getUserId()).getCheckedData(); + MemberUserRespDTO brokerageUser = order.getBrokerageUserId() != null ? + memberUserApi.getUser(order.getBrokerageUserId()).getCheckedData() : null; + List orderLogs = tradeOrderLogService.getOrderLogListByOrderId(id); + return success(TradeOrderConvert.INSTANCE.convert(order, orderItems, orderLogs, user, brokerageUser)); + } + + @GetMapping("/get-express-track-list") + @Operation(summary = "获得交易订单的物流轨迹") + @Parameter(name = "id", description = "交易订单编号") + @PreAuthorize("@ss.hasPermission('trade:order:query')") + public CommonResult> getOrderExpressTrackList(@RequestParam("id") Long id) { + return success(TradeOrderConvert.INSTANCE.convertList02( + tradeOrderQueryService.getExpressTrackList(id))); + } + + @PutMapping("/delivery") + @Operation(summary = "订单发货") + @PreAuthorize("@ss.hasPermission('trade:order:update')") + public CommonResult deliveryOrder(@RequestBody TradeOrderDeliveryReqVO deliveryReqVO) { + tradeOrderUpdateService.deliveryOrder(deliveryReqVO); + return success(true); + } + + @PutMapping("/update-remark") + @Operation(summary = "订单备注") + @PreAuthorize("@ss.hasPermission('trade:order:update')") + public CommonResult updateOrderRemark(@RequestBody TradeOrderRemarkReqVO reqVO) { + tradeOrderUpdateService.updateOrderRemark(reqVO); + return success(true); + } + + @PutMapping("/update-price") + @Operation(summary = "订单调价") + @PreAuthorize("@ss.hasPermission('trade:order:update')") + public CommonResult updateOrderPrice(@RequestBody TradeOrderUpdatePriceReqVO reqVO) { + tradeOrderUpdateService.updateOrderPrice(reqVO); + return success(true); + } + + @PutMapping("/update-address") + @Operation(summary = "修改订单收货地址") + @PreAuthorize("@ss.hasPermission('trade:order:update')") + public CommonResult updateOrderAddress(@RequestBody TradeOrderUpdateAddressReqVO reqVO) { + tradeOrderUpdateService.updateOrderAddress(reqVO); + return success(true); + } + + @PutMapping("/pick-up-by-id") + @Operation(summary = "订单核销") + @Parameter(name = "id", description = "交易订单编号") + @PreAuthorize("@ss.hasPermission('trade:order:pick-up')") + public CommonResult pickUpOrderById(@RequestParam("id") Long id) { + tradeOrderUpdateService.pickUpOrderByAdmin(getLoginUserId(), id); + return success(true); + } + + @PutMapping("/pick-up-by-verify-code") + @Operation(summary = "订单核销") + @Parameter(name = "pickUpVerifyCode", description = "自提核销码") + @PreAuthorize("@ss.hasPermission('trade:order:pick-up')") + public CommonResult pickUpOrderByVerifyCode(@RequestParam("pickUpVerifyCode") String pickUpVerifyCode) { + tradeOrderUpdateService.pickUpOrderByAdmin(getLoginUserId(), pickUpVerifyCode); + return success(true); + } + + @GetMapping("/get-by-pick-up-verify-code") + @Operation(summary = "查询核销码对应的订单") + @Parameter(name = "pickUpVerifyCode", description = "自提核销码") + @PreAuthorize("@ss.hasPermission('trade:order:query')") + public CommonResult getByPickUpVerifyCode(@RequestParam("pickUpVerifyCode") String pickUpVerifyCode) { + TradeOrderDO tradeOrder = tradeOrderUpdateService.getByPickUpVerifyCode(pickUpVerifyCode); + return success(TradeOrderConvert.INSTANCE.convert2(tradeOrder, null)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderBaseVO.java new file mode 100644 index 0000000..258aba9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderBaseVO.java @@ -0,0 +1,151 @@ +package com.tashow.cloud.trade.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 交易订单 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class TradeOrderBaseVO { + + // ========== 订单基本信息 ========== + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "订单流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1146347329394184195") + private String no; + + @Schema(description = "下单时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "订单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "订单来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer terminal; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long userId; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + private String userIp; + + @Schema(description = "用户备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + private String userRemark; + + @Schema(description = "订单状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "购买的商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer productCount; + + @Schema(description = "订单完成时间") + private LocalDateTime finishTime; + + @Schema(description = "订单取消时间") + private LocalDateTime cancelTime; + + @Schema(description = "取消类型", example = "10") + private Integer cancelType; + + @Schema(description = "商家备注", example = "你猜一下") + private String remark; + + // ========== 价格 + 支付基本信息 ========== + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long payOrderId; + + @Schema(description = "是否已支付", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean payStatus; + + @Schema(description = "付款时间") + private LocalDateTime payTime; + + @Schema(description = "支付渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_lite") + private String payChannelCode; + + @Schema(description = "商品原价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer totalPrice; + + @Schema(description = "订单优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer discountPrice; + + @Schema(description = "运费金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer deliveryPrice; + + @Schema(description = "订单调价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer adjustPrice; + + @Schema(description = "应付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer payPrice; + + // ========== 收件 + 物流基本信息 ========== + + @Schema(description = "配送方式", example = "10") + private Integer deliveryType; + + @Schema(description = "自提门店", example = "10") + private Long pickUpStoreId; + + @Schema(description = "自提核销码", example = "10") + private Long pickUpVerifyCode; + + @Schema(description = "配送模板编号", example = "1024") + private Long deliveryTemplateId; + + @Schema(description = "发货物流公司编号", example = "1024") + private Long logisticsId; + + @Schema(description = "发货物流单号", example = "1024") + private String logisticsNo; + + @Schema(description = "发货时间") + private LocalDateTime deliveryTime; + + @Schema(description = "收货时间") + private LocalDateTime receiveTime; + + @Schema(description = "收件人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + private String receiverName; + + @Schema(description = "收件人手机", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000") + private String receiverMobile; + + @Schema(description = "收件人地区编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110000") + private Integer receiverAreaId; + + @Schema(description = "收件人详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "中关村大街 1 号") + private String receiverDetailAddress; + + // ========== 售后基本信息 ========== + + @Schema(description = "售后状态", example = "1") + private Integer afterSaleStatus; + + @Schema(description = "退款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer refundPrice; + + // ========== 营销基本信息 ========== + + @Schema(description = "优惠劵编号", example = "1024") + private Long couponId; + + @Schema(description = "优惠劵减免金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer couponPrice; + + @Schema(description = "积分抵扣的金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer pointPrice; + + @Schema(description = "VIP 减免金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "888") + private Integer vipPrice; + + @Schema(description = "推广人编号", example = "1") + private Long brokerageUserId; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderDeliveryReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderDeliveryReqVO.java new file mode 100644 index 0000000..e43b192 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderDeliveryReqVO.java @@ -0,0 +1,23 @@ +package com.tashow.cloud.trade.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 订单发货 Request VO") +@Data +public class TradeOrderDeliveryReqVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "订单编号不能为空") + private Long id; + + @Schema(description = "发货物流公司编号", example = "1") + @NotNull(message = "发货物流公司不能为空") + private Long logisticsId; + + @Schema(description = "发货物流单号", example = "SF123456789") + private String logisticsNo; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderDetailRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderDetailRespVO.java new file mode 100644 index 0000000..b913a73 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderDetailRespVO.java @@ -0,0 +1,63 @@ +package com.tashow.cloud.trade.controller.admin.order.vo; + +import com.tashow.cloud.trade.controller.admin.base.member.user.MemberUserRespVO; +import com.tashow.cloud.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 交易订单的详情 Response VO") +@Data +public class TradeOrderDetailRespVO extends TradeOrderBaseVO { + + /** + * 订单项列表 + */ + private List items; + + /** + * 下单用户信息 + */ + private MemberUserRespVO user; + /** + * 推广用户信息 + */ + private MemberUserRespVO brokerageUser; + + /** + * 操作日志列表 + */ + private List logs; + + @Schema(description = "收件人地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海 上海市 普陀区") + private String receiverAreaName; + + @Schema(description = "管理后台 - 交易订单的操作日志") + @Data + public static class OrderLog { + + @Schema(description = "操作详情", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单发货") + private String content; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-06-01 10:50:20") + private LocalDateTime createTime; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer userType; + + } + + @Schema(description = "管理后台 - 交易订单的详情的订单项目") + @Data + public static class Item extends TradeOrderItemBaseVO { + + /** + * 属性数组 + */ + private List properties; + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderItemBaseVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderItemBaseVO.java new file mode 100644 index 0000000..6742d75 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderItemBaseVO.java @@ -0,0 +1,67 @@ +package com.tashow.cloud.trade.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 交易订单项 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class TradeOrderItemBaseVO { + + // ========== 订单项基本信息 ========== + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long userId; + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long orderId; + + // ========== 商品基本信息 ========== + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long spuId; + + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String spuName; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long skuId; + + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "购买数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + + // ========== 价格 + 支付基本信息 ========== + + @Schema(description = "商品原价(单)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer price; + + @Schema(description = "商品优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer discountPrice; + + @Schema(description = "商品实付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer payPrice; + + @Schema(description = "子订单分摊金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer orderPartPrice; + + @Schema(description = "分摊后子订单实付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer orderDividePrice; + + // ========== 营销基本信息 ========== + + // TODO 芋艿:在捉摸一下 + + // ========== 售后基本信息 ========== + + @Schema(description = "售后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer afterSaleStatus; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderPageItemRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderPageItemRespVO.java new file mode 100644 index 0000000..7036a8e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderPageItemRespVO.java @@ -0,0 +1,35 @@ +package com.tashow.cloud.trade.controller.admin.order.vo; + +import com.tashow.cloud.trade.controller.admin.base.member.user.MemberUserRespVO; +import com.tashow.cloud.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 交易订单的分页项 Response VO") +@Data +public class TradeOrderPageItemRespVO extends TradeOrderBaseVO { + + @Schema(description = "收件人地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海 上海市 普陀区") + private String receiverAreaName; + + @Schema(description = "订单项列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List items; + + @Schema(description = "用户信息", requiredMode = Schema.RequiredMode.REQUIRED) + private MemberUserRespVO user; + + @Schema(description = "推广人信息") + private MemberUserRespVO brokerageUser; + + @Schema(description = "管理后台 - 交易订单的分页项的订单项目") + @Data + public static class Item extends TradeOrderItemBaseVO { + + @Schema(description = "属性列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List properties; + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderPageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderPageReqVO.java new file mode 100644 index 0000000..006b756 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderPageReqVO.java @@ -0,0 +1,64 @@ +package com.tashow.cloud.trade.controller.admin.order.vo; + +import cn.iocoder.yudao.framework.common.enums.TerminalEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 交易订单的分页 Request VO") +@Data +public class TradeOrderPageReqVO extends PageParam { + + @Schema(description = "订单号", example = "88888888") + private String no; + + @Schema(description = "用户编号", example = "1024") + private Long userId; + + @Schema(description = "用户昵称", example = "小王") + private String userNickname; + + @Schema(description = "用户手机号", example = "小王") + @Mobile + private String userMobile; + + @Schema(description = "配送方式", example = "1") + private Integer deliveryType; + + @Schema(description = "发货物流公司编号", example = "1") + private Long logisticsId; + + @Schema(description = "自提门店编号", example = "[1,2]") + private List pickUpStoreIds; + + @Schema(description = "自提核销码", example = "12345678") + private String pickUpVerifyCode; + + @Schema(description = "订单类型", example = "1") + private Integer type; + + @Schema(description = "订单状态", example = "1") + @InEnum(value = TradeOrderStatusEnum.class, message = "订单状态必须是 {value}") + private Integer status; + + @Schema(description = "支付渠道", example = "wx_lite") + private String payChannelCode; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "订单来源", example = "10") + @InEnum(value = TerminalEnum.class, message = "订单来源 {value}") + private Integer terminal; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderRemarkReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderRemarkReqVO.java new file mode 100644 index 0000000..afdbe74 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderRemarkReqVO.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.trade.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 订单备注 Request VO") +@Data +public class TradeOrderRemarkReqVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "订单编号不能为空") + private Long id; + + @Schema(description = "商家备注", example = "你猜一下") + @NotEmpty(message = "订单备注不能为空") + private String remark; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderSummaryRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderSummaryRespVO.java new file mode 100644 index 0000000..c1b197e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderSummaryRespVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 交易订单统计 Response VO") +@Data +public class TradeOrderSummaryRespVO { + + @Schema(description = "订单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long orderCount; + + @Schema(description = "订单金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long orderPayPrice; + + @Schema(description = "退款单数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long afterSaleCount; + + @Schema(description = "退款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long afterSalePrice; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderUpdateAddressReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderUpdateAddressReqVO.java new file mode 100644 index 0000000..529f3a6 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderUpdateAddressReqVO.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.trade.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 订单修改地址 Request VO") +@Data +public class TradeOrderUpdateAddressReqVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "订单编号不能为空") + private Long id; + + @Schema(description = "收件人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "z张三") + @NotEmpty(message = "收件人名称不能为空") + private String receiverName; + + @Schema(description = "收件人手机", requiredMode = Schema.RequiredMode.REQUIRED, example = "19988188888") + @NotEmpty(message = "收件人手机不能为空") + private String receiverMobile; + + @Schema(description = "收件人地区编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7310") + @NotNull(message = "收件人地区编号不能为空") + private Integer receiverAreaId; + + @Schema(description = "收件人详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "昆明市五华区xxx小区xxx") + @NotEmpty(message = "收件人详细地址不能为空") + private String receiverDetailAddress; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderUpdatePriceReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderUpdatePriceReqVO.java new file mode 100644 index 0000000..810ec78 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/admin/order/vo/TradeOrderUpdatePriceReqVO.java @@ -0,0 +1,20 @@ +package com.tashow.cloud.trade.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 订单改价 Request VO") +@Data +public class TradeOrderUpdatePriceReqVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "订单编号不能为空") + private Long id; + + @Schema(description = "订单调价,单位:分。正数,加价;负数,减价", requiredMode = Schema.RequiredMode.REQUIRED, example = "-100") + @NotNull(message = "订单调价价格不能为空") + private Integer adjustPrice; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/AppAfterSaleController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/AppAfterSaleController.java new file mode 100644 index 0000000..7247828 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/AppAfterSaleController.java @@ -0,0 +1,67 @@ +package com.tashow.cloud.trade.controller.app.aftersale; + +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 com.tashow.cloud.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO; +import com.tashow.cloud.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO; +import com.tashow.cloud.trade.controller.app.aftersale.vo.AppAfterSaleRespVO; +import com.tashow.cloud.trade.convert.aftersale.AfterSaleConvert; +import com.tashow.cloud.trade.service.aftersale.AfterSaleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 交易售后") +@RestController +@RequestMapping("/trade/after-sale") +@Validated +@Slf4j +public class AppAfterSaleController { + + @Resource + private AfterSaleService afterSaleService; + + @GetMapping(value = "/page") + @Operation(summary = "获得售后分页") + public CommonResult> getAfterSalePage(PageParam pageParam) { + return success(AfterSaleConvert.INSTANCE.convertPage02( + afterSaleService.getAfterSalePage(getLoginUserId(), pageParam))); + } + + @GetMapping(value = "/get") + @Operation(summary = "获得售后订单") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + public CommonResult getAfterSale(@RequestParam("id") Long id) { + return success(AfterSaleConvert.INSTANCE.convert(afterSaleService.getAfterSale(getLoginUserId(), id))); + } + + @PostMapping(value = "/create") + @Operation(summary = "申请售后") + public CommonResult createAfterSale(@RequestBody AppAfterSaleCreateReqVO createReqVO) { + return success(afterSaleService.createAfterSale(getLoginUserId(), createReqVO)); + } + + @PutMapping(value = "/delivery") + @Operation(summary = "退回货物") + public CommonResult deliveryAfterSale(@RequestBody AppAfterSaleDeliveryReqVO deliveryReqVO) { + afterSaleService.deliveryAfterSale(getLoginUserId(), deliveryReqVO); + return success(true); + } + + @DeleteMapping(value = "/cancel") + @Operation(summary = "取消售后") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + public CommonResult cancelAfterSale(@RequestParam("id") Long id) { + afterSaleService.cancelAfterSale(getLoginUserId(), id); + return success(true); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/AppAfterSaleLogController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/AppAfterSaleLogController.java new file mode 100644 index 0000000..0bdb933 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/AppAfterSaleLogController.java @@ -0,0 +1,42 @@ +package com.tashow.cloud.trade.controller.app.aftersale; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import com.tashow.cloud.trade.controller.app.aftersale.vo.log.AppAfterSaleLogRespVO; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleLogDO; +import com.tashow.cloud.trade.service.aftersale.AfterSaleLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +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 java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 售后日志") +@RestController +@RequestMapping("/trade/after-sale-log") +@Validated +@Slf4j +public class AppAfterSaleLogController { + + @Resource + private AfterSaleLogService afterSaleLogService; + + @GetMapping("/list") + @Operation(summary = "获得售后日志列表") + @Parameter(name = "afterSaleId", description = "售后编号", required = true, example = "1") + public CommonResult> getAfterSaleLogList( + @RequestParam("afterSaleId") Long afterSaleId) { + List logs = afterSaleLogService.getAfterSaleLogList(afterSaleId); + return success(BeanUtils.toBean(logs, AppAfterSaleLogRespVO.class)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/AppAfterSaleCreateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/AppAfterSaleCreateReqVO.java new file mode 100644 index 0000000..b96b24f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/AppAfterSaleCreateReqVO.java @@ -0,0 +1,40 @@ +package com.tashow.cloud.trade.controller.app.aftersale.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleWayEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "用户 App - 交易售后创建 Request VO") +@Data +public class AppAfterSaleCreateReqVO { + + @Schema(description = "订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "订单项编号不能为空") + private Long orderItemId; + + @Schema(description = "售后方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "售后方式不能为空") + @InEnum(value = AfterSaleWayEnum.class, message = "售后方式必须是 {value}") + private Integer way; + + @Schema(description = "退款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "退款金额不能为空") + @Min(value = 1, message = "退款金额必须大于 0") + private Integer refundPrice; + + @Schema(description = "申请原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "申请原因不能为空") + private String applyReason; + + @Schema(description = "补充描述", example = "商品质量不好") + private String applyDescription; + + @Schema(description = "补充凭证图片", example = "https://www.iocoder.cn/1.png, https://www.iocoder.cn/2.png") + private List applyPicUrls; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/AppAfterSaleDeliveryReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/AppAfterSaleDeliveryReqVO.java new file mode 100644 index 0000000..9c4bef8 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/AppAfterSaleDeliveryReqVO.java @@ -0,0 +1,24 @@ +package com.tashow.cloud.trade.controller.app.aftersale.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Schema(description = "用户 App - 交易售后退回货物 Request VO") +@Data +public class AppAfterSaleDeliveryReqVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "售后编号不能为空") + private Long id; + + @Schema(description = "退货物流公司编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "退货物流公司编号不能为空") + private Long logisticsId; + + @Schema(description = "退货物流单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "SF123456789") + @NotNull(message = "退货物流单号不能为空") + private String logisticsNo; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/AppAfterSaleRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/AppAfterSaleRespVO.java new file mode 100644 index 0000000..e67d033 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/AppAfterSaleRespVO.java @@ -0,0 +1,109 @@ +package com.tashow.cloud.trade.controller.app.aftersale.vo; + +import com.tashow.cloud.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "用户 App - 交易售后 Response VO") +@Data +public class AppAfterSaleRespVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "售后流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1146347329394184195") + private String no; + + @Schema(description = "售后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "售后方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer way; + + @Schema(description = "售后类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "申请原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String applyReason; + + @Schema(description = "补充描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String applyDescription; + + @Schema(description = "补充凭证图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private List applyPicUrls; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + // ========== 交易订单相关 ========== + + @Schema(description = "交易订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long orderId; + + @Schema(description = "交易订单流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String orderNo; + + @Schema(description = "交易订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long orderItemId; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long spuId; + + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String spuName; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long skuId; + + /** + * 属性数组 + */ + private List properties; + + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/01.jpg") + private String picUrl; + + @Schema(description = "退货商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + + // ========== 审批相关 ========== + + /** + * 审批备注 + * + * 注意,只有审批不通过才会填写 + */ + private String auditReason; + + // ========== 退款相关 ========== + + @Schema(description = "退款金额,单位:分", example = "100") + private Integer refundPrice; + + @Schema(description = "退款时间") + private LocalDateTime refundTime; + + // ========== 退货相关 ========== + + @Schema(description = "退货物流公司编号", example = "1") + private Long logisticsId; + + @Schema(description = "退货物流单号", example = "SF123456789") + private String logisticsNo; + + @Schema(description = "退货时间") + private LocalDateTime deliveryTime; + + @Schema(description = "收货时间") + private LocalDateTime receiveTime; + + @Schema(description = "收货备注") + private String receiveReason; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/log/AppAfterSaleLogRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/log/AppAfterSaleLogRespVO.java new file mode 100644 index 0000000..349bd23 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/aftersale/vo/log/AppAfterSaleLogRespVO.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.trade.controller.app.aftersale.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - App 交易售后日志 Response VO") +@Data +public class AppAfterSaleLogRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20669") + private Long id; + + @Schema(description = "操作明细", requiredMode = Schema.RequiredMode.REQUIRED, example = "维权完成,退款金额:¥37776.00") + private String content; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/package-info.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/package-info.java new file mode 100644 index 0000000..efe3170 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/package-info.java @@ -0,0 +1,4 @@ +/** + * 基础包,放一些通用的 VO 类 + */ +package com.tashow.cloud.trade.controller.app.base; diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/property/AppProductPropertyValueDetailRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/property/AppProductPropertyValueDetailRespVO.java new file mode 100644 index 0000000..65e5ab8 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/property/AppProductPropertyValueDetailRespVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.app.base.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 商品属性值的明细 Response VO") +@Data +public class AppProductPropertyValueDetailRespVO { + + @Schema(description = "属性的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long propertyId; + + @Schema(description = "属性的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "颜色") + private String propertyName; + + @Schema(description = "属性值的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long valueId; + + @Schema(description = "属性值的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "红色") + private String valueName; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/sku/AppProductSkuBaseRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/sku/AppProductSkuBaseRespVO.java new file mode 100644 index 0000000..2085a00 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/sku/AppProductSkuBaseRespVO.java @@ -0,0 +1,34 @@ +package com.tashow.cloud.trade.controller.app.base.sku; + +import com.tashow.cloud.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 商品 SKU 基础 Response VO + * + * @author 芋道源码 + */ +@Data +public class AppProductSkuBaseRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "图片地址", example = "https://www.iocoder.cn/xx.png") + private String picUrl; + + @Schema(description = "销售价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer price; + + @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer stock; + + /** + * 属性数组 + */ + private List properties; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java new file mode 100644 index 0000000..b9c98c5 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.trade.controller.app.base.spu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 商品 SPU 基础 Response VO + * + * @author 芋道源码 + */ +@Data +public class AppProductSpuBaseRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "商品主图地址", example = "https://www.iocoder.cn/xx.png") + private String picUrl; + + @Schema(description = "商品分类编号", example = "1") + private Long categoryId; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/AppBrokerageRecordController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/AppBrokerageRecordController.java new file mode 100644 index 0000000..2ca90c7 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/AppBrokerageRecordController.java @@ -0,0 +1,51 @@ +package com.tashow.cloud.trade.controller.app.brokerage; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import com.tashow.cloud.trade.controller.app.brokerage.vo.record.AppBrokerageProductPriceRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.record.AppBrokerageRecordPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.record.AppBrokerageRecordRespVO; +import com.tashow.cloud.trade.convert.brokerage.BrokerageRecordConvert; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageRecordDO; +import com.tashow.cloud.trade.service.brokerage.BrokerageRecordService; +import io.swagger.v3.oas.annotations.Operation; +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.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.annotation.Resource; +import jakarta.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 APP - 分销用户") +@RestController +@RequestMapping("/trade/brokerage-record") +@Validated +@Slf4j +public class AppBrokerageRecordController { + + @Resource + private BrokerageRecordService brokerageRecordService; + + @GetMapping("/page") + @Operation(summary = "获得分销记录分页") + public CommonResult> getBrokerageRecordPage(@Valid AppBrokerageRecordPageReqVO pageReqVO) { + PageResult pageResult = brokerageRecordService.getBrokerageRecordPage( + BrokerageRecordConvert.INSTANCE.convert(pageReqVO, getLoginUserId())); + return success(BeanUtils.toBean(pageResult, AppBrokerageRecordRespVO.class)); + } + + @GetMapping("/get-product-brokerage-price") + @Operation(summary = "获得商品的分销金额") + public CommonResult getProductBrokeragePrice(@RequestParam("spuId") Long spuId) { + return success(brokerageRecordService.calculateProductBrokeragePrice(getLoginUserId(), spuId)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/AppBrokerageUserController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/AppBrokerageUserController.java new file mode 100644 index 0000000..d4de603 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/AppBrokerageUserController.java @@ -0,0 +1,136 @@ +package com.tashow.cloud.trade.controller.app.brokerage; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.*; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.*; +import com.tashow.cloud.trade.convert.brokerage.BrokerageRecordConvert; +import com.tashow.cloud.trade.convert.brokerage.BrokerageUserConvert; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageUserDO; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; +import com.tashow.cloud.trade.service.brokerage.BrokerageRecordService; +import com.tashow.cloud.trade.service.brokerage.BrokerageUserService; +import com.tashow.cloud.trade.service.brokerage.BrokerageWithdrawService; +import com.tashow.cloud.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static java.util.Arrays.asList; + +@Tag(name = "用户 APP - 分销用户") +@RestController +@RequestMapping("/trade/brokerage-user") +@Validated +@Slf4j +public class AppBrokerageUserController { + + @Resource + private BrokerageUserService brokerageUserService; + @Resource + private BrokerageRecordService brokerageRecordService; + @Resource + private BrokerageWithdrawService brokerageWithdrawService; + @Resource + private MemberUserApi memberUserApi; + + @GetMapping("/get") + @Operation(summary = "获得个人分销信息") + public CommonResult getBrokerageUser() { + Optional user = Optional.ofNullable(brokerageUserService.getOrCreateBrokerageUser(getLoginUserId())); + // 返回数据 + AppBrokerageUserRespVO respVO = new AppBrokerageUserRespVO() + .setBrokerageEnabled(user.map(BrokerageUserDO::getBrokerageEnabled).orElse(false)) + .setBrokeragePrice(user.map(BrokerageUserDO::getBrokeragePrice).orElse(0)) + .setFrozenPrice(user.map(BrokerageUserDO::getFrozenPrice).orElse(0)); + return success(respVO); + } + + @PutMapping("/bind") + @Operation(summary = "绑定推广员") + public CommonResult bindBrokerageUser(@Valid @RequestBody AppBrokerageUserBindReqVO reqVO) { + return success(brokerageUserService.bindBrokerageUser(getLoginUserId(), reqVO.getBindUserId())); + } + + @GetMapping("/get-summary") + @Operation(summary = "获得个人分销统计") + public CommonResult getBrokerageUserSummary() { + // 查询当前登录用户信息 + Long userId = getLoginUserId(); + BrokerageUserDO brokerageUser = brokerageUserService.getBrokerageUser(userId); + // 统计用户昨日的佣金 + LocalDateTime yesterday = LocalDateTime.now().minusDays(1); + LocalDateTime beginTime = LocalDateTimeUtil.beginOfDay(yesterday); + LocalDateTime endTime = LocalDateTimeUtil.endOfDay(yesterday); + Integer yesterdayPrice = brokerageRecordService.getSummaryPriceByUserId(userId, + BrokerageRecordBizTypeEnum.ORDER, BrokerageRecordStatusEnum.SETTLEMENT, beginTime, endTime); + // 统计用户提现的佣金 + Integer withdrawPrice = brokerageWithdrawService.getWithdrawSummaryListByUserId(Collections.singleton(userId), + asList(BrokerageWithdrawStatusEnum.AUDIT_SUCCESS, BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS)).stream() + .findFirst().map(BrokerageWithdrawSummaryRespBO::getPrice).orElse(0); + // 统计分销用户数量(一级) + Long firstBrokerageUserCount = brokerageUserService.getBrokerageUserCountByBindUserId(userId, 1); + // 统计分销用户数量(二级) + Long secondBrokerageUserCount = brokerageUserService.getBrokerageUserCountByBindUserId(userId, 2); + + // 拼接返回 + return success(BrokerageUserConvert.INSTANCE.convert(yesterdayPrice, withdrawPrice, firstBrokerageUserCount, secondBrokerageUserCount, brokerageUser)); + } + + @GetMapping("/rank-page-by-user-count") + @Operation(summary = "获得分销用户排行分页(基于用户量)") + public CommonResult> getBrokerageUserRankPageByUserCount(AppBrokerageUserRankPageReqVO pageReqVO) { + // 分页查询 + PageResult pageResult = brokerageUserService.getBrokerageUserRankPageByUserCount(pageReqVO); + // 拼接数据 + Map userMap = memberUserApi.getUserMap(convertSet(pageResult.getList(), AppBrokerageUserRankByUserCountRespVO::getId)); + return success(BrokerageUserConvert.INSTANCE.convertPage03(pageResult, userMap)); + } + + @GetMapping("/rank-page-by-price") + @Operation(summary = "获得分销用户排行分页(基于佣金)") + public CommonResult> getBrokerageUserChildSummaryPageByPrice(AppBrokerageUserRankPageReqVO pageReqVO) { + // 分页查询 + PageResult pageResult = brokerageRecordService.getBrokerageUserChildSummaryPageByPrice(pageReqVO); + // 拼接数据 + Map userMap = memberUserApi.getUserMap(convertSet(pageResult.getList(), AppBrokerageUserRankByPriceRespVO::getId)); + return success(BrokerageRecordConvert.INSTANCE.convertPage03(pageResult, userMap)); + } + + @GetMapping("/child-summary-page") + @Operation(summary = "获得下级分销统计分页") + public CommonResult> getBrokerageUserChildSummaryPage( + AppBrokerageUserChildSummaryPageReqVO pageReqVO) { + PageResult pageResult = brokerageUserService.getBrokerageUserChildSummaryPage(pageReqVO, getLoginUserId()); + return success(pageResult); + } + + @GetMapping("/get-rank-by-price") + @Operation(summary = "获得分销用户排行(基于佣金)") + @Parameter(name = "times", description = "时间段", required = true) + public CommonResult getRankByPrice( + @RequestParam("times") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) LocalDateTime[] times) { + return success(brokerageRecordService.getUserRankByPrice(getLoginUserId(), times)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/AppBrokerageWithdrawController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/AppBrokerageWithdrawController.java new file mode 100644 index 0000000..ef75590 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/AppBrokerageWithdrawController.java @@ -0,0 +1,47 @@ +package com.tashow.cloud.trade.controller.app.brokerage; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawRespVO; +import com.tashow.cloud.trade.convert.brokerage.BrokerageWithdrawConvert; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageWithdrawDO; +import com.tashow.cloud.trade.service.brokerage.BrokerageWithdrawService; +import io.swagger.v3.oas.annotations.Operation; +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 jakarta.annotation.Resource; +import jakarta.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 APP - 分销提现") +@RestController +@RequestMapping("/trade/brokerage-withdraw") +@Validated +@Slf4j +public class AppBrokerageWithdrawController { + + @Resource + private BrokerageWithdrawService brokerageWithdrawService; + + @GetMapping("/page") + @Operation(summary = "获得分销提现分页") + public CommonResult> getBrokerageWithdrawPage(AppBrokerageWithdrawPageReqVO pageReqVO) { + PageResult pageResult = brokerageWithdrawService.getBrokerageWithdrawPage( + BrokerageWithdrawConvert.INSTANCE.convert(pageReqVO, getLoginUserId())); + return success(BrokerageWithdrawConvert.INSTANCE.convertPage03(pageResult)); + } + + @PostMapping("/create") + @Operation(summary = "创建分销提现") + public CommonResult createBrokerageWithdraw(@RequestBody @Valid AppBrokerageWithdrawCreateReqVO createReqVO) { + return success(brokerageWithdrawService.createBrokerageWithdraw(getLoginUserId(), createReqVO)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/record/AppBrokerageProductPriceRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/record/AppBrokerageProductPriceRespVO.java new file mode 100644 index 0000000..8a85851 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/record/AppBrokerageProductPriceRespVO.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 商品的分销金额 Response VO") +@Data +public class AppBrokerageProductPriceRespVO { + + @Schema(description = "是否开启", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Boolean enabled; + + @Schema(description = "分销最小金额,单位:分", example = "100") + private Integer brokerageMinPrice; + + @Schema(description = "分销最大金额,单位:分", example = "100") + private Integer brokerageMaxPrice; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/record/AppBrokerageRecordPageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/record/AppBrokerageRecordPageReqVO.java new file mode 100644 index 0000000..9846bcb --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/record/AppBrokerageRecordPageReqVO.java @@ -0,0 +1,31 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.record; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; +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 = "应用 App - 分销记录分页 Request VO") +@Data +public class AppBrokerageRecordPageReqVO extends PageParam { + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(value = BrokerageRecordBizTypeEnum.class, message = "业务类型必须是 {value}") + private Integer bizType; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(value = BrokerageRecordStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/record/AppBrokerageRecordRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/record/AppBrokerageRecordRespVO.java new file mode 100644 index 0000000..33c71a9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/record/AppBrokerageRecordRespVO.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 分销记录 Response VO") +@Data +public class AppBrokerageRecordRespVO { + + @Schema(description = "记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long id; + + @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private String bizId; + + @Schema(description = "分销标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "用户下单") + private String title; + + @Schema(description = "分销金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer price; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "完成时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime finishTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserBindReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserBindReqVO.java new file mode 100644 index 0000000..732d15c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserBindReqVO.java @@ -0,0 +1,16 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Schema(description = "应用 App - 绑定推广员 Request VO") +@Data +public class AppBrokerageUserBindReqVO { + + @Schema(description = "推广员编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "推广员编号不能为空") + private Long bindUserId; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserChildSummaryPageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserChildSummaryPageReqVO.java new file mode 100644 index 0000000..7e24edb --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserChildSummaryPageReqVO.java @@ -0,0 +1,30 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.user; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.SortingField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.NotNull; + +@Schema(description = "用户 App - 下级分销统计分页 Request VO") +@Data +public class AppBrokerageUserChildSummaryPageReqVO extends PageParam { + + public static final String SORT_FIELD_USER_COUNT = "userCount"; + public static final String SORT_FIELD_ORDER_COUNT = "orderCount"; + public static final String SORT_FIELD_PRICE = "price"; + + @Schema(description = "用户昵称", example = "李") // 模糊匹配 + private String nickname; + + @Schema(description = "排序字段", example = "userCount") + private SortingField sortingField; + + @Schema(description = "下级的级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") // 1 - 直接下级;2 - 间接下级 + @NotNull(message = "下级的级别不能为空") + @Range(min = 1, max = 2, message = "下级的级别只能是 {min} 或者 {max}") + private Integer level; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserChildSummaryRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserChildSummaryRespVO.java new file mode 100644 index 0000000..ede169c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserChildSummaryRespVO.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 下级分销统计 Response VO") +@Data +public class AppBrokerageUserChildSummaryRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王") + private String nickname; + + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xxx.jpg") + private String avatar; + + @Schema(description = "佣金金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer brokeragePrice; + + @Schema(description = "分销订单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer brokerageOrderCount; + + @Schema(description = "分销用户数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") + private Integer brokerageUserCount; + + @Schema(description = "绑定推广员的时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime brokerageTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserMySummaryRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserMySummaryRespVO.java new file mode 100644 index 0000000..1798d4f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserMySummaryRespVO.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 个人分销统计 Response VO") +@Data +public class AppBrokerageUserMySummaryRespVO { + + @Schema(description = "昨天的佣金,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer yesterdayPrice; + + @Schema(description = "提现的佣金,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer withdrawPrice; + + @Schema(description = "可用的佣金,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "2408") + private Integer brokeragePrice; + + @Schema(description = "冻结的佣金,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "234") + private Integer frozenPrice; + + @Schema(description = "分销用户数量(一级)", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long firstBrokerageUserCount; + + @Schema(description = "分销用户数量(二级)", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long secondBrokerageUserCount; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRankByPriceRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRankByPriceRespVO.java new file mode 100644 index 0000000..67239c9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRankByPriceRespVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 分销排行用户(基于用户量) Response VO") +@Data +public class AppBrokerageUserRankByPriceRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王") + private String nickname; + + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xxx.jpg") + private String avatar; + + @Schema(description = "佣金金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer brokeragePrice; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRankByUserCountRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRankByUserCountRespVO.java new file mode 100644 index 0000000..a7a1972 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRankByUserCountRespVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 分销排行用户(基于用户量) Response VO") +@Data +public class AppBrokerageUserRankByUserCountRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王") + private String nickname; + + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xxx.jpg") + private String avatar; + + @Schema(description = "邀请用户数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer brokerageUserCount; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRankPageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRankPageReqVO.java new file mode 100644 index 0000000..ba7c384 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRankPageReqVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.user; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import jakarta.validation.constraints.NotEmpty; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "应用 App - 分销用户排行 Request VO") +@Data +public class AppBrokerageUserRankPageReqVO extends PageParam { + + @Schema(description = "开始 + 结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @NotEmpty(message = "时间不能为空") + private LocalDateTime[] times; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRespVO.java new file mode 100644 index 0000000..b8e869e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/user/AppBrokerageUserRespVO.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 分销用户信息 Response VO") +@Data +public class AppBrokerageUserRespVO { + + @Schema(description = "是否有分销资格", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean brokerageEnabled; + + @Schema(description = "可用的佣金,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "2408") + private Integer brokeragePrice; + + @Schema(description = "冻结的佣金,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "234") + private Integer frozenPrice; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java new file mode 100644 index 0000000..b471e0b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawCreateReqVO.java @@ -0,0 +1,75 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw; + +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; + +@Schema(description = "用户 App - 分销提现创建 Request VO") +@Data +public class AppBrokerageWithdrawCreateReqVO { + + @Schema(description = "提现方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(value = BrokerageWithdrawTypeEnum.class, message = "提现方式必须是 {value}") + private Integer type; + + @Schema(description = "提现金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + @PositiveOrZero(message = "提现金额不能小于 0") + @NotNull(message = "提现金额不能为空") + private Integer price; + + // ========== 银行卡、微信、支付宝 提现相关字段 ========== + + @Schema(description = "提现账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456789") + @NotBlank(message = "提现账号不能为空", groups = {Bank.class, Wechat.class, Alipay.class}) + private String accountNo; + + // ========== 微信、支付宝 提现相关字段 ========== + + @Schema(description = "收款码的图片", example = "https://www.iocoder.cn/1.png") + @URL(message = "收款码的图片,必须是一个 URL") + private String accountQrCodeUrl; + + // ========== 银行卡 提现相关字段 ========== + + @Schema(description = "持卡人姓名", example = "张三") + @NotBlank(message = "持卡人姓名不能为空", groups = {Bank.class}) + private String name; + @Schema(description = "提现银行", example = "1") + @NotNull(message = "提现银行不能为空", groups = {Bank.class}) + private String bankName; + @Schema(description = "开户地址", example = "海淀支行") + private String bankAddress; + + public interface Wallet { + } + + public interface Bank { + } + + public interface Wechat { + } + + public interface Alipay { + } + + public void validate(Validator validator) { + if (BrokerageWithdrawTypeEnum.WALLET.getType().equals(type)) { + ValidationUtils.validate(validator, this, Wallet.class); + } else if (BrokerageWithdrawTypeEnum.BANK.getType().equals(type)) { + ValidationUtils.validate(validator, this, Bank.class); + } else if (BrokerageWithdrawTypeEnum.WECHAT.getType().equals(type)) { + ValidationUtils.validate(validator, this, Wechat.class); + } else if (BrokerageWithdrawTypeEnum.ALIPAY.getType().equals(type)) { + ValidationUtils.validate(validator, this, Alipay.class); + } + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawPageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawPageReqVO.java new file mode 100644 index 0000000..117aa83 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawPageReqVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "应用 App - 分销提现分页 Request VO") +@Data +public class AppBrokerageWithdrawPageReqVO extends PageParam { + + @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(value = BrokerageWithdrawTypeEnum.class, message = "类型必须是 {value}") + private Integer type; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(value = BrokerageWithdrawStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawRespVO.java new file mode 100644 index 0000000..3705ee9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/brokerage/vo/withdraw/AppBrokerageWithdrawRespVO.java @@ -0,0 +1,27 @@ +package com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 分销提现 Response VO") +@Data +public class AppBrokerageWithdrawRespVO { + + @Schema(description = "提现编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long id; + + @Schema(description = "提现状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer status; + + @Schema(description = "提现状态名", requiredMode = Schema.RequiredMode.REQUIRED, example = "审核中") + private String statusName; + + @Schema(description = "提现金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer price; + + @Schema(description = "提现时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/AppCartController.http b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/AppCartController.http new file mode 100644 index 0000000..2eda3c2 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/AppCartController.http @@ -0,0 +1,42 @@ +### 请求 /trade/cart/add 接口 => 成功 +POST {{appApi}}/trade/cart/add +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} +Content-Type: application/json + +{ + "skuId": 1, + "count": 10, + "addStatus": true +} + +### 请求 /trade/cart/update 接口 => 成功 +PUT {{appApi}}/trade/cart/update +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} +Content-Type: application/json + +{ + "id": 35, + "count": 5 +} + +### 请求 /trade/cart/delete 接口 => 成功 +DELETE {{appApi}}/trade/cart/delete?ids=1 +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} + +### 请求 /trade/cart/get-count 接口 => 成功 +GET {{appApi}}/trade/cart/get-count +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} + +### 请求 /trade/cart/get-count-map 接口 => 成功 +GET {{appApi}}/trade/cart/get-count-map +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} + +### 请求 /trade/cart/list 接口 => 成功 +GET {{appApi}}/trade/cart/list +tenant-id: {{appTenantId}} +Authorization: Bearer {{appToken}} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/AppCartController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/AppCartController.java new file mode 100644 index 0000000..471f88b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/AppCartController.java @@ -0,0 +1,80 @@ +package com.tashow.cloud.trade.controller.app.cart; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.trade.controller.app.cart.vo.*; +import com.tashow.cloud.trade.controller.app.cart.vo.*; +import com.tashow.cloud.trade.service.cart.CartService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 购物车") +@RestController +@RequestMapping("/trade/cart") +@RequiredArgsConstructor +@Validated +@Slf4j +public class AppCartController { + + @Resource + private CartService cartService; + + @PostMapping("/add") + @Operation(summary = "添加购物车商品") + public CommonResult addCart(@Valid @RequestBody AppCartAddReqVO addCountReqVO) { + return success(cartService.addCart(getLoginUserId(), addCountReqVO)); + } + + @PutMapping("/update-count") + @Operation(summary = "更新购物车商品数量") + public CommonResult updateCartCount(@Valid @RequestBody AppCartUpdateCountReqVO updateReqVO) { + cartService.updateCartCount(getLoginUserId(), updateReqVO); + return success(true); + } + + @PutMapping("/update-selected") + @Operation(summary = "更新购物车商品选中") + public CommonResult updateCartSelected(@Valid @RequestBody AppCartUpdateSelectedReqVO updateReqVO) { + cartService.updateCartSelected(getLoginUserId(), updateReqVO); + return success(true); + } + + @PutMapping("/reset") + @Operation(summary = "重置购物车商品") + public CommonResult resetCart(@Valid @RequestBody AppCartResetReqVO updateReqVO) { + cartService.resetCart(getLoginUserId(), updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除购物车商品") + @Parameter(name = "ids", description = "购物车商品编号", required = true, example = "1024,2048") + public CommonResult deleteCart(@RequestParam("ids") List ids) { + cartService.deleteCart(getLoginUserId(), ids); + return success(true); + } + + @GetMapping("get-count") + @Operation(summary = "查询用户在购物车中的商品数量") + public CommonResult getCartCount() { + return success(cartService.getCartCount(getLoginUserId())); + } + + @GetMapping("/list") + @Operation(summary = "查询用户的购物车列表") + public CommonResult getCartList() { + return success(cartService.getCartList(getLoginUserId())); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartAddReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartAddReqVO.java new file mode 100644 index 0000000..ecf9716 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartAddReqVO.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.trade.controller.app.cart.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "用户 App - 购物车添加购物项 Request VO") +@Data +public class AppCartAddReqVO { + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED,example = "1024") + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + @Schema(description = "新增商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数量不能为空") + @Min(value = 1, message = "商品数量必须大于等于 1") + private Integer count; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartDetailRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartDetailRespVO.java new file mode 100644 index 0000000..eb8a0e4 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartDetailRespVO.java @@ -0,0 +1,117 @@ +package com.tashow.cloud.trade.controller.app.cart.vo; + +import com.tashow.cloud.trade.controller.app.base.sku.AppProductSkuBaseRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 用户的购物车明细 Response VO") +@Data +public class AppCartDetailRespVO { + + /** + * 商品分组数组 + */ + private List itemGroups; + + /** + * 费用 + */ + private Order order; + + @Schema(description = "商品分组") // 多个商品,参加同一个活动,从而形成分组 + @Data + public static class ItemGroup { + + /** + * 商品数组 + */ + private List items; + /** + * 营销活动,订单级别 + */ + private Promotion promotion; + + } + + @Schema(description = "商品 SKU") + @Data + public static class Sku extends AppProductSkuBaseRespVO { + + /** + * SPU 信息 + */ + private AppProductSkuBaseRespVO spu; + + // ========== 购物车相关的字段 ========== + + @Schema(description = "商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + @Schema(description = "是否选中", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean selected; + + // ========== 价格相关的字段,对应 PriceCalculateRespDTO.OrderItem 的属性 ========== + + // TODO 芋艿:后续可以去除一些无用的字段 + + @Schema(description = "商品原价(单)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer originalPrice; + @Schema(description = "商品原价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Integer totalOriginalPrice; + @Schema(description = "商品级优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "300") + private Integer totalPromotionPrice; + @Schema(description = "最终购买金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "400") + private Integer totalPresentPrice; + @Schema(description = "最终购买金额(单)", requiredMode = Schema.RequiredMode.REQUIRED, example = "500") + private Integer presentPrice; + @Schema(description = "应付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "600") + private Integer totalPayPrice; + + // ========== 营销相关的字段 ========== + /** + * 营销活动,商品级别 + */ + private Promotion promotion; + + } + + @Schema(description = "订单") // 对应 PriceCalculateRespDTO.Order 类,用于费用(合计) + @Data + public static class Order { + + // TODO 芋艿:后续可以去除一些无用的字段 + + @Schema(description = "商品原价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer skuOriginalPrice; + @Schema(description = "商品优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Integer skuPromotionPrice; + @Schema(description = "订单优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "300") + private Integer orderPromotionPrice; + @Schema(description = "运费金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "400") + private Integer deliveryPrice; + @Schema(description = "应付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "500") + private Integer payPrice; + + } + + @Schema(description = "营销活动") // 对应 PriceCalculateRespDTO.Promotion 类的属性 + @Data + public static class Promotion { + + @Schema(description = "营销编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") // 营销活动的编号、优惠劵的编号 + private Long id; + @Schema(description = "营销名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "xx 活动") + private String name; + @Schema(description = "营销类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + // ========== 匹配情况 ========== + @Schema(description = "是否满足优惠条件", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean meet; + @Schema(description = "满足条件的提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "圣诞价:省 150.00 元") + private String meetTip; + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartListRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartListRespVO.java new file mode 100644 index 0000000..b76321c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartListRespVO.java @@ -0,0 +1,48 @@ +package com.tashow.cloud.trade.controller.app.cart.vo; + +import com.tashow.cloud.trade.controller.app.base.sku.AppProductSkuBaseRespVO; +import com.tashow.cloud.trade.controller.app.base.spu.AppProductSpuBaseRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 用户的购物列表 Response VO") +@Data +public class AppCartListRespVO { + + /** + * 有效的购物项数组 + */ + private List validList; + + /** + * 无效的购物项数组 + */ + private List invalidList; + + @Schema(description = "购物项") + @Data + public static class Cart { + + @Schema(description = "购物项的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + + @Schema(description = "是否选中", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean selected; + + /** + * 商品 SPU + */ + private AppProductSpuBaseRespVO spu; + /** + * 商品 SKU + */ + private AppProductSkuBaseRespVO sku; + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartResetReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartResetReqVO.java new file mode 100644 index 0000000..785aded --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartResetReqVO.java @@ -0,0 +1,26 @@ +package com.tashow.cloud.trade.controller.app.cart.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "用户 App - 购物车重置 Request VO") +@Data +public class AppCartResetReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED,example = "1024") + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + @Schema(description = "商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数量不能为空") + @Min(message = "数量必须大于 0", value = 1L) + private Integer count; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartUpdateCountReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartUpdateCountReqVO.java new file mode 100644 index 0000000..243d59e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartUpdateCountReqVO.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.controller.app.cart.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "用户 App - 购物车更新数量 Request VO") +@Data +public class AppCartUpdateCountReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数量不能为空") + @Min(message = "数量必须大于 0", value = 1L) + private Integer count; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartUpdateSelectedReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartUpdateSelectedReqVO.java new file mode 100644 index 0000000..185fae1 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/cart/vo/AppCartUpdateSelectedReqVO.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.trade.controller.app.cart.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; +import java.util.Collection; + +@Schema(description = "用户 App - 购物车更新是否选中 Request VO") +@Data +public class AppCartUpdateSelectedReqVO { + + @Schema(description = "编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024,2048") + @NotNull(message = "编号列表不能为空") + private Collection ids; + + @Schema(description = "是否选中", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否选中不能为空") + private Boolean selected; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/config/AppTradeConfigController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/config/AppTradeConfigController.java new file mode 100644 index 0000000..dbc9213 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/config/AppTradeConfigController.java @@ -0,0 +1,46 @@ +package com.tashow.cloud.trade.controller.app.config; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.tashow.cloud.trade.controller.app.config.vo.AppTradeConfigRespVO; +import com.tashow.cloud.trade.convert.config.TradeConfigConvert; +import com.tashow.cloud.trade.dal.dataobject.config.TradeConfigDO; +import com.tashow.cloud.trade.service.config.TradeConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.security.PermitAll; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +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.RestController; + +import jakarta.annotation.Resource; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 交易配置") +@RestController +@RequestMapping("/trade/config") +@RequiredArgsConstructor +@Validated +@Slf4j +public class AppTradeConfigController { + + @Resource + private TradeConfigService tradeConfigService; + + @Value("${yudao.tencent-lbs-key}") + private String tencentLbsKey; + + @GetMapping("/get") + @Operation(summary = "获得交易配置") + @PermitAll + public CommonResult getTradeConfig() { + TradeConfigDO config = ObjUtil.defaultIfNull(tradeConfigService.getTradeConfig(), new TradeConfigDO()); + return success(TradeConfigConvert.INSTANCE.convert02(config).setTencentLbsKey(tencentLbsKey)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/config/vo/AppTradeConfigRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/config/vo/AppTradeConfigRespVO.java new file mode 100644 index 0000000..e87c6d0 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/config/vo/AppTradeConfigRespVO.java @@ -0,0 +1,44 @@ +package com.tashow.cloud.trade.controller.app.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "用户 App - 交易配置 Response VO") +@Data +public class AppTradeConfigRespVO { + + @Schema(description = "腾讯地图 KEY", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + private String tencentLbsKey; + + // ========== 配送相关 ========== + + @Schema(description = "是否开启自提", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否开启自提不能为空") + private Boolean deliveryPickUpEnabled; + + // ========== 售后相关 ========== + + @Schema(description = "售后的退款理由", requiredMode = Schema.RequiredMode.REQUIRED) + private List afterSaleRefundReasons; + + @Schema(description = "售后的退货理由", requiredMode = Schema.RequiredMode.REQUIRED) + private List afterSaleReturnReasons; + + // ========== 分销相关 ========== + + @Schema(description = "分销海报地址数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List brokeragePosterUrls; + + @Schema(description = "佣金冻结时间(天)", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer brokerageFrozenDays; + + @Schema(description = "佣金提现最小金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer brokerageWithdrawMinPrice; + + @Schema(description = "提现方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]") + private List brokerageWithdrawTypes; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/AppDeliverExpressController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/AppDeliverExpressController.java new file mode 100644 index 0000000..717d04b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/AppDeliverExpressController.java @@ -0,0 +1,41 @@ +package com.tashow.cloud.trade.controller.app.delivery; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.tashow.cloud.trade.controller.app.delivery.vo.express.AppDeliveryExpressRespVO; +import com.tashow.cloud.trade.convert.delivery.DeliveryExpressConvert; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.tashow.cloud.trade.service.delivery.DeliveryExpressService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.security.PermitAll; +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.RestController; + +import jakarta.annotation.Resource; +import java.util.Comparator; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 快递公司") +@RestController +@RequestMapping("/trade/delivery/express") +@Validated +public class AppDeliverExpressController { + + @Resource + private DeliveryExpressService deliveryExpressService; + + @GetMapping("/list") + @Operation(summary = "获得快递公司列表") + @PermitAll + public CommonResult> getDeliveryExpressList() { + List list = deliveryExpressService.getDeliveryExpressListByStatus(CommonStatusEnum.ENABLE.getStatus()); + list.sort(Comparator.comparing(DeliveryExpressDO::getSort)); + return success(DeliveryExpressConvert.INSTANCE.convertList03(list)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/AppDeliverPickUpStoreController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/AppDeliverPickUpStoreController.java new file mode 100644 index 0000000..fe259f2 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/AppDeliverPickUpStoreController.java @@ -0,0 +1,58 @@ +package com.tashow.cloud.trade.controller.app.delivery; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.tashow.cloud.trade.controller.app.delivery.vo.pickup.AppDeliveryPickUpStoreRespVO; +import com.tashow.cloud.trade.convert.delivery.DeliveryPickUpStoreConvert; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import com.tashow.cloud.trade.service.delivery.DeliveryPickUpStoreService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.security.PermitAll; +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 jakarta.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 自提门店") +@RestController +@RequestMapping("/trade/delivery/pick-up-store") +@Validated +public class AppDeliverPickUpStoreController { + + @Resource + private DeliveryPickUpStoreService deliveryPickUpStoreService; + + @GetMapping("/list") + @Operation(summary = "获得自提门店列表") + @Parameters({ + @Parameter(name = "latitude", description = "精度", example = "110"), + @Parameter(name = "longitude", description = "纬度", example = "120") + }) + @PermitAll + public CommonResult> getDeliveryPickUpStoreList( + @RequestParam(value = "latitude", required = false) Double latitude, + @RequestParam(value = "longitude", required = false) Double longitude) { + List list = deliveryPickUpStoreService.getDeliveryPickUpStoreListByStatus( + CommonStatusEnum.ENABLE.getStatus()); + return success(DeliveryPickUpStoreConvert.INSTANCE.convertList(list, latitude, longitude)); + } + + @GetMapping("/get") + @Operation(summary = "获得自提门店") + @Parameter(name = "id", description = "门店编号") + @PermitAll + public CommonResult getOrder(@RequestParam("id") Long id) { + DeliveryPickUpStoreDO store = deliveryPickUpStoreService.getDeliveryPickUpStore(id); + return success(DeliveryPickUpStoreConvert.INSTANCE.convert03(store)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/vo/config/AppDeliveryConfigRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/vo/config/AppDeliveryConfigRespVO.java new file mode 100644 index 0000000..843b0b8 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/vo/config/AppDeliveryConfigRespVO.java @@ -0,0 +1,17 @@ +package com.tashow.cloud.trade.controller.app.delivery.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +// TODO 芋艿:后续要实现下,配送配置;后续融合到 AppTradeConfigRespVO 中 +@Schema(description = "用户 App - 配送配置 Response VO") +@Data +public class AppDeliveryConfigRespVO { + + @Schema(description = "腾讯地图 KEY", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + private String tencentLbsKey; + + @Schema(description = "是否开启自提", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean pickUpEnable; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/vo/express/AppDeliveryExpressRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/vo/express/AppDeliveryExpressRespVO.java new file mode 100644 index 0000000..ca9f525 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/vo/express/AppDeliveryExpressRespVO.java @@ -0,0 +1,16 @@ +package com.tashow.cloud.trade.controller.app.delivery.vo.express; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 快递公司 Response VO") +@Data +public class AppDeliveryExpressRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "门店名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "顺丰") + private String name; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/vo/pickup/AppDeliveryPickUpStoreRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/vo/pickup/AppDeliveryPickUpStoreRespVO.java new file mode 100644 index 0000000..324a98d --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/delivery/vo/pickup/AppDeliveryPickUpStoreRespVO.java @@ -0,0 +1,40 @@ +package com.tashow.cloud.trade.controller.app.delivery.vo.pickup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 自提门店 Response VO") +@Data +public class AppDeliveryPickUpStoreRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128") + private Long id; + + @Schema(description = "门店名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + private String name; + + @Schema(description = "门店 logo", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String logo; + + @Schema(description = "门店手机", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601892312") + private String phone; + + @Schema(description = "区域编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18733") + private Integer areaId; + + @Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区") + private String areaName; + + @Schema(description = "门店详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "复旦大学路 188 号") + private String detailAddress; + + @Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED, example = "5.88") + private Double latitude; + + @Schema(description = "经度", requiredMode = Schema.RequiredMode.REQUIRED, example = "6.99") + private Double longitude; + + @Schema(description = "距离,单位:千米", example = "100") // 只有在用户传递了经纬度时,才进行计算 + private Double distance; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/AppTradeOrderController.http b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/AppTradeOrderController.http new file mode 100644 index 0000000..f51ddff --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/AppTradeOrderController.http @@ -0,0 +1,69 @@ +### /trade-order/settlement 获得订单结算信息(基于商品) +GET {{appApi}}/trade/order/settlement?type=0&items[0].skuId=1&items[0].count=2&items[1].skuId=2&items[1].count=3&couponId=1 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +### /trade-order/settlement 获得订单结算信息(基于购物车) +GET {{appApi}}/trade/order/settlement?type=0&items[0].cartId=50&couponId=1 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +### /trade-order/create 创建订单(基于商品)【快递】 +POST {{appApi}}/trade/order/create +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +{ + "pointStatus": true, + "deliveryType": 1, + "addressId": 21, + "items": [ + { + "skuId": 1, + "count": 2 + } + ], + "remark": "我是备注" +} + +### /trade-order/create 创建订单(基于商品)【自提】 +POST {{appApi}}/trade/order/create +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +{ + "pointStatus": true, + "deliveryType": 2, + "pickUpStoreId": 1, + "items": [ + { + "skuId": 1, + "count": 2 + } + ], + "remark": "我是备注", + "receiverName": "土豆", + "receiverMobile": "15601691300" +} + +### 获得订单交易的分页 +GET {{appApi}}/trade/order/page?pageNo=1&pageSize=10 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +### 获得订单交易的详细 +GET {{appApi}}/trade/order/get-detail?id=21 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +### 获得交易订单的物流轨迹 +GET {{appApi}}/trade/order/get-express-track-list?id=70 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} + +### /trade-order/settlement-product 获得商品结算信息 +GET {{appApi}}/trade/order/settlement-product?spuIds=633 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenantId}} \ No newline at end of file diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/AppTradeOrderController.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/AppTradeOrderController.java new file mode 100644 index 0000000..71c11fa --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/AppTradeOrderController.java @@ -0,0 +1,204 @@ +package com.tashow.cloud.trade.controller.app.order; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import cn.iocoder.yudao.module.trade.controller.app.order.vo.*; +import com.tashow.cloud.trade.controller.app.order.vo.*; +import com.tashow.cloud.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; +import com.tashow.cloud.trade.controller.app.order.vo.item.AppTradeOrderItemRespVO; +import com.tashow.cloud.trade.convert.order.TradeOrderConvert; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; +import com.tashow.cloud.trade.framework.order.config.TradeOrderProperties; +import com.tashow.cloud.trade.service.aftersale.AfterSaleService; +import com.tashow.cloud.trade.service.delivery.DeliveryExpressService; +import com.tashow.cloud.trade.service.order.TradeOrderQueryService; +import com.tashow.cloud.trade.service.order.TradeOrderUpdateService; +import com.tashow.cloud.trade.service.price.TradePriceService; +import com.google.common.collect.Maps; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +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.convertSet; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 交易订单") +@RestController +@RequestMapping("/trade/order") +@Validated +@Slf4j +public class AppTradeOrderController { + + @Resource + private TradeOrderUpdateService tradeOrderUpdateService; + @Resource + private TradeOrderQueryService tradeOrderQueryService; + @Resource + private DeliveryExpressService deliveryExpressService; + @Resource + private AfterSaleService afterSaleService; + @Resource + private TradePriceService priceService; + + @Resource + private TradeOrderProperties tradeOrderProperties; + + @GetMapping("/settlement") + @Operation(summary = "获得订单结算信息") + public CommonResult settlementOrder(@Valid AppTradeOrderSettlementReqVO settlementReqVO) { + return success(tradeOrderUpdateService.settlementOrder(getLoginUserId(), settlementReqVO)); + } + + @GetMapping("/settlement-product") + @Operation(summary = "获得商品结算信息", description = "用于商品列表、商品详情,获得参与活动后的价格信息") + @Parameter(name = "spuIds", description = "商品 SPU 编号数组") + @PermitAll + public CommonResult> settlementProduct(@RequestParam("spuIds") List spuIds) { + return success(priceService.calculateProductPrice(getLoginUserId(), spuIds)); + } + + @PostMapping("/create") + @Operation(summary = "创建订单") + public CommonResult createOrder(@Valid @RequestBody AppTradeOrderCreateReqVO createReqVO) { + TradeOrderDO order = tradeOrderUpdateService.createOrder(getLoginUserId(), createReqVO); + return success(new AppTradeOrderCreateRespVO().setId(order.getId()).setPayOrderId(order.getPayOrderId())); + } + + @PostMapping("/update-paid") + @Operation(summary = "更新订单为已支付") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll + public CommonResult updateOrderPaid(@RequestBody PayOrderNotifyReqDTO notifyReqDTO) { + tradeOrderUpdateService.updateOrderPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getPayOrderId()); + return success(true); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得交易订单") + @Parameters({ + @Parameter(name = "id", description = "交易订单编号"), + @Parameter(name = "sync", description = "是否同步支付状态", example = "true") + }) + public CommonResult getOrderDetail(@RequestParam("id") Long id, + @RequestParam(value = "sync", required = false) Boolean sync) { + // 1.1 查询订单 + TradeOrderDO order = tradeOrderQueryService.getOrder(getLoginUserId(), id); + if (order == null) { + return success(null); + } + // 1.2 sync 仅在等待支付 + if (Boolean.TRUE.equals(sync) + && TradeOrderStatusEnum.isUnpaid(order.getStatus()) && !order.getPayStatus()) { + tradeOrderUpdateService.syncOrderPayStatusQuietly(order.getId(), order.getPayOrderId()); + // 重新查询,因为同步后,可能会有变化 + order = tradeOrderQueryService.getOrder(id); + } + + // 2.1 查询订单项 + List orderItems = tradeOrderQueryService.getOrderItemListByOrderId(order.getId()); + // 2.2 查询物流公司 + DeliveryExpressDO express = order.getLogisticsId() != null && order.getLogisticsId() > 0 ? + deliveryExpressService.getDeliveryExpress(order.getLogisticsId()) : null; + // 2.3 最终组合 + return success(TradeOrderConvert.INSTANCE.convert02(order, orderItems, tradeOrderProperties, express)); + } + + @GetMapping("/get-express-track-list") + @Operation(summary = "获得交易订单的物流轨迹") + @Parameter(name = "id", description = "交易订单编号") + public CommonResult> getOrderExpressTrackList(@RequestParam("id") Long id) { + return success(TradeOrderConvert.INSTANCE.convertList02( + tradeOrderQueryService.getExpressTrackList(id, getLoginUserId()))); + } + + @GetMapping("/page") + @Operation(summary = "获得交易订单分页") + public CommonResult> getOrderPage(AppTradeOrderPageReqVO reqVO) { + // 查询订单 + PageResult pageResult = tradeOrderQueryService.getOrderPage(getLoginUserId(), reqVO); + // 查询订单项 + List orderItems = tradeOrderQueryService.getOrderItemListByOrderId( + convertSet(pageResult.getList(), TradeOrderDO::getId)); + // 最终组合 + return success(TradeOrderConvert.INSTANCE.convertPage02(pageResult, orderItems)); + } + + @GetMapping("/get-count") + @Operation(summary = "获得交易订单数量") + public CommonResult> getOrderCount() { + Map orderCount = Maps.newLinkedHashMapWithExpectedSize(5); + // 全部 + orderCount.put("allCount", tradeOrderQueryService.getOrderCount(getLoginUserId(), null, null)); + // 待付款(未支付) + orderCount.put("unpaidCount", tradeOrderQueryService.getOrderCount(getLoginUserId(), + TradeOrderStatusEnum.UNPAID.getStatus(), null)); + // 待发货 + orderCount.put("undeliveredCount", tradeOrderQueryService.getOrderCount(getLoginUserId(), + TradeOrderStatusEnum.UNDELIVERED.getStatus(), null)); + // 待收货 + orderCount.put("deliveredCount", tradeOrderQueryService.getOrderCount(getLoginUserId(), + TradeOrderStatusEnum.DELIVERED.getStatus(), null)); + // 待评价 + orderCount.put("uncommentedCount", tradeOrderQueryService.getOrderCount(getLoginUserId(), + TradeOrderStatusEnum.COMPLETED.getStatus(), false)); + // 售后数量 + orderCount.put("afterSaleCount", afterSaleService.getApplyingAfterSaleCount(getLoginUserId())); + return success(orderCount); + } + + @PutMapping("/receive") + @Operation(summary = "确认交易订单收货") + @Parameter(name = "id", description = "交易订单编号") + public CommonResult receiveOrder(@RequestParam("id") Long id) { + tradeOrderUpdateService.receiveOrderByMember(getLoginUserId(), id); + return success(true); + } + + @DeleteMapping("/cancel") + @Operation(summary = "取消交易订单") + @Parameter(name = "id", description = "交易订单编号") + public CommonResult cancelOrder(@RequestParam("id") Long id) { + tradeOrderUpdateService.cancelOrderByMember(getLoginUserId(), id); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除交易订单") + @Parameter(name = "id", description = "交易订单编号") + public CommonResult deleteOrder(@RequestParam("id") Long id) { + tradeOrderUpdateService.deleteOrder(getLoginUserId(), id); + return success(true); + } + + // ========== 订单项 ========== + + @GetMapping("/item/get") + @Operation(summary = "获得交易订单项") + @Parameter(name = "id", description = "交易订单项编号") + public CommonResult getOrderItem(@RequestParam("id") Long id) { + TradeOrderItemDO item = tradeOrderQueryService.getOrderItem(getLoginUserId(), id); + return success(TradeOrderConvert.INSTANCE.convert03(item)); + } + + @PostMapping("/item/create-comment") + @Operation(summary = "创建交易订单项的评价") + public CommonResult createOrderItemComment(@RequestBody AppTradeOrderItemCommentCreateReqVO createReqVO) { + return success(tradeOrderUpdateService.createOrderItemCommentByMember(getLoginUserId(), createReqVO)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppOrderExpressTrackRespDTO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppOrderExpressTrackRespDTO.java new file mode 100644 index 0000000..e5c591e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppOrderExpressTrackRespDTO.java @@ -0,0 +1,23 @@ +package com.tashow.cloud.trade.controller.app.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 快递查询的轨迹 Resp DTO + * + * @author jason + */ +@Schema(description = "用户 App - 快递查询的轨迹 Response VO") +@Data +public class AppOrderExpressTrackRespDTO { + + @Schema(description = "发生时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime time; + + @Schema(description = "快递状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "已签收") + private String content; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderCreateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderCreateReqVO.java new file mode 100644 index 0000000..8f22f80 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderCreateReqVO.java @@ -0,0 +1,21 @@ +package com.tashow.cloud.trade.controller.app.order.vo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import lombok.Data; + +@Schema(description = "用户 App - 交易订单创建 Request VO") +@Data +public class AppTradeOrderCreateReqVO extends AppTradeOrderSettlementReqVO { + + @Schema(description = "备注", example = "这个是我的订单哟") + private String remark; + + @AssertTrue(message = "配送方式不能为空") + @JsonIgnore + public boolean isDeliveryTypeNotNull() { + return getDeliveryType() != null; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderCreateRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderCreateRespVO.java new file mode 100644 index 0000000..13c9eb8 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderCreateRespVO.java @@ -0,0 +1,16 @@ +package com.tashow.cloud.trade.controller.app.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 交易订单创建 Response VO") +@Data +public class AppTradeOrderCreateRespVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long payOrderId; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderDetailRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderDetailRespVO.java new file mode 100644 index 0000000..1d56c11 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderDetailRespVO.java @@ -0,0 +1,151 @@ +package com.tashow.cloud.trade.controller.app.order.vo; + +import com.tashow.cloud.trade.controller.app.order.vo.item.AppTradeOrderItemRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "用户 App - 订单交易的明细 Response VO") +@Data +public class AppTradeOrderDetailRespVO { + + // ========== 订单基本信息 ========== + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "订单流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1146347329394184195") + private String no; + + @Schema(description = "订单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer type; + + @Schema(description = "下单时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "用户备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + private String userRemark; + + @Schema(description = "订单状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "购买的商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer productCount; + + @Schema(description = "订单完成时间") + private LocalDateTime finishTime; + + @Schema(description = "订单取消时间") + private LocalDateTime cancelTime; + + @Schema(description = "是否评价", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean commentStatus; + + // ========== 价格 + 支付基本信息 ========== + + @Schema(description = "是否已支付", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean payStatus; + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long payOrderId; + + @Schema(description = "付款时间") + private LocalDateTime payTime; + + @Schema(description = "付款超时时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime payExpireTime; + + @Schema(description = "支付渠道", example = "wx_lite_pay") + private String payChannelCode; + @Schema(description = "支付渠道名", example = "微信小程序支付") + private String payChannelName; + + @Schema(description = "商品原价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer totalPrice; + + @Schema(description = "订单优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer discountPrice; + + @Schema(description = "运费金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer deliveryPrice; + + @Schema(description = "订单调价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer adjustPrice; + + @Schema(description = "应付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer payPrice; + + // ========== 收件 + 物流基本信息 ========== + + @Schema(description = "配送方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer deliveryType; + + @Schema(description = "发货物流公司编号", example = "10") + private Long logisticsId; + + @Schema(description = "发货物流名称", example = "顺丰快递") + private String logisticsName; + + @Schema(description = "发货物流单号", example = "1024") + private String logisticsNo; + + @Schema(description = "发货时间") + private LocalDateTime deliveryTime; + + @Schema(description = "收货时间") + private LocalDateTime receiveTime; + + @Schema(description = "收件人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + private String receiverName; + + @Schema(description = "收件人手机", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000") + private String receiverMobile; + + @Schema(description = "收件人地区编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110000") + private Integer receiverAreaId; + + @Schema(description = "收件人地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海 上海市 普陀区") + private String receiverAreaName; + + @Schema(description = "收件人详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "中关村大街 1 号") + private String receiverDetailAddress; + + @Schema(description = "自提门店编号", example = "1088") + private Long pickUpStoreId; + + @Schema(description = "自提核销码", example = "40964096") + private String pickUpVerifyCode; + + // ========== 售后基本信息 ========== + + @Schema(description = "售后状态", example = "0") + private Integer refundStatus; + + @Schema(description = "退款金额,单位:分", example = "100") + private Integer refundPrice; + + // ========== 营销基本信息 ========== + + @Schema(description = "优惠劵编号", example = "1024") + private Long couponId; + + @Schema(description = "优惠劵减免金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer couponPrice; + + @Schema(description = "积分抵扣的金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer pointPrice; + + @Schema(description = "VIP 减免金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "888") + private Integer vipPrice; + + @Schema(description = "拼团记录编号", example = "100") + private Long combinationRecordId; + + /** + * 订单项数组 + */ + private List items; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderPageItemRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderPageItemRespVO.java new file mode 100644 index 0000000..55943ad --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderPageItemRespVO.java @@ -0,0 +1,58 @@ +package com.tashow.cloud.trade.controller.app.order.vo; + +import com.tashow.cloud.trade.controller.app.order.vo.item.AppTradeOrderItemRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "用户 App - 订单交易的分页项 Response VO") +@Data +public class AppTradeOrderPageItemRespVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "订单流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1146347329394184195") + private String no; + + @Schema(description = "订单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer type; + + @Schema(description = "订单状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "购买的商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer productCount; + + @Schema(description = "是否评价", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean commentStatus; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + // ========== 价格 + 支付基本信息 ========== + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long payOrderId; + + @Schema(description = "应付金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer payPrice; + + // ========== 收件 + 物流基本信息 ========== + + @Schema(description = "配送方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer deliveryType; + + /** + * 订单项数组 + */ + private List items; + + // ========== 营销基本信息 ========== + + @Schema(description = "拼团记录编号", example = "100") + private Long combinationRecordId; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderPageReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderPageReqVO.java new file mode 100644 index 0000000..1d79faa --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderPageReqVO.java @@ -0,0 +1,20 @@ +package com.tashow.cloud.trade.controller.app.order.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "交易订单分页 Request VO") +@Data +public class AppTradeOrderPageReqVO extends PageParam { + + @Schema(description = "订单状态", example = "1") + @InEnum(value = TradeOrderStatusEnum.class, message = "订单状态必须是 {value}") + private Integer status; + + @Schema(description = "是否评价", example = "true") + private Boolean commentStatus; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderSettlementReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderSettlementReqVO.java new file mode 100644 index 0000000..ba06d1b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderSettlementReqVO.java @@ -0,0 +1,109 @@ +package com.tashow.cloud.trade.controller.app.order.vo; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.common.validation.Mobile; +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 交易订单结算 Request VO") +@Data +@Valid +public class AppTradeOrderSettlementReqVO { + + @Schema(description = "商品项数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "商品不能为空") + private List items; + + @Schema(description = "优惠劵编号", example = "1024") + private Long couponId; + + @Schema(description = "是否使用积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否使用积分不能为空") + private Boolean pointStatus; + + // ========== 配送相关相关字段 ========== + @Schema(description = "配送方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(value = DeliveryTypeEnum.class, message = "配送方式不正确") + private Integer deliveryType; + + @Schema(description = "收件地址编号", example = "1") + private Long addressId; + + @Schema(description = "自提门店编号", example = "1088") + private Long pickUpStoreId; + @Schema(description = "收件人名称", example = "芋艿") // 选择门店自提时,该字段为联系人名 + private String receiverName; + @Schema(description = "收件人手机", example = "15601691300") // 选择门店自提时,该字段为联系人手机 + @Mobile(message = "收件人手机格式不正确") + private String receiverMobile; + + // ========== 秒杀活动相关字段 ========== + @Schema(description = "秒杀活动编号", example = "1024") + private Long seckillActivityId; + + // ========== 拼团活动相关字段 ========== + @Schema(description = "拼团活动编号", example = "1024") + private Long combinationActivityId; + + @Schema(description = "拼团团长编号", example = "2048") + private Long combinationHeadId; + + // ========== 砍价活动相关字段 ========== + @Schema(description = "砍价记录编号", example = "123") + private Long bargainRecordId; + + // ========== 积分商城活动相关字段 ========== + @Schema(description = "积分商城活动编号", example = "123") + private Long pointActivityId; + + @AssertTrue(message = "活动商品每次只能购买一种规格") + @JsonIgnore + public boolean isValidActivityItems() { + // 校验是否是活动订单 + if (ObjUtil.isAllEmpty(seckillActivityId, combinationActivityId, combinationHeadId, bargainRecordId)) { + return true; + } + // 校验订单项是否超出 + return items.size() == 1; + } + + @Data + @Schema(description = "用户 App - 商品项") + @Valid + public static class Item { + + @Schema(description = "商品 SKU 编号", example = "2048") + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + @Schema(description = "购买数量", example = "1") + @Min(value = 1, message = "购买数量最小值为 {value}") + private Integer count; + + @Schema(description = "购物车项的编号", example = "1024") + private Long cartId; + + @AssertTrue(message = "商品不正确") + @JsonIgnore + public boolean isValid() { + // 组合一:skuId + count 使用商品 SKU + if (skuId != null && count != null) { + return true; + } + // 组合二:cartId 使用购物车项 + return cartId != null; + } + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java new file mode 100644 index 0000000..d39b48b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java @@ -0,0 +1,174 @@ +package com.tashow.cloud.trade.controller.app.order.vo; + +import com.tashow.cloud.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "用户 App - 交易订单结算信息 Response VO") +@Data +public class AppTradeOrderSettlementRespVO { + + @Schema(description = "交易类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") // 对应 TradeOrderTypeEnum 枚举 + private Integer type; + + @Schema(description = "购物项数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List items; + + @Schema(description = "优惠劵数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List coupons; // 可用 + 不可用 + + @Schema(description = "费用", requiredMode = Schema.RequiredMode.REQUIRED) + private Price price; + + @Schema(description = "收件地址", requiredMode = Schema.RequiredMode.REQUIRED) + private Address address; + + @Schema(description = "已使用的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer usePoint; + + @Schema(description = "总积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer totalPoint; + + /** + * 营销活动数组 + * + * 只对应 {@link TradePriceCalculateRespBO.Price#items} 商品匹配的活动 + */ + private List promotions; + + @Schema(description = "购物项") + @Data + public static class Item { + + // ========== SPU 信息 ========== + + @Schema(description = "品类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long categoryId; + @Schema(description = "SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long spuId; + @Schema(description = "SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "Apple iPhone 12") + private String spuName; + + // ========== SKU 信息 ========== + + @Schema(description = "SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer skuId; + @Schema(description = "价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer price; + @Schema(description = "图片地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "属性数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private List properties; + + // ========== 购物车信息 ========== + + @Schema(description = "购物车编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long cartId; + + @Schema(description = "购买数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + + } + + @Schema(description = "费用(合计)") + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Price { + + @Schema(description = "商品原价(总),单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "500") + private Integer totalPrice; + + @Schema(description = "订单优惠(总),单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "66") + private Integer discountPrice; + + @Schema(description = "运费金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer deliveryPrice; + + @Schema(description = "优惠劵减免金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer couponPrice; + + @Schema(description = "积分抵扣的金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer pointPrice; + + @Schema(description = "VIP 减免金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") + private Integer vipPrice; + + @Schema(description = "实际支付金额(总),单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "450") + private Integer payPrice; + + } + + @Schema(description = "地址信息") + @Data + public static class Address { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "收件人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王") + private String name; + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + private String mobile; + + @Schema(description = "地区编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long areaId; + @Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区") + private String areaName; + + @Schema(description = "详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "望京悠乐汇 A 座") + private String detailAddress; + + @Schema(description = "是否默认收件地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean defaultStatus; + + } + + @Schema(description = "优惠劵信息") + @Data + public static class Coupon { + + @Schema(description = "优惠劵编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送") + private String name; + + @Schema(description = "是否设置满多少金额可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 单位:分;0 - 不限制 + private Integer usePrice; + + @Schema(description = "固定日期 - 生效开始时间") + private LocalDateTime validStartTime; + + @Schema(description = "固定日期 - 生效结束时间") + private LocalDateTime validEndTime; + + @Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer discountType; + + @Schema(description = "折扣百分比", example = "80") // 例如说,80% 为 80 + private Integer discountPercent; + + @Schema(description = "优惠金额", example = "10") + private Integer discountPrice; + + @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用 + private Integer discountLimitPrice; + + @Schema(description = "是否可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean match; + + @Schema(description = "不可用原因", example = "优惠劵已过期") + private String mismatchReason; + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeProductSettlementRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeProductSettlementRespVO.java new file mode 100644 index 0000000..f6e31c3 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/AppTradeProductSettlementRespVO.java @@ -0,0 +1,81 @@ +package com.tashow.cloud.trade.controller.app.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Schema(description = "用户 App - 商品结算信息 Response VO") +@Data +public class AppTradeProductSettlementRespVO { + + @Schema(description = "SPU 商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long spuId; + + @Schema(description = "SKU 价格信息数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private List skus; + + @Schema(description = "满减送活动信息", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private RewardActivity rewardActivity; + + @Schema(description = "SKU 价格信息") + @Data + public static class Sku implements Serializable { + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "优惠后价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer promotionPrice; + + @Schema(description = "营销类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "4") + private Integer promotionType; // 对应 PromotionTypeEnum 枚举,目前只有 4 和 6 两种 + + @Schema(description = "营销编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long promotionId; // 目前只有限时折扣活动的编号 + + @Schema(description = "活动结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime promotionEndTime; + + } + + @Schema(description = "满减送活动信息") + @Data + public static class RewardActivity { + + @Schema(description = "满减活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "条件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer conditionType; + + @Schema(description = "优惠规则的数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List rules; + + } + + @Schema(description = "优惠规则") + @Data + public static class RewardActivityRule { + + @Schema(description = "优惠门槛", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 1. 满 N 元,单位:分; 2. 满 N 件 + private Integer limit; + + @Schema(description = "优惠价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer discountPrice; + + @Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean freeDelivery; + + @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer point; + + @Schema(description = "赠送的优惠劵编号的数组") + private Map giveCouponTemplateCounts; + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/item/AppTradeOrderItemCommentCreateReqVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/item/AppTradeOrderItemCommentCreateReqVO.java new file mode 100644 index 0000000..f4f8ace --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/item/AppTradeOrderItemCommentCreateReqVO.java @@ -0,0 +1,38 @@ +package com.tashow.cloud.trade.controller.app.order.vo.item; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; + +@Schema(description = "用户 App - 商品评价创建 Request VO") +@Data +public class AppTradeOrderItemCommentCreateReqVO { + + @Schema(description = "是否匿名", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否匿名不能为空") + private Boolean anonymous; + + @Schema(description = "交易订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2312312") + @NotNull(message = "交易订单项编号不能为空") + private Long orderItemId; + + @Schema(description = "描述星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "描述星级 1-5 分不能为空") + private Integer descriptionScores; + + @Schema(description = "服务星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "服务星级 1-5 分不能为空") + private Integer benefitScores; + + @Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "穿身上很漂亮诶(*^▽^*)") + @NotNull(message = "评论内容不能为空") + private String content; + + @Schema(description = "评论图片地址数组,以逗号分隔最多上传 9 张", requiredMode = Schema.RequiredMode.REQUIRED, example = "[https://www.iocoder.cn/xx.png]") + @Size(max = 9, message = "评论图片地址数组长度不能超过 9 张") + private List picUrls; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/item/AppTradeOrderItemRespVO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/item/AppTradeOrderItemRespVO.java new file mode 100644 index 0000000..1db5a42 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/app/order/vo/item/AppTradeOrderItemRespVO.java @@ -0,0 +1,61 @@ +package com.tashow.cloud.trade.controller.app.order.vo.item; + +import com.tashow.cloud.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 订单交易项 Response VO") +@Data +public class AppTradeOrderItemRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long orderId; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long spuId; + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String spuName; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long skuId; + + /** + * 属性数组 + */ + private List properties; + + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "购买数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + + @Schema(description = "是否评价", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean commentStatus; + + // ========== 价格 + 支付基本信息 ========== + + @Schema(description = "商品原价(单)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer price; + + @Schema(description = "应付金额(总),单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer payPrice; + + // ========== 营销基本信息 ========== + + // TODO 芋艿:在捉摸一下 + + // ========== 售后基本信息 ========== + + @Schema(description = "售后编号", example = "1024") + private Long afterSaleId; + + @Schema(description = "售后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer afterSaleStatus; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/package-info.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/controller/package-info.java new file mode 100644 index 0000000..516cd24 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/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 com.tashow.cloud.trade.controller; diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/aftersale/AfterSaleConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/aftersale/AfterSaleConvert.java new file mode 100644 index 0000000..e8c5e71 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/aftersale/AfterSaleConvert.java @@ -0,0 +1,88 @@ +package com.tashow.cloud.trade.convert.aftersale; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.iocoder.yudao.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.tashow.cloud.trade.controller.admin.aftersale.vo.AfterSaleDetailRespVO; +import com.tashow.cloud.trade.controller.admin.aftersale.vo.AfterSaleRespPageItemVO; +import com.tashow.cloud.trade.controller.admin.aftersale.vo.log.AfterSaleLogRespVO; +import com.tashow.cloud.trade.controller.admin.base.member.user.MemberUserRespVO; +import com.tashow.cloud.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderBaseVO; +import com.tashow.cloud.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO; +import com.tashow.cloud.trade.controller.app.aftersale.vo.AppAfterSaleRespVO; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleDO; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleLogDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import com.tashow.cloud.trade.framework.order.config.TradeOrderProperties; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface AfterSaleConvert { + + AfterSaleConvert INSTANCE = Mappers.getMapper(AfterSaleConvert.class); + + @Mappings({ + @Mapping(target = "id", ignore = true), + @Mapping(target = "createTime", ignore = true), + @Mapping(target = "updateTime", ignore = true), + @Mapping(target = "creator", ignore = true), + @Mapping(target = "updater", ignore = true), + }) + AfterSaleDO convert(AppAfterSaleCreateReqVO createReqVO, TradeOrderItemDO tradeOrderItem); + + @Mappings({ + @Mapping(source = "afterSale.orderId", target = "merchantOrderId"), + @Mapping(source = "afterSale.id", target = "merchantRefundId"), + @Mapping(source = "afterSale.applyReason", target = "reason"), + @Mapping(source = "afterSale.refundPrice", target = "price"), + @Mapping(source = "orderProperties.payAppKey", target = "appKey") + }) + PayRefundCreateReqDTO convert(String userIp, AfterSaleDO afterSale, TradeOrderProperties orderProperties); + + MemberUserRespVO convert(MemberUserRespDTO bean); + + PageResult convertPage(PageResult page); + + default PageResult convertPage(PageResult pageResult, + Map memberUsers) { + PageResult voPageResult = convertPage(pageResult); + // 处理会员 + voPageResult.getList().forEach(afterSale -> afterSale.setUser( + convert(memberUsers.get(afterSale.getUserId())))); + return voPageResult; + } + + ProductPropertyValueDetailRespVO convert(ProductPropertyValueDetailRespDTO bean); + + AppAfterSaleRespVO convert(AfterSaleDO bean); + + PageResult convertPage02(PageResult page); + + default AfterSaleDetailRespVO convert(AfterSaleDO afterSale, TradeOrderDO order, TradeOrderItemDO orderItem, + MemberUserRespDTO user, List logs) { + AfterSaleDetailRespVO respVO = convert02(afterSale); + // 处理用户信息 + respVO.setUser(convert(user)); + // 处理订单信息 + respVO.setOrder(convert(order)); + respVO.setOrderItem(convert02(orderItem)); + // 处理售后日志 + respVO.setLogs(convertList1(logs)); + return respVO; + } + + List convertList1(List list); + AfterSaleDetailRespVO convert02(AfterSaleDO bean); + AfterSaleDetailRespVO.OrderItem convert02(TradeOrderItemDO bean); + TradeOrderBaseVO convert(TradeOrderDO bean); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/aftersale/AfterSaleLogConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/aftersale/AfterSaleLogConvert.java new file mode 100644 index 0000000..2beeb60 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/aftersale/AfterSaleLogConvert.java @@ -0,0 +1,15 @@ +package com.tashow.cloud.trade.convert.aftersale; + +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleLogDO; +import com.tashow.cloud.trade.service.aftersale.bo.AfterSaleLogCreateReqBO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface AfterSaleLogConvert { + + AfterSaleLogConvert INSTANCE = Mappers.getMapper(AfterSaleLogConvert.class); + + AfterSaleLogDO convert(AfterSaleLogCreateReqBO bean); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/brokerage/BrokerageRecordConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/brokerage/BrokerageRecordConvert.java new file mode 100644 index 0000000..cd913b0 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/brokerage/BrokerageRecordConvert.java @@ -0,0 +1,79 @@ +package com.tashow.cloud.trade.convert.brokerage; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.record.BrokerageRecordRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.record.AppBrokerageRecordPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByPriceRespVO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageRecordDO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageUserDO; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 佣金记录 Convert + * + * @author owen + */ +@Mapper +public interface BrokerageRecordConvert { + + BrokerageRecordConvert INSTANCE = Mappers.getMapper(BrokerageRecordConvert.class); + + BrokerageRecordRespVO convert(BrokerageRecordDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default BrokerageRecordDO convert(BrokerageUserDO user, BrokerageRecordBizTypeEnum bizType, String bizId, + Integer brokerageFrozenDays, int brokeragePrice, LocalDateTime unfreezeTime, + String title, Long sourceUserId, Integer sourceUserLevel) { + brokerageFrozenDays = ObjectUtil.defaultIfNull(brokerageFrozenDays, 0); + // 不冻结时,佣金直接就是结算状态 + Integer status = brokerageFrozenDays > 0 + ? BrokerageRecordStatusEnum.WAIT_SETTLEMENT.getStatus() + : BrokerageRecordStatusEnum.SETTLEMENT.getStatus(); + return new BrokerageRecordDO().setUserId(user.getId()) + .setBizType(bizType.getType()).setBizId(bizId) + .setPrice(brokeragePrice).setTotalPrice(user.getBrokeragePrice()) + .setTitle(title) + .setDescription(StrUtil.format(bizType.getDescription(), MoneyUtils.fenToYuanStr(Math.abs(brokeragePrice)))) + .setStatus(status).setFrozenDays(brokerageFrozenDays).setUnfreezeTime(unfreezeTime) + .setSourceUserLevel(sourceUserLevel).setSourceUserId(sourceUserId); + } + + default PageResult convertPage(PageResult pageResult, Map userMap) { + PageResult result = convertPage(pageResult); + for (BrokerageRecordRespVO respVO : result.getList()) { + Optional.ofNullable(userMap.get(respVO.getUserId())).ifPresent(user -> + respVO.setUserNickname(user.getNickname()).setUserAvatar(user.getAvatar())); + Optional.ofNullable(userMap.get(respVO.getSourceUserId())).ifPresent(user -> + respVO.setSourceUserNickname(user.getNickname()).setSourceUserAvatar(user.getAvatar())); + } + return result; + } + + BrokerageRecordPageReqVO convert(AppBrokerageRecordPageReqVO pageReqVO, Long userId); + + default PageResult convertPage03(PageResult pageResult, Map userMap) { + for (AppBrokerageUserRankByPriceRespVO vo : pageResult.getList()) { + copyTo(userMap.get(vo.getId()), vo); + } + return pageResult; + } + + void copyTo(MemberUserRespDTO from, @MappingTarget AppBrokerageUserRankByPriceRespVO to); +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/brokerage/BrokerageUserConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/brokerage/BrokerageUserConvert.java new file mode 100644 index 0000000..302059c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/brokerage/BrokerageUserConvert.java @@ -0,0 +1,99 @@ +package com.tashow.cloud.trade.convert.brokerage; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.user.BrokerageUserRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserChildSummaryRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserMySummaryRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByUserCountRespVO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageUserDO; +import com.tashow.cloud.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO; +import com.tashow.cloud.trade.service.brokerage.bo.UserBrokerageSummaryRespBO; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 分销用户 Convert + * + * @author owen + */ +@Mapper +public interface BrokerageUserConvert { + + BrokerageUserConvert INSTANCE = Mappers.getMapper(BrokerageUserConvert.class); + + BrokerageUserRespVO convert(BrokerageUserDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page, Map userMap, Map brokerageUserCountMap, Map userOrderSummaryMap); + + default PageResult convertPage(PageResult pageResult, + Map userMap, + Map brokerageUserCountMap, + Map userOrderSummaryMap, + Map withdrawMap) { + PageResult result = convertPage(pageResult, userMap, brokerageUserCountMap, userOrderSummaryMap); + for (BrokerageUserRespVO userVO : result.getList()) { + // 用户信息 + copyTo(userMap.get(userVO.getId()), userVO); + // 推广用户数量 + userVO.setBrokerageUserCount(MapUtil.getInt(brokerageUserCountMap, userVO.getId(), 0)); + // 推广订单数量、推广订单金额 + Optional orderSummaryOptional = Optional.ofNullable(userOrderSummaryMap.get(userVO.getId())); + userVO.setBrokerageOrderCount(orderSummaryOptional.map(UserBrokerageSummaryRespBO::getCount).orElse(0)) + .setBrokerageOrderPrice(orderSummaryOptional.map(UserBrokerageSummaryRespBO::getPrice).orElse(0)); + // 已提现次数、已提现金额 + Optional withdrawSummaryOptional = Optional.ofNullable(withdrawMap.get(userVO.getId())); + userVO.setWithdrawCount(withdrawSummaryOptional.map(BrokerageWithdrawSummaryRespBO::getCount).orElse(0)) + .setWithdrawPrice(withdrawSummaryOptional.map(BrokerageWithdrawSummaryRespBO::getPrice).orElse(0)); + } + return result; + } + + default BrokerageUserRespVO copyTo(MemberUserRespDTO source, BrokerageUserRespVO target) { + if (target == null) { + return null; + } + Optional.ofNullable(source).ifPresent( + user -> target.setNickname(user.getNickname()).setAvatar(user.getAvatar())); + return target; + } + + default PageResult convertPage03(PageResult pageResult, + Map userMap) { + pageResult.getList().forEach(vo -> copyTo(userMap.get(vo.getId()), vo)); + return pageResult; + } + + void copyTo(MemberUserRespDTO from, @MappingTarget AppBrokerageUserRankByUserCountRespVO to); + + default AppBrokerageUserMySummaryRespVO convert(Integer yesterdayPrice, Integer withdrawPrice, + Long firstBrokerageUserCount, Long secondBrokerageUserCount, + BrokerageUserDO brokerageUser) { + AppBrokerageUserMySummaryRespVO respVO = new AppBrokerageUserMySummaryRespVO() + .setYesterdayPrice(ObjUtil.defaultIfNull(yesterdayPrice, 0)) + .setWithdrawPrice(ObjUtil.defaultIfNull(withdrawPrice, 0)) + .setBrokeragePrice(0).setFrozenPrice(0) + .setFirstBrokerageUserCount(ObjUtil.defaultIfNull(firstBrokerageUserCount, 0L)) + .setSecondBrokerageUserCount(ObjUtil.defaultIfNull(secondBrokerageUserCount, 0L)); + // 设置 brokeragePrice、frozenPrice 字段 + Optional.ofNullable(brokerageUser) + .ifPresent(user -> respVO.setBrokeragePrice(user.getBrokeragePrice()).setFrozenPrice(user.getFrozenPrice())); + return respVO; + } + + default void copyTo(List list, Map userMap) { + for (AppBrokerageUserChildSummaryRespVO vo : list) { + Optional.ofNullable(userMap.get(vo.getId())).ifPresent(user -> + vo.setNickname(user.getNickname()).setAvatar(user.getAvatar())); + } + } +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/brokerage/BrokerageWithdrawConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/brokerage/BrokerageWithdrawConvert.java new file mode 100644 index 0000000..ed66039 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/brokerage/BrokerageWithdrawConvert.java @@ -0,0 +1,57 @@ +package com.tashow.cloud.trade.convert.brokerage; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawRespVO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageWithdrawDO; +import cn.iocoder.yudao.module.trade.enums.DictTypeConstants; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 佣金提现 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface BrokerageWithdrawConvert { + + BrokerageWithdrawConvert INSTANCE = Mappers.getMapper(BrokerageWithdrawConvert.class); + + BrokerageWithdrawDO convert(AppBrokerageWithdrawCreateReqVO createReqVO, Long userId, Integer feePrice); + + BrokerageWithdrawRespVO convert(BrokerageWithdrawDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default PageResult convertPage(PageResult pageResult, Map userMap) { + PageResult result = convertPage(pageResult); + for (BrokerageWithdrawRespVO vo : result.getList()) { + vo.setUserNickname(Optional.ofNullable(userMap.get(vo.getUserId())).map(MemberUserRespDTO::getNickname).orElse(null)); + } + return result; + } + + PageResult convertPage02(PageResult pageResult); + + default PageResult convertPage03(PageResult pageResult) { + PageResult result = convertPage02(pageResult); + for (AppBrokerageWithdrawRespVO vo : result.getList()) { + vo.setStatusName(DictFrameworkUtils.getDictDataLabel(DictTypeConstants.BROKERAGE_WITHDRAW_STATUS, vo.getStatus())); + } + return result; + } + + BrokerageWithdrawPageReqVO convert(AppBrokerageWithdrawPageReqVO pageReqVO, Long userId); +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/cart/TradeCartConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/cart/TradeCartConvert.java new file mode 100644 index 0000000..99a9b33 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/cart/TradeCartConvert.java @@ -0,0 +1,53 @@ +package com.tashow.cloud.trade.convert.cart; + +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; +import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum; +import com.tashow.cloud.trade.controller.app.base.sku.AppProductSkuBaseRespVO; +import com.tashow.cloud.trade.controller.app.base.spu.AppProductSpuBaseRespVO; +import com.tashow.cloud.trade.controller.app.cart.vo.AppCartListRespVO; +import com.tashow.cloud.trade.dal.dataobject.cart.CartDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +@Mapper +public interface TradeCartConvert { + + TradeCartConvert INSTANCE = Mappers.getMapper(TradeCartConvert.class); + + default AppCartListRespVO convertList(List carts, + List spus, List skus) { + Map spuMap = convertMap(spus, ProductSpuRespDTO::getId); + Map skuMap = convertMap(skus, ProductSkuRespDTO::getId); + // 遍历,开始转换 + List validList = new ArrayList<>(carts.size()); + List invalidList = new ArrayList<>(); + carts.forEach(cart -> { + AppCartListRespVO.Cart cartVO = new AppCartListRespVO.Cart(); + cartVO.setId(cart.getId()).setCount(cart.getCount()).setSelected(cart.getSelected()); + ProductSpuRespDTO spu = spuMap.get(cart.getSpuId()); + ProductSkuRespDTO sku = skuMap.get(cart.getSkuId()); + cartVO.setSpu(convert(spu)).setSku(convert(sku)); + // 如果 SPU 不存在,或者下架,或者库存不足,说明是无效的 + if (spu == null + || !ProductSpuStatusEnum.isEnable(spu.getStatus()) + || spu.getStock() <= 0) { + cartVO.setSelected(false); // 强制设置成不可选中 + invalidList.add(cartVO); + } else { + // 虽然 SKU 可能也会不存在,但是可以通过购物车重新选择 + validList.add(cartVO); + } + }); + return new AppCartListRespVO().setValidList(validList).setInvalidList(invalidList); + } + AppProductSpuBaseRespVO convert(ProductSpuRespDTO spu); + AppProductSkuBaseRespVO convert(ProductSkuRespDTO sku); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/config/TradeConfigConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/config/TradeConfigConvert.java new file mode 100644 index 0000000..a21589f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/config/TradeConfigConvert.java @@ -0,0 +1,25 @@ +package com.tashow.cloud.trade.convert.config; + +import com.tashow.cloud.trade.controller.admin.config.vo.TradeConfigRespVO; +import com.tashow.cloud.trade.controller.admin.config.vo.TradeConfigSaveReqVO; +import com.tashow.cloud.trade.controller.app.config.vo.AppTradeConfigRespVO; +import com.tashow.cloud.trade.dal.dataobject.config.TradeConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 交易中心配置 Convert + * + * @author owen + */ +@Mapper +public interface TradeConfigConvert { + + TradeConfigConvert INSTANCE = Mappers.getMapper(TradeConfigConvert.class); + + TradeConfigDO convert(TradeConfigSaveReqVO bean); + + TradeConfigRespVO convert(TradeConfigDO bean); + + AppTradeConfigRespVO convert02(TradeConfigDO tradeConfig); +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/delivery/DeliveryExpressConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/delivery/DeliveryExpressConvert.java new file mode 100644 index 0000000..81c10ce --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/delivery/DeliveryExpressConvert.java @@ -0,0 +1,34 @@ +package com.tashow.cloud.trade.convert.delivery; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.express.*; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.*; +import com.tashow.cloud.trade.controller.app.delivery.vo.express.AppDeliveryExpressRespVO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface DeliveryExpressConvert { + + DeliveryExpressConvert INSTANCE = Mappers.getMapper(DeliveryExpressConvert.class); + + DeliveryExpressDO convert(DeliveryExpressCreateReqVO bean); + + DeliveryExpressDO convert(DeliveryExpressUpdateReqVO bean); + + DeliveryExpressRespVO convert(DeliveryExpressDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + List convertList1(List list); + + List convertList03(List list); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/delivery/DeliveryExpressTemplateConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/delivery/DeliveryExpressTemplateConvert.java new file mode 100644 index 0000000..02b20be --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/delivery/DeliveryExpressTemplateConvert.java @@ -0,0 +1,94 @@ +package com.tashow.cloud.trade.convert.delivery; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.expresstemplate.*; +import com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate.*; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateChargeDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateFreeDO; +import com.tashow.cloud.trade.service.delivery.bo.DeliveryExpressTemplateRespBO; +import com.google.common.collect.Maps; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.findFirst; + +@Mapper +public interface DeliveryExpressTemplateConvert { + + DeliveryExpressTemplateConvert INSTANCE = Mappers.getMapper(DeliveryExpressTemplateConvert.class); + + // ========== Template ========== + + DeliveryExpressTemplateDO convert(DeliveryExpressTemplateCreateReqVO bean); + + DeliveryExpressTemplateDO convert(DeliveryExpressTemplateUpdateReqVO bean); + + DeliveryExpressTemplateRespVO convert(DeliveryExpressTemplateDO bean); + + DeliveryExpressTemplateDetailRespVO convert2(DeliveryExpressTemplateDO bean); + + List convertList(List list); + + List convertList1(List list); + + PageResult convertPage(PageResult page); + + default DeliveryExpressTemplateDetailRespVO convert(DeliveryExpressTemplateDO bean, + List chargeList, + List freeList) { + DeliveryExpressTemplateDetailRespVO respVO = convert2(bean); + respVO.setCharges(convertTemplateChargeList(chargeList)); + respVO.setFrees(convertTemplateFreeList(freeList)); + return respVO; + } + + // ========== Template Charge ========== + + DeliveryExpressTemplateChargeDO convertTemplateCharge(Long templateId, Integer chargeMode, DeliveryExpressTemplateChargeBaseVO vo); + + DeliveryExpressTemplateRespBO.Charge convertTemplateCharge(DeliveryExpressTemplateChargeDO bean); + + default List convertTemplateChargeList(Long templateId, Integer chargeMode, List list) { + return CollectionUtils.convertList(list, vo -> convertTemplateCharge(templateId, chargeMode, vo)); + } + + // ========== Template Free ========== + + DeliveryExpressTemplateFreeDO convertTemplateFree(Long templateId, DeliveryExpressTemplateFreeBaseVO vo); + + DeliveryExpressTemplateRespBO.Free convertTemplateFree(DeliveryExpressTemplateFreeDO bean); + + List convertTemplateChargeList(List list); + + List convertTemplateFreeList(List list); + + default List convertTemplateFreeList(Long templateId, List list) { + return CollectionUtils.convertList(list, vo -> convertTemplateFree(templateId, vo)); + } + + default Map convertMap(Integer areaId, List templateList, + List chargeList, + List freeList) { + Map> templateIdChargeMap = convertMultiMap(chargeList, + DeliveryExpressTemplateChargeDO::getTemplateId); + Map> templateIdFreeMap = convertMultiMap(freeList, + DeliveryExpressTemplateFreeDO::getTemplateId); + // 组合运费模板配置 RespBO + Map result = Maps.newHashMapWithExpectedSize(templateList.size()); + templateList.forEach(template -> { + DeliveryExpressTemplateRespBO bo = new DeliveryExpressTemplateRespBO() + .setChargeMode(template.getChargeMode()) + .setCharge(convertTemplateCharge(findFirst(templateIdChargeMap.get(template.getId()), charge -> charge.getAreaIds().contains(areaId)))) + .setFree(convertTemplateFree(findFirst(templateIdFreeMap.get(template.getId()), free -> free.getAreaIds().contains(areaId)))); + result.put(template.getId(), bo); + }); + return result; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/delivery/DeliveryPickUpStoreConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/delivery/DeliveryPickUpStoreConvert.java new file mode 100644 index 0000000..3b6b841 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/delivery/DeliveryPickUpStoreConvert.java @@ -0,0 +1,55 @@ +package com.tashow.cloud.trade.convert.delivery; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreCreateReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreRespVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreSimpleRespVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreUpdateReqVO; +import com.tashow.cloud.trade.controller.app.delivery.vo.pickup.AppDeliveryPickUpStoreRespVO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface DeliveryPickUpStoreConvert { + + DeliveryPickUpStoreConvert INSTANCE = Mappers.getMapper(DeliveryPickUpStoreConvert.class); + + DeliveryPickUpStoreDO convert(DeliveryPickUpStoreCreateReqVO bean); + + DeliveryPickUpStoreDO convert(DeliveryPickUpStoreUpdateReqVO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList1(List list); + @Mapping(source = "areaId", target = "areaName", qualifiedByName = "convertAreaIdToAreaName") + DeliveryPickUpStoreSimpleRespVO convert02(DeliveryPickUpStoreDO bean); + + @Named("convertAreaIdToAreaName") + default String convertAreaIdToAreaName(Integer areaId) { + return AreaUtils.format(areaId); + } + + default List convertList(List list, + Double latitude, Double longitude) { + return CollectionUtils.convertList(list, store -> { + AppDeliveryPickUpStoreRespVO storeVO = convert03(store); + if (latitude != null && longitude != null) { + storeVO.setDistance(NumberUtils.getDistance(latitude, longitude, storeVO.getLatitude(), storeVO.getLongitude())); + } + return storeVO; + }); + } + @Mapping(source = "areaId", target = "areaName", qualifiedByName = "convertAreaIdToAreaName") + AppDeliveryPickUpStoreRespVO convert03(DeliveryPickUpStoreDO bean); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/order/TradeOrderConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/order/TradeOrderConvert.java new file mode 100644 index 0000000..160b0de --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/order/TradeOrderConvert.java @@ -0,0 +1,291 @@ +package com.tashow.cloud.trade.convert.order; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; +import cn.iocoder.yudao.module.pay.enums.DictTypeConstants; +import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO; +import cn.iocoder.yudao.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO; +import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO; +import com.tashow.cloud.trade.controller.admin.base.member.user.MemberUserRespVO; +import com.tashow.cloud.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +import cn.iocoder.yudao.module.trade.controller.admin.order.vo.*; +import com.tashow.cloud.trade.controller.admin.order.vo.*; +import com.tashow.cloud.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import cn.iocoder.yudao.module.trade.controller.app.order.vo.*; +import com.tashow.cloud.trade.controller.app.order.vo.*; +import com.tashow.cloud.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; +import com.tashow.cloud.trade.controller.app.order.vo.item.AppTradeOrderItemRespVO; +import com.tashow.cloud.trade.dal.dataobject.cart.CartDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderLogDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; +import com.tashow.cloud.trade.framework.order.config.TradeOrderProperties; +import com.tashow.cloud.trade.service.brokerage.bo.BrokerageAddReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime; + +@Mapper +public interface TradeOrderConvert { + + TradeOrderConvert INSTANCE = Mappers.getMapper(TradeOrderConvert.class); + + @Mappings({ + @Mapping(target = "id", ignore = true), + @Mapping(source = "userId", target = "userId"), + @Mapping(source = "createReqVO.couponId", target = "couponId"), + @Mapping(target = "remark", ignore = true), + @Mapping(source = "createReqVO.remark", target = "userRemark"), + @Mapping(source = "calculateRespBO.price.totalPrice", target = "totalPrice"), + @Mapping(source = "calculateRespBO.price.discountPrice", target = "discountPrice"), + @Mapping(source = "calculateRespBO.price.deliveryPrice", target = "deliveryPrice"), + @Mapping(source = "calculateRespBO.price.couponPrice", target = "couponPrice"), + @Mapping(source = "calculateRespBO.price.pointPrice", target = "pointPrice"), + @Mapping(source = "calculateRespBO.price.vipPrice", target = "vipPrice"), + @Mapping(source = "calculateRespBO.price.payPrice", target = "payPrice") + }) + TradeOrderDO convert(Long userId, AppTradeOrderCreateReqVO createReqVO, TradePriceCalculateRespBO calculateRespBO); + + TradeOrderRespDTO convert(TradeOrderDO orderDO); + + default List convertList(TradeOrderDO tradeOrderDO, TradePriceCalculateRespBO calculateRespBO) { + return CollectionUtils.convertList(calculateRespBO.getItems(), item -> { + TradeOrderItemDO orderItem = convert(item); + orderItem.setOrderId(tradeOrderDO.getId()); + orderItem.setUserId(tradeOrderDO.getUserId()); + orderItem.setAfterSaleStatus(TradeOrderItemAfterSaleStatusEnum.NONE.getStatus()); + orderItem.setCommentStatus(false); + return orderItem; + }); + } + + TradeOrderItemDO convert(TradePriceCalculateRespBO.OrderItem item); + + default ProductSkuUpdateStockReqDTO convert(List list) { + List items = CollectionUtils.convertList(list, item -> + new ProductSkuUpdateStockReqDTO.Item().setId(item.getSkuId()).setIncrCount(item.getCount())); + return new ProductSkuUpdateStockReqDTO(items); + } + + default ProductSkuUpdateStockReqDTO convertNegative(List list) { + List items = CollectionUtils.convertList(list, item -> + new ProductSkuUpdateStockReqDTO.Item().setId(item.getSkuId()).setIncrCount(-item.getCount())); + return new ProductSkuUpdateStockReqDTO(items); + } + + default PayOrderCreateReqDTO convert(TradeOrderDO order, List orderItems, + TradeOrderProperties orderProperties) { + PayOrderCreateReqDTO createReqDTO = new PayOrderCreateReqDTO() + .setAppKey(orderProperties.getPayAppKey()).setUserIp(order.getUserIp()); + // 商户相关字段 + createReqDTO.setMerchantOrderId(String.valueOf(order.getId())); + String subject = orderItems.get(0).getSpuName(); + subject = StrUtils.maxLength(subject, PayOrderCreateReqDTO.SUBJECT_MAX_LENGTH); // 避免超过 32 位 + createReqDTO.setSubject(subject); + createReqDTO.setBody(subject); // TODO 芋艿:临时写死 + // 订单相关字段 + createReqDTO.setPrice(order.getPayPrice()).setExpireTime(addTime(orderProperties.getPayExpireTime())); + return createReqDTO; + } + + default PageResult convertPage(PageResult pageResult, + List orderItems, + Map memberUserMap) { + Map> orderItemMap = convertMultiMap(orderItems, TradeOrderItemDO::getOrderId); + // 转化 List + List orderVOs = CollectionUtils.convertList(pageResult.getList(), order -> { + List xOrderItems = orderItemMap.get(order.getId()); + TradeOrderPageItemRespVO orderVO = convert(order, xOrderItems); + // 处理收货地址 + orderVO.setReceiverAreaName(AreaUtils.format(order.getReceiverAreaId())); + // 增加用户信息 + orderVO.setUser(convertUser(memberUserMap.get(orderVO.getUserId()))); + // 增加推广人信息 + orderVO.setBrokerageUser(convertUser(memberUserMap.get(orderVO.getBrokerageUserId()))); + return orderVO; + }); + return new PageResult<>(orderVOs, pageResult.getTotal()); + } + + MemberUserRespVO convertUser(MemberUserRespDTO memberUserRespDTO); + + TradeOrderPageItemRespVO convert(TradeOrderDO order, List items); + + ProductPropertyValueDetailRespVO convert(ProductPropertyValueDetailRespDTO bean); + + default TradeOrderDetailRespVO convert(TradeOrderDO order, List orderItems, + List orderLogs, + MemberUserRespDTO user, MemberUserRespDTO brokerageUser) { + TradeOrderDetailRespVO orderVO = convert2(order, orderItems); + // 处理收货地址 + orderVO.setReceiverAreaName(AreaUtils.format(order.getReceiverAreaId())); + // 处理用户信息 + orderVO.setUser(convert(user)); + orderVO.setBrokerageUser(convert(brokerageUser)); + // 处理日志 + orderVO.setLogs(convertList03(orderLogs)); + return orderVO; + } + List convertList03(List orderLogs); + + TradeOrderDetailRespVO convert2(TradeOrderDO order, List items); + + MemberUserRespVO convert(MemberUserRespDTO bean); + + default PageResult convertPage02(PageResult pageResult, + List orderItems) { + Map> orderItemMap = convertMultiMap(orderItems, TradeOrderItemDO::getOrderId); + // 转化 List + List orderVOs = CollectionUtils.convertList(pageResult.getList(), order -> { + List xOrderItems = orderItemMap.get(order.getId()); + return convert02(order, xOrderItems); + }); + return new PageResult<>(orderVOs, pageResult.getTotal()); + } + + AppTradeOrderPageItemRespVO convert02(TradeOrderDO order, List items); + + AppProductPropertyValueDetailRespVO convert02(ProductPropertyValueDetailRespDTO bean); + + default AppTradeOrderDetailRespVO convert02(TradeOrderDO order, List orderItems, + TradeOrderProperties tradeOrderProperties, + DeliveryExpressDO express) { + AppTradeOrderDetailRespVO orderVO = convert3(order, orderItems); + orderVO.setPayExpireTime(order.getCreateTime().plus(tradeOrderProperties.getPayExpireTime())); + if (StrUtil.isNotEmpty(order.getPayChannelCode())) { + orderVO.setPayChannelName(DictFrameworkUtils.getDictDataLabel(DictTypeConstants.CHANNEL_CODE, order.getPayChannelCode())); + } + // 处理收货地址 + orderVO.setReceiverAreaName(AreaUtils.format(order.getReceiverAreaId())); + if (express != null) { + orderVO.setLogisticsId(express.getId()).setLogisticsName(express.getName()); + } + return orderVO; + } + + AppTradeOrderDetailRespVO convert3(TradeOrderDO order, List items); + + AppTradeOrderItemRespVO convert03(TradeOrderItemDO bean); + + @Mappings({ + @Mapping(target = "skuId", source = "tradeOrderItemDO.skuId"), + @Mapping(target = "orderId", source = "tradeOrderItemDO.orderId"), + @Mapping(target = "orderItemId", source = "tradeOrderItemDO.id"), + @Mapping(target = "descriptionScores", source = "createReqVO.descriptionScores"), + @Mapping(target = "benefitScores", source = "createReqVO.benefitScores"), + @Mapping(target = "content", source = "createReqVO.content"), + @Mapping(target = "picUrls", source = "createReqVO.picUrls"), + @Mapping(target = "anonymous", source = "createReqVO.anonymous"), + @Mapping(target = "userId", source = "tradeOrderItemDO.userId") + }) + ProductCommentCreateReqDTO convert04(AppTradeOrderItemCommentCreateReqVO createReqVO, TradeOrderItemDO tradeOrderItemDO); + + TradePriceCalculateReqBO convert(AppTradeOrderSettlementReqVO settlementReqVO); + + default TradePriceCalculateReqBO convert(Long userId, AppTradeOrderSettlementReqVO settlementReqVO, + List cartList) { + TradePriceCalculateReqBO reqBO = new TradePriceCalculateReqBO().setUserId(userId) + .setItems(new ArrayList<>(settlementReqVO.getItems().size())) + .setCouponId(settlementReqVO.getCouponId()).setPointStatus(settlementReqVO.getPointStatus()) + // 物流信息 + .setDeliveryType(settlementReqVO.getDeliveryType()).setAddressId(settlementReqVO.getAddressId()) + .setPickUpStoreId(settlementReqVO.getPickUpStoreId()) + // 各种活动 + .setSeckillActivityId(settlementReqVO.getSeckillActivityId()) + .setBargainRecordId(settlementReqVO.getBargainRecordId()) + .setCombinationActivityId(settlementReqVO.getCombinationActivityId()) + .setCombinationHeadId(settlementReqVO.getCombinationHeadId()) + .setPointActivityId(settlementReqVO.getPointActivityId()); + // 商品项的构建 + Map cartMap = convertMap(cartList, CartDO::getId); + for (AppTradeOrderSettlementReqVO.Item item : settlementReqVO.getItems()) { + // 情况一:skuId + count + if (item.getSkuId() != null) { + reqBO.getItems().add(new TradePriceCalculateReqBO.Item().setSkuId(item.getSkuId()).setCount(item.getCount()) + .setSelected(true)); // true 的原因,下单一定选中 + continue; + } + // 情况二:cartId + CartDO cart = cartMap.get(item.getCartId()); + if (cart == null) { + continue; + } + reqBO.getItems().add(new TradePriceCalculateReqBO.Item().setSkuId(cart.getSkuId()).setCount(cart.getCount()) + .setCartId(item.getCartId()).setSelected(true)); // true 的原因,下单一定选中 + } + return reqBO; + } + + default AppTradeOrderSettlementRespVO convert(TradePriceCalculateRespBO calculate, MemberAddressRespDTO address) { + AppTradeOrderSettlementRespVO respVO = convert0(calculate, address); + if (address != null) { + respVO.getAddress().setAreaName(AreaUtils.format(address.getAreaId())); + } + return respVO; + } + + AppTradeOrderSettlementRespVO convert0(TradePriceCalculateRespBO calculate, MemberAddressRespDTO address); + + List convertList02(List list); + + TradeOrderDO convert(TradeOrderUpdateAddressReqVO reqVO); + + TradeOrderDO convert(TradeOrderUpdatePriceReqVO reqVO); + + TradeOrderDO convert(TradeOrderRemarkReqVO reqVO); + + default BrokerageAddReqBO convert(MemberUserRespDTO user, TradeOrderItemDO item, + ProductSpuRespDTO spu, ProductSkuRespDTO sku) { + BrokerageAddReqBO bo = new BrokerageAddReqBO().setBizId(String.valueOf(item.getId())).setSourceUserId(item.getUserId()) + .setBasePrice(item.getPayPrice()) + .setTitle(StrUtil.format("{}成功购买{}", user.getNickname(), item.getSpuName())) + .setFirstFixedPrice(0).setSecondFixedPrice(0); + if (BooleanUtil.isTrue(spu.getSubCommissionType())) { + bo.setFirstFixedPrice(sku.getFirstBrokeragePrice()).setSecondFixedPrice(sku.getSecondBrokeragePrice()); + } + return bo; + } + + @Named("convertList04") + List convertList04(List list); + + @Mappings({ + @Mapping(target = "activityId", source = "order.combinationActivityId"), + @Mapping(target = "spuId", source = "item.spuId"), + @Mapping(target = "skuId", source = "item.skuId"), + @Mapping(target = "count", source = "item.count"), + @Mapping(target = "orderId", source = "order.id"), + @Mapping(target = "userId", source = "order.userId"), + @Mapping(target = "headId", source = "order.combinationHeadId"), + @Mapping(target = "combinationPrice", source = "item.payPrice"), + }) + CombinationRecordCreateReqDTO convert(TradeOrderDO order, TradeOrderItemDO item); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/order/TradeOrderLogConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/order/TradeOrderLogConvert.java new file mode 100644 index 0000000..dfe7aee --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/convert/order/TradeOrderLogConvert.java @@ -0,0 +1,15 @@ +package com.tashow.cloud.trade.convert.order; + +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderLogDO; +import com.tashow.cloud.trade.service.order.bo.TradeOrderLogCreateReqBO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface TradeOrderLogConvert { + + TradeOrderLogConvert INSTANCE = Mappers.getMapper(TradeOrderLogConvert.class); + + TradeOrderLogDO convert(TradeOrderLogCreateReqBO bean); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/aftersale/AfterSaleDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/aftersale/AfterSaleDO.java new file mode 100644 index 0000000..52bbb34 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/aftersale/AfterSaleDO.java @@ -0,0 +1,203 @@ +package com.tashow.cloud.trade.dal.dataobject.aftersale; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleStatusEnum; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleTypeEnum; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleWayEnum; +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.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 售后订单,用于处理 {@link TradeOrderDO} 交易订单的退款退货流程 + * + * @author 芋道源码 + */ +@TableName(value = "trade_after_sale", autoResultMap = true) +@KeySequence("trade_after_sale_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class AfterSaleDO extends BaseDO { + + /** + * 售后编号,主键自增 + */ + private Long id; + /** + * 售后单号 + * + * 例如说,1146347329394184195 + */ + private String no; + /** + * 退款状态 + * + * 枚举 {@link AfterSaleStatusEnum} + */ + private Integer status; + /** + * 售后方式 + * + * 枚举 {@link AfterSaleWayEnum} + */ + private Integer way; + /** + * 售后类型 + * + * 枚举 {@link AfterSaleTypeEnum} + */ + private Integer type; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + /** + * 申请原因 + * + * type = 退款,对应 trade_after_sale_refund_reason 类型 + * type = 退货退款,对应 trade_after_sale_refund_and_return_reason 类型 + */ + private String applyReason; + /** + * 补充描述 + */ + private String applyDescription; + /** + * 补充凭证图片 + * + * 数组,以逗号分隔 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List applyPicUrls; + + // ========== 交易订单相关 ========== + /** + * 交易订单编号 + * + * 关联 {@link TradeOrderDO#getId()} + */ + private Long orderId; + /** + * 订单流水号 + * + * 冗余 {@link TradeOrderDO#getNo()} + */ + private String orderNo; + /** + * 交易订单项编号 + * + * 关联 {@link TradeOrderItemDO#getId()} + */ + private Long orderItemId; + /** + * 商品 SPU 编号 + * + * 关联 ProductSpuDO 的 id 字段 + * 冗余 {@link TradeOrderItemDO#getSpuId()} + */ + private Long spuId; + /** + * 商品 SPU 名称 + * + * 关联 ProductSkuDO 的 name 字段 + * 冗余 {@link TradeOrderItemDO#getSpuName()} + */ + private String spuName; + /** + * 商品 SKU 编号 + * + * 关联 ProductSkuDO 的编号 + */ + private Long skuId; + /** + * 属性数组,JSON 格式 + * + * 冗余 {@link TradeOrderItemDO#getProperties()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List properties; + /** + * 商品图片 + * + * 冗余 {@link TradeOrderItemDO#getPicUrl()} + */ + private String picUrl; + /** + * 退货商品数量 + */ + private Integer count; + + // ========== 审批相关 ========== + + /** + * 审批时间 + */ + private LocalDateTime auditTime; + /** + * 审批人 + * + * 关联 AdminUserDO 的 id 编号 + */ + private Long auditUserId; + /** + * 审批备注 + * + * 注意,只有审批不通过才会填写 + */ + private String auditReason; + + // ========== 退款相关 ========== + /** + * 退款金额,单位:分。 + */ + private Integer refundPrice; + /** + * 支付退款编号 + * + * 对接 pay-module-biz 支付服务的退款订单编号,即 PayRefundDO 的 id 编号 + */ + private Long payRefundId; + /** + * 退款时间 + */ + private LocalDateTime refundTime; + + // ========== 退货相关 ========== + /** + * 退货物流公司编号 + * + * 关联 LogisticsDO 的 id 编号 + */ + private Long logisticsId; + /** + * 退货物流单号 + */ + private String logisticsNo; + /** + * 退货时间 + */ + private LocalDateTime deliveryTime; + /** + * 收货时间 + */ + private LocalDateTime receiveTime; + /** + * 收货备注 + * + * 注意,只有拒绝收货才会填写 + */ + private String receiveReason; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/aftersale/AfterSaleLogDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/aftersale/AfterSaleLogDO.java new file mode 100644 index 0000000..e52ef02 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/aftersale/AfterSaleLogDO.java @@ -0,0 +1,71 @@ +package com.tashow.cloud.trade.dal.dataobject.aftersale; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleOperateTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 交易售后日志 DO + * + * @author 芋道源码 + */ +@TableName("trade_after_sale_log") +@KeySequence("trade_after_sale_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AfterSaleLogDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + * + * 关联 1:AdminUserDO 的 id 字段 + * 关联 2:MemberUserDO 的 id 字段 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + /** + * 售后编号 + * + * 关联 {@link AfterSaleDO#getId()} + */ + private Long afterSaleId; + /** + * 操作前状态 + */ + private Integer beforeStatus; + /** + * 操作后状态 + */ + private Integer afterStatus; + + /** + * 操作类型 + * + * 枚举 {@link AfterSaleOperateTypeEnum} + */ + private Integer operateType; + /** + * 操作明细 + */ + private String content; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/brokerage/BrokerageRecordDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/brokerage/BrokerageRecordDO.java new file mode 100644 index 0000000..415b9d2 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/brokerage/BrokerageRecordDO.java @@ -0,0 +1,97 @@ +package com.tashow.cloud.trade.dal.dataobject.brokerage; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; +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 + * + * @author owen + */ +@TableName("trade_brokerage_record") +@KeySequence("trade_brokerage_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BrokerageRecordDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Integer id; + /** + * 用户编号 + *

+ * 关联 MemberUserDO.id + */ + private Long userId; + /** + * 业务编号 + */ + private String bizId; + /** + * 业务类型 + *

+ * 枚举 {@link BrokerageRecordBizTypeEnum} + */ + private Integer bizType; + + /** + * 标题 + */ + private String title; + /** + * 说明 + */ + private String description; + + /** + * 金额 + */ + private Integer price; + /** + * 当前总佣金 + */ + private Integer totalPrice; + + /** + * 状态 + *

+ * 枚举 {@link BrokerageRecordStatusEnum} + */ + private Integer status; + + /** + * 冻结时间(天) + */ + private Integer frozenDays; + /** + * 解冻时间 + */ + private LocalDateTime unfreezeTime; + + /** + * 来源用户等级 + *

+ * 被推广用户和 {@link #userId} 的推广层级关系 + */ + private Integer sourceUserLevel; + /** + * 来源用户编号 + *

+ * 关联 MemberUserDO.id 字段,被推广用户的编号 + */ + private Long sourceUserId; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/brokerage/BrokerageUserDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/brokerage/BrokerageUserDO.java new file mode 100644 index 0000000..66c6907 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/brokerage/BrokerageUserDO.java @@ -0,0 +1,62 @@ +package com.tashow.cloud.trade.dal.dataobject.brokerage; + +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; + +/** + * 分销用户 DO + * + * @author owen + */ +@TableName("trade_brokerage_user") +@KeySequence("trade_brokerage_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BrokerageUserDO extends BaseDO { + + /** + * 用户编号 + *

+ * 对应 MemberUserDO 的 id 字段 + */ + @TableId + private Long id; + + /** + * 推广员编号 + *

+ * 关联 MemberUserDO 的 id 字段 + */ + private Long bindUserId; + /** + * 推广员绑定时间 + */ + private LocalDateTime bindUserTime; + + /** + * 是否有分销资格 + */ + private Boolean brokerageEnabled; + /** + * 成为分销员时间 + */ + private LocalDateTime brokerageTime; + + /** + * 可用佣金 + */ + private Integer brokeragePrice; + /** + * 冻结佣金 + */ + private Integer frozenPrice; +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/brokerage/BrokerageWithdrawDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/brokerage/BrokerageWithdrawDO.java new file mode 100644 index 0000000..8e81bc9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/brokerage/BrokerageWithdrawDO.java @@ -0,0 +1,98 @@ +package com.tashow.cloud.trade.dal.dataobject.brokerage; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum; +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 + * + * @author 芋道源码 + */ +@TableName("trade_brokerage_withdraw") +@KeySequence("trade_brokerage_withdraw_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BrokerageWithdrawDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 字段 + */ + private Long userId; + + /** + * 提现金额,单位:分 + */ + private Integer price; + /** + * 提现手续费,单位:分 + */ + private Integer feePrice; + /** + * 当前总佣金,单位:分 + */ + private Integer totalPrice; + /** + * 提现类型 + *

+ * 枚举 {@link BrokerageWithdrawTypeEnum} + */ + private Integer type; + + /** + * 真实姓名 + */ + private String name; + /** + * 账号 + */ + private String accountNo; + /** + * 银行名称 + */ + private String bankName; + /** + * 开户地址 + */ + private String bankAddress; + /** + * 收款码 + */ + private String accountQrCodeUrl; + /** + * 状态 + *

+ * 枚举 {@link BrokerageWithdrawStatusEnum} + */ + private Integer status; + /** + * 审核驳回原因 + */ + private String auditReason; + /** + * 审核时间 + */ + private LocalDateTime auditTime; + /** + * 备注 + */ + private String remark; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/cart/CartDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/cart/CartDO.java new file mode 100644 index 0000000..65da533 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/cart/CartDO.java @@ -0,0 +1,61 @@ +package com.tashow.cloud.trade.dal.dataobject.cart; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 购物车的商品信息 DO + * + * 每个商品,对应一条记录,通过 {@link #spuId} 和 {@link #skuId} 关联 + * + * @author 芋道源码 + */ +@TableName("trade_cart") +@KeySequence("trade_cart_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class CartDO extends BaseDO { + + // ========= 基础字段 BEGIN ========= + + /** + * 编号,唯一自增 + */ + private Long id; + + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + + // ========= 商品信息 ========= + + /** + * 商品 SPU 编号 + * + * 关联 ProductSpuDO 的 id 编号 + */ + private Long spuId; + /** + * 商品 SKU 编号 + * + * 关联 ProductSkuDO 的 id 编号 + */ + private Long skuId; + /** + * 商品购买数量 + */ + private Integer count; + /** + * 是否选中 + */ + private Boolean selected; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/config/TradeConfigDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/config/TradeConfigDO.java new file mode 100644 index 0000000..061407e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/config/TradeConfigDO.java @@ -0,0 +1,118 @@ +package com.tashow.cloud.trade.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.IntegerListTypeHandler; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageBindModeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageEnabledConditionEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * 交易中心配置 DO + * + * @author owen + */ +@TableName(value = "trade_config", autoResultMap = true) +@KeySequence("trade_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TradeConfigDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + + // ========== 售后相关 ========== + + /** + * 售后的退款理由 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List afterSaleRefundReasons; + /** + * 售后的退货理由 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List afterSaleReturnReasons; + + // ========== 配送相关 ========== + + /** + * 是否启用全场包邮 + */ + private Boolean deliveryExpressFreeEnabled; + /** + * 全场包邮的最小金额,单位:分 + */ + private Integer deliveryExpressFreePrice; + + /** + * 是否开启自提 + */ + private Boolean deliveryPickUpEnabled; + + // ========== 分销相关 ========== + + /** + * 是否启用分佣 + */ + private Boolean brokerageEnabled; + /** + * 分佣模式 + *

+ * 枚举 {@link BrokerageEnabledConditionEnum 对应的类} + */ + private Integer brokerageEnabledCondition; + /** + * 分销关系绑定模式 + *

+ * 枚举 {@link BrokerageBindModeEnum 对应的类} + */ + private Integer brokerageBindMode; + /** + * 分销海报图地址数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List brokeragePosterUrls; + /** + * 一级返佣比例 + */ + private Integer brokerageFirstPercent; + /** + * 二级返佣比例 + */ + private Integer brokerageSecondPercent; + /** + * 用户提现最低金额 + */ + private Integer brokerageWithdrawMinPrice; + /** + * 用户提现手续费百分比 + */ + private Integer brokerageWithdrawFeePercent; + /** + * 佣金冻结时间(天) + */ + private Integer brokerageFrozenDays; + /** + * 提现方式 + *

+ * 枚举 {@link BrokerageWithdrawTypeEnum 对应的类} + */ + @TableField(typeHandler = IntegerListTypeHandler.class) + private List brokerageWithdrawTypes; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressDO.java new file mode 100644 index 0000000..0aeaef8 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressDO.java @@ -0,0 +1,60 @@ +package com.tashow.cloud.trade.dal.dataobject.delivery; + +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.Data; + +/** + * 快递公司 DO + * + * @author jason + */ +@TableName(value ="trade_delivery_express") +@KeySequence("trade_delivery_express_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryExpressDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + + /** + * 快递公司 code + */ + private String code; + + /** + * 快递公司名称 + */ + private String name; + + /** + * 快递公司 logo + */ + private String logo; + + /** + * 排序 + */ + private Integer sort; + + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + // TODO 芋艿:c 和结算相关的字段,后续在看 + // partnerId 是否需要月结账号 + // partnerKey 是否需要月结密码 + // net 是否需要取件网店 + // account 账号 + // password 网点名称 + // isShow 是否显示 +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressTemplateChargeDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressTemplateChargeDO.java new file mode 100644 index 0000000..c04f086 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressTemplateChargeDO.java @@ -0,0 +1,67 @@ +package com.tashow.cloud.trade.dal.dataobject.delivery; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.IntegerListTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.List; + +/** + * 快递运费模板计费配置 DO + * + * @author jason + */ +@TableName(value ="trade_delivery_express_template_charge", autoResultMap = true) +@KeySequence("trade_delivery_express_template_charge_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryExpressTemplateChargeDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + + /** + * 配送模板编号 + * + * 关联 {@link DeliveryExpressTemplateDO#getId()} + */ + private Long templateId; + + /** + * 配送区域编号列表 + */ + @TableField(typeHandler = IntegerListTypeHandler.class) + private List areaIds; + + /** + * 配送计费方式 + * + * 冗余 {@link DeliveryExpressTemplateDO#getChargeMode()} + */ + private Integer chargeMode; + + /** + * 首件数量(件数,重量,或体积) + */ + private Double startCount; + /** + * 起步价,单位:分 + */ + private Integer startPrice; + + /** + * 续件数量(件, 重量,或体积) + */ + private Double extraCount; + /** + * 额外价,单位:分 + */ + private Integer extraPrice; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressTemplateDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressTemplateDO.java new file mode 100644 index 0000000..00d2df5 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressTemplateDO.java @@ -0,0 +1,43 @@ +package com.tashow.cloud.trade.dal.dataobject.delivery; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryExpressChargeModeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 快递运费模板 DO + * + * @author jason + */ +@TableName("trade_delivery_express_template") +@KeySequence("trade_delivery_express_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryExpressTemplateDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + + /** + * 模板名称 + */ + private String name; + + /** + * 配送计费方式 + * + * 枚举 {@link DeliveryExpressChargeModeEnum} + */ + private Integer chargeMode; + + /** + * 排序 + */ + private Integer sort; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressTemplateFreeDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressTemplateFreeDO.java new file mode 100644 index 0000000..e666b97 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryExpressTemplateFreeDO.java @@ -0,0 +1,57 @@ +package com.tashow.cloud.trade.dal.dataobject.delivery; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.IntegerListTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.List; + +/** + * 快递运费模板包邮配置 DO + * + * @author jason + */ +@TableName(value ="trade_delivery_express_template_free", autoResultMap = true) +@KeySequence("trade_delivery_express_template_free_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryExpressTemplateFreeDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 配送模板编号 + * + * 关联 {@link DeliveryExpressTemplateDO#getId()} + */ + private Long templateId; + + + /** + * 配送区域编号列表 + */ + @TableField(typeHandler = IntegerListTypeHandler.class) + private List areaIds; + + /** + * 包邮金额,单位:分 + * + * 订单总金额 > 包邮金额时,才免运费 + */ + private Integer freePrice; + + /** + * 包邮件数 + * + * 订单总件数 > 包邮件数时,才免运费 + */ + private Integer freeCount; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryPickUpStoreDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryPickUpStoreDO.java new file mode 100644 index 0000000..fd29a8f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/delivery/DeliveryPickUpStoreDO.java @@ -0,0 +1,98 @@ +package com.tashow.cloud.trade.dal.dataobject.delivery; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalTime; +import java.util.List; + +/** + * 自提门店 DO + * + * @author jason + */ +@TableName(value ="trade_delivery_pick_up_store", autoResultMap = true) +@KeySequence("trade_delivery_pick_up_store_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryPickUpStoreDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 门店名称 + */ + private String name; + + /** + * 门店简介 + */ + private String introduction; + + /** + * 门店手机 + */ + private String phone; + + /** + * 区域编号 + */ + private Integer areaId; + + /** + * 门店详细地址 + */ + private String detailAddress; + + /** + * 门店 logo + */ + private String logo; + + /** + * 营业开始时间 + */ + private LocalTime openingTime; + + /** + * 营业结束时间 + */ + private LocalTime closingTime; + + /** + * 纬度 + */ + private Double latitude; + /** + * 经度 + */ + private Double longitude; + + /** + * 核销员工用户编号数组 + * + * 订单自提核销时,只有对应门店的店员才能核销 + * + * 关联 {@link AdminUserRespDTO#getId()} 管理员编号 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List verifyUserIds; + + /** + * 门店状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/order/TradeOrderDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/order/TradeOrderDO.java new file mode 100644 index 0000000..f81552e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/order/TradeOrderDO.java @@ -0,0 +1,363 @@ +package com.tashow.cloud.trade.dal.dataobject.order; + +import cn.iocoder.yudao.framework.common.enums.TerminalEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageUserDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderCancelTypeEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 交易订单 DO + * + * @author 芋道源码 + */ +@TableName(value = "trade_order", autoResultMap = true) +@KeySequence("trade_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TradeOrderDO extends BaseDO { + + /** + * 发货物流公司编号 - 空(无需发货) + */ + public static final Long LOGISTICS_ID_NULL = 0L; + + // ========== 订单基本信息 ========== + /** + * 订单编号,主键自增 + */ + private Long id; + /** + * 订单流水号 + * + * 例如说,1146347329394184195 + */ + private String no; + /** + * 订单类型 + * + * 枚举 {@link TradeOrderTypeEnum} + */ + private Integer type; + /** + * 订单来源 + * + * 枚举 {@link TerminalEnum} + */ + private Integer terminal; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + /** + * 用户 IP + */ + private String userIp; + /** + * 用户备注 + */ + private String userRemark; + /** + * 订单状态 + * + * 枚举 {@link TradeOrderStatusEnum} + */ + private Integer status; + /** + * 购买的商品数量 + */ + private Integer productCount; + /** + * 订单完成时间 + */ + private LocalDateTime finishTime; + /** + * 订单取消时间 + */ + private LocalDateTime cancelTime; + /** + * 取消类型 + * + * 枚举 {@link TradeOrderCancelTypeEnum} + */ + private Integer cancelType; + /** + * 商家备注 + */ + private String remark; + /** + * 是否评价 + * + * true - 已评价 + * false - 未评价 + */ + private Boolean commentStatus; + + /** + * 推广人编号 + * + * 关联 {@link BrokerageUserDO#getId()} 字段,即 {@link MemberUserRespDTO#getId()} 字段 + */ + private Long brokerageUserId; + + // ========== 价格 + 支付基本信息 ========== + + // 价格文档 - 淘宝:https://open.taobao.com/docV3.htm?docId=108471&docType=1 + // 价格文档 - 京东到家:https://openo2o.jddj.com/api/getApiDetail/182/4d1494c5e7ac4679bfdaaed950c5bc7f.htm + // 价格文档 - 有赞:https://doc.youzanyun.com/detail/API/0/906 + + /** + * 支付订单编号 + * + * 对接 pay-module-biz 支付服务的支付订单编号,即 PayOrderDO 的 id 编号 + */ + private Long payOrderId; + /** + * 是否已支付 + * + * true - 已经支付过 + * false - 没有支付过 + */ + private Boolean payStatus; + /** + * 付款时间 + */ + private LocalDateTime payTime; + /** + * 支付渠道 + * + * 对应 PayChannelEnum 枚举 + */ + private String payChannelCode; + + /** + * 商品原价,单位:分 + * + * totalPrice = {@link TradeOrderItemDO#getPrice()} * {@link TradeOrderItemDO#getCount()} 求和 + * + * 对应 taobao 的 trade.total_fee 字段 + */ + private Integer totalPrice; + /** + * 优惠金额,单位:分 + * + * 对应 taobao 的 order.discount_fee 字段 + */ + private Integer discountPrice; + /** + * 运费金额,单位:分 + */ + private Integer deliveryPrice; + /** + * 订单调价,单位:分 + * + * 正数,加价;负数,减价 + */ + private Integer adjustPrice; + /** + * 应付金额(总),单位:分 + * + * = {@link #totalPrice} + * - {@link #couponPrice} + * - {@link #pointPrice} + * - {@link #discountPrice} + * + {@link #deliveryPrice} + * + {@link #adjustPrice} + * - {@link #vipPrice} + */ + private Integer payPrice; + + // ========== 收件 + 物流基本信息 ========== + /** + * 配送方式 + * + * 枚举 {@link DeliveryTypeEnum} + */ + private Integer deliveryType; + /** + * 发货物流公司编号 + * + * 如果无需发货,则 logisticsId 设置为 0。原因是,不想再添加额外字段 + * + * 关联 {@link DeliveryExpressDO#getId()} + */ + private Long logisticsId; + /** + * 发货物流单号 + * + * 如果无需发货,则 logisticsNo 设置 ""。原因是,不想再添加额外字段 + */ + private String logisticsNo; + /** + * 发货时间 + */ + private LocalDateTime deliveryTime; + + /** + * 收货时间 + */ + private LocalDateTime receiveTime; + /** + * 收件人名称 + */ + private String receiverName; + /** + * 收件人手机 + */ + private String receiverMobile; + /** + * 收件人地区编号 + */ + private Integer receiverAreaId; + /** + * 收件人详细地址 + */ + private String receiverDetailAddress; + + /** + * 自提门店编号 + * + * 关联 {@link DeliveryPickUpStoreDO#getId()} + */ + private Long pickUpStoreId; + /** + * 自提核销码 + */ + private String pickUpVerifyCode; + + // ========== 售后基本信息 ========== + /** + * 售后状态 + * + * 枚举 {@link TradeOrderRefundStatusEnum} + */ + private Integer refundStatus; + /** + * 退款金额,单位:分 + * + * 注意,退款并不会影响 {@link #payPrice} 实际支付金额 + * 也就说,一个订单最终产生多少金额的收入 = payPrice - refundPrice + */ + private Integer refundPrice; + + // ========== 营销基本信息 ========== + /** + * 优惠劵编号 + */ + private Long couponId; + /** + * 优惠劵减免金额,单位:分 + * + * 对应 taobao 的 trade.coupon_fee 字段 + */ + private Integer couponPrice; + /** + * 使用的积分 + */ + private Integer usePoint; + /** + * 积分抵扣的金额,单位:分 + * + * 对应 taobao 的 trade.point_fee 字段 + */ + private Integer pointPrice; + /** + * 赠送的积分 + */ + private Integer givePoint; + /** + * 退还的使用的积分 + */ + private Integer refundPoint; + /** + * VIP 减免金额,单位:分 + */ + private Integer vipPrice; + + /** + * 赠送的优惠劵 + * + * key: 优惠劵模版编号 + * value:对应的优惠券数量 + * + * 目的:用于订单支付后赠送优惠券 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map giveCouponTemplateCounts; + /** + * 赠送的优惠劵编号 + * + * 目的:用于后续取消或者售后订单时,需要扣减赠送 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List giveCouponIds; + + /** + * 秒杀活动编号 + * + * 关联 SeckillActivityDO 的 id 字段 + */ + private Long seckillActivityId; + + /** + * 砍价活动编号 + * + * 关联 BargainActivityDO 的 id 字段 + */ + private Long bargainActivityId; + /** + * 砍价记录编号 + * + * 关联 BargainRecordDO 的 id 字段 + */ + private Long bargainRecordId; + + /** + * 拼团活动编号 + * + * 关联 CombinationActivityDO 的 id 字段 + */ + private Long combinationActivityId; + /** + * 拼团团长编号 + * + * 关联 CombinationRecordDO 的 headId 字段 + */ + private Long combinationHeadId; + /** + * 拼团记录编号 + * + * 关联 CombinationRecordDO 的 id 字段 + */ + private Long combinationRecordId; + + /** + * 积分商城活动的编号 + * + * 关联 PointActivityDO 的 id 字段 + */ + private Long pointActivityId; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/order/TradeOrderItemDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/order/TradeOrderItemDO.java new file mode 100644 index 0000000..6b36292 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/order/TradeOrderItemDO.java @@ -0,0 +1,215 @@ +package com.tashow.cloud.trade.dal.dataobject.order; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleDO; +import com.tashow.cloud.trade.dal.dataobject.cart.CartDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum; +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.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * 交易订单项 DO + * + * @author 芋道源码 + */ +@TableName(value = "trade_order_item", autoResultMap = true) +@KeySequence("trade_order_item_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class TradeOrderItemDO extends BaseDO { + + // ========== 订单项基本信息 ========== + /** + * 编号 + */ + private Long id; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + /** + * 订单编号 + * + * 关联 {@link TradeOrderDO#getId()} + */ + private Long orderId; + /** + * 购物车项编号 + * + * 关联 {@link CartDO#getId()} + */ + private Long cartId; + + // ========== 商品基本信息; 冗余较多字段,减少关联查询 ========== + /** + * 商品 SPU 编号 + * + * 关联 ProductSkuDO 的 spuId 编号 + */ + private Long spuId; + /** + * 商品 SPU 名称 + * + * 冗余 ProductSkuDO 的 spuName 编号 + */ + private String spuName; + /** + * 商品 SKU 编号 + * + * 关联 ProductSkuDO 的 id 编号 + */ + private Long skuId; + /** + * 属性数组,JSON 格式 + * + * 冗余 ProductSkuDO 的 properties 字段 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List properties; + /** + * 商品图片 + */ + private String picUrl; + /** + * 购买数量 + */ + private Integer count; + /** + * 是否评价 + * + * true - 已评价 + * false - 未评价 + */ + private Boolean commentStatus; + + // ========== 价格 + 支付基本信息 ========== + + /** + * 商品原价(单),单位:分 + * + * 对应 ProductSkuDO 的 price 字段 + * 对应 taobao 的 order.price 字段 + */ + private Integer price; + /** + * 优惠金额(总),单位:分 + * + * 对应 taobao 的 order.discount_fee 字段 + */ + private Integer discountPrice; + /** + * 运费金额(总),单位:分 + */ + private Integer deliveryPrice; + /** + * 订单调价(总),单位:分 + * + * 正数,加价;负数,减价 + */ + private Integer adjustPrice; + /** + * 应付金额(总),单位:分 + * + * = {@link #price} * {@link #count} + * - {@link #couponPrice} + * - {@link #pointPrice} + * - {@link #discountPrice} + * + {@link #deliveryPrice} + * + {@link #adjustPrice} + * - {@link #vipPrice} + */ + private Integer payPrice; + + // ========== 营销基本信息 ========== + + /** + * 优惠劵减免金额,单位:分 + * + * 对应 taobao 的 trade.coupon_fee 字段 + */ + private Integer couponPrice; + /** + * 积分抵扣的金额,单位:分 + * + * 对应 taobao 的 trade.point_fee 字段 + */ + private Integer pointPrice; + /** + * 使用的积分 + * + * 目的:用于后续取消或者售后订单时,需要归还赠送 + */ + private Integer usePoint; + /** + * 赠送的积分 + * + * 目的:用于后续取消或者售后订单时,需要扣减赠送 + */ + private Integer givePoint; + /** + * VIP 减免金额,单位:分 + */ + private Integer vipPrice; + + // ========== 售后基本信息 ========== + + /** + * 售后单编号 + * + * 关联 {@link AfterSaleDO#getId()} 字段 + */ + private Long afterSaleId; + /** + * 售后状态 + * + * 枚举 {@link TradeOrderItemAfterSaleStatusEnum} + */ + private Integer afterSaleStatus; + + /** + * 商品属性 + */ + @Data + public static class Property implements Serializable { + + /** + * 属性编号 + * + * 关联 ProductPropertyDO 的 id 编号 + */ + private Long propertyId; + /** + * 属性名字 + * + * 关联 ProductPropertyDO 的 name 字段 + */ + private String propertyName; + + /** + * 属性值编号 + * + * 关联 ProductPropertyValueDO 的 id 编号 + */ + private Long valueId; + /** + * 属性值名字 + * + * 关联 ProductPropertyValueDO 的 name 字段 + */ + private String valueName; + + } + +} + diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/order/TradeOrderLogDO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/order/TradeOrderLogDO.java new file mode 100644 index 0000000..8b8d9c8 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/dataobject/order/TradeOrderLogDO.java @@ -0,0 +1,81 @@ +package com.tashow.cloud.trade.dal.dataobject.order; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderOperateTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 订单日志 DO + * + * @author 陈賝 + */ +@TableName("trade_order_log") +@KeySequence("trade_order_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TradeOrderLogDO extends BaseDO { + + /** + * 用户类型 - 系统 + * + * 例如说:Job 自动过期订单时,通过系统自动操作 + */ + public static final Integer USER_TYPE_SYSTEM = 0; + /** + * 用户编号 - 系统 + */ + public static final Long USER_ID_SYSTEM = 0L; + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + * + * 关联 AdminUserDO 的 id 字段、或者 MemberUserDO 的 id 字段 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + /** + * 订单号 + * + * 关联 {@link TradeOrderDO#getId()} + */ + private Long orderId; + /** + * 操作前状态 + */ + private Integer beforeStatus; + /** + * 操作后状态 + */ + private Integer afterStatus; + + /** + * 操作类型 + * + * {@link TradeOrderOperateTypeEnum} + */ + private Integer operateType; + /** + * 订单日志信息 + */ + private String content; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/aftersale/AfterSaleLogMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/aftersale/AfterSaleLogMapper.java new file mode 100644 index 0000000..9d31711 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/aftersale/AfterSaleLogMapper.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.trade.dal.mysql.aftersale; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleLogDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface AfterSaleLogMapper extends BaseMapperX { + + default List selectListByAfterSaleId(Long afterSaleId) { + return selectList(new LambdaQueryWrapper() + .eq(AfterSaleLogDO::getAfterSaleId, afterSaleId) + .orderByDesc(AfterSaleLogDO::getCreateTime)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/aftersale/AfterSaleMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/aftersale/AfterSaleMapper.java new file mode 100644 index 0000000..d6ed877 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/aftersale/AfterSaleMapper.java @@ -0,0 +1,52 @@ +package com.tashow.cloud.trade.dal.mysql.aftersale; + +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 com.tashow.cloud.trade.controller.admin.aftersale.vo.AfterSalePageReqVO; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; + +@Mapper +public interface AfterSaleMapper extends BaseMapperX { + + default PageResult selectPage(AfterSalePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(AfterSaleDO::getUserId, reqVO.getUserId()) + .likeIfPresent(AfterSaleDO::getNo, reqVO.getNo()) + .eqIfPresent(AfterSaleDO::getStatus, reqVO.getStatus()) + .eqIfPresent(AfterSaleDO::getType, reqVO.getType()) + .eqIfPresent(AfterSaleDO::getWay, reqVO.getWay()) + .likeIfPresent(AfterSaleDO::getOrderNo, reqVO.getOrderNo()) + .likeIfPresent(AfterSaleDO::getSpuName, reqVO.getSpuName()) + .betweenIfPresent(AfterSaleDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(AfterSaleDO::getId)); + } + + default PageResult selectPage(Long userId, PageParam pageParam) { + return selectPage(pageParam, new LambdaQueryWrapperX() + .eqIfPresent(AfterSaleDO::getUserId, userId) + .orderByDesc(AfterSaleDO::getId)); + } + + default int updateByIdAndStatus(Long id, Integer status, AfterSaleDO update) { + return update(update, new LambdaUpdateWrapper() + .eq(AfterSaleDO::getId, id).eq(AfterSaleDO::getStatus, status)); + } + + default AfterSaleDO selectByIdAndUserId(Long id, Long userId) { + return selectOne(AfterSaleDO::getId, id, + AfterSaleDO::getUserId, userId); + } + + default Long selectCountByUserIdAndStatus(Long userId, Collection statuses) { + return selectCount(new LambdaQueryWrapperX() + .eq(AfterSaleDO::getUserId, userId) + .in(AfterSaleDO::getStatus, statuses)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/brokerage/BrokerageRecordMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/brokerage/BrokerageRecordMapper.java new file mode 100644 index 0000000..284ce2f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/brokerage/BrokerageRecordMapper.java @@ -0,0 +1,112 @@ +package com.tashow.cloud.trade.dal.mysql.brokerage; + +import cn.hutool.core.bean.BeanUtil; +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 com.tashow.cloud.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByPriceRespVO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageRecordDO; +import com.tashow.cloud.trade.service.brokerage.bo.UserBrokerageSummaryRespBO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.github.yulichang.toolkit.MPJWrappers; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 佣金记录 Mapper + * + * @author owen + */ +@Mapper +public interface BrokerageRecordMapper extends BaseMapperX { + + default PageResult selectPage(BrokerageRecordPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(BrokerageRecordDO::getUserId, reqVO.getUserId()) + .eqIfPresent(BrokerageRecordDO::getBizType, reqVO.getBizType()) + .eqIfPresent(BrokerageRecordDO::getStatus, reqVO.getStatus()) + .eqIfPresent(BrokerageRecordDO::getSourceUserLevel, reqVO.getSourceUserLevel()) + .betweenIfPresent(BrokerageRecordDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(BrokerageRecordDO::getId)); + } + + default List selectListByStatusAndUnfreezeTimeLt(Integer status, LocalDateTime unfreezeTime) { + return selectList(new LambdaQueryWrapper() + .eq(BrokerageRecordDO::getStatus, status) + .lt(BrokerageRecordDO::getUnfreezeTime, unfreezeTime)); + } + + default int updateByIdAndStatus(Integer id, Integer status, BrokerageRecordDO updateObj) { + return update(updateObj, new LambdaQueryWrapper() + .eq(BrokerageRecordDO::getId, id) + .eq(BrokerageRecordDO::getStatus, status)); + } + + default List selectListByBizTypeAndBizId(Integer bizType, String bizId) { + return selectList(BrokerageRecordDO::getBizType, bizType, + BrokerageRecordDO::getBizId, bizId); + } + + default List selectCountAndSumPriceByUserIdInAndBizTypeAndStatus(Collection userIds, + Integer bizType, + Integer status) { + List> list = selectMaps(MPJWrappers.lambdaJoin(BrokerageRecordDO.class) + .select(BrokerageRecordDO::getUserId) + .selectCount(BrokerageRecordDO::getId, UserBrokerageSummaryRespBO::getCount) + .selectSum(BrokerageRecordDO::getPrice) + .in(BrokerageRecordDO::getUserId, userIds) + .eq(BrokerageRecordDO::getBizType, bizType) + .eq(BrokerageRecordDO::getStatus, status) + .groupBy(BrokerageRecordDO::getUserId)); // 按照 userId 聚合 + return BeanUtil.copyToList(list, UserBrokerageSummaryRespBO.class); + // selectJoinList有BUG,会与租户插件冲突:解析SQL时,发生异常 https://gitee.com/best_handsome/mybatis-plus-join/issues/I84GYW +// return selectJoinList(UserBrokerageSummaryBO.class, MPJWrappers.lambdaJoin(BrokerageRecordDO.class) +// .select(BrokerageRecordDO::getUserId) +// .selectCount(BrokerageRecordDO::getId, UserBrokerageSummaryBO::getCount) +// .selectSum(BrokerageRecordDO::getPrice) +// .in(BrokerageRecordDO::getUserId, userIds) +// .eq(BrokerageRecordDO::getBizType, bizType) +// .eq(BrokerageRecordDO::getStatus, status) +// .groupBy(BrokerageRecordDO::getUserId)); + } + + @Select("SELECT SUM(price) FROM trade_brokerage_record " + + "WHERE user_id = #{userId} AND biz_type = #{bizType} AND status = #{status} " + + "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} AND deleted = FALSE") + Integer selectSummaryPriceByUserIdAndBizTypeAndCreateTimeBetween(@Param("userId") Long userId, + @Param("bizType") Integer bizType, + @Param("status") Integer status, + @Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + // TODO @芋艿:收敛掉 @Select 注解操作,统一成 MyBatis-Plus 的方式,或者 xml + @Select("SELECT user_id AS id, SUM(price) AS brokeragePrice FROM trade_brokerage_record " + + "WHERE biz_type = #{bizType} AND status = #{status} AND deleted = FALSE " + + "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} " + + "GROUP BY user_id " + + "ORDER BY brokeragePrice DESC") + IPage selectSummaryPricePageGroupByUserId(IPage page, + @Param("bizType") Integer bizType, + @Param("status") Integer status, + @Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + @Select("SELECT COUNT(1) FROM trade_brokerage_record " + + "WHERE biz_type = #{bizType} AND status = #{status} AND deleted = FALSE " + + "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} " + + "GROUP BY user_id HAVING SUM(price) > #{brokeragePrice}") + Integer selectCountByPriceGt(@Param("brokeragePrice") Integer brokeragePrice, + @Param("bizType") Integer bizType, + @Param("status") Integer status, + @Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/brokerage/BrokerageUserMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/brokerage/BrokerageUserMapper.java new file mode 100644 index 0000000..ded1c6a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/brokerage/BrokerageUserMapper.java @@ -0,0 +1,167 @@ +package com.tashow.cloud.trade.dal.mysql.brokerage; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.SortingField; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.user.BrokerageUserPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserChildSummaryRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByUserCountRespVO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageUserDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * 分销用户 Mapper + * + * @author owen + */ +@Mapper +public interface BrokerageUserMapper extends BaseMapperX { + + default PageResult selectPage(BrokerageUserPageReqVO reqVO, List ids) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .inIfPresent(BrokerageUserDO::getId, ids) + .eqIfPresent(BrokerageUserDO::getBrokerageEnabled, reqVO.getBrokerageEnabled()) + .betweenIfPresent(BrokerageUserDO::getCreateTime, reqVO.getCreateTime()) + .betweenIfPresent(BrokerageUserDO::getBindUserTime, reqVO.getBindUserTime()) + .orderByDesc(BrokerageUserDO::getId)); + } + + /** + * 更新用户可用佣金(增加) + * + * @param id 用户编号 + * @param incrCount 增加佣金(正数) + */ + default void updatePriceIncr(Long id, Integer incrCount) { + Assert.isTrue(incrCount > 0); + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" brokerage_price = brokerage_price + " + incrCount) + .eq(BrokerageUserDO::getId, id); + update(null, lambdaUpdateWrapper); + } + + /** + * 更新用户可用佣金(减少) + * 注意:理论上佣金可能已经提现,这时会扣出负数,确保平台不会造成损失 + * + * @param id 用户编号 + * @param incrCount 增加佣金(负数) + * @return 更新行数 + */ + default int updatePriceDecr(Long id, Integer incrCount) { + Assert.isTrue(incrCount < 0); + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" brokerage_price = brokerage_price + " + incrCount) // 负数,所以使用 + 号 + .eq(BrokerageUserDO::getId, id); + return update(null, lambdaUpdateWrapper); + } + + /** + * 更新用户冻结佣金(增加) + * + * @param id 用户编号 + * @param incrCount 增加冻结佣金(正数) + */ + default void updateFrozenPriceIncr(Long id, Integer incrCount) { + Assert.isTrue(incrCount > 0); + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" frozen_price = frozen_price + " + incrCount) + .eq(BrokerageUserDO::getId, id); + update(null, lambdaUpdateWrapper); + } + + /** + * 更新用户冻结佣金(减少) + * 注意:理论上冻结佣金可能已经解冻,这时会扣出负数,确保平台不会造成损失 + * + * @param id 用户编号 + * @param incrCount 减少冻结佣金(负数) + */ + default void updateFrozenPriceDecr(Long id, Integer incrCount) { + Assert.isTrue(incrCount < 0); + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" frozen_price = frozen_price + " + incrCount) // 负数,所以使用 + 号 + .eq(BrokerageUserDO::getId, id); + update(null, lambdaUpdateWrapper); + } + + /** + * 更新用户冻结佣金(减少), 更新用户佣金(增加) + * + * @param id 用户编号 + * @param incrCount 减少冻结佣金(负数) + * @return 更新条数 + */ + default int updateFrozenPriceDecrAndPriceIncr(Long id, Integer incrCount) { + Assert.isTrue(incrCount < 0); + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" frozen_price = frozen_price + " + incrCount + // 负数,所以使用 + 号 + ", brokerage_price = brokerage_price + " + -incrCount) // 负数,所以使用 - 号 + .eq(BrokerageUserDO::getId, id) + .ge(BrokerageUserDO::getFrozenPrice, -incrCount); // cas 逻辑 + return update(null, lambdaUpdateWrapper); + } + + default void updateBindUserIdAndBindUserTimeToNull(Long id) { + update(null, new LambdaUpdateWrapper() + .eq(BrokerageUserDO::getId, id) + .set(BrokerageUserDO::getBindUserId, null).set(BrokerageUserDO::getBindUserTime, null)); + } + + default void updateEnabledFalseAndBrokerageTimeToNull(Long id) { + update(null, new LambdaUpdateWrapper() + .eq(BrokerageUserDO::getId, id) + .set(BrokerageUserDO::getBrokerageEnabled, false).set(BrokerageUserDO::getBrokerageTime, null)); + } + + @Select("SELECT bind_user_id AS id, COUNT(1) AS brokerageUserCount FROM trade_brokerage_user " + + "WHERE bind_user_id IS NOT NULL AND deleted = FALSE " + + "AND bind_user_time BETWEEN #{beginTime} AND #{endTime} " + + "GROUP BY bind_user_id " + + "ORDER BY brokerageUserCount DESC") + IPage selectCountPageGroupByBindUserId(Page page, + @Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 下级分销统计(分页) + * + * @param bizType 业务类型 + * @param status 状态 + * @param ids 用户编号列表 + * @param sortingField 排序字段 + * @return 下级分销统计分页列表 + */ + IPage selectSummaryPageByUserId(Page page, + @Param("bizType") Integer bizType, + @Param("status") Integer status, + @Param("ids") Collection ids, + @Param("sortingField") SortingField sortingField); + + /** + * 获得被 bindUserIds 推广的用户编号数组 + * + * @param bindUserIds 推广员编号数组 + * @return 用户编号数组 + */ + default List selectIdListByBindUserIdIn(Collection bindUserIds) { + return Convert.toList(Long.class, + selectObjs(new LambdaQueryWrapperX() + .select(Collections.singletonList(BrokerageUserDO::getId)) // 只查询 id 字段,加速返回速度 + .in(BrokerageUserDO::getBindUserId, bindUserIds))); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/brokerage/BrokerageWithdrawMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/brokerage/BrokerageWithdrawMapper.java new file mode 100644 index 0000000..200a7c9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/brokerage/BrokerageWithdrawMapper.java @@ -0,0 +1,64 @@ +package com.tashow.cloud.trade.dal.mysql.brokerage; + +import cn.hutool.core.bean.BeanUtil; +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 com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageWithdrawDO; +import com.tashow.cloud.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.github.yulichang.wrapper.MPJLambdaWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 佣金提现 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface BrokerageWithdrawMapper extends BaseMapperX { + + default PageResult selectPage(BrokerageWithdrawPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(BrokerageWithdrawDO::getUserId, reqVO.getUserId()) + .eqIfPresent(BrokerageWithdrawDO::getType, reqVO.getType()) + .likeIfPresent(BrokerageWithdrawDO::getName, reqVO.getName()) + .eqIfPresent(BrokerageWithdrawDO::getAccountNo, reqVO.getAccountNo()) + .likeIfPresent(BrokerageWithdrawDO::getBankName, reqVO.getBankName()) + .eqIfPresent(BrokerageWithdrawDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BrokerageWithdrawDO::getCreateTime, reqVO.getCreateTime()) + .orderByAsc(BrokerageWithdrawDO::getStatus).orderByDesc(BrokerageWithdrawDO::getId)); + } + + default int updateByIdAndStatus(Long id, Integer status, BrokerageWithdrawDO updateObj) { + return update(updateObj, new LambdaUpdateWrapper() + .eq(BrokerageWithdrawDO::getId, id) + .eq(BrokerageWithdrawDO::getStatus, status)); + } + + default List selectCountAndSumPriceByUserIdAndStatus(Collection userIds, + Collection status) { + List> list = selectMaps(new MPJLambdaWrapper() + .select(BrokerageWithdrawDO::getUserId) + .selectCount(BrokerageWithdrawDO::getId, BrokerageWithdrawSummaryRespBO::getCount) + .selectSum(BrokerageWithdrawDO::getPrice) + .in(BrokerageWithdrawDO::getUserId, userIds) + .in(BrokerageWithdrawDO::getStatus, status) + .groupBy(BrokerageWithdrawDO::getUserId)); + return BeanUtil.copyToList(list, BrokerageWithdrawSummaryRespBO.class); + // selectJoinList有BUG,会与租户插件冲突:解析SQL时,发生异常 https://gitee.com/best_handsome/mybatis-plus-join/issues/I84GYW +// return selectJoinList(UserWithdrawSummaryBO.class, new MPJLambdaWrapper() +// .select(BrokerageWithdrawDO::getUserId) +// .selectCount(BrokerageWithdrawDO::getId, UserWithdrawSummaryBO::getCount) +// .selectSum(BrokerageWithdrawDO::getPrice) +// .in(BrokerageWithdrawDO::getUserId, userIds) +// .eq(BrokerageWithdrawDO::getStatus, status) +// .groupBy(BrokerageWithdrawDO::getUserId)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/cart/CartMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/cart/CartMapper.java new file mode 100644 index 0000000..4aecbc5 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/cart/CartMapper.java @@ -0,0 +1,62 @@ +package com.tashow.cloud.trade.dal.mysql.cart; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.tashow.cloud.trade.dal.dataobject.cart.CartDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Mapper +public interface CartMapper extends BaseMapperX { + + default CartDO selectByUserIdAndSkuId(Long userId, Long skuId) { + return selectOne(CartDO::getUserId, userId, + CartDO::getSkuId, skuId); + } + + default Integer selectSumByUserId(Long userId) { + // SQL sum 查询 + List> result = selectMaps(new QueryWrapper() + .select("SUM(count) AS sumCount") + .eq("user_id", userId) + .eq("selected", true)); // 只计算选中的 + // 获得数量 + return CollUtil.getFirst(result) != null ? MapUtil.getInt(result.get(0), "sumCount") : 0; + } + + default CartDO selectById(Long id, Long userId) { + return selectOne(CartDO::getId, id, + CartDO::getUserId, userId); + } + + default List selectListByIds(Collection ids, Long userId) { + return selectList(new LambdaQueryWrapper() + .in(CartDO::getId, ids) + .eq(CartDO::getUserId, userId)); + } + + default List selectListByUserId(Long userId) { + return selectList(new LambdaQueryWrapper() + .eq(CartDO::getUserId, userId)); + } + + default List selectListByUserId(Long userId, Set ids) { + return selectList(new LambdaQueryWrapper() + .eq(CartDO::getUserId, userId) + .in(CartDO::getId, ids)); + } + + default void updateByIds(Collection ids, Long userId, CartDO updateObj) { + update(updateObj, new LambdaQueryWrapper() + .in(CartDO::getId, ids) + .eq(CartDO::getUserId, userId)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/config/TradeConfigMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/config/TradeConfigMapper.java new file mode 100644 index 0000000..ed27b62 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/config/TradeConfigMapper.java @@ -0,0 +1,15 @@ +package com.tashow.cloud.trade.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.tashow.cloud.trade.dal.dataobject.config.TradeConfigDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 交易中心配置 Mapper + * + * @author owen + */ +@Mapper +public interface TradeConfigMapper extends BaseMapperX { + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressMapper.java new file mode 100644 index 0000000..6774000 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressMapper.java @@ -0,0 +1,48 @@ +package com.tashow.cloud.trade.dal.mysql.delivery; + +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 com.tashow.cloud.trade.controller.admin.delivery.vo.express.DeliveryExpressExportReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.DeliveryExpressPageReqVO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface DeliveryExpressMapper extends BaseMapperX { + + default PageResult selectPage(DeliveryExpressPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DeliveryExpressDO::getCode, reqVO.getCode()) + .likeIfPresent(DeliveryExpressDO::getName, reqVO.getName()) + .eqIfPresent(DeliveryExpressDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DeliveryExpressDO::getCreateTime, reqVO.getCreateTime()) + .orderByAsc(DeliveryExpressDO::getSort)); + } + + default List selectList(DeliveryExpressExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(DeliveryExpressDO::getCode, reqVO.getCode()) + .likeIfPresent(DeliveryExpressDO::getName, reqVO.getName()) + .eqIfPresent(DeliveryExpressDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DeliveryExpressDO::getCreateTime, reqVO.getCreateTime()) + .orderByAsc(DeliveryExpressDO::getSort)); + } + + default DeliveryExpressDO selectByCode(String code) { + return selectOne(new LambdaQueryWrapper() + .eq(DeliveryExpressDO::getCode, code)); + } + + default List selectListByStatus(Integer status) { + return selectList(DeliveryExpressDO::getStatus, status); + } + +} + + + + diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressTemplateChargeMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressTemplateChargeMapper.java new file mode 100644 index 0000000..8df59fa --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressTemplateChargeMapper.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.trade.dal.mysql.delivery; + + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateChargeDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface DeliveryExpressTemplateChargeMapper extends BaseMapperX { + + default List selectListByTemplateId(Long templateId){ + return selectList(new LambdaQueryWrapper() + .eq(DeliveryExpressTemplateChargeDO::getTemplateId, templateId)); + } + + default int deleteByTemplateId(Long templateId){ + return delete(new LambdaQueryWrapper() + .eq(DeliveryExpressTemplateChargeDO::getTemplateId, templateId)); + } + + default List selectByTemplateIds(Collection templateIds) { + return selectList(DeliveryExpressTemplateChargeDO::getTemplateId, templateIds); + } + +} + + + + diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressTemplateFreeMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressTemplateFreeMapper.java new file mode 100644 index 0000000..96c0c1a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressTemplateFreeMapper.java @@ -0,0 +1,31 @@ +package com.tashow.cloud.trade.dal.mysql.delivery; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateFreeDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface DeliveryExpressTemplateFreeMapper extends BaseMapperX { + + default List selectListByTemplateId(Long templateId) { + return selectList(new LambdaQueryWrapper() + .eq(DeliveryExpressTemplateFreeDO::getTemplateId, templateId)); + } + + default int deleteByTemplateId(Long templateId) { + return delete(new LambdaQueryWrapper() + .eq(DeliveryExpressTemplateFreeDO::getTemplateId, templateId)); + } + + default List selectListByTemplateIds(Collection templateIds) { + return selectList(DeliveryExpressTemplateFreeDO::getTemplateId, templateIds); + } +} + + + + diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressTemplateMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressTemplateMapper.java new file mode 100644 index 0000000..a2e8a74 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryExpressTemplateMapper.java @@ -0,0 +1,26 @@ +package com.tashow.cloud.trade.dal.mysql.delivery; + + +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 com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplatePageReqVO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DeliveryExpressTemplateMapper extends BaseMapperX { + + default PageResult selectPage(DeliveryExpressTemplatePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DeliveryExpressTemplateDO::getName, reqVO.getName()) + .eqIfPresent(DeliveryExpressTemplateDO::getChargeMode, reqVO.getChargeMode()) + .betweenIfPresent(DeliveryExpressTemplateDO::getCreateTime, reqVO.getCreateTime()) + .orderByAsc(DeliveryExpressTemplateDO::getSort)); + } + + default DeliveryExpressTemplateDO selectByName(String name) { + return selectOne(DeliveryExpressTemplateDO::getName,name); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryPickUpStoreMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryPickUpStoreMapper.java new file mode 100644 index 0000000..409250a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/delivery/DeliveryPickUpStoreMapper.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.trade.dal.mysql.delivery; + +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 com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStorePageReqVO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface DeliveryPickUpStoreMapper extends BaseMapperX { + + default PageResult selectPage(DeliveryPickUpStorePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DeliveryPickUpStoreDO::getName, reqVO.getName()) + .eqIfPresent(DeliveryPickUpStoreDO::getPhone, reqVO.getPhone()) + .eqIfPresent(DeliveryPickUpStoreDO::getAreaId, reqVO.getAreaId()) + .eqIfPresent(DeliveryPickUpStoreDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DeliveryPickUpStoreDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(DeliveryPickUpStoreDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(DeliveryPickUpStoreDO::getStatus, status); + } + +} + + + + diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/order/TradeOrderItemMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/order/TradeOrderItemMapper.java new file mode 100644 index 0000000..e9926d3 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/order/TradeOrderItemMapper.java @@ -0,0 +1,56 @@ +package com.tashow.cloud.trade.dal.mysql.order; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Mapper +public interface TradeOrderItemMapper extends BaseMapperX { + + default int updateAfterSaleStatus(Long id, Integer oldAfterSaleStatus, Integer newAfterSaleStatus, + Long afterSaleId) { + return update(new TradeOrderItemDO().setAfterSaleStatus(newAfterSaleStatus).setAfterSaleId(afterSaleId), + new LambdaUpdateWrapper<>(new TradeOrderItemDO().setId(id).setAfterSaleStatus(oldAfterSaleStatus))); + } + + default List selectListByOrderId(Long orderId) { + return selectList(TradeOrderItemDO::getOrderId, orderId); + } + + default List selectListByOrderId(Collection orderIds) { + return selectList(TradeOrderItemDO::getOrderId, orderIds); + } + + default TradeOrderItemDO selectByIdAndUserId(Long orderItemId, Long loginUserId) { + return selectOne(new LambdaQueryWrapperX() + .eq(TradeOrderItemDO::getId, orderItemId) + .eq(TradeOrderItemDO::getUserId, loginUserId)); + } + + default List selectListByOrderIdAndCommentStatus(Long orderId, Boolean commentStatus) { + return selectList(new LambdaQueryWrapperX() + .eq(TradeOrderItemDO::getOrderId, orderId) + .eq(TradeOrderItemDO::getCommentStatus, commentStatus)); + } + + default int selectProductSumByOrderId(@Param("orderIds") Set orderIds) { + // SQL sum 查询 + List> result = selectMaps(new QueryWrapper() + .select("SUM(count) AS sumCount") + .in("order_id", orderIds)); // 只计算选中的 + // 获得数量 + return CollUtil.getFirst(result) != null ? MapUtil.getInt(result.get(0), "sumCount") : 0; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/order/TradeOrderLogMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/order/TradeOrderLogMapper.java new file mode 100644 index 0000000..2328179 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/order/TradeOrderLogMapper.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.trade.dal.mysql.order; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderLogDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface TradeOrderLogMapper extends BaseMapperX { + + default List selectListByOrderId(Long orderId) { + return selectList(new LambdaQueryWrapper() + .eq(TradeOrderLogDO::getOrderId, orderId) + .orderByDesc(TradeOrderLogDO::getCreateTime)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/order/TradeOrderMapper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/order/TradeOrderMapper.java new file mode 100644 index 0000000..674da2b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/mysql/order/TradeOrderMapper.java @@ -0,0 +1,140 @@ +package com.tashow.cloud.trade.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.framework.mybatis.core.query.MPJLambdaWrapperX; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderPageReqVO; +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeOrderPageReqVO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Mapper +public interface TradeOrderMapper extends BaseMapperX { + + default int updateByIdAndStatus(Long id, Integer status, TradeOrderDO update) { + return update(update, new LambdaUpdateWrapper() + .eq(TradeOrderDO::getId, id).eq(TradeOrderDO::getStatus, status)); + } + + default TradeOrderDO selectByIdAndUserId(Long id, Long userId) { + return selectOne(TradeOrderDO::getId, id, TradeOrderDO::getUserId, userId); + } + + default PageResult selectPage(TradeOrderPageReqVO reqVO, Set userIds) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TradeOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(TradeOrderDO::getUserId, reqVO.getUserId()) + .eqIfPresent(TradeOrderDO::getDeliveryType, reqVO.getDeliveryType()) + .inIfPresent(TradeOrderDO::getUserId, userIds) + .eqIfPresent(TradeOrderDO::getType, reqVO.getType()) + .eqIfPresent(TradeOrderDO::getStatus, reqVO.getStatus()) + .eqIfPresent(TradeOrderDO::getPayChannelCode, reqVO.getPayChannelCode()) + .eqIfPresent(TradeOrderDO::getTerminal, reqVO.getTerminal()) + .eqIfPresent(TradeOrderDO::getLogisticsId, reqVO.getLogisticsId()) + .inIfPresent(TradeOrderDO::getPickUpStoreId, reqVO.getPickUpStoreIds()) + .likeIfPresent(TradeOrderDO::getPickUpVerifyCode, reqVO.getPickUpVerifyCode()) + .betweenIfPresent(TradeOrderDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(TradeOrderDO::getId)); + } + + // TODO @疯狂:如果用 map 返回,要不这里直接用 TradeOrderSummaryRespVO 返回?也算合理,就当 sql 查询出这么个玩意~~ + default List> selectOrderSummaryGroupByRefundStatus(TradeOrderPageReqVO reqVO, Set userIds) { + return selectMaps(new MPJLambdaWrapperX() + .selectAs(TradeOrderDO::getRefundStatus, TradeOrderDO::getRefundStatus) // 售后状态 + .selectCount(TradeOrderDO::getId, "count") // 售后状态对应的数量 + .selectSum(TradeOrderDO::getPayPrice, "price") // 售后状态对应的支付金额 + .likeIfPresent(TradeOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(TradeOrderDO::getUserId, reqVO.getUserId()) + .eqIfPresent(TradeOrderDO::getDeliveryType, reqVO.getDeliveryType()) + .inIfPresent(TradeOrderDO::getUserId, userIds) + .eqIfPresent(TradeOrderDO::getType, reqVO.getType()) + .eqIfPresent(TradeOrderDO::getStatus, reqVO.getStatus()) + .eqIfPresent(TradeOrderDO::getPayChannelCode, reqVO.getPayChannelCode()) + .eqIfPresent(TradeOrderDO::getTerminal, reqVO.getTerminal()) + .eqIfPresent(TradeOrderDO::getLogisticsId, reqVO.getLogisticsId()) + .inIfPresent(TradeOrderDO::getPickUpStoreId, reqVO.getPickUpStoreIds()) + .likeIfPresent(TradeOrderDO::getPickUpVerifyCode, reqVO.getPickUpVerifyCode()) + .betweenIfPresent(TradeOrderDO::getCreateTime, reqVO.getCreateTime()) + .groupBy(TradeOrderDO::getRefundStatus)); // 按售后状态分组 + } + + default PageResult selectPage(AppTradeOrderPageReqVO reqVO, Long userId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eq(TradeOrderDO::getUserId, userId) + .eqIfPresent(TradeOrderDO::getStatus, reqVO.getStatus()) + .eqIfPresent(TradeOrderDO::getCommentStatus, reqVO.getCommentStatus()) + .orderByDesc(TradeOrderDO::getId)); // TODO 芋艿:未来不同的 status,不同的排序 + } + + default Long selectCountByUserIdAndStatus(Long userId, Integer status, Boolean commentStatus) { + return selectCount(new LambdaQueryWrapperX() + .eq(TradeOrderDO::getUserId, userId) + .eqIfPresent(TradeOrderDO::getStatus, status) + .eqIfPresent(TradeOrderDO::getCommentStatus, commentStatus)); + } + + default TradeOrderDO selectOrderByIdAndUserId(Long orderId, Long loginUserId) { + return selectOne(new LambdaQueryWrapperX() + .eq(TradeOrderDO::getId, orderId) + .eq(TradeOrderDO::getUserId, loginUserId)); + } + + default List selectListByStatusAndCreateTimeLt(Integer status, LocalDateTime createTime) { + return selectList(new LambdaUpdateWrapper() + .eq(TradeOrderDO::getStatus, status) + .lt(TradeOrderDO::getCreateTime, createTime)); + } + + default List selectListByStatusAndDeliveryTimeLt(Integer status, LocalDateTime deliveryTime) { + return selectList(new LambdaUpdateWrapper() + .eq(TradeOrderDO::getStatus, status) + .lt(TradeOrderDO::getDeliveryTime, deliveryTime)); + } + + default List selectListByStatusAndReceiveTimeLt(Integer status, LocalDateTime receive, + Boolean commentStatus) { + return selectList(new LambdaUpdateWrapper() + .eq(TradeOrderDO::getStatus, status) + .lt(TradeOrderDO::getReceiveTime, receive) + .eq(TradeOrderDO::getCommentStatus, commentStatus)); + } + + default List selectListByUserIdAndActivityId(Long userId, Long activityId, TradeOrderTypeEnum type) { + LambdaQueryWrapperX queryWrapperX = new LambdaQueryWrapperX<>(); + queryWrapperX.eq(TradeOrderDO::getUserId, userId); + if (TradeOrderTypeEnum.isSeckill(type.getType())) { + queryWrapperX.eq(TradeOrderDO::getSeckillActivityId, activityId); + } + if (TradeOrderTypeEnum.isBargain(type.getType())) { + queryWrapperX.eq(TradeOrderDO::getBargainActivityId, activityId); + } + if (TradeOrderTypeEnum.isCombination(type.getType())) { + queryWrapperX.eq(TradeOrderDO::getCombinationActivityId, activityId); + } + if (TradeOrderTypeEnum.isPoint(type.getType())) { + queryWrapperX.eq(TradeOrderDO::getPointActivityId, activityId); + } + return selectList(queryWrapperX); + } + + default TradeOrderDO selectOneByPickUpVerifyCode(String pickUpVerifyCode) { + return selectOne(TradeOrderDO::getPickUpVerifyCode, pickUpVerifyCode); + } + + default TradeOrderDO selectByUserIdAndCombinationActivityIdAndStatus(Long userId, Long combinationActivityId, Integer status) { + return selectOne(new LambdaQueryWrapperX() + .eq(TradeOrderDO::getUserId, userId) + .eq(TradeOrderDO::getStatus, status) + .eq(TradeOrderDO::getCombinationActivityId, combinationActivityId) + ); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/redis/RedisKeyConstants.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/redis/RedisKeyConstants.java new file mode 100644 index 0000000..6876753 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/redis/RedisKeyConstants.java @@ -0,0 +1,26 @@ +package com.tashow.cloud.trade.dal.redis; + +/** + * 交易 Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 交易序号的缓存 + * + * KEY 格式:trade_no:{prefix} + * VALUE 数据格式:编号自增 + */ + String TRADE_NO = "trade_no:"; + + /** + * 交易序号的缓存 + * + * KEY 格式:express_track:{code-logisticsNo-receiverMobile} + * VALUE 数据格式 String, 物流信息集合 + */ + String EXPRESS_TRACK = "express_track"; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/redis/no/TradeNoRedisDAO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/redis/no/TradeNoRedisDAO.java new file mode 100644 index 0000000..08f61d7 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/dal/redis/no/TradeNoRedisDAO.java @@ -0,0 +1,44 @@ +package com.tashow.cloud.trade.dal.redis.no; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import com.tashow.cloud.trade.dal.redis.RedisKeyConstants; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import jakarta.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * 订单序号的 Redis DAO + * + * @author HUIHUI + */ +@Repository +public class TradeNoRedisDAO { + + public static final String TRADE_ORDER_NO_PREFIX = "o"; + + public static final String AFTER_SALE_NO_PREFIX = "r"; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 生成序号 + * + * @param prefix 前缀 + * @return 序号 + */ + public String generate(String prefix) { + // 递增序号 + String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN); + String key = RedisKeyConstants.TRADE_NO + noPrefix; + Long no = stringRedisTemplate.opsForValue().increment(key); + // 设置过期时间 + stringRedisTemplate.expire(key, Duration.ofMinutes(1L)); + return noPrefix + no; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/config/AfterSaleLogConfiguration.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/config/AfterSaleLogConfiguration.java new file mode 100644 index 0000000..12c3961 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/config/AfterSaleLogConfiguration.java @@ -0,0 +1,22 @@ +package com.tashow.cloud.trade.framework.aftersale.config; + +import com.tashow.cloud.trade.framework.aftersale.core.aop.AfterSaleLogAspect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +// TODO @chenchen:改成 aftersale 好点哈; +/** + * trade 模块的 afterSaleLog 组件的 Configuration + * + * @author 陈賝 + * @since 2023/6/18 11:09 + */ +@Configuration(proxyBeanMethods = false) +public class AfterSaleLogConfiguration { + + @Bean + public AfterSaleLogAspect afterSaleLogAspect() { + return new AfterSaleLogAspect(); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/core/annotations/AfterSaleLog.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/core/annotations/AfterSaleLog.java new file mode 100644 index 0000000..fafa151 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/core/annotations/AfterSaleLog.java @@ -0,0 +1,27 @@ +package com.tashow.cloud.trade.framework.aftersale.core.annotations; + +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleOperateTypeEnum; +import com.tashow.cloud.trade.framework.aftersale.core.aop.AfterSaleLogAspect; + +import java.lang.annotation.*; + +/** + * 售后日志的注解 + * + * 写在方法上时,会自动记录售后日志 + * + * @author 陈賝 + * @since 2023/6/8 17:04 + * @see AfterSaleLogAspect + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface AfterSaleLog { + + /** + * 操作类型 + */ + AfterSaleOperateTypeEnum operateType(); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/core/aop/AfterSaleLogAspect.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/core/aop/AfterSaleLogAspect.java new file mode 100644 index 0000000..fb67a3f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/core/aop/AfterSaleLogAspect.java @@ -0,0 +1,133 @@ +package com.tashow.cloud.trade.framework.aftersale.core.aop; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderLogDO; +import com.tashow.cloud.trade.framework.aftersale.core.annotations.AfterSaleLog; +import com.tashow.cloud.trade.service.aftersale.AfterSaleLogService; +import com.tashow.cloud.trade.service.aftersale.bo.AfterSaleLogCreateReqBO; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; + +import jakarta.annotation.Resource; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static java.util.Collections.emptyMap; + +/** + * 售后订单的操作记录的 AOP 切面 + * + * @author 陈賝 + * @since 2023/6/13 13:54 + */ +@Slf4j +@Aspect +public class AfterSaleLogAspect { + + /** + * 用户编号 + * + * 目前的使用场景:支付回调时,需要强制设置下用户编号 + */ + private static final ThreadLocal USER_ID = new ThreadLocal<>(); + /** + * 用户类型 + */ + private static final ThreadLocal USER_TYPE = new ThreadLocal<>(); + /** + * 订单编号 + */ + private static final ThreadLocal AFTER_SALE_ID = new ThreadLocal<>(); + /** + * 操作前的状态 + */ + private static final ThreadLocal BEFORE_STATUS = new ThreadLocal<>(); + /** + * 操作后的状态 + */ + private static final ThreadLocal AFTER_STATUS = new ThreadLocal<>(); + /** + * 拓展参数 Map,用于格式化操作内容 + */ + private static final ThreadLocal> EXTS = new ThreadLocal<>(); + + @Resource + private AfterSaleLogService afterSaleLogService; + + @AfterReturning(pointcut = "@annotation(afterSaleLog)") + public void doAfterReturning(JoinPoint joinPoint, AfterSaleLog afterSaleLog) { + try { + // 1.1 操作用户 + Integer userType = getUserType(); + Long userId = getUserId(); + // 1.2 售后信息 + Long afterSaleId = AFTER_SALE_ID.get(); + if (afterSaleId == null) { // 如果未设置,只有注解,说明不需要记录日志 + return; + } + Integer beforeStatus = BEFORE_STATUS.get(); + Integer afterStatus = AFTER_STATUS.get(); + Map exts = ObjectUtil.defaultIfNull(EXTS.get(), emptyMap()); + String content = StrUtil.format(afterSaleLog.operateType().getContent(), exts); + + // 2. 记录日志 + AfterSaleLogCreateReqBO createBO = new AfterSaleLogCreateReqBO() + .setUserId(userId).setUserType(userType) + .setAfterSaleId(afterSaleId).setBeforeStatus(beforeStatus).setAfterStatus(afterStatus) + .setOperateType(afterSaleLog.operateType().getType()).setContent(content); + afterSaleLogService.createAfterSaleLog(createBO); + } catch (Exception exception) { + log.error("[doAfterReturning][afterSaleLog({}) 日志记录错误]", toJsonString(afterSaleLog), exception); + } finally { + clear(); + } + } + + /** + * 获得用户类型 + * + * 如果没有,则约定为 {@link TradeOrderLogDO#getUserType()} 系统 + * + * @return 用户类型 + */ + private static Integer getUserType() { + return ObjectUtil.defaultIfNull(WebFrameworkUtils.getLoginUserType(), TradeOrderLogDO.USER_TYPE_SYSTEM); + } + + /** + * 获得用户编号 + * + * 如果没有,则约定为 {@link TradeOrderLogDO#getUserId()} 系统 + * + * @return 用户类型 + */ + private static Long getUserId() { + return ObjectUtil.defaultIfNull(WebFrameworkUtils.getLoginUserId(), TradeOrderLogDO.USER_ID_SYSTEM); + } + + public static void setAfterSale(Long id, Integer beforeStatus, Integer afterStatus, Map exts) { + AFTER_SALE_ID.set(id); + BEFORE_STATUS.set(beforeStatus); + AFTER_STATUS.set(afterStatus); + EXTS.set(exts); + } + + public static void setUserInfo(Long userId, Integer userType) { + USER_ID.set(userId); + USER_TYPE.set(userType); + } + + private static void clear() { + USER_ID.remove(); + USER_TYPE.remove(); + AFTER_SALE_ID.remove(); + BEFORE_STATUS.remove(); + AFTER_STATUS.remove(); + EXTS.remove(); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/core/utils/AfterSaleLogUtils.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/core/utils/AfterSaleLogUtils.java new file mode 100644 index 0000000..4e4d83c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/aftersale/core/utils/AfterSaleLogUtils.java @@ -0,0 +1,25 @@ +package com.tashow.cloud.trade.framework.aftersale.core.utils; + + +import com.tashow.cloud.trade.framework.aftersale.core.aop.AfterSaleLogAspect; + +import java.util.Map; + +/** + * 操作日志工具类 + * 目前主要的作用,是提供给业务代码,记录操作明细和拓展字段 + * + * @author 芋道源码 + */ +public class AfterSaleLogUtils { + + public static void setAfterSaleInfo(Long id, Integer beforeStatus, Integer afterStatus) { + setAfterSaleInfo(id, beforeStatus, afterStatus, null); + } + + public static void setAfterSaleInfo(Long id, Integer beforeStatus, Integer afterStatus, + Map exts) { + AfterSaleLogAspect.setAfterSale(id, beforeStatus, afterStatus, exts); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/config/ExpressClientConfig.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/config/ExpressClientConfig.java new file mode 100644 index 0000000..35fe77b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/config/ExpressClientConfig.java @@ -0,0 +1,32 @@ +package com.tashow.cloud.trade.framework.delivery.config; + +import com.tashow.cloud.trade.framework.delivery.core.client.ExpressClient; +import com.tashow.cloud.trade.framework.delivery.core.client.ExpressClientFactory; +import com.tashow.cloud.trade.framework.delivery.core.client.impl.ExpressClientFactoryImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * 快递客户端端配置类: + * + * 1. 快递客户端工厂 {@link ExpressClientFactory} + * 2. 默认的快递客户端实现 {@link ExpressClient} + * + * @author jason + */ +@Configuration(proxyBeanMethods = false) +public class ExpressClientConfig { + + @Bean + public ExpressClientFactory expressClientFactory(TradeExpressProperties tradeExpressProperties, + RestTemplate restTemplate) { + return new ExpressClientFactoryImpl(tradeExpressProperties, restTemplate); + } + + @Bean + public ExpressClient defaultExpressClient(ExpressClientFactory expressClientFactory) { + return expressClientFactory.getDefaultExpressClient(); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/config/TradeExpressProperties.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/config/TradeExpressProperties.java new file mode 100644 index 0000000..8251209 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/config/TradeExpressProperties.java @@ -0,0 +1,89 @@ +package com.tashow.cloud.trade.framework.delivery.config; + +import com.tashow.cloud.trade.framework.delivery.core.enums.ExpressClientEnum; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; + +// TODO @芋艿:未来要不要放数据库中?考虑 saas 多租户时,不同租户使用不同的配置? +/** + * 交易运费快递的配置项 + * + * @author jason + */ +@Component +@ConfigurationProperties(prefix = "yudao.trade.express") +@Data +@Validated +public class TradeExpressProperties { + + /** + * 快递客户端 + * + * 默认不提供,需要提醒用户配置一个快递服务商。 + */ + private ExpressClientEnum client = ExpressClientEnum.NOT_PROVIDE; + + /** + * 快递鸟配置 + */ + @Valid + private KdNiaoConfig kdNiao; + /** + * 快递 100 配置 + */ + @Valid + private Kd100Config kd100; + + /** + * 快递鸟配置项目 + */ + @Data + public static class KdNiaoConfig { + + /** + * 快递鸟用户 ID + */ + @NotEmpty(message = "快递鸟用户 ID 配置项不能为空") + private String businessId; + /** + * 快递鸟 API Key + */ + @NotEmpty(message = "快递鸟 Api Key 配置项不能为空") + private String apiKey; + + /** + * 接口指令 + * + * 1. 1002:免费版(只能查询申通、圆通快递) + * 2. 8001:付费版 + */ + @NotEmpty(message = "RequestType 配置项不能为空") + private String requestType = "1002"; + + } + + /** + * 快递 100 配置项 + */ + @Data + public static class Kd100Config { + + /** + * 快递 100 授权码 + */ + @NotEmpty(message = "快递 100 授权码配置项不能为空") + private String customer; + /** + * 快递 100 授权 key + */ + @NotEmpty(message = "快递 100 授权 Key 配置项不能为空") + private String key; + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/ExpressClient.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/ExpressClient.java new file mode 100644 index 0000000..63c343e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/ExpressClient.java @@ -0,0 +1,23 @@ +package com.tashow.cloud.trade.framework.delivery.core.client; + +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; + +import java.util.List; + +/** + * 快递客户端接口 + * + * @author jason + */ +public interface ExpressClient { + + /** + * 快递实时查询 + * + * @param reqDTO 查询请求参数 + */ + // TODO @jason:返回字段可以参考 https://doc.youzanyun.com/detail/API/0/5 响应的 data + List getExpressTrackList(ExpressTrackQueryReqDTO reqDTO); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/ExpressClientFactory.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/ExpressClientFactory.java new file mode 100644 index 0000000..33398bb --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/ExpressClientFactory.java @@ -0,0 +1,24 @@ +package com.tashow.cloud.trade.framework.delivery.core.client; + +import com.tashow.cloud.trade.framework.delivery.core.enums.ExpressClientEnum; + +/** + * 快递客户端工厂接口:用于创建和缓存快递客户端 + * + * @author jason + */ +public interface ExpressClientFactory { + + /** + * 获取默认的快递客户端 + */ + ExpressClient getDefaultExpressClient(); + + /** + * 通过枚举获取快递客户端,如果不存在,就创建一个对应快递客户端 + * + * @param clientEnum 快递客户端枚举 + */ + ExpressClient getOrCreateExpressClient(ExpressClientEnum clientEnum); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/convert/ExpressQueryConvert.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/convert/ExpressQueryConvert.java new file mode 100644 index 0000000..3e65fdd --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/convert/ExpressQueryConvert.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.convert; + +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.kd100.Kd100ExpressQueryReqDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.kd100.Kd100ExpressQueryRespDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.kdniao.KdNiaoExpressQueryReqDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.kdniao.KdNiaoExpressQueryRespDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface ExpressQueryConvert { + + ExpressQueryConvert INSTANCE = Mappers.getMapper(ExpressQueryConvert.class); + + List convertList(List list); + @Mapping(source = "acceptTime", target = "time") + @Mapping(source = "acceptStation", target = "content") + ExpressTrackRespDTO convert(KdNiaoExpressQueryRespDTO.ExpressTrack track); + + List convertList2(List list); + @Mapping(source = "context", target = "content") + ExpressTrackRespDTO convert(Kd100ExpressQueryRespDTO.ExpressTrack track); + + KdNiaoExpressQueryReqDTO convert(ExpressTrackQueryReqDTO dto); + + Kd100ExpressQueryReqDTO convert2(ExpressTrackQueryReqDTO dto); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java new file mode 100644 index 0000000..6f3f871 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java @@ -0,0 +1,36 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.dto; + +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import lombok.Data; + +/** + * 快递轨迹的查询 Req DTO + * + * @author jason + */ +@Data +public class ExpressTrackQueryReqDTO { + + /** + * 快递公司编码 + *

+ * 对应 {@link DeliveryExpressDO#getCode()} + */ + private String expressCode; + + /** + * 发货快递单号 + */ + private String logisticsNo; + + /** + * 收、寄件人的电话号码 + */ + private String phone; + + /** + * 自定义名称(顺丰专用) + */ + private String customerName; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/ExpressTrackRespDTO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/ExpressTrackRespDTO.java new file mode 100644 index 0000000..53f09d7 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/ExpressTrackRespDTO.java @@ -0,0 +1,25 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 快递查询的轨迹 Resp DTO + * + * @author jason + */ +@Data +public class ExpressTrackRespDTO { + + /** + * 发生时间 + */ + private LocalDateTime time; + + /** + * 快递状态 + */ + private String content; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryReqDTO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryReqDTO.java new file mode 100644 index 0000000..5d5592a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryReqDTO.java @@ -0,0 +1,33 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.dto.kd100; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 快递 100 快递查询 Req DTO + * + * @author jason + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Kd100ExpressQueryReqDTO { + + /** + * 快递公司编码 + */ + @JsonProperty("com") + private String expressCode; + + /** + * 快递单号 + */ + @JsonProperty("num") + private String logisticsNo; + + /** + * 收、寄件人的电话号码 + */ + private String phone; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java new file mode 100644 index 0000000..54cc4ec --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java @@ -0,0 +1,71 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.dto.kd100; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 快递 100 实时快递查询 Resp DTO + * + * 参见 快递 100 文档 + * + * @author jason + */ +@Data +public class Kd100ExpressQueryRespDTO { + + /** + * 快递公司编码 + */ + @JsonProperty("com") + private String expressCompanyCode; + /** + * 快递单号 + */ + @JsonProperty("nu") + private String logisticsNo; + /** + * 快递单当前状态 + */ + private String state; + + /** + * 查询结果 + * + * 失败返回 "false" + */ + private String result; + /** + * 查询结果失败时的错误信息 + */ + private String message; + + /** + * 轨迹数组 + */ + @JsonProperty("data") + private List tracks; + + @Data + public static class ExpressTrack { + + /** + * 轨迹发生时间 + */ + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime time; + + /** + * 轨迹描述 + */ + private String context; + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java new file mode 100644 index 0000000..e8ecce3 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java @@ -0,0 +1,38 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.dto.kdniao; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 快递鸟快递查询 Req DTO + * + * @author jason + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class KdNiaoExpressQueryReqDTO { + + /** + * 快递公司编码 + */ + @JsonProperty("ShipperCode") + private String expressCode; + /** + * 快递单号 + */ + @JsonProperty("LogisticCode") + private String logisticsNo; + /** + * 订单编号 + */ + @JsonProperty("OrderCode") + private String orderNo; + + /** + * 自定义名称(顺丰专用) + */ + @JsonProperty("CustomerName") + private String customerName; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryRespDTO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryRespDTO.java new file mode 100644 index 0000000..edbabc6 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryRespDTO.java @@ -0,0 +1,99 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.dto.kdniao; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 快递鸟快递查询 Resp DTO + * + * 参见 快递鸟接口文档 + * + * @author jason + */ +@Data +public class KdNiaoExpressQueryRespDTO { + + /** + * 快递公司编码 + */ + @JsonProperty("ShipperCode") + private String shipperCode; + + /** + * 快递单号 + */ + @JsonProperty("LogisticCode") + private String logisticsNo; + + /** + * 订单编号 + */ + @JsonProperty("OrderCode") + private String orderNo; + + /** + * 用户 ID + */ + @JsonProperty("EBusinessID") + private String businessId; + + /** + * 普通物流状态 + * + * 0 - 暂无轨迹信息 + * 1 - 已揽收 + * 2 - 在途中 + * 3 - 签收 + * 4 - 问题件 + * 5 - 转寄 + * 6 - 清关 + */ + @JsonProperty("State") + private String state; + + /** + * 成功与否 + */ + @JsonProperty("Success") + private Boolean success; + /** + * 失败原因 + */ + @JsonProperty("Reason") + private String reason; + + /** + * 轨迹数组 + */ + @JsonProperty("Traces") + private List tracks; + + @Data + public static class ExpressTrack { + + /** + * 发生时间 + */ + @JsonProperty("AcceptTime") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private LocalDateTime acceptTime; + + /** + * 轨迹描述 + */ + @JsonProperty("AcceptStation") + private String acceptStation; + + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/ExpressClientFactoryImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/ExpressClientFactoryImpl.java new file mode 100644 index 0000000..1e4f9f9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/ExpressClientFactoryImpl.java @@ -0,0 +1,54 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.impl; + +import cn.hutool.core.lang.Assert; +import com.tashow.cloud.trade.framework.delivery.config.TradeExpressProperties; +import com.tashow.cloud.trade.framework.delivery.core.client.ExpressClient; +import com.tashow.cloud.trade.framework.delivery.core.client.ExpressClientFactory; +import com.tashow.cloud.trade.framework.delivery.core.client.impl.kd100.Kd100ExpressClient; +import com.tashow.cloud.trade.framework.delivery.core.client.impl.kdniao.KdNiaoExpressClient; +import com.tashow.cloud.trade.framework.delivery.core.enums.ExpressClientEnum; +import lombok.AllArgsConstructor; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 快递客户端工厂实现类 + * + * @author jason + */ +@AllArgsConstructor +public class ExpressClientFactoryImpl implements ExpressClientFactory { + + private final Map clientMap = new ConcurrentHashMap<>(8); + + private final TradeExpressProperties tradeExpressProperties; + private final RestTemplate restTemplate; + + @Override + public ExpressClient getDefaultExpressClient() { + ExpressClient defaultClient = getOrCreateExpressClient(tradeExpressProperties.getClient()); + Assert.notNull("默认的快递客户端不能为空"); + return defaultClient; + } + + @Override + public ExpressClient getOrCreateExpressClient(ExpressClientEnum clientEnum) { + return clientMap.computeIfAbsent(clientEnum, + client -> createExpressClient(client, tradeExpressProperties)); + } + + private ExpressClient createExpressClient(ExpressClientEnum queryProviderEnum, + TradeExpressProperties tradeExpressProperties) { + switch (queryProviderEnum) { + case NOT_PROVIDE: + return new NoProvideExpressClient(); + case KD_NIAO: + return new KdNiaoExpressClient(restTemplate, tradeExpressProperties.getKdNiao()); + case KD_100: + return new Kd100ExpressClient(restTemplate, tradeExpressProperties.getKd100()); + } + return null; + } +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/NoProvideExpressClient.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/NoProvideExpressClient.java new file mode 100644 index 0000000..0bad6b0 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/NoProvideExpressClient.java @@ -0,0 +1,24 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.impl; + +import com.tashow.cloud.trade.framework.delivery.core.client.ExpressClient; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.EXPRESS_CLIENT_NOT_PROVIDE; + +/** + * 未实现的快递客户端,用来提醒用户需要接入快递服务商, + * + * @author jason + */ +public class NoProvideExpressClient implements ExpressClient { + + @Override + public List getExpressTrackList(ExpressTrackQueryReqDTO reqDTO) { + throw exception(EXPRESS_CLIENT_NOT_PROVIDE); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/kd100/Kd100ExpressClient.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/kd100/Kd100ExpressClient.java new file mode 100644 index 0000000..6b75399 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/kd100/Kd100ExpressClient.java @@ -0,0 +1,107 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.impl.kd100; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.tashow.cloud.trade.framework.delivery.config.TradeExpressProperties; +import com.tashow.cloud.trade.framework.delivery.core.client.ExpressClient; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.kd100.Kd100ExpressQueryReqDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.kd100.Kd100ExpressQueryRespDTO; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.EXPRESS_API_QUERY_ERROR; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.EXPRESS_API_QUERY_FAILED; +import static com.tashow.cloud.trade.framework.delivery.core.client.convert.ExpressQueryConvert.INSTANCE; + +/** + * 快递 100 客户端 + * + * @author jason + */ +@Slf4j +@AllArgsConstructor +public class Kd100ExpressClient implements ExpressClient { + + private static final String REAL_TIME_QUERY_URL = "https://poll.kuaidi100.com/poll/query.do"; + + private final RestTemplate restTemplate; + private final TradeExpressProperties.Kd100Config config; + + /** + * 查询快递轨迹 + * + * @see 接口文档 + * + * @param reqDTO 查询请求参数 + * @return 快递轨迹 + */ + @Override + public List getExpressTrackList(ExpressTrackQueryReqDTO reqDTO) { + // 发起请求 + Kd100ExpressQueryReqDTO requestDTO = INSTANCE.convert2(reqDTO) + .setExpressCode(reqDTO.getExpressCode().toLowerCase()); + Kd100ExpressQueryRespDTO respDTO = httpRequest(REAL_TIME_QUERY_URL, requestDTO, + Kd100ExpressQueryRespDTO.class); + + // 处理结果 + if (Objects.equals("false", respDTO.getResult())) { + throw exception(EXPRESS_API_QUERY_FAILED, respDTO.getMessage()); + } + if (CollUtil.isEmpty(respDTO.getTracks())) { + return Collections.emptyList(); + } + return INSTANCE.convertList2(respDTO.getTracks()); + } + + /** + * 快递 100 API 请求 + * + * @param url 请求 url + * @param req 对应请求的请求参数 + * @param respClass 对应请求的响应 class + * @param 每个请求的请求结构 Req DTO + * @param 每个请求的响应结构 Resp DTO + */ + private Resp httpRequest(String url, Req req, Class respClass) { + // 请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // 请求体 + String param = JsonUtils.toJsonString(req); + String sign = generateReqSign(param, config.getKey(), config.getCustomer()); // 签名 + MultiValueMap requestBody = new LinkedMultiValueMap<>(); + requestBody.add("customer", config.getCustomer()); + requestBody.add("sign", sign); + requestBody.add("param", param); + log.debug("[httpRequest][请求参数({})]", requestBody); + + // 发送请求 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + log.debug("[httpRequest][的响应结果({})]", responseEntity); + // 处理响应 + if (!responseEntity.getStatusCode().is2xxSuccessful()) { + throw exception(EXPRESS_API_QUERY_ERROR); + } + return JsonUtils.parseObject(responseEntity.getBody(), respClass); + } + + private String generateReqSign(String param, String key, String customer) { + String plainText = String.format("%s%s%s", param, key, customer); + return HexUtil.encodeHexStr(DigestUtil.md5(plainText), false); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java new file mode 100644 index 0000000..1a19cb2 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java @@ -0,0 +1,127 @@ +package com.tashow.cloud.trade.framework.delivery.core.client.impl.kdniao; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.net.URLEncodeUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.tashow.cloud.trade.framework.delivery.config.TradeExpressProperties; +import com.tashow.cloud.trade.framework.delivery.core.client.ExpressClient; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.kdniao.KdNiaoExpressQueryReqDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.kdniao.KdNiaoExpressQueryRespDTO; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.EXPRESS_API_QUERY_FAILED; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.EXPRESS_API_QUERY_ERROR; +import static com.tashow.cloud.trade.framework.delivery.core.client.convert.ExpressQueryConvert.INSTANCE; + +/** + * 快递鸟客户端 + * + * @author jason + */ +@Slf4j +@AllArgsConstructor +public class KdNiaoExpressClient implements ExpressClient { + + private static final String REAL_TIME_QUERY_URL = "https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx"; + + private final RestTemplate restTemplate; + private final TradeExpressProperties.KdNiaoConfig config; + + /** + * 查询快递轨迹【免费版】 + * + * 仅支持 3 家:申通快递、圆通速递、百世快递 + * + * @see 接口文档 + * + * @param reqDTO 查询请求参数 + * @return 快递轨迹 + */ + @Override + public List getExpressTrackList(ExpressTrackQueryReqDTO reqDTO) { + // 发起请求 + KdNiaoExpressQueryReqDTO requestDTO = INSTANCE.convert(reqDTO) + .setExpressCode(reqDTO.getExpressCode().toUpperCase()); + if (ObjUtil.equal(requestDTO.getExpressCode(), "SF") + && StrUtil.isBlank(reqDTO.getCustomerName()) + && StrUtil.length(reqDTO.getPhone()) >= 4) { + requestDTO.setCustomerName(StrUtil.subSufByLength(reqDTO.getPhone(), 4)); + } + KdNiaoExpressQueryRespDTO respDTO = httpRequest(REAL_TIME_QUERY_URL, config.getRequestType(), + requestDTO, KdNiaoExpressQueryRespDTO.class); + + // 处理结果 + if (respDTO == null || !respDTO.getSuccess()) { + throw exception(EXPRESS_API_QUERY_FAILED, respDTO == null ? "" : respDTO.getReason()); + } + if (CollUtil.isEmpty(respDTO.getTracks())) { + return Collections.emptyList(); + } + return INSTANCE.convertList(respDTO.getTracks()); + } + + /** + * 快递鸟 API 请求 + * + * @param url 请求 url + * @param requestType 对应的请求指令 (快递鸟的 RequestType) + * @param req 对应请求的请求参数 + * @param respClass 对应请求的响应 class + * @param 每个请求的请求结构 Req DTO + * @param 每个请求的响应结构 Resp DTO + */ + private Resp httpRequest(String url, String requestType, Req req, Class respClass) { + // 请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // 请求体 + String reqData = JsonUtils.toJsonString(req); + String dataSign = generateDataSign(reqData, config.getApiKey()); + MultiValueMap requestBody = new LinkedMultiValueMap<>(); + requestBody.add("RequestData", reqData); + requestBody.add("DataType", "2"); + requestBody.add("EBusinessID", config.getBusinessId()); + requestBody.add("DataSign", dataSign); + requestBody.add("RequestType", requestType); + log.debug("[httpRequest][RequestType({}) 的请求参数({})]", requestType, requestBody); + + // 发送请求 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + log.debug("[httpRequest][RequestType({}) 的响应结果({})", requestType, responseEntity); + // 处理响应 + if (!responseEntity.getStatusCode().is2xxSuccessful()) { + throw exception(EXPRESS_API_QUERY_ERROR); + } + return JsonUtils.parseObject(responseEntity.getBody(), respClass); + } + + /** + * 快递鸟生成请求签名 + * + * 参见 签名说明 + * + * @param reqData 请求实体 + * @param apiKey api Key + */ + private String generateDataSign(String reqData, String apiKey) { + String plainText = String.format("%s%s", reqData, apiKey); + return URLEncodeUtil.encode(Base64.encode(DigestUtil.md5Hex(plainText))); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/enums/ExpressClientEnum.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/enums/ExpressClientEnum.java new file mode 100644 index 0000000..d505136 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/delivery/core/enums/ExpressClientEnum.java @@ -0,0 +1,28 @@ +package com.tashow.cloud.trade.framework.delivery.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 快递客户端枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum ExpressClientEnum { + + NOT_PROVIDE("not-provide","未提供"), + KD_NIAO("kd-niao", "快递鸟"), + KD_100("kd-100", "快递100"); + + /** + * 快递服务商唯一编码 + */ + private final String code; + /** + * 快递服务商名称 + */ + private final String name; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/config/TradeOrderConfig.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/config/TradeOrderConfig.java new file mode 100644 index 0000000..6559535 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/config/TradeOrderConfig.java @@ -0,0 +1,13 @@ +package com.tashow.cloud.trade.framework.order.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author LeeYan9 + * @since 2022-09-15 + */ +@Configuration +@EnableConfigurationProperties(TradeOrderProperties.class) +public class TradeOrderConfig { +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/config/TradeOrderProperties.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/config/TradeOrderProperties.java new file mode 100644 index 0000000..243380c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/config/TradeOrderProperties.java @@ -0,0 +1,51 @@ +package com.tashow.cloud.trade.framework.order.config; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotNull; + +import java.time.Duration; + +/** + * 交易订单的配置项 + * + * @author LeeYan9 + * @since 2022-09-15 + */ +@ConfigurationProperties(prefix = "yudao.trade.order") +@Data +@Validated +public class TradeOrderProperties { + + private static final String PAY_APP_KEY_DEFAULT = "mall"; + + /** + * 支付应用标识 + * + * 在 pay 模块的 [支付管理 -> 应用信息] 里添加 + */ + @NotEmpty(message = "Pay 应用标识不能为空") + private String payAppKey = PAY_APP_KEY_DEFAULT; + + /** + * 支付超时时间 + */ + @NotNull(message = "支付超时时间不能为空") + private Duration payExpireTime; + + /** + * 收货超时时间 + */ + @NotNull(message = "收货超时时间不能为空") + private Duration receiveExpireTime; + + /** + * 评论超时时间 + */ + @NotNull(message = "评论超时时间不能为空") + private Duration commentExpireTime; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/core/annotations/TradeOrderLog.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/core/annotations/TradeOrderLog.java new file mode 100644 index 0000000..0cb0f8a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/core/annotations/TradeOrderLog.java @@ -0,0 +1,31 @@ +package com.tashow.cloud.trade.framework.order.core.annotations; + +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderOperateTypeEnum; +import com.tashow.cloud.trade.framework.order.core.aop.TradeOrderLogAspect; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; + +/** + * 交易订单的操作日志 AOP 注解 + * + * @author 陈賝 + * @since 2023/7/6 15:37 + * @see TradeOrderLogAspect + */ +@Target({METHOD, ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TradeOrderLog { + + /** + * 操作类型 + */ + TradeOrderOperateTypeEnum operateType(); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/core/aop/TradeOrderLogAspect.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/core/aop/TradeOrderLogAspect.java new file mode 100644 index 0000000..106e935 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/core/aop/TradeOrderLogAspect.java @@ -0,0 +1,136 @@ +package com.tashow.cloud.trade.framework.order.core.aop; + + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderLogDO; +import com.tashow.cloud.trade.framework.order.core.annotations.TradeOrderLog; +import com.tashow.cloud.trade.service.order.TradeOrderLogService; +import com.tashow.cloud.trade.service.order.bo.TradeOrderLogCreateReqBO; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static java.util.Collections.emptyMap; + +/** + * 交易订单的操作日志的记录 AOP 切面 + * + * @author 陈賝 + * @since 2023/6/13 13:54 + */ +@Component +@Aspect +@Slf4j +public class TradeOrderLogAspect { + + /** + * 用户编号 + * + * 目前的使用场景:支付回调时,需要强制设置下用户编号 + */ + private static final ThreadLocal USER_ID = new ThreadLocal<>(); + /** + * 用户类型 + */ + private static final ThreadLocal USER_TYPE = new ThreadLocal<>(); + /** + * 订单编号 + */ + private static final ThreadLocal ORDER_ID = new ThreadLocal<>(); + /** + * 操作前的状态 + */ + private static final ThreadLocal BEFORE_STATUS = new ThreadLocal<>(); + /** + * 操作后的状态 + */ + private static final ThreadLocal AFTER_STATUS = new ThreadLocal<>(); + /** + * 拓展参数 Map,用于格式化操作内容 + */ + private static final ThreadLocal> EXTS = new ThreadLocal<>(); + + @Resource + private TradeOrderLogService orderLogService; + + @AfterReturning("@annotation(orderLog)") + public void doAfterReturning(JoinPoint joinPoint, TradeOrderLog orderLog) { + try { + // 1.1 操作用户 + Integer userType = getUserType(); + Long userId = getUserId(); + // 1.2 订单信息 + Long orderId = ORDER_ID.get(); + if (orderId == null) { // 如果未设置,只有注解,说明不需要记录日志 + return; + } + Integer beforeStatus = BEFORE_STATUS.get(); + Integer afterStatus = AFTER_STATUS.get(); + Map exts = ObjectUtil.defaultIfNull(EXTS.get(), emptyMap()); + String content = StrUtil.format(orderLog.operateType().getContent(), exts); + + // 2. 记录日志 + TradeOrderLogCreateReqBO createBO = new TradeOrderLogCreateReqBO() + .setUserId(userId).setUserType(userType) + .setOrderId(orderId).setBeforeStatus(beforeStatus).setAfterStatus(afterStatus) + .setOperateType(orderLog.operateType().getType()).setContent(content); + orderLogService.createOrderLog(createBO); + } catch (Exception ex) { + log.error("[doAfterReturning][orderLog({}) 订单日志错误]", toJsonString(orderLog), ex); + } finally { + clear(); + } + } + + /** + * 获得用户类型 + * + * 如果没有,则约定为 {@link TradeOrderLogDO#getUserType()} 系统 + * + * @return 用户类型 + */ + private static Integer getUserType() { + return ObjectUtil.defaultIfNull(WebFrameworkUtils.getLoginUserType(), TradeOrderLogDO.USER_TYPE_SYSTEM); + } + + /** + * 获得用户编号 + * + * 如果没有,则约定为 {@link TradeOrderLogDO#getUserId()} 系统 + * + * @return 用户类型 + */ + private static Long getUserId() { + return ObjectUtil.defaultIfNull(WebFrameworkUtils.getLoginUserId(), TradeOrderLogDO.USER_ID_SYSTEM); + } + + public static void setOrderInfo(Long id, Integer beforeStatus, Integer afterStatus, Map exts) { + ORDER_ID.set(id); + BEFORE_STATUS.set(beforeStatus); + AFTER_STATUS.set(afterStatus); + EXTS.set(exts); + } + + public static void setUserInfo(Long userId, Integer userType) { + USER_ID.set(userId); + USER_TYPE.set(userType); + } + + private static void clear() { + USER_ID.remove(); + USER_TYPE.remove(); + ORDER_ID.remove(); + BEFORE_STATUS.remove(); + AFTER_STATUS.remove(); + EXTS.remove(); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/core/utils/TradeOrderLogUtils.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/core/utils/TradeOrderLogUtils.java new file mode 100644 index 0000000..c5c90a8 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/order/core/utils/TradeOrderLogUtils.java @@ -0,0 +1,27 @@ +package com.tashow.cloud.trade.framework.order.core.utils; + +import com.tashow.cloud.trade.framework.order.core.aop.TradeOrderLogAspect; + +import java.util.Map; + +/** + * 交易订单的操作日志 Utils + * + * @author 芋道源码 + */ +public class TradeOrderLogUtils { + + public static void setOrderInfo(Long id, Integer beforeStatus, Integer afterStatus) { + TradeOrderLogAspect.setOrderInfo(id, beforeStatus, afterStatus, null); + } + + public static void setOrderInfo(Long id, Integer beforeStatus, Integer afterStatus, + Map exts) { + TradeOrderLogAspect.setOrderInfo(id, beforeStatus, afterStatus, exts); + } + + public static void setUserInfo(Long userId, Integer userType) { + TradeOrderLogAspect.setUserInfo(userId, userType); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/package-info.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/package-info.java new file mode 100644 index 0000000..c1fb394 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 trade 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.tashow.cloud.trade.framework; diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/rpc/config/RpcConfiguration.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/rpc/config/RpcConfiguration.java new file mode 100644 index 0000000..45a201d --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/rpc/config/RpcConfiguration.java @@ -0,0 +1,41 @@ +package com.tashow.cloud.trade.framework.rpc.config; + +import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; +import cn.iocoder.yudao.module.member.api.config.MemberConfigApi; +import cn.iocoder.yudao.module.member.api.level.MemberLevelApi; +import cn.iocoder.yudao.module.member.api.point.MemberPointApi; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; +import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi; +import cn.iocoder.yudao.module.pay.api.transfer.PayTransferApi; +import cn.iocoder.yudao.module.pay.api.wallet.PayWalletApi; +import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi; +import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi; +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; +import cn.iocoder.yudao.module.promotion.api.bargain.BargainActivityApi; +import cn.iocoder.yudao.module.promotion.api.bargain.BargainRecordApi; +import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi; +import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; +import cn.iocoder.yudao.module.promotion.api.discount.DiscountActivityApi; +import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi; +import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; +import cn.iocoder.yudao.module.promotion.api.seckill.SeckillActivityApi; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.social.SocialClientApi; +import cn.iocoder.yudao.module.system.api.social.SocialUserApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableFeignClients(clients = { + BargainActivityApi.class, BargainRecordApi.class, CombinationRecordApi.class, + CouponApi.class, DiscountActivityApi.class, RewardActivityApi.class, SeckillActivityApi.class, PointActivityApi.class, + MemberUserApi.class, MemberPointApi.class, MemberLevelApi.class, MemberAddressApi.class, MemberConfigApi.class, + ProductSpuApi.class, ProductSkuApi.class, ProductCommentApi.class, ProductCategoryApi.class, + PayOrderApi.class, PayRefundApi.class, PayTransferApi.class, PayWalletApi.class, + AdminUserApi.class, NotifyMessageSendApi.class, SocialClientApi.class, SocialUserApi.class +}) +public class RpcConfiguration { +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/rpc/package-info.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/rpc/package-info.java new file mode 100644 index 0000000..a2ded57 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/rpc/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.tashow.cloud.trade.framework.rpc; diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/security/config/SecurityConfiguration.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/security/config/SecurityConfiguration.java new file mode 100644 index 0000000..05e19b5 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,39 @@ +package com.tashow.cloud.trade.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import cn.iocoder.yudao.module.trade.enums.ApiConstants; +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.AuthorizeHttpRequestsConfigurer; + +/** + * Trade 模块的 Security 配置 + */ +@Configuration("tradeSecurityConfiguration") +public class SecurityConfiguration { + + @Bean("tradeAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + // Swagger 接口文档 + registry.requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers("/webjars/**").permitAll() + .requestMatchers("/swagger-ui").permitAll() + .requestMatchers("/swagger-ui/**").permitAll(); + // Spring Boot Actuator 的安全配置 + registry.requestMatchers("/actuator").anonymous() + .requestMatchers("/actuator/**").anonymous(); + // Druid 监控 + registry.requestMatchers("/druid/**").anonymous(); + // RPC 服务的安全配置 + registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); + } + + }; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/security/core/package-info.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/security/core/package-info.java new file mode 100644 index 0000000..5aa553c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.tashow.cloud.trade.framework.security.core; diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/package-info.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/package-info.java new file mode 100644 index 0000000..bffd270 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/package-info.java @@ -0,0 +1,8 @@ +/** + * trade 模块,product 模块,主要实现商品相关功能 + * 例如:品牌、商品分类、spu、sku等功能。 + * + * 1. Controller URL:以 /product/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 product_ 开头,方便在数据库中区分 + */ +package com.tashow.cloud.trade; diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleLogService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleLogService.java new file mode 100644 index 0000000..2e957e7 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleLogService.java @@ -0,0 +1,34 @@ +package com.tashow.cloud.trade.service.aftersale; + + +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleLogDO; +import com.tashow.cloud.trade.service.aftersale.bo.AfterSaleLogCreateReqBO; + +import java.util.List; + +/** + * 交易售后日志 Service 接口 + * + * @author 陈賝 + * @since 2023/6/12 14:18 + */ +public interface AfterSaleLogService { + + /** + * 创建售后日志 + * + * @param createReqBO 日志记录 + * @author 陈賝 + * @since 2023/6/12 14:18 + */ + void createAfterSaleLog(AfterSaleLogCreateReqBO createReqBO); + + /** + * 获取售后日志 + * + * @param afterSaleId 售后编号 + * @return 售后日志 + */ + List getAfterSaleLogList(Long afterSaleId); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleLogServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleLogServiceImpl.java new file mode 100644 index 0000000..70edc5d --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleLogServiceImpl.java @@ -0,0 +1,36 @@ +package com.tashow.cloud.trade.service.aftersale; + +import com.tashow.cloud.trade.convert.aftersale.AfterSaleLogConvert; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleLogDO; +import com.tashow.cloud.trade.dal.mysql.aftersale.AfterSaleLogMapper; +import com.tashow.cloud.trade.service.aftersale.bo.AfterSaleLogCreateReqBO; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.Resource; +import java.util.List; + +/** + * 交易售后日志 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class AfterSaleLogServiceImpl implements AfterSaleLogService { + + @Resource + private AfterSaleLogMapper afterSaleLogMapper; + + @Override + public void createAfterSaleLog(AfterSaleLogCreateReqBO createReqBO) { + AfterSaleLogDO afterSaleLog = AfterSaleLogConvert.INSTANCE.convert(createReqBO); + afterSaleLogMapper.insert(afterSaleLog); + } + + @Override + public List getAfterSaleLogList(Long afterSaleId) { + return afterSaleLogMapper.selectListByAfterSaleId(afterSaleId); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleService.java new file mode 100644 index 0000000..1bbe972 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleService.java @@ -0,0 +1,127 @@ +package com.tashow.cloud.trade.service.aftersale; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.trade.controller.admin.aftersale.vo.AfterSaleDisagreeReqVO; +import com.tashow.cloud.trade.controller.admin.aftersale.vo.AfterSalePageReqVO; +import com.tashow.cloud.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO; +import com.tashow.cloud.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO; +import com.tashow.cloud.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleDO; + +/** + * 售后订单 Service 接口 + * + * @author 芋道源码 + */ +public interface AfterSaleService { + + /** + * 【管理员】获得售后订单分页 + * + * @param pageReqVO 分页查询 + * @return 售后订单分页 + */ + PageResult getAfterSalePage(AfterSalePageReqVO pageReqVO); + + /** + * 【会员】获得售后订单分页 + * + * @param userId 用户编号 + * @param pageParam 分页参数 + * @return 售后订单分页 + */ + PageResult getAfterSalePage(Long userId, PageParam pageParam); + + /** + * 【会员】获得售后单 + * + * @param userId 用户编号 + * @param id 售后编号 + * @return 售后订单 + */ + AfterSaleDO getAfterSale(Long userId, Long id); + + /** + * 【管理员】获得售后单 + * + * @param id 售后编号 + * @return 售后订单 + */ + AfterSaleDO getAfterSale(Long id); + + /** + * 【会员】创建售后订单 + * + * @param userId 会员用户编号 + * @param createReqVO 创建 Request 信息 + * @return 售后编号 + */ + Long createAfterSale(Long userId, AppAfterSaleCreateReqVO createReqVO); + + /** + * 【管理员】同意售后订单 + * + * @param userId 管理员用户编号 + * @param id 售后编号 + */ + void agreeAfterSale(Long userId, Long id); + + /** + * 【管理员】拒绝售后订单 + * + * @param userId 管理员用户编号 + * @param auditReqVO 审批 Request 信息 + */ + void disagreeAfterSale(Long userId, AfterSaleDisagreeReqVO auditReqVO); + + /** + * 【会员】退回货物 + * + * @param userId 会员用户编号 + * @param deliveryReqVO 退货 Request 信息 + */ + void deliveryAfterSale(Long userId, AppAfterSaleDeliveryReqVO deliveryReqVO); + + /** + * 【管理员】确认收货 + * + * @param userId 管理员编号 + * @param id 售后编号 + */ + void receiveAfterSale(Long userId, Long id); + + /** + * 【管理员】拒绝收货 + * + * @param userId 管理员用户编号 + * @param refuseReqVO 拒绝收货 Request 信息 + */ + void refuseAfterSale(Long userId, AfterSaleRefuseReqVO refuseReqVO); + + /** + * 【管理员】确认退款 + * + * @param userId 管理员用户编号 + * @param userIp 管理员用户 IP + * @param id 售后编号 + */ + void refundAfterSale(Long userId, String userIp, Long id); + + /** + * 【会员】取消售后 + * + * @param userId 会员用户编号 + * @param id 售后编号 + */ + void cancelAfterSale(Long userId, Long id); + + /** + * 【会员】获得正在进行中的售后订单数量 + * + * @param userId 用户编号 + * @return 数量 + */ + Long getApplyingAfterSaleCount(Long userId); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleServiceImpl.java new file mode 100644 index 0000000..9e1e542 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/AfterSaleServiceImpl.java @@ -0,0 +1,430 @@ +package com.tashow.cloud.trade.service.aftersale; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi; +import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO; +import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum; +import com.tashow.cloud.trade.controller.admin.aftersale.vo.AfterSaleDisagreeReqVO; +import com.tashow.cloud.trade.controller.admin.aftersale.vo.AfterSalePageReqVO; +import com.tashow.cloud.trade.controller.admin.aftersale.vo.AfterSaleRefuseReqVO; +import com.tashow.cloud.trade.controller.app.aftersale.vo.AppAfterSaleCreateReqVO; +import com.tashow.cloud.trade.controller.app.aftersale.vo.AppAfterSaleDeliveryReqVO; +import com.tashow.cloud.trade.convert.aftersale.AfterSaleConvert; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import com.tashow.cloud.trade.dal.mysql.aftersale.AfterSaleMapper; +import com.tashow.cloud.trade.dal.redis.no.TradeNoRedisDAO; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleOperateTypeEnum; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleStatusEnum; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleTypeEnum; +import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleWayEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.framework.aftersale.core.annotations.AfterSaleLog; +import com.tashow.cloud.trade.framework.aftersale.core.utils.AfterSaleLogUtils; +import com.tashow.cloud.trade.framework.order.config.TradeOrderProperties; +import com.tashow.cloud.trade.service.delivery.DeliveryExpressService; +import com.tashow.cloud.trade.service.order.TradeOrderQueryService; +import com.tashow.cloud.trade.service.order.TradeOrderUpdateService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * 售后订单 Service 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@Service +@Validated +public class AfterSaleServiceImpl implements AfterSaleService { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private TradeOrderUpdateService tradeOrderUpdateService; + @Resource + private TradeOrderQueryService tradeOrderQueryService; + @Resource + private DeliveryExpressService deliveryExpressService; + + @Resource + private AfterSaleMapper tradeAfterSaleMapper; + @Resource + private TradeNoRedisDAO tradeNoRedisDAO; + + @Resource + private PayRefundApi payRefundApi; + @Resource + private CombinationRecordApi combinationRecordApi; + + @Resource + private TradeOrderProperties tradeOrderProperties; + + @Override + public PageResult getAfterSalePage(AfterSalePageReqVO pageReqVO) { + return tradeAfterSaleMapper.selectPage(pageReqVO); + } + + @Override + public PageResult getAfterSalePage(Long userId, PageParam pageParam) { + return tradeAfterSaleMapper.selectPage(userId, pageParam); + } + + @Override + public AfterSaleDO getAfterSale(Long userId, Long id) { + return tradeAfterSaleMapper.selectByIdAndUserId(id, userId); + } + + @Override + public AfterSaleDO getAfterSale(Long id) { + return tradeAfterSaleMapper.selectById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @AfterSaleLog(operateType = AfterSaleOperateTypeEnum.MEMBER_CREATE) + public Long createAfterSale(Long userId, AppAfterSaleCreateReqVO createReqVO) { + // 第一步,前置校验 + TradeOrderItemDO tradeOrderItem = validateOrderItemApplicable(userId, createReqVO); + + // 第二步,存储售后订单 + AfterSaleDO afterSale = createAfterSale(createReqVO, tradeOrderItem); + return afterSale.getId(); + } + + /** + * 校验交易订单项是否可以申请售后 + * + * @param userId 用户编号 + * @param createReqVO 售后创建信息 + * @return 交易订单项 + */ + private TradeOrderItemDO validateOrderItemApplicable(Long userId, AppAfterSaleCreateReqVO createReqVO) { + // 校验订单项存在 + TradeOrderItemDO orderItem = tradeOrderQueryService.getOrderItem(userId, createReqVO.getOrderItemId()); + if (orderItem == null) { + throw exception(ORDER_ITEM_NOT_FOUND); + } + // 已申请售后,不允许再发起售后申请 + if (!TradeOrderItemAfterSaleStatusEnum.isNone(orderItem.getAfterSaleStatus())) { + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_ITEM_APPLIED); + } + // 申请的退款金额,不能超过商品的价格 + if (createReqVO.getRefundPrice() > orderItem.getPayPrice()) { + throw exception(AFTER_SALE_CREATE_FAIL_REFUND_PRICE_ERROR); + } + + // 校验订单存在 + TradeOrderDO order = tradeOrderQueryService.getOrder(userId, orderItem.getOrderId()); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // TODO 芋艿:超过一定时间,不允许售后 + // 已取消,无法发起售后 + if (TradeOrderStatusEnum.isCanceled(order.getStatus())) { + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_CANCELED); + } + // 未支付,无法发起售后 + if (!TradeOrderStatusEnum.havePaid(order.getStatus())) { + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_PAID); + } + // 如果是【退货退款】的情况,需要额外校验是否发货 + if (createReqVO.getWay().equals(AfterSaleWayEnum.RETURN_AND_REFUND.getWay()) + && !TradeOrderStatusEnum.haveDelivered(order.getStatus())) { + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_DELIVERED); + } + // 如果是拼团订单,则进行中不允许售后 + if (TradeOrderTypeEnum.isCombination(order.getType())) { + CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId( + order.getUserId(), order.getId()).getCheckedData(); + if (combinationRecord != null && CombinationRecordStatusEnum.isInProgress(combinationRecord.getStatus())) { + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS); + } + } + return orderItem; + } + + private AfterSaleDO createAfterSale(AppAfterSaleCreateReqVO createReqVO, + TradeOrderItemDO orderItem) { + // 创建售后单 + AfterSaleDO afterSale = AfterSaleConvert.INSTANCE.convert(createReqVO, orderItem); + afterSale.setNo(tradeNoRedisDAO.generate(TradeNoRedisDAO.AFTER_SALE_NO_PREFIX)); + afterSale.setStatus(AfterSaleStatusEnum.APPLY.getStatus()); + // 标记是售中还是售后 + TradeOrderDO order = tradeOrderQueryService.getOrder(orderItem.getUserId(), orderItem.getOrderId()); + afterSale.setOrderNo(order.getNo()); // 记录 orderNo 订单流水,方便后续检索 + afterSale.setType(TradeOrderStatusEnum.isCompleted(order.getStatus()) + ? AfterSaleTypeEnum.AFTER_SALE.getType() : AfterSaleTypeEnum.IN_SALE.getType()); + tradeAfterSaleMapper.insert(afterSale); + + // 更新交易订单项的售后状态 + tradeOrderUpdateService.updateOrderItemWhenAfterSaleCreate(orderItem.getId(), afterSale.getId()); + + // 记录售后日志 + AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), null, + AfterSaleStatusEnum.APPLY.getStatus()); + + // TODO 发送售后消息 + return afterSale; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @AfterSaleLog(operateType = AfterSaleOperateTypeEnum.ADMIN_AGREE_APPLY) + public void agreeAfterSale(Long userId, Long id) { + // 校验售后单存在,并状态未审批 + AfterSaleDO afterSale = validateAfterSaleAuditable(id); + + // 更新售后单的状态 + // 情况一:退款:标记为 WAIT_REFUND 状态。后续等退款发起成功后,在标记为 COMPLETE 状态 + // 情况二:退货退款:需要等用户退货后,才能发起退款 + Integer newStatus = afterSale.getWay().equals(AfterSaleWayEnum.REFUND.getWay()) ? + AfterSaleStatusEnum.WAIT_REFUND.getStatus() : AfterSaleStatusEnum.SELLER_AGREE.getStatus(); + updateAfterSaleStatus(afterSale.getId(), AfterSaleStatusEnum.APPLY.getStatus(), new AfterSaleDO() + .setStatus(newStatus).setAuditUserId(userId).setAuditTime(LocalDateTime.now())); + + // 记录售后日志 + AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), newStatus); + + // TODO 发送售后消息 + } + + @Override + @Transactional(rollbackFor = Exception.class) + @AfterSaleLog(operateType = AfterSaleOperateTypeEnum.ADMIN_DISAGREE_APPLY) + public void disagreeAfterSale(Long userId, AfterSaleDisagreeReqVO auditReqVO) { + // 校验售后单存在,并状态未审批 + AfterSaleDO afterSale = validateAfterSaleAuditable(auditReqVO.getId()); + + // 更新售后单的状态 + Integer newStatus = AfterSaleStatusEnum.SELLER_DISAGREE.getStatus(); + updateAfterSaleStatus(afterSale.getId(), AfterSaleStatusEnum.APPLY.getStatus(), new AfterSaleDO() + .setStatus(newStatus).setAuditUserId(userId).setAuditTime(LocalDateTime.now()) + .setAuditReason(auditReqVO.getAuditReason())); + + // 记录售后日志 + AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), newStatus); + + // TODO 发送售后消息 + + // 更新交易订单项的售后状态为【未申请】 + tradeOrderUpdateService.updateOrderItemWhenAfterSaleCancel(afterSale.getOrderItemId()); + } + + /** + * 校验售后单是否可审批(同意售后、拒绝售后) + * + * @param id 售后编号 + * @return 售后单 + */ + private AfterSaleDO validateAfterSaleAuditable(Long id) { + AfterSaleDO afterSale = tradeAfterSaleMapper.selectById(id); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtil.notEqual(afterSale.getStatus(), AfterSaleStatusEnum.APPLY.getStatus())) { + throw exception(AFTER_SALE_AUDIT_FAIL_STATUS_NOT_APPLY); + } + return afterSale; + } + + private void updateAfterSaleStatus(Long id, Integer status, AfterSaleDO updateObj) { + int updateCount = tradeAfterSaleMapper.updateByIdAndStatus(id, status, updateObj); + if (updateCount == 0) { + throw exception(AFTER_SALE_UPDATE_STATUS_FAIL); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + @AfterSaleLog(operateType = AfterSaleOperateTypeEnum.MEMBER_DELIVERY) + public void deliveryAfterSale(Long userId, AppAfterSaleDeliveryReqVO deliveryReqVO) { + // 校验售后单存在,并状态未退货 + AfterSaleDO afterSale = tradeAfterSaleMapper.selectByIdAndUserId(deliveryReqVO.getId(), userId); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtil.notEqual(afterSale.getStatus(), AfterSaleStatusEnum.SELLER_AGREE.getStatus())) { + throw exception(AFTER_SALE_DELIVERY_FAIL_STATUS_NOT_SELLER_AGREE); + } + DeliveryExpressDO express = deliveryExpressService.validateDeliveryExpress(deliveryReqVO.getLogisticsId()); + + // 更新售后单的物流信息 + updateAfterSaleStatus(afterSale.getId(), AfterSaleStatusEnum.SELLER_AGREE.getStatus(), new AfterSaleDO() + .setStatus(AfterSaleStatusEnum.BUYER_DELIVERY.getStatus()) + .setLogisticsId(deliveryReqVO.getLogisticsId()).setLogisticsNo(deliveryReqVO.getLogisticsNo()) + .setDeliveryTime(LocalDateTime.now())); + + // 记录售后日志 + AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), + AfterSaleStatusEnum.BUYER_DELIVERY.getStatus(), + MapUtil.builder().put("deliveryName", express.getName()) + .put("logisticsNo", deliveryReqVO.getLogisticsNo()).build()); + + // TODO 发送售后消息 + } + + @Override + @Transactional(rollbackFor = Exception.class) + @AfterSaleLog(operateType = AfterSaleOperateTypeEnum.ADMIN_AGREE_RECEIVE) + public void receiveAfterSale(Long userId, Long id) { + // 校验售后单存在,并状态为已退货 + AfterSaleDO afterSale = validateAfterSaleReceivable(id); + + // 更新售后单的状态 + updateAfterSaleStatus(afterSale.getId(), AfterSaleStatusEnum.BUYER_DELIVERY.getStatus(), new AfterSaleDO() + .setStatus(AfterSaleStatusEnum.WAIT_REFUND.getStatus()).setReceiveTime(LocalDateTime.now())); + + // 记录售后日志 + AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), + AfterSaleStatusEnum.WAIT_REFUND.getStatus()); + + // TODO 发送售后消息 + } + + @Override + @Transactional(rollbackFor = Exception.class) + @AfterSaleLog(operateType = AfterSaleOperateTypeEnum.ADMIN_DISAGREE_RECEIVE) + public void refuseAfterSale(Long userId, AfterSaleRefuseReqVO refuseReqVO) { + // 校验售后单存在,并状态为已退货 + AfterSaleDO afterSale = tradeAfterSaleMapper.selectById(refuseReqVO.getId()); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtil.notEqual(afterSale.getStatus(), AfterSaleStatusEnum.BUYER_DELIVERY.getStatus())) { + throw exception(AFTER_SALE_CONFIRM_FAIL_STATUS_NOT_BUYER_DELIVERY); + } + + // 更新售后单的状态 + updateAfterSaleStatus(afterSale.getId(), AfterSaleStatusEnum.BUYER_DELIVERY.getStatus(), new AfterSaleDO() + .setStatus(AfterSaleStatusEnum.SELLER_REFUSE.getStatus()).setReceiveTime(LocalDateTime.now()) + .setReceiveReason(refuseReqVO.getRefuseMemo())); + + // 记录售后日志 + AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), + AfterSaleStatusEnum.SELLER_REFUSE.getStatus(), + MapUtil.of("reason", refuseReqVO.getRefuseMemo())); + + // TODO 发送售后消息 + + // 更新交易订单项的售后状态为【未申请】 + tradeOrderUpdateService.updateOrderItemWhenAfterSaleCancel(afterSale.getOrderItemId()); + } + + /** + * 校验售后单是否可收货,即处于买家已发货 + * + * @param id 售后编号 + * @return 售后单 + */ + private AfterSaleDO validateAfterSaleReceivable(Long id) { + AfterSaleDO afterSale = tradeAfterSaleMapper.selectById(id); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtil.notEqual(afterSale.getStatus(), AfterSaleStatusEnum.BUYER_DELIVERY.getStatus())) { + throw exception(AFTER_SALE_CONFIRM_FAIL_STATUS_NOT_BUYER_DELIVERY); + } + return afterSale; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @AfterSaleLog(operateType = AfterSaleOperateTypeEnum.ADMIN_REFUND) + public void refundAfterSale(Long userId, String userIp, Long id) { + // 校验售后单的状态,并状态待退款 + AfterSaleDO afterSale = tradeAfterSaleMapper.selectById(id); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtil.notEqual(afterSale.getStatus(), AfterSaleStatusEnum.WAIT_REFUND.getStatus())) { + throw exception(AFTER_SALE_REFUND_FAIL_STATUS_NOT_WAIT_REFUND); + } + + // 发起退款单。注意,需要在事务提交后,再进行发起,避免重复发起 + createPayRefund(userIp, afterSale); + + // 更新售后单的状态为【已完成】 + updateAfterSaleStatus(afterSale.getId(), AfterSaleStatusEnum.WAIT_REFUND.getStatus(), new AfterSaleDO() + .setStatus(AfterSaleStatusEnum.COMPLETE.getStatus()).setRefundTime(LocalDateTime.now())); + + // 记录售后日志 + AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), + AfterSaleStatusEnum.COMPLETE.getStatus()); + + // TODO 发送售后消息 + + // 更新交易订单项的售后状态为【已完成】 + tradeOrderUpdateService.updateOrderItemWhenAfterSaleSuccess(afterSale.getOrderItemId(), afterSale.getRefundPrice()); + } + + private void createPayRefund(String userIp, AfterSaleDO afterSale) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + // 创建退款单 + PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties) + .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));; + Long payRefundId = payRefundApi.createRefund(createReqDTO).getCheckedData(); + // 更新售后单的退款单号 + tradeAfterSaleMapper.updateById(new AfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId)); + } + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @AfterSaleLog(operateType = AfterSaleOperateTypeEnum.MEMBER_CANCEL) + public void cancelAfterSale(Long userId, Long id) { + // 校验售后单的状态,并状态待退款 + AfterSaleDO afterSale = tradeAfterSaleMapper.selectByIdAndUserId(id, userId); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (!ObjectUtils.equalsAny(afterSale.getStatus(), AfterSaleStatusEnum.APPLY.getStatus(), + AfterSaleStatusEnum.SELLER_AGREE.getStatus(), + AfterSaleStatusEnum.BUYER_DELIVERY.getStatus())) { + throw exception(AFTER_SALE_CANCEL_FAIL_STATUS_NOT_APPLY_OR_AGREE_OR_BUYER_DELIVERY); + } + + // 更新售后单的状态为【已取消】 + updateAfterSaleStatus(afterSale.getId(), afterSale.getStatus(), new AfterSaleDO() + .setStatus(AfterSaleStatusEnum.BUYER_CANCEL.getStatus())); + + // 记录售后日志 + AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), + AfterSaleStatusEnum.BUYER_CANCEL.getStatus()); + + // TODO 发送售后消息 + + // 更新交易订单项的售后状态为【未申请】 + tradeOrderUpdateService.updateOrderItemWhenAfterSaleCancel(afterSale.getOrderItemId()); + } + + @Override + public Long getApplyingAfterSaleCount(Long userId) { + return tradeAfterSaleMapper.selectCountByUserIdAndStatus(userId, AfterSaleStatusEnum.APPLYING_STATUSES); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/bo/AfterSaleLogCreateReqBO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/bo/AfterSaleLogCreateReqBO.java new file mode 100644 index 0000000..842ce12 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/aftersale/bo/AfterSaleLogCreateReqBO.java @@ -0,0 +1,57 @@ +package com.tashow.cloud.trade.service.aftersale.bo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * 售后日志的创建 Request BO + * + * @author 陈賝 + * @since 2023/6/19 09:54 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AfterSaleLogCreateReqBO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 售后编号 + */ + @NotNull(message = "售后编号不能为空") + private Long afterSaleId; + /** + * 操作前状态 + */ + private Integer beforeStatus; + /** + * 操作后状态 + */ + @NotNull(message = "操作后的状态不能为空") + private Integer afterStatus; + + /** + * 操作类型 + */ + @NotNull(message = "操作类型不能为空") + private Integer operateType; + /** + * 操作明细 + */ + @NotEmpty(message = "操作明细不能为空") + private String content; +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageRecordService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageRecordService.java new file mode 100644 index 0000000..fc4825a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageRecordService.java @@ -0,0 +1,158 @@ +package com.tashow.cloud.trade.service.brokerage; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.record.AppBrokerageProductPriceRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByPriceRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankPageReqVO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageRecordDO; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; +import com.tashow.cloud.trade.service.brokerage.bo.BrokerageAddReqBO; +import com.tashow.cloud.trade.service.brokerage.bo.UserBrokerageSummaryRespBO; +import jakarta.validation.Valid; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 佣金记录 Service 接口 + * + * @author owen + */ +public interface BrokerageRecordService { + + /** + * 获得佣金记录 + * + * @param id 编号 + * @return 佣金记录 + */ + BrokerageRecordDO getBrokerageRecord(Long id); + + /** + * 获得佣金记录分页 + * + * @param pageReqVO 分页查询 + * @return 佣金记录分页 + */ + PageResult getBrokerageRecordPage(BrokerageRecordPageReqVO pageReqVO); + + /** + * 增加佣金【多级分佣】 + * + * @param userId 会员编号 + * @param bizType 业务类型 + * @param list 请求参数列表 + */ + void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, @Valid List list); + + /** + * 增加佣金【只针对自己】 + * + * @param userId 会员编号 + * @param bizType 业务类型 + * @param bizId 业务编号 + * @param brokeragePrice 佣金 + * @param title 标题 + */ + void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, String bizId, Integer brokeragePrice, String title); + + /** + * 减少佣金【只针对自己】 + * + * @param userId 会员编号 + * @param bizType 业务类型 + * @param bizId 业务编号 + * @param brokeragePrice 佣金 + * @param title 标题 + */ + default void reduceBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, String bizId, Integer brokeragePrice, String title) { + addBrokerage(userId, bizType, bizId, -brokeragePrice, title); + } + + /** + * 取消佣金:将佣金记录,状态修改为已失效 + * + * @param bizType 业务类型 + * @param bizId 业务编号 + */ + void cancelBrokerage(BrokerageRecordBizTypeEnum bizType, String bizId); + + /** + * 解冻佣金:将待结算的佣金记录,状态修改为已结算 + * + * @return 解冻佣金的数量 + */ + int unfreezeRecord(); + + /** + * 按照 userId,汇总每个用户的佣金 + * + * @param userIds 用户编号 + * @param bizType 业务类型 + * @param status 佣金状态 + * @return 用户佣金汇总 List + */ + List getUserBrokerageSummaryListByUserId(Collection userIds, + Integer bizType, Integer status); + + /** + * 按照 userId,汇总每个用户的佣金 + * + * @param userIds 用户编号 + * @param bizType 业务类型 + * @param status 佣金状态 + * @return 用户佣金汇总 Map + */ + default Map getUserBrokerageSummaryMapByUserId(Collection userIds, + Integer bizType, Integer status) { + return convertMap(getUserBrokerageSummaryListByUserId(userIds, bizType, status), + UserBrokerageSummaryRespBO::getUserId); + } + + /** + * 获得用户佣金合计 + * + * @param userId 用户编号 + * @param bizType 业务类型 + * @param status 状态 + * @param beginTime 开始时间 + * @param endTime 截止时间 + * @return 用户佣金合计 + */ + Integer getSummaryPriceByUserId(Long userId, BrokerageRecordBizTypeEnum bizType, BrokerageRecordStatusEnum status, + LocalDateTime beginTime, LocalDateTime endTime); + + /** + * 获得用户佣金排行分页列表(基于佣金总数) + * + * @param pageReqVO 分页查询 + * @return 排行榜分页 + */ + PageResult getBrokerageUserChildSummaryPageByPrice( + AppBrokerageUserRankPageReqVO pageReqVO); + + /** + * 获取用户的排名(基于佣金总数) + * + * @param userId 用户编号 + * @param times 时间范围 + * @return 用户的排名 + */ + Integer getUserRankByPrice(Long userId, LocalDateTime[] times); + + /** + * 计算商品被购买后,推广员可以得到的佣金 + * + * @param userId 用户编号 + * @param spuId 商品编号 + * @return 用户佣金 + */ + AppBrokerageProductPriceRespVO calculateProductBrokeragePrice(Long userId, Long spuId); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageRecordServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageRecordServiceImpl.java new file mode 100644 index 0000000..223e005 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageRecordServiceImpl.java @@ -0,0 +1,370 @@ +package com.tashow.cloud.trade.service.brokerage; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.*; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.record.AppBrokerageProductPriceRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByPriceRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankPageReqVO; +import com.tashow.cloud.trade.convert.brokerage.BrokerageRecordConvert; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageRecordDO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageUserDO; +import com.tashow.cloud.trade.dal.dataobject.config.TradeConfigDO; +import com.tashow.cloud.trade.dal.mysql.brokerage.BrokerageRecordMapper; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; +import com.tashow.cloud.trade.service.brokerage.bo.BrokerageAddReqBO; +import com.tashow.cloud.trade.service.brokerage.bo.UserBrokerageSummaryRespBO; +import com.tashow.cloud.trade.service.config.TradeConfigService; +import com.baomidou.mybatisplus.core.metadata.IPage; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMaxValue; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getMinValue; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH; + +/** + * 佣金记录 Service 实现类 + * + * @author owen + */ +@Slf4j +@Service +@Validated +public class BrokerageRecordServiceImpl implements BrokerageRecordService { + + @Resource + private BrokerageRecordMapper brokerageRecordMapper; + @Resource + private TradeConfigService tradeConfigService; + @Resource + private BrokerageUserService brokerageUserService; + + @Resource + private ProductSpuApi productSpuApi; + @Resource + private ProductSkuApi productSkuApi; + + @Override + public BrokerageRecordDO getBrokerageRecord(Long id) { + return brokerageRecordMapper.selectById(id); + } + + @Override + public PageResult getBrokerageRecordPage(BrokerageRecordPageReqVO pageReqVO) { + return brokerageRecordMapper.selectPage(pageReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, List list) { + TradeConfigDO memberConfig = tradeConfigService.getTradeConfig(); + // 0 未启用分销功能 + if (memberConfig == null || !BooleanUtil.isTrue(memberConfig.getBrokerageEnabled())) { + log.error("[addBrokerage][增加佣金失败:brokerageEnabled 未配置,userId({}) bizType({}) list({})", userId, bizType, list); + return; + } + + // 1.1 获得一级推广人 + BrokerageUserDO firstUser = brokerageUserService.getBindBrokerageUser(userId); + if (firstUser == null || !BooleanUtil.isTrue(firstUser.getBrokerageEnabled())) { + return; + } + // 1.2 计算一级分佣 + addBrokerage(firstUser, list, memberConfig.getBrokerageFrozenDays(), memberConfig.getBrokerageFirstPercent(), + bizType, 1); + + // 2.1 获得二级推广员 + if (firstUser.getBindUserId() == null) { + return; + } + BrokerageUserDO secondUser = brokerageUserService.getBrokerageUser(firstUser.getBindUserId()); + if (secondUser == null || !BooleanUtil.isTrue(secondUser.getBrokerageEnabled())) { + return; + } + // 2.2 计算二级分佣 + addBrokerage(secondUser, list, memberConfig.getBrokerageFrozenDays(), memberConfig.getBrokerageSecondPercent(), + bizType, 2); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelBrokerage(BrokerageRecordBizTypeEnum bizType, String bizId) { + List records = brokerageRecordMapper.selectListByBizTypeAndBizId(bizType.getType(), bizId); + if (CollUtil.isEmpty(records)) { + log.error("[cancelBrokerage][bizId({}) bizType({}) 更新为已失效失败:记录不存在]", bizId, bizType); + return; + } + + records.forEach(record -> { + // 1. 更新佣金记录为已失效 + BrokerageRecordDO updateObj = new BrokerageRecordDO().setStatus(BrokerageRecordStatusEnum.CANCEL.getStatus()); + int updateRows = brokerageRecordMapper.updateByIdAndStatus(record.getId(), record.getStatus(), updateObj); + if (updateRows == 0) { + log.error("[cancelBrokerage][record({}) 更新为已失效失败]", record.getId()); + return; + } + + // 2. 更新用户的佣金 + if (BrokerageRecordStatusEnum.WAIT_SETTLEMENT.getStatus().equals(record.getStatus())) { + brokerageUserService.updateUserFrozenPrice(record.getUserId(), -record.getPrice()); + } else if (BrokerageRecordStatusEnum.SETTLEMENT.getStatus().equals(record.getStatus())) { + brokerageUserService.updateUserPrice(record.getUserId(), -record.getPrice()); + } + }); + } + + /** + * 计算佣金 + * + * @param basePrice 佣金基数 + * @param percent 佣金比例 + * @param fixedPrice 固定佣金 + * @return 佣金 + */ + int calculatePrice(Integer basePrice, Integer percent, Integer fixedPrice) { + // 1. 优先使用固定佣金 + if (fixedPrice != null && fixedPrice > 0) { + return ObjectUtil.defaultIfNull(fixedPrice, 0); + } + // 2. 根据比例计算佣金 + if (basePrice != null && basePrice > 0 && percent != null && percent > 0) { + return MoneyUtils.calculateRatePriceFloor(basePrice, Double.valueOf(percent)); + } + return 0; + } + + /** + * 增加用户佣金 + * + * @param user 用户 + * @param list 佣金增加参数列表 + * @param brokerageFrozenDays 冻结天数 + * @param brokeragePercent 佣金比例 + * @param bizType 业务类型 + * @param sourceUserLevel 来源用户等级 + */ + private void addBrokerage(BrokerageUserDO user, List list, Integer brokerageFrozenDays, + Integer brokeragePercent, BrokerageRecordBizTypeEnum bizType, Integer sourceUserLevel) { + // 1.1 处理冻结时间 + LocalDateTime unfreezeTime = null; + if (brokerageFrozenDays != null && brokerageFrozenDays > 0) { + unfreezeTime = LocalDateTime.now().plusDays(brokerageFrozenDays); + } + // 1.2 计算分佣 + int totalBrokerage = 0; + List records = new ArrayList<>(); + for (BrokerageAddReqBO item : list) { + // 计算金额 + Integer fixedPrice; + if (Objects.equals(sourceUserLevel, 1)) { + fixedPrice = item.getFirstFixedPrice(); + } else if (Objects.equals(sourceUserLevel, 2)) { + fixedPrice = item.getSecondFixedPrice(); + } else { + throw new IllegalArgumentException(StrUtil.format("用户等级({}) 不合法", sourceUserLevel)); + } + int brokeragePrice = calculatePrice(item.getBasePrice(), brokeragePercent, fixedPrice); + if (brokeragePrice <= 0) { + continue; + } + totalBrokerage += brokeragePrice; + // 创建记录实体 + records.add(BrokerageRecordConvert.INSTANCE.convert(user, bizType, item.getBizId(), + brokerageFrozenDays, brokeragePrice, unfreezeTime, item.getTitle(), + item.getSourceUserId(), sourceUserLevel)); + } + if (CollUtil.isEmpty(records)) { + return; + } + // 1.3 保存佣金记录 + brokerageRecordMapper.insertBatch(records); + + // 2. 更新用户佣金 + if (brokerageFrozenDays != null && brokerageFrozenDays > 0) { // 更新用户冻结佣金 + brokerageUserService.updateUserFrozenPrice(user.getId(), totalBrokerage); + } else { // 更新用户可用佣金 + brokerageUserService.updateUserPrice(user.getId(), totalBrokerage); + } + } + + @Override + public int unfreezeRecord() { + // 1. 查询待结算的佣金记录 + List records = brokerageRecordMapper.selectListByStatusAndUnfreezeTimeLt( + BrokerageRecordStatusEnum.WAIT_SETTLEMENT.getStatus(), LocalDateTime.now()); + if (CollUtil.isEmpty(records)) { + return 0; + } + + // 2. 遍历执行 + int count = 0; + for (BrokerageRecordDO record : records) { + try { + boolean success = getSelf().unfreezeRecord(record); + if (success) { + count++; + } + } catch (Exception e) { + log.error("[unfreezeRecord][record({}) 更新为已结算失败]", record.getId(), e); + } + } + return count; + } + + /** + * 解冻单条佣金记录 + * + * @param record 佣金记录 + * @return 解冻是否成功 + */ + @Transactional(rollbackFor = Exception.class) + public boolean unfreezeRecord(BrokerageRecordDO record) { + // 更新记录状态 + BrokerageRecordDO updateObj = new BrokerageRecordDO() + .setStatus(BrokerageRecordStatusEnum.SETTLEMENT.getStatus()) + .setUnfreezeTime(LocalDateTime.now()); + int updateRows = brokerageRecordMapper.updateByIdAndStatus(record.getId(), record.getStatus(), updateObj); + if (updateRows == 0) { + log.error("[unfreezeRecord][record({}) 更新为已结算失败]", record.getId()); + return false; + } + + // 更新用户冻结佣金 + brokerageUserService.updateFrozenPriceDecrAndPriceIncr(record.getUserId(), -record.getPrice()); + log.info("[unfreezeRecord][record({}) 更新为已结算成功]", record.getId()); + return true; + } + + @Override + public List getUserBrokerageSummaryListByUserId(Collection userIds, + Integer bizType, Integer status) { + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyList(); + } + return brokerageRecordMapper.selectCountAndSumPriceByUserIdInAndBizTypeAndStatus(userIds, bizType, status); + } + + @Override + public Integer getSummaryPriceByUserId(Long userId, BrokerageRecordBizTypeEnum bizType, BrokerageRecordStatusEnum status, + LocalDateTime beginTime, LocalDateTime endTime) { + return brokerageRecordMapper.selectSummaryPriceByUserIdAndBizTypeAndCreateTimeBetween(userId, + bizType.getType(), status.getStatus(), beginTime, endTime); + } + + @Override + public PageResult getBrokerageUserChildSummaryPageByPrice(AppBrokerageUserRankPageReqVO pageReqVO) { + IPage pageResult = brokerageRecordMapper.selectSummaryPricePageGroupByUserId( + MyBatisUtils.buildPage(pageReqVO), + BrokerageRecordBizTypeEnum.ORDER.getType(), BrokerageRecordStatusEnum.SETTLEMENT.getStatus(), + ArrayUtil.get(pageReqVO.getTimes(), 0), ArrayUtil.get(pageReqVO.getTimes(), 1)); + return new PageResult<>(pageResult.getRecords(), pageResult.getTotal()); + } + + @Override + public Integer getUserRankByPrice(Long userId, LocalDateTime[] times) { + // 用户的推广金额 + Integer price = brokerageRecordMapper.selectSummaryPriceByUserIdAndBizTypeAndCreateTimeBetween(userId, + BrokerageRecordBizTypeEnum.ORDER.getType(), BrokerageRecordStatusEnum.SETTLEMENT.getStatus(), + ArrayUtil.get(times, 0), ArrayUtil.get(times, 1)); + // 排在用户前面的人数 + Integer greaterCount = brokerageRecordMapper.selectCountByPriceGt(price, + BrokerageRecordBizTypeEnum.ORDER.getType(), BrokerageRecordStatusEnum.SETTLEMENT.getStatus(), + ArrayUtil.get(times, 0), ArrayUtil.get(times, 1)); + // 获得排名 + return ObjUtil.defaultIfNull(greaterCount, 0) + 1; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, String bizId, Integer brokeragePrice, String title) { + // 1. 校验佣金余额 + BrokerageUserDO user = brokerageUserService.getBrokerageUser(userId); + int balance = Optional.of(user) + .map(BrokerageUserDO::getBrokeragePrice).orElse(0); + if (balance + brokeragePrice < 0) { + throw exception(BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH, MoneyUtils.fenToYuanStr(balance)); + } + + // 2. 更新佣金余额 + boolean success = brokerageUserService.updateUserPrice(userId, brokeragePrice); + if (!success) { + // 失败时,则抛出异常。只会出现扣减佣金时,余额不足的情况 + throw exception(BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH, MoneyUtils.fenToYuanStr(balance)); + } + + // 3. 新增记录 + BrokerageRecordDO record = BrokerageRecordConvert.INSTANCE.convert(user, bizType, bizId, 0, brokeragePrice, + null, title, null, null); + brokerageRecordMapper.insert(record); + } + + @Override + public AppBrokerageProductPriceRespVO calculateProductBrokeragePrice(Long userId, Long spuId) { + // 1. 构建默认的返回值 + AppBrokerageProductPriceRespVO respVO = new AppBrokerageProductPriceRespVO().setEnabled(false) + .setBrokerageMinPrice(0).setBrokerageMaxPrice(0); + + // 2.1 校验分销功能是否开启 + TradeConfigDO tradeConfig = tradeConfigService.getTradeConfig(); + if (tradeConfig == null || BooleanUtil.isFalse(tradeConfig.getBrokerageEnabled())) { + return respVO; + } + // 2.2 校验用户是否有分销资格 + respVO.setEnabled(brokerageUserService.getUserBrokerageEnabled(getLoginUserId())); + if (BooleanUtil.isFalse(respVO.getEnabled())) { + return respVO; + } + // 2.3 校验商品是否存在 + ProductSpuRespDTO spu = productSpuApi.getSpu(spuId).getCheckedData(); + if (spu == null) { + return respVO; + } + + // 3.1 商品单独分佣模式 + Integer fixedMinPrice = 0; + Integer fixedMaxPrice = 0; + Integer spuMinPrice = 0; + Integer spuMaxPrice = 0; + List skuList = productSkuApi.getSkuListBySpuId(ListUtil.of(spuId)).getCheckedData(); + if (BooleanUtil.isTrue(spu.getSubCommissionType())) { + fixedMinPrice = getMinValue(skuList, ProductSkuRespDTO::getFirstBrokeragePrice); + fixedMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getFirstBrokeragePrice); + // 3.2 全局分佣模式(根据商品价格比例计算) + } else { + spuMinPrice = getMinValue(skuList, ProductSkuRespDTO::getPrice); + spuMaxPrice = getMaxValue(skuList, ProductSkuRespDTO::getPrice); + } + respVO.setBrokerageMinPrice(calculatePrice(spuMinPrice, tradeConfig.getBrokerageFirstPercent(), fixedMinPrice)); + respVO.setBrokerageMaxPrice(calculatePrice(spuMaxPrice, tradeConfig.getBrokerageFirstPercent(), fixedMaxPrice)); + return respVO; + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private BrokerageRecordServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageUserService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageUserService.java new file mode 100644 index 0000000..b2ea00e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageUserService.java @@ -0,0 +1,145 @@ +package com.tashow.cloud.trade.service.brokerage; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.user.BrokerageUserCreateReqVO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.user.BrokerageUserPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserChildSummaryPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserChildSummaryRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByUserCountRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankPageReqVO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageUserDO; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +/** + * 分销用户 Service 接口 + * + * @author owen + */ +public interface BrokerageUserService { + + /** + * 获得分销用户 + * + * @param id 编号 + * @return 分销用户 + */ + BrokerageUserDO getBrokerageUser(Long id); + + /** + * 获得分销用户分页 + * + * @param pageReqVO 分页查询 + * @return 分销用户分页 + */ + PageResult getBrokerageUserPage(BrokerageUserPageReqVO pageReqVO); + + /** + * 修改推广员编号 + * + * @param id 用户编号 + * @param bindUserId 推广员编号 + */ + void updateBrokerageUserId(Long id, Long bindUserId); + + /** + * 修改推广资格 + * + * @param id 用户编号 + * @param enabled 推广资格 + */ + void updateBrokerageUserEnabled(Long id, Boolean enabled); + + /** + * 获得用户的推广人 + * + * @param id 用户编号 + * @return 用户的推广人 + */ + BrokerageUserDO getBindBrokerageUser(Long id); + + /** + * 获得或创建分销用户 + * + * @param id 用户编号 + * @return 分销用户 + */ + BrokerageUserDO getOrCreateBrokerageUser(Long id); + + /** + * 更新用户佣金 + * + * @param id 用户编号 + * @param price 用户可用佣金 + * @return 更新结果 + */ + boolean updateUserPrice(Long id, Integer price); + + /** + * 更新用户冻结佣金 + * + * @param id 用户编号 + * @param frozenPrice 用户冻结佣金 + */ + void updateUserFrozenPrice(Long id, Integer frozenPrice); + + /** + * 更新用户冻结佣金(减少),更新用户佣金(增加) + * + * @param id 用户编号 + * @param frozenPrice 减少冻结佣金(负数) + */ + void updateFrozenPriceDecrAndPriceIncr(Long id, Integer frozenPrice); + + /** + * 获得推广用户数量 + * + * @param bindUserId 绑定的推广员编号 + * @param level 推广用户等级 + * @return 推广用户数量 + */ + Long getBrokerageUserCountByBindUserId(Long bindUserId, Integer level); + + /** + * 【会员】绑定推广员 + * + * @param userId 用户编号 + * @param bindUserId 推广员编号 + * @return 是否绑定 + */ + boolean bindBrokerageUser(@NotNull Long userId, @NotNull Long bindUserId); + + /** + * 【管理员】创建分销用户 + * + * @param createReqVO 请求 + * @return 编号 + */ + Long createBrokerageUser(@Valid BrokerageUserCreateReqVO createReqVO); + + /** + * 获取用户是否有分销资格 + * + * @param userId 用户编号 + * @return 是否有分销资格 + */ + Boolean getUserBrokerageEnabled(Long userId); + + /** + * 获得推广人排行 + * + * @param pageReqVO 分页查询 + * @return 推广人排行 + */ + PageResult getBrokerageUserRankPageByUserCount(AppBrokerageUserRankPageReqVO pageReqVO); + + /** + * 获得下级分销统计分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 下级分销统计分页 + */ + PageResult getBrokerageUserChildSummaryPage(AppBrokerageUserChildSummaryPageReqVO pageReqVO, Long userId); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageUserServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageUserServiceImpl.java new file mode 100644 index 0000000..7bfca21 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageUserServiceImpl.java @@ -0,0 +1,385 @@ +package com.tashow.cloud.trade.service.brokerage; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.user.BrokerageUserCreateReqVO; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.user.BrokerageUserPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserChildSummaryPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserChildSummaryRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByUserCountRespVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankPageReqVO; +import com.tashow.cloud.trade.convert.brokerage.BrokerageUserConvert; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageUserDO; +import com.tashow.cloud.trade.dal.dataobject.config.TradeConfigDO; +import com.tashow.cloud.trade.dal.mysql.brokerage.BrokerageUserMapper; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageBindModeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageEnabledConditionEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum; +import com.tashow.cloud.trade.service.config.TradeConfigService; +import com.baomidou.mybatisplus.core.metadata.IPage; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMapByFilter; + +/** + * 分销用户 Service 实现类 + * + * @author owen + */ +@Service +@Validated +public class BrokerageUserServiceImpl implements BrokerageUserService { + + @Resource + private BrokerageUserMapper brokerageUserMapper; + + @Resource + private TradeConfigService tradeConfigService; + + @Resource + private MemberUserApi memberUserApi; + + @Override + public BrokerageUserDO getBrokerageUser(Long id) { + return brokerageUserMapper.selectById(id); + } + + @Override + public PageResult getBrokerageUserPage(BrokerageUserPageReqVO pageReqVO) { + List childIds = getChildUserIdsByLevel(pageReqVO.getBindUserId(), pageReqVO.getLevel()); + // 有”绑定用户编号“查询条件时,没有查到下级会员,直接返回空 + if (pageReqVO.getBindUserId() != null && CollUtil.isEmpty(childIds)) { + return PageResult.empty(); + } + return brokerageUserMapper.selectPage(pageReqVO, childIds); + } + + @Override + public void updateBrokerageUserId(Long id, Long bindUserId) { + // 校验存在 + BrokerageUserDO brokerageUser = validateBrokerageUserExists(id); + // 绑定关系未发生变化 + if (Objects.equals(brokerageUser.getBindUserId(), bindUserId)) { + return; + } + + // 情况一:清除推广员 + if (bindUserId == null) { + // 清除推广员 + brokerageUserMapper.updateBindUserIdAndBindUserTimeToNull(id); + return; + } + + // 情况二:修改推广员 + validateCanBindUser(brokerageUser, bindUserId); + brokerageUserMapper.updateById(fillBindUserData(bindUserId, new BrokerageUserDO().setId(id))); + } + + @Override + public void updateBrokerageUserEnabled(Long id, Boolean enabled) { + // 校验存在 + validateBrokerageUserExists(id); + if (BooleanUtil.isTrue(enabled)) { + // 开通推广资格 + brokerageUserMapper.updateById(new BrokerageUserDO().setId(id) + .setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now())); + } else { + // 取消推广资格 + brokerageUserMapper.updateEnabledFalseAndBrokerageTimeToNull(id); + } + } + + private BrokerageUserDO validateBrokerageUserExists(Long id) { + BrokerageUserDO brokerageUserDO = brokerageUserMapper.selectById(id); + if (brokerageUserDO == null) { + throw exception(BROKERAGE_USER_NOT_EXISTS); + } + return brokerageUserDO; + } + + @Override + public BrokerageUserDO getBindBrokerageUser(Long id) { + return Optional.ofNullable(id) + .map(this::getBrokerageUser) + .map(BrokerageUserDO::getBindUserId) + .map(this::getBrokerageUser) + .orElse(null); + } + + @Override + public BrokerageUserDO getOrCreateBrokerageUser(Long id) { + BrokerageUserDO brokerageUser = brokerageUserMapper.selectById(id); + // 特殊:人人分销的情况下,如果分销人为空则创建分销人 + if (brokerageUser == null && ObjUtil.equal(BrokerageEnabledConditionEnum.ALL.getCondition(), + tradeConfigService.getTradeConfig().getBrokerageEnabledCondition())) { + brokerageUser = new BrokerageUserDO().setId(id).setBrokerageEnabled(true).setBrokeragePrice(0) + .setBrokerageTime(LocalDateTime.now()).setFrozenPrice(0); + brokerageUserMapper.insert(brokerageUser); + } + return brokerageUser; + } + + @Override + public boolean updateUserPrice(Long id, Integer price) { + if (price > 0) { + brokerageUserMapper.updatePriceIncr(id, price); + } else if (price < 0) { + return brokerageUserMapper.updatePriceDecr(id, price) > 0; + } + return true; + } + + @Override + public void updateUserFrozenPrice(Long id, Integer frozenPrice) { + if (frozenPrice > 0) { + brokerageUserMapper.updateFrozenPriceIncr(id, frozenPrice); + } else if (frozenPrice < 0) { + brokerageUserMapper.updateFrozenPriceDecr(id, frozenPrice); + } + } + + @Override + public void updateFrozenPriceDecrAndPriceIncr(Long id, Integer frozenPrice) { + Assert.isTrue(frozenPrice < 0); + int updateRows = brokerageUserMapper.updateFrozenPriceDecrAndPriceIncr(id, frozenPrice); + if (updateRows == 0) { + throw exception(BROKERAGE_USER_FROZEN_PRICE_NOT_ENOUGH); + } + } + + @Override + public Long getBrokerageUserCountByBindUserId(Long bindUserId, Integer level) { + List childIds = getChildUserIdsByLevel(bindUserId, level); + return (long) CollUtil.size(childIds); + } + + @Override + public boolean bindBrokerageUser(Long userId, Long bindUserId) { + // 1. 获得分销用户 + boolean isNewBrokerageUser = false; + BrokerageUserDO brokerageUser = brokerageUserMapper.selectById(userId); + if (brokerageUser == null) { // 分销用户不存在的情况:1. 新注册;2. 旧数据;3. 分销功能关闭后又打开 + isNewBrokerageUser = true; + brokerageUser = new BrokerageUserDO().setId(userId).setBrokerageEnabled(false).setBrokeragePrice(0).setFrozenPrice(0); + } + + // 2.1 校验是否能绑定用户 + boolean validated = isUserCanBind(brokerageUser); + if (!validated) { + return false; + } + // 2.3 校验能否绑定 + validateCanBindUser(brokerageUser, bindUserId); + // 2.3 绑定用户 + if (isNewBrokerageUser) { + Integer enabledCondition = tradeConfigService.getTradeConfig().getBrokerageEnabledCondition(); + if (BrokerageEnabledConditionEnum.ALL.getCondition().equals(enabledCondition)) { // 人人分销:用户默认就有分销资格 + brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now()); + } else { + brokerageUser.setBrokerageEnabled(false).setBrokerageTime(LocalDateTime.now()); + } + brokerageUserMapper.insert(fillBindUserData(bindUserId, brokerageUser)); + } else { + brokerageUserMapper.updateById(fillBindUserData(bindUserId, new BrokerageUserDO().setId(userId))); + } + return true; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createBrokerageUser(BrokerageUserCreateReqVO createReqVO) { + // 1.1 校验分销用户是否已存在 + BrokerageUserDO brokerageUser = brokerageUserMapper.selectById(createReqVO.getUserId()); + if (brokerageUser != null) { + throw exception(BROKERAGE_CREATE_USER_EXISTS); + } + // 1.2 校验是否能绑定用户 + brokerageUser = BeanUtils.toBean(createReqVO, BrokerageUserDO.class).setId(createReqVO.getUserId()) + .setBrokerageTime(LocalDateTime.now()); + validateCanBindUser(brokerageUser, createReqVO.getBindUserId()); + + // 2. 创建分销人 + brokerageUserMapper.insert(brokerageUser); + return brokerageUser.getId(); + } + + /** + * 补全绑定用户的字段 + * + * @param bindUserId 绑定的用户编号 + * @param brokerageUser update 对象 + * @return 补全后的 update 对象 + */ + private BrokerageUserDO fillBindUserData(Long bindUserId, BrokerageUserDO brokerageUser) { + return brokerageUser.setBindUserId(bindUserId).setBindUserTime(LocalDateTime.now()); + } + + @Override + public Boolean getUserBrokerageEnabled(Long userId) { + // 全局分销功能是否开启 + TradeConfigDO tradeConfig = tradeConfigService.getTradeConfig(); + if (tradeConfig == null || BooleanUtil.isFalse(tradeConfig.getBrokerageEnabled())) { + return false; + } + + // 用户是否有分销资格 + return Optional.ofNullable(getBrokerageUser(userId)) + .map(BrokerageUserDO::getBrokerageEnabled) + .orElse(false); + } + + @Override + public PageResult getBrokerageUserRankPageByUserCount(AppBrokerageUserRankPageReqVO pageReqVO) { + IPage pageResult = brokerageUserMapper.selectCountPageGroupByBindUserId(MyBatisUtils.buildPage(pageReqVO), + ArrayUtil.get(pageReqVO.getTimes(), 0), ArrayUtil.get(pageReqVO.getTimes(), 1)); + return new PageResult<>(pageResult.getRecords(), pageResult.getTotal()); + } + + @Override + public PageResult getBrokerageUserChildSummaryPage(AppBrokerageUserChildSummaryPageReqVO pageReqVO, Long userId) { + // 1.1 查询下级用户编号列表 + List childIds = getChildUserIdsByLevel(userId, pageReqVO.getLevel()); + if (CollUtil.isEmpty(childIds)) { + return PageResult.empty(); + } + // 1.2 根据昵称过滤下级用户 + List users = memberUserApi.getUserList(childIds).getCheckedData(); + Map userMap = convertMapByFilter(users, + user -> StrUtil.contains(user.getNickname(), pageReqVO.getNickname()), + MemberUserRespDTO::getId); + if (CollUtil.isEmpty(userMap)) { + return PageResult.empty(); + } + + // 2. 分页查询 + IPage pageResult = brokerageUserMapper.selectSummaryPageByUserId( + MyBatisUtils.buildPage(pageReqVO), BrokerageRecordBizTypeEnum.ORDER.getType(), + BrokerageRecordStatusEnum.SETTLEMENT.getStatus(), userMap.keySet(), pageReqVO.getSortingField() + ); + + // 3. 拼接数据并返回 + BrokerageUserConvert.INSTANCE.copyTo(pageResult.getRecords(), userMap); + return new PageResult<>(pageResult.getRecords(), pageResult.getTotal()); + } + + private boolean isUserCanBind(BrokerageUserDO user) { + // 校验分销功能是否启用 + TradeConfigDO tradeConfig = tradeConfigService.getTradeConfig(); + if (tradeConfig == null || !BooleanUtil.isTrue(tradeConfig.getBrokerageEnabled())) { + return false; + } + + // 校验分销关系绑定模式 + if (BrokerageBindModeEnum.REGISTER.getMode().equals(tradeConfig.getBrokerageBindMode())) { + // 判断是否为新用户:注册时间在 30 秒内的,都算新用户 + if (!isNewRegisterUser(user.getId())) { + throw exception(BROKERAGE_BIND_MODE_REGISTER); // 只有在注册时可以绑定 + } + } else if (BrokerageBindModeEnum.ANYTIME.getMode().equals(tradeConfig.getBrokerageBindMode())) { + if (user.getBindUserId() != null) { + throw exception(BROKERAGE_BIND_OVERRIDE); // 已绑定了推广人 + } + } + return true; + } + + /** + * 判断是否为新用户 + *

+ * 标准:注册时间在 30 秒内的,都算新用户 + *

+ * 疑问:为什么通过这样的方式实现? + * 回答:因为注册在 member 模块,希望它和 trade 模块解耦,所以只能用这种约定的逻辑。 + * + * @param userId 用户编号 + * @return 是否新用户 + */ + private boolean isNewRegisterUser(Long userId) { + MemberUserRespDTO user = memberUserApi.getUser(userId).getCheckedData(); + return user != null && LocalDateTimeUtils.afterNow(user.getCreateTime().plusSeconds(30)); + } + + private void validateCanBindUser(BrokerageUserDO user, Long bindUserId) { + // 1.1 校验推广人是否存在 + MemberUserRespDTO bindUserInfo = memberUserApi.getUser(bindUserId).getCheckedData(); + if (bindUserInfo == null) { + throw exception(BROKERAGE_USER_NOT_EXISTS); + } + // 1.2 校验要绑定的用户有无推广资格 + BrokerageUserDO bindUser = getOrCreateBrokerageUser(bindUserId); + if (bindUser == null || BooleanUtil.isFalse(bindUser.getBrokerageEnabled())) { + throw exception(BROKERAGE_BIND_USER_NOT_ENABLED); + } + + // 2. 校验绑定自己 + if (Objects.equals(user.getId(), bindUserId)) { + throw exception(BROKERAGE_BIND_SELF); + } + + // 3. 下级不能绑定自己的上级 + for (int i = 0; i <= Short.MAX_VALUE; i++) { + if (Objects.equals(bindUser.getBindUserId(), user.getId())) { + throw exception(BROKERAGE_BIND_LOOP); + } + bindUser = getBrokerageUser(bindUser.getBindUserId()); + // 找到根节点,结束循环 + if (bindUser == null || bindUser.getBindUserId() == null) { + break; + } + } + } + + /** + * 根据绑定用户编号,获得下级用户编号列表 + * + * @param bindUserId 绑定用户编号 + * @param level 下级用户的层级。 + * 如果 level 为空,则查询 1+2 两个层级 + * @return 下级用户编号列表 + */ + private List getChildUserIdsByLevel(Long bindUserId, Integer level) { + if (bindUserId == null) { + return Collections.emptyList(); + } + // 先查第 1 级 + List bindUserIds = brokerageUserMapper.selectIdListByBindUserIdIn(Collections.singleton(bindUserId)); + if (CollUtil.isEmpty(bindUserIds)) { + return Collections.emptyList(); + } + + // 情况一:level 为空,查询所有级别 + if (level == null) { + // 再查第 2 级,并合并结果 + bindUserIds.addAll(brokerageUserMapper.selectIdListByBindUserIdIn(bindUserIds)); + return bindUserIds; + } + // 情况二:level 为 1,只查询第 1 级 + if (level == 1) { + return bindUserIds; + } + // 情况三:level 为 1,只查询第 2 级 + if (level == 2) { + return brokerageUserMapper.selectIdListByBindUserIdIn(bindUserIds); + } + throw exception(BROKERAGE_USER_LEVEL_NOT_SUPPORT); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageWithdrawService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageWithdrawService.java new file mode 100644 index 0000000..9e0ab63 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageWithdrawService.java @@ -0,0 +1,91 @@ +package com.tashow.cloud.trade.service.brokerage; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageWithdrawDO; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; +import com.tashow.cloud.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 佣金提现 Service 接口 + * + * @author 芋道源码 + */ +public interface BrokerageWithdrawService { + + /** + * 【管理员】审核佣金提现 + * + * @param id 佣金编号 + * @param status 审核状态 + * @param auditReason 驳回原因 + * @param userIp 操作 IP + */ + void auditBrokerageWithdraw(Long id, BrokerageWithdrawStatusEnum status, String auditReason, String userIp); + + /** + * 获得佣金提现 + * + * @param id 编号 + * @return 佣金提现 + */ + BrokerageWithdrawDO getBrokerageWithdraw(Long id); + + /** + * 获得佣金提现分页 + * + * @param pageReqVO 分页查询 + * @return 佣金提现分页 + */ + PageResult getBrokerageWithdrawPage(BrokerageWithdrawPageReqVO pageReqVO); + + /** + * 【会员】创建佣金提现 + * + * @param userId 会员用户编号 + * @param createReqVO 创建信息 + * @return 佣金提现编号 + */ + Long createBrokerageWithdraw(Long userId, AppBrokerageWithdrawCreateReqVO createReqVO); + + /** + * 【API】更新佣金提现的转账结果 + * + * 目前用于支付回调,标记提现转账结果 + * + * @param id 提现编号 + * @param payTransferId 转账订单编号 + */ + void updateBrokerageWithdrawTransferred(Long id, Long payTransferId); + + /** + * 按照 userId,汇总每个用户的提现 + * + * @param userIds 用户编号 + * @param status 提现状态 + * @return 用户提现汇总 List + */ + List getWithdrawSummaryListByUserId(Collection userIds, + Collection status); + + /** + * 按照 userId,汇总每个用户的提现 + * + * @param userIds 用户编号 + * @param status 提现状态 + * @return 用户提现汇总 Map + */ + default Map getWithdrawSummaryMapByUserId(Set userIds, + Collection status) { + return convertMap(getWithdrawSummaryListByUserId(userIds, status), BrokerageWithdrawSummaryRespBO::getUserId); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageWithdrawServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageWithdrawServiceImpl.java new file mode 100644 index 0000000..520692b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/BrokerageWithdrawServiceImpl.java @@ -0,0 +1,250 @@ +package com.tashow.cloud.trade.service.brokerage; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.module.pay.api.transfer.PayTransferApi; +import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO; +import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO; +import cn.iocoder.yudao.module.pay.api.wallet.PayWalletApi; +import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO; +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum; +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum; +import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.social.SocialUserApi; +import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO; +import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; +import com.tashow.cloud.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO; +import com.tashow.cloud.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO; +import com.tashow.cloud.trade.convert.brokerage.BrokerageWithdrawConvert; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageWithdrawDO; +import com.tashow.cloud.trade.dal.dataobject.config.TradeConfigDO; +import com.tashow.cloud.trade.dal.mysql.brokerage.BrokerageWithdrawMapper; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum; +import com.tashow.cloud.trade.framework.order.config.TradeOrderProperties; +import com.tashow.cloud.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO; +import com.tashow.cloud.trade.service.config.TradeConfigService; +import jakarta.annotation.Resource; +import jakarta.validation.Validator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; + +/** + * 佣金提现 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService { + + @Resource + private BrokerageWithdrawMapper brokerageWithdrawMapper; + + @Resource + private BrokerageRecordService brokerageRecordService; + @Resource + private TradeConfigService tradeConfigService; + + @Resource + private NotifyMessageSendApi notifyMessageSendApi; + @Resource + private PayTransferApi payTransferApi; + @Resource + private SocialUserApi socialUserApi; + @Resource + private PayWalletApi payWalletApi; + + @Resource + private Validator validator; + + @Resource + private TradeOrderProperties tradeOrderProperties; + + @Override + @Transactional(rollbackFor = Exception.class) + public void auditBrokerageWithdraw(Long id, BrokerageWithdrawStatusEnum status, String auditReason, String userIp) { + // 1.1 校验存在 + BrokerageWithdrawDO withdraw = validateBrokerageWithdrawExists(id); + // 1.2 校验状态为审核中 + if (ObjectUtil.notEqual(BrokerageWithdrawStatusEnum.AUDITING.getStatus(), withdraw.getStatus())) { + throw exception(BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING); + } + + // 2. 更新状态 + int rows = brokerageWithdrawMapper.updateByIdAndStatus(id, BrokerageWithdrawStatusEnum.AUDITING.getStatus(), + new BrokerageWithdrawDO().setStatus(status.getStatus()).setAuditReason(auditReason).setAuditTime(LocalDateTime.now())); + if (rows == 0) { + throw exception(BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING); + } + + // 3.1 审批通过的后续处理 + if (BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.equals(status)) { + auditBrokerageWithdrawSuccess(withdraw); + // 3.2 审批不通过的后续处理 + } else if (BrokerageWithdrawStatusEnum.AUDIT_FAIL.equals(status)) { + brokerageRecordService.addBrokerage(withdraw.getUserId(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT, + String.valueOf(withdraw.getId()), withdraw.getPrice(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT.getTitle()); + } else { + throw new IllegalArgumentException("不支持的提现状态:" + status); + } + } + + private void auditBrokerageWithdrawSuccess(BrokerageWithdrawDO withdraw) { + // 1.1 钱包 + if (BrokerageWithdrawTypeEnum.WALLET.getType().equals(withdraw.getType())) { + payWalletApi.addWalletBalance(new PayWalletAddBalanceReqDTO() + .setUserId(withdraw.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue()) + .setBizType(PayWalletBizTypeEnum.BROKERAGE_WITHDRAW.getType()).setBizId(withdraw.getId().toString()) + .setPrice(withdraw.getPrice())).checkError(); + // 1.2 微信 API + } else if (BrokerageWithdrawTypeEnum.WECHAT_API.getType().equals(withdraw.getType())) { + // TODO @luchi:这里,要加个转账单号的记录;另外,调用 API 转账,是立马成功,还是有延迟的哈? + Long payTransferId = createPayTransfer(withdraw); + // 1.3 剩余类型,都是手动打款,所以不处理 + } else { + // TODO 可优化:未来可以考虑,接入支付宝、银联等 API 转账,实现自动打款 + log.info("[auditBrokerageWithdrawSuccess][withdraw({}) 类型({}) 手动打款,无需处理]", withdraw.getId(), withdraw.getType()); + } + + // 2. 非支付 API,则直接体现成功 + if (!BrokerageWithdrawTypeEnum.isApi(withdraw.getType())) { + brokerageWithdrawMapper.updateByIdAndStatus(withdraw.getId(), BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.getStatus(), + new BrokerageWithdrawDO().setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus())); + } + } + + private Long createPayTransfer(BrokerageWithdrawDO withdraw) { + // 1.1 获取微信 openid + SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId( + UserTypeEnum.MEMBER.getValue(), withdraw.getUserId(), SocialTypeEnum.WECHAT_MINI_APP.getType()).getCheckedData(); + // TODO @luchi:这里,需要校验非空。如果空的话,要有业务异常哈; + // 1.2 构建请求 + PayTransferCreateReqDTO payTransferCreateReqDTO = new PayTransferCreateReqDTO() + .setAppKey(tradeOrderProperties.getPayAppKey()) + .setChannelCode("wx_lite").setType(PayTransferTypeEnum.WX_BALANCE.getType()) + .setMerchantTransferId(withdraw.getId().toString()) + .setPrice(withdraw.getPrice()) + .setSubject("佣金提现") + .setOpenid(socialUser.getOpenid()).setUserIp(getClientIP()); + // 2. 发起请求 + return payTransferApi.createTransfer(payTransferCreateReqDTO).getCheckedData(); + } + + private BrokerageWithdrawDO validateBrokerageWithdrawExists(Long id) { + BrokerageWithdrawDO withdraw = brokerageWithdrawMapper.selectById(id); + if (withdraw == null) { + throw exception(BROKERAGE_WITHDRAW_NOT_EXISTS); + } + return withdraw; + } + + @Override + public BrokerageWithdrawDO getBrokerageWithdraw(Long id) { + return brokerageWithdrawMapper.selectById(id); + } + + @Override + public PageResult getBrokerageWithdrawPage(BrokerageWithdrawPageReqVO pageReqVO) { + return brokerageWithdrawMapper.selectPage(pageReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createBrokerageWithdraw(Long userId, AppBrokerageWithdrawCreateReqVO createReqVO) { + // 1.1 校验提现金额 + TradeConfigDO tradeConfig = validateWithdrawPrice(createReqVO.getPrice()); + // 1.2 校验提现参数 + createReqVO.validate(validator); + + // 2.1 计算手续费 + Integer feePrice = calculateFeePrice(createReqVO.getPrice(), tradeConfig.getBrokerageWithdrawFeePercent()); + // 2.2 创建佣金提现记录 + BrokerageWithdrawDO withdraw = BrokerageWithdrawConvert.INSTANCE.convert(createReqVO, userId, feePrice); + brokerageWithdrawMapper.insert(withdraw); + + // 3. 创建用户佣金记录 + // 注意,佣金是否充足,reduceBrokerage 已经进行校验 + brokerageRecordService.reduceBrokerage(userId, BrokerageRecordBizTypeEnum.WITHDRAW, String.valueOf(withdraw.getId()), + createReqVO.getPrice(), BrokerageRecordBizTypeEnum.WITHDRAW.getTitle()); + return withdraw.getId(); + } + + /** + * 计算提现手续费 + * + * @param withdrawPrice 提现金额 + * @param percent 手续费百分比 + * @return 提现手续费 + */ + private Integer calculateFeePrice(Integer withdrawPrice, Integer percent) { + Integer feePrice = 0; + if (percent != null && percent > 0) { + feePrice = MoneyUtils.calculateRatePrice(withdrawPrice, Double.valueOf(percent)); + } + return feePrice; + } + + /** + * 校验提现金额要求 + * + * @param withdrawPrice 提现金额 + * @return 分销配置 + */ + private TradeConfigDO validateWithdrawPrice(Integer withdrawPrice) { + TradeConfigDO tradeConfig = tradeConfigService.getTradeConfig(); + if (tradeConfig.getBrokerageWithdrawMinPrice() != null && withdrawPrice < tradeConfig.getBrokerageWithdrawMinPrice()) { + throw exception(BROKERAGE_WITHDRAW_MIN_PRICE, MoneyUtils.fenToYuanStr(tradeConfig.getBrokerageWithdrawMinPrice())); + } + return tradeConfig; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateBrokerageWithdrawTransferred(Long id, Long payTransferId) { + BrokerageWithdrawDO withdraw = validateBrokerageWithdrawExists(id); + PayTransferRespDTO transfer = payTransferApi.getTransfer(payTransferId).getCheckedData(); + // TODO @luchi:建议参考支付那,即使成功的情况下,也要各种校验;金额是否匹配、转账单号是否匹配、是否重复调用; + if (PayTransferStatusEnum.isSuccess(transfer.getStatus())) { + withdraw.setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus()); + // TODO @luchi:发送站内信 + } else if (PayTransferStatusEnum.isPendingStatus(transfer.getStatus())) { + // TODO @luchi:这里,是不是不用更新哈? + withdraw.setStatus(BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.getStatus()); + } else { + withdraw.setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_FAIL.getStatus()); + // 3.2 驳回时需要退还用户佣金 + brokerageRecordService.addBrokerage(withdraw.getUserId(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT, + String.valueOf(withdraw.getId()), withdraw.getPrice(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT.getTitle()); + } + brokerageWithdrawMapper.updateById(withdraw); + } + + @Override + public List getWithdrawSummaryListByUserId(Collection userIds, + Collection statuses) { + if (CollUtil.isEmpty(userIds) || CollUtil.isEmpty(statuses)) { + return Collections.emptyList(); + } + return brokerageWithdrawMapper.selectCountAndSumPriceByUserIdAndStatus(userIds, + convertSet(statuses, BrokerageWithdrawStatusEnum::getStatus)); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/bo/BrokerageAddReqBO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/bo/BrokerageAddReqBO.java new file mode 100644 index 0000000..a7b78f4 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/bo/BrokerageAddReqBO.java @@ -0,0 +1,53 @@ +package com.tashow.cloud.trade.service.brokerage.bo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * 佣金 增加 Request BO + * + * @author owen + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BrokerageAddReqBO { + + /** + * 业务编号 + */ + @NotBlank(message = "业务编号不能为空") + private String bizId; + /** + * 佣金基数 + */ + @NotNull(message = "佣金基数不能为空") + private Integer basePrice; + /** + * 一级佣金(固定) + */ + @NotNull(message = "一级佣金(固定)不能为空") + private Integer firstFixedPrice; + /** + * 二级佣金(固定) + */ + private Integer secondFixedPrice; + + /** + * 来源用户编号 + */ + @NotNull(message = "来源用户编号不能为空") + private Long sourceUserId; + + /** + * 佣金记录标题 + */ + @NotEmpty(message = "佣金记录标题不能为空") + private String title; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/bo/BrokerageWithdrawSummaryRespBO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/bo/BrokerageWithdrawSummaryRespBO.java new file mode 100644 index 0000000..2356a06 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/bo/BrokerageWithdrawSummaryRespBO.java @@ -0,0 +1,31 @@ +package com.tashow.cloud.trade.service.brokerage.bo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 佣金提现合计 BO + * + * @author owen + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BrokerageWithdrawSummaryRespBO { + + /** + * 用户编号 + */ + private Long userId; + + /** + * 提现次数 + */ + private Integer count; + /** + * 提现金额 + */ + private Integer price; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/bo/UserBrokerageSummaryRespBO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/bo/UserBrokerageSummaryRespBO.java new file mode 100644 index 0000000..b313b92 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/brokerage/bo/UserBrokerageSummaryRespBO.java @@ -0,0 +1,30 @@ +package com.tashow.cloud.trade.service.brokerage.bo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 用户佣金合计 BO + * + * @author owen + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserBrokerageSummaryRespBO { + + /** + * 用户编号 + */ + private Long userId; + /** + * 推广数量 + */ + private Integer count; + /** + * 佣金总额 + */ + private Integer price; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/cart/CartService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/cart/CartService.java new file mode 100644 index 0000000..a1ff188 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/cart/CartService.java @@ -0,0 +1,87 @@ +package com.tashow.cloud.trade.service.cart; + +import cn.iocoder.yudao.module.trade.controller.app.cart.vo.*; +import com.tashow.cloud.trade.controller.app.cart.vo.*; +import com.tashow.cloud.trade.dal.dataobject.cart.CartDO; + +import jakarta.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * 购物车 Service 接口 + * + * @author 芋道源码 + */ +public interface CartService { + + /** + * 添加商品到购物车 + * + * @param userId 用户编号 + * @param addReqVO 添加信息 + * @return 购物项的编号 + */ + Long addCart(Long userId, @Valid AppCartAddReqVO addReqVO); + + /** + * 更新购物车商品数量 + * + * @param userId 用户编号 + * @param updateCountReqVO 更新信息 + */ + void updateCartCount(Long userId, AppCartUpdateCountReqVO updateCountReqVO); + + /** + * 更新购物车选中状态 + * + * @param userId 用户编号 + * @param updateSelectedReqVO 更新信息 + */ + void updateCartSelected(Long userId, @Valid AppCartUpdateSelectedReqVO updateSelectedReqVO); + + /** + * 重置购物车商品 + * + * 使用场景:在一个购物车项对应的商品失效(例如说 SPU 被下架),可以重新选择对应的 SKU + * + * @param userId 用户编号 + * @param updateReqVO 重置信息 + */ + void resetCart(Long userId, AppCartResetReqVO updateReqVO); + + /** + * 删除购物车商品 + * + * @param userId 用户编号 + * @param ids 购物项的编号 + */ + void deleteCart(Long userId, Collection ids); + + /** + * 查询用户在购物车中的商品数量 + * + * @param userId 用户编号 + * @return 商品数量 + */ + Integer getCartCount(Long userId); + + /** + * 查询用户的购物车列表 + * + * @param userId 用户编号 + * @return 购物车列表 + */ + AppCartListRespVO getCartList(Long userId); + + /** + * 查询用户的购物车列表 + * + * @param userId 用户编号 + * @param ids 购物项的编号 + * @return 购物车列表 + */ + List getCartList(Long userId, Set ids); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/cart/CartServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/cart/CartServiceImpl.java new file mode 100644 index 0000000..2fa30a3 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/cart/CartServiceImpl.java @@ -0,0 +1,197 @@ +package com.tashow.cloud.trade.service.cart; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; +import cn.iocoder.yudao.module.trade.controller.app.cart.vo.*; +import com.tashow.cloud.trade.controller.app.cart.vo.*; +import com.tashow.cloud.trade.convert.cart.TradeCartConvert; +import com.tashow.cloud.trade.dal.dataobject.cart.CartDO; +import com.tashow.cloud.trade.dal.mysql.cart.CartMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.Resource; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS; +import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.CARD_ITEM_NOT_FOUND; +import static java.util.Collections.emptyList; + +/** + * 购物车 Service 实现类 + * + * // TODO 芋艿:未来优化:购物车的价格计算,支持营销信息;目前不支持的原因,前端界面需要前端 pr 支持下;例如说:会员价格; + * + * @author 芋道源码 + */ +@Service +@Validated +public class CartServiceImpl implements CartService { + + @Resource + private CartMapper cartMapper; + + @Resource + private ProductSpuApi productSpuApi; + @Resource + private ProductSkuApi productSkuApi; + + @Override + public Long addCart(Long userId, AppCartAddReqVO addReqVO) { + // 查询 TradeCartDO + CartDO cart = cartMapper.selectByUserIdAndSkuId(userId, addReqVO.getSkuId()); + // 校验 SKU + Integer count = addReqVO.getCount(); + ProductSkuRespDTO sku = checkProductSku(addReqVO.getSkuId(), count); + + // 情况一:存在,则进行数量更新 + if (cart != null) { + cartMapper.updateById(new CartDO().setId(cart.getId()).setSelected(true) + .setCount(cart.getCount() + count)); + return cart.getId(); + // 情况二:不存在,则进行插入 + } else { + cart = new CartDO().setUserId(userId).setSelected(true) + .setSpuId(sku.getSpuId()).setSkuId(sku.getId()).setCount(count); + cartMapper.insert(cart); + } + return cart.getId(); + } + + @Override + public void updateCartCount(Long userId, AppCartUpdateCountReqVO updateReqVO) { + // 校验 TradeCartDO 存在 + CartDO cart = cartMapper.selectById(updateReqVO.getId(), userId); + if (cart == null) { + throw exception(CARD_ITEM_NOT_FOUND); + } + // 校验商品 SKU + checkProductSku(cart.getSkuId(), updateReqVO.getCount()); + + // 更新数量 + cartMapper.updateById(new CartDO().setId(cart.getId()) + .setCount(updateReqVO.getCount())); + } + + @Override + public void updateCartSelected(Long userId, AppCartUpdateSelectedReqVO updateSelectedReqVO) { + cartMapper.updateByIds(updateSelectedReqVO.getIds(), userId, + new CartDO().setSelected(updateSelectedReqVO.getSelected())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void resetCart(Long userId, AppCartResetReqVO resetReqVO) { + // 第一步:删除原本的购物项 + CartDO oldCart = cartMapper.selectById(resetReqVO.getId(), userId); + if (oldCart == null) { + throw exception(CARD_ITEM_NOT_FOUND); + } + cartMapper.deleteById(oldCart.getId()); + + // 第二步:添加新的购物项 + CartDO newCart = cartMapper.selectByUserIdAndSkuId(userId, resetReqVO.getSkuId()); + if (newCart != null) { + updateCartCount(userId, new AppCartUpdateCountReqVO() + .setId(newCart.getId()).setCount(resetReqVO.getCount())); + } else { + addCart(userId, new AppCartAddReqVO().setSkuId(resetReqVO.getSkuId()) + .setCount(resetReqVO.getCount())); + } + } + + /** + * 购物车删除商品 + * + * @param userId 用户编号 + * @param ids 商品 SKU 编号的数组 + */ + @Override + public void deleteCart(Long userId, Collection ids) { + // 查询 TradeCartDO 列表 + List carts = cartMapper.selectListByIds(ids, userId); + if (CollUtil.isEmpty(carts)) { + return; + } + + // 批量标记删除 + cartMapper.deleteByIds(convertSet(carts, CartDO::getId)); + } + + @Override + public Integer getCartCount(Long userId) { + // TODO 芋艿:需要算上 selected + return cartMapper.selectSumByUserId(userId); + } + + @Override + public AppCartListRespVO getCartList(Long userId) { + // 获得购物车的商品 + List carts = cartMapper.selectListByUserId(userId); + carts.sort(Comparator.comparing(CartDO::getId).reversed()); + // 如果未空,则返回空结果 + if (CollUtil.isEmpty(carts)) { + return new AppCartListRespVO().setValidList(emptyList()) + .setInvalidList(emptyList()); + } + + // 查询 SPU、SKU 列表 + List spus = productSpuApi.getSpuList(convertSet(carts, CartDO::getSpuId)).getCheckedData(); + List skus = productSkuApi.getSkuList(convertSet(carts, CartDO::getSkuId)).getCheckedData(); + + // 如果 SPU 被删除,则删除购物车对应的商品。延迟删除 + // 为什么不是 SKU 被删除呢?因为 SKU 被删除时,还可以通过 SPU 选择其它 SKU + deleteCartIfSpuDeleted(carts, spus); + + // 拼接数据 + return TradeCartConvert.INSTANCE.convertList(carts, spus, skus); + } + + @Override + public List getCartList(Long userId, Set ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return cartMapper.selectListByUserId(userId, ids); + } + + private void deleteCartIfSpuDeleted(List carts, List spus) { + // 如果 SPU 被删除,则删除购物车对应的商品。延迟删除 + carts.removeIf(cart -> { + if (spus.stream().noneMatch(spu -> spu.getId().equals(cart.getSpuId()))) { + cartMapper.deleteById(cart.getId()); + return true; + } + return false; + }); + } + + /** + * 校验商品 SKU 是否合法 + * 1. 是否存在 + * 2. 是否下架 + * 3. 库存不足 + * + * @param skuId 商品 SKU 编号 + * @param count 商品数量 + * @return 商品 SKU + */ + private ProductSkuRespDTO checkProductSku(Long skuId, Integer count) { + ProductSkuRespDTO sku = productSkuApi.getSku(skuId).getCheckedData(); + if (sku == null) { + throw exception(SKU_NOT_EXISTS); + } + if (count > sku.getStock()) { + throw exception(SKU_STOCK_NOT_ENOUGH); + } + return sku; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/config/TradeConfigService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/config/TradeConfigService.java new file mode 100644 index 0000000..a2b53c2 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/config/TradeConfigService.java @@ -0,0 +1,29 @@ +package com.tashow.cloud.trade.service.config; + +import com.tashow.cloud.trade.controller.admin.config.vo.TradeConfigSaveReqVO; +import com.tashow.cloud.trade.dal.dataobject.config.TradeConfigDO; + +import jakarta.validation.Valid; + +/** + * 交易中心配置 Service 接口 + * + * @author owen + */ +public interface TradeConfigService { + + /** + * 更新交易中心配置 + * + * @param updateReqVO 更新信息 + */ + void saveTradeConfig(@Valid TradeConfigSaveReqVO updateReqVO); + + /** + * 获得交易中心配置 + * + * @return 交易中心配置 + */ + TradeConfigDO getTradeConfig(); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/config/TradeConfigServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/config/TradeConfigServiceImpl.java new file mode 100644 index 0000000..b9e2951 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/config/TradeConfigServiceImpl.java @@ -0,0 +1,44 @@ +package com.tashow.cloud.trade.service.config; + +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import com.tashow.cloud.trade.controller.admin.config.vo.TradeConfigSaveReqVO; +import com.tashow.cloud.trade.convert.config.TradeConfigConvert; +import com.tashow.cloud.trade.dal.dataobject.config.TradeConfigDO; +import com.tashow.cloud.trade.dal.mysql.config.TradeConfigMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.Resource; +import java.util.List; + +/** + * 交易中心配置 Service 实现类 + * + * @author owen + */ +@Service +@Validated +public class TradeConfigServiceImpl implements TradeConfigService { + + @Resource + private TradeConfigMapper tradeConfigMapper; + + @Override + public void saveTradeConfig(TradeConfigSaveReqVO saveReqVO) { + // 存在,则进行更新 + TradeConfigDO dbConfig = getTradeConfig(); + if (dbConfig != null) { + tradeConfigMapper.updateById(TradeConfigConvert.INSTANCE.convert(saveReqVO).setId(dbConfig.getId())); + return; + } + // 不存在,则进行插入 + tradeConfigMapper.insert(TradeConfigConvert.INSTANCE.convert(saveReqVO)); + } + + @Override + public TradeConfigDO getTradeConfig() { + List list = tradeConfigMapper.selectList(); + return CollectionUtils.getFirst(list); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressService.java new file mode 100644 index 0000000..aded2b3 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressService.java @@ -0,0 +1,82 @@ +package com.tashow.cloud.trade.service.delivery; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.DeliveryExpressCreateReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.DeliveryExpressExportReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.DeliveryExpressPageReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.DeliveryExpressUpdateReqVO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; + +import jakarta.validation.Valid; +import java.util.List; + +/** + * 快递公司 Service 接口 + * + * @author jason + */ +public interface DeliveryExpressService { + + /** + * 创建快递公司 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDeliveryExpress(@Valid DeliveryExpressCreateReqVO createReqVO); + + /** + * 更新快递公司 + * + * @param updateReqVO 更新信息 + */ + void updateDeliveryExpress(@Valid DeliveryExpressUpdateReqVO updateReqVO); + + /** + * 删除快递公司 + * + * @param id 编号 + */ + void deleteDeliveryExpress(Long id); + + /** + * 获得快递公司 + * + * @param id 编号 + * @return 快递公司 + */ + DeliveryExpressDO getDeliveryExpress(Long id); + + /** + * 校验快递公司是否合法 + * + * @param id 编号 + * @return 快递公司 + */ + DeliveryExpressDO validateDeliveryExpress(Long id); + + /** + * 获得快递公司分页 + * + * @param pageReqVO 分页查询 + * @return 快递公司分页 + */ + PageResult getDeliveryExpressPage(DeliveryExpressPageReqVO pageReqVO); + + /** + * 获得快递公司列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 快递公司列表 + */ + List getDeliveryExpressList(DeliveryExpressExportReqVO exportReqVO); + + /** + * 获取指定状态的快递公司列表 + * + * @param status 状态 + * @return 快递公司列表 + */ + List getDeliveryExpressListByStatus(Integer status); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressServiceImpl.java new file mode 100644 index 0000000..9eb7e37 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressServiceImpl.java @@ -0,0 +1,113 @@ +package com.tashow.cloud.trade.service.delivery; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.DeliveryExpressCreateReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.DeliveryExpressExportReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.DeliveryExpressPageReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.express.DeliveryExpressUpdateReqVO; +import com.tashow.cloud.trade.convert.delivery.DeliveryExpressConvert; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.tashow.cloud.trade.dal.mysql.delivery.DeliveryExpressMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * 快递公司 Service 实现类 + * + * @author jason + */ +@Service +@Validated +public class DeliveryExpressServiceImpl implements DeliveryExpressService { + + @Resource + private DeliveryExpressMapper deliveryExpressMapper; + + @Override + public Long createDeliveryExpress(DeliveryExpressCreateReqVO createReqVO) { + //校验编码是否唯一 + validateExpressCodeUnique(createReqVO.getCode(), null); + // 插入 + DeliveryExpressDO deliveryExpress = DeliveryExpressConvert.INSTANCE.convert(createReqVO); + deliveryExpressMapper.insert(deliveryExpress); + // 返回 + return deliveryExpress.getId(); + } + + @Override + public void updateDeliveryExpress(DeliveryExpressUpdateReqVO updateReqVO) { + // 校验存在 + validateDeliveryExpressExists(updateReqVO.getId()); + //校验编码是否唯一 + validateExpressCodeUnique(updateReqVO.getCode(), updateReqVO.getId()); + // 更新 + DeliveryExpressDO updateObj = DeliveryExpressConvert.INSTANCE.convert(updateReqVO); + deliveryExpressMapper.updateById(updateObj); + } + + @Override + public void deleteDeliveryExpress(Long id) { + // 校验存在 + validateDeliveryExpressExists(id); + // 删除 + deliveryExpressMapper.deleteById(id); + } + + private void validateExpressCodeUnique(String code, Long id) { + DeliveryExpressDO express = deliveryExpressMapper.selectByCode(code); + if (express == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的快递公司 + if (id == null) { + throw exception(EXPRESS_CODE_DUPLICATE); + } + if (!express.getId().equals(id)) { + throw exception(EXPRESS_CODE_DUPLICATE); + } + } + private void validateDeliveryExpressExists(Long id) { + if (deliveryExpressMapper.selectById(id) == null) { + throw exception(EXPRESS_NOT_EXISTS); + } + } + + @Override + public DeliveryExpressDO getDeliveryExpress(Long id) { + return deliveryExpressMapper.selectById(id); + } + + @Override + public DeliveryExpressDO validateDeliveryExpress(Long id) { + DeliveryExpressDO deliveryExpress = deliveryExpressMapper.selectById(id); + if (deliveryExpress == null) { + throw exception(EXPRESS_NOT_EXISTS); + } + if (deliveryExpress.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { + throw exception(EXPRESS_STATUS_NOT_ENABLE); + } + return deliveryExpress; + } + + @Override + public PageResult getDeliveryExpressPage(DeliveryExpressPageReqVO pageReqVO) { + return deliveryExpressMapper.selectPage(pageReqVO); + } + + @Override + public List getDeliveryExpressList(DeliveryExpressExportReqVO exportReqVO) { + return deliveryExpressMapper.selectList(exportReqVO); + } + + @Override + public List getDeliveryExpressListByStatus(Integer status) { + return deliveryExpressMapper.selectListByStatus(status); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressTemplateService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressTemplateService.java new file mode 100644 index 0000000..1a8de58 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressTemplateService.java @@ -0,0 +1,95 @@ +package com.tashow.cloud.trade.service.delivery; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplateCreateReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplateDetailRespVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplatePageReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplateUpdateReqVO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO; +import com.tashow.cloud.trade.service.delivery.bo.DeliveryExpressTemplateRespBO; + +import jakarta.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 快递运费模板 Service 接口 + * + * @author jason + */ +public interface DeliveryExpressTemplateService { + + /** + * 创建快递运费模板 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDeliveryExpressTemplate(@Valid DeliveryExpressTemplateCreateReqVO createReqVO); + + /** + * 更新快递运费模板 + * + * @param updateReqVO 更新信息 + */ + void updateDeliveryExpressTemplate(@Valid DeliveryExpressTemplateUpdateReqVO updateReqVO); + + /** + * 删除快递运费模板 + * + * @param id 编号 + */ + void deleteDeliveryExpressTemplate(Long id); + + /** + * 获得快递运费模板 + * + * @param id 编号 + * @return 快递运费模板详情 + */ + DeliveryExpressTemplateDetailRespVO getDeliveryExpressTemplate(Long id); + + /** + * 获得快递运费模板列表 + * + * @param ids 编号 + * @return 快递运费模板列表 + */ + List getDeliveryExpressTemplateList(Collection ids); + + /** + * 获得快递运费模板列表 + * + * @return 快递运费模板列表 + */ + List getDeliveryExpressTemplateList(); + + /** + * 获得快递运费模板分页 + * + * @param pageReqVO 分页查询 + * @return 快递运费模板分页 + */ + PageResult getDeliveryExpressTemplatePage(DeliveryExpressTemplatePageReqVO pageReqVO); + + /** + * 校验快递运费模板 + * + * 如果校验不通过,抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 异常 + * + * @param templateId 模板编号 + * @return 快递运费模板 + */ + DeliveryExpressTemplateDO validateDeliveryExpressTemplate(Long templateId); + + /** + * 基于运费模板编号数组和收件人地址区域编号,获取匹配运费模板 + * + * @param ids 编号列表 + * @param areaId 区域编号 + * @return Map (templateId -> 运费模板设置) + */ + Map getExpressTemplateMapByIdsAndArea(Collection ids, Integer areaId); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java new file mode 100644 index 0000000..bd92771 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java @@ -0,0 +1,219 @@ +package com.tashow.cloud.trade.service.delivery; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.expresstemplate.*; +import com.tashow.cloud.trade.controller.admin.delivery.vo.expresstemplate.*; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateChargeDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressTemplateFreeDO; +import com.tashow.cloud.trade.dal.mysql.delivery.DeliveryExpressTemplateChargeMapper; +import com.tashow.cloud.trade.dal.mysql.delivery.DeliveryExpressTemplateFreeMapper; +import com.tashow.cloud.trade.dal.mysql.delivery.DeliveryExpressTemplateMapper; +import com.tashow.cloud.trade.service.delivery.bo.DeliveryExpressTemplateRespBO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.Resource; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static com.tashow.cloud.trade.convert.delivery.DeliveryExpressTemplateConvert.INSTANCE; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.EXPRESS_TEMPLATE_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.EXPRESS_TEMPLATE_NOT_EXISTS; + +/** + * 快递运费模板 Service 实现类 + * + * @author jason + */ +@Service +@Validated +public class DeliveryExpressTemplateServiceImpl implements DeliveryExpressTemplateService { + + @Resource + private DeliveryExpressTemplateMapper expressTemplateMapper; + @Resource + private DeliveryExpressTemplateChargeMapper expressTemplateChargeMapper; + @Resource + private DeliveryExpressTemplateFreeMapper expressTemplateFreeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createDeliveryExpressTemplate(DeliveryExpressTemplateCreateReqVO createReqVO) { + // 校验模板名是否唯一 + validateTemplateNameUnique(createReqVO.getName(), null); + + // 插入 + DeliveryExpressTemplateDO template = INSTANCE.convert(createReqVO); + expressTemplateMapper.insert(template); + // 插入运费模板计费表 + if (CollUtil.isNotEmpty(createReqVO.getCharges())) { + expressTemplateChargeMapper.insertBatch( + INSTANCE.convertTemplateChargeList(template.getId(), createReqVO.getChargeMode(), createReqVO.getCharges()) + ); + } + // 插入运费模板包邮表 + if (CollUtil.isNotEmpty(createReqVO.getFrees())) { + expressTemplateFreeMapper.insertBatch( + INSTANCE.convertTemplateFreeList(template.getId(), createReqVO.getFrees()) + ); + } + return template.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDeliveryExpressTemplate(DeliveryExpressTemplateUpdateReqVO updateReqVO) { + // 校验存在 + validateDeliveryExpressTemplateExists(updateReqVO.getId()); + // 校验模板名是否唯一 + validateTemplateNameUnique(updateReqVO.getName(), updateReqVO.getId()); + + // 更新运费从表 + updateExpressTemplateCharge(updateReqVO.getId(), updateReqVO.getChargeMode(), updateReqVO.getCharges()); + // 更新包邮从表 + updateExpressTemplateFree(updateReqVO.getId(), updateReqVO.getFrees()); + // 更新模板主表 + DeliveryExpressTemplateDO updateObj = INSTANCE.convert(updateReqVO); + expressTemplateMapper.updateById(updateObj); + } + + private void updateExpressTemplateFree(Long templateId, List frees) { + // 第一步,对比新老数据,获得添加、修改、删除的列表 + List oldList = expressTemplateFreeMapper.selectListByTemplateId(templateId); + List newList = INSTANCE.convertTemplateFreeList(templateId, frees); + List> diffList = CollectionUtils.diffList(oldList, newList, + (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getId())); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + expressTemplateFreeMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + expressTemplateFreeMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + expressTemplateFreeMapper.deleteBatchIds(convertList(diffList.get(2), DeliveryExpressTemplateFreeDO::getId)); + } + } + + private void updateExpressTemplateCharge(Long templateId, Integer chargeMode, List charges) { + // 第一步,对比新老数据,获得添加、修改、删除的列表 + List oldList = expressTemplateChargeMapper.selectListByTemplateId(templateId); + List newList = INSTANCE.convertTemplateChargeList(templateId, chargeMode, charges); + List> diffList = diffList(oldList, newList, (oldVal, newVal) -> { + boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId()); + if (same) { + newVal.setChargeMode(chargeMode); // 更新下收费模式 + } + return same; + }); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + expressTemplateChargeMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + expressTemplateChargeMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + expressTemplateChargeMapper.deleteBatchIds(convertList(diffList.get(2), DeliveryExpressTemplateChargeDO::getId)); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDeliveryExpressTemplate(Long id) { + // 校验存在 + validateDeliveryExpressTemplateExists(id); + + // 删除主表 + expressTemplateMapper.deleteById(id); + // 删除运费从表 + expressTemplateChargeMapper.deleteByTemplateId(id); + // 删除包邮从表 + expressTemplateFreeMapper.deleteByTemplateId(id); + } + + /** + * 校验运费模板名是否唯一 + * + * @param name 模板名称 + * @param id 运费模板编号,可以为 null + */ + private void validateTemplateNameUnique(String name, Long id) { + DeliveryExpressTemplateDO template = expressTemplateMapper.selectByName(name); + if (template == null) { + return; + } + // 如果 id 为空 + if (id == null) { + throw exception(EXPRESS_TEMPLATE_NAME_DUPLICATE); + } + if (!template.getId().equals(id)) { + throw exception(EXPRESS_TEMPLATE_NAME_DUPLICATE); + } + } + + private void validateDeliveryExpressTemplateExists(Long id) { + if (expressTemplateMapper.selectById(id) == null) { + throw exception(EXPRESS_TEMPLATE_NOT_EXISTS); + } + } + + @Override + public DeliveryExpressTemplateDetailRespVO getDeliveryExpressTemplate(Long id) { + List chargeList = expressTemplateChargeMapper.selectListByTemplateId(id); + List freeList = expressTemplateFreeMapper.selectListByTemplateId(id); + DeliveryExpressTemplateDO template = expressTemplateMapper.selectById(id); + return INSTANCE.convert(template, chargeList, freeList); + } + + @Override + public List getDeliveryExpressTemplateList(Collection ids) { + return expressTemplateMapper.selectBatchIds(ids); + } + + @Override + public List getDeliveryExpressTemplateList() { + return expressTemplateMapper.selectList(); + } + + @Override + public PageResult getDeliveryExpressTemplatePage(DeliveryExpressTemplatePageReqVO pageReqVO) { + return expressTemplateMapper.selectPage(pageReqVO); + } + + @Override + public DeliveryExpressTemplateDO validateDeliveryExpressTemplate(Long templateId) { + DeliveryExpressTemplateDO template = expressTemplateMapper.selectById(templateId); + if (template == null) { + throw exception(EXPRESS_TEMPLATE_NOT_EXISTS); + } + return template; + } + + @Override + public Map getExpressTemplateMapByIdsAndArea(Collection ids, Integer areaId) { + Assert.notNull(areaId, "区域编号 {} 不能为空", areaId); + // 查询 template 数组 + if (CollUtil.isEmpty(ids)) { + return Collections.emptyMap(); + } + List templateList = expressTemplateMapper.selectBatchIds(ids); + // 查询 templateCharge 数组 + List chargeList = expressTemplateChargeMapper.selectByTemplateIds(ids); + // 查询 templateFree 数组 + List freeList = expressTemplateFreeMapper.selectListByTemplateIds(ids); + + // 组合运费模板配置 RespBO + return INSTANCE.convertMap(areaId, templateList, chargeList, freeList); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryPickUpStoreService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryPickUpStoreService.java new file mode 100644 index 0000000..5c0f29e --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryPickUpStoreService.java @@ -0,0 +1,82 @@ +package com.tashow.cloud.trade.service.delivery; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpBindReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreCreateReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStorePageReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreUpdateReqVO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; + +/** + * 自提门店 Service 接口 + * + * @author jason + */ +public interface DeliveryPickUpStoreService { + + /** + * 创建自提门店 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDeliveryPickUpStore(@Valid DeliveryPickUpStoreCreateReqVO createReqVO); + + /** + * 更新自提门店 + * + * @param updateReqVO 更新信息 + */ + void updateDeliveryPickUpStore(@Valid DeliveryPickUpStoreUpdateReqVO updateReqVO); + + /** + * 删除自提门店 + * + * @param id 编号 + */ + void deleteDeliveryPickUpStore(Long id); + + /** + * 获得自提门店 + * + * @param id 编号 + * @return 自提门店 + */ + DeliveryPickUpStoreDO getDeliveryPickUpStore(Long id); + + /** + * 获得自提门店列表 + * + * @param ids 编号 + * @return 自提门店列表 + */ + List getDeliveryPickUpStoreList(Collection ids); + + /** + * 获得指定状态的自提门店列表 + * + * @param status 状态 + * @return 自提门店列表 + */ + List getDeliveryPickUpStoreListByStatus(Integer status); + + /** + * 获得自提门店分页 + * + * @param pageReqVO 分页查询 + * @return 自提门店分页 + */ + PageResult getDeliveryPickUpStorePage(DeliveryPickUpStorePageReqVO pageReqVO); + + /** + * 绑定自提店员 + * + * @param bindReqVO 绑定数据 + */ + void bindDeliveryPickUpStore(DeliveryPickUpBindReqVO bindReqVO); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryPickUpStoreServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryPickUpStoreServiceImpl.java new file mode 100644 index 0000000..91637d5 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/DeliveryPickUpStoreServiceImpl.java @@ -0,0 +1,103 @@ +package com.tashow.cloud.trade.service.delivery; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpBindReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreCreateReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStorePageReqVO; +import com.tashow.cloud.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreUpdateReqVO; +import com.tashow.cloud.trade.convert.delivery.DeliveryPickUpStoreConvert; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import com.tashow.cloud.trade.dal.mysql.delivery.DeliveryPickUpStoreMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +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.trade.enums.ErrorCodeConstants.PICK_UP_STORE_NOT_EXISTS; + +/** + * 自提门店 Service 实现类 + * + * @author jason + */ +@Service +@Validated +public class DeliveryPickUpStoreServiceImpl implements DeliveryPickUpStoreService { + + @Resource + private DeliveryPickUpStoreMapper deliveryPickUpStoreMapper; + + @Resource + private AdminUserApi adminUserApi; + + @Override + public Long createDeliveryPickUpStore(DeliveryPickUpStoreCreateReqVO createReqVO) { + // 插入 + DeliveryPickUpStoreDO deliveryPickUpStore = DeliveryPickUpStoreConvert.INSTANCE.convert(createReqVO); + deliveryPickUpStoreMapper.insert(deliveryPickUpStore); + // 返回 + return deliveryPickUpStore.getId(); + } + + @Override + public void updateDeliveryPickUpStore(DeliveryPickUpStoreUpdateReqVO updateReqVO) { + // 校验存在 + validateDeliveryPickUpStoreExists(updateReqVO.getId()); + // 更新 + DeliveryPickUpStoreDO updateObj = DeliveryPickUpStoreConvert.INSTANCE.convert(updateReqVO); + deliveryPickUpStoreMapper.updateById(updateObj); + } + + @Override + public void deleteDeliveryPickUpStore(Long id) { + // 校验存在 + validateDeliveryPickUpStoreExists(id); + // 删除 + deliveryPickUpStoreMapper.deleteById(id); + } + + private void validateDeliveryPickUpStoreExists(Long id) { + DeliveryPickUpStoreDO deliveryPickUpStore = deliveryPickUpStoreMapper.selectById(id); + if (deliveryPickUpStore == null) { + throw exception(PICK_UP_STORE_NOT_EXISTS); + } + } + + @Override + public DeliveryPickUpStoreDO getDeliveryPickUpStore(Long id) { + return deliveryPickUpStoreMapper.selectById(id); + } + + @Override + public List getDeliveryPickUpStoreList(Collection ids) { + return deliveryPickUpStoreMapper.selectBatchIds(ids); + } + + @Override + public List getDeliveryPickUpStoreListByStatus(Integer status) { + return deliveryPickUpStoreMapper.selectListByStatus(status); + } + + @Override + public PageResult getDeliveryPickUpStorePage(DeliveryPickUpStorePageReqVO pageReqVO) { + return deliveryPickUpStoreMapper.selectPage(pageReqVO); + } + + @Override + public void bindDeliveryPickUpStore(DeliveryPickUpBindReqVO bindReqVO) { + // 1.1 校验门店存在 + validateDeliveryPickUpStoreExists(bindReqVO.getId()); + // 1.2 校验用户存在 + adminUserApi.validateUserList(bindReqVO.getVerifyUserIds()).checkError(); + + // 2. 更新 + DeliveryPickUpStoreDO updateObj = BeanUtils.toBean(bindReqVO, DeliveryPickUpStoreDO.class); + deliveryPickUpStoreMapper.updateById(updateObj); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/bo/DeliveryExpressTemplateRespBO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/bo/DeliveryExpressTemplateRespBO.java new file mode 100644 index 0000000..2156556 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/delivery/bo/DeliveryExpressTemplateRespBO.java @@ -0,0 +1,80 @@ +package com.tashow.cloud.trade.service.delivery.bo; + +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryExpressChargeModeEnum; +import lombok.Data; + +/** + * 运费模板配置 Resp BO + * + * @author jason + */ +@Data +public class DeliveryExpressTemplateRespBO { + + /** + * 配送计费方式 + * + * 枚举 {@link DeliveryExpressChargeModeEnum} + */ + private Integer chargeMode; + + /** + * 运费模板快递运费设置 + */ + private Charge charge; + + /** + * 运费模板包邮设置 + */ + private Free free; + + /** + * 快递运费模板费用配置 BO + * + * @author jason + */ + @Data + public static class Charge { + + /** + * 首件数量(件数,重量,或体积) + */ + private Double startCount; + /** + * 起步价,单位:分 + */ + private Integer startPrice; + /** + * 续件数量(件, 重量,或体积) + */ + private Double extraCount; + /** + * 额外价,单位:分 + */ + private Integer extraPrice; + } + + /** + * 快递运费模板包邮配置 BO + * + * @author jason + */ + @Data + public static class Free { + + /** + * 包邮金额,单位:分 + * + * 订单总金额 > 包邮金额时,才免运费 + */ + private Integer freePrice; + + /** + * 包邮件数 + * + * 订单总件数 > 包邮件数时,才免运费 + */ + private Integer freeCount; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/message/TradeMessageService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/message/TradeMessageService.java new file mode 100644 index 0000000..b9d6d3a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/message/TradeMessageService.java @@ -0,0 +1,19 @@ +package com.tashow.cloud.trade.service.message; + +import com.tashow.cloud.trade.service.message.bo.TradeOrderMessageWhenDeliveryOrderReqBO; + +/** + * Trade 消息 service 接口 + * + * @author HUIHUI + */ +public interface TradeMessageService { + + /** + * 订单发货时发送通知 + * + * @param reqBO 发送消息 + */ + void sendMessageWhenDeliveryOrder(TradeOrderMessageWhenDeliveryOrderReqBO reqBO); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/message/TradeMessageServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/message/TradeMessageServiceImpl.java new file mode 100644 index 0000000..75f26dc --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/message/TradeMessageServiceImpl.java @@ -0,0 +1,44 @@ +package com.tashow.cloud.trade.service.message; + +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import cn.iocoder.yudao.module.trade.enums.MessageTemplateConstants; +import com.tashow.cloud.trade.service.message.bo.TradeOrderMessageWhenDeliveryOrderReqBO; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +/** + * Trade 消息 service 实现类 + * + * @author HUIHUI + */ +@Service +@Validated +public class TradeMessageServiceImpl implements TradeMessageService { + + @Resource + private NotifyMessageSendApi notifyMessageSendApi; + + @Override + public void sendMessageWhenDeliveryOrder(TradeOrderMessageWhenDeliveryOrderReqBO reqBO) { + if (true) { + return; + } + // 1、构造消息 + Map msgMap = new HashMap<>(2); + msgMap.put("orderId", reqBO.getOrderId()); + msgMap.put("deliveryMessage", reqBO.getMessage()); + // TODO 芋艿:看下模版 + // 2、发送站内信 + notifyMessageSendApi.sendSingleMessageToMember( + new NotifySendSingleToUserReqDTO() + .setUserId(reqBO.getUserId()) + .setTemplateCode(MessageTemplateConstants.SMS_ORDER_DELIVERY) + .setTemplateParams(msgMap)).checkError(); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/message/bo/TradeOrderMessageWhenDeliveryOrderReqBO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/message/bo/TradeOrderMessageWhenDeliveryOrderReqBO.java new file mode 100644 index 0000000..c1c76e1 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/message/bo/TradeOrderMessageWhenDeliveryOrderReqBO.java @@ -0,0 +1,32 @@ +package com.tashow.cloud.trade.service.message.bo; + +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * 订单发货时通知创建 Req BO + * + * @author HUIHUI + */ +@Data +public class TradeOrderMessageWhenDeliveryOrderReqBO { + + /** + * 订单编号 + */ + @NotNull(message = "订单编号不能为空") + private Long orderId; + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 消息 + */ + @NotEmpty(message = "发送消息不能为空") + private String message; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderLogService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderLogService.java new file mode 100644 index 0000000..2d4bfdc --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderLogService.java @@ -0,0 +1,35 @@ +package com.tashow.cloud.trade.service.order; + +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderLogDO; +import com.tashow.cloud.trade.service.order.bo.TradeOrderLogCreateReqBO; +import org.springframework.scheduling.annotation.Async; + +import java.util.List; + +/** + * 交易下单日志 Service 接口 + * + * @author 陈賝 + * @since 2023/7/6 15:44 + */ +public interface TradeOrderLogService { + + /** + * 创建交易下单日志 + * + * @param logDTO 日志记录 + * @author 陈賝 + * @since 2023/7/6 15:45 + */ + @Async + void createOrderLog(TradeOrderLogCreateReqBO logDTO); + + /** + * 获得交易订单日志列表 + * + * @param orderId 订单编号 + * @return 交易订单日志列表 + */ + List getOrderLogListByOrderId(Long orderId); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderLogServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderLogServiceImpl.java new file mode 100644 index 0000000..2d34e09 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderLogServiceImpl.java @@ -0,0 +1,34 @@ +package com.tashow.cloud.trade.service.order; + +import com.tashow.cloud.trade.convert.order.TradeOrderLogConvert; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderLogDO; +import com.tashow.cloud.trade.dal.mysql.order.TradeOrderLogMapper; +import com.tashow.cloud.trade.service.order.bo.TradeOrderLogCreateReqBO; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import java.util.List; + +/** + * 交易下单日志 Service 实现类 + * + * @author 陈賝 + * @since 2023/7/6 15:44 + */ +@Service +public class TradeOrderLogServiceImpl implements TradeOrderLogService { + + @Resource + private TradeOrderLogMapper tradeOrderLogMapper; + + @Override + public void createOrderLog(TradeOrderLogCreateReqBO createReqBO) { + tradeOrderLogMapper.insert(TradeOrderLogConvert.INSTANCE.convert(createReqBO)); + } + + @Override + public List getOrderLogListByOrderId(Long orderId) { + return tradeOrderLogMapper.selectListByOrderId(orderId); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderQueryService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderQueryService.java new file mode 100644 index 0000000..40708c2 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderQueryService.java @@ -0,0 +1,160 @@ +package com.tashow.cloud.trade.service.order; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderPageReqVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderSummaryRespVO; +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeOrderPageReqVO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; + +import java.util.Collection; +import java.util.List; + +import static java.util.Collections.singleton; + +/** + * 交易订单【读】 Service 接口 + * + * @author 芋道源码 + */ +public interface TradeOrderQueryService { + + // =================== Order =================== + + /** + * 获得指定编号的交易订单 + * + * @param id 交易订单编号 + * @return 交易订单 + */ + TradeOrderDO getOrder(Long id); + + /** + * 获得指定用户,指定的交易订单 + * + * @param userId 用户编号 + * @param id 交易订单编号 + * @return 交易订单 + */ + TradeOrderDO getOrder(Long userId, Long id); + + /** + * 获得指定用户,指定活动,指定状态的交易订单 + * + * @param userId 用户编号 + * @param combinationActivityId 活动编号 + * @param status 订单状态 + * @return 交易订单 + */ + TradeOrderDO getOrderByUserIdAndStatusAndCombination(Long userId, Long combinationActivityId, Integer status); + + /** + * 获得订单列表 + * + * @param ids 订单编号数组 + * @return 订单列表 + */ + List getOrderList(Collection ids); + + /** + * 【管理员】获得交易订单分页 + * + * @param reqVO 分页请求 + * @return 交易订单 + */ + PageResult getOrderPage(TradeOrderPageReqVO reqVO); + + /** + * 获得订单统计 + * + * @param reqVO 请求参数 + * @return 订单统计 + */ + TradeOrderSummaryRespVO getOrderSummary(TradeOrderPageReqVO reqVO); + + /** + * 【会员】获得交易订单分页 + * + * @param userId 用户编号 + * @param reqVO 分页请求 + * @return 交易订单 + */ + PageResult getOrderPage(Long userId, AppTradeOrderPageReqVO reqVO); + + /** + * 【会员】获得交易订单数量 + * + * @param userId 用户编号 + * @param status 订单状态。如果为空,则不进行筛选 + * @param commonStatus 评价状态。如果为空,则不进行筛选 + * @return 订单数量 + */ + Long getOrderCount(Long userId, Integer status, Boolean commonStatus); + + /** + * 【前台】获得订单的物流轨迹 + * + * @param id 订单编号 + * @param userId 用户编号 + * @return 物流轨迹数组 + */ + List getExpressTrackList(Long id, Long userId); + + /** + * 【后台】获得订单的物流轨迹 + * + * @param id 订单编号 + * @return 物流轨迹数组 + */ + List getExpressTrackList(Long id); + + /** + * 【会员】在指定活动下,用户购买的商品数量 + * + * @param userId 用户编号 + * @param activityId 活动编号 + * @param type 订单类型 + * @return 活动商品数量 + */ + int getActivityProductCount(Long userId, Long activityId, TradeOrderTypeEnum type); + + // =================== Order Item =================== + + /** + * 获得指定用户,指定的交易订单项 + * + * @param userId 用户编号 + * @param itemId 交易订单项编号 + * @return 交易订单项 + */ + TradeOrderItemDO getOrderItem(Long userId, Long itemId); + + /** + * 获得交易订单项 + * + * @param id 交易订单项编号 itemId + * @return 交易订单项 + */ + TradeOrderItemDO getOrderItem(Long id); + + /** + * 根据交易订单编号,查询交易订单项 + * + * @param orderId 交易订单编号 + * @return 交易订单项数组 + */ + default List getOrderItemListByOrderId(Long orderId) { + return getOrderItemListByOrderId(singleton(orderId)); + } + + /** + * 根据交易订单编号数组,查询交易订单项 + * + * @param orderIds 交易订单编号数组 + * @return 交易订单项数组 + */ + List getOrderItemListByOrderId(Collection orderIds); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderQueryServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderQueryServiceImpl.java new file mode 100644 index 0000000..411bbc6 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderQueryServiceImpl.java @@ -0,0 +1,260 @@ +package com.tashow.cloud.trade.service.order; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +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.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderPageReqVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderSummaryRespVO; +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeOrderPageReqVO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import com.tashow.cloud.trade.dal.mysql.order.TradeOrderItemMapper; +import com.tashow.cloud.trade.dal.mysql.order.TradeOrderMapper; +import com.tashow.cloud.trade.dal.redis.RedisKeyConstants; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.framework.delivery.core.client.ExpressClientFactory; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.tashow.cloud.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; +import com.tashow.cloud.trade.service.delivery.DeliveryExpressService; +import jakarta.annotation.Resource; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.EXPRESS_NOT_EXISTS; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_NOT_FOUND; + +/** + * 交易订单【读】 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class TradeOrderQueryServiceImpl implements TradeOrderQueryService { + + @Resource + private ExpressClientFactory expressClientFactory; + + @Resource + private TradeOrderMapper tradeOrderMapper; + @Resource + private TradeOrderItemMapper tradeOrderItemMapper; + + @Resource + private DeliveryExpressService deliveryExpressService; + + @Resource + private MemberUserApi memberUserApi; + + // =================== Order =================== + + @Override + public TradeOrderDO getOrder(Long id) { + return tradeOrderMapper.selectById(id); + } + + @Override + public TradeOrderDO getOrder(Long userId, Long id) { + TradeOrderDO order = tradeOrderMapper.selectById(id); + if (order != null + && ObjectUtil.notEqual(order.getUserId(), userId)) { + return null; + } + return order; + } + + @Override + public TradeOrderDO getOrderByUserIdAndStatusAndCombination(Long userId, Long combinationActivityId, Integer status) { + return tradeOrderMapper.selectByUserIdAndCombinationActivityIdAndStatus(userId, combinationActivityId, status); + } + + @Override + public List getOrderList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return tradeOrderMapper.selectBatchIds(ids); + } + + @Override + public PageResult getOrderPage(TradeOrderPageReqVO reqVO) { + // 根据用户查询条件构建用户编号列表 + Set userIds = buildQueryConditionUserIds(reqVO); + if (userIds == null) { // 没查询到用户,说明肯定也没他的订单 + return PageResult.empty(); + } + + // 分页查询 + return tradeOrderMapper.selectPage(reqVO, userIds); + } + + private Set buildQueryConditionUserIds(TradeOrderPageReqVO reqVO) { + // 获得 userId 相关的查询 + Set userIds = new HashSet<>(); + if (StrUtil.isNotEmpty(reqVO.getUserMobile())) { + MemberUserRespDTO user = memberUserApi.getUserByMobile(reqVO.getUserMobile()).getCheckedData(); + if (user == null) { // 没查询到用户,说明肯定也没他的订单 + return null; + } + userIds.add(user.getId()); + } + if (StrUtil.isNotEmpty(reqVO.getUserNickname())) { + List users = memberUserApi.getUserListByNickname(reqVO.getUserNickname()).getCheckedData(); + if (CollUtil.isEmpty(users)) { // 没查询到用户,说明肯定也没他的订单 + return null; + } + userIds.addAll(convertSet(users, MemberUserRespDTO::getId)); + } + return userIds; + } + + @Override + public TradeOrderSummaryRespVO getOrderSummary(TradeOrderPageReqVO reqVO) { + // 根据用户查询条件构建用户编号列表 + Set userIds = buildQueryConditionUserIds(reqVO); + if (userIds == null) { // 没查询到用户,说明肯定也没他的订单 + return new TradeOrderSummaryRespVO(); + } + // 查询每个售后状态对应的数量、金额 + List> list = tradeOrderMapper.selectOrderSummaryGroupByRefundStatus(reqVO, userIds); + + TradeOrderSummaryRespVO vo = new TradeOrderSummaryRespVO().setAfterSaleCount(0L).setAfterSalePrice(0L); + for (Map map : list) { + Long count = MapUtil.getLong(map, "count", 0L); + Long price = MapUtil.getLong(map, "price", 0L); + // 未退款的计入订单,部分退款、全部退款计入售后 + if (TradeOrderRefundStatusEnum.NONE.getStatus().equals(MapUtil.getInt(map, "refundStatus"))) { + vo.setOrderCount(count).setOrderPayPrice(price); + } else { + vo.setAfterSaleCount(vo.getAfterSaleCount() + count).setAfterSalePrice(vo.getAfterSalePrice() + price); + } + } + return vo; + } + + @Override + public PageResult getOrderPage(Long userId, AppTradeOrderPageReqVO reqVO) { + return tradeOrderMapper.selectPage(reqVO, userId); + } + + @Override + public Long getOrderCount(Long userId, Integer status, Boolean commentStatus) { + return tradeOrderMapper.selectCountByUserIdAndStatus(userId, status, commentStatus); + } + + @Override + public List getExpressTrackList(Long id, Long userId) { + // 查询订单 + TradeOrderDO order = tradeOrderMapper.selectByIdAndUserId(id, userId); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 查询物流 + return getExpressTrackList(order); + } + + @Override + public List getExpressTrackList(Long id) { + // 查询订单 + TradeOrderDO order = tradeOrderMapper.selectById(id); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 查询物流 + return getExpressTrackList(order); + } + + @Override + public int getActivityProductCount(Long userId, Long activityId, TradeOrderTypeEnum type) { + // 获得订单列表 + List orders = tradeOrderMapper.selectListByUserIdAndActivityId(userId, activityId, type); + orders.removeIf(order -> TradeOrderStatusEnum.isCanceled(order.getStatus())); // 过滤掉【已取消】的订单 + if (CollUtil.isEmpty(orders)) { + return 0; + } + // 获得订单项列表 + return tradeOrderItemMapper.selectProductSumByOrderId(convertSet(orders, TradeOrderDO::getId)); + } + + /** + * 获得订单的物流轨迹 + * + * @param order 订单 + * @return 物流轨迹 + */ + private List getExpressTrackList(TradeOrderDO order) { + if (order.getLogisticsId() == null) { + return Collections.emptyList(); + } + // 查询物流公司 + DeliveryExpressDO express = deliveryExpressService.getDeliveryExpress(order.getLogisticsId()); + if (express == null) { + throw exception(EXPRESS_NOT_EXISTS); + } + // 查询物流轨迹 + return getSelf().getExpressTrackList(express.getCode(), order.getLogisticsNo(), order.getReceiverMobile()); + } + + /** + * 查询物流轨迹 + *

+ * 缓存的目的:考虑及时性要求不高,但是每次调用需要钱 + * + * @param code 快递公司编码 + * @param logisticsNo 发货快递单号 + * @param receiverMobile 收、寄件人的电话号码 + * @return 物流轨迹 + */ + @Cacheable(cacheNames = RedisKeyConstants.EXPRESS_TRACK, key = "#code + '-' + #logisticsNo + '-' + #receiverMobile", + unless = "#result == null") + public List getExpressTrackList(String code, String logisticsNo, String receiverMobile) { + return expressClientFactory.getDefaultExpressClient().getExpressTrackList(new ExpressTrackQueryReqDTO() + .setExpressCode(code).setLogisticsNo(logisticsNo).setPhone(receiverMobile)); + } + + // =================== Order Item =================== + + @Override + public TradeOrderItemDO getOrderItem(Long userId, Long itemId) { + TradeOrderItemDO orderItem = tradeOrderItemMapper.selectById(itemId); + if (orderItem != null + && ObjectUtil.notEqual(orderItem.getUserId(), userId)) { + return null; + } + return orderItem; + } + + @Override + public TradeOrderItemDO getOrderItem(Long id) { + return tradeOrderItemMapper.selectById(id); + } + + @Override + public List getOrderItemListByOrderId(Collection orderIds) { + if (CollUtil.isEmpty(orderIds)) { + return Collections.emptyList(); + } + return tradeOrderItemMapper.selectListByOrderId(orderIds); + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private TradeOrderQueryServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderUpdateService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderUpdateService.java new file mode 100644 index 0000000..98c0118 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderUpdateService.java @@ -0,0 +1,221 @@ +package com.tashow.cloud.trade.service.order; + +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderRemarkReqVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderUpdatePriceReqVO; +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeOrderCreateReqVO; +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeOrderSettlementReqVO; +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO; +import com.tashow.cloud.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +/** + * 交易订单【写】Service 接口 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface TradeOrderUpdateService { + + // =================== Order =================== + + /** + * 获得订单结算信息 + * + * @param userId 登录用户 + * @param settlementReqVO 订单结算请求 + * @return 订单结算结果 + */ + AppTradeOrderSettlementRespVO settlementOrder(Long userId, AppTradeOrderSettlementReqVO settlementReqVO); + + /** + * 【会员】创建交易订单 + * + * @param userId 登录用户 + * @param createReqVO 创建交易订单请求模型 + * @return 交易订单的 + */ + TradeOrderDO createOrder(Long userId, AppTradeOrderCreateReqVO createReqVO); + + /** + * 更新交易订单已支付 + * + * @param id 交易订单编号 + * @param payOrderId 支付订单编号 + */ + void updateOrderPaid(Long id, Long payOrderId); + + /** + * 同步订单的支付状态 + * + * 1. Quietly 表示,即使同步失败,也不会抛出异常 + * 2. 什么时候回出现异常?因为是主动同步,可能和支付模块的回调通知 {@link #updateOrderPaid(Long, Long)} 存在并发冲突,导致抛出异常 + * + * @param id 订单编号 + * @param payOrderId 支付订单编号 + */ + void syncOrderPayStatusQuietly(Long id, Long payOrderId); + + /** + * 【管理员】发货交易订单 + * + * @param deliveryReqVO 发货请求 + */ + void deliveryOrder(TradeOrderDeliveryReqVO deliveryReqVO); + + /** + * 【会员】收货交易订单 + * + * @param userId 用户编号 + * @param id 订单编号 + */ + void receiveOrderByMember(Long userId, Long id); + + /** + * 【系统】自动收货交易订单 + * + * @return 收货数量 + */ + int receiveOrderBySystem(); + + /** + * 【会员】取消交易订单 + * + * @param userId 用户编号 + * @param id 订单编号 + */ + void cancelOrderByMember(Long userId, Long id); + + /** + * 【系统】自动取消订单 + * + * @return 取消数量 + */ + int cancelOrderBySystem(); + + /** + * 【会员】删除订单 + * + * @param userId 用户编号 + * @param id 订单编号 + */ + void deleteOrder(Long userId, Long id); + + /** + * 【管理员】交易订单备注 + * + * @param reqVO 请求 + */ + void updateOrderRemark(TradeOrderRemarkReqVO reqVO); + + /** + * 【管理员】调整价格 + * + * @param reqVO 请求 + */ + void updateOrderPrice(TradeOrderUpdatePriceReqVO reqVO); + + /** + * 【管理员】调整地址 + * + * @param reqVO 请求 + */ + void updateOrderAddress(TradeOrderUpdateAddressReqVO reqVO); + + /** + * 【管理员】核销订单 + * + * @param userId 管理员编号 + * @param id 订单编号 + */ + void pickUpOrderByAdmin(Long userId, Long id); + + /** + * 【管理员】核销订单 + * + * @param userId 管理员编号 + * @param pickUpVerifyCode 自提核销码 + */ + void pickUpOrderByAdmin(Long userId, String pickUpVerifyCode); + + /** + * 【管理员】根据自提核销码,查询订单 + * + * @param pickUpVerifyCode 自提核销码 + */ + TradeOrderDO getByPickUpVerifyCode(String pickUpVerifyCode); + + // =================== Order Item =================== + + /** + * 当售后申请后,更新交易订单项的售后状态 + * + * @param id 交易订单项编号 + * @param afterSaleId 售后单编号 + */ + void updateOrderItemWhenAfterSaleCreate(@NotNull Long id, @NotNull Long afterSaleId); + + /** + * 当售后完成后,更新交易订单项的售后状态 + * + * @param id 交易订单项编号 + * @param refundPrice 退款金额 + */ + void updateOrderItemWhenAfterSaleSuccess(@NotNull Long id, @NotNull Integer refundPrice); + + /** + * 当售后取消(用户取消、管理员驳回、管理员拒绝收货)后,更新交易订单项的售后状态 + * + * @param id 交易订单项编号 + */ + void updateOrderItemWhenAfterSaleCancel(@NotNull Long id); + + /** + * 【会员】创建订单项的评论 + * + * @param userId 用户编号 + * @param createReqVO 创建请求 + * @return 得到评价 id + */ + Long createOrderItemCommentByMember(Long userId, AppTradeOrderItemCommentCreateReqVO createReqVO); + + /** + * 【系统】创建订单项的评论 + * + * @return 被评论的订单数 + */ + int createOrderItemCommentBySystem(); + + /** + * 更新拼团相关信息到订单 + * + * @param orderId 订单编号 + * @param activityId 拼团活动编号 + * @param combinationRecordId 拼团记录编号 + * @param headId 团长编号 + */ + void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId); + + /** + * 取消支付订单 + * + * @param userId 用户编号 + * @param orderId 订单编号 + * @param cancelType 取消类型 + */ + void cancelPaidOrder(Long userId, Long orderId, Integer cancelType); + + /** + * 更新下单赠送的优惠券编号到订单 + * + * @param userId 用户编号 + * @param orderId 订单编号 + * @param giveCouponIds 赠送的优惠券编号列表 + */ + void updateOrderGiveCouponIds(Long userId, Long orderId, List giveCouponIds); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderUpdateServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderUpdateServiceImpl.java new file mode 100644 index 0000000..2c5e43f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -0,0 +1,991 @@ +package com.tashow.cloud.trade.service.order; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; +import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; +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.enums.order.PayOrderStatusEnum; +import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi; +import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO; +import cn.iocoder.yudao.module.system.api.social.SocialClientApi; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderRemarkReqVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO; +import com.tashow.cloud.trade.controller.admin.order.vo.TradeOrderUpdatePriceReqVO; +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeOrderCreateReqVO; +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeOrderSettlementReqVO; +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO; +import com.tashow.cloud.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; +import com.tashow.cloud.trade.convert.order.TradeOrderConvert; +import com.tashow.cloud.trade.dal.dataobject.cart.CartDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import com.tashow.cloud.trade.dal.mysql.order.TradeOrderItemMapper; +import com.tashow.cloud.trade.dal.mysql.order.TradeOrderMapper; +import com.tashow.cloud.trade.dal.redis.no.TradeNoRedisDAO; +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum; +import com.tashow.cloud.trade.framework.order.config.TradeOrderProperties; +import com.tashow.cloud.trade.framework.order.core.annotations.TradeOrderLog; +import com.tashow.cloud.trade.framework.order.core.utils.TradeOrderLogUtils; +import com.tashow.cloud.trade.service.cart.CartService; +import com.tashow.cloud.trade.service.delivery.DeliveryExpressService; +import com.tashow.cloud.trade.service.delivery.DeliveryPickUpStoreService; +import com.tashow.cloud.trade.service.message.TradeMessageService; +import com.tashow.cloud.trade.service.message.bo.TradeOrderMessageWhenDeliveryOrderReqBO; +import com.tashow.cloud.trade.service.order.handler.TradeOrderHandler; +import com.tashow.cloud.trade.service.price.TradePriceService; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import com.tashow.cloud.trade.service.price.calculator.TradePriceCalculatorHelper; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.minusTime; +import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getTerminal; +import static cn.iocoder.yudao.module.trade.enums.MessageTemplateConstants.WXA_ORDER_DELIVERY; + +/** + * 交易订单【写】Service 实现类 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +@Service +@Slf4j +public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { + + @Resource + private TradeOrderMapper tradeOrderMapper; + @Resource + private TradeOrderItemMapper tradeOrderItemMapper; + @Resource + private TradeNoRedisDAO tradeNoRedisDAO; + + @Resource + private List tradeOrderHandlers; + + @Resource + private CartService cartService; + @Resource + private TradePriceService tradePriceService; + @Resource + private DeliveryExpressService deliveryExpressService; + @Resource + private TradeMessageService tradeMessageService; + @Resource + private DeliveryPickUpStoreService pickUpStoreService; + + @Resource + private PayOrderApi payOrderApi; + @Resource + private MemberAddressApi addressApi; + @Resource + private ProductCommentApi productCommentApi; + @Resource + public SocialClientApi socialClientApi; + @Resource + public PayRefundApi payRefundApi; + + @Resource + private TradeOrderProperties tradeOrderProperties; + + // =================== Order =================== + + @Override + public AppTradeOrderSettlementRespVO settlementOrder(Long userId, AppTradeOrderSettlementReqVO settlementReqVO) { + // 1. 获得收货地址 + MemberAddressRespDTO address = getAddress(userId, settlementReqVO.getAddressId()); + if (address != null) { + settlementReqVO.setAddressId(address.getId()); + } + + // 2. 计算价格 + TradePriceCalculateRespBO calculateRespBO = calculatePrice(userId, settlementReqVO); + + // 3. 拼接返回 + return TradeOrderConvert.INSTANCE.convert(calculateRespBO, address); + } + + /** + * 获得用户地址 + * + * @param userId 用户编号 + * @param addressId 地址编号 + * @return 地址 + */ + private MemberAddressRespDTO getAddress(Long userId, Long addressId) { + if (addressId != null) { + return addressApi.getAddress(addressId, userId).getCheckedData(); + } + return addressApi.getDefaultAddress(userId).getCheckedData(); + } + + /** + * 计算订单价格 + * + * @param userId 用户编号 + * @param settlementReqVO 结算信息 + * @return 订单价格 + */ + private TradePriceCalculateRespBO calculatePrice(Long userId, AppTradeOrderSettlementReqVO settlementReqVO) { + // 1. 如果来自购物车,则获得购物车的商品 + List cartList = cartService.getCartList(userId, + convertSet(settlementReqVO.getItems(), AppTradeOrderSettlementReqVO.Item::getCartId)); + + // 2. 计算价格 + TradePriceCalculateReqBO calculateReqBO = TradeOrderConvert.INSTANCE.convert(userId, settlementReqVO, cartList); + calculateReqBO.getItems().forEach(item -> Assert.isTrue(item.getSelected(), // 防御性编程,保证都是选中的 + "商品({}) 未设置为选中", item.getSkuId())); + return tradePriceService.calculateOrderPrice(calculateReqBO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.MEMBER_CREATE) + public TradeOrderDO createOrder(Long userId, AppTradeOrderCreateReqVO createReqVO) { + // 1.1 价格计算 + TradePriceCalculateRespBO calculateRespBO = calculatePrice(userId, createReqVO); + // 1.2 构建订单 + TradeOrderDO order = buildTradeOrder(userId, createReqVO, calculateRespBO); + List orderItems = buildTradeOrderItems(order, calculateRespBO); + + // 2. 订单创建前的逻辑 + tradeOrderHandlers.forEach(handler -> handler.beforeOrderCreate(order, orderItems)); + + // 3. 保存订单 + tradeOrderMapper.insert(order); + orderItems.forEach(orderItem -> orderItem.setOrderId(order.getId())); + tradeOrderItemMapper.insertBatch(orderItems); + + // 4. 订单创建后的逻辑 + afterCreateTradeOrder(order, orderItems, createReqVO); + return order; + } + + private TradeOrderDO buildTradeOrder(Long userId, AppTradeOrderCreateReqVO createReqVO, + TradePriceCalculateRespBO calculateRespBO) { + TradeOrderDO order = TradeOrderConvert.INSTANCE.convert(userId, createReqVO, calculateRespBO); + order.setType(calculateRespBO.getType()); + order.setNo(tradeNoRedisDAO.generate(TradeNoRedisDAO.TRADE_ORDER_NO_PREFIX)); + order.setStatus(TradeOrderStatusEnum.UNPAID.getStatus()); + order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()); + order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum)); + order.setUserIp(getClientIP()).setTerminal(getTerminal()); + // 使用 + 赠送优惠券 + order.setGiveCouponTemplateCounts(calculateRespBO.getGiveCouponTemplateCounts()); + // 支付 + 退款信息 + order.setAdjustPrice(0).setPayStatus(false); + order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0); + // 物流信息 + order.setDeliveryType(createReqVO.getDeliveryType()); + if (Objects.equals(createReqVO.getDeliveryType(), DeliveryTypeEnum.EXPRESS.getType())) { + MemberAddressRespDTO address = addressApi.getAddress(createReqVO.getAddressId(), userId).getCheckedData(); + Assert.notNull(address, "地址({}) 不能为空", createReqVO.getAddressId()); // 价格计算时,已经计算 + order.setReceiverName(address.getName()).setReceiverMobile(address.getMobile()) + .setReceiverAreaId(address.getAreaId()).setReceiverDetailAddress(address.getDetailAddress()); + } else if (Objects.equals(createReqVO.getDeliveryType(), DeliveryTypeEnum.PICK_UP.getType())) { + order.setReceiverName(createReqVO.getReceiverName()).setReceiverMobile(createReqVO.getReceiverMobile()); + order.setPickUpVerifyCode(RandomUtil.randomNumbers(8)); // 随机一个核销码,长度为 8 位 + } + return order; + } + + private List buildTradeOrderItems(TradeOrderDO tradeOrderDO, + TradePriceCalculateRespBO calculateRespBO) { + return TradeOrderConvert.INSTANCE.convertList(tradeOrderDO, calculateRespBO); + } + + /** + * 订单创建后,执行后置逻辑 + *

+ * 例如说:优惠劵的扣减、积分的扣减、支付单的创建等等 + * + * @param order 订单 + * @param orderItems 订单项 + * @param createReqVO 创建订单请求 + */ + private void afterCreateTradeOrder(TradeOrderDO order, List orderItems, + AppTradeOrderCreateReqVO createReqVO) { + // 1. 执行订单创建后置处理器 + tradeOrderHandlers.forEach(handler -> handler.afterOrderCreate(order, orderItems)); + + // 2. 删除购物车商品 + Set cartIds = convertSet(createReqVO.getItems(), AppTradeOrderSettlementReqVO.Item::getCartId); + if (CollUtil.isNotEmpty(cartIds)) { + cartService.deleteCart(order.getUserId(), cartIds); + } + + // 3. 生成预支付 + // 特殊情况:积分兑换时,可能支付金额为零 + if (order.getPayPrice() > 0) { + createPayOrder(order, orderItems); + } + + // 4. 插入订单日志 + TradeOrderLogUtils.setOrderInfo(order.getId(), null, order.getStatus()); + + // TODO @LeeYan9: 是可以思考下, 订单的营销优惠记录, 应该记录在哪里, 微信讨论起来! + } + + private void createPayOrder(TradeOrderDO order, List orderItems) { + // 创建支付单,用于后续的支付 + PayOrderCreateReqDTO payOrderCreateReqDTO = TradeOrderConvert.INSTANCE.convert( + order, orderItems, tradeOrderProperties); + Long payOrderId = payOrderApi.createOrder(payOrderCreateReqDTO).getCheckedData(); + + // 更新到交易单上 + tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()).setPayOrderId(payOrderId)); + order.setPayOrderId(payOrderId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.MEMBER_PAY) + public void updateOrderPaid(Long id, Long payOrderId) { + // 1.1 校验订单是否存在 + TradeOrderDO order = validateOrderExists(id); + // 1.2 校验订单已支付 + if (!TradeOrderStatusEnum.isUnpaid(order.getStatus()) || order.getPayStatus()) { + // 特殊:如果订单已支付,且支付单号相同,直接返回,说明重复回调 + if (ObjectUtil.equals(order.getPayOrderId(), payOrderId)) { + log.warn("[updateOrderPaid][order({}) 已支付,且支付单号相同({}),直接返回]", order, payOrderId); + return; + } + log.error("[updateOrderPaid][order({}) 支付单不匹配({}),请进行处理!order 数据是:{}]", + id, payOrderId, JsonUtils.toJsonString(order)); + throw exception(ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + + // 2. 校验支付订单的合法性 + PayOrderRespDTO payOrder = validatePayOrderPaid(order, payOrderId); + + // 3. 更新 TradeOrderDO 状态为已支付,等待发货 + int updateCount = tradeOrderMapper.updateByIdAndStatus(id, order.getStatus(), + new TradeOrderDO().setStatus(TradeOrderStatusEnum.UNDELIVERED.getStatus()).setPayStatus(true) + .setPayTime(LocalDateTime.now()).setPayChannelCode(payOrder.getChannelCode())); + if (updateCount == 0) { + throw exception(ORDER_UPDATE_PAID_STATUS_NOT_UNPAID); + } + + // 4. 执行 TradeOrderHandler 的后置处理 + List orderItems = tradeOrderItemMapper.selectListByOrderId(id); + tradeOrderHandlers.forEach(handler -> handler.afterPayOrder(order, orderItems)); + + // 5. 记录订单日志 + TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.UNDELIVERED.getStatus()); + TradeOrderLogUtils.setUserInfo(order.getUserId(), UserTypeEnum.MEMBER.getValue()); + } + + @Override + public void syncOrderPayStatusQuietly(Long id, Long payOrderId) { + PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId).getCheckedData(); + if (payOrder == null) { + return; + } + if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) { + return; + } + try { + getSelf().updateOrderPaid(id, payOrderId); + } catch (Throwable e) { + log.warn("[syncOrderPayStatusQuietly][id({}) payOrderId({}) 同步支付状态失败]", id, payOrderId, e); + } + } + + /** + * 校验支付订单的合法性 + * + * @param order 交易订单 + * @param payOrderId 支付订单编号 + * @return 支付订单 + */ + private PayOrderRespDTO validatePayOrderPaid(TradeOrderDO order, Long payOrderId) { + // 1. 校验支付单是否存在 + PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId).getCheckedData(); + if (payOrder == null) { + log.error("[validatePayOrderPaid][order({}) payOrder({}) 不存在,请进行处理!]", order.getId(), payOrderId); + throw exception(ORDER_NOT_FOUND); + } + + // 2.1 校验支付单已支付 + if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) { + log.error("[validatePayOrderPaid][order({}) payOrder({}) 未支付,请进行处理!payOrder 数据是:{}]", + order.getId(), payOrderId, JsonUtils.toJsonString(payOrder)); + throw exception(ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS); + } + // 2.2 校验支付金额一致 + if (ObjectUtil.notEqual(payOrder.getPrice(), order.getPayPrice())) { + log.error("[validatePayOrderPaid][order({}) payOrder({}) 支付金额不匹配,请进行处理!order 数据是:{},payOrder 数据是:{}]", + order.getId(), payOrderId, JsonUtils.toJsonString(order), JsonUtils.toJsonString(payOrder)); + throw exception(ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH); + } + // 2.2 校验支付订单匹配(二次) + if (ObjectUtil.notEqual(payOrder.getMerchantOrderId(), order.getId().toString())) { + log.error("[validatePayOrderPaid][order({}) 支付单不匹配({}),请进行处理!payOrder 数据是:{}]", + order.getId(), payOrderId, JsonUtils.toJsonString(payOrder)); + throw exception(ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + return payOrder; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.ADMIN_DELIVERY) + public void deliveryOrder(TradeOrderDeliveryReqVO deliveryReqVO) { + // 1.1 校验并获得交易订单(可发货) + TradeOrderDO order = validateOrderDeliverable(deliveryReqVO.getId()); + // 1.2 校验 deliveryType 是否为快递,是快递才可以发货 + if (ObjectUtil.notEqual(order.getDeliveryType(), DeliveryTypeEnum.EXPRESS.getType())) { + throw exception(ORDER_DELIVERY_FAIL_DELIVERY_TYPE_NOT_EXPRESS); + } + + // 2. 更新订单为已发货 + TradeOrderDO updateOrderObj = new TradeOrderDO(); + // 2.1 快递发货 + DeliveryExpressDO express = null; + if (ObjectUtil.notEqual(deliveryReqVO.getLogisticsId(), TradeOrderDO.LOGISTICS_ID_NULL)) { + express = deliveryExpressService.validateDeliveryExpress(deliveryReqVO.getLogisticsId()); + updateOrderObj.setLogisticsId(deliveryReqVO.getLogisticsId()).setLogisticsNo(deliveryReqVO.getLogisticsNo()); + } else { + // 2.2 无需发货 + updateOrderObj.setLogisticsId(0L).setLogisticsNo(""); + } + // 执行更新 + updateOrderObj.setStatus(TradeOrderStatusEnum.DELIVERED.getStatus()).setDeliveryTime(LocalDateTime.now()); + int updateCount = tradeOrderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), updateOrderObj); + if (updateCount == 0) { + throw exception(ORDER_DELIVERY_FAIL_STATUS_NOT_UNDELIVERED); + } + + // 3. 记录订单日志 + TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.DELIVERED.getStatus(), + MapUtil.builder().put("deliveryName", express != null ? express.getName() : "") + .put("logisticsNo", express != null ? deliveryReqVO.getLogisticsNo() : "").build()); + + // 4.1 发送站内信 + tradeMessageService.sendMessageWhenDeliveryOrder(new TradeOrderMessageWhenDeliveryOrderReqBO() + .setOrderId(order.getId()).setUserId(order.getUserId()).setMessage(null)); + // 4.2 发送订阅消息 + getSelf().sendDeliveryOrderMessage(order, deliveryReqVO); + } + + @Async + public void sendDeliveryOrderMessage(TradeOrderDO order, TradeOrderDeliveryReqVO deliveryReqVO) { + // 构建并发送模版消息 + Long orderId = order.getId(); + socialClientApi.sendWxaSubscribeMessage(new SocialWxaSubscribeMessageSendReqDTO() + .setUserId(order.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue()) + .setTemplateTitle(WXA_ORDER_DELIVERY) + .setPage("pages/order/detail?id=" + orderId) // 订单详情页 + .addMessage("character_string3", String.valueOf(orderId)) // 订单编号 + .addMessage("phrase6", TradeOrderStatusEnum.DELIVERED.getName()) // 订单状态 + .addMessage("date4", LocalDateTimeUtil.formatNormal(LocalDateTime.now()))// 发货时间 + .addMessage("character_string5", StrUtil.blankToDefault(deliveryReqVO.getLogisticsNo(), "-")) // 快递单号 + .addMessage("thing9", order.getReceiverDetailAddress())).checkError(); // 收货地址 + } + + /** + * 校验交易订单满足被发货的条件 + *

+ * 1. 交易订单未发货 + * + * @param id 交易订单编号 + * @return 交易订单 + */ + private TradeOrderDO validateOrderDeliverable(Long id) { + TradeOrderDO order = validateOrderExists(id); + // 1. 校验订单是否未发货 + if (ObjectUtil.notEqual(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) { + throw exception(ORDER_DELIVERY_FAIL_REFUND_STATUS_NOT_NONE); + } + + // 2. 执行 TradeOrderHandler 前置处理 + tradeOrderHandlers.forEach(handler -> handler.beforeDeliveryOrder(order)); + return order; + } + + @NotNull + private TradeOrderDO validateOrderExists(Long id) { + // 校验订单是否存在 + TradeOrderDO order = tradeOrderMapper.selectById(id); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + return order; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.MEMBER_RECEIVE) + public void receiveOrderByMember(Long userId, Long id) { + // 校验并获得交易订单(可收货) + TradeOrderDO order = validateOrderReceivable(userId, id); + + // 收货订单 + receiveOrder0(order); + } + + @Override + public int receiveOrderBySystem() { + // 1. 查询过期的待支付订单 + LocalDateTime expireTime = minusTime(tradeOrderProperties.getReceiveExpireTime()); + List orders = tradeOrderMapper.selectListByStatusAndDeliveryTimeLt( + TradeOrderStatusEnum.DELIVERED.getStatus(), expireTime); + if (CollUtil.isEmpty(orders)) { + return 0; + } + + // 2. 遍历执行,逐个取消 + int count = 0; + for (TradeOrderDO order : orders) { + try { + getSelf().receiveOrderBySystem(order); + count++; + } catch (Throwable e) { + log.error("[receiveOrderBySystem][order({}) 自动收货订单异常]", order.getId(), e); + } + } + return count; + } + + /** + * 自动收货单个订单 + * + * @param order 订单 + */ + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.SYSTEM_RECEIVE) + public void receiveOrderBySystem(TradeOrderDO order) { + receiveOrder0(order); + } + + /** + * 收货订单的核心实现 + * + * @param order 订单 + */ + private void receiveOrder0(TradeOrderDO order) { + // 更新 TradeOrderDO 状态为已完成 + int updateCount = tradeOrderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), + new TradeOrderDO().setStatus(TradeOrderStatusEnum.COMPLETED.getStatus()).setReceiveTime(LocalDateTime.now())); + if (updateCount == 0) { + throw exception(ORDER_RECEIVE_FAIL_STATUS_NOT_DELIVERED); + } + + // 插入订单日志 + TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.COMPLETED.getStatus()); + } + + /** + * 校验交易订单满足可售货的条件 + *

+ * 1. 交易订单待收货 + * + * @param userId 用户编号 + * @param id 交易订单编号 + * @return 交易订单 + */ + private TradeOrderDO validateOrderReceivable(Long userId, Long id) { + // 校验订单是否存在 + TradeOrderDO order = tradeOrderMapper.selectByIdAndUserId(id, userId); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 校验订单是否是待收货状态 + if (!TradeOrderStatusEnum.isDelivered(order.getStatus())) { + throw exception(ORDER_RECEIVE_FAIL_STATUS_NOT_DELIVERED); + } + return order; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.MEMBER_CANCEL) + public void cancelOrderByMember(Long userId, Long id) { + // 1.1 校验存在 + TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(id, userId); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 1.2 校验状态 + if (ObjectUtil.notEqual(order.getStatus(), TradeOrderStatusEnum.UNPAID.getStatus())) { + throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID); + } + + // 2. 取消订单 + cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL); + } + + @Override + public int cancelOrderBySystem() { + // 1. 查询过期的待支付订单 + LocalDateTime expireTime = minusTime(tradeOrderProperties.getPayExpireTime()); + List orders = tradeOrderMapper.selectListByStatusAndCreateTimeLt( + TradeOrderStatusEnum.UNPAID.getStatus(), expireTime); + if (CollUtil.isEmpty(orders)) { + return 0; + } + + // 2. 遍历执行,逐个取消 + int count = 0; + for (TradeOrderDO order : orders) { + try { + getSelf().cancelOrderBySystem(order); + count++; + } catch (Throwable e) { + log.error("[cancelOrderBySystem][order({}) 过期订单异常]", order.getId(), e); + } + } + return count; + } + + /** + * 自动取消单个订单 + * + * @param order 订单 + */ + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.SYSTEM_CANCEL) + public void cancelOrderBySystem(TradeOrderDO order) { + cancelOrder0(order, TradeOrderCancelTypeEnum.PAY_TIMEOUT); + } + + /** + * 取消订单的核心实现 + * + * @param order 订单 + * @param cancelType 取消类型 + */ + private void cancelOrder0(TradeOrderDO order, TradeOrderCancelTypeEnum cancelType) { + // 1. 更新 TradeOrderDO 状态为已取消 + int updateCount = tradeOrderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), + new TradeOrderDO().setStatus(TradeOrderStatusEnum.CANCELED.getStatus()) + .setCancelType(cancelType.getType()).setCancelTime(LocalDateTime.now())); + if (updateCount == 0) { + throw exception(ORDER_CANCEL_FAIL_STATUS_NOT_UNPAID); + } + + // 2. 执行 TradeOrderHandler 的后置处理 + List orderItems = tradeOrderItemMapper.selectListByOrderId(order.getId()); + tradeOrderHandlers.forEach(handler -> handler.afterCancelOrder(order, orderItems)); + + // 3. 增加订单日志 + TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.CANCELED.getStatus()); + } + + /** + * 如果金额全部被退款,则取消订单 + * 如果还有未被退款的金额,则无需取消订单 + * + * @param order 订单 + * @param refundPrice 退款金额 + */ + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.ADMIN_CANCEL_AFTER_SALE) + public void cancelOrderByAfterSale(TradeOrderDO order, Integer refundPrice) { + // 1. 更新订单 + if (refundPrice < order.getPayPrice()) { + return; + } + tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()) + .setStatus(TradeOrderStatusEnum.CANCELED.getStatus()) + .setCancelType(TradeOrderCancelTypeEnum.AFTER_SALE_CLOSE.getType()).setCancelTime(LocalDateTime.now())); + + // 2. 执行 TradeOrderHandler 的后置处理 + List orderItems = tradeOrderItemMapper.selectListByOrderId(order.getId()); + tradeOrderHandlers.forEach(handler -> handler.afterCancelOrder(order, orderItems)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.MEMBER_DELETE) + public void deleteOrder(Long userId, Long id) { + // 1.1 校验存在 + TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(id, userId); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 1.2 校验状态 + if (ObjectUtil.notEqual(order.getStatus(), TradeOrderStatusEnum.CANCELED.getStatus())) { + throw exception(ORDER_DELETE_FAIL_STATUS_NOT_CANCEL); + } + // 2. 删除订单 + tradeOrderMapper.deleteById(id); + + // 3. 记录日志 + TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus()); + } + + @Override + public void updateOrderRemark(TradeOrderRemarkReqVO reqVO) { + // 校验并获得交易订单 + validateOrderExists(reqVO.getId()); + + // 更新 + TradeOrderDO order = TradeOrderConvert.INSTANCE.convert(reqVO); + tradeOrderMapper.updateById(order); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.ADMIN_UPDATE_PRICE) + public void updateOrderPrice(TradeOrderUpdatePriceReqVO reqVO) { + // 1.1 校验交易订单 + TradeOrderDO order = validateOrderExists(reqVO.getId()); + if (order.getPayStatus()) { + throw exception(ORDER_UPDATE_PRICE_FAIL_PAID); + } + // 1.2 校验调价金额是否变化 + if (order.getAdjustPrice() > 0) { + throw exception(ORDER_UPDATE_PRICE_FAIL_ALREADY); + } + // 1.3 支付价格不能为 0 + int newPayPrice = order.getPayPrice() + reqVO.getAdjustPrice(); + if (newPayPrice <= 0) { + throw exception(ORDER_UPDATE_PRICE_FAIL_PRICE_ERROR); + } + + // 2. 更新订单 + tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()) + .setAdjustPrice(reqVO.getAdjustPrice() + order.getAdjustPrice()).setPayPrice(newPayPrice)); + + // 3. 更新 TradeOrderItem,需要做 adjustPrice 的分摊 + List orderOrderItems = tradeOrderItemMapper.selectListByOrderId(order.getId()); + List dividePrices = TradePriceCalculatorHelper.dividePrice2(orderOrderItems, reqVO.getAdjustPrice()); + List updateItems = new ArrayList<>(); + for (int i = 0; i < orderOrderItems.size(); i++) { + TradeOrderItemDO item = orderOrderItems.get(i); + updateItems.add(new TradeOrderItemDO().setId(item.getId()) + .setAdjustPrice(item.getAdjustPrice() + dividePrices.get(i)) + .setPayPrice(item.getPayPrice() + dividePrices.get(i))); + } + tradeOrderItemMapper.updateBatch(updateItems); + + // 4. 更新支付订单 + payOrderApi.updatePayOrderPrice(order.getPayOrderId(), newPayPrice).checkError(); + + // 5. 记录订单日志 + TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus(), + MapUtil.builder().put("oldPayPrice", MoneyUtils.fenToYuanStr(order.getPayPrice())) + .put("adjustPrice", MoneyUtils.fenToYuanStr(reqVO.getAdjustPrice())) + .put("newPayPrice", MoneyUtils.fenToYuanStr(newPayPrice)).build()); + } + + @Override + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.ADMIN_UPDATE_ADDRESS) + public void updateOrderAddress(TradeOrderUpdateAddressReqVO reqVO) { + // 校验交易订单 + TradeOrderDO order = validateOrderExists(reqVO.getId()); + // 只有待发货状态,才可以修改订单收货地址; + if (!TradeOrderStatusEnum.isUndelivered(order.getStatus())) { + throw exception(ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED); + } + + // 更新 + tradeOrderMapper.updateById(TradeOrderConvert.INSTANCE.convert(reqVO)); + + // 记录订单日志 + TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus()); + } + + @Override + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.ADMIN_PICK_UP_RECEIVE) + public void pickUpOrderByAdmin(Long userId, Long id) { + getSelf().pickUpOrder(userId, tradeOrderMapper.selectById(id)); + } + + @Override + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.ADMIN_PICK_UP_RECEIVE) + public void pickUpOrderByAdmin(Long userId, String pickUpVerifyCode) { + getSelf().pickUpOrder(userId, tradeOrderMapper.selectOneByPickUpVerifyCode(pickUpVerifyCode)); + } + + @Override + public TradeOrderDO getByPickUpVerifyCode(String pickUpVerifyCode) { + return tradeOrderMapper.selectOneByPickUpVerifyCode(pickUpVerifyCode); + } + + @Transactional(rollbackFor = Exception.class) + public void pickUpOrder(Long userId, TradeOrderDO order) { + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + if (ObjUtil.notEqual(DeliveryTypeEnum.PICK_UP.getType(), order.getDeliveryType())) { + throw exception(ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP); + } + DeliveryPickUpStoreDO deliveryPickUpStore = pickUpStoreService.getDeliveryPickUpStore(order.getPickUpStoreId()); + if (deliveryPickUpStore == null + || !CollUtil.contains(deliveryPickUpStore.getVerifyUserIds(), userId)) { + throw exception(ORDER_PICK_UP_FAIL_NOT_VERIFY_USER); + } + + receiveOrder0(order); + } + + // =================== Order Item =================== + + @Override + public void updateOrderItemWhenAfterSaleCreate(Long id, Long afterSaleId) { + // 更新订单项 + updateOrderItemAfterSaleStatus(id, TradeOrderItemAfterSaleStatusEnum.NONE.getStatus(), + TradeOrderItemAfterSaleStatusEnum.APPLY.getStatus(), afterSaleId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateOrderItemWhenAfterSaleSuccess(Long id, Integer refundPrice) { + // 1.1 更新订单项 + updateOrderItemAfterSaleStatus(id, TradeOrderItemAfterSaleStatusEnum.APPLY.getStatus(), + TradeOrderItemAfterSaleStatusEnum.SUCCESS.getStatus(), null); + // 1.2 执行 TradeOrderHandler 的后置处理 + TradeOrderItemDO orderItem = tradeOrderItemMapper.selectById(id); + TradeOrderDO order = tradeOrderMapper.selectById(orderItem.getOrderId()); + tradeOrderHandlers.forEach(handler -> handler.afterCancelOrderItem(order, orderItem)); + + // 2.1 更新订单的退款金额、积分 + Integer orderRefundPrice = order.getRefundPrice() + refundPrice; + Integer orderRefundPoint = order.getRefundPoint() + orderItem.getUsePoint(); + Integer refundStatus = isAllOrderItemAfterSaleSuccess(order.getId()) ? + TradeOrderRefundStatusEnum.ALL.getStatus() // 如果都售后成功,则需要取消订单 + : TradeOrderRefundStatusEnum.PART.getStatus(); + tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()) + .setRefundStatus(refundStatus) + .setRefundPrice(orderRefundPrice).setRefundPoint(orderRefundPoint)); + // 2.2 如果全部退款,则进行取消订单 + getSelf().cancelOrderByAfterSale(order, orderRefundPrice); + } + + @Override + public void updateOrderItemWhenAfterSaleCancel(Long id) { + // 更新订单项 + updateOrderItemAfterSaleStatus(id, TradeOrderItemAfterSaleStatusEnum.APPLY.getStatus(), + TradeOrderItemAfterSaleStatusEnum.NONE.getStatus(), null); + } + + private void updateOrderItemAfterSaleStatus(Long id, Integer oldAfterSaleStatus, Integer newAfterSaleStatus, + Long afterSaleId) { + // 更新订单项 + int updateCount = tradeOrderItemMapper.updateAfterSaleStatus(id, oldAfterSaleStatus, newAfterSaleStatus, afterSaleId); + if (updateCount <= 0) { + throw exception(ORDER_ITEM_UPDATE_AFTER_SALE_STATUS_FAIL); + } + + } + + /** + * 判断指定订单的所有订单项,是不是都售后成功 + * + * @param id 订单编号 + * @return 是否都售后成功 + */ + private boolean isAllOrderItemAfterSaleSuccess(Long id) { + List orderItems = tradeOrderItemMapper.selectListByOrderId(id); + return orderItems.stream().allMatch(orderItem -> Objects.equals(orderItem.getAfterSaleStatus(), + TradeOrderItemAfterSaleStatusEnum.SUCCESS.getStatus())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.MEMBER_COMMENT) + public Long createOrderItemCommentByMember(Long userId, AppTradeOrderItemCommentCreateReqVO createReqVO) { + // 1.1 先通过订单项 ID,查询订单项是否存在 + TradeOrderItemDO orderItem = tradeOrderItemMapper.selectByIdAndUserId(createReqVO.getOrderItemId(), userId); + if (orderItem == null) { + throw exception(ORDER_ITEM_NOT_FOUND); + } + // 1.2 校验订单相关状态 + TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderItem.getOrderId(), userId); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + if (ObjectUtil.notEqual(order.getStatus(), TradeOrderStatusEnum.COMPLETED.getStatus())) { + throw exception(ORDER_COMMENT_FAIL_STATUS_NOT_COMPLETED); + } + if (ObjectUtil.notEqual(order.getCommentStatus(), Boolean.FALSE)) { + throw exception(ORDER_COMMENT_STATUS_NOT_FALSE); + } + + // 2. 创建评价 + Long commentId = createOrderItemComment0(orderItem, createReqVO); + + // 3. 如果订单项都评论了,则更新订单评价状态 + List orderItems = tradeOrderItemMapper.selectListByOrderId(order.getId()); + if (!anyMatch(orderItems, item -> Objects.equals(item.getCommentStatus(), Boolean.FALSE))) { + tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()).setCommentStatus(Boolean.TRUE) + .setFinishTime(LocalDateTime.now())); + // 增加订单日志。注意:只有在所有订单项都评价后,才会增加 + TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus()); + } + return commentId; + } + + @Override + public int createOrderItemCommentBySystem() { + // 1. 查询过期的待支付订单 + LocalDateTime expireTime = minusTime(tradeOrderProperties.getCommentExpireTime()); + List orders = tradeOrderMapper.selectListByStatusAndReceiveTimeLt( + TradeOrderStatusEnum.COMPLETED.getStatus(), expireTime, false); + if (CollUtil.isEmpty(orders)) { + return 0; + } + + // 2. 遍历执行,逐个取消 + int count = 0; + for (TradeOrderDO order : orders) { + try { + getSelf().createOrderItemCommentBySystemBySystem(order); + count++; + } catch (Throwable e) { + log.error("[createOrderItemCommentBySystem][order({}) 过期订单异常]", order.getId(), e); + } + } + return count; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateOrderCombinationInfo(Long orderId, Long activityId, Long combinationRecordId, Long headId) { + tradeOrderMapper.updateById( + new TradeOrderDO().setId(orderId).setCombinationActivityId(activityId) + .setCombinationRecordId(combinationRecordId).setCombinationHeadId(headId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelPaidOrder(Long userId, Long orderId, Integer cancelType) { + // 1.1 这里校验下 cancelType 只允许拼团关闭; + if (ObjUtil.notEqual(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getType(), cancelType)) { + return; + } + // 1.2 检验订单存在 + TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + + // 1.3 校验订单是否支付 + if (!order.getPayStatus()) { + throw exception(ORDER_CANCEL_PAID_FAIL, "已支付"); + } + // 1.3 校验订单是否未退款 + if (ObjUtil.notEqual(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) { + throw exception(ORDER_CANCEL_PAID_FAIL, "未退款"); + } + + // 2.1 取消订单 + cancelOrder0(order, TradeOrderCancelTypeEnum.COMBINATION_CLOSE); + // 2.2 创建退款单 + payRefundApi.createRefund(new PayRefundCreateReqDTO() + .setAppKey(tradeOrderProperties.getPayAppKey()) // 支付应用 + .setUserIp(NetUtil.getLocalhostStr()) // 使用本机 IP,因为是服务器发起退款的 + .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 + .setMerchantRefundId(String.valueOf(order.getId())) + .setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice())).checkError(); // 价格信息 + } + + @Override + public void updateOrderGiveCouponIds(Long userId, Long orderId, List giveCouponIds) { + // 1. 检验订单存在 + TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + + // 2. 更新订单赠送的优惠券编号列表 + tradeOrderMapper.updateById(new TradeOrderDO().setId(orderId).setGiveCouponIds(giveCouponIds)); + } + + /** + * 创建单个订单的评论 + * + * @param order 订单 + */ + @Transactional(rollbackFor = Exception.class) + @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.SYSTEM_COMMENT) + public void createOrderItemCommentBySystemBySystem(TradeOrderDO order) { + // 1. 查询未评论的订单项 + List orderItems = tradeOrderItemMapper.selectListByOrderIdAndCommentStatus( + order.getId(), Boolean.FALSE); + if (CollUtil.isEmpty(orderItems)) { + return; + } + + // 2. 逐个评论 + for (TradeOrderItemDO orderItem : orderItems) { + // 2.1 创建评价 + AppTradeOrderItemCommentCreateReqVO commentCreateReqVO = new AppTradeOrderItemCommentCreateReqVO() + .setOrderItemId(orderItem.getId()).setAnonymous(false).setContent("") + .setBenefitScores(5).setDescriptionScores(5); + createOrderItemComment0(orderItem, commentCreateReqVO); + + // 2.2 更新订单项评价状态 + tradeOrderItemMapper.updateById(new TradeOrderItemDO().setId(orderItem.getId()).setCommentStatus(Boolean.TRUE)); + } + + // 3. 所有订单项都评论了,则更新订单评价状态 + tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()).setCommentStatus(Boolean.TRUE) + .setFinishTime(LocalDateTime.now())); + // 增加订单日志。注意:只有在所有订单项都评价后,才会增加 + TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus()); + } + + /** + * 创建订单项的评论的核心实现 + * + * @param orderItem 订单项 + * @param createReqVO 评论内容 + * @return 评论编号 + */ + private Long createOrderItemComment0(TradeOrderItemDO orderItem, AppTradeOrderItemCommentCreateReqVO createReqVO) { + // 1. 创建评价 + ProductCommentCreateReqDTO productCommentCreateReqDTO = TradeOrderConvert.INSTANCE.convert04(createReqVO, orderItem); + Long commentId = productCommentApi.createComment(productCommentCreateReqDTO).getCheckedData(); + + // 2. 更新订单项评价状态 + tradeOrderItemMapper.updateById(new TradeOrderItemDO().setId(orderItem.getId()).setCommentStatus(Boolean.TRUE)); + return commentId; + } + + // =================== 营销相关的操作 =================== + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private TradeOrderUpdateServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/bo/TradeOrderLogCreateReqBO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/bo/TradeOrderLogCreateReqBO.java new file mode 100644 index 0000000..297e55a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/bo/TradeOrderLogCreateReqBO.java @@ -0,0 +1,54 @@ +package com.tashow.cloud.trade.service.order.bo; + +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * 订单日志的创建 Request BO + * + * @author 陈賝 + * @since 2023/7/6 15:27 + */ +@Data +public class TradeOrderLogCreateReqBO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 订单编号 + */ + @NotNull(message = "订单编号不能为空") + private Long orderId; + /** + * 操作前状态 + */ + private Integer beforeStatus; + /** + * 操作后状态 + */ + @NotNull(message = "操作后的状态不能为空") + private Integer afterStatus; + + /** + * 操作类型 + */ + @NotNull(message = "操作类型不能为空") + private Integer operateType; + /** + * 操作明细 + */ + @NotEmpty(message = "操作明细不能为空") + private String content; + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeBargainOrderHandler.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeBargainOrderHandler.java new file mode 100644 index 0000000..f94a386 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeBargainOrderHandler.java @@ -0,0 +1,78 @@ +package com.tashow.cloud.trade.service.order.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.promotion.api.bargain.BargainActivityApi; +import cn.iocoder.yudao.module.promotion.api.bargain.BargainRecordApi; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.List; + +/** + * 砍价订单的 {@link TradeOrderHandler} 实现类 + * + * @author HUIHUI + */ +@Component +public class TradeBargainOrderHandler implements TradeOrderHandler { + + @Resource + private BargainActivityApi bargainActivityApi; + @Resource + private BargainRecordApi bargainRecordApi; + + @Override + public void beforeOrderCreate(TradeOrderDO order, List orderItems) { + if (!TradeOrderTypeEnum.isBargain(order.getType())) { + return; + } + // 明确校验一下 + Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品"); + + // 扣减砍价活动的库存 + bargainActivityApi.updateBargainActivityStock(order.getBargainActivityId(), + -orderItems.get(0).getCount()).checkError(); + } + + @Override + public void afterOrderCreate(TradeOrderDO order, List orderItems) { + if (!TradeOrderTypeEnum.isBargain(order.getType())) { + return; + } + // 明确校验一下 + Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品"); + + // 记录砍价记录对应的订单编号 + bargainRecordApi.updateBargainRecordOrderId(order.getBargainRecordId(), order.getId()).checkError(); + } + + @Override + public void afterCancelOrder(TradeOrderDO order, List orderItems) { + if (!TradeOrderTypeEnum.isBargain(order.getType())) { + return; + } + // 明确校验一下 + Assert.isTrue(orderItems.size() == 1, "砍价时,只允许选择一个商品"); + + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚 + orderItems = filterOrderItemListByNoneAfterSale(orderItems); + if (CollUtil.isEmpty(orderItems)) { + return; + } + afterCancelOrderItem(order, orderItems.get(0)); + } + + @Override + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) { + if (!TradeOrderTypeEnum.isBargain(order.getType())) { + return; + } + // 恢复(增加)砍价活动的库存 + bargainActivityApi.updateBargainActivityStock(order.getBargainActivityId(), orderItem.getCount()).checkError(); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeBrokerageOrderHandler.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeBrokerageOrderHandler.java new file mode 100644 index 0000000..915cd4d --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeBrokerageOrderHandler.java @@ -0,0 +1,114 @@ +package com.tashow.cloud.trade.service.order.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; +import com.tashow.cloud.trade.convert.order.TradeOrderConvert; +import com.tashow.cloud.trade.dal.dataobject.brokerage.BrokerageUserDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum; +import com.tashow.cloud.trade.service.brokerage.BrokerageRecordService; +import com.tashow.cloud.trade.service.brokerage.BrokerageUserService; +import com.tashow.cloud.trade.service.brokerage.bo.BrokerageAddReqBO; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 订单分销的 {@link TradeOrderHandler} 实现类 + * + * @author 芋道源码 + */ +@Component +public class TradeBrokerageOrderHandler implements TradeOrderHandler { + + @Resource + private MemberUserApi memberUserApi; + @Resource + private ProductSpuApi productSpuApi; + @Resource + private ProductSkuApi productSkuApi; + + @Resource + private BrokerageRecordService brokerageRecordService; + @Resource + private BrokerageUserService brokerageUserService; + + @Override + public void beforeOrderCreate(TradeOrderDO order, List orderItems) { + // 设置订单推广人 + BrokerageUserDO brokerageUser = brokerageUserService.getBrokerageUser(order.getUserId()); + if (brokerageUser != null && brokerageUser.getBindUserId() != null) { + order.setBrokerageUserId(brokerageUser.getBindUserId()); + } + } + + @Override + public void afterPayOrder(TradeOrderDO order, List orderItems) { + if (order.getBrokerageUserId() == null) { + return; + } + addBrokerage(order.getUserId(), orderItems); + } + + @Override + public void afterCancelOrder(TradeOrderDO order, List orderItems) { + // 如果是未支付的订单,不会产生分销结果,所以直接 return + if (!order.getPayStatus()) { + return; + } + if (order.getBrokerageUserId() == null) { + return; + } + + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚 + orderItems = filterOrderItemListByNoneAfterSale(orderItems); + if (CollUtil.isEmpty(orderItems)) { + return; + } + orderItems.forEach(orderItem -> afterCancelOrderItem(order, orderItem)); + } + + @Override + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) { + if (order.getBrokerageUserId() == null) { + return; + } + brokerageRecordService.cancelBrokerage(BrokerageRecordBizTypeEnum.ORDER, String.valueOf(orderItem.getId())); + } + + /** + * 创建分销记录 + *

+ * 目前是支付成功后,就会创建分销记录。 + *

+ * 业内还有两种做法,可以根据自己的业务调整: + * 1. 确认收货后,才创建分销记录 + * 2. 支付 or 下单成功时,创建分销记录(冻结),确认收货解冻或者 n 天后解冻 + * + * @param userId 用户编号 + * @param orderItems 订单项 + */ + protected void addBrokerage(Long userId, List orderItems) { + MemberUserRespDTO user = memberUserApi.getUser(userId).getCheckedData(); + Assert.notNull(user); + ProductSpuRespDTO spu = productSpuApi.getSpu(orderItems.get(0).getSpuId()).getCheckedData(); + Assert.notNull(spu); + ProductSkuRespDTO sku = productSkuApi.getSku(orderItems.get(0).getSkuId()).getCheckedData(); + + // 每一个订单项,都会去生成分销记录 + List addList = convertList(orderItems, + item -> TradeOrderConvert.INSTANCE.convert(user, item, spu, sku)); + brokerageRecordService.addBrokerage(userId, BrokerageRecordBizTypeEnum.ORDER, addList); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeCombinationOrderHandler.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeCombinationOrderHandler.java new file mode 100644 index 0000000..a83c79a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeCombinationOrderHandler.java @@ -0,0 +1,97 @@ +package com.tashow.cloud.trade.service.order.handler; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateRespDTO; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO; +import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum; +import com.tashow.cloud.trade.convert.order.TradeOrderConvert; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.service.order.TradeOrderQueryService; +import com.tashow.cloud.trade.service.order.TradeOrderUpdateService; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_CREATE_FAIL_EXIST_UNPAID; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS; + +/** + * 拼团订单的 {@link TradeOrderHandler} 实现类 + * + * @author HUIHUI + */ +@Component +public class TradeCombinationOrderHandler implements TradeOrderHandler { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private TradeOrderUpdateService orderUpdateService; + @Resource + private TradeOrderQueryService orderQueryService; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private CombinationRecordApi combinationRecordApi; + + @Override + public void beforeOrderCreate(TradeOrderDO order, List orderItems) { + // 如果不是拼团订单则结束 + if (!TradeOrderTypeEnum.isCombination(order.getType())) { + return; + } + Assert.isTrue(orderItems.size() == 1, "拼团时,只允许选择一个商品"); + + // 1. 校验是否满足拼团活动相关限制 + TradeOrderItemDO item = orderItems.get(0); + combinationRecordApi.validateCombinationRecord(order.getUserId(), order.getCombinationActivityId(), + order.getCombinationHeadId(), item.getSkuId(), item.getCount()).checkError(); + + // 2. 校验该用户是否存在未支付的拼团活动订单,避免一个拼团可以下多个单子了 + TradeOrderDO activityOrder = orderQueryService.getOrderByUserIdAndStatusAndCombination( + order.getUserId(), order.getCombinationActivityId(), TradeOrderStatusEnum.UNPAID.getStatus()); + if (activityOrder != null) { + throw exception(ORDER_CREATE_FAIL_EXIST_UNPAID); + } + } + + @Override + public void afterPayOrder(TradeOrderDO order, List orderItems) { + // 1.如果不是拼团订单则结束 + if (!TradeOrderTypeEnum.isCombination(order.getType())) { + return; + } + Assert.isTrue(orderItems.size() == 1, "拼团时,只允许选择一个商品"); + + // 2. 创建拼团记录 + TradeOrderItemDO item = orderItems.get(0); + CombinationRecordCreateRespDTO combinationRecord = combinationRecordApi.createCombinationRecord( + TradeOrderConvert.INSTANCE.convert(order, item)).getCheckedData(); + + // 3. 更新拼团相关信息到订单。为什么几个字段都要更新? + // 原因是:如果创建订单时自己是团长的情况下 combinationHeadId 是为 null 的,设置团长编号这个操作时在订单是否后创建拼团记录时才设置的。 + orderUpdateService.updateOrderCombinationInfo(order.getId(), order.getCombinationActivityId(), + combinationRecord.getCombinationRecordId(), combinationRecord.getCombinationHeadId()); + } + + @Override + public void beforeDeliveryOrder(TradeOrderDO order) { + if (!TradeOrderTypeEnum.isCombination(order.getType())) { + return; + } + // 校验订单拼团是否成功 + CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId(order.getUserId(), order.getId()).getCheckedData(); + Assert.notNull(combinationRecord, "订单({})对应的拼团记录不存在", order.getId()); + if (!CombinationRecordStatusEnum.isSuccess(combinationRecord.getStatus())) { + throw exception(ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS); + } + } + +} + diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeCouponOrderHandler.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeCouponOrderHandler.java new file mode 100644 index 0000000..3af8668 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeCouponOrderHandler.java @@ -0,0 +1,76 @@ +package com.tashow.cloud.trade.service.order.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; +import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import com.tashow.cloud.trade.service.order.TradeOrderQueryService; +import com.tashow.cloud.trade.service.order.TradeOrderUpdateService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 优惠劵的 {@link TradeOrderHandler} 实现类 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class TradeCouponOrderHandler implements TradeOrderHandler { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private TradeOrderUpdateService orderUpdateService; + @Resource + private TradeOrderQueryService orderQueryService; + + @Resource + private CouponApi couponApi; + + @Override + public void afterOrderCreate(TradeOrderDO order, List orderItems) { + if (order.getCouponId() == null || order.getCouponId() <= 0) { + return; + } + // 不在前置扣减的原因,是因为优惠劵要记录使用的订单号 + couponApi.useCoupon(new CouponUseReqDTO().setId(order.getCouponId()).setUserId(order.getUserId()) + .setOrderId(order.getId())).checkError(); + } + + @Override + public void afterPayOrder(TradeOrderDO order, List orderItems) { + if (CollUtil.isEmpty(order.getGiveCouponTemplateCounts())) { + return; + } + // 赠送优惠券 + try { + List couponIds = couponApi.takeCouponsByAdmin(order.getGiveCouponTemplateCounts(), order.getUserId()).getCheckedData(); + if (CollUtil.isEmpty(couponIds)) { + return; + } + orderUpdateService.updateOrderGiveCouponIds(order.getUserId(), order.getId(), couponIds); + } catch (Exception e) { + log.error("[afterPayOrder][order({}) 赠送优惠券({})失败,需要手工补偿]", order.getId(), order.getGiveCouponTemplateCounts(), e); + } + } + + @Override + public void afterCancelOrder(TradeOrderDO order, List orderItems) { + // 情况一:退还订单使用的优惠券 + if (order.getCouponId() != null && order.getCouponId() > 0) { + // 退回优惠劵 + couponApi.returnUsedCoupon(order.getCouponId()); + } + // 情况二:收回赠送的优惠券 + if (CollUtil.isEmpty(order.getGiveCouponIds())) { + return; + } + couponApi.invalidateCouponsByAdmin(order.getGiveCouponIds(), order.getUserId()).checkError(); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeMemberPointOrderHandler.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeMemberPointOrderHandler.java new file mode 100644 index 0000000..1dd3b64 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeMemberPointOrderHandler.java @@ -0,0 +1,118 @@ +package com.tashow.cloud.trade.service.order.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.member.api.level.MemberLevelApi; +import cn.iocoder.yudao.module.member.api.point.MemberPointApi; +import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum; +import cn.iocoder.yudao.module.member.enums.point.MemberPointBizTypeEnum; +import com.tashow.cloud.trade.dal.dataobject.aftersale.AfterSaleDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import com.tashow.cloud.trade.service.aftersale.AfterSaleService; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue; + +/** + * 会员积分、等级的 {@link TradeOrderHandler} 实现类 + * + * @author owen + */ +@Component +public class TradeMemberPointOrderHandler implements TradeOrderHandler { + + @Resource + private MemberPointApi memberPointApi; + @Resource + private MemberLevelApi memberLevelApi; + + @Resource + private AfterSaleService afterSaleService; + + @Override + public void afterOrderCreate(TradeOrderDO order, List orderItems) { + // 扣减用户积分(订单抵扣)。不在前置扣减的原因,是因为积分扣减时,需要记录关联业务 + reducePoint(order.getUserId(), order.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE, order.getId()); + } + + @Override + public void afterPayOrder(TradeOrderDO order, List orderItems) { + // 增加用户积分(订单赠送) + addPoint(order.getUserId(), order.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE, + order.getId()); + + // 增加用户经验 + memberLevelApi.addExperience(order.getUserId(), order.getPayPrice(), + MemberExperienceBizTypeEnum.ORDER_GIVE.getType(), String.valueOf(order.getId())).checkError(); + } + + @Override + public void afterCancelOrder(TradeOrderDO order, List orderItems) { + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚 + orderItems = filterOrderItemListByNoneAfterSale(orderItems); + if (CollUtil.isEmpty(orderItems)) { + return; + } + + // 增加(回滚)用户积分(订单抵扣) + Integer usePoint = getSumValue(orderItems, TradeOrderItemDO::getUsePoint, Integer::sum); + addPoint(order.getUserId(), usePoint, MemberPointBizTypeEnum.ORDER_USE_CANCEL, + order.getId()); + + // 如下的返还,需要经过支持,也就是经历 afterPayOrder 流程 + if (!order.getPayStatus()) { + return; + } + // 扣减(回滚)积分(订单赠送) + Integer givePoint = getSumValue(orderItems, TradeOrderItemDO::getGivePoint, Integer::sum); + reducePoint(order.getUserId(), givePoint, MemberPointBizTypeEnum.ORDER_GIVE_CANCEL, + order.getId()); + // 扣减(回滚)用户经验 + int payPrice = order.getPayPrice() - order.getRefundPrice(); + memberLevelApi.addExperience(order.getUserId(), payPrice, + MemberExperienceBizTypeEnum.ORDER_GIVE_CANCEL.getType(), String.valueOf(order.getId())).checkError(); + } + + @Override + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) { + // 增加(回滚)积分(订单抵扣) + addPoint(order.getUserId(), orderItem.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE_CANCEL_ITEM, orderItem.getId()); + // 扣减(回滚)积分(订单赠送) + reducePoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE_CANCEL_ITEM, orderItem.getId()); + + // 扣减(回滚)用户经验 + AfterSaleDO afterSale = afterSaleService.getAfterSale(orderItem.getAfterSaleId()); + memberLevelApi.reduceExperience(order.getUserId(), afterSale.getRefundPrice(), + MemberExperienceBizTypeEnum.ORDER_GIVE_CANCEL_ITEM.getType(), String.valueOf(orderItem.getId())).checkError(); + } + + /** + * 添加用户积分 + *

+ * 目前是支付成功后,就会创建积分记录。 + *

+ * 业内还有两种做法,可以根据自己的业务调整: + * 1. 确认收货后,才创建积分记录 + * 2. 支付 or 下单成功时,创建积分记录(冻结),确认收货解冻或者 n 天后解冻 + * + * @param userId 用户编号 + * @param point 增加积分数量 + * @param bizType 业务编号 + * @param bizId 业务编号 + */ + protected void addPoint(Long userId, Integer point, MemberPointBizTypeEnum bizType, Long bizId) { + if (point != null && point > 0) { + memberPointApi.addPoint(userId, point, bizType.getType(), String.valueOf(bizId)); + } + } + + protected void reducePoint(Long userId, Integer point, MemberPointBizTypeEnum bizType, Long bizId) { + if (point != null && point > 0) { + memberPointApi.reducePoint(userId, point, bizType.getType(), String.valueOf(bizId)).checkError(); + } + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeOrderHandler.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeOrderHandler.java new file mode 100644 index 0000000..0fd426a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeOrderHandler.java @@ -0,0 +1,78 @@ +package com.tashow.cloud.trade.service.order.handler; + +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum; + +import java.util.List; + +/** + * 订单活动特殊逻辑处理器 handler 接口 + * 提供订单生命周期钩子接口;订单创建前、订单创建后、订单支付后、订单取消 + * + * @author HUIHUI + */ +public interface TradeOrderHandler { + + /** + * 订单创建前 + * + * @param order 订单 + * @param orderItems 订单项 + */ + default void beforeOrderCreate(TradeOrderDO order, List orderItems) {} + + /** + * 订单创建后 + * + * @param order 订单 + * @param orderItems 订单项 + */ + default void afterOrderCreate(TradeOrderDO order, List orderItems) {} + + /** + * 支付订单后 + * + * @param order 订单 + * @param orderItems 订单项 + */ + default void afterPayOrder(TradeOrderDO order, List orderItems) {} + + /** + * 订单取消后 + * + * @param order 订单 + * @param orderItems 订单项 + */ + default void afterCancelOrder(TradeOrderDO order, List orderItems) {} + + /** + * 订单项取消后 + * + * @param order 订单 + * @param orderItem 订单项 + */ + default void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {} + + /** + * 订单发货前 + * + * @param order 订单 + */ + default void beforeDeliveryOrder(TradeOrderDO order) {} + + // ========== 公用方法 ========== + + /** + * 过滤“未售后”的订单项列表 + * + * @param orderItems 订单项列表 + * @return 过滤后的订单项列表 + */ + default List filterOrderItemListByNoneAfterSale(List orderItems) { + return CollectionUtils.filterList(orderItems, + item -> TradeOrderItemAfterSaleStatusEnum.isNone(item.getAfterSaleStatus())); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradePointOrderHandler.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradePointOrderHandler.java new file mode 100644 index 0000000..66337d9 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradePointOrderHandler.java @@ -0,0 +1,83 @@ +package com.tashow.cloud.trade.service.order.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS; + +/** + * 积分商城活动订单的 {@link TradeOrderHandler} 实现类 + * + * @author HUIHUI + */ +@Component +public class TradePointOrderHandler implements TradeOrderHandler { + + @Resource + private PointActivityApi pointActivityApi; + @Resource + private MemberUserApi memberUserApi; + + @Override + public void beforeOrderCreate(TradeOrderDO order, List orderItems) { + if (!TradeOrderTypeEnum.isPoint(order.getType())) { + return; + } + // 明确校验一下 + Assert.isTrue(orderItems.size() == 1, "积分商城活动兑换商品兑换时,只允许选择一个商品"); + // 校验用户剩余积分是否足够兑换商品 + MemberUserRespDTO user = memberUserApi.getUser(order.getUserId()).getCheckedData(); + if (user.getPoint() < order.getUsePoint()) { + throw exception(ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS); + } + + // 扣减积分商城活动的库存 + pointActivityApi.updatePointStockDecr(order.getPointActivityId(), + orderItems.get(0).getSkuId(), orderItems.get(0).getCount()).checkError(); + + // 如果支付金额为 0,则直接设置为已支付 + if (Objects.equals(order.getPayPrice(), 0)) { + order.setPayStatus(true).setStatus(TradeOrderStatusEnum.UNDELIVERED.getStatus()); + } + } + + @Override + public void afterCancelOrder(TradeOrderDO order, List orderItems) { + if (!TradeOrderTypeEnum.isPoint(order.getType())) { + return; + } + // 明确校验一下 + Assert.isTrue(orderItems.size() == 1, "积分商城活动兑换商品兑换时,只允许选择一个商品"); + + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚 + orderItems = filterOrderItemListByNoneAfterSale(orderItems); + if (CollUtil.isEmpty(orderItems)) { + return; + } + afterCancelOrderItem(order, orderItems.get(0)); + } + + @Override + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) { + if (!TradeOrderTypeEnum.isPoint(order.getType())) { + return; + } + // 恢复积分商城活动的库存 + pointActivityApi.updatePointStockIncr(order.getPointActivityId(), + orderItem.getSkuId(), orderItem.getCount()).checkError(); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeProductSkuOrderHandler.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeProductSkuOrderHandler.java new file mode 100644 index 0000000..be2ff51 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeProductSkuOrderHandler.java @@ -0,0 +1,46 @@ +package com.tashow.cloud.trade.service.order.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; +import com.tashow.cloud.trade.convert.order.TradeOrderConvert; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.List; + +import static java.util.Collections.singletonList; + +/** + * 商品 SKU 库存的 {@link TradeOrderHandler} 实现类 + * + * @author 芋道源码 + */ +@Component +public class TradeProductSkuOrderHandler implements TradeOrderHandler { + + @Resource + private ProductSkuApi productSkuApi; + + @Override + public void beforeOrderCreate(TradeOrderDO order, List orderItems) { + productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convertNegative(orderItems)).checkError(); + } + + @Override + public void afterCancelOrder(TradeOrderDO order, List orderItems) { + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚 + orderItems = filterOrderItemListByNoneAfterSale(orderItems); + if (CollUtil.isEmpty(orderItems)) { + return; + } + productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(orderItems)).checkError(); + } + + @Override + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) { + productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(singletonList(orderItem))).checkError(); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeSeckillOrderHandler.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeSeckillOrderHandler.java new file mode 100644 index 0000000..7fb0122 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/order/handler/TradeSeckillOrderHandler.java @@ -0,0 +1,64 @@ +package com.tashow.cloud.trade.service.order.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.promotion.api.seckill.SeckillActivityApi; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderDO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.List; + +/** + * 秒杀订单的 {@link TradeOrderHandler} 实现类 + * + * @author HUIHUI + */ +@Component +public class TradeSeckillOrderHandler implements TradeOrderHandler { + + @Resource + private SeckillActivityApi seckillActivityApi; + + @Override + public void beforeOrderCreate(TradeOrderDO order, List orderItems) { + if (!TradeOrderTypeEnum.isSeckill(order.getType())) { + return; + } + // 明确校验一下 + Assert.isTrue(orderItems.size() == 1, "秒杀时,只允许选择一个商品"); + + // 扣减秒杀活动的库存 + seckillActivityApi.updateSeckillStockDecr(order.getSeckillActivityId(), + orderItems.get(0).getSkuId(), orderItems.get(0).getCount()).checkError(); + } + + @Override + public void afterCancelOrder(TradeOrderDO order, List orderItems) { + if (!TradeOrderTypeEnum.isSeckill(order.getType())) { + return; + } + // 明确校验一下 + Assert.isTrue(orderItems.size() == 1, "秒杀时,只允许选择一个商品"); + + // 售后的订单项,已经在 afterCancelOrderItem 回滚库存,所以这里不需要重复回滚 + orderItems = filterOrderItemListByNoneAfterSale(orderItems); + if (CollUtil.isEmpty(orderItems)) { + return; + } + afterCancelOrderItem(order, orderItems.get(0)); + } + + @Override + public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) { + if (!TradeOrderTypeEnum.isSeckill(order.getType())) { + return; + } + // 恢复秒杀活动的库存 + seckillActivityApi.updateSeckillStockIncr(order.getSeckillActivityId(), + orderItem.getSkuId(), orderItem.getCount()).checkError(); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/TradePriceService.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/TradePriceService.java new file mode 100644 index 0000000..232e479 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/TradePriceService.java @@ -0,0 +1,34 @@ +package com.tashow.cloud.trade.service.price; + +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeProductSettlementRespVO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 价格计算 Service 接口 + * + * @author 芋道源码 + */ +public interface TradePriceService { + + /** + * 【订单】价格计算 + * + * @param calculateReqDTO 计算信息 + * @return 计算结果 + */ + TradePriceCalculateRespBO calculateOrderPrice(@Valid TradePriceCalculateReqBO calculateReqDTO); + + /** + * 【商品】价格计算,用于商品列表、商品详情 + * + * @param userId 用户编号,允许为空 + * @param spuIds 商品 SPU 编号数组 + * @return 计算结果 + */ + List calculateProductPrice(Long userId, List spuIds); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/TradePriceServiceImpl.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/TradePriceServiceImpl.java new file mode 100644 index 0000000..a876b7c --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/TradePriceServiceImpl.java @@ -0,0 +1,155 @@ +package com.tashow.cloud.trade.service.price; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO; +import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; +import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi; +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; +import cn.iocoder.yudao.module.promotion.api.discount.DiscountActivityApi; +import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO; +import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; +import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import com.tashow.cloud.trade.controller.app.order.vo.AppTradeProductSettlementRespVO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import com.tashow.cloud.trade.service.price.calculator.TradeDiscountActivityPriceCalculator; +import com.tashow.cloud.trade.service.price.calculator.TradePriceCalculator; +import com.tashow.cloud.trade.service.price.calculator.TradePriceCalculatorHelper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +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.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS; +import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_PAY_PRICE_ILLEGAL; + +/** + * 价格计算 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class TradePriceServiceImpl implements TradePriceService { + + @Resource + private ProductSkuApi productSkuApi; + @Resource + private ProductSpuApi productSpuApi; + @Resource + private DiscountActivityApi discountActivityApi; + @Resource + private RewardActivityApi rewardActivityApi; + + @Resource + private List priceCalculators; + + @Resource + private TradeDiscountActivityPriceCalculator discountActivityPriceCalculator; + + @Override + public TradePriceCalculateRespBO calculateOrderPrice(TradePriceCalculateReqBO calculateReqBO) { + // 1.1 获得商品 SKU 数组 + List skuList = checkSkuList(calculateReqBO); + // 1.2 获得商品 SPU 数组 + List spuList = checkSpuList(skuList); + + // 2.1 计算价格 + TradePriceCalculateRespBO calculateRespBO = TradePriceCalculatorHelper + .buildCalculateResp(calculateReqBO, spuList, skuList); + priceCalculators.forEach(calculator -> calculator.calculate(calculateReqBO, calculateRespBO)); + // 2.2 如果最终支付金额小于等于 0,则抛出业务异常 + if (calculateReqBO.getPointActivityId() == null // 积分订单,允许支付金额为 0 + && calculateRespBO.getPrice().getPayPrice() <= 0) { + log.error("[calculatePrice][价格计算不正确,请求 calculateReqDTO({}),结果 priceCalculate({})]", + calculateReqBO, calculateRespBO); + throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL); + } + return calculateRespBO; + } + + private List checkSkuList(TradePriceCalculateReqBO reqBO) { + // 获得商品 SKU 数组 + Map skuIdCountMap = convertMap(reqBO.getItems(), + TradePriceCalculateReqBO.Item::getSkuId, TradePriceCalculateReqBO.Item::getCount); + List skus = productSkuApi.getSkuList(skuIdCountMap.keySet()).getCheckedData(); + + // 校验商品 SKU + skus.forEach(sku -> { + Integer count = skuIdCountMap.get(sku.getId()); + if (count == null) { + throw exception(SKU_NOT_EXISTS); + } + if (count > sku.getStock()) { + throw exception(SKU_STOCK_NOT_ENOUGH); + } + }); + return skus; + } + + private List checkSpuList(List skuList) { + return productSpuApi.validateSpuList(convertSet(skuList, ProductSkuRespDTO::getSpuId)).getCheckedData(); + } + + @Override + public List calculateProductPrice(Long userId, List spuIds) { + // 1.1 获得 SPU 与 SKU 的映射 + List allSkuList = productSkuApi.getSkuListBySpuId(spuIds).getCheckedData(); + Map> spuIdAndSkuListMap = convertMultiMap(allSkuList, ProductSkuRespDTO::getSpuId); + // 1.2 获得会员等级 + MemberLevelRespDTO level = discountActivityPriceCalculator.getMemberLevel(userId); + // 1.3 获得限时折扣活动 + Map skuIdAndDiscountMap = convertMap( + discountActivityApi.getMatchDiscountProductListBySkuIds(convertSet(allSkuList, ProductSkuRespDTO::getId)).getCheckedData(), + DiscountProductRespDTO::getSkuId); + // 1.4 获得满减送活动 + List rewardActivityMap = rewardActivityApi.getMatchRewardActivityListBySpuIds(spuIds).getCheckedData(); + + // 2. 价格计算 + return convertList(spuIds, spuId -> { + AppTradeProductSettlementRespVO spuVO = new AppTradeProductSettlementRespVO().setSpuId(spuId); + // 2.1 优惠价格 + List skuList = spuIdAndSkuListMap.get(spuId); + List skuVOList = convertList(skuList, sku -> { + AppTradeProductSettlementRespVO.Sku skuVO = new AppTradeProductSettlementRespVO.Sku() + .setId(sku.getId()); + TradePriceCalculateRespBO.OrderItem orderItem = new TradePriceCalculateRespBO.OrderItem() + .setPayPrice(sku.getPrice()).setCount(1); + // 计算限时折扣的优惠价格 + DiscountProductRespDTO discountProduct = skuIdAndDiscountMap.get(sku.getId()); + Integer discountPrice = discountActivityPriceCalculator.calculateActivityPrice(discountProduct, orderItem); + // 计算 VIP 优惠金额 + Integer vipPrice = discountActivityPriceCalculator.calculateVipPrice(level, orderItem); + if (discountPrice <= 0 && vipPrice <= 0) { + return skuVO; + } + // 选择一个大的优惠 + if (discountPrice > vipPrice) { + return skuVO.setPromotionPrice(sku.getPrice() - discountPrice) + .setPromotionType(PromotionTypeEnum.DISCOUNT_ACTIVITY.getType()) + .setPromotionId(discountProduct.getId()).setPromotionEndTime(discountProduct.getActivityEndTime()); + } else { + return skuVO.setPromotionPrice(sku.getPrice() - vipPrice) + .setPromotionType(PromotionTypeEnum.MEMBER_LEVEL.getType()); + } + }); + spuVO.setSkus(skuVOList); + // 2.2 满减送活动 + RewardActivityMatchRespDTO rewardActivity = CollUtil.findOne(rewardActivityMap, + activity -> CollUtil.contains(activity.getSpuIds(), spuId)); + spuVO.setRewardActivity(BeanUtils.toBean(rewardActivity, AppTradeProductSettlementRespVO.RewardActivity.class)); + return spuVO; + }); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/bo/TradePriceCalculateReqBO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/bo/TradePriceCalculateReqBO.java new file mode 100644 index 0000000..83643ae --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/bo/TradePriceCalculateReqBO.java @@ -0,0 +1,125 @@ +package com.tashow.cloud.trade.service.price.bo; + +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * 价格计算 Request BO + * + * @author yudao源码 + */ +@Data +public class TradePriceCalculateReqBO { + + /** + * 用户编号 + * + * 对应 MemberUserDO 的 id 编号 + */ + private Long userId; + + /** + * 优惠劵编号 + * + * 对应 CouponDO 的 id 编号 + */ + private Long couponId; + + /** + * 是否使用积分 + */ + @NotNull(message = "是否使用积分不能为空") + private Boolean pointStatus; + + /** + * 配送方式 + * + * 枚举 {@link DeliveryTypeEnum} + */ + private Integer deliveryType; + /** + * 收货地址编号 + * + * 对应 MemberAddressDO 的 id 编号 + */ + private Long addressId; + /** + * 自提门店编号 + * + * 对应 PickUpStoreDO 的 id 编号 + */ + private Long pickUpStoreId; + + /** + * 商品 SKU 数组 + */ + @NotNull(message = "商品数组不能为空") + private List items; + + // ========== 秒杀活动相关字段 ========== + /** + * 秒杀活动编号 + */ + private Long seckillActivityId; + + // ========== 拼团活动相关字段 ========== + /** + * 拼团活动编号 + */ + private Long combinationActivityId; + + /** + * 拼团团长编号 + */ + private Long combinationHeadId; + + // ========== 砍价活动相关字段 ========== + /** + * 砍价记录编号 + */ + private Long bargainRecordId; + + // ========== 积分商城活动相关字段 ========== + /** + * 积分商城活动编号 + */ + private Long pointActivityId; + + /** + * 商品 SKU + */ + @Data + @Valid + public static class Item { + + /** + * SKU 编号 + */ + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + /** + * SKU 数量 + */ + @NotNull(message = "商品 SKU 数量不能为空") + @Min(value = 0L, message = "商品 SKU 数量必须大于等于 0") + private Integer count; + + /** + * 购物车项的编号 + */ + private Long cartId; + + /** + * 是否选中 + */ + @NotNull(message = "是否选中不能为空") + private Boolean selected; + + } +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/bo/TradePriceCalculateRespBO.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/bo/TradePriceCalculateRespBO.java new file mode 100644 index 0000000..c33d66b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/bo/TradePriceCalculateRespBO.java @@ -0,0 +1,405 @@ +package com.tashow.cloud.trade.service.price.bo; + +import cn.iocoder.yudao.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 价格计算 Response BO + * + * 整体设计,参考 taobao 的技术文档: + * 1. 订单管理 + * 2. 常用订单金额说明 + * + * @author 芋道源码 + */ +@Data +public class TradePriceCalculateRespBO { + + /** + * 订单类型 + * + * 枚举 {@link TradeOrderTypeEnum} + */ + private Integer type; + + /** + * 订单价格 + */ + private Price price; + + /** + * 订单项数组 + */ + private List items; + + /** + * 营销活动数组 + * + * 只对应 {@link Price#items} 商品匹配的活动 + */ + private List promotions; + + /** + * 使用的优惠劵编号 + */ + private Long couponId; + /** + * 用户的优惠劵列表(可用 + 不可用) + */ + private List coupons; + + /** + * 会员剩余积分 + */ + private Integer totalPoint; + /** + * 使用的积分 + */ + private Integer usePoint; + + /** + * 赠送的积分 + */ + private Integer givePoint; + + /** + * 砍价活动编号 + */ + private Long bargainActivityId; + + /** + * 是否包邮 + */ + private Boolean freeDelivery; + + /** + * 赠送的优惠劵 + * + * key: 优惠劵模版编号 + * value:对应的优惠券数量 + * + * 目的:用于订单支付后赠送优惠券 + */ + private Map giveCouponTemplateCounts; + + /** + * 订单价格 + */ + @Data + public static class Price { + + /** + * 商品原价(总),单位:分 + * + * 基于 {@link OrderItem#getPrice()} * {@link OrderItem#getCount()} 求和 + * + * 对应 taobao 的 trade.total_fee 字段 + */ + private Integer totalPrice; + /** + * 订单优惠(总),单位:分 + * + * 对应 taobao 的 order.discount_fee 字段 + */ + private Integer discountPrice; + /** + * 运费金额,单位:分 + */ + private Integer deliveryPrice; + /** + * 优惠劵减免金额(总),单位:分 + * + * 对应 taobao 的 trade.coupon_fee 字段 + */ + private Integer couponPrice; + /** + * 积分抵扣的金额,单位:分 + * + * 对应 taobao 的 trade.point_fee 字段 + */ + private Integer pointPrice; + /** + * VIP 减免金额,单位:分 + */ + private Integer vipPrice; + /** + * 最终购买金额(总),单位:分 + * + * = {@link #totalPrice} + * - {@link #couponPrice} + * - {@link #pointPrice} + * - {@link #discountPrice} + * + {@link #deliveryPrice} + * - {@link #vipPrice} + */ + private Integer payPrice; + + } + + /** + * 订单商品 SKU + */ + @Data + public static class OrderItem { + + /** + * SPU 编号 + */ + private Long spuId; + /** + * SKU 编号 + */ + private Long skuId; + /** + * 购买数量 + */ + private Integer count; + /** + * 购物车项的编号 + */ + private Long cartId; + /** + * 是否选中 + */ + private Boolean selected; + + /** + * 商品原价(单),单位:分 + * + * 对应 ProductSkuDO 的 price 字段 + * 对应 taobao 的 order.price 字段 + */ + private Integer price; + /** + * 优惠金额(总),单位:分 + * + * 对应 taobao 的 order.discount_fee 字段 + */ + private Integer discountPrice; + /** + * 运费金额(总),单位:分 + */ + private Integer deliveryPrice; + /** + * 优惠劵减免金额,单位:分 + * + * 对应 taobao 的 trade.coupon_fee 字段 + */ + private Integer couponPrice; + /** + * 积分抵扣的金额,单位:分 + * + * 对应 taobao 的 trade.point_fee 字段 + */ + private Integer pointPrice; + /** + * 使用的积分 + */ + private Integer usePoint; + /** + * VIP 减免金额,单位:分 + */ + private Integer vipPrice; + /** + * 应付金额(总),单位:分 + * + * = {@link #price} * {@link #count} + * - {@link #couponPrice} + * - {@link #pointPrice} + * - {@link #discountPrice} + * + {@link #deliveryPrice} + * - {@link #vipPrice} + */ + private Integer payPrice; + + // ========== 商品 SPU 信息 ========== + /** + * 商品名 + */ + private String spuName; + /** + * 商品图片 + * + * 优先级:SKU.picUrl > SPU.picUrl + */ + private String picUrl; + /** + * 分类编号 + */ + private Long categoryId; + + // ========== 物流相关字段 ========= + + /** + * 配送方式数组 + * + * 对应 DeliveryTypeEnum 枚举 + */ + private List deliveryTypes; + + /** + * 物流配置模板编号 + * + * 对应 TradeDeliveryExpressTemplateDO 的 id 编号 + */ + private Long deliveryTemplateId; + + // ========== 商品 SKU 信息 ========== + /** + * 商品重量,单位:kg 千克 + */ + private Double weight; + /** + * 商品体积,单位:m^3 平米 + */ + private Double volume; + + /** + * 商品属性数组 + */ + private List properties; + + /** + * 赠送的积分 + */ + private Integer givePoint; + + } + + /** + * 营销明细 + */ + @Data + public static class Promotion { + + /** + * 营销编号 + * + * 例如说:营销活动的编号、优惠劵的编号 + */ + private Long id; + /** + * 营销名字 + */ + private String name; + /** + * 营销类型 + * + * 枚举 {@link PromotionTypeEnum} + */ + private Integer type; + /** + * 计算时的原价(总),单位:分 + */ + private Integer totalPrice; + /** + * 计算时的优惠(总),单位:分 + */ + private Integer discountPrice; + /** + * 匹配的商品 SKU 数组 + */ + private List items; + + // ========== 匹配情况 ========== + + /** + * 是否满足优惠条件 + */ + private Boolean match; + /** + * 满足条件的提示 + * + * 如果 {@link #match} = true 满足,则提示“圣诞价:省 150.00 元” + * 如果 {@link #match} = false 不满足,则提示“购满 85 元,可减 40 元” + */ + private String description; + + } + + /** + * 营销匹配的商品 SKU + */ + @Data + public static class PromotionItem { + + /** + * 商品 SKU 编号 + */ + private Long skuId; + /** + * 计算时的原价(总),单位:分 + */ + private Integer totalPrice; + /** + * 计算时的优惠(总),单位:分 + */ + private Integer discountPrice; + + } + + /** + * 优惠劵信息 + */ + @Data + public static class Coupon { + + /** + * 优惠劵编号 + */ + private Long id; + /** + * 优惠劵名 + */ + private String name; + + /** + * 是否设置满多少金额可用,单位:分 + */ + private Integer usePrice; + + /** + * 生效开始时间 + */ + private LocalDateTime validStartTime; + /** + * 生效结束时间 + */ + private LocalDateTime validEndTime; + + /** + * 优惠类型 + */ + private Integer discountType; + /** + * 折扣百分比 + */ + private Integer discountPercent; + /** + * 优惠金额,单位:分 + */ + private Integer discountPrice; + /** + * 折扣上限,单位:分 + */ + private Integer discountLimitPrice; + + /** + * 是否匹配 + */ + private Boolean match; + /** + * 不匹配的原因 + */ + private String mismatchReason; + + } + + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeBargainActivityPriceCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeBargainActivityPriceCalculator.java new file mode 100644 index 0000000..aae3752 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeBargainActivityPriceCalculator.java @@ -0,0 +1,58 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.promotion.api.bargain.BargainRecordApi; +import cn.iocoder.yudao.module.promotion.api.bargain.dto.BargainValidateJoinRespDTO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +// TODO huihui:单测需要补充 +/** + * 砍价活动的 {@link TradePriceCalculator} 实现类 + * + * @author 芋道源码 + */ +@Component +@Order(TradePriceCalculator.ORDER_BARGAIN_ACTIVITY) +public class TradeBargainActivityPriceCalculator implements TradePriceCalculator { + + @Resource + private BargainRecordApi bargainRecordApi; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 1. 判断订单类型和是否具有拼团记录编号 + if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.BARGAIN.getType())) { + return; + } + Assert.isTrue(param.getItems().size() == 1, "砍价时,只允许选择一个商品"); + Assert.isTrue(param.getItems().get(0).getCount() == 1, "砍价时,只允许选择一个商品"); + // 2. 校验是否可以参与砍价 + TradePriceCalculateRespBO.OrderItem orderItem = result.getItems().get(0); + BargainValidateJoinRespDTO bargainActivity = bargainRecordApi.validateJoinBargain( + param.getUserId(), param.getBargainRecordId(), orderItem.getSkuId()).getCheckedData(); + + // 3.1 记录优惠明细 + Integer discountPrice = orderItem.getPayPrice() - bargainActivity.getBargainPrice() * orderItem.getCount(); + // TODO 芋艿:极端情况,优惠金额为负数,需要处理 + TradePriceCalculatorHelper.addPromotion(result, orderItem, + param.getSeckillActivityId(), bargainActivity.getName(), PromotionTypeEnum.BARGAIN_ACTIVITY.getType(), + StrUtil.format("砍价活动:省 {} 元", TradePriceCalculatorHelper.formatPrice(discountPrice)), + discountPrice); + // 3.2 更新 SKU 优惠金额 + orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice); + TradePriceCalculatorHelper.recountPayPrice(orderItem); + TradePriceCalculatorHelper.recountAllPrice(result); + // 4. 特殊:设置对应的砍价活动编号 + result.setBargainActivityId(bargainActivity.getActivityId()); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeCombinationActivityPriceCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeCombinationActivityPriceCalculator.java new file mode 100644 index 0000000..432ac7f --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeCombinationActivityPriceCalculator.java @@ -0,0 +1,54 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationValidateJoinRespDTO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +// TODO @puhui999:单测可以后补下 + +/** + * 拼团活动的 {@link TradePriceCalculator} 实现类 + * + * @author HUIHUI + */ +@Component +@Order(TradePriceCalculator.ORDER_COMBINATION_ACTIVITY) +public class TradeCombinationActivityPriceCalculator implements TradePriceCalculator { + + @Resource + private CombinationRecordApi combinationRecordApi; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 1. 判断订单类型和是否具有拼团活动编号 + if (param.getCombinationActivityId() == null) { + return; + } + Assert.isTrue(param.getItems().size() == 1, "拼团时,只允许选择一个商品"); + // 2. 校验是否可以参与拼团 + TradePriceCalculateRespBO.OrderItem orderItem = result.getItems().get(0); + CombinationValidateJoinRespDTO combinationActivity = combinationRecordApi.validateJoinCombination( + param.getUserId(), param.getCombinationActivityId(), param.getCombinationHeadId(), + orderItem.getSkuId(), orderItem.getCount()).getCheckedData(); + + // 3.1 记录优惠明细 + Integer discountPrice = orderItem.getPayPrice() - combinationActivity.getCombinationPrice() * orderItem.getCount(); + TradePriceCalculatorHelper.addPromotion(result, orderItem, + param.getCombinationActivityId(), combinationActivity.getName(), PromotionTypeEnum.COMBINATION_ACTIVITY.getType(), + StrUtil.format("拼团活动:省 {} 元", TradePriceCalculatorHelper.formatPrice(discountPrice)), + discountPrice); + // 3.2 更新 SKU 优惠金额 + orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice); + TradePriceCalculatorHelper.recountPayPrice(orderItem); + TradePriceCalculatorHelper.recountAllPrice(result); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeCouponPriceCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeCouponPriceCalculator.java new file mode 100644 index 0000000..3c3f745 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeCouponPriceCalculator.java @@ -0,0 +1,162 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi; +import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import jakarta.annotation.Resource; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.function.Predicate; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_CAN_NOT_USE; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER; + +/** + * 优惠劵的 {@link TradePriceCalculator} 实现类 + * + * @author 芋道源码 + */ +@Component +@Order(TradePriceCalculator.ORDER_COUPON) +public class TradeCouponPriceCalculator implements TradePriceCalculator { + + @Resource + private CouponApi couponApi; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 只有【普通】订单,才允许使用优惠劵 + if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) { + if (param.getCouponId() != null) { + throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER); + } + return; + } + + // 1.1 加载用户的优惠劵列表 + List coupons = couponApi.getCouponListByUserId(param.getUserId(), CouponStatusEnum.UNUSED.getStatus()).getCheckedData(); + coupons.removeIf(coupon -> LocalDateTimeUtils.beforeNow(coupon.getValidEndTime())); + // 1.2 计算优惠劵的使用条件 + result.setCoupons(calculateCoupons(coupons, result)); + + // 2. 校验优惠劵是否可用 + if (param.getCouponId() == null) { + return; + } + TradePriceCalculateRespBO.Coupon couponBO = CollUtil.findOne(result.getCoupons(), item -> item.getId().equals(param.getCouponId())); + CouponRespDTO coupon = CollUtil.findOne(coupons, item -> item.getId().equals(param.getCouponId())); + if (couponBO == null || coupon == null) { + throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, "优惠劵不存在"); + } + if (Boolean.FALSE.equals(couponBO.getMatch())) { + throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, couponBO.getMismatchReason()); + } + + // 3.1 计算可以优惠的金额 + List orderItems = filterMatchCouponOrderItems(result, coupon); + Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); + Integer couponPrice = getCouponPrice(coupon, totalPayPrice); + // 3.2 计算分摊的优惠金额 + List divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice); + + // 4.1 记录使用的优惠劵 + result.setCouponId(param.getCouponId()); + // 4.2 记录优惠明细 + TradePriceCalculatorHelper.addPromotion(result, orderItems, + param.getCouponId(), couponBO.getName(), PromotionTypeEnum.COUPON.getType(), + StrUtil.format("优惠劵:省 {} 元", TradePriceCalculatorHelper.formatPrice(couponPrice)), + divideCouponPrices); + // 4.3 更新 SKU 优惠金额 + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + orderItem.setCouponPrice(divideCouponPrices.get(i)); + TradePriceCalculatorHelper.recountPayPrice(orderItem); + } + TradePriceCalculatorHelper.recountAllPrice(result); + } + + /** + * 计算用户的优惠劵列表(可用 + 不可用) + * + * @param coupons 优惠劵 + * @param result 计算结果 + * @return 优惠劵列表 + */ + private List calculateCoupons(List coupons, + TradePriceCalculateRespBO result) { + return convertList(coupons, coupon -> { + TradePriceCalculateRespBO.Coupon matchCoupon = BeanUtils.toBean(coupon, TradePriceCalculateRespBO.Coupon.class); + // 1.1 优惠劵未到使用时间 + if (LocalDateTimeUtils.afterNow(coupon.getValidStartTime())) { + return matchCoupon.setMatch(false).setMismatchReason("优惠劵未到使用时间"); + } + // 1.2 优惠劵没有匹配的商品 + List orderItems = filterMatchCouponOrderItems(result, coupon); + if (CollUtil.isEmpty(orderItems)) { + return matchCoupon.setMatch(false).setMismatchReason("优惠劵没有匹配的商品"); + } + // 1.3 差 %1$,.2f 元可用优惠劵 + Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); + if (totalPayPrice < coupon.getUsePrice()) { + return matchCoupon.setMatch(false) + .setMismatchReason(String.format("差 %1$,.2f 元可用优惠劵", (coupon.getUsePrice() - totalPayPrice) / 100D)); + } + // 1.4 优惠金额超过订单金额 + Integer couponPrice = getCouponPrice(coupon, totalPayPrice); + if (couponPrice >= totalPayPrice) { + return matchCoupon.setMatch(false).setMismatchReason("优惠金额超过订单金额"); + } + + // 2. 满足条件 + return matchCoupon.setMatch(true); + }); + } + + private Integer getCouponPrice(CouponRespDTO coupon, Integer totalPayPrice) { + if (PromotionDiscountTypeEnum.PRICE.getType().equals(coupon.getDiscountType())) { // 减价 + return coupon.getDiscountPrice(); + } else if (PromotionDiscountTypeEnum.PERCENT.getType().equals(coupon.getDiscountType())) { // 打折 + int couponPrice = totalPayPrice - (totalPayPrice * coupon.getDiscountPercent() / 100); + return coupon.getDiscountLimitPrice() == null ? couponPrice + : Math.min(couponPrice, coupon.getDiscountLimitPrice()); // 优惠上限 + } + throw new IllegalArgumentException(String.format("优惠劵(%s) 的优惠类型不正确", coupon)); + } + + /** + * 获得优惠劵可使用的订单项(商品)列表 + * + * @param result 计算结果 + * @param coupon 优惠劵 + * @return 订单项(商品)列表 + */ + private List filterMatchCouponOrderItems(TradePriceCalculateRespBO result, + CouponRespDTO coupon) { + Predicate matchPredicate = TradePriceCalculateRespBO.OrderItem::getSelected; + if (PromotionProductScopeEnum.SPU.getScope().equals(coupon.getProductScope())) { + matchPredicate = matchPredicate // 额外加如下条件 + .and(orderItem -> coupon.getProductScopeValues().contains(orderItem.getSpuId())); + } else if (PromotionProductScopeEnum.CATEGORY.getScope().equals(coupon.getProductScope())) { + matchPredicate = matchPredicate // 额外加如下条件 + .and(orderItem -> coupon.getProductScopeValues().contains(orderItem.getCategoryId())); + } + return filterList(result.getItems(), matchPredicate); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeDeliveryPriceCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeDeliveryPriceCalculator.java new file mode 100644 index 0000000..89afbfd --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeDeliveryPriceCalculator.java @@ -0,0 +1,240 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.member.api.address.MemberAddressApi; +import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO; +import com.tashow.cloud.trade.dal.dataobject.config.TradeConfigDO; +import com.tashow.cloud.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryExpressChargeModeEnum; +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum; +import com.tashow.cloud.trade.service.config.TradeConfigService; +import com.tashow.cloud.trade.service.delivery.DeliveryExpressTemplateService; +import com.tashow.cloud.trade.service.delivery.DeliveryPickUpStoreService; +import com.tashow.cloud.trade.service.delivery.bo.DeliveryExpressTemplateRespBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; + +/** + * 运费的 {@link TradePriceCalculator} 实现类 + * + * @author jason + */ +@Component +@Order(TradePriceCalculator.ORDER_DELIVERY) +@Slf4j +public class TradeDeliveryPriceCalculator implements TradePriceCalculator { + + @Resource + private MemberAddressApi addressApi; + + @Resource + private DeliveryPickUpStoreService deliveryPickUpStoreService; + @Resource + private DeliveryExpressTemplateService deliveryExpressTemplateService; + @Resource + private TradeConfigService tradeConfigService; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + if (param.getDeliveryType() == null) { + return; + } + // 校验是不是存在商品不能门店自提,或者不能快递发货的情况。就是说,配送方式不匹配哈 + if (CollectionUtils.anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) { + throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL); + } + + if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) { + calculateByPickUp(param); + } else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) { + calculateExpress(param, result); + } + } + + private void calculateByPickUp(TradePriceCalculateReqBO param) { + if (param.getPickUpStoreId() == null) { + // 价格计算时,如果为空就不算~最终下单,会校验该字段不允许空 + return; + } + DeliveryPickUpStoreDO pickUpStore = deliveryPickUpStoreService.getDeliveryPickUpStore(param.getPickUpStoreId()); + if (pickUpStore == null || CommonStatusEnum.DISABLE.getStatus().equals(pickUpStore.getStatus())) { + throw exception(PICK_UP_STORE_NOT_EXISTS); + } + } + + // ========= 快递发货 ========== + + private void calculateExpress(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 0. 得到收件地址区域 + if (param.getAddressId() == null) { + // 价格计算时,如果为空就不算~最终下单,会校验该字段不允许空 + return; + } + MemberAddressRespDTO address = addressApi.getAddress(param.getAddressId(), param.getUserId()).getCheckedData(); + Assert.notNull(address, "收件人({})的地址,不能为空", param.getUserId()); + + // 情况一:全局包邮 + if (isGlobalExpressFree(result)) { + return; + } + + // 情况二:活动包邮 + if (Boolean.TRUE.equals(result.getFreeDelivery())) { + return; + } + + // 情况三:快递模版 + // 2.1 过滤出已选中的商品 SKU + List selectedItem = filterList(result.getItems(), OrderItem::getSelected); + Set deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId); + Map expressTemplateMap = + deliveryExpressTemplateService.getExpressTemplateMapByIdsAndArea(deliveryTemplateIds, address.getAreaId()); + // 2.2 计算配送费用 + if (CollUtil.isEmpty(expressTemplateMap)) { + log.error("[calculate][找不到商品 templateIds {} areaId{} 对应的运费模板]", deliveryTemplateIds, address.getAreaId()); + throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND); + } + calculateDeliveryPrice(selectedItem, expressTemplateMap, result); + } + + /** + * 是否全局包邮 + * + * @param result 计算结果 + * @return 是否包邮 + */ + private boolean isGlobalExpressFree(TradePriceCalculateRespBO result) { + TradeConfigDO config = tradeConfigService.getTradeConfig(); + // 情况一:交易中心配置不存在默认不包邮 + if (config == null) { + return false; + } + // 情况二:开启了全局包邮 && 满足包邮金额 + return Boolean.TRUE.equals(config.getDeliveryExpressFreeEnabled()) && + result.getPrice().getPayPrice() >= config.getDeliveryExpressFreePrice(); + } + + private void calculateDeliveryPrice(List selectedSkus, + Map expressTemplateMap, + TradePriceCalculateRespBO result) { + // 按商品运费模板来计算商品的运费:相同的运费模板可能对应多条订单商品 SKU + Map> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); + // 依次计算快递运费 + for (Map.Entry> entry : template2ItemMap.entrySet()) { + Long templateId = entry.getKey(); + List orderItems = entry.getValue(); + DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId); + if (templateBO == null) { + log.error("[calculateDeliveryPrice][不能计算快递运费,找不到 templateId({}) 对应的运费模板配置]", templateId); + continue; + } + // 1. 优先判断是否包邮。如果包邮不计算快递运费 + if (isExpressTemplateFree(orderItems, templateBO.getChargeMode(), templateBO.getFree())) { + continue; + } + // 2. 计算快递运费 + calculateExpressFeeByChargeMode(orderItems, templateBO.getChargeMode(), templateBO.getCharge()); + } + TradePriceCalculatorHelper.recountAllPrice(result); + } + + /** + * 按配送方式来计算运费 + * + * @param orderItems SKU 商品项目 + * @param chargeMode 配送计费方式 + * @param templateCharge 快递运费配置 + */ + private void calculateExpressFeeByChargeMode(List orderItems, Integer chargeMode, + DeliveryExpressTemplateRespBO.Charge templateCharge) { + if (templateCharge == null) { + log.error("[calculateExpressFeeByChargeMode][计算快递运费时,找不到 SKU({}) 对应的运费模版]", orderItems); + return; + } + double totalChargeValue = getTotalChargeValue(orderItems, chargeMode); + // 1. 计算 SKU 商品快递费用 + int deliveryPrice; + if (totalChargeValue <= templateCharge.getStartCount()) { + deliveryPrice = templateCharge.getStartPrice(); + } else { + double remainWeight = totalChargeValue - templateCharge.getStartCount(); + // 剩余重量/ 续件 = 续件的次数. 向上取整 + int extraNum = (int) Math.ceil(remainWeight / templateCharge.getExtraCount()); + int extraPrice = templateCharge.getExtraPrice() * extraNum; + deliveryPrice = templateCharge.getStartPrice() + extraPrice; + } + + // 2. 分摊快递费用到 SKU. 退费的时候,可能按照 SKU 考虑退费金额 + int remainPrice = deliveryPrice; + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem item = orderItems.get(i); + int partPrice; + double chargeValue = getChargeValue(item, chargeMode); + if (i < orderItems.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 + partPrice = (int) (deliveryPrice * (chargeValue / totalChargeValue)); + remainPrice -= partPrice; + } else { + partPrice = remainPrice; + } + Assert.isTrue(partPrice >= 0, "分摊金额必须大于等于 0"); + // 更新快递运费 + item.setDeliveryPrice(partPrice); + TradePriceCalculatorHelper.recountPayPrice(item); + } + } + + /** + * 检查是否包邮 + * + * @param chargeMode 配送计费方式 + * @param templateFree 包邮配置 + */ + private boolean isExpressTemplateFree(List orderItems, Integer chargeMode, + DeliveryExpressTemplateRespBO.Free templateFree) { + if (templateFree == null) { + return false; + } + double totalChargeValue = getTotalChargeValue(orderItems, chargeMode); + double totalPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); + return totalChargeValue >= templateFree.getFreeCount() && totalPrice >= templateFree.getFreePrice(); + } + + private double getTotalChargeValue(List orderItems, Integer chargeMode) { + double total = 0; + for (OrderItem orderItem : orderItems) { + total += getChargeValue(orderItem, chargeMode); + } + return total; + } + + private double getChargeValue(OrderItem orderItem, Integer chargeMode) { + DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode); + switch (chargeModeEnum) { + case COUNT: + return orderItem.getCount(); + case WEIGHT: + return orderItem.getWeight() != null ? orderItem.getWeight() * orderItem.getCount() : 0; + case VOLUME: + return orderItem.getVolume() != null ? orderItem.getVolume() * orderItem.getCount() : 0; + default: + throw new IllegalArgumentException(StrUtil.format("未知的计费模式({})", chargeMode)); + } + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java new file mode 100644 index 0000000..affc422 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java @@ -0,0 +1,154 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.module.member.api.level.MemberLevelApi; +import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.promotion.api.discount.DiscountActivityApi; +import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import jakarta.annotation.Resource; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.number.MoneyUtils.calculateRatePrice; +import static com.tashow.cloud.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice; + +/** + * 限时折扣的 {@link TradePriceCalculator} 实现类 + * + * 由于“会员折扣”和“限时折扣”是冲突,需要选择优惠金额多的,所以也放在这里计算 + * + * @author 芋道源码 + */ +@Component +@Order(TradePriceCalculator.ORDER_DISCOUNT_ACTIVITY) +public class TradeDiscountActivityPriceCalculator implements TradePriceCalculator { + + @Resource + private DiscountActivityApi discountActivityApi; + @Resource + private MemberLevelApi memberLevelApi; + @Resource + private MemberUserApi memberUserApi; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 0. 只有【普通】订单,才计算该优惠 + if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) { + return; + } + + // 1.1 获得 SKU 对应的限时折扣活动 + List discountProducts = discountActivityApi.getMatchDiscountProductListBySkuIds( + convertSet(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSkuId)).getCheckedData(); + Map discountProductMap = convertMap(discountProducts, DiscountProductRespDTO::getSkuId); + // 1.2 获得会员等级 + MemberLevelRespDTO level = getMemberLevel(param.getUserId()); + + // 2. 计算每个 SKU 的优惠金额 + result.getItems().forEach(orderItem -> { + if (!orderItem.getSelected()) { + return; + } + // 2.1 计算限时折扣的优惠金额 + DiscountProductRespDTO discountProduct = discountProductMap.get(orderItem.getSkuId()); + Integer discountPrice = calculateActivityPrice(discountProduct, orderItem); + // 2.2 计算 VIP 优惠金额 + Integer vipPrice = calculateVipPrice(level, orderItem); + if (discountPrice <= 0 && vipPrice <= 0) { + return; + } + + // 3. 选择优惠金额多的 + if (discountPrice > vipPrice) { + TradePriceCalculatorHelper.addPromotion(result, orderItem, + discountProduct.getActivityId(), discountProduct.getActivityName(), PromotionTypeEnum.DISCOUNT_ACTIVITY.getType(), + StrUtil.format("限时折扣:省 {} 元", formatPrice(discountPrice)), + discountPrice); + // 更新 SKU 优惠金额 + orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice); + } else { + assert level != null; + TradePriceCalculatorHelper.addPromotion(result, orderItem, + level.getId(), level.getName(), PromotionTypeEnum.MEMBER_LEVEL.getType(), + String.format("会员等级折扣:省 %s 元", formatPrice(vipPrice)), + vipPrice); + // 更新 SKU 的优惠金额 + orderItem.setVipPrice(vipPrice); + } + + // 4. 分摊优惠 + TradePriceCalculatorHelper.recountPayPrice(orderItem); + TradePriceCalculatorHelper.recountAllPrice(result); + }); + } + + /** + * 获得用户的等级 + * + * @param userId 用户编号 + * @return 用户等级 + */ + public MemberLevelRespDTO getMemberLevel(Long userId) { + MemberUserRespDTO user = memberUserApi.getUser(userId).getCheckedData(); + if (user == null || user.getLevelId() == null || user.getLevelId() <= 0) { + return null; + } + return memberLevelApi.getMemberLevel(user.getLevelId()).getCheckedData(); + } + + /** + * 计算优惠活动的价格 + * + * @param discount 优惠活动 + * @param orderItem 交易项 + * @return 优惠价格 + */ + public Integer calculateActivityPrice(DiscountProductRespDTO discount, + TradePriceCalculateRespBO.OrderItem orderItem) { + if (discount == null) { + return 0; + } + Integer newPrice = orderItem.getPayPrice(); + if (PromotionDiscountTypeEnum.PRICE.getType().equals(discount.getDiscountType())) { // 减价 + newPrice -= discount.getDiscountPrice() * orderItem.getCount(); + } else if (PromotionDiscountTypeEnum.PERCENT.getType().equals(discount.getDiscountType())) { // 打折 + newPrice = calculateRatePrice(orderItem.getPayPrice(), discount.getDiscountPercent() / 100.0); + } else { + throw new IllegalArgumentException(String.format("优惠活动的商品(%s) 的优惠类型不正确", discount)); + } + return orderItem.getPayPrice() - newPrice; + } + + /** + * 计算会员 VIP 的优惠价格 + * + * @param level 会员等级 + * @param orderItem 交易项 + * @return 优惠价格 + */ + public Integer calculateVipPrice(MemberLevelRespDTO level, + TradePriceCalculateRespBO.OrderItem orderItem) { + if (level == null + || CommonStatusEnum.isDisable(level.getStatus()) + || level.getDiscountPercent() == null) { + return 0; + } + Integer newPrice = calculateRatePrice(orderItem.getPayPrice(), level.getDiscountPercent().doubleValue()); + return orderItem.getPayPrice() - newPrice; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePointActivityPriceCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePointActivityPriceCalculator.java new file mode 100644 index 0000000..7245896 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePointActivityPriceCalculator.java @@ -0,0 +1,94 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi; +import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.service.order.TradeOrderQueryService; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT; + +/** + * 积分商城的 {@link TradePriceCalculator} 实现类 + * + * @author owen + */ +@Component +@Order(TradePriceCalculator.ORDER_POINT_ACTIVITY) +@Slf4j +public class TradePointActivityPriceCalculator implements TradePriceCalculator { + + @Resource + private PointActivityApi pointActivityApi; + @Resource + private MemberUserApi memberUserApi; + + @Resource + private TradeOrderQueryService tradeOrderQueryService; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 1.1 判断订单类型是否为积分商城活动 + if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.POINT.getType())) { + return; + } + // 1.2 初始化积分 + MemberUserRespDTO user = memberUserApi.getUser(param.getUserId()).getCheckedData(); + result.setTotalPoint(user.getPoint()).setUsePoint(0); + + // 1.3 校验用户积分余额 + if (user.getPoint() == null || user.getPoint() <= 0) { + return; + } + + Assert.isTrue(param.getItems().size() == 1, "积分商城兑换商品时,只允许选择一个商品"); + // 2. 校验是否可以参与积分商城活动 + TradePriceCalculateRespBO.OrderItem orderItem = result.getItems().get(0); + PointValidateJoinRespDTO activity = validateJoinPointActivity( + param.getUserId(), param.getPointActivityId(), + orderItem.getSkuId(), orderItem.getCount()); + + // 3.0 积分兑换前置校验 + Assert.isTrue(activity.getPoint() >= 1, "积分商城商品兑换积分必须大于 1"); + // 3.1 记录优惠明细 + int usePoint = activity.getPoint() * orderItem.getCount(); + result.setUsePoint(usePoint); + orderItem.setUsePoint(usePoint); + int discountPrice = orderItem.getPayPrice(); // 情况一:单使用积分兑换 + if (activity.getPrice() != null && activity.getPrice() > 0) { // 情况二:积分 + 金额 + discountPrice = orderItem.getPayPrice() - activity.getPrice() * orderItem.getCount(); + } + TradePriceCalculatorHelper.addPromotion(result, orderItem, + param.getPointActivityId(), "积分商城活动", PromotionTypeEnum.POINT.getType(), + StrUtil.format("积分商城活动:省 {} 元", TradePriceCalculatorHelper.formatPrice(discountPrice)), + discountPrice); + // 3.2 更新 SKU 优惠金额 + orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice); + TradePriceCalculatorHelper.recountPayPrice(orderItem); + TradePriceCalculatorHelper.recountAllPrice(result); + } + + private PointValidateJoinRespDTO validateJoinPointActivity(Long userId, Long activityId, Long skuId, Integer count) { + // 1. 校验是否可以参与积分商城活动 + PointValidateJoinRespDTO pointValidateJoinRespDTO = pointActivityApi.validateJoinPointActivity(activityId, skuId, count).getCheckedData(); + // 2. 校验总限购数量,目前只有 trade 有具体下单的数据,需要交给 trade 价格计算使用 + int pointProductCount = tradeOrderQueryService.getActivityProductCount(userId, activityId, TradeOrderTypeEnum.POINT); + if (pointProductCount + count > pointValidateJoinRespDTO.getCount()) { + throw exception(PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT); + } + return pointValidateJoinRespDTO; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePointGiveCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePointGiveCalculator.java new file mode 100644 index 0000000..8c82296 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePointGiveCalculator.java @@ -0,0 +1,63 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.util.BooleanUtil; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.module.member.api.config.MemberConfigApi; +import cn.iocoder.yudao.module.member.api.config.dto.MemberConfigRespDTO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.List; +import java.util.Optional; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; + +/** + * 赠送积分的 {@link TradePriceCalculator} 实现类 + * + * @author owen + */ +@Component +@Order(TradePriceCalculator.ORDER_POINT_GIVE) +@Slf4j +public class TradePointGiveCalculator implements TradePriceCalculator { + + @Resource + private MemberConfigApi memberConfigApi; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 1.1 校验积分功能是否开启 + int givePointPerYuan = Optional.ofNullable(memberConfigApi.getConfig().getCheckedData()) + .filter(config -> BooleanUtil.isTrue(config.getPointTradeDeductEnable())) + .map(MemberConfigRespDTO::getPointTradeGivePoint) + .orElse(0); + if (givePointPerYuan <= 0) { + return; + } + // 1.2 校验支付金额 + if (result.getPrice().getPayPrice() <= 0) { + return; + } + + // 2.1 计算赠送积分 + int givePoint = MoneyUtils.calculateRatePriceFloor(result.getPrice().getPayPrice(), (double) givePointPerYuan); + // 2.2 计算分摊的赠送积分 + List orderItems = filterList(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSelected); + List dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, givePoint); + + // 3.2 更新 SKU 赠送积分 + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + // 商品可能赠送了积分,所以这里要加上 + orderItem.setGivePoint(orderItem.getGivePoint() + dividePoints.get(i)); + } + // 3.3 更新订单赠送积分 + TradePriceCalculatorHelper.recountAllGivePoint(result); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePointUsePriceCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePointUsePriceCalculator.java new file mode 100644 index 0000000..ea63495 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePointUsePriceCalculator.java @@ -0,0 +1,117 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.member.api.config.MemberConfigApi; +import cn.iocoder.yudao.module.member.api.config.dto.MemberConfigRespDTO; +import cn.iocoder.yudao.module.member.api.user.MemberUserApi; +import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_PAY_PRICE_ILLEGAL; + +/** + * 使用积分的 {@link TradePriceCalculator} 实现类 + * + * @author owen + */ +@Component +@Order(TradePriceCalculator.ORDER_POINT_USE) +@Slf4j +public class TradePointUsePriceCalculator implements TradePriceCalculator { + + @Resource + private MemberConfigApi memberConfigApi; + @Resource + private MemberUserApi memberUserApi; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 判断订单类型是否不为积分商城活动 + if (ObjectUtil.equal(result.getType(), TradeOrderTypeEnum.POINT.getType())) { + return; + } + // 0. 初始化积分 + MemberUserRespDTO user = memberUserApi.getUser(param.getUserId()).getCheckedData(); + result.setTotalPoint(user.getPoint()).setUsePoint(0); + + // 1.1 校验是否使用积分 + if (!BooleanUtil.isTrue(param.getPointStatus())) { + return; + } + // 1.2 校验积分抵扣是否开启 + MemberConfigRespDTO config = memberConfigApi.getConfig().getCheckedData(); + if (!isDeductPointEnable(config)) { + return; + } + // 1.3 校验用户积分余额 + if (user.getPoint() == null || user.getPoint() <= 0) { + return; + } + + // 2.1 计算积分优惠金额 + int pointPrice = calculatePointPrice(config, user.getPoint(), result); + // 2.2 计算分摊的积分、抵扣金额 + List orderItems = filterList(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSelected); + List dividePointPrices = TradePriceCalculatorHelper.dividePrice(orderItems, pointPrice); + List divideUsePoints = TradePriceCalculatorHelper.dividePrice(orderItems, result.getUsePoint()); + + // 3.1 记录优惠明细 + TradePriceCalculatorHelper.addPromotion(result, orderItems, + param.getUserId(), "积分抵扣", PromotionTypeEnum.POINT.getType(), + StrUtil.format("积分抵扣:省 {} 元", TradePriceCalculatorHelper.formatPrice(pointPrice)), + dividePointPrices); + // 3.2 更新 SKU 优惠金额 + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + orderItem.setPointPrice(dividePointPrices.get(i)); + orderItem.setUsePoint(divideUsePoints.get(i)); + TradePriceCalculatorHelper.recountPayPrice(orderItem); + } + TradePriceCalculatorHelper.recountAllPrice(result); + } + + private boolean isDeductPointEnable(MemberConfigRespDTO config) { + return config != null && + BooleanUtil.isTrue(config.getPointTradeDeductEnable()) && // 积分功能是否启用 + config.getPointTradeDeductUnitPrice() != null && config.getPointTradeDeductUnitPrice() > 0; // 有没有配置:1 积分抵扣多少分 + } + + private Integer calculatePointPrice(MemberConfigRespDTO config, Integer usePoint, TradePriceCalculateRespBO result) { + // 每个订单最多可以使用的积分数量 + if (config.getPointTradeDeductMaxPrice() != null && config.getPointTradeDeductMaxPrice() > 0) { + usePoint = Math.min(usePoint, config.getPointTradeDeductMaxPrice()); + } + // TODO @疯狂:这里应该是,抵扣到只剩下 0.01; + // 积分优惠金额(分) + int pointPrice = usePoint * config.getPointTradeDeductUnitPrice(); + if (result.getPrice().getPayPrice() <= pointPrice) { + // 禁止 0 元购 + throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL); + } +// // 允许0 元购!!!:用户积分比较多时,积分可以抵扣的金额要大于支付金额,这时需要根据支付金额反推使用多少积分 +// if (result.getPrice().getPayPrice() < pointPrice) { +// pointPrice = result.getPrice().getPayPrice(); +// // 反推需要扣除的积分 +// usePoint = NumberUtil.toBigDecimal(pointPrice) +// .divide(NumberUtil.toBigDecimal(config.getPointTradeDeductUnitPrice()), 0, RoundingMode.HALF_UP) +// .intValue(); +// } + // 记录使用的积分 + result.setUsePoint(usePoint); + return pointPrice; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePriceCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePriceCalculator.java new file mode 100644 index 0000000..e2bfc7d --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePriceCalculator.java @@ -0,0 +1,40 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; + +/** + * 价格计算的计算器接口 + * + * 优惠计算顺序: + * 1. 积分抵现、会员价、优惠券、粉丝专享价、满减送哪个优先计算? + * + * @author 芋道源码 + */ +public interface TradePriceCalculator { + + int ORDER_SECKILL_ACTIVITY = 8; + int ORDER_BARGAIN_ACTIVITY = 8; + int ORDER_COMBINATION_ACTIVITY = 8; + int ORDER_POINT_ACTIVITY = 8; + + int ORDER_DISCOUNT_ACTIVITY = 10; + int ORDER_REWARD_ACTIVITY = 20; + int ORDER_COUPON = 30; + int ORDER_POINT_USE = 40; + /** + * 快递运费的计算 + * + * 放在各种营销活动、优惠劵后面 + */ + int ORDER_DELIVERY = 50; + /** + * 赠送积分,放最后 + * + * 放在 {@link #ORDER_DELIVERY} 后面的原因,是运费也会产生费用,需要赠送对应积分 + */ + int ORDER_POINT_GIVE = 999; + + void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result); + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePriceCalculatorHelper.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePriceCalculatorHelper.java new file mode 100644 index 0000000..10c5a2b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradePriceCalculatorHelper.java @@ -0,0 +1,345 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; +import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; +import com.tashow.cloud.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue; +import static java.util.Collections.singletonList; + +/** + * {@link TradePriceCalculator} 的工具类 + * + * 主要实现对 {@link TradePriceCalculateRespBO} 计算结果的操作 + * + * @author 芋道源码 + */ +public class TradePriceCalculatorHelper { + + public static TradePriceCalculateRespBO buildCalculateResp(TradePriceCalculateReqBO param, + List spuList, List skuList) { + // 创建 PriceCalculateRespDTO 对象 + TradePriceCalculateRespBO result = new TradePriceCalculateRespBO(); + result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>()); + + // 创建它的 OrderItem 属性 + result.setItems(new ArrayList<>(param.getItems().size())); + Map spuMap = convertMap(spuList, ProductSpuRespDTO::getId); + Map skuMap = convertMap(skuList, ProductSkuRespDTO::getId); + param.getItems().forEach(item -> { + ProductSkuRespDTO sku = skuMap.get(item.getSkuId()); + if (sku == null) { + return; + } + ProductSpuRespDTO spu = spuMap.get(sku.getSpuId()); + if (spu == null) { + return; + } + // 商品项 + TradePriceCalculateRespBO.OrderItem orderItem = new TradePriceCalculateRespBO.OrderItem(); + result.getItems().add(orderItem); + orderItem.setSpuId(sku.getSpuId()).setSkuId(sku.getId()) + .setCount(item.getCount()).setCartId(item.getCartId()).setSelected(item.getSelected()); + // sku 价格 + orderItem.setPrice(sku.getPrice()).setPayPrice(sku.getPrice() * item.getCount()) + .setDiscountPrice(0).setDeliveryPrice(0).setCouponPrice(0).setPointPrice(0).setVipPrice(0); + // sku 信息 + orderItem.setPicUrl(sku.getPicUrl()).setProperties(sku.getProperties()) + .setWeight(sku.getWeight()).setVolume(sku.getVolume()); + // spu 信息 + orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId()) + .setDeliveryTypes(spu.getDeliveryTypes()).setDeliveryTemplateId(spu.getDeliveryTemplateId()) + .setGivePoint(spu.getGiveIntegral()).setUsePoint(0); + if (StrUtil.isBlank(orderItem.getPicUrl())) { + orderItem.setPicUrl(spu.getPicUrl()); + } + }); + + // 创建它的 Price 属性 + result.setPrice(new TradePriceCalculateRespBO.Price()); + recountAllPrice(result); + recountAllGivePoint(result); + return result; + } + + /** + * 计算订单类型 + * + * @param param 计算参数 + * @return 订单类型 + */ + private static Integer getOrderType(TradePriceCalculateReqBO param) { + if (param.getSeckillActivityId() != null) { + return TradeOrderTypeEnum.SECKILL.getType(); + } + if (param.getCombinationActivityId() != null) { + return TradeOrderTypeEnum.COMBINATION.getType(); + } + if (param.getBargainRecordId() != null) { + return TradeOrderTypeEnum.BARGAIN.getType(); + } + if (param.getPointActivityId() != null) { + return TradeOrderTypeEnum.POINT.getType(); + } + return TradeOrderTypeEnum.NORMAL.getType(); + } + + /** + * 基于订单项,重新计算 price 总价 + * + * @param result 计算结果 + */ + public static void recountAllPrice(TradePriceCalculateRespBO result) { + // 先重置 + TradePriceCalculateRespBO.Price price = result.getPrice(); + price.setTotalPrice(0).setDiscountPrice(0).setDeliveryPrice(0) + .setCouponPrice(0).setPointPrice(0).setVipPrice(0).setPayPrice(0); + // 再合计 item + result.getItems().forEach(item -> { + if (!item.getSelected()) { + return; + } + price.setTotalPrice(price.getTotalPrice() + item.getPrice() * item.getCount()); + price.setDiscountPrice(price.getDiscountPrice() + item.getDiscountPrice()); + price.setDeliveryPrice(price.getDeliveryPrice() + item.getDeliveryPrice()); + price.setCouponPrice(price.getCouponPrice() + item.getCouponPrice()); + price.setPointPrice(price.getPointPrice() + item.getPointPrice()); + price.setVipPrice(price.getVipPrice() + item.getVipPrice()); + price.setPayPrice(price.getPayPrice() + item.getPayPrice()); + }); + } + + /** + * 基于订单项,重新计算赠送积分 + * + * @param result 计算结果 + */ + public static void recountAllGivePoint(TradePriceCalculateRespBO result) { + result.setGivePoint(getSumValue(result.getItems(), item -> item.getSelected() ? item.getGivePoint() : 0, Integer::sum)); + } + + /** + * 重新计算单个订单项的支付金额 + * + * @param orderItem 订单项 + */ + public static void recountPayPrice(TradePriceCalculateRespBO.OrderItem orderItem) { + orderItem.setPayPrice(orderItem.getPrice() * orderItem.getCount() + - orderItem.getDiscountPrice() + + orderItem.getDeliveryPrice() + - orderItem.getCouponPrice() + - orderItem.getPointPrice() + - orderItem.getVipPrice() + ); + } + + /** + * 重新计算每个订单项的支付金额 + * + * 【目前主要是单测使用】 + * + * @param orderItems 订单项数组 + */ + public static void recountPayPrice(List orderItems) { + orderItems.forEach(orderItem -> { + if (orderItem.getDiscountPrice() == null) { + orderItem.setDiscountPrice(0); + } + if (orderItem.getDeliveryPrice() == null) { + orderItem.setDeliveryPrice(0); + } + if (orderItem.getCouponPrice() == null) { + orderItem.setCouponPrice(0); + } + if (orderItem.getPointPrice() == null) { + orderItem.setPointPrice(0); + } + if (orderItem.getUsePoint() == null) { + orderItem.setUsePoint(0); + } + if (orderItem.getGivePoint() == null) { + orderItem.setGivePoint(0); + } + if (orderItem.getVipPrice() == null) { + orderItem.setVipPrice(0); + } + recountPayPrice(orderItem); + }); + } + + /** + * 计算已选中的订单项,总支付金额 + * + * @param orderItems 订单项数组 + * @return 总支付金额 + */ + public static Integer calculateTotalPayPrice(List orderItems) { + return getSumValue(orderItems, + orderItem -> orderItem.getSelected() ? orderItem.getPayPrice() : 0, // 未选中的情况下,不计算支付金额 + Integer::sum); + } + + /** + * 计算已选中的订单项,总商品数 + * + * @param orderItems 订单项数组 + * @return 总商品数 + */ + public static Integer calculateTotalCount(List orderItems) { + return getSumValue(orderItems, + orderItem -> orderItem.getSelected() ? orderItem.getCount() : 0, // 未选中的情况下,不计算数量 + Integer::sum); + } + + /** + * 按照支付金额,返回每个订单项的分摊金额数组 + * + * 实际上 price 不仅仅可以传递的是金额,也可以是积分。因为它的实现逻辑,就是根据 payPrice 做分摊而已 + * + * @param orderItems 订单项数组 + * @param price 金额 + * @return 分摊金额数组,和传入的 orderItems 一一对应 + */ + public static List dividePrice(List orderItems, Integer price) { + Integer total = calculateTotalPayPrice(orderItems); + assert total != null; + // 遍历每一个,进行分摊 + List prices = new ArrayList<>(orderItems.size()); + int remainPrice = price; + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + // 1. 如果是未选中,则分摊为 0 + if (!orderItem.getSelected()) { + prices.add(0); + continue; + } + // 2. 如果选中,则按照百分比,进行分摊 + int partPrice; + if (i < orderItems.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 + partPrice = (int) (price * (1.0D * orderItem.getPayPrice() / total)); + remainPrice -= partPrice; + } else { + partPrice = remainPrice; + } + Assert.isTrue(partPrice >= 0, "分摊金额必须大于等于 0"); + prices.add(partPrice); + } + return prices; + } + + /** + * 计算订单调价价格分摊 + * + * 和 {@link #dividePrice(List, Integer)} 逻辑一致,只是传入的是 TradeOrderItemDO 对象 + * + * @param items 订单项 + * @param price 订单支付金额 + * @return 分摊金额数组,和传入的 orderItems 一一对应 + */ + public static List dividePrice2(List items, Integer price) { + Integer total = getSumValue(items, TradeOrderItemDO::getPayPrice, Integer::sum); + assert total != null; + // 遍历每一个,进行分摊 + List prices = new ArrayList<>(items.size()); + int remainPrice = price; + for (int i = 0; i < items.size(); i++) { + TradeOrderItemDO orderItem = items.get(i); + int partPrice; + if (i < items.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 + partPrice = (int) (price * (1.0D * orderItem.getPrice() / total)); + remainPrice -= partPrice; + } else { + partPrice = remainPrice; + } + prices.add(partPrice); + } + return prices; + } + + /** + * 添加【匹配】单个 OrderItem 的营销明细 + * + * @param result 价格计算结果 + * @param orderItem 单个订单商品 SKU + * @param id 营销编号 + * @param name 营销名字 + * @param description 满足条件的提示 + * @param type 营销类型 + * @param discountPrice 单个订单商品 SKU 的优惠价格(总) + */ + public static void addPromotion(TradePriceCalculateRespBO result, TradePriceCalculateRespBO.OrderItem orderItem, + Long id, String name, Integer type, String description, Integer discountPrice) { + addPromotion(result, singletonList(orderItem), id, name, type, description, singletonList(discountPrice)); + } + + /** + * 添加【匹配】多个 OrderItem 的营销明细 + * + * @param result 价格计算结果 + * @param orderItems 多个订单商品 SKU + * @param id 营销编号 + * @param name 营销名字 + * @param description 满足条件的提示 + * @param type 营销类型 + * @param discountPrices 多个订单商品 SKU 的优惠价格(总),和 orderItems 一一对应 + */ + public static void addPromotion(TradePriceCalculateRespBO result, List orderItems, + Long id, String name, Integer type, String description, List discountPrices) { + // 创建营销明细 Item + List promotionItems = new ArrayList<>(discountPrices.size()); + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + promotionItems.add(new TradePriceCalculateRespBO.PromotionItem().setSkuId(orderItem.getSkuId()) + .setTotalPrice(orderItem.getPayPrice()).setDiscountPrice(discountPrices.get(i))); + } + // 创建营销明细 + TradePriceCalculateRespBO.Promotion promotion = new TradePriceCalculateRespBO.Promotion() + .setId(id).setName(name).setType(type) + .setTotalPrice(calculateTotalPayPrice(orderItems)) + .setDiscountPrice(getSumValue(discountPrices, value -> value, Integer::sum)) + .setItems(promotionItems).setMatch(true).setDescription(description); + result.getPromotions().add(promotion); + } + + /** + * 添加【不匹配】多个 OrderItem 的营销明细 + * + * @param result 价格计算结果 + * @param orderItems 多个订单商品 SKU + * @param id 营销编号 + * @param name 营销名字 + * @param description 满足条件的提示 + * @param type 营销类型 + */ + public static void addNotMatchPromotion(TradePriceCalculateRespBO result, List orderItems, + Long id, String name, Integer type, String description) { + // 创建营销明细 Item + List promotionItems = CollectionUtils.convertList(orderItems, + orderItem -> new TradePriceCalculateRespBO.PromotionItem().setSkuId(orderItem.getSkuId()) + .setTotalPrice(orderItem.getPayPrice()).setDiscountPrice(0)); + // 创建营销明细 + TradePriceCalculateRespBO.Promotion promotion = new TradePriceCalculateRespBO.Promotion() + .setId(id).setName(name).setType(type) + .setTotalPrice(calculateTotalPayPrice(orderItems)) + .setDiscountPrice(0) + .setItems(promotionItems).setMatch(false).setDescription(description); + result.getPromotions().add(promotion); + } + + public static String formatPrice(Integer price) { + return String.format("%.2f", price / 100d); + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java new file mode 100644 index 0000000..595710a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -0,0 +1,160 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; +import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import jakarta.annotation.Resource; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static com.tashow.cloud.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice; + +// TODO @puhui999:相关的单测,建议改一改 + +/** + * 满减送活动的 {@link TradePriceCalculator} 实现类 + * + * @author 芋道源码 + */ +@Component +@Order(TradePriceCalculator.ORDER_REWARD_ACTIVITY) +public class TradeRewardActivityPriceCalculator implements TradePriceCalculator { + + @Resource + private RewardActivityApi rewardActivityApi; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 0. 只有【普通】订单,才计算该优惠 + if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) { + return; + } + // 获得 SKU 对应的满减送活动 + List rewardActivities = rewardActivityApi.getMatchRewardActivityListBySpuIds( + convertSet(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSpuId)).getCheckedData(); + if (CollUtil.isEmpty(rewardActivities)) { + return; + } + // 处理最新的满减送活动 + if (!rewardActivities.isEmpty()) { + calculate(param, result, rewardActivities.get(0)); + } + } + + private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result, + RewardActivityMatchRespDTO rewardActivity) { + // 1.1 获得满减送的订单项(商品)列表 + List orderItems = filterMatchActivityOrderItems(result, rewardActivity); + if (CollUtil.isEmpty(orderItems)) { + return; + } + // 1.2 获得最大匹配的满减送活动的规则 + RewardActivityMatchRespDTO.Rule rule = getMaxMatchRewardActivityRule(rewardActivity, orderItems); + if (rule == null) { + TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems, + rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), + "满减送:" + rewardActivity.getRules().get(0).getDescription()); + return; + } + + // 2.1 计算可以优惠的金额 + Integer newDiscountPrice = rule.getDiscountPrice(); + // 2.2 计算分摊的优惠金额 + List divideDiscountPrices = TradePriceCalculatorHelper.dividePrice(orderItems, newDiscountPrice); + // 2.3 计算是否包邮 + if (Boolean.TRUE.equals(rule.getFreeDelivery())) { + result.setFreeDelivery(true); + } + + // 3.1 记录使用的优惠劵 + result.setCouponId(param.getCouponId()); + // 3.2 记录优惠明细 + TradePriceCalculatorHelper.addPromotion(result, orderItems, + rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), + StrUtil.format("满减送:省 {} 元", formatPrice(rule.getDiscountPrice())), + divideDiscountPrices); + // 3.3 更新 SKU 优惠金额 + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + orderItem.setDiscountPrice(orderItem.getDiscountPrice() + divideDiscountPrices.get(i)); + TradePriceCalculatorHelper.recountPayPrice(orderItem); + } + TradePriceCalculatorHelper.recountAllPrice(result); + + // 4.1 记录赠送的积分 + if (rule.getPoint() != null && rule.getPoint() > 0) { + List dividePoints = TradePriceCalculatorHelper.dividePrice(orderItems, rule.getPoint()); + for (int i = 0; i < orderItems.size(); i++) { + // 商品可能赠送了积分,所以这里要加上 + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + orderItem.setGivePoint(orderItem.getGivePoint() + dividePoints.get(i)); + } + } + // 4.2 记录订单是否包邮 + if (Boolean.TRUE.equals(rule.getFreeDelivery())) { + // 只要满足一个活动包邮那么这单就包邮 + result.setFreeDelivery(true); + } + // 4.3 记录赠送的优惠券 + if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) { + for (Map.Entry entry : rule.getGiveCouponTemplateCounts().entrySet()) { + result.getGiveCouponTemplateCounts().put(entry.getKey(), + result.getGiveCouponTemplateCounts().getOrDefault(entry.getKey(), 0) + entry.getValue()); + } + } + } + + /** + * 获得满减送的订单项(商品)列表 + * + * @param result 计算结果 + * @param rewardActivity 满减送活动 + * @return 订单项(商品)列表 + */ + private List filterMatchActivityOrderItems(TradePriceCalculateRespBO result, + RewardActivityMatchRespDTO rewardActivity) { + return filterList(result.getItems(), orderItem -> CollUtil.contains(rewardActivity.getSpuIds(), orderItem.getSpuId())); + } + + /** + * 获得最大匹配的满减送活动的规则 + * + * @param rewardActivity 满减送活动 + * @param orderItems 商品项 + * @return 匹配的活动规则 + */ + private RewardActivityMatchRespDTO.Rule getMaxMatchRewardActivityRule(RewardActivityMatchRespDTO rewardActivity, + List orderItems) { + // 1. 计算数量和价格 + Integer count = TradePriceCalculatorHelper.calculateTotalCount(orderItems); + Integer price = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); + assert count != null && price != null; + + // 2. 倒序找一个最大优惠的规则 + for (int i = rewardActivity.getRules().size() - 1; i >= 0; i--) { + RewardActivityMatchRespDTO.Rule rule = rewardActivity.getRules().get(i); + if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType()) + && price >= rule.getLimit()) { + return rule; + } + if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType()) + && count >= rule.getLimit()) { + return rule; + } + } + return null; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeSeckillActivityPriceCalculator.java b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeSeckillActivityPriceCalculator.java new file mode 100644 index 0000000..cc02e34 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/java/com/tashow/cloud/trade/service/price/calculator/TradeSeckillActivityPriceCalculator.java @@ -0,0 +1,72 @@ +package com.tashow.cloud.trade.service.price.calculator; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.promotion.api.seckill.SeckillActivityApi; +import cn.iocoder.yudao.module.promotion.api.seckill.dto.SeckillValidateJoinRespDTO; +import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; +import com.tashow.cloud.trade.service.order.TradeOrderQueryService; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateReqBO; +import com.tashow.cloud.trade.service.price.bo.TradePriceCalculateRespBO; +import jakarta.annotation.Resource; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT; + +// TODO huihui:单测需要补充 + +/** + * 秒杀活动的 {@link TradePriceCalculator} 实现类 + * + * @author HUIHUI + */ +@Component +@Order(TradePriceCalculator.ORDER_SECKILL_ACTIVITY) +public class TradeSeckillActivityPriceCalculator implements TradePriceCalculator { + + @Resource + private SeckillActivityApi seckillActivityApi; + + @Resource + private TradeOrderQueryService tradeOrderQueryService; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 1. 判断订单类型和是否具有秒杀活动编号 + if (param.getSeckillActivityId() == null) { + return; + } + Assert.isTrue(param.getItems().size() == 1, "秒杀时,只允许选择一个商品"); + // 2. 校验是否可以参与秒杀 + TradePriceCalculateRespBO.OrderItem orderItem = result.getItems().get(0); + SeckillValidateJoinRespDTO seckillActivity = validateJoinSeckill( + param.getUserId(), param.getSeckillActivityId(), + orderItem.getSkuId(), orderItem.getCount()); + + // 3.1 记录优惠明细 + Integer discountPrice = orderItem.getPayPrice() - seckillActivity.getSeckillPrice() * orderItem.getCount(); + TradePriceCalculatorHelper.addPromotion(result, orderItem, + param.getSeckillActivityId(), seckillActivity.getName(), PromotionTypeEnum.SECKILL_ACTIVITY.getType(), + StrUtil.format("秒杀活动:省 {} 元", TradePriceCalculatorHelper.formatPrice(discountPrice)), + discountPrice); + // 3.2 更新 SKU 优惠金额 + orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice); + TradePriceCalculatorHelper.recountPayPrice(orderItem); + TradePriceCalculatorHelper.recountAllPrice(result); + } + + private SeckillValidateJoinRespDTO validateJoinSeckill(Long userId, Long activityId, Long skuId, Integer count) { + // 1. 校验是否可以参与秒杀 + SeckillValidateJoinRespDTO seckillActivity = seckillActivityApi.validateJoinSeckill(activityId, skuId, count).getCheckedData(); + // 2. 校验总限购数量,目前只有 trade 有具体下单的数据,需要交给 trade 价格计算使用 + int seckillProductCount = tradeOrderQueryService.getActivityProductCount(userId, activityId, TradeOrderTypeEnum.SECKILL); + if (seckillProductCount + count > seckillActivity.getTotalLimitCount()) { + throw exception(PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT); + } + return seckillActivity; + } + +} diff --git a/tashow-module/tashow-module-trade/src/main/resources/application-dev.yaml b/tashow-module/tashow-module-trade/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..afa7754 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/resources/application-dev.yaml @@ -0,0 +1,116 @@ +--- #################### 注册中心 + 配置中心相关配置 #################### + +spring: + cloud: + nacos: + server-addr: 127.0.0.1:8848 # Nacos 服务器地址 + username: # Nacos 账号 + password: # Nacos 密码 + discovery: # 【配置中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 + config: # 【注册中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + 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: + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + username: root + password: 123456 + slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + data: + redis: + host: 400-infra.server.iocoder.cn # 地址 + port: 6379 # 端口 + database: 1 # 数据库索引 +# password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### +xxl: + job: + admin: + addresses: http://127.0.0.1:9090/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] + # Spring Boot Admin Server 服务端的相关配置 + context-path: /admin # 配置 Spring + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + demo: true # 开启演示模式 + tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc diff --git a/tashow-module/tashow-module-trade/src/main/resources/application-local.yaml b/tashow-module/tashow-module-trade/src/main/resources/application-local.yaml new file mode 100644 index 0000000..ec71289 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/resources/application-local.yaml @@ -0,0 +1,138 @@ +--- #################### 注册中心 + 配置中心相关配置 #################### + +spring: + cloud: + nacos: + server-addr: 127.0.0.1:8848 # Nacos 服务器地址 + username: # Nacos 账号 + password: # Nacos 密码 + discovery: # 【配置中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 + config: # 【注册中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 + 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: 1 # 初始连接数 + min-idle: 1 # 最小连接池数量 + 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: + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例 + # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 + # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 + # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例 + # url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例 + username: root + password: 123456 + # username: sa # SQL Server 连接的示例 + # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例 + # username: SYSDBA # DM 连接的示例 + # password: SYSDBA # DM 连接的示例 + slave: # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + data: + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 +# password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### + +xxl: + job: + enabled: false # 是否开启调度中心,默认为 true 开启 + admin: + addresses: http://127.0.0.1:9090/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.trade.dal.mysql: debug + org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示 + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + env: # 多环境的配置项 + tag: ${HOSTNAME} + security: + mock-enable: true + access-log: # 访问日志的配置项 + enable: false + tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc diff --git a/tashow-module/tashow-module-trade/src/main/resources/application.yaml b/tashow-module/tashow-module-trade/src/main/resources/application.yaml new file mode 100644 index 0000000..b45cf90 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/resources/application.yaml @@ -0,0 +1,141 @@ +spring: + application: + name: trade-server + + profiles: + active: local + + main: + allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 + allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务 + + config: + import: + - optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置 + - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置 + + # Servlet 配置 + servlet: + # 文件上传相关配置项 + multipart: + max-file-size: 16MB # 单个文件大小 + max-request-size: 32MB # 设置总上传的文件大小 + + # 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 小时 + +server: + port: 48102 + +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + +--- #################### 接口文档配置 #################### + +springdoc: + api-docs: + enabled: true # 1. 是否开启 Swagger 接文档的元数据 + path: /v3/api-docs + swagger-ui: + enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面 + path: /swagger-ui + default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档 + +knife4j: + enable: false # TODO 芋艿:需要关闭增强,具体原因见:https://github.com/xiaoymin/knife4j/issues/874 + setting: + language: zh_cn + +# MyBatis Plus 的配置项 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 + global-config: + db-config: + 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) + banner: false # 关闭控制台的 Banner 打印 + type-aliases-package: ${yudao.info.base-package}.dal.dataobject + encryptor: + password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成 + +mybatis-plus-join: + banner: false # 关闭控制台的 Banner 打印 + +# Spring Data Redis 配置 +spring: + data: + redis: + repositories: + enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度 + +# VO 转换(数据翻译)相关 +easy-trans: + is-enable-global: true # 启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口 + +--- #################### RPC 远程调用相关配置 #################### + +--- #################### 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.trade + web: + admin-ui: + url: http://dashboard.yudao.iocoder.cn # Admin 管理后台 UI 的地址 + xss: + enable: false + exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + swagger: + title: 管理后台 + description: 提供管理员管理的所有功能 + version: ${yudao.info.version} + tenant: # 多租户相关配置项 + enable: true + ignore-urls: + ignore-tables: + trade: + order: + pay-expire-time: 2h # 支付的过期时间 + receive-expire-time: 14d # 收货的过期时间 + comment-expire-time: 7d # 评论的过期时间 + express: + client: KD_NIAO + kd-niao: + api-key: cb022f1e-48f1-4c4a-a723-9001ac9676b8 + business-id: 1809751 + request-type: 1002 # 免费版 1002;付费版 8001 + kd100: + key: pLXUGAwK5305 + customer: E77DF18BE109F454A5CD319E44BF5177 + +debug: false diff --git a/tashow-module/tashow-module-trade/src/main/resources/logback-spring.xml b/tashow-module/tashow-module-trade/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b1b9f3f --- /dev/null +++ b/tashow-module/tashow-module-trade/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/tashow-module/tashow-module-trade/src/main/resources/mapper/brokerage/BrokerageUserMapper.xml b/tashow-module/tashow-module-trade/src/main/resources/mapper/brokerage/BrokerageUserMapper.xml new file mode 100644 index 0000000..151b33a --- /dev/null +++ b/tashow-module/tashow-module-trade/src/main/resources/mapper/brokerage/BrokerageUserMapper.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/tashow-module/tashow-module-trade/src/test/resources/application-unit-test.yaml b/tashow-module/tashow-module-trade/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..ab50eb5 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/test/resources/application-unit-test.yaml @@ -0,0 +1,60 @@ +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 默认的配置足够使用,一般不需要进行调优 + data: + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + info: + base-package: cn.iocoder.yudao.module + trade: + order: + app-id: 1 + merchant-order-id: 1 + express: + kd-niao: + api-key: xxxx + business-id: xxxxx + kd100: + customer: xxxxx + key: xxxxx + client: not_provide \ No newline at end of file diff --git a/tashow-module/tashow-module-trade/src/test/resources/logback.xml b/tashow-module/tashow-module-trade/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/tashow-module/tashow-module-trade/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tashow-module/tashow-module-trade/src/test/resources/sql/clean.sql b/tashow-module/tashow-module-trade/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..f7f3477 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/test/resources/sql/clean.sql @@ -0,0 +1,7 @@ +DELETE FROM trade_order; +DELETE FROM trade_order_item; +DELETE FROM trade_after_sale; +DELETE FROM trade_after_sale_log; +DELETE FROM trade_brokerage_user; +DELETE FROM trade_brokerage_record; +DELETE FROM "trade_brokerage_withdraw"; diff --git a/tashow-module/tashow-module-trade/src/test/resources/sql/create_tables.sql b/tashow-module/tashow-module-trade/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..1d7ed24 --- /dev/null +++ b/tashow-module/tashow-module-trade/src/test/resources/sql/create_tables.sql @@ -0,0 +1,235 @@ +CREATE TABLE IF NOT EXISTS "trade_order" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "no" varchar NOT NULL, + "type" int NOT NULL, + "terminal" int NOT NULL, + "user_id" bigint NOT NULL, + "user_ip" varchar NOT NULL, + "user_remark" varchar, + "status" int NOT NULL, + "product_count" int NOT NULL, + "cancel_type" int, + "remark" varchar, + "comment_status" boolean, + "brokerage_user_id" bigint, + "pay_status" bit NOT NULL, + "pay_time" datetime, + "finish_time" datetime, + "cancel_time" datetime, + "total_price" int NULL, + "order_price" int NULL, + "discount_price" int NOT NULL, + "delivery_price" int NOT NULL, + "adjust_price" int NOT NULL, + "pay_price" int NOT NULL, + "delivery_type" int NOT NULL, + "pay_order_id" bigint, + "pay_channel_code" varchar, + "delivery_template_id" bigint, + "logistics_id" bigint, + "logistics_no" varchar, + "delivery_time" datetime, + "receive_time" datetime, + "receiver_name" varchar NOT NULL, + "receiver_mobile" varchar NOT NULL, + "receiver_area_id" int NOT NULL, + "receiver_post_code" int, + "receiver_detail_address" varchar NOT NULL, + "pick_up_store_id" long NULL, + "pick_up_verify_code" varchar NULL, + "refund_status" int NULL, + "refund_price" int NULL, + "after_sale_status" int NULL, + "coupon_id" bigint NOT NULL, + "coupon_price" int NOT NULL, + "use_point" int NULL, + "point_price" int NOT NULL, + "give_point" int NULL, + "refund_point" int NULL, + "vip_price" int NULL, + "give_coupons_map" varchar NULL, + "seckill_activity_id" long NULL, + "bargain_activity_id" long NULL, + "bargain_record_id" long NULL, + "combination_activity_id" long NULL, + "combination_head_id" long NULL, + "combination_record_id" long NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '交易订单表'; + +CREATE TABLE IF NOT EXISTS "trade_order_item" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "order_id" bigint NOT NULL, + "cart_id" int NULL, + "spu_id" bigint NOT NULL, + "spu_name" varchar NOT NULL, + "sku_id" bigint NOT NULL, + "properties" varchar, + "pic_url" varchar, + "count" int NOT NULL, + "comment_status" boolean NULL, + "price" int NOT NULL, + "discount_price" int NOT NULL, + "delivery_price" int NULL, + "adjust_price" int NULL, + "pay_price" int NOT NULL, + "coupon_price" int NULL, + "point_price" int NULL, + "use_point" int NULL, + "give_point" int NULL, + "vip_price" int NULL, + "after_sale_id" long NULL, + "after_sale_status" int NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '交易订单明细表'; + +CREATE TABLE IF NOT EXISTS "trade_after_sale" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "no" varchar NOT NULL, + "status" int NOT NULL, + "type" int NOT NULL, + "way" int NOT NULL, + "user_id" bigint NOT NULL, + "apply_reason" varchar NOT NULL, + "apply_description" varchar, + "apply_pic_urls" varchar, + "order_id" bigint NOT NULL, + "order_no" varchar NOT NULL, + "order_item_id" bigint NOT NULL, + "spu_id" bigint NOT NULL, + "spu_name" varchar NOT NULL, + "sku_id" bigint NOT NULL, + "properties" varchar, + "pic_url" varchar, + "count" int NOT NULL, + "audit_time" varchar, + "audit_user_id" bigint, + "audit_reason" varchar, + "refund_price" int NOT NULL, + "pay_refund_id" bigint, + "refund_time" varchar, + "logistics_id" bigint, + "logistics_no" varchar, + "delivery_time" varchar, + "receive_time" varchar, + "receive_reason" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '交易售后表'; + +CREATE TABLE IF NOT EXISTS "trade_after_sale_log" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" int NOT NULL, + "after_sale_id" bigint NOT NULL, + "order_id" bigint NOT NULL, + "order_item_id" bigint NOT NULL, + "before_status" int, + "after_status" int NOT NULL, + "content" varchar NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '交易售后日志'; + +CREATE TABLE IF NOT EXISTS "trade_brokerage_user" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "bind_user_id" bigint NOT NULL, + "bind_user_time" varchar, + "brokerage_enabled" bit NOT NULL, + "brokerage_time" varchar, + "price" int NOT NULL, + "frozen_price" int NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT '分销用户'; +CREATE TABLE IF NOT EXISTS "trade_brokerage_record" +( + "id" int NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "biz_id" varchar NOT NULL, + "biz_type" varchar NOT NULL, + "title" varchar NOT NULL, + "price" int NOT NULL, + "total_price" int NOT NULL, + "description" varchar NOT NULL, + "status" varchar NOT NULL, + "frozen_days" int NOT NULL, + "unfreeze_time" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '佣金记录'; +CREATE TABLE IF NOT EXISTS "trade_brokerage_withdraw" +( + "id" int NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "price" int NOT NULL, + "fee_price" int NOT NULL, + "total_price" int NOT NULL, + "type" varchar NOT NULL, + "name" varchar, + "account_no" varchar, + "bank_name" varchar, + "bank_address" varchar, + "account_qr_code_url" varchar, + "status" varchar NOT NULL, + "audit_reason" varchar, + "audit_time" varchar, + "remark" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '佣金提现'; + +CREATE TABLE IF NOT EXISTS "trade_delivery_express" +( + "id" int NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar NULL, + "name" varchar, + "logo" varchar NULL, + "sort" int NOT NULL, + "status" int NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '佣金提现'; \ No newline at end of file