【同步】BOOT 和 CLOUD 的功能(PAY 相关功能)

(cherry picked from commit 5e2138265fb0247989847db4847b824643980821)
This commit is contained in:
YunaiV 2025-05-11 18:08:17 +08:00 committed by dengqichen
parent a7a2c464e0
commit 59029ac68b
124 changed files with 2273 additions and 1923 deletions

View File

@ -81,7 +81,7 @@
<justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>1.9.4</jimureport.version>
<weixin-java.version>4.7.4.B</weixin-java.version>
<weixin-java.version>4.7.5.B</weixin-java.version>
</properties>
<dependencyManagement>

View File

@ -8,12 +8,16 @@ import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
import static cn.hutool.core.date.DatePattern.UTC_MS_WITH_XXX_OFFSET_PATTERN;
import static cn.hutool.core.date.DatePattern.createFormatter;
/**
* 时间工具类用于 {@link java.time.LocalDateTime}
*
@ -26,6 +30,8 @@ public class LocalDateTimeUtils {
*/
public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
public static DateTimeFormatter UTC_MS_WITH_XXX_OFFSET_FORMATTER = createFormatter(UTC_MS_WITH_XXX_OFFSET_PATTERN);
/**
* 解析时间
*

View File

@ -7,6 +7,9 @@ package cn.iocoder.yudao.module.trade.enums;
*/
public interface DictTypeConstants {
String BROKERAGE_RECORD_STATUS = "brokerage_record_status"; // 佣金记录状态
String BROKERAGE_WITHDRAW_TYPE = "brokerage_withdraw_type"; // 佣金提现类型
String BROKERAGE_WITHDRAW_STATUS = "brokerage_withdraw_status"; // 佣金提现状态
}

View File

@ -36,8 +36,10 @@ public interface ErrorCodeConstants {
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, "交易订单创建失败,原因:用户积分不足");
ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_034, "交易订单更新支付订单退款状态失败,原因:退款单不存在");
ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_035, "交易订单更新支付订单退款状态失败,原因:退款单状态不是【退款成功】");
ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_036, "交易订单自提失败,原因:你没有核销该门店订单的权限");
ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_037, "交易订单创建失败,原因:用户积分不足");
// ========== After Sale 模块 1-011-000-100 ==========
ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
@ -51,9 +53,13 @@ public interface ErrorCodeConstants {
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_REFUND_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_111, "退款失败,退款单不存在");
ErrorCode AFTER_SALE_REFUND_FAIL_REFUND_NOT_SUCCESS_OR_FAILURE = new ErrorCode(1_011_000_112, "退款失败,退款单未退款");
ErrorCode AFTER_SALE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1_011_000_113, "退款失败,退款金额不匹配");
ErrorCode AFTER_SALE_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1_011_000_114, "退款失败,退款单不匹配");
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, "订单拼团中,无法申请售后");
new ErrorCode(1_011_000_115, "取消售后单失败,售后单状态不是【待审核】或【卖家同意】或【商家待收货】");
ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_COMBINATION_IN_PROGRESS = new ErrorCode(1_011_000_116, "订单拼团中,无法申请售后");
// ========== Cart 模块 1-011-002-000 ==========
ErrorCode CARD_ITEM_NOT_FOUND = new ErrorCode(1_011_002_000, "购物车项不存在");
@ -101,5 +107,10 @@ public interface ErrorCodeConstants {
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, "您当前最多可提现 {} 元");
ErrorCode BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR = new ErrorCode(1_011_008_005, "提现单更新转账状态失败,转账单不匹配");
ErrorCode BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_STATUS_NOT_SUCCESS_OR_CLOSED = new ErrorCode(1_011_008_006, "提现单更新转账状态失败,转账单状态不是成功或关闭状态");
ErrorCode BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_PRICE_NOT_MATCH = new ErrorCode(1_011_008_007, "提现单更新转账状态失败,转账单金额不匹配");
ErrorCode BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_MERCHANT_EXISTS = new ErrorCode(1_011_008_008, "提现单更新转账状态失败,转账单的商户订单不匹配");
ErrorCode BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_CHANNEL_NOT_MATCH = new ErrorCode(1_011_008_009, "提现单更新转账状态失败,转账渠道不匹配");
}

View File

@ -19,7 +19,9 @@ public enum AfterSaleOperateTypeEnum {
MEMBER_DELIVERY(20, "会员填写退货物流信息,快递公司:{deliveryName},快递单号:{logisticsNo}"),
ADMIN_AGREE_RECEIVE(21, "商家收货"),
ADMIN_DISAGREE_RECEIVE(22, "商家拒绝收货,原因:{reason}"),
ADMIN_REFUND(30, "商家退款"),
ADMIN_REFUND(30, "商家发起退款"),
SYSTEM_REFUND_SUCCESS(31, "退款成功"),
SYSTEM_REFUND_FAIL(32, "退款失败"),
MEMBER_CANCEL(40, "会员取消退款"),
;

View File

@ -6,7 +6,6 @@ import lombok.Getter;
import java.util.Arrays;
// TODO 芋艿提现的打通在纠结下
/**
* 佣金提现状态枚举
*

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.trade.enums.brokerage;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -17,9 +18,10 @@ public enum BrokerageWithdrawTypeEnum implements ArrayValuable<Integer> {
WALLET(1, "钱包"),
BANK(2, "银行卡"),
WECHAT(3, "微信"), // 手动打款
ALIPAY(4, "支付宝"),
WECHAT_QR(3, "微信收款码"), // 手动打款
ALIPAY_QR(4, "支付宝收款码"), // 手动打款
WECHAT_API(5, "微信零钱"), // 自动打款通过微信转账 API
ALIPAY_API(6, "支付宝余额"), // 自动打款通过支付宝转账 API
;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(BrokerageWithdrawTypeEnum::getType).toArray(Integer[]::new);
@ -45,7 +47,7 @@ public enum BrokerageWithdrawTypeEnum implements ArrayValuable<Integer> {
* @return 是否
*/
public static boolean isApi(Integer type) {
return WECHAT_API.getType().equals(type);
return ObjectUtils.equalsAny(type, WALLET.getType(), ALIPAY_API.getType(), WECHAT_API.getType());
}
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.trade.controller.admin.aftersale;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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;
@ -15,10 +16,12 @@ import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleLogService;
import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -46,6 +49,8 @@ public class AfterSaleController {
@Resource
private TradeOrderQueryService tradeOrderQueryService;
@Resource
private TradeOrderUpdateService tradeOrderUpdateService;
@Resource
private AfterSaleLogService afterSaleLogService;
@Resource
private MemberUserApi memberUserApi;
@ -133,11 +138,19 @@ public class AfterSaleController {
@PostMapping("/update-refunded")
@Operation(summary = "更新售后订单为已退款") // pay-module 支付服务进行回调可见 PayNotifyJob
@PermitAll // 无需登录安全由 PayDemoOrderService 内部校验实现
public CommonResult<Boolean> updateAfterRefund(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
// 目前业务逻辑不需要做任何事情
// 当然退款会有小概率会失败的情况可以监控失败状态进行告警
@PermitAll // 无需登录安全由 AfterSaleService 内部校验实现
public CommonResult<Boolean> updateAfterSaleRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO);
if (StrUtil.startWithAny(notifyReqDTO.getMerchantRefundId(), "order-")) {
tradeOrderUpdateService.updatePaidOrderRefunded(
Long.parseLong(notifyReqDTO.getMerchantRefundId()),
notifyReqDTO.getPayRefundId());
} else {
afterSaleService.updateAfterSaleRefunded(
Long.parseLong(notifyReqDTO.getMerchantRefundId()),
Long.parseLong(notifyReqDTO.getMerchantOrderId()),
notifyReqDTO.getPayRefundId());
}
return success(true);
}

View File

@ -82,10 +82,9 @@ public class BrokerageWithdrawController {
return success(BrokerageWithdrawConvert.INSTANCE.convertPage(pageResult, userMap));
}
// TODO @luchiupdate-transferredurl 改成这个 update-paid update-refunded 保持一致
@PostMapping("/update-transfer")
@Operation(summary = "更新转账订单为转账成功") // pay-module 支付服务进行回调可见 PayNotifyJob
@PermitAll // 无需登录安全由 PayDemoOrderService 内部校验实现
@PostMapping("/update-transferred")
@Operation(summary = "更新佣金提现的转账结果") // pay-module 支付服务进行回调可见 PayNotifyJob
@PermitAll // 无需登录安全由 BrokerageWithdrawService 内部校验实现
public CommonResult<Boolean> updateBrokerageWithdrawTransferred(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO);
brokerageWithdrawService.updateBrokerageWithdrawTransferred(

View File

@ -1,68 +0,0 @@
package cn.iocoder.yudao.module.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;
}

View File

@ -28,10 +28,10 @@ public class BrokerageWithdrawPageReqVO extends PageParam {
private Integer type;
@Schema(description = "真实姓名", example = "赵六")
private String name;
private String userName;
@Schema(description = "账号", example = "886779132")
private String accountNo;
private String userAccount;
@Schema(description = "银行名称", example = "1")
private String bankName;

View File

@ -1,11 +1,12 @@
package cn.iocoder.yudao.module.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;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
@Schema(description = "管理后台 - 驳回申请 Request VO")
@Data
@ToString(callSuper = true)

View File

@ -2,24 +2,68 @@ package cn.iocoder.yudao.module.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 {
public class BrokerageWithdrawRespVO {
@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 = "11436")
private Long userId;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String userNickname;
@Schema(description = "提现金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "18781")
private Integer price;
@Schema(description = "提现手续费", requiredMode = Schema.RequiredMode.REQUIRED, example = "11417")
private Integer feePrice;
@Schema(description = "当前总佣金", requiredMode = Schema.RequiredMode.REQUIRED, example = "18576")
private Integer totalPrice;
@Schema(description = "提现类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer type;
@Schema(description = "真实姓名", example = "赵六")
private String userName;
@Schema(description = "收款账号", example = "88677912132")
private String userAccount;
@Schema(description = "银行名称", example = "1")
private String bankName;
@Schema(description = "开户地址", example = "海淀支行")
private String bankAddress;
@Schema(description = "收款码", example = "https://www.iocoder.cn")
private String qrCodeUrl;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
@Schema(description = "审核驳回原因", example = "不对")
private String auditReason;
@Schema(description = "审核时间")
private LocalDateTime auditTime;
@Schema(description = "备注", example = "随便")
private String remark;
@Schema(description = "转账单编号", example = "1024")
private Long payTransferId;
@Schema(description = "转账错误提示", example = "余额不足")
private String transferErrorMsg;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@ -3,14 +3,18 @@ package cn.iocoder.yudao.module.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 cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.record.AppBrokerageProductPriceRespVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.record.AppBrokerageRecordPageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.record.AppBrokerageRecordRespVO;
import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageRecordConvert;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageRecordDO;
import cn.iocoder.yudao.module.trade.enums.DictTypeConstants;
import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageRecordService;
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.GetMapping;
@ -18,9 +22,6 @@ 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;
@ -38,8 +39,9 @@ public class AppBrokerageRecordController {
@Operation(summary = "获得分销记录分页")
public CommonResult<PageResult<AppBrokerageRecordRespVO>> getBrokerageRecordPage(@Valid AppBrokerageRecordPageReqVO pageReqVO) {
PageResult<BrokerageRecordDO> pageResult = brokerageRecordService.getBrokerageRecordPage(
BrokerageRecordConvert.INSTANCE.convert(pageReqVO, getLoginUserId()));
return success(BeanUtils.toBean(pageResult, AppBrokerageRecordRespVO.class));
BeanUtils.toBean(pageReqVO, BrokerageRecordPageReqVO.class).setUserId(getLoginUserId()));
return success(BeanUtils.toBean(pageResult, AppBrokerageRecordRespVO.class, recordVO ->
recordVO.setStatusName(DictFrameworkUtils.getDictDataLabel(DictTypeConstants.BROKERAGE_RECORD_STATUS, recordVO.getStatus()))));
}
@GetMapping("/get-product-brokerage-price")

View File

@ -1,21 +1,31 @@
package cn.iocoder.yudao.module.trade.controller.app.brokerage;
import cn.hutool.core.util.ObjUtil;
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.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.module.pay.api.transfer.PayTransferApi;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawPageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawRespVO;
import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageWithdrawConvert;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO;
import cn.iocoder.yudao.module.trade.enums.DictTypeConstants;
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum;
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum;
import cn.iocoder.yudao.module.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.Resource;
import jakarta.validation.Valid;
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.Objects;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
@ -30,12 +40,39 @@ public class AppBrokerageWithdrawController {
@Resource
private BrokerageWithdrawService brokerageWithdrawService;
@Resource
private PayTransferApi payTransferApi;
@GetMapping("/page")
@Operation(summary = "获得分销提现分页")
public CommonResult<PageResult<AppBrokerageWithdrawRespVO>> getBrokerageWithdrawPage(AppBrokerageWithdrawPageReqVO pageReqVO) {
PageResult<BrokerageWithdrawDO> pageResult = brokerageWithdrawService.getBrokerageWithdrawPage(
BrokerageWithdrawConvert.INSTANCE.convert(pageReqVO, getLoginUserId()));
return success(BrokerageWithdrawConvert.INSTANCE.convertPage03(pageResult));
BeanUtils.toBean(pageReqVO, BrokerageWithdrawPageReqVO.class).setUserId(getLoginUserId()));
return success(BeanUtils.toBean(pageResult, AppBrokerageWithdrawRespVO.class, withdrawVO ->
withdrawVO.setTypeName(DictFrameworkUtils.getDictDataLabel(DictTypeConstants.BROKERAGE_WITHDRAW_TYPE, withdrawVO.getType()))
.setStatusName(DictFrameworkUtils.getDictDataLabel(DictTypeConstants.BROKERAGE_WITHDRAW_STATUS, withdrawVO.getStatus()))));
}
@GetMapping("/get")
@Operation(summary = "获得佣金提现")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<AppBrokerageWithdrawRespVO> getBrokerageWithdraw(@RequestParam("id") Long id) {
BrokerageWithdrawDO withdraw = brokerageWithdrawService.getBrokerageWithdraw(id);
if (withdraw == null || ObjUtil.notEqual(withdraw.getUserId(), getLoginUserId())) {
return success(null);
}
// 审核中转账中并且是微信转账需要返回 mchId 用于确认收款
AppBrokerageWithdrawRespVO withdrawVO = BeanUtils.toBean(withdraw, AppBrokerageWithdrawRespVO.class);
if (Objects.equals(withdraw.getStatus(), BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.getStatus())
&& Objects.equals(withdraw.getType(), BrokerageWithdrawTypeEnum.WECHAT_API.getType())
&& withdraw.getPayTransferId() != null) {
PayTransferRespDTO transfer = payTransferApi.getTransfer(withdraw.getPayTransferId()).getCheckedData();
if (transfer != null) {
withdrawVO.setTransferChannelPackageInfo(transfer.getChannelPackageInfo())
.setTransferChannelMchId(transfer.getChannelMchId());
}
}
return success(withdrawVO);
}
@PostMapping("/create")

View File

@ -20,11 +20,11 @@ public class AppBrokerageRecordPageReqVO extends PageParam {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
@Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@Schema(description = "业务类型", example = "1")
@InEnum(value = BrokerageRecordBizTypeEnum.class, message = "业务类型必须是 {value}")
private Integer bizType;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@Schema(description = "状态", example = "1")
@InEnum(value = BrokerageRecordStatusEnum.class, message = "状态必须是 {value}")
private Integer status;

View File

@ -24,6 +24,9 @@ public class AppBrokerageRecordRespVO {
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
@Schema(description = "状态名", requiredMode = Schema.RequiredMode.REQUIRED, example = "待结算")
private String statusName;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;

View File

@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.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.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
@ -22,42 +24,49 @@ public class AppBrokerageWithdrawCreateReqVO {
@Schema(description = "提现金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000")
@PositiveOrZero(message = "提现金额不能小于 0")
@Min(value = 30, message = "微信提现金额不能小于 0.3", groups = {WechatApi.class})
@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;
@NotBlank(message = "提现账号不能为空", groups = {Bank.class, WechatApi.class, AlipayApi.class})
private String userAccount;
// ========== 微信支付宝 提现相关字段 ==========
@Schema(description = "提现姓名", example = "张三")
@NotBlank(message = "提现姓名不能为空", groups = {Bank.class, WechatApi.class, AlipayApi.class})
private String userName;
@Schema(description = "收款码的图片", example = "https://www.iocoder.cn/1.png")
@URL(message = "收款码的图片,必须是一个 URL")
private String accountQrCodeUrl;
@URL(message = "收款码的图片,必须是一个 URL", groups = {WechatQR.class, AlipayQR.class})
private String qrCodeUrl;
// ========== 银行卡 提现相关字段 ==========
@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;
@Schema(description = "转账渠道", example = "wx_lite")
@NotNull(message = "转账渠道不能为空", groups = {WechatApi.class})
@InEnum(PayChannelEnum.class)
private String transferChannelCode;
public interface Wallet {
}
public interface Bank {
}
public interface Wechat {
public interface WechatQR {
}
public interface Alipay {
public interface WechatApi {
}
public interface AlipayQR {
}
public interface AlipayApi {
}
public void validate(Validator validator) {
@ -65,10 +74,14 @@ public class AppBrokerageWithdrawCreateReqVO {
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);
} else if (BrokerageWithdrawTypeEnum.WECHAT_QR.getType().equals(type)) {
ValidationUtils.validate(validator, this, WechatQR.class);
} else if (BrokerageWithdrawTypeEnum.WECHAT_API.getType().equals(type)) {
ValidationUtils.validate(validator, this, WechatApi.class);
} else if (BrokerageWithdrawTypeEnum.ALIPAY_QR.getType().equals(type)) {
ValidationUtils.validate(validator, this, AlipayQR.class);
} else if (BrokerageWithdrawTypeEnum.ALIPAY_API.getType().equals(type)) {
ValidationUtils.validate(validator, this, AlipayApi.class);
}
}

View File

@ -1,22 +1,22 @@
package cn.iocoder.yudao.module.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;
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 AppBrokerageWithdrawPageReqVO extends PageParam {
@Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@InEnum(value = BrokerageWithdrawTypeEnum.class, message = "类型必须是 {value}")
private Integer type;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@InEnum(value = BrokerageWithdrawStatusEnum.class, message = "状态必须是 {value}")
private Integer status;
}

View File

@ -12,6 +12,12 @@ public class AppBrokerageWithdrawRespVO {
@Schema(description = "提现编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Long id;
@Schema(description = "提现类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer type;
@Schema(description = "提现类型名", requiredMode = Schema.RequiredMode.REQUIRED, example = "微信")
private String typeName;
@Schema(description = "提现状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer status;
@ -24,4 +30,15 @@ public class AppBrokerageWithdrawRespVO {
@Schema(description = "提现时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
// ========== 微信转账专属 ==========
@Schema(description = "转账单编号", example = "1024")
private Long payTransferId;
@Schema(description = "渠道 package 信息")
private String transferChannelPackageInfo;
@Schema(description = "渠道商户号")
private String transferChannelMchId;
}

View File

@ -1,15 +1,11 @@
package cn.iocoder.yudao.module.trade.convert.brokerage;
import cn.hutool.core.math.Money;
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 cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.record.BrokerageRecordRespVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.record.AppBrokerageRecordPageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.record.AppBrokerageRecordRespVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByPriceRespVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageRecordDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
@ -68,8 +64,6 @@ public interface BrokerageRecordConvert {
return result;
}
BrokerageRecordPageReqVO convert(AppBrokerageRecordPageReqVO pageReqVO, Long userId);
default PageResult<AppBrokerageUserRankByPriceRespVO> convertPage03(PageResult<AppBrokerageUserRankByPriceRespVO> pageResult, Map<Long, MemberUserRespDTO> userMap) {
for (AppBrokerageUserRankByPriceRespVO vo : pageResult.getList()) {
copyTo(userMap.get(vo.getId()), vo);

View File

@ -1,15 +1,9 @@
package cn.iocoder.yudao.module.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 cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRespVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawPageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawRespVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO;
import cn.iocoder.yudao.module.trade.enums.DictTypeConstants;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@ -27,8 +21,6 @@ public interface BrokerageWithdrawConvert {
BrokerageWithdrawConvert INSTANCE = Mappers.getMapper(BrokerageWithdrawConvert.class);
BrokerageWithdrawDO convert(AppBrokerageWithdrawCreateReqVO createReqVO, Long userId, Integer feePrice);
BrokerageWithdrawRespVO convert(BrokerageWithdrawDO bean);
List<BrokerageWithdrawRespVO> convertList(List<BrokerageWithdrawDO> list);
@ -43,15 +35,4 @@ public interface BrokerageWithdrawConvert {
return result;
}
PageResult<AppBrokerageWithdrawRespVO> convertPage02(PageResult<BrokerageWithdrawDO> pageResult);
default PageResult<AppBrokerageWithdrawRespVO> convertPage03(PageResult<BrokerageWithdrawDO> pageResult) {
PageResult<AppBrokerageWithdrawRespVO> 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);
}

View File

@ -57,13 +57,25 @@ public class BrokerageWithdrawDO extends BaseDO {
private Integer type;
/**
* 真实姓名
* 提现姓名
* 1. {@link BrokerageWithdrawTypeEnum#BANK}银行开户人
* 2. {@link BrokerageWithdrawTypeEnum#WECHAT_API}微信真名
* 3. {@link BrokerageWithdrawTypeEnum#ALIPAY_API}支付宝真名
*/
private String name;
private String userName;
/**
* 账号
* 提现账号
* 1. {@link BrokerageWithdrawTypeEnum#BANK}银行账号
* 2. {@link BrokerageWithdrawTypeEnum#WECHAT_API}微信 openid
* 3. {@link BrokerageWithdrawTypeEnum#ALIPAY_API}支付宝账号
*/
private String accountNo;
private String userAccount;
/**
* 收款码
*/
private String qrCodeUrl;
/**
* 银行名称
*/
@ -72,10 +84,7 @@ public class BrokerageWithdrawDO extends BaseDO {
* 开户地址
*/
private String bankAddress;
/**
* 收款码
*/
private String accountQrCodeUrl;
/**
* 状态
* <p>
@ -95,4 +104,27 @@ public class BrokerageWithdrawDO extends BaseDO {
*/
private String remark;
// ========== 转账相关字段 ==========
/**
* 转账单编号
*
* 关联 {@link cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO#getId()}
*/
private Long payTransferId;
/**
* 转账渠道
*
* 枚举 {@link cn.iocoder.yudao.module.pay.enums.PayChannelEnum}
*/
private String transferChannelCode;
/**
* 转账成功时间
*/
private LocalDateTime transferTime;
/**
* 转账错误提示
*/
private String transferErrorMsg;
}

View File

@ -27,18 +27,18 @@ public interface BrokerageWithdrawMapper extends BaseMapperX<BrokerageWithdrawDO
return selectPage(reqVO, new LambdaQueryWrapperX<BrokerageWithdrawDO>()
.eqIfPresent(BrokerageWithdrawDO::getUserId, reqVO.getUserId())
.eqIfPresent(BrokerageWithdrawDO::getType, reqVO.getType())
.likeIfPresent(BrokerageWithdrawDO::getName, reqVO.getName())
.eqIfPresent(BrokerageWithdrawDO::getAccountNo, reqVO.getAccountNo())
.likeIfPresent(BrokerageWithdrawDO::getUserName, reqVO.getUserName())
.likeIfPresent(BrokerageWithdrawDO::getUserAccount, reqVO.getUserAccount())
.likeIfPresent(BrokerageWithdrawDO::getBankName, reqVO.getBankName())
.eqIfPresent(BrokerageWithdrawDO::getStatus, reqVO.getStatus())
.betweenIfPresent(BrokerageWithdrawDO::getCreateTime, reqVO.getCreateTime())
.orderByAsc(BrokerageWithdrawDO::getStatus).orderByDesc(BrokerageWithdrawDO::getId));
.orderByDesc(BrokerageWithdrawDO::getId));
}
default int updateByIdAndStatus(Long id, Integer status, BrokerageWithdrawDO updateObj) {
default int updateByIdAndStatus(Long id, Integer whereStatus, BrokerageWithdrawDO updateObj) {
return update(updateObj, new LambdaUpdateWrapper<BrokerageWithdrawDO>()
.eq(BrokerageWithdrawDO::getId, id)
.eq(BrokerageWithdrawDO::getStatus, status));
.eq(BrokerageWithdrawDO::getStatus, whereStatus));
}
default List<BrokerageWithdrawSummaryRespBO> selectCountAndSumPriceByUserIdAndStatus(Collection<Long> userIds,

View File

@ -1,18 +1,20 @@
package cn.iocoder.yudao.module.trade.framework.aftersale.core.aop;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderLogDO;
import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleOperateTypeEnum;
import cn.iocoder.yudao.module.trade.framework.aftersale.core.annotations.AfterSaleLog;
import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleLogService;
import cn.iocoder.yudao.module.trade.service.aftersale.bo.AfterSaleLogCreateReqBO;
import jakarta.annotation.Resource;
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;
@ -50,6 +52,10 @@ public class AfterSaleLogAspect {
* 操作后的状态
*/
private static final ThreadLocal<Integer> AFTER_STATUS = new ThreadLocal<>();
/**
* 操作类型动态场景需要使用
*/
private static final ThreadLocal<AfterSaleOperateTypeEnum> OPERATE_TYPE = new ThreadLocal<>();
/**
* 拓展参数 Map用于格式化操作内容
*/
@ -69,6 +75,7 @@ public class AfterSaleLogAspect {
if (afterSaleId == null) { // 如果未设置只有注解说明不需要记录日志
return;
}
AfterSaleOperateTypeEnum operateType = ObjUtil.defaultIfNull(OPERATE_TYPE.get(), afterSaleLog.operateType());
Integer beforeStatus = BEFORE_STATUS.get();
Integer afterStatus = AFTER_STATUS.get();
Map<String, Object> exts = ObjectUtil.defaultIfNull(EXTS.get(), emptyMap());
@ -78,7 +85,7 @@ public class AfterSaleLogAspect {
AfterSaleLogCreateReqBO createBO = new AfterSaleLogCreateReqBO()
.setUserId(userId).setUserType(userType)
.setAfterSaleId(afterSaleId).setBeforeStatus(beforeStatus).setAfterStatus(afterStatus)
.setOperateType(afterSaleLog.operateType().getType()).setContent(content);
.setOperateType(operateType.getType()).setContent(content);
afterSaleLogService.createAfterSaleLog(createBO);
} catch (Exception exception) {
log.error("[doAfterReturning][afterSaleLog({}) 日志记录错误]", toJsonString(afterSaleLog), exception);
@ -116,6 +123,10 @@ public class AfterSaleLogAspect {
EXTS.set(exts);
}
public static void setAfterSaleOperateType(AfterSaleOperateTypeEnum operateType) {
OPERATE_TYPE.set(operateType);
}
public static void setUserInfo(Long userId, Integer userType) {
USER_ID.set(userId);
USER_TYPE.set(userType);
@ -127,6 +138,7 @@ public class AfterSaleLogAspect {
AFTER_SALE_ID.remove();
BEFORE_STATUS.remove();
AFTER_STATUS.remove();
OPERATE_TYPE.remove();
EXTS.remove();
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.trade.framework.aftersale.core.utils;
import cn.iocoder.yudao.module.trade.enums.aftersale.AfterSaleOperateTypeEnum;
import cn.iocoder.yudao.module.trade.framework.aftersale.core.aop.AfterSaleLogAspect;
import java.util.Map;
@ -13,6 +14,10 @@ import java.util.Map;
*/
public class AfterSaleLogUtils {
public static void setAfterSaleOperateType(AfterSaleOperateTypeEnum operateType) {
AfterSaleLogAspect.setAfterSaleOperateType(operateType);
}
public static void setAfterSaleInfo(Long id, Integer beforeStatus, Integer afterStatus) {
setAfterSaleInfo(id, beforeStatus, afterStatus, null);
}

View File

@ -108,6 +108,15 @@ public interface AfterSaleService {
*/
void refundAfterSale(Long userId, String userIp, Long id);
/**
* 更新售后订单为已退款
*
* @param id 售后编号
* @param orderId 订单编号
* @param payRefundId 支付退款编号
*/
void updateAfterSaleRefunded(Long id, Long orderId, Long payRefundId);
/**
* 会员取消售后
*

View File

@ -7,6 +7,8 @@ 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.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
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;
@ -41,13 +43,12 @@ 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;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
/**
@ -184,8 +185,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
// 记录售后日志
AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), null,
AfterSaleStatusEnum.APPLY.getStatus());
// TODO 发送售后消息
return afterSale;
}
@ -206,8 +205,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
// 记录售后日志
AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), newStatus);
// TODO 发送售后消息
}
@Override
@ -226,8 +223,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
// 记录售后日志
AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), newStatus);
// TODO 发送售后消息
// 更新交易订单项的售后状态为未申请
tradeOrderUpdateService.updateOrderItemWhenAfterSaleCancel(afterSale.getOrderItemId());
}
@ -281,8 +276,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
AfterSaleStatusEnum.BUYER_DELIVERY.getStatus(),
MapUtil.<String, Object>builder().put("deliveryName", express.getName())
.put("logisticsNo", deliveryReqVO.getLogisticsNo()).build());
// TODO 发送售后消息
}
@Override
@ -299,8 +292,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
// 记录售后日志
AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(),
AfterSaleStatusEnum.WAIT_REFUND.getStatus());
// TODO 发送售后消息
}
@Override
@ -326,8 +317,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
AfterSaleStatusEnum.SELLER_REFUSE.getStatus(),
MapUtil.of("reason", refuseReqVO.getRefuseMemo()));
// TODO 发送售后消息
// 更新交易订单项的售后状态为未申请
tradeOrderUpdateService.updateOrderItemWhenAfterSaleCancel(afterSale.getOrderItemId());
}
@ -365,33 +354,90 @@ public class AfterSaleServiceImpl implements AfterSaleService {
// 发起退款单注意需要在事务提交后再进行发起避免重复发起
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());
afterSale.getStatus()); // 特殊这里状态不变而是最终 updateAfterSaleRefunded 处理
}
private void createPayRefund(String userIp, AfterSaleDO afterSale) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
// 创建退款单
PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties)
.setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));
Long payRefundId = payRefundApi.createRefund(createReqDTO).getCheckedData();
@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));
}
});
// 更新售后单的退款单号
tradeAfterSaleMapper.updateById(new AfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));
}
@Override
@Transactional(rollbackFor = Exception.class)
@AfterSaleLog(operateType = AfterSaleOperateTypeEnum.SYSTEM_REFUND_SUCCESS)
public void updateAfterSaleRefunded(Long id, Long orderId, Long payRefundId) {
// 1. 校验售后单的状态并状态待退款
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);
}
// 2. 校验退款单
PayRefundRespDTO payRefund = validatePayRefund(afterSale, payRefundId);
// 3. 处理退款结果
if (PayRefundStatusEnum.isSuccess(payRefund.getStatus())) {
// 情况一退款成功
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());
// 更新交易订单项的售后状态为已完成
tradeOrderUpdateService.updateOrderItemWhenAfterSaleSuccess(afterSale.getOrderItemId(), afterSale.getRefundPrice());
// 情况二退款失败
} else if (PayRefundStatusEnum.isFailure(payRefund.getStatus())) {
// 记录售后日志
AfterSaleLogUtils.setAfterSaleOperateType(AfterSaleOperateTypeEnum.SYSTEM_REFUND_FAIL);
AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(), afterSale.getStatus());
}
}
/**
* 校验退款单的合法性
*
* @param afterSale 售后单
* @param payRefundId 退款单编号
* @return 退款单
*/
private PayRefundRespDTO validatePayRefund(AfterSaleDO afterSale, Long payRefundId) {
// 1. 校验退款单是否存在
PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId).getCheckedData();
if (payRefund == null) {
log.error("[validatePayRefund][afterSale({}) payRefund({}) 不存在,请进行处理!]", afterSale.getId(), payRefundId);
throw exception(AFTER_SALE_REFUND_FAIL_REFUND_NOT_FOUND);
}
// 2.1 校验退款单无退款结果成功失败
if (!PayRefundStatusEnum.isSuccess(payRefund.getStatus())
&& !PayRefundStatusEnum.isFailure(payRefund.getStatus())) {
log.error("[validatePayRefund][afterSale({}) payRefund({}) 无退款结果请进行处理payRefund 数据是:{}]",
afterSale.getId(), payRefundId, toJsonString(payRefund));
throw exception(AFTER_SALE_REFUND_FAIL_REFUND_NOT_SUCCESS_OR_FAILURE);
}
// 2.2 校验退款金额一致
if (ObjectUtil.notEqual(payRefund.getRefundPrice(), afterSale.getRefundPrice())) {
log.error("[validatePayRefund][afterSale({}) payRefund({}) 退款金额不匹配请进行处理afterSale 数据是:{}payRefund 数据是:{}]",
afterSale.getId(), payRefundId, toJsonString(afterSale), toJsonString(payRefund));
throw exception(AFTER_SALE_REFUND_FAIL_REFUND_PRICE_NOT_MATCH);
}
// 2.3 校验退款订单匹配二次
if (ObjectUtil.notEqual(payRefund.getMerchantRefundId(), afterSale.getId().toString())) {
log.error("[validatePayRefund][afterSale({}) 退款单不匹配({})请进行处理payRefund 数据是:{}]",
afterSale.getId(), payRefundId, toJsonString(payRefund));
throw exception(AFTER_SALE_REFUND_FAIL_REFUND_ORDER_ID_ERROR);
}
return payRefund;
}
@Override
@ -417,8 +463,6 @@ public class AfterSaleServiceImpl implements AfterSaleService {
AfterSaleLogUtils.setAfterSaleInfo(afterSale.getId(), afterSale.getStatus(),
AfterSaleStatusEnum.BUYER_CANCEL.getStatus());
// TODO 发送售后消息
// 更新交易订单项的售后状态为未申请
tradeOrderUpdateService.updateOrderItemWhenAfterSaleCancel(afterSale.getOrderItemId());
}

View File

@ -1,25 +1,24 @@
package cn.iocoder.yudao.module.trade.service.brokerage;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
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.PayTransferCreateRespDTO;
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.api.wallet.dto.PayWalletRespDTO;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
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 cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO;
import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageWithdrawConvert;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageWithdrawMapper;
@ -29,6 +28,7 @@ import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum;
import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
import cn.iocoder.yudao.module.trade.service.brokerage.bo.BrokerageWithdrawSummaryRespBO;
import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;
import com.google.common.base.Objects;
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import lombok.extern.slf4j.Slf4j;
@ -40,10 +40,12 @@ import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
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.convertSet;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_TRANSFER_NOT_FOUND;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
/**
@ -64,13 +66,9 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
@Resource
private TradeConfigService tradeConfigService;
@Resource
private NotifyMessageSendApi notifyMessageSendApi;
@Resource
private PayTransferApi payTransferApi;
@Resource
private SocialUserApi socialUserApi;
@Resource
private PayWalletApi payWalletApi;
@Resource
@ -84,15 +82,24 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
public void auditBrokerageWithdraw(Long id, BrokerageWithdrawStatusEnum status, String auditReason, String userIp) {
// 1.1 校验存在
BrokerageWithdrawDO withdraw = validateBrokerageWithdrawExists(id);
// 1.2 特殊重新转账如果是提现失败并且状态是审核中那么更新状态为审核中并且清空 transferErrorMsg
if (BrokerageWithdrawStatusEnum.WITHDRAW_FAIL.getStatus().equals(withdraw.getStatus())) {
int updateCount = brokerageWithdrawMapper.updateByIdAndStatus(id, withdraw.getStatus(),
new BrokerageWithdrawDO().setStatus(BrokerageWithdrawStatusEnum.AUDITING.getStatus()).setTransferErrorMsg(""));
if (updateCount == 0) {
throw exception(BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING);
}
withdraw.setStatus(BrokerageWithdrawStatusEnum.AUDITING.getStatus()).setTransferErrorMsg("");
}
// 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(),
int updateCount = brokerageWithdrawMapper.updateByIdAndStatus(id, withdraw.getStatus(),
new BrokerageWithdrawDO().setStatus(status.getStatus()).setAuditReason(auditReason).setAuditTime(LocalDateTime.now()));
if (rows == 0) {
if (updateCount == 0) {
throw exception(BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING);
}
@ -109,44 +116,48 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
}
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());
// 情况一通过 API 转账
if (BrokerageWithdrawTypeEnum.isApi(withdraw.getType())) {
createPayTransfer(withdraw);
return;
}
// 2. 非支付 API则直接体现成功
if (!BrokerageWithdrawTypeEnum.isApi(withdraw.getType())) {
brokerageWithdrawMapper.updateByIdAndStatus(withdraw.getId(), BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.getStatus(),
new BrokerageWithdrawDO().setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus()));
}
// 情况二 API 转账手动打款
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_PROGRAM.getType()).getCheckedData();
// TODO @luchi这里需要校验非空如果空的话要有业务异常哈
private void createPayTransfer(BrokerageWithdrawDO withdraw) {
// 1.1 获取基础信息
String userAccount = withdraw.getUserAccount();
String userName = withdraw.getUserName();
String channelCode = null;
Map<String, String> channelExtras = null;
if (Objects.equal(withdraw.getType(), BrokerageWithdrawTypeEnum.ALIPAY_API.getType())) {
channelCode = PayChannelEnum.ALIPAY_PC.getCode();
} else if (Objects.equal(withdraw.getType(), BrokerageWithdrawTypeEnum.WECHAT_API.getType())) {
channelCode = withdraw.getTransferChannelCode();
userAccount = withdraw.getUserAccount();
// 特殊微信需要有报备信息
channelExtras = PayTransferCreateReqDTO.buildWeiXinChannelExtra1000("佣金提现", "佣金提现");
} else if (Objects.equal(withdraw.getType(), BrokerageWithdrawTypeEnum.WALLET.getType())) {
PayWalletRespDTO wallet = payWalletApi.getOrCreateWallet(withdraw.getUserId(), UserTypeEnum.MEMBER.getValue()).getCheckedData();
Assert.notNull(wallet, "钱包不存在");
channelCode = PayChannelEnum.WALLET.getCode();
userAccount = wallet.getId().toString();
}
// 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();
PayTransferCreateReqDTO transferReqDTO = new PayTransferCreateReqDTO()
.setAppKey(tradeOrderProperties.getPayAppKey()).setChannelCode(channelCode)
.setMerchantTransferId(withdraw.getId().toString()).setSubject("佣金提现").setPrice(withdraw.getPrice())
.setUserAccount(userAccount).setUserName(userName).setUserIp(getClientIP())
.setChannelExtras(channelExtras);
// 1.3 发起请求
PayTransferCreateRespDTO transferRespDTO = payTransferApi.createTransfer(transferReqDTO).getCheckedData();
// 2. 更新提现记录
brokerageWithdrawMapper.updateById(new BrokerageWithdrawDO().setId(withdraw.getId())
.setPayTransferId(transferRespDTO.getId()).setTransferChannelCode(channelCode));
}
private BrokerageWithdrawDO validateBrokerageWithdrawExists(Long id) {
@ -178,7 +189,8 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
// 2.1 计算手续费
Integer feePrice = calculateFeePrice(createReqVO.getPrice(), tradeConfig.getBrokerageWithdrawFeePercent());
// 2.2 创建佣金提现记录
BrokerageWithdrawDO withdraw = BrokerageWithdrawConvert.INSTANCE.convert(createReqVO, userId, feePrice);
BrokerageWithdrawDO withdraw = BeanUtils.toBean(createReqVO, BrokerageWithdrawDO.class)
.setUserId(userId).setFeePrice(feePrice);
brokerageWithdrawMapper.insert(withdraw);
// 3. 创建用户佣金记录
@ -220,22 +232,71 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
@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());
// 1.1 校验提现单是否存在
BrokerageWithdrawDO withdraw = brokerageWithdrawMapper.selectById(id);
if (withdraw == null) {
log.error("[updateBrokerageWithdrawTransferred][withdraw({}) payTransfer({}) 不存在提现单,请进行处理!]", id, payTransferId);
throw exception(BROKERAGE_WITHDRAW_NOT_EXISTS);
}
brokerageWithdrawMapper.updateById(withdraw);
// 1.2 校验提现单已经结束成功或失败
if (ObjectUtils.equalsAny(withdraw.getStatus(), BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus(),
BrokerageWithdrawStatusEnum.WITHDRAW_FAIL.getStatus())) {
// 特殊转账单编号相同直接返回说明重复回调
if (ObjectUtil.equal(withdraw.getPayTransferId(), payTransferId)) {
log.warn("[updateBrokerageWithdrawTransferred][withdraw({}) 已结束,且转账单编号相同({}),直接返回]", withdraw, payTransferId);
return;
}
// 异常转账单编号不同说明转账单编号错误
log.error("[updateBrokerageWithdrawTransferred][withdraw({}) 转账单不匹配({}),请进行处理!]", withdraw, payTransferId);
throw exception(BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR);
}
// 2. 校验转账单的合法性
PayTransferRespDTO payTransfer = validateBrokerageTransferStatusCanUpdate(withdraw, payTransferId);
// 3. 更新提现单状态
Integer newStatus = PayTransferStatusEnum.isSuccess(payTransfer.getStatus()) ? BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus() :
PayTransferStatusEnum.isClosed(payTransfer.getStatus()) ? BrokerageWithdrawStatusEnum.WITHDRAW_FAIL.getStatus() : null;
Assert.notNull(newStatus, "转账单状态({}) 不合法", payTransfer.getStatus());
brokerageWithdrawMapper.updateByIdAndStatus(withdraw.getId(), withdraw.getStatus(),
new BrokerageWithdrawDO().setStatus(newStatus)
.setTransferTime(payTransfer.getSuccessTime())
.setTransferErrorMsg(payTransfer.getChannelErrorMsg()));
}
private PayTransferRespDTO validateBrokerageTransferStatusCanUpdate(BrokerageWithdrawDO withdraw, Long payTransferId) {
// 1. 校验转账单是否存在
PayTransferRespDTO payTransfer = payTransferApi.getTransfer(payTransferId).getCheckedData();
if (payTransfer == null) {
log.error("[validateBrokerageTransferStatusCanUpdate][withdraw({}) payTransfer({}) 不存在,请进行处理!]", withdraw.getId(), payTransferId);
throw exception(PAY_TRANSFER_NOT_FOUND);
}
// 2.1 校验转账单已成功或关闭
if (!PayTransferStatusEnum.isSuccessOrClosed(payTransfer.getStatus())) {
log.error("[validateBrokerageTransferStatusCanUpdate][withdraw({}) payTransfer({}) 未结束请进行处理payTransfer 数据是:{}]",
withdraw.getId(), payTransferId, JsonUtils.toJsonString(payTransfer));
throw exception(BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_STATUS_NOT_SUCCESS_OR_CLOSED);
}
// 2.2 校验转账金额一致
if (ObjectUtil.notEqual(payTransfer.getPrice(), withdraw.getPrice())) {
log.error("[validateBrokerageTransferStatusCanUpdate][withdraw({}) payTransfer({}) 转账金额不匹配请进行处理withdraw 数据是:{}payTransfer 数据是:{}]",
withdraw.getId(), payTransferId, JsonUtils.toJsonString(withdraw), JsonUtils.toJsonString(payTransfer));
throw exception(BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_PRICE_NOT_MATCH);
}
// 2.3 校验转账订单匹配
if (ObjectUtil.notEqual(payTransfer.getMerchantTransferId(), withdraw.getId().toString())) {
log.error("[validateBrokerageTransferStatusCanUpdate][withdraw({}) 转账单不匹配({})请进行处理payTransfer 数据是:{}]",
withdraw.getId(), payTransferId, JsonUtils.toJsonString(payTransfer));
throw exception(BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_MERCHANT_EXISTS);
}
// 2.4 校验转账渠道一致
if (ObjectUtil.notEqual(payTransfer.getChannelCode(), withdraw.getTransferChannelCode())) {
log.error("[validateBrokerageTransferStatusCanUpdate][withdraw({}) payTransfer({}) 转账渠道不匹配请进行处理withdraw 数据是:{}payTransfer 数据是:{}]",
withdraw.getId(), payTransferId, JsonUtils.toJsonString(withdraw), JsonUtils.toJsonString(payTransfer));
throw exception(BROKERAGE_WITHDRAW_UPDATE_STATUS_FAIL_PAY_CHANNEL_NOT_MATCH);
}
return payTransfer;
}
@Override

View File

@ -209,6 +209,14 @@ public interface TradeOrderUpdateService {
*/
void cancelPaidOrder(Long userId, Long orderId, Integer cancelType);
/**
* 取消支付订单的退款回调
*
* @param id 订单编号
* @param payRefundId 支付退款编号
*/
void updatePaidOrderRefunded(Long id, Long payRefundId);
/**
* 更新下单赠送的优惠券编号到订单
*

View File

@ -20,7 +20,9 @@ import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
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;
@ -280,7 +282,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
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;
@ -420,7 +422,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
.addMessage("phrase6", TradeOrderStatusEnum.DELIVERED.getName()) // 订单状态
.addMessage("date4", LocalDateTimeUtil.formatNormal(LocalDateTime.now()))// 发货时间
.addMessage("character_string5", StrUtil.blankToDefault(deliveryReqVO.getLogisticsNo(), "-")) // 快递单号
.addMessage("thing9", order.getReceiverDetailAddress())).checkError(); // 收货地址
.addMessage("thing9", order.getReceiverDetailAddress())).getCheckedData(); // 收货地址
}
/**
@ -938,10 +940,24 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
.setAppKey(tradeOrderProperties.getPayAppKey()) // 支付应用
.setUserIp(NetUtil.getLocalhostStr()) // 使用本机 IP因为是服务器发起退款的
.setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
.setMerchantRefundId(String.valueOf(order.getId()))
// 特殊因为订单支持 AfterSale 单个售后退款也支持整单退款所以需要通过 order- 进行下区分
// 具体可见 AfterSaleController updateAfterSaleRefunded 方法
.setMerchantRefundId("order-" + order.getId())
.setReason(TradeOrderCancelTypeEnum.COMBINATION_CLOSE.getName()).setPrice(order.getPayPrice())).checkError(); // 价格信息
}
@Override
public void updatePaidOrderRefunded(Long id, Long payRefundId) {
PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId).getCheckedData();
if (payRefund == null) {
throw exception(ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND);
}
// 特殊因为在 cancelPaidOrder 已经进行订单的取消所以这里必须退款成功
if (!PayRefundStatusEnum.isSuccess(payRefund.getStatus())) {
throw exception(ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS);
}
}
@Override
public void updateOrderGiveCouponIds(Long userId, Long orderId, List<Long> giveCouponIds) {
// 1. 检验订单存在

View File

@ -24,6 +24,12 @@ public class PayRefundNotifyReqDTO {
@NotEmpty(message = "商户退款单编号不能为空")
private String merchantOrderId;
/**
* 商户退款编号
*/
@NotEmpty(message = "商户退款编号不能为空")
private String merchantRefundId;
/**
* 支付退款编号
*/

View File

@ -2,7 +2,10 @@ package cn.iocoder.yudao.module.pay.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
@ -10,6 +13,9 @@ import lombok.Data;
* @author jason
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayTransferNotifyReqDTO {
/**
@ -23,4 +29,5 @@ public class PayTransferNotifyReqDTO {
*/
@NotNull(message = "转账订单编号不能为空")
private Long payTransferId;
}

View File

@ -18,6 +18,13 @@ public class PayRefundRespDTO {
*/
private Long id;
/**
* 渠道编码
*
* 枚举 PayChannelEnum
*/
private String channelCode;
// ========== 退款相关字段 ==========
/**
* 退款状态
@ -35,9 +42,24 @@ public class PayRefundRespDTO {
* 商户订单编号
*/
private String merchantOrderId;
/**
* 商户退款编号
*/
private String merchantRefundId;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
// ========== 渠道相关字段 ==========
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道的错误提示
*/
private String channelErrorMsg;
}

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pay.api.transfer;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateRespDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation;
@ -23,7 +24,7 @@ public interface PayTransferApi {
@PostMapping(PREFIX + "/create")
@Operation(summary = "创建转账单")
CommonResult<Long> createTransfer(@Valid @RequestBody PayTransferCreateReqDTO reqDTO);
CommonResult<PayTransferCreateRespDTO> createTransfer(@Valid @RequestBody PayTransferCreateReqDTO reqDTO);
@GetMapping(PREFIX + "/get")
@Operation(summary = "获得转账单")

View File

@ -1,13 +1,15 @@
package cn.iocoder.yudao.module.pay.api.transfer.dto;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -24,6 +26,9 @@ public class PayTransferCreateReqDTO {
@NotNull(message = "应用标识不能为空")
private String appKey;
/**
* 转账渠道
*/
@NotEmpty(message = "转账渠道不能为空")
private String channelCode;
@ -32,17 +37,12 @@ public class PayTransferCreateReqDTO {
*/
private Map<String, String> channelExtras;
/**
* 用户 IP
*/
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
/**
* 类型
*/
@NotNull(message = "转账类型不能为空")
@InEnum(PayTransferTypeEnum.class)
private Integer type;
/**
* 商户转账单编号
*/
@ -62,16 +62,59 @@ public class PayTransferCreateReqDTO {
@NotEmpty(message = "转账标题不能为空")
private String subject;
/**
* 收款人账号
*
* 微信场景下openid
* 支付宝场景下支付宝账号
*/
@NotEmpty(message = "收款人账号不能为空")
private String userAccount;
/**
* 收款人姓名
*/
@NotBlank(message = "收款人姓名不能为空", groups = {PayTransferTypeEnum.Alipay.class})
private String userName;
@NotBlank(message = "支付宝登录号不能为空", groups = {PayTransferTypeEnum.Alipay.class})
private String alipayLogonId;
/**
* 微信现金营销场景
*
* @param activityName 活动名称
* @param rewardDescription 奖励说明
* @return channelExtras
*/
public static Map<String, String> buildWeiXinChannelExtra1000(String activityName, String rewardDescription) {
return buildWeiXinChannelExtra(1000,
"活动名称", activityName,
"奖励说明", rewardDescription);
}
/**
* 微信企业报销场景
*
* @param expenseType 报销类型
* @param expenseDescription 报销说明
* @return channelExtras
*/
public static Map<String, String> buildWeiXinChannelExtra1006(String expenseType, String expenseDescription) {
return buildWeiXinChannelExtra(1006,
"报销类型", expenseType,
"报销说明", expenseDescription);
}
private static Map<String, String> buildWeiXinChannelExtra(Integer sceneId, String... values) {
Map<String, String> channelExtras = new HashMap<>();
// 构建场景报备信息列表
List<Map<String, String>> sceneReportInfos = new ArrayList<>();
for (int i = 0; i < values.length; i += 2) {
Map<String, String> info = new HashMap<>();
info.put("infoType", values[i]);
info.put("infoContent", values[i + 1]);
sceneReportInfos.add(info);
}
// 设置场景ID和场景报备信息
channelExtras.put("sceneId", StrUtil.toString(sceneId));
channelExtras.put("sceneReportInfos", JsonUtils.toJsonString(sceneReportInfos));
return channelExtras;
}
// ========== 微信转账相关字段 ==========
@NotBlank(message = "微信 openId 不能为空", groups = {PayTransferTypeEnum.WxPay.class})
private String openid;
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.pay.api.transfer.dto;
import lombok.Data;
/**
* 转账单创建 Response DTO
*
* @author 芋道源码
*/
@Data
public class PayTransferCreateRespDTO {
/**
* 编号
*/
private Long id;
// ========== 其它字段 ==========
/**
* 渠道 package 信息
*
* 特殊目前只有微信转账有这个东西
* @see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012716430">JSAPI 调起用户确认收款</a>
*/
private String channelPackageInfo;
}

View File

@ -1,8 +1,11 @@
package cn.iocoder.yudao.module.pay.api.transfer.dto;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class PayTransferRespDTO {
@ -16,6 +19,22 @@ public class PayTransferRespDTO {
*/
private String no;
/**
* 转账渠道编码
*
* 枚举 {@link PayChannelEnum}
*/
private String channelCode;
// ========== 商户相关字段 ==========
/**
* 商户转账单编号
*/
private String merchantTransferId;
// ========== 转账相关字段 ==========
/**
* 转账金额单位
*/
@ -28,4 +47,35 @@ public class PayTransferRespDTO {
*/
private Integer status;
/**
* 订单转账成功时间
*/
private LocalDateTime successTime;
// ========== 其它字段 ==========
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道的错误提示
*/
private String channelErrorMsg;
/**
* 渠道 package 信息
*
* 特殊目前只有微信转账有这个东西
* @see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012716430">JSAPI 调起用户确认收款</a>
*/
private String channelPackageInfo;
/**
* 渠道商户号
*
* 特殊目前只有微信转账有这个东西
* @see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012716430">JSAPI 调起用户确认收款</a>
*/
private String channelMchId;
}

View File

@ -2,13 +2,18 @@ package cn.iocoder.yudao.module.pay.api.wallet;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO;
import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletRespDTO;
import cn.iocoder.yudao.module.pay.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
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;
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿fallbackFactory =
@Tag(name = "RPC 服务 - 钱包")
@ -20,4 +25,14 @@ public interface PayWalletApi {
@Operation(summary = "添加钱包余额")
CommonResult<Boolean> addWalletBalance(@Valid @RequestBody PayWalletAddBalanceReqDTO reqDTO);
@GetMapping(PREFIX + "/get-or-create")
@Operation(summary = "获取钱包信息")
@Parameters({
@Parameter(name = "userId", description = "用户编号", required = true, example = "1024"),
@Parameter(name = "userType", description = "用户类型", required = true, example = "1")
})
CommonResult<PayWalletRespDTO> getOrCreateWallet(@RequestParam("userId") Long userId,
@RequestParam("userType") Integer userType);
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.pay.api.wallet.dto;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import lombok.Data;
/**
* 钱包 Response DTO
*
* @author jason
*/
@Data
public class PayWalletRespDTO {
/**
* 编号
*/
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;
}

View File

@ -15,4 +15,6 @@ public interface DictTypeConstants {
String NOTIFY_STATUS = "pay_notify_status"; // 回调状态
String TRANSFER_STATUS = "pay_transfer_status"; // 转账状态
}

View File

@ -65,13 +65,12 @@ public interface ErrorCodeConstants {
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, "转账单不处于待转账或转账中");
ErrorCode PAY_TRANSFER_CREATE_CHANNEL_NOT_MATCH = new ErrorCode(1_007_009_002, "转账发起失败,原因:两次相同转账请求的类型不匹配");
ErrorCode PAY_TRANSFER_CREATE_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "转账发起失败,原因:两次相同转账请求的金额不匹配");
ErrorCode PAY_TRANSFER_CREATE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_007_009_004, "转账发起失败,原因:已经存在相同的转账单,且状态不是已关闭");
ErrorCode PAY_TRANSFER_NOTIFY_FAIL_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_006, "通知转账结果失败,原因:转账单不处于待转账");
ErrorCode PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING = new ErrorCode(1_007_009_007, "通知转账结果失败,原因:转账单不处于待转账或转账中");
// ========== 示例订单 1-007-900-000 ==========
ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1_007_900_000, "示例订单不存在");
@ -86,8 +85,13 @@ public interface ErrorCodeConstants {
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, "转账失败,转账单金额不匹配");
// ========== 示例提现单 1-007-901-000 ==========
ErrorCode DEMO_WITHDRAW_NOT_FOUND = new ErrorCode(1_007_901_000, "示例提现单不存在");
ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR = new ErrorCode(1_007_901_001, "更新示例提现单状态失败,支付转账单编号不匹配");
ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_STATUS_NOT_SUCCESS_OR_CLOSED = new ErrorCode(1_007_901_002, "更新示例提现单状态失败,支付转账单状态不是【转账成功】或【转账失败】状态");
ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_PRICE_NOT_MATCH = new ErrorCode(1_007_901_003, "更新示例提现单状态失败,支付转账单金额不匹配");
ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_MERCHANT_EXISTS = new ErrorCode(1_007_901_004, "更新示例提现单状态失败,支付转账单商户订单号不匹配");
ErrorCode DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_CHANNEL_NOT_MATCH = new ErrorCode(1_007_901_005, "更新示例提现单状态失败,支付转账单渠道不匹配");
ErrorCode DEMO_WITHDRAW_TRANSFER_FAIL_STATUS_NOT_WAITING_OR_CLOSED = new ErrorCode(1_007_901_008, "发起转账失败,原因:示例提现单状态不是【等待提现】或【提现关闭】");
}

View File

@ -1,8 +1,11 @@
package cn.iocoder.yudao.module.pay.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 支付渠道的编码的枚举
*
@ -10,7 +13,7 @@ import lombok.Getter;
*/
@Getter
@AllArgsConstructor
public enum PayChannelEnum {
public enum PayChannelEnum implements ArrayValuable<String> {
WX_PUB("wx_pub", "微信 JSAPI 支付"), // 公众号网页
WX_LITE("wx_lite", "微信小程序支付"),
@ -28,6 +31,8 @@ public enum PayChannelEnum {
WALLET("wallet", "钱包支付");
public static final String[] ARRAYS = Arrays.stream(values()).map(PayChannelEnum::getCode).toArray(String[]::new);
/**
* 编码
*
@ -39,4 +44,9 @@ public enum PayChannelEnum {
*/
private final String name;
@Override
public String[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.pay.enums.demo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* 示例提现单的状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum PayDemoWithdrawStatusEnum {
WAITING(0, "等待提现"),
SUCCESS(10, "提现成功"),
CLOSED(20, "提现关闭");
/**
* 状态
*/
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());
}
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.pay.enums.demo;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 示例提现单的类型枚举
*
* @author owen
*/
@AllArgsConstructor
@Getter
public enum PayDemoWithdrawTypeEnum implements ArrayValuable<Integer> {
WECHAT(2, "微信"),
ALIPAY(1, "支付宝"),
WALLET(3, "钱包"),
;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(PayDemoWithdrawTypeEnum::getType).toArray(Integer[]::new);
/**
* 类型
*/
private final Integer type;
/**
* 名字
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@ -6,6 +6,8 @@ import lombok.Getter;
import java.util.Objects;
/**
* 渠道的转账状态枚举
*
* @author jason
*/
@Getter
@ -13,16 +15,9 @@ import java.util.Objects;
public enum PayTransferStatusEnum {
WAITING(0, "等待转账"),
/**
* TODO 转账到银行卡. 会有T+0 T+1 到账的请情况 还未实现
*/
IN_PROGRESS(10, "转账进行中"),
SUCCESS(20, "转账成功"),
/**
* 转账关闭 (失败或者其它情况) // TODO 改成 转账失败状态
*/
CLOSED(30, "转账关闭");
PROCESSING(5, "转账进行中"),
SUCCESS(10, "转账成功"),
CLOSED(20, "转账关闭");
/**
* 状态
@ -44,16 +39,29 @@ public enum PayTransferStatusEnum {
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());
public static boolean isProgressing(Integer status) {
return Objects.equals(status, PROCESSING.getStatus());
}
/**
* 是否处于待转账或者转账中的状态
*
* @param status 状态
* @return 是否
*/
public static boolean isPendingStatus(Integer status) {
return Objects.equals(status, WAITING.getStatus()) || Objects.equals(status, IN_PROGRESS.getStatus());
public static boolean isWaitingOrProcessing(Integer status) {
return isWaiting(status) || isProgressing(status);
}
/**
* 是否处于成功或者关闭中的状态
*
* @param status 状态
* @return 是否
*/
public static boolean isSuccessOrClosed(Integer status) {
return isSuccess(status) || isClosed(status);
}
}

View File

@ -1,44 +0,0 @@
package cn.iocoder.yudao.module.pay.enums.transfer;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 转账类型枚举
*
* @author jason
*/
@AllArgsConstructor
@Getter
public enum PayTransferTypeEnum implements ArrayValuable<Integer> {
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());
}
}

View File

@ -20,7 +20,7 @@ public enum PayWalletBizTypeEnum implements ArrayValuable<Integer> {
PAYMENT(3, "支付"),
PAYMENT_REFUND(4, "支付退款"),
UPDATE_BALANCE(5, "更新余额"),
BROKERAGE_WITHDRAW(6, "分佣提现");
TRANSFER(6, "转账");
/**
* 业务分类

View File

@ -1,9 +1,11 @@
package cn.iocoder.yudao.module.pay.api.refund;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
@ -21,12 +23,13 @@ public class PayRefundApiImpl implements PayRefundApi {
@Override
public CommonResult<Long> createRefund(PayRefundCreateReqDTO reqDTO) {
return success(payRefundService.createPayRefund(reqDTO));
return success(payRefundService.createRefund(reqDTO));
}
@Override
public CommonResult<PayRefundRespDTO> getRefund(Long id) {
return success(PayRefundConvert.INSTANCE.convert02(payRefundService.getRefund(id)));
PayRefundDO refund = payRefundService.getRefund(id);
return success(BeanUtils.toBean(refund, PayRefundRespDTO.class));
}
}

View File

@ -2,9 +2,13 @@ package cn.iocoder.yudao.module.pay.api.transfer;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateRespDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
@ -24,16 +28,26 @@ public class PayTransferApiImpl implements PayTransferApi {
@Resource
private PayTransferService payTransferService;
@Resource
private PayChannelService payChannelService;
@Override
public CommonResult<Long> createTransfer(PayTransferCreateReqDTO reqDTO) {
public CommonResult<PayTransferCreateRespDTO> createTransfer(PayTransferCreateReqDTO reqDTO) {
return success(payTransferService.createTransfer(reqDTO));
}
@Override
public CommonResult<PayTransferRespDTO> getTransfer(Long id) {
PayTransferDO transfer = payTransferService.getTransfer(id);
return success(BeanUtils.toBean(transfer, PayTransferRespDTO.class));
if (transfer == null) {
return null;
}
PayChannelDO channel = payChannelService.getChannel(transfer.getChannelId());
String mchId = null;
if (channel != null && channel.getConfig() instanceof WxPayClientConfig) {
mchId = ((WxPayClientConfig) channel.getConfig()).getMchId();
}
return success(BeanUtils.toBean(transfer, PayTransferRespDTO.class).setChannelMchId(mchId));
}
}

View File

@ -2,7 +2,9 @@ package cn.iocoder.yudao.module.pay.api.wallet;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletAddBalanceReqDTO;
import cn.iocoder.yudao.module.pay.api.wallet.dto.PayWalletRespDTO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
@ -36,4 +38,10 @@ public class PayWalletApiImpl implements PayWalletApi {
return success(true);
}
@Override
public CommonResult<PayWalletRespDTO> getOrCreateWallet(Long userId, Integer userType) {
PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType);
return success(BeanUtils.toBean(wallet, PayWalletRespDTO.class));
}
}

View File

@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.pay.controller.admin.demo;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
import cn.iocoder.yudao.module.pay.convert.demo.PayDemoOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import cn.iocoder.yudao.module.pay.service.demo.PayDemoOrderService;
import io.swagger.v3.oas.annotations.Operation;
@ -23,7 +23,7 @@ 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 = "管理后台 - 示例订单")
@Tag(name = "管理后台 - 示例订单") // 目的演示支付退款功能
@RestController
@RequestMapping("/pay/demo-order")
@Validated
@ -42,7 +42,7 @@ public class PayDemoOrderController {
@Operation(summary = "获得示例订单分页")
public CommonResult<PageResult<PayDemoOrderRespVO>> getDemoOrderPage(@Valid PageParam pageVO) {
PageResult<PayDemoOrderDO> pageResult = payDemoOrderService.getDemoOrderPage(pageVO);
return success(PayDemoOrderConvert.INSTANCE.convertPage(pageResult));
return success(BeanUtils.toBean(pageResult, PayDemoOrderRespVO.class));
}
@PostMapping("/update-paid")
@ -66,7 +66,9 @@ public class PayDemoOrderController {
@Operation(summary = "更新示例订单为已退款") // pay-module 支付服务进行回调可见 PayNotifyJob
@PermitAll // 无需登录安全由 PayDemoOrderService 内部校验实现
public CommonResult<Boolean> updateDemoOrderRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
payDemoOrderService.updateDemoOrderRefunded(Long.valueOf(notifyReqDTO.getMerchantOrderId()),
payDemoOrderService.updateDemoOrderRefunded(
Long.valueOf(notifyReqDTO.getMerchantOrderId()),
notifyReqDTO.getMerchantRefundId(),
notifyReqDTO.getPayRefundId());
return success(true);
}

View File

@ -1,51 +0,0 @@
package cn.iocoder.yudao.module.pay.controller.admin.demo;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferRespVO;
import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoTransferDO;
import cn.iocoder.yudao.module.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<Long> createDemoTransfer(@Valid @RequestBody PayDemoTransferCreateReqVO createReqVO) {
return success(demoTransferService.createDemoTransfer(createReqVO));
}
@GetMapping("/page")
@Operation(summary = "获得示例转账订单分页")
public CommonResult<PageResult<PayDemoTransferRespVO>> getDemoTransferPage(@Valid PageParam pageVO) {
PageResult<PayDemoTransferDO> pageResult = demoTransferService.getDemoTransferPage(pageVO);
return success(PayDemoTransferConvert.INSTANCE.convertPage(pageResult));
}
@PostMapping("/update-status")
@Operation(summary = "更新示例转账订单的转账状态") // pay-module 转账服务进行回调
@PermitAll // 无需登录安全由 PayDemoTransferService 内部校验实现
public CommonResult<Boolean> updateDemoTransferStatus(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
demoTransferService.updateDemoTransferStatus(Long.valueOf(notifyReqDTO.getMerchantTransferId()),
notifyReqDTO.getPayTransferId());
return success(true);
}
}

View File

@ -0,0 +1,50 @@
### 请求 /pay/demo-withdraw/create 接口(支付宝)
POST {{baseUrl}}/pay/demo-withdraw/create
Authorization: Bearer {{token}}
Content-Type: application/json
tenant-id: {{adminTenantId}}
{
"type": 1,
"subject": "测试转账",
"price": 10,
"userAccount": "oespxk7368@sandbox.com",
"userName": "oespxk7368"
}
### 请求 /pay/demo-withdraw/create 接口(微信余额)
POST {{baseUrl}}/pay/demo-withdraw/create
Authorization: Bearer {{token}}
Content-Type: application/json
tenant-id: {{adminTenantId}}
{
"type": 2,
"subject": "测试转账",
"price": 1,
"userAccount": "oiSC85elO_OZogXODC5RoGyXamK4",
"userName": "芋艿"
}
### 请求 /pay/demo-withdraw/create 接口(钱包余额)
POST {{baseUrl}}/pay/demo-withdraw/create
Authorization: Bearer {{token}}
Content-Type: application/json
tenant-id: {{adminTenantId}}
{
"type": 3,
"subject": "测试转账",
"price": 1,
"userAccount": "1"
}
### 请求 /pay/demo-withdraw/transfer 接口(发起转账)
POST {{baseUrl}}/pay/demo-withdraw/transfer?id=1
Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
### 请求 /pay/demo-withdraw/page 接口(查询分页)
GET {{baseUrl}}/pay/demo-withdraw/page?pageNo=1&pageSize=10
Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}

View File

@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.pay.controller.admin.demo;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.withdraw.PayDemoWithdrawCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.withdraw.PayDemoWithdrawRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoWithdrawDO;
import cn.iocoder.yudao.module.pay.service.demo.PayDemoWithdrawService;
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;
@Tag(name = "管理后台 - 示例提现订单") // 目的演示转账功能
@RestController
@RequestMapping("/pay/demo-withdraw")
@Validated
public class PayDemoWithdrawController {
@Resource
private PayDemoWithdrawService demoWithdrawService;
@PostMapping("/create")
@Operation(summary = "创建示例提现单")
public CommonResult<Long> createDemoWithdraw(@Valid @RequestBody PayDemoWithdrawCreateReqVO createReqVO) {
Long id = demoWithdrawService.createDemoWithdraw(createReqVO);
return success(id);
}
@PostMapping("/transfer")
@Operation(summary = "提现单转账")
@Parameter(name = "id", required = true, description = "提现单编号", example = "1024")
public CommonResult<Long> transferDemoWithdraw(@RequestParam("id") Long id) {
Long payTransferId = demoWithdrawService.transferDemoWithdraw(id);
return success(payTransferId);
}
@GetMapping("/page")
@Operation(summary = "获得示例提现单分页")
public CommonResult<PageResult<PayDemoWithdrawRespVO>> getDemoWithdrawPage(@Valid PageParam pageVO) {
PageResult<PayDemoWithdrawDO> pageResult = demoWithdrawService.getDemoWithdrawPage(pageVO);
return success(BeanUtils.toBean(pageResult, PayDemoWithdrawRespVO.class));
}
@PostMapping("/update-transferred")
@Operation(summary = "更新示例提现单的转账状态") // pay-module 转账服务进行回调
@PermitAll // 无需登录安全由 PayDemoTransferService 内部校验实现
public CommonResult<Boolean> updateDemoWithdrawTransferred(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
demoWithdrawService.updateDemoWithdrawTransferred(Long.valueOf(notifyReqDTO.getMerchantTransferId()),
notifyReqDTO.getPayTransferId());
return success(true);
}
}

View File

@ -1,67 +0,0 @@
package cn.iocoder.yudao.module.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("待实现");
}
}
}
}

View File

@ -1,47 +0,0 @@
package cn.iocoder.yudao.module.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;
}

View File

@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.withdraw;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.pay.enums.demo.PayDemoWithdrawTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
@Schema(description = "管理后台 - 示例提现单创建 Request VO")
@Data
public class PayDemoWithdrawCreateReqVO {
@Schema(description = "提现标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿是一种菜")
@NotEmpty(message = "提现标题不能为空")
private String subject;
@Schema(description = "提现金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "提现金额不能为空")
@Min(value = 1, message = "提现金额必须大于零")
private Integer price;
@Schema(description = "收款人账号", requiredMode= Schema.RequiredMode.REQUIRED, example = "test1")
@NotBlank(message = "收款人账号不能为空")
private String userAccount;
@Schema(description = "收款人姓名", example = "test1")
private String userName;
@Schema(description = "提现方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "提现方式不能为空")
@InEnum(PayDemoWithdrawTypeEnum.class)
private Integer type;
@AssertTrue(message = "收款人姓名")
public boolean isUserNameValid() {
// 特殊支付宝必须填写用户名
return ObjectUtil.notEqual(type, PayDemoWithdrawTypeEnum.ALIPAY.getType())
|| ObjectUtil.isNotEmpty(userName);
}
}

View File

@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.withdraw;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 示例转账单创建 Request VO")
@Data
public class PayDemoWithdrawRespVO {
@Schema(description = "转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "提现标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "吃饭报销")
private String subject;
@Schema(description = "提现金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "22338")
private Integer price;
@Schema(description = "收款人姓名", example = "test")
private String userName;
@Schema(description = "收款人账号", example = "32167")
private String userAccount;
@Schema(description = "提现类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer type;
@Schema(description = "提现状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer status;
// ========== 转账相关字段 ==========
@Schema(description = "转账单编号", example = "23695")
private Long payTransferId;
@Schema(description = "转账渠道", example = "wx_lite")
private String transferChannelCode;
@Schema(description = "转账成功时间")
private LocalDateTime transferTime;
@Schema(description = "转账失败原因", example = "IP 不正确")
private String transferErrorMsg;
}

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.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.common.util.object.BeanUtils;
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;
@ -11,7 +12,6 @@ import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO;
import cn.iocoder.yudao.module.pay.convert.notify.PayNotifyTaskConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
@ -138,7 +138,12 @@ public class PayNotifyController {
// 拼接返回
PayAppDO app = appService.getApp(task.getAppId());
List<PayNotifyLogDO> logs = notifyService.getNotifyLogList(id);
return success(PayNotifyTaskConvert.INSTANCE.convert(task, app, logs));
return success(BeanUtils.toBean(task, PayNotifyTaskDetailRespVO.class, respVO -> {
if (app != null) {
respVO.setAppName(app.getName());
}
respVO.setLogs(BeanUtils.toBean(logs, PayNotifyTaskDetailRespVO.Log.class));
}));
}
@GetMapping("/page")
@ -150,8 +155,15 @@ public class PayNotifyController {
return success(PageResult.empty());
}
// 拼接返回
Map<Long, PayAppDO> appMap = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTaskDO::getAppId));
return success(PayNotifyTaskConvert.INSTANCE.convertPage(pageResult, appMap));
Map<Long, PayAppDO> apps = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTaskDO::getAppId));
// 转换对象
return success(BeanUtils.toBean(pageResult, PayNotifyTaskRespVO.class, order -> {
PayAppDO app = apps.get(order.getAppId());
if (app != null) {
order.setAppName(app.getName());
}
}));
}
}

View File

@ -1,45 +0,0 @@
package cn.iocoder.yudao.module.pay.controller.admin.notify.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 回调通知 Base VO提供给添加修改详细的子 VO 使用
* 如果子 VO 存在差异的字段请不要添加到这里影响 Swagger 文档生成
*/
@Data
public class PayNotifyTaskBaseVO {
@Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10636")
private Long appId;
@Schema(description = "通知类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Byte type;
@Schema(description = "数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6722")
private Long dataId;
@Schema(description = "通知状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Byte status;
@Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26697")
private String merchantOrderId;
@Schema(description = "下一次通知时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime nextNotifyTime;
@Schema(description = "最后一次执行时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime lastExecuteTime;
@Schema(description = "当前通知次数", requiredMode = Schema.RequiredMode.REQUIRED)
private Byte notifyTimes;
@Schema(description = "最大可通知次数", requiredMode = Schema.RequiredMode.REQUIRED)
private Byte maxNotifyTimes;
@Schema(description = "异步通知地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn")
private String notifyUrl;
}

View File

@ -13,19 +13,7 @@ import java.util.List;
@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;
public class PayNotifyTaskDetailRespVO extends PayNotifyTaskRespVO {
@Schema(description = "回调日志列表")
private List<Log> logs;

View File

@ -32,6 +32,12 @@ public class PayNotifyTaskPageReqVO extends PageParam {
@Schema(description = "商户订单编号", example = "26697")
private String merchantOrderId;
@Schema(description = "商户退款编号", example = "26697")
private String merchantRefundId;
@Schema(description = "商户转账编号", example = "26697")
private String merchantTransferId;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;

View File

@ -1,25 +1,56 @@
package cn.iocoder.yudao.module.pay.controller.admin.notify.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.*;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 回调通知 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayNotifyTaskRespVO extends PayNotifyTaskBaseVO {
public class PayNotifyTaskRespVO {
@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, example = "10636")
private Long appId;
@Schema(description = "应用名称", example = "wx_pay")
private String appName;
@Schema(description = "通知类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Byte type;
@Schema(description = "数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6722")
private Long dataId;
@Schema(description = "商户订单编号", example = "26697")
private String merchantOrderId;
@Schema(description = "商户退款编号", example = "26697")
private String merchantRefundId;
@Schema(description = "商户转账编号", example = "26697")
private String merchantTransferId;
@Schema(description = "通知状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Byte status;
@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)
private LocalDateTime createTime;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
}

View File

@ -12,10 +12,12 @@ import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.WalletPayClient;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -51,6 +53,8 @@ public class PayOrderController {
private PayOrderService orderService;
@Resource
private PayAppService appService;
@Resource
private PayWalletService payWalletService;
@GetMapping("/get")
@Operation(summary = "获得支付订单")
@ -92,11 +96,11 @@ public class PayOrderController {
public CommonResult<PayOrderSubmitRespVO> submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) {
// 1. 钱包支付事需要额外传 user_id user_type
if (Objects.equals(reqVO.getChannelCode(), PayChannelEnum.WALLET.getCode())) {
Map<String, String> 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);
if (reqVO.getChannelExtras() == null) {
reqVO.setChannelExtras(Maps.newHashMapWithExpectedSize(1));
}
PayWalletDO wallet = payWalletService.getOrCreateWallet(getLoginUserId(), getLoginUserType());
reqVO.getChannelExtras().put(WalletPayClient.WALLET_ID_KEY, String.valueOf(wallet.getId()));
}
// 2. 提交支付
@ -123,7 +127,7 @@ public class PayOrderController {
@PreAuthorize("@ss.hasPermission('pay:order:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportOrderExcel(@Valid PayOrderExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
HttpServletResponse response) throws IOException {
List<PayOrderDO> list = orderService.getOrderList(exportReqVO);
if (CollectionUtil.isEmpty(list)) {
ExcelUtils.write(response, "支付订单.xls", "数据",

View File

@ -25,7 +25,6 @@ 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;
@ -78,13 +77,8 @@ public class PayRefundController {
@PreAuthorize("@ss.hasPermission('pay:refund:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportRefundExcel(@Valid PayRefundExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
HttpServletResponse response) throws IOException {
List<PayRefundDO> list = refundService.getRefundList(exportReqVO);
if (CollectionUtil.isEmpty(list)) {
ExcelUtils.write(response, "退款订单.xls", "数据",
PayRefundExcelVO.class, new ArrayList<>());
return;
}
// 拼接返回
Map<Long, PayAppDO> appMap = appService.getAppMap(convertList(list, PayRefundDO::getAppId));

View File

@ -1,21 +1,35 @@
package cn.iocoder.yudao.module.pay.controller.admin.transfer;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.*;
import cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.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.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 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.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.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@Tag(name = "管理后台 - 转账单")
@RestController
@ -25,27 +39,53 @@ public class PayTransferController {
@Resource
private PayTransferService payTransferService;
@PostMapping("/create")
@Operation(summary = "创建转账单,发起转账")
@PreAuthorize("@ss.hasPermission('pay:transfer:create')")
public CommonResult<PayTransferCreateRespVO> createPayTransfer(@Valid @RequestBody PayTransferCreateReqVO reqVO) {
PayTransferDO payTransfer = payTransferService.createTransfer(reqVO, getClientIP());
return success(new PayTransferCreateRespVO().setId(payTransfer.getId()).setStatus(payTransfer.getStatus()));
}
@Resource
private PayAppService payAppService;
@GetMapping("/get")
@Operation(summary = "获得转账订单")
@PreAuthorize("@ss.hasPermission('pay:transfer:query')")
public CommonResult<PayTransferRespVO> getTransfer(@RequestParam("id") Long id) {
return success(PayTransferConvert.INSTANCE.convert(payTransferService.getTransfer(id)));
PayTransferDO transfer = payTransferService.getTransfer(id);
if (transfer == null) {
return success(new PayTransferRespVO());
}
// 拼接数据
PayAppDO app = payAppService.getApp(transfer.getAppId());
return success(BeanUtils.toBean(transfer, PayTransferRespVO.class, transferVO -> {
if (app != null) {
transferVO.setAppName(app.getName());
}
}));
}
@GetMapping("/page")
@Operation(summary = "获得转账订单分页")
@PreAuthorize("@ss.hasPermission('pay:transfer:query')")
public CommonResult<PageResult<PayTransferPageItemRespVO>> getTransferPage(@Valid PayTransferPageReqVO pageVO) {
public CommonResult<PageResult<PayTransferRespVO>> getTransferPage(@Valid PayTransferPageReqVO pageVO) {
PageResult<PayTransferDO> pageResult = payTransferService.getTransferPage(pageVO);
return success(PayTransferConvert.INSTANCE.convertPage(pageResult));
// 拼接数据
Map<Long, PayAppDO> apps = payAppService.getAppMap(convertList(pageResult.getList(), PayTransferDO::getAppId));
return success(BeanUtils.toBean(pageResult, PayTransferRespVO.class, transferVO -> {
if (apps.containsKey(transferVO.getAppId())) {
transferVO.setAppName(apps.get(transferVO.getAppId()).getName());
}
}));
}
@GetMapping("/export-excel")
@Operation(summary = "导出转账订单 Excel")
@PreAuthorize("@ss.hasPermission('pay:transfer:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportTransfer(PayTransferPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PAGE_SIZE_NONE);
PageResult<PayTransferRespVO> pageResult = getTransferPage(pageReqVO).getData();
// 导出 Excel
ExcelUtils.write(response, "转账订单.xls", "数据", PayTransferRespVO.class, pageResult.getList());
}
}

View File

@ -1,95 +0,0 @@
package cn.iocoder.yudao.module.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<String, String> 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;
}
}

View File

@ -1,62 +0,0 @@
package cn.iocoder.yudao.module.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;
}

View File

@ -1,12 +1,9 @@
package cn.iocoder.yudao.module.pay.controller.admin.transfer.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
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;
@ -27,10 +24,7 @@ public class PayTransferPageReqVO extends PageParam {
private String channelCode;
@Schema(description = "商户转账单编号", example = "17481")
private String merchantTransferId;
@Schema(description = "类型", example = "2")
private Integer type;
private String merchantOrderId;
@Schema(description = "转账状态", example = "2")
private Integer status;
@ -38,6 +32,9 @@ public class PayTransferPageReqVO extends PageParam {
@Schema(description = "收款人姓名", example = "王五")
private String userName;
@Schema(description = "收款人账号", example = "26589")
private String userAccount;
@Schema(description = "渠道转账单号")
private String channelTransferNo;

View File

@ -1,60 +1,78 @@
package cn.iocoder.yudao.module.pay.controller.admin.transfer.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
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.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.time.LocalDateTime;
import java.util.Map;
@Schema(description = "管理后台 - 转账单 Response VO")
@Data
@ExcelIgnoreUnannotated
public class PayTransferRespVO {
@ExcelProperty("转账单编号")
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2931")
private Long id;
@ExcelProperty("转账单号")
@Schema(description = "转账单号", requiredMode = Schema.RequiredMode.REQUIRED)
private String no;
@Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12831")
private Long appId;
@ExcelProperty("应用名称")
@Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String appName;
@Schema(description = "转账渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24833")
private Long channelId;
@ExcelProperty(value = "转账渠道", converter = DictConvert.class)
@DictFormat(DictTypeConstants.CHANNEL_CODE)
@Schema(description = "转账渠道编码", requiredMode = Schema.RequiredMode.REQUIRED)
private String channelCode;
@ExcelProperty("商户转账单编号")
@Schema(description = "商户转账单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17481")
private String merchantTransferId;
@Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer type;
@ExcelProperty(value = "转账状态", converter = DictConvert.class)
@DictFormat(DictTypeConstants.TRANSFER_STATUS)
@Schema(description = "转账状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer status;
@ExcelProperty("转账成功时间")
@Schema(description = "转账成功时间")
private LocalDateTime successTime;
@Schema(description = "转账金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "964")
@ExcelProperty(value = "转账金额", converter = MoneyConvert.class)
private Integer price;
@Schema(description = "转账标题", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("转账标题")
@Schema(description = "转账标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "冲冲冲!")
private String subject;
@Schema(description = "收款人姓名", example = "王五")
@ExcelProperty("收款人姓名")
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 = "26589")
@ExcelProperty("收款人账号")
private String userAccount;
@Schema(description = "异步通知商户地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn")
private String notifyUrl;
@ExcelProperty("用户 IP")
@Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED)
private String userIp;
@ -62,11 +80,13 @@ public class PayTransferRespVO {
private Map<String, String> channelExtras;
@Schema(description = "渠道转账单号")
@ExcelProperty("渠道转账单号")
private String channelTransferNo;
@Schema(description = "调用渠道的错误码")
private String channelErrorCode;
@ExcelProperty("渠道错误提示")
@Schema(description = "调用渠道的错误提示")
private String channelErrorMsg;
@ -74,6 +94,7 @@ public class PayTransferRespVO {
private String channelNotifyData;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}

View File

@ -7,13 +7,14 @@ import cn.iocoder.yudao.module.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 jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
@ -36,8 +37,7 @@ public class PayWalletRechargeController {
return success(true);
}
// TODO @jason发起退款 post 操作哈
@GetMapping("/refund")
@PostMapping("/refund")
@Operation(summary = "发起钱包充值退款")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<Boolean> refundWalletRecharge(@RequestParam("id") Long id) {
@ -50,7 +50,9 @@ public class PayWalletRechargeController {
@PermitAll // 无需登录 内部校验实现
public CommonResult<Boolean> updateWalletRechargeRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
walletRechargeService.updateWalletRechargeRefunded(
Long.valueOf(notifyReqDTO.getMerchantOrderId()), notifyReqDTO.getPayRefundId());
Long.valueOf(notifyReqDTO.getMerchantOrderId()),
Long.valueOf(notifyReqDTO.getMerchantRefundId()),
notifyReqDTO.getPayRefundId());
return success(true);
}

View File

@ -9,9 +9,11 @@ import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitReqV
import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.WalletPayClient;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -22,7 +24,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -39,6 +40,8 @@ public class AppPayOrderController {
@Resource
private PayOrderService payOrderService;
@Resource
private PayWalletService payWalletService;
@GetMapping("/get")
@Operation(summary = "获得支付订单")
@ -63,11 +66,11 @@ public class AppPayOrderController {
public CommonResult<AppPayOrderSubmitRespVO> submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) {
// 1. 钱包支付事需要额外传 user_id user_type
if (Objects.equals(reqVO.getChannelCode(), PayChannelEnum.WALLET.getCode())) {
Map<String, String> 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);
if (reqVO.getChannelExtras() == null) {
reqVO.setChannelExtras(Maps.newHashMapWithExpectedSize(1));
}
PayWalletDO wallet = payWalletService.getOrCreateWallet(getLoginUserId(), getLoginUserType());
reqVO.getChannelExtras().put(WalletPayClient.WALLET_ID_KEY, String.valueOf(wallet.getId()));
}
// 2. 提交支付

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.pay.controller.app.transfer;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.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 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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "用户 APP - 转账单")
@RestController
@RequestMapping("/pay/transfer")
@Validated
@Slf4j
public class AppPayTransferController {
@Resource
private PayTransferService transferService;
@GetMapping("/sync")
@Operation(summary = "同步转账单") // 目的解决微信转账的异步回调可能有延迟的问题
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<Boolean> syncTransfer(@RequestParam("id") Long id) {
transferService.syncTransfer(id);
return success(true);
}
}

View File

@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.pay.convert.demo;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* 示例订单 Convert
*
* @author 芋道源码
*/
@Mapper
public interface PayDemoOrderConvert {
PayDemoOrderConvert INSTANCE = Mappers.getMapper(PayDemoOrderConvert.class);
PayDemoOrderDO convert(PayDemoOrderCreateReqVO bean);
PayDemoOrderRespVO convert(PayDemoOrderDO bean);
PageResult<PayDemoOrderRespVO> convertPage(PageResult<PayDemoOrderDO> page);
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.pay.convert.demo;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferRespVO;
import cn.iocoder.yudao.module.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<PayDemoTransferRespVO> convertPage(PageResult<PayDemoTransferDO> pageResult);
}

View File

@ -1,43 +0,0 @@
package cn.iocoder.yudao.module.pay.convert.notify;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.Map;
/**
* 支付通知 Convert
*
* @author 芋道源码
*/
@Mapper
public interface PayNotifyTaskConvert {
PayNotifyTaskConvert INSTANCE = Mappers.getMapper(PayNotifyTaskConvert.class);
PayNotifyTaskRespVO convert(PayNotifyTaskDO bean);
default PageResult<PayNotifyTaskRespVO> convertPage(PageResult<PayNotifyTaskDO> page, Map<Long, PayAppDO> appMap){
PageResult<PayNotifyTaskRespVO> result = convertPage(page);
result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName())));
return result;
}
PageResult<PayNotifyTaskRespVO> convertPage(PageResult<PayNotifyTaskDO> page);
default PayNotifyTaskDetailRespVO convert(PayNotifyTaskDO task, PayAppDO app, List<PayNotifyLogDO> logs) {
PayNotifyTaskDetailRespVO respVO = convert(task, logs);
if (app != null) {
respVO.setAppName(app.getName());
}
return respVO;
}
PayNotifyTaskDetailRespVO convert(PayNotifyTaskDO task, List<PayNotifyLogDO> logs);
}

View File

@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundDetailsRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExcelVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageItemRespVO;
@ -42,8 +41,6 @@ public interface PayRefundConvert {
PayRefundDO convert(PayRefundCreateReqDTO bean);
PayRefundRespDTO convert02(PayRefundDO bean);
default List<PayRefundExcelVO> convertList(List<PayRefundDO> list, Map<Long, PayAppDO> appMap) {
return CollectionUtils.convertList(list, order -> {
PayRefundExcelVO excelVO = convertExcel(order);

View File

@ -1,31 +0,0 @@
package cn.iocoder.yudao.module.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 cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageItemRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferRespVO;
import cn.iocoder.yudao.module.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<PayTransferPageItemRespVO> convertPage(PageResult<PayTransferDO> pageResult);
}

View File

@ -1,84 +0,0 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.demo;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import cn.iocoder.yudao.module.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") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
public class PayDemoTransferDO extends BaseDO {
/**
* 订单编号
*/
@TableId
private Long id;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 转账类型
* <p>
* 枚举 {@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;
}

View File

@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.demo;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.enums.demo.PayDemoWithdrawStatusEnum;
import cn.iocoder.yudao.module.pay.enums.demo.PayDemoWithdrawTypeEnum;
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_demo_withdraw", autoResultMap = true)
@KeySequence("pay_demo_withdraw_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
public class PayDemoWithdrawDO extends BaseDO {
/**
* 提现单编号自增
*/
@TableId
private Long id;
/**
* 提现标题
*/
private String subject;
/**
* 提现金额单位
*/
private Integer price;
/**
* 收款人账号
*/
private String userAccount;
/**
* 收款人姓名
*/
private String userName;
/**
* 提现方式
*
* 枚举 {@link PayDemoWithdrawTypeEnum}
*/
private Integer type;
/**
* 提现状态
*
* 枚举 {@link PayDemoWithdrawStatusEnum}
*/
private Integer status;
// ========== 转账相关字段 ==========
/**
* 转账单编号
*
* 关联 {@link PayTransferDO#getId()}
*/
private Long payTransferId;
/**
* 转账渠道
*
* 枚举 {@link cn.iocoder.yudao.module.pay.enums.PayChannelEnum}
*/
private String transferChannelCode;
/**
* 转账成功时间
*/
private LocalDateTime transferTime;
/**
* 转账错误提示
*/
private String transferErrorMsg;
}

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
@ -60,6 +61,7 @@ public class PayNotifyTaskDO extends TenantBaseDO {
*
* 1. {@link PayNotifyTypeEnum#ORDER} 关联 {@link PayOrderDO#getId()}
* 2. {@link PayNotifyTypeEnum#REFUND} 关联 {@link PayRefundDO#getId()}
* 3. {@link PayNotifyTypeEnum#TRANSFER} 关联 {@link PayTransferDO#getId()}
*/
private Long dataId;
/**
@ -67,7 +69,11 @@ public class PayNotifyTaskDO extends TenantBaseDO {
*/
private String merchantOrderId;
/**
* 商户转账单编号
* 商户退款编号
*/
private String merchantRefundId;
/**
* 商户转账编号
*/
private String merchantTransferId;
/**

View File

@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.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 cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
@ -16,7 +15,6 @@ import lombok.Data;
import java.time.LocalDateTime;
import java.util.Map;
// TODO 芋艿需要详细 review
/**
* 转账单 DO
*
@ -35,7 +33,6 @@ public class PayTransferDO extends BaseDO {
/**
* 转账单号
*
*/
private String no;
@ -70,13 +67,6 @@ public class PayTransferDO extends BaseDO {
// ========== 转账相关字段 ==========
/**
* 类型
*
* 枚举 {@link PayTransferTypeEnum}
*/
private Integer type;
/**
* 转账标题
*/
@ -87,6 +77,10 @@ public class PayTransferDO extends BaseDO {
*/
private Integer price;
/**
* 收款人账号
*/
private String userAccount;
/**
* 收款人姓名
*/
@ -104,19 +98,6 @@ public class PayTransferDO extends BaseDO {
*/
private LocalDateTime successTime;
// ========== 支付宝转账相关字段 ==========
/**
* 支付宝登录号
*/
private String alipayLogonId;
// ========== 微信转账相关字段 ==========
/**
* 微信 openId
*/
private String openid;
// ========== 其它字段 ==========
/**
@ -151,8 +132,15 @@ public class PayTransferDO extends BaseDO {
/**
* 渠道的同步/异步通知的内容
*
*/
private String channelNotifyData;
/**
* 渠道 package 信息
*
* 特殊目前只有微信转账有这个东西
* @see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012716430">JSAPI 调起用户确认收款</a>
*/
private String channelPackageInfo;
}

View File

@ -1,17 +0,0 @@
package cn.iocoder.yudao.module.pay.dal.mysql.demo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoTransferDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayDemoTransferMapper extends BaseMapperX<PayDemoTransferDO> {
default PageResult<PayDemoTransferDO> selectPage(PageParam pageParam){
return selectPage(pageParam, new LambdaQueryWrapperX<PayDemoTransferDO>()
.orderByDesc(PayDemoTransferDO::getId));
}
}

View File

@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.pay.dal.mysql.demo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoWithdrawDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayDemoWithdrawMapper extends BaseMapperX<PayDemoWithdrawDO> {
default PageResult<PayDemoWithdrawDO> selectPage(PageParam pageParam){
return selectPage(pageParam, new LambdaQueryWrapperX<PayDemoWithdrawDO>()
.orderByDesc(PayDemoWithdrawDO::getId));
}
default int updateByIdAndStatus(Long id, Integer whereStatus, PayDemoWithdrawDO updateObj) {
return update(updateObj, new LambdaQueryWrapperX<PayDemoWithdrawDO>()
.eq(PayDemoWithdrawDO::getId, id)
.eq(PayDemoWithdrawDO::getStatus, whereStatus));
}
}

View File

@ -37,6 +37,8 @@ public interface PayNotifyTaskMapper extends BaseMapperX<PayNotifyTaskDO> {
.eqIfPresent(PayNotifyTaskDO::getDataId, reqVO.getDataId())
.eqIfPresent(PayNotifyTaskDO::getStatus, reqVO.getStatus())
.eqIfPresent(PayNotifyTaskDO::getMerchantOrderId, reqVO.getMerchantOrderId())
.eqIfPresent(PayNotifyTaskDO::getMerchantRefundId, reqVO.getMerchantRefundId())
.eqIfPresent(PayNotifyTaskDO::getMerchantTransferId, reqVO.getMerchantTransferId())
.betweenIfPresent(PayNotifyTaskDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayNotifyTaskDO::getId));
}

View File

@ -8,19 +8,27 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
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 PayTransferMapper extends BaseMapperX<PayTransferDO> {
default int updateByIdAndStatus(Long id, List<Integer> status, PayTransferDO updateObj) {
default int updateByIdAndStatus(Long id, List<Integer> whereStatuses, PayTransferDO updateObj) {
return update(updateObj, new LambdaQueryWrapper<PayTransferDO>()
.eq(PayTransferDO::getId, id).in(PayTransferDO::getStatus, status));
.eq(PayTransferDO::getId, id)
.in(PayTransferDO::getStatus, whereStatuses));
}
default PayTransferDO selectByAppIdAndMerchantTransferId(Long appId, String merchantTransferId){
default int updateByIdAndStatus(Long id, Integer whereStatus, PayTransferDO updateObj) {
return update(updateObj, new LambdaQueryWrapper<PayTransferDO>()
.eq(PayTransferDO::getId, id)
.eq(PayTransferDO::getStatus, whereStatus));
}
default PayTransferDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) {
return selectOne(PayTransferDO::getAppId, appId,
PayTransferDO::getMerchantTransferId, merchantTransferId);
PayTransferDO::getMerchantTransferId, merchantOrderId);
}
default PageResult<PayTransferDO> selectPage(PayTransferPageReqVO reqVO) {
@ -28,17 +36,17 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
.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::getMerchantTransferId, reqVO.getMerchantOrderId())
.eqIfPresent(PayTransferDO::getStatus, reqVO.getStatus())
.likeIfPresent(PayTransferDO::getUserName, reqVO.getUserName())
.likeIfPresent(PayTransferDO::getUserAccount, reqVO.getUserAccount())
.eqIfPresent(PayTransferDO::getChannelTransferNo, reqVO.getChannelTransferNo())
.betweenIfPresent(PayTransferDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayTransferDO::getId));
}
default List<PayTransferDO> selectListByStatus(Integer status) {
return selectList(PayTransferDO::getStatus, status);
default List<PayTransferDO> selectListByStatus(Collection<Integer> statuses) {
return selectList(PayTransferDO::getStatus, statuses);
}
default PayTransferDO selectByAppIdAndNo(Long appId, String no) {
@ -46,6 +54,10 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
PayTransferDO::getNo, no);
}
default PayTransferDO selectByNo(String no) {
return selectOne(PayTransferDO::getNo, no);
}
}

View File

@ -14,14 +14,16 @@ 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 cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletTransactionService;
import lombok.extern.slf4j.Slf4j;
@ -40,13 +42,14 @@ import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.REFUND_NOT_FO
@Slf4j
public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
public static final String USER_ID_KEY = "user_id";
public static final String USER_TYPE_KEY = "user_type";
public static final String WALLET_ID_KEY = "walletId";
private PayWalletService wallService;
private PayWalletTransactionService walletTransactionService;
private PayOrderService orderService;
private PayRefundService refundService;
private PayTransferService transferService;
public WalletPayClient(Long channelId, NonePayClientConfig config) {
super(channelId, PayChannelEnum.WALLET.getCode(), config);
@ -63,19 +66,18 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
}
@Override
@SuppressWarnings("PatternVariableCanBeUsed")
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());
Long walletId = MapUtil.getLong(reqDTO.getChannelExtras(), WALLET_ID_KEY);
Assert.notNull(walletId, "钱包编号");
PayWalletTransactionDO transaction = wallService.orderPay(walletId,
reqDTO.getOutTradeNo(), reqDTO.getPrice());
return PayOrderRespDTO.successOf(transaction.getNo(), transaction.getCreator(),
transaction.getCreateTime(),
reqDTO.getOutTradeNo(), transaction);
} catch (Throwable ex) {
log.error("[doUnifiedOrder] 失败", ex);
log.error("[doUnifiedOrder][reqDTO({}) 异常]", reqDTO, ex);
Integer errorCode = INTERNAL_SERVER_ERROR.getCode();
String errorMsg = INTERNAL_SERVER_ERROR.getMsg();
if (ex instanceof ServiceException) {
@ -123,6 +125,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
}
@Override
@SuppressWarnings("PatternVariableCanBeUsed")
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
try {
PayWalletTransactionDO payWalletTransaction = wallService.orderRefund(reqDTO.getOutRefundNo(),
@ -130,7 +133,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
return PayRefundRespDTO.successOf(payWalletTransaction.getNo(), payWalletTransaction.getCreateTime(),
reqDTO.getOutRefundNo(), payWalletTransaction);
} catch (Throwable ex) {
log.error("[doUnifiedRefund] 失败", ex);
log.error("[doUnifiedRefund][reqDOT({}) 异常]", reqDTO, ex);
Integer errorCode = INTERNAL_SERVER_ERROR.getCode();
String errorMsg = INTERNAL_SERVER_ERROR.getMsg();
if (ex instanceof ServiceException) {
@ -178,18 +181,71 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
}
@Override
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("未实现");
}
@Override
@SuppressWarnings("PatternVariableCanBeUsed")
public PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
throw new UnsupportedOperationException("待实现");
try {
Long walletId = Long.parseLong(reqDTO.getUserAccount());
PayWalletTransactionDO transaction = wallService.addWalletBalance(walletId, String.valueOf(reqDTO.getOutTransferNo()),
PayWalletBizTypeEnum.TRANSFER, reqDTO.getPrice());
return PayTransferRespDTO.successOf(transaction.getNo(), transaction.getCreateTime(),
reqDTO.getOutTransferNo(), transaction);
} catch (Throwable ex) {
log.error("[doUnifiedTransfer][reqDTO({}) 异常]", reqDTO, 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 PayTransferRespDTO.closedOf(String.valueOf(errorCode), errorMsg,
reqDTO.getOutTransferNo(), "");
}
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
throw new UnsupportedOperationException("待实现");
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("钱包支付无转账回调");
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo) {
if (transferService == null) {
transferService = SpringUtil.getBean(PayTransferService.class);
}
// 获取转账单
PayTransferDO transfer = transferService.getTransferByNo(outTradeNo);
// 转账单不存在返回关闭状态
if (transfer == null) {
return PayTransferRespDTO.closedOf(String.valueOf(PAY_ORDER_EXTENSION_NOT_FOUND.getCode()),
PAY_ORDER_EXTENSION_NOT_FOUND.getMsg(), outTradeNo, "");
}
// 关闭状态
if (PayTransferStatusRespEnum.isClosed(transfer.getStatus())) {
return PayTransferRespDTO.closedOf(transfer.getChannelErrorCode(),
transfer.getChannelErrorMsg(), outTradeNo, "");
}
// 成功状态
if (PayTransferStatusRespEnum.isSuccess(transfer.getStatus())) {
PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction(
String.valueOf(transfer.getId()), PayWalletBizTypeEnum.TRANSFER);
Assert.notNull(walletTransaction, "转账单 {} 钱包流水不能为空", outTradeNo);
return PayTransferRespDTO.successOf(walletTransaction.getNo(), walletTransaction.getCreateTime(),
outTradeNo, walletTransaction);
}
// 处理中状态
if (PayTransferStatusRespEnum.isProcessing(transfer.getStatus())) {
return PayTransferRespDTO.processingOf(transfer.getChannelTransferNo(),
outTradeNo, transfer);
}
// 等待状态
if (transfer.getStatus().equals(PayTransferStatusRespEnum.WAITING.getStatus())) {
return PayTransferRespDTO.waitingOf(transfer.getChannelTransferNo(),
outTradeNo, transfer);
}
// 其它状态为无效状态
log.error("[doGetTransfer] 转账单 {} 的状态不正确", outTradeNo);
throw new IllegalStateException(String.format("转账单[%s] 状态不正确", outTradeNo));
}
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.pay.service.app;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
@ -17,6 +18,7 @@ import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -116,6 +118,9 @@ public class PayAppServiceImpl implements PayAppService {
@Override
public List<PayAppDO> getAppList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return Collections.emptyList();
}
return appMapper.selectBatchIds(ids);
}

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import jakarta.validation.Valid;
/**
@ -58,8 +59,9 @@ public interface PayDemoOrderService {
* 更新示例订单为已退款
*
* @param id 编号
* @param refundId 退款编号
* @param payRefundId 退款订单号
*/
void updateDemoOrderRefunded(Long id, Long payRefundId);
void updateDemoOrderRefunded(Long id, String refundId, Long payRefundId);
}

View File

@ -25,7 +25,6 @@ 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;
@ -97,7 +96,6 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
// 2.2 更新支付单到 demo 订单
payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(demoOrder.getId())
.setPayOrderId(payOrderId));
// 返回
return demoOrder.getId();
}
@ -121,14 +119,14 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
}
// 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));
log.error("[updateDemoOrderPaid][order({}) 支付单不匹配({}),请进行处理!]",
order, payOrderId);
throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR);
}
@ -186,7 +184,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
// 2.1 生成退款单号
// 一般来说用户发起退款的时候都会单独插入一个售后维权表然后使用该表的 id 作为 refundId
// 这里我们是个简单的 demo所以没有售后维权表直接使用订单 id + "-refund" 来演示
// 这里我们是个简单的 demo所以没有售后维权表直接使用订单 id + "-refund" 来演示
String refundId = order.getId() + "-refund";
// 2.2 创建退款单
Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
@ -217,16 +215,18 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
}
@Override
public void updateDemoOrderRefunded(Long id, Long payRefundId) {
public void updateDemoOrderRefunded(Long id, String refundId, Long payRefundId) {
// 1. 校验并获得退款订单可退款
PayRefundRespDTO payRefund = validateDemoOrderCanRefunded(id, payRefundId);
PayRefundRespDTO payRefund = validateDemoOrderCanRefunded(id, refundId, payRefundId);
// 2.2 更新退款单到 demo 订单
payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id)
.setRefundTime(payRefund.getSuccessTime()));
}
private PayRefundRespDTO validateDemoOrderCanRefunded(Long id, Long payRefundId) {
private PayRefundRespDTO validateDemoOrderCanRefunded(Long id, String refundId, Long payRefundId) {
// 1.1 校验示例订单
// 一般来说这里应该用 refundId 来查询退款单然后再校验订单是否匹配
// 这里我们是个简单的 demo所以没有售后维权表直接使用订单 id 来查询订单
PayDemoOrderDO order = payDemoOrderMapper.selectById(id);
if (order == null) {
throw exception(DEMO_ORDER_NOT_FOUND);
@ -243,7 +243,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
if (payRefund == null) {
throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND);
}
// 2.2
// 2.2 必须是退款成功状态
if (!PayRefundStatusEnum.isSuccess(payRefund.getStatus())) {
throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS);
}
@ -254,7 +254,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH);
}
// 2.4 校验退款订单匹配二次
if (notEqual(payRefund.getMerchantOrderId(), id.toString())) {
if (notEqual(payRefund.getMerchantRefundId(), id.toString() + "-refund")) {
log.error("[validateDemoOrderCanRefunded][order({}) 退款单不匹配({})请进行处理payRefund 数据是:{}]",
id, payRefundId, toJsonString(payRefund));
throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR);

View File

@ -1,38 +0,0 @@
package cn.iocoder.yudao.module.pay.service.demo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
import cn.iocoder.yudao.module.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<PayDemoTransferDO> getDemoTransferPage(PageParam pageVO);
/**
* 更新转账业务示例订单的转账状态
*
* @param id 编号
* @param payTransferId 转账单编号
*/
void updateDemoTransferStatus(Long id, Long payTransferId);
}

View File

@ -1,26 +1,31 @@
package cn.iocoder.yudao.module.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.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoTransferDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoTransferMapper;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
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.PayTransferCreateRespDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.withdraw.PayDemoWithdrawCreateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoWithdrawDO;
import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoWithdrawMapper;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.enums.demo.PayDemoWithdrawStatusEnum;
import cn.iocoder.yudao.module.pay.enums.demo.PayDemoWithdrawTypeEnum;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.Validator;
import lombok.extern.slf4j.Slf4j;
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.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.WAITING;
/**
* 示例转账业务 Service 实现类
@ -29,71 +34,162 @@ import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.W
*/
@Service
@Validated
public class PayDemoTransferServiceImpl implements PayDemoTransferService {
@Slf4j
public class PayDemoTransferServiceImpl implements PayDemoWithdrawService {
/**
* 接入的实力应用编号
* 接入的支付应用标识
*
* [支付管理 -> 应用信息] 里添加
*/
private static final Long TRANSFER_APP_ID = 8L;
private static final String PAY_APP_KEY = "demo";
@Resource
private PayDemoTransferMapper demoTransferMapper;
private PayDemoWithdrawMapper demoTransferMapper;
@Resource
private PayTransferService payTransferService;
@Resource
private Validator validator;
private PayTransferApi payTransferApi;
@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();
public Long createDemoWithdraw(@Valid PayDemoWithdrawCreateReqVO reqVO) {
PayDemoWithdrawDO withdraw = BeanUtils.toBean(reqVO, PayDemoWithdrawDO.class)
.setTransferChannelCode(getTransferChannelCode(reqVO.getType()))
.setStatus(PayDemoWithdrawStatusEnum.WAITING.getStatus());
demoTransferMapper.insert(withdraw);
return withdraw.getId();
}
@Override
public PageResult<PayDemoTransferDO> getDemoTransferPage(PageParam pageVO) {
public Long transferDemoWithdraw(Long id) {
// 1.1 校验提现单
PayDemoWithdrawDO withdraw = validateDemoWithdrawCanTransfer(id);
// 1.2 特殊如果是转账失败的情况需要充值下
if (PayDemoWithdrawStatusEnum.isClosed(withdraw.getStatus())) {
int updateCount = demoTransferMapper.updateByIdAndStatus(withdraw.getId(), withdraw.getStatus(),
new PayDemoWithdrawDO().setStatus(PayDemoWithdrawStatusEnum.WAITING.getStatus()).setTransferErrorMsg(""));
if (updateCount == 0) {
throw exception(DEMO_WITHDRAW_TRANSFER_FAIL_STATUS_NOT_WAITING_OR_CLOSED);
}
withdraw.setStatus(PayDemoWithdrawStatusEnum.WAITING.getStatus());
}
// 2.1 创建支付单
PayTransferCreateReqDTO transferReqDTO = new PayTransferCreateReqDTO()
.setAppKey(PAY_APP_KEY).setChannelCode(withdraw.getTransferChannelCode()).setUserIp(getClientIP()) // 支付应用
.setMerchantTransferId(String.valueOf(withdraw.getId())) // 业务的订单编号
.setSubject(withdraw.getSubject()).setPrice(withdraw.getPrice()) // 价格信息
.setUserAccount(withdraw.getUserAccount()).setUserName(withdraw.getUserName()); // 收款信息
if (ObjectUtil.equal(withdraw.getType(), PayDemoWithdrawTypeEnum.WECHAT.getType())) {
// 注意微信很特殊提现需要写明用途
transferReqDTO.setChannelExtras(PayTransferCreateReqDTO.buildWeiXinChannelExtra1000(
"测试活动", "测试奖励"));
}
PayTransferCreateRespDTO transferRespDTO = payTransferApi.createTransfer(transferReqDTO).getCheckedData();
// 2.2 更新转账单到 demo 示例提现单并将状态更新为转账中
demoTransferMapper.updateByIdAndStatus(withdraw.getId(), withdraw.getStatus(),
new PayDemoWithdrawDO().setPayTransferId(transferRespDTO.getId()));
return transferRespDTO.getId();
}
private PayDemoWithdrawDO validateDemoWithdrawCanTransfer(Long id) {
// 校验存在
PayDemoWithdrawDO withdraw = demoTransferMapper.selectById(id);
if (withdraw == null) {
throw exception(DEMO_WITHDRAW_NOT_FOUND);
}
// 校验状态只有等待中或转账失败的订单才能发起转账
if (!PayDemoWithdrawStatusEnum.isWaiting(withdraw.getStatus())
&& !PayDemoWithdrawStatusEnum.isClosed(withdraw.getStatus())) {
throw exception(DEMO_WITHDRAW_TRANSFER_FAIL_STATUS_NOT_WAITING_OR_CLOSED);
}
return withdraw;
}
private String getTransferChannelCode(Integer type) {
if (ObjectUtil.equal(type, PayDemoWithdrawTypeEnum.ALIPAY.getType())) {
return PayChannelEnum.ALIPAY_PC.getCode();
}
if (ObjectUtil.equal(type, PayDemoWithdrawTypeEnum.WECHAT.getType())) {
return PayChannelEnum.WX_LITE.getCode();
}
if (ObjectUtil.equal(type, PayDemoWithdrawTypeEnum.WALLET.getType())) {
return PayChannelEnum.WALLET.getCode();
}
throw new IllegalArgumentException("未知提现方式:" + type);
}
@Override
public PageResult<PayDemoWithdrawDO> getDemoWithdrawPage(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()));
public void updateDemoWithdrawTransferred(Long id, Long payTransferId) {
// 1.1 校验转账单是否存在
PayDemoWithdrawDO withdraw = demoTransferMapper.selectById(id);
if (withdraw == null) {
log.error("[updateDemoWithdrawStatus][withdraw({}) payOrder({}) 不存在提现单,请进行处理!]", id, payTransferId);
throw exception(DEMO_WITHDRAW_NOT_FOUND);
}
// 1.2 校验转账单已成结束成功或失败
if (PayDemoWithdrawStatusEnum.isSuccess(withdraw.getStatus())
|| PayDemoWithdrawStatusEnum.isClosed(withdraw.getStatus())) {
// 特殊转账单编号相同直接返回说明重复回调
if (ObjectUtil.equal(withdraw.getPayTransferId(), payTransferId)) {
log.warn("[updateDemoWithdrawStatus][withdraw({}) 已结束,且转账单编号相同({}),直接返回]", withdraw, payTransferId);
return;
}
// 异常转账单编号不同说明转账单编号错误
log.error("[updateDemoWithdrawStatus][withdraw({}) 转账单不匹配({}),请进行处理!]", withdraw, payTransferId);
throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_ID_ERROR);
}
// 2. 校验提现单的合法性
PayTransferRespDTO payTransfer = validateDemoTransferStatusCanUpdate(withdraw, payTransferId);
// 3. 更新示例订单状态
Integer newStatus = PayTransferStatusEnum.isSuccess(payTransfer.getStatus()) ? PayDemoWithdrawStatusEnum.SUCCESS.getStatus() :
PayTransferStatusEnum.isClosed(payTransfer.getStatus()) ? PayDemoWithdrawStatusEnum.CLOSED.getStatus() : null;
Assert.notNull(newStatus, "转账单状态({}) 不合法", payTransfer.getStatus());
demoTransferMapper.updateByIdAndStatus(withdraw.getId(), withdraw.getStatus(),
new PayDemoWithdrawDO().setStatus(newStatus).setTransferTime(payTransfer.getSuccessTime())
.setTransferErrorMsg(payTransfer.getChannelErrorMsg()));
}
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) {
private PayTransferRespDTO validateDemoTransferStatusCanUpdate(PayDemoWithdrawDO withdraw, Long payTransferId) {
// 1. 校验转账单是否存在
PayTransferRespDTO payTransfer = payTransferApi.getTransfer(payTransferId).getCheckedData();
if (payTransfer == null) {
log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) payTransfer({}) 不存在,请进行处理!]", withdraw.getId(), payTransferId);
throw exception(PAY_TRANSFER_NOT_FOUND);
}
if (!Objects.equals(demoTransfer.getPrice(), transfer.getPrice())) {
throw exception(DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH);
// 2.1 校验转账单已成功
if (!PayTransferStatusEnum.isSuccessOrClosed(payTransfer.getStatus())) {
log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) payTransfer({}) 未结束请进行处理payTransfer 数据是:{}]",
withdraw.getId(), payTransferId, JsonUtils.toJsonString(payTransfer));
throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_TRANSFER_STATUS_NOT_SUCCESS_OR_CLOSED);
}
if (ObjectUtil.notEqual(transfer.getMerchantTransferId(), id.toString())) {
throw exception(DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR);
// 2.2 校验转账金额一致
if (ObjectUtil.notEqual(payTransfer.getPrice(), withdraw.getPrice())) {
log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) payTransfer({}) 转账金额不匹配请进行处理withdraw 数据是:{}payTransfer 数据是:{}]",
withdraw.getId(), payTransferId, JsonUtils.toJsonString(withdraw), JsonUtils.toJsonString(payTransfer));
throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_PRICE_NOT_MATCH);
}
// TODO 校验账号
return transfer;
// 2.3 校验转账订单匹配二次
if (ObjectUtil.notEqual(payTransfer.getMerchantTransferId(), withdraw.getId().toString())) {
log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) 转账单不匹配({})请进行处理payTransfer 数据是:{}]",
withdraw.getId(), payTransferId, JsonUtils.toJsonString(payTransfer));
throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_MERCHANT_EXISTS);
}
// 2.4 校验转账渠道一致
if (ObjectUtil.notEqual(payTransfer.getChannelCode(), withdraw.getTransferChannelCode())) {
log.error("[validateDemoTransferStatusCanUpdate][withdraw({}) payTransfer({}) 转账渠道不匹配请进行处理withdraw 数据是:{}payTransfer 数据是:{}]",
withdraw.getId(), payTransferId, JsonUtils.toJsonString(withdraw), JsonUtils.toJsonString(payTransfer));
throw exception(DEMO_WITHDRAW_UPDATE_STATUS_FAIL_PAY_CHANNEL_NOT_MATCH);
}
return payTransfer;
}
}

View File

@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.pay.service.demo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.withdraw.PayDemoWithdrawCreateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoWithdrawDO;
import jakarta.validation.Valid;
/**
* 示例提现单 Service 接口
*
* @author jason
*/
public interface PayDemoWithdrawService {
/**
* 创建示例提现单
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createDemoWithdraw(@Valid PayDemoWithdrawCreateReqVO createReqVO);
/**
* 提现单转账
*
* @param id 提现单编号
* @return 转账编号
*/
Long transferDemoWithdraw(Long id);
/**
* 获得示例提现单分页
*
* @param pageVO 分页查询参数
*/
PageResult<PayDemoWithdrawDO> getDemoWithdrawPage(PageParam pageVO);
/**
* 更新示例提现单的状态
*
* @param id 编号
* @param payTransferId 转账单编号
*/
void updateDemoWithdrawTransferred(Long id, Long payTransferId);
}

View File

@ -29,16 +29,17 @@ import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import cn.iocoder.yudao.module.pay.service.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.annotation.Async;
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 jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
@ -97,19 +98,19 @@ public class PayNotifyServiceImpl implements PayNotifyService {
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 字段
// 补充 appId + notifyUrl + merchant* 字段
if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
PayOrderDO order = orderService.getOrder(task.getDataId()); // 不进行非空判断有问题直接异常
task.setAppId(order.getAppId()).
setMerchantOrderId(order.getMerchantOrderId()).setNotifyUrl(order.getNotifyUrl());
task.setAppId(order.getAppId()).setNotifyUrl(order.getNotifyUrl())
.setMerchantOrderId(order.getMerchantOrderId());
} 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());
PayRefundDO refund = refundService.getRefund(task.getDataId());
task.setAppId(refund.getAppId()).setNotifyUrl(refund.getNotifyUrl())
.setMerchantOrderId(refund.getMerchantOrderId()).setMerchantRefundId(refund.getMerchantRefundId());
} 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());
task.setAppId(transfer.getAppId()).setNotifyUrl(transfer.getNotifyUrl())
.setMerchantTransferId(transfer.getMerchantTransferId());
}
// 执行插入
@ -117,10 +118,13 @@ public class PayNotifyServiceImpl implements PayNotifyService {
// 必须在事务提交后在发起任务否则 PayNotifyTaskDO 还没入库就提前回调接入的业务
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
executeNotify(task);
// 异步的原因避免阻塞当前事务无需等待结果
getSelf().executeNotifyAsync(task);
}
});
}
@ -166,7 +170,17 @@ public class PayNotifyServiceImpl implements PayNotifyService {
}
/**
* 同步执行单个支付通知
* 异步执行单个支付通知
*
* @param task 通知任务
*/
@Async
public void executeNotifyAsync(PayNotifyTaskDO task) {
executeNotify(task);
}
/**
* 加锁执行单个支付通知
*
* @param task 通知任务
*/
@ -223,10 +237,11 @@ public class PayNotifyServiceImpl implements PayNotifyService {
.payOrderId(task.getDataId()).build();
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
.merchantRefundId(task.getMerchantRefundId())
.payRefundId(task.getDataId()).build();
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
request = new PayTransferNotifyReqDTO().setMerchantTransferId(task.getMerchantTransferId())
.setPayTransferId(task.getDataId());
request = PayTransferNotifyReqDTO.builder().merchantTransferId(task.getMerchantTransferId())
.payTransferId(task.getDataId()).build();
} else {
throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task));
}

View File

@ -32,12 +32,12 @@ import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
import 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 jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
@ -140,7 +140,7 @@ public class PayOrderServiceImpl implements PayOrderService {
PayOrderDO order = validateOrderCanSubmit(reqVO.getId());
// 1.32 校验支付渠道是否有效
PayChannelDO channel = validateChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
PayClient client = channelService.getPayClient(channel.getId());
PayClient<?> client = channelService.getPayClient(channel.getId());
// 2. 插入 PayOrderExtensionDO
String no = noRedisDAO.generate(payProperties.getOrderNoPrefix());
@ -237,7 +237,7 @@ public class PayOrderServiceImpl implements PayOrderService {
appService.validPayApp(appId);
// 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(appId, channelCode);
PayClient client = channelService.getPayClient(channel.getId());
PayClient<?> client = channelService.getPayClient(channel.getId());
if (client == null) {
log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(CHANNEL_NOT_FOUND);

View File

@ -62,7 +62,7 @@ public interface PayRefundService {
* @param reqDTO 退款申请信息
* @return 退款单号
*/
Long createPayRefund(PayRefundCreateReqDTO reqDTO);
Long createRefund(PayRefundCreateReqDTO reqDTO);
/**
* 渠道的退款通知

View File

@ -91,14 +91,14 @@ public class PayRefundServiceImpl implements PayRefundService {
}
@Override
public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
public Long createRefund(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());
PayClient<?> client = channelService.getPayClient(channel.getId());
if (client == null) {
log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(CHANNEL_NOT_FOUND);

View File

@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.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 cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import jakarta.validation.Valid;
@ -15,24 +15,13 @@ import jakarta.validation.Valid;
*/
public interface PayTransferService {
/**
* 创建转账单并发起转账
*
* 此时会发起转账渠道的调用
*
* @param reqVO 请求
* @param userIp 用户 ip
* @return 渠道的返回结果
*/
PayTransferDO createTransfer(@Valid PayTransferCreateReqVO reqVO, String userIp);
/**
* 创建转账单并发起转账
*
* @param reqDTO 创建请求
* @return 转账单编号
*/
Long createTransfer(@Valid PayTransferCreateReqDTO reqDTO);
PayTransferCreateRespDTO createTransfer(@Valid PayTransferCreateReqDTO reqDTO);
/**
* 获取转账单
@ -40,6 +29,14 @@ public interface PayTransferService {
*/
PayTransferDO getTransfer(Long id);
/**
* 根据转账单号获取转账单
*
* @param no 转账单号
* @return 转账单
*/
PayTransferDO getTransferByNo(String no);
/**
* 获得转账单分页
*
@ -55,6 +52,13 @@ public interface PayTransferService {
*/
int syncTransfer();
/**
* 单个同步渠道转账单状态
*
* @param id 转账单编号
*/
void syncTransfer(Long id);
/**
* 渠道的转账通知
*

View File

@ -5,14 +5,14 @@ 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.common.util.object.BeanUtils;
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 cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
@ -26,7 +26,6 @@ import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -34,9 +33,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.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等彻底实现完单测写写
@ -64,62 +61,51 @@ public class PayTransferServiceImpl implements PayTransferService {
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) {
public PayTransferCreateRespDTO 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());
PayClient<?> client = channelService.getPayClient(channel.getId());
if (client == null) {
log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(CHANNEL_NOT_FOUND);
}
// 1.3 校验转账单已经发起过转账
// 1.3 校验转账单已经发起过转账
PayTransferDO transfer = validateTransferCanCreate(reqDTO, payApp.getId());
// 2.1 情况一不存在创建转账单则进行创建
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());
transfer = BeanUtils.toBean(reqDTO, PayTransferDO.class)
.setAppId(channel.getAppId()).setChannelId(channel.getId())
.setNo(no).setStatus(PayTransferStatusEnum.WAITING.getStatus())
.setNotifyUrl(payApp.getTransferNotifyUrl());
transferMapper.insert(transfer);
} else {
// 2.2 情况二存在创建转账单但是状态为关闭则更新为等待中
transferMapper.updateByIdAndStatus(transfer.getId(), transfer.getStatus(),
new PayTransferDO().setStatus(PayTransferStatusEnum.WAITING.getStatus()));
}
PayTransferRespDTO unifiedTransferResp = null;
try {
// 3. 调用三方渠道发起转账
PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer)
.setOutTransferNo(transfer.getNo());
transferUnifiedReq.setNotifyUrl(genChannelTransferNotifyUrl(channel));
PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
PayTransferUnifiedReqDTO transferUnifiedReq = BeanUtils.toBean(reqDTO, PayTransferUnifiedReqDTO.class)
.setOutTransferNo(transfer.getNo())
.setNotifyUrl(genChannelTransferNotifyUrl(channel));
unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
// 4. 通知转账结果
getSelf().notifyTransfer(channel, unifiedTransferResp);
} catch (Throwable e) {
// 注意这里仅打印异常不进行抛出
// 原因是虽然调用支付渠道进行转账发生异常网络请求超时实际转账成功这个结果后续转账轮询可以拿到
// 或者使用相同 no 再次发起转账请求
log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
// 或者使用相同 no 再次发起转账请求
log.error("[createTransfer][转账编号({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
}
return transfer.getId();
return new PayTransferCreateRespDTO().setId(transfer.getId())
.setChannelPackageInfo(unifiedTransferResp != null ? unifiedTransferResp.getChannelPackageInfo() : null);
}
/**
@ -132,21 +118,23 @@ public class PayTransferServiceImpl implements PayTransferService {
return payProperties.getTransferNotifyUrl() + "/" + channel.getId();
}
private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto, Long appId) {
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, dto.getMerchantTransferId());
private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO reqDTO, Long appId) {
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantOrderId(appId, reqDTO.getMerchantTransferId());
if (transfer != null) {
// 已经存在,并且状态不为等待状态说明已经调用渠道转账并返回结果.
if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
// 只有转账单状态为关闭才能再次发起转账
if (!PayTransferStatusEnum.isClosed(transfer.getStatus())) {
throw exception(PAY_TRANSFER_CREATE_FAIL_STATUS_NOT_CLOSED);
}
if (ObjectUtil.notEqual(dto.getPrice(), transfer.getPrice())) {
throw exception(PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH);
// 校验参数是否一致
if (ObjectUtil.notEqual(reqDTO.getPrice(), transfer.getPrice())) {
throw exception(PAY_TRANSFER_CREATE_PRICE_NOT_MATCH);
}
if (ObjectUtil.notEqual(dto.getType(), transfer.getType())) {
throw exception(PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH);
if (ObjectUtil.notEqual(reqDTO.getChannelCode(), transfer.getChannelCode())) {
throw exception(PAY_TRANSFER_CREATE_CHANNEL_NOT_MATCH);
}
}
// 如果状态为等待状态不知道渠道转账是否发起成功 允许使用相同的 no 再次发起转账渠道会保证幂等
// 如果状态为等待状态不知道渠道转账是否发起成功
// 特殊允许使用相同的 no 再次发起转账渠道会保证幂等
return transfer;
}
@ -162,94 +150,95 @@ public class PayTransferServiceImpl implements PayTransferService {
notifyTransferClosed(channel, notify);
}
// 转账处理中的回调
if (PayTransferStatusRespEnum.isInProgress(notify.getStatus())) {
notifyTransferInProgress(channel, notify);
if (PayTransferStatusRespEnum.isProcessing(notify.getStatus())) {
notifyTransferProgressing(channel, notify);
}
// WAITING 状态无需处理
}
private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
private void notifyTransferProgressing(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())) { // 如果已经是转账中直接返回不用重复更新
if (PayTransferStatusEnum.isProgressing(transfer.getStatus())) { // 如果已经是转账中直接返回不用重复更新
log.info("[notifyTransferProgressing][transfer({}) 已经是转账中状态,无需更新]", transfer.getId());
return;
}
if (!isWaiting(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_IS_NOT_WAITING);
}
// 2.更新
// 2. 更新状态
int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(),
CollUtil.newArrayList(WAITING.getStatus()),
new PayTransferDO().setStatus(IN_PROGRESS.getStatus()));
PayTransferStatusEnum.WAITING.getStatus(),
new PayTransferDO().setStatus(PayTransferStatusEnum.PROCESSING.getStatus())
.setChannelPackageInfo(notify.getChannelPackageInfo()));
if (updateCounts == 0) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_IS_NOT_WAITING);
}
log.info("[notifyTransferInProgress][transfer({}) 更新为转账进行中状态]", transfer.getId());
log.info("[notifyTransferProgressing][transfer({}) 更新为转账进行中状态]", transfer.getId());
}
private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
// 1. 校验状态
PayTransferDO transfer = transferMapper.selectByAppIdAndNo(channel.getAppId(), notify.getOutTransferNo());
if (transfer == null) {
throw exception(PAY_TRANSFER_NOT_FOUND);
}
if (isSuccess(transfer.getStatus())) { // 如果已成功直接返回不用重复更新
if (PayTransferStatusEnum.isSuccess(transfer.getStatus())) { // 如果已成功直接返回不用重复更新
log.info("[notifyTransferSuccess][transfer({}) 已经是成功状态,无需更新]", transfer.getId());
return;
}
if (!isPendingStatus(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
if (!PayTransferStatusEnum.isWaitingOrProcessing(transfer.getStatus())) {
throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING);
}
// 2.更新
// 2. 更新状态
int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(),
CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()),
new PayTransferDO().setStatus(SUCCESS.getStatus()).setSuccessTime(notify.getSuccessTime())
CollUtil.newArrayList(PayTransferStatusEnum.WAITING.getStatus(), PayTransferStatusEnum.PROCESSING.getStatus()),
new PayTransferDO().setStatus(PayTransferStatusEnum.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);
throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING);
}
log.info("[updateTransferSuccess][transfer({}) 更新为已转账]", transfer.getId());
log.info("[notifyTransferSuccess][transfer({}) 更新为已转账]", transfer.getId());
// 3. 插入转账通知记录
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
transfer.getId());
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(), transfer.getId());
}
private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
// 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());
if (PayTransferStatusEnum.isClosed(transfer.getStatus())) { // 如果已是关闭状态直接返回不用重复更新
log.info("[notifyTransferClosed][transfer({}) 已经是关闭状态,无需更新]", transfer.getId());
return;
}
if (!isPendingStatus(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
if (!PayTransferStatusEnum.isWaitingOrProcessing(transfer.getStatus())) {
throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING);
}
// 2.更新
// 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)));
CollUtil.newArrayList(PayTransferStatusEnum.WAITING.getStatus(), PayTransferStatusEnum.PROCESSING.getStatus()),
new PayTransferDO().setStatus(PayTransferStatusEnum.CLOSED.getStatus())
.setChannelTransferNo(notify.getChannelTransferNo())
.setChannelNotifyData(JsonUtils.toJsonString(notify))
.setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()));
if (updateCount == 0) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
throw exception(PAY_TRANSFER_NOTIFY_FAIL_STATUS_NOT_WAITING_OR_PROCESSING);
}
log.info("[updateTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId());
log.info("[notifyTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId());
// 3. 插入转账通知记录
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
transfer.getId());
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(), transfer.getId());
}
@Override
@ -257,6 +246,11 @@ public class PayTransferServiceImpl implements PayTransferService {
return transferMapper.selectById(id);
}
@Override
public PayTransferDO getTransferByNo(String no) {
return transferMapper.selectByNo(no);
}
@Override
public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
return transferMapper.selectPage(pageReqVO);
@ -264,27 +258,39 @@ public class PayTransferServiceImpl implements PayTransferService {
@Override
public int syncTransfer() {
List<PayTransferDO> list = transferMapper.selectListByStatus(WAITING.getStatus());
List<PayTransferDO> list = transferMapper.selectListByStatus(CollUtil.newArrayList(
PayTransferStatusEnum.WAITING.getStatus(), PayTransferStatusEnum.PROCESSING.getStatus()));
if (CollUtil.isEmpty(list)) {
return 0;
}
int count = 0;
for (PayTransferDO transfer : list) {
if (!transfer.getId().equals(54L)) {
continue;
}
count += syncTransfer(transfer) ? 1 : 0;
}
return count;
}
@Override
public void syncTransfer(Long id) {
PayTransferDO transfer = transferMapper.selectById(id);
if (transfer == null) {
throw exception(PAY_TRANSFER_NOT_FOUND);
}
syncTransfer(transfer);
}
private boolean syncTransfer(PayTransferDO transfer) {
try {
// 1. 查询转账订单信息
PayClient payClient = channelService.getPayClient(transfer.getChannelId());
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()));
PayTransferRespDTO resp = payClient.getTransfer(transfer.getNo());
// 2. 回调转账结果
notifyTransfer(transfer.getChannelId(), resp);
@ -310,4 +316,5 @@ public class PayTransferServiceImpl implements PayTransferService {
private PayTransferServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}

Some files were not shown because too many files have changed in this diff Show More