Merge remote-tracking branch 'origin/master' into feature/springdoc

# Conflicts:
#	README.md
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyController.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/ProductPropertyValueController.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/ProductPropertyViewRespVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/property/ProductPropertyAndValueRespVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/property/ProductPropertyBaseVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/property/ProductPropertyCreateReqVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/property/ProductPropertyListReqVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/property/ProductPropertyPageReqVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/property/ProductPropertyRespVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/property/ProductPropertyUpdateReqVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/value/ProductPropertyValueBaseVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/value/ProductPropertyValueCreateReqVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/value/ProductPropertyValuePageReqVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/value/ProductPropertyValueRespVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/value/ProductPropertyValueUpdateReqVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/sku/vo/ProductSkuBaseVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/sku/vo/ProductSkuCreateOrUpdateReqVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/ProductSpuController.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuBaseVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuPageReqVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppSpuPageReqVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppSpuPageRespVO.java
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppSpuRespVO.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/base/property/AppProductPropertyValueDetailRespVO.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/AppTradeOrderPageReqVO.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/TradeOrderItemRespVO.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/vo/TradeOrderRespVO.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/vo/PayNotifyOrderReqVO.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/vo/PayRefundOrderReqVO.java
#	yudao-server/pom.xml
#	yudao-server/src/main/java/cn/iocoder/yudao/module/shop/controller/app/AppShopOrderController.java
This commit is contained in:
xingyu
2023-01-04 10:48:18 +08:00
471 changed files with 17761 additions and 4522 deletions

View File

@@ -1,18 +0,0 @@
package cn.iocoder.yudao.module.pay.api;
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
import cn.iocoder.yudao.module.pay.api.order.PayOrderInfoCreateReqDTO;
import org.springframework.stereotype.Service;
/**
* TODO 注释
*/
@Service
public class PayOrderApiImpl implements PayOrderApi {
@Override
public Long createPayOrder(PayOrderInfoCreateReqDTO reqDTO) {
return null;
}
}

View File

@@ -1,19 +1,34 @@
//package cn.iocoder.yudao.module.pay.api.order;
//
//import org.springframework.stereotype.Service;
//import org.springframework.transaction.annotation.Transactional;
//
///**
// * @author LeeYan9
// * @since 2022-09-06
// */
//@Service
//public class PayOrderApiImpl implements PayOrderApi {
//
// @Override
// @Transactional(rollbackFor = Exception.class)
// public Long createPayOrder(PayOrderInfoCreateReqDTO reqDTO) {
// return null;
// }
//
//}
package cn.iocoder.yudao.module.pay.api.order;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 支付单 API 实现类
*
* @author 芋道源码
*/
@Service
public class PayOrderApiImpl implements PayOrderApi {
@Resource
private PayOrderService payOrderService;
@Override
public Long createOrder(PayOrderCreateReqDTO reqDTO) {
return payOrderService.createPayOrder(reqDTO);
}
@Override
public PayOrderRespDTO getOrder(Long id) {
PayOrderDO order = payOrderService.getOrder(id);
return PayOrderConvert.INSTANCE.convert2(order);
}
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.pay.api.refund;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
/**
* 退款单 API 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class PayRefundApiImpl implements PayRefundApi {
@Override
public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
// TODO 芋艿:暂未实现
return null;
}
@Override
public PayRefundRespDTO getPayRefund(Long id) {
// TODO 芋艿:暂未实现
return null;
}
}

View File

@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.pay.controller.admin.notify;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
@Api(tags = "管理后台 - 支付通知")
@RestController
@RequestMapping("/pay/notify")
@Validated
@Slf4j
public class PayNotifyController {
@Resource
private PayOrderService orderService;
@Resource
private PayRefundService refundService;
@Resource
private PayClientFactory payClientFactory;
/**
* 统一的跳转页面,支付宝跳转参数说明
*
* <a href="https://opendocs.alipay.com/open/203/105285#前台回跳参数说明">支付宝 - 前台回跳参数说明</a>
*
* @param channelId 渠道编号
* @return 返回跳转页面
*/
@GetMapping(value = "/return/{channelId}")
@ApiOperation("渠道统一的支付成功返回地址")
@Deprecated // TODO yunai如果是 way 的情况,应该是跳转回前端地址
public String returnCallback(@PathVariable("channelId") Long channelId,
@RequestParam Map<String, String> params) {
log.info("[returnCallback][app_id({}) 跳转]", params.get("app_id"));
return String.format("渠道[%s]支付成功", channelId);
}
/**
* 统一的渠道支付回调,支付宝的退款回调
*
* @param channelId 渠道编号
* @param params form 参数
* @param body request body
* @return 成功返回 "success"
*/
@PostMapping(value = "/callback/{channelId}")
@ApiOperation(value = "支付渠道的统一回调接口", notes = "包括支付回调,退款回调")
@PermitAll
@OperateLog(enable = false) // 回调地址,无需记录操作日志
public String notifyCallback(@PathVariable("channelId") Long channelId,
@RequestParam Map<String, String> params,
@RequestBody String body) throws Exception {
// 校验支付渠道是否存在
PayClient payClient = payClientFactory.getPayClient(channelId);
if (payClient == null) {
log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId);
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 校验通知数据是否合法
PayNotifyDataDTO notifyData = PayNotifyDataDTO.builder().params(params).body(body).build();
payClient.verifyNotifyData(notifyData);
// 情况一:如果是退款,则发起退款通知
if (payClient.isRefundNotify(notifyData)) {
refundService.notifyPayRefund(channelId, PayNotifyDataDTO.builder().params(params).body(body).build());
return "success";
}
// 情况二:如果非退款,则发起支付通知
orderService.notifyPayOrder(channelId, PayNotifyDataDTO.builder().params(params).body(body).build());
return "success";
}
}

View File

@@ -0,0 +1,10 @@
### /pay/create 提交支付订单
POST {{appApi}}/pay/order/submit
Content-Type: application/json
Authorization: Bearer {{appToken}}
tenant-id: {{appTenentId}}
{
"id": 125,
"channelCode": "alipay_qr"
}

View File

@@ -2,29 +2,25 @@ package cn.iocoder.yudao.module.pay.controller.app.order;
import cn.hutool.core.bean.BeanUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO;
import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderSubmitReqDTO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderSubmitRespDTO;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
@Tag(name = "用户 APP - 支付订单")
@RestController
@@ -35,11 +31,6 @@ public class AppPayOrderController {
@Resource
private PayOrderService orderService;
@Resource
private PayRefundService refundService;
@Resource
private PayClientFactory payClientFactory;
@PostMapping("/submit")
@Operation(summary = "提交支付订单")
@@ -59,64 +50,4 @@ public class AppPayOrderController {
return success(AppPayOrderSubmitRespVO.builder().invokeResponse(respDTO.getInvokeResponse()).build());
}
// ========== 支付渠道的回调 ==========
// TODO @芋艿:是不是放到 notify 模块更合适
//TODO 芋道源码 换成了统一的地址了 /notify/{channelId},测试通过可以删除
@PostMapping("/notify/wx-pub/{channelId}")
@Operation(summary = "通知微信公众号支付的结果")
public String notifyWxPayOrder(@PathVariable("channelId") Long channelId,
@RequestBody String xmlData) throws Exception {
orderService.notifyPayOrder(channelId, PayNotifyDataDTO.builder().body(xmlData).build());
return "success";
}
/**
* 统一的跳转页面, 支付宝跳转参数说明
* https://opendocs.alipay.com/open/203/105285#%E5%89%8D%E5%8F%B0%E5%9B%9E%E8%B7%B3%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
* @param channelId 渠道id
* @return 返回跳转页面
*/
@GetMapping(value = "/return/{channelId}")
@Operation(summary = "渠道统一的支付成功返回地址")
public String returnAliPayOrder(@PathVariable("channelId") Long channelId, @RequestParam Map<String, String> params){
//TODO 可以根据渠道和 app_id 返回不同的页面
log.info("app_id is {}", params.get("app_id"));
return String.format("渠道[%s]支付成功", channelId);
}
/**
* 统一的渠道支付回调,支付宝的退款回调
*
* @param channelId 渠道编号
* @param params form 参数
* @param originData http request body
* @return 成功返回 "success"
*/
@PostMapping(value = "/notify/{channelId}")
@Operation(summary = "渠道统一的支付成功,或退款成功 通知url")
public String notifyChannelPay(@PathVariable("channelId") Long channelId,
@RequestParam Map<String, String> params,
@RequestBody String originData) throws Exception {
// 校验支付渠道是否存在
PayClient payClient = payClientFactory.getPayClient(channelId);
if (payClient == null) {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channelId);
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 校验通知数据是否合法
PayNotifyDataDTO notifyData = PayNotifyDataDTO.builder().params(params).body(originData).build();
payClient.verifyNotifyData(notifyData);
// 如果是退款,则发起退款通知
if (payClient.isRefundNotify(notifyData)) {
refundService.notifyPayRefund(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build());
return "success";
}
// 如果非退款,则发起支付通知
orderService.notifyPayOrder(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build());
return "success";
}
}

View File

@@ -1,16 +1,18 @@
package cn.iocoder.yudao.module.pay.convert.order;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderDetailsRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExcelVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageItemRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderRespVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderSubmitReqDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.math.BigDecimal;
@@ -40,13 +42,14 @@ public interface PayOrderConvert {
List<PayOrderExcelVO> convertList02(List<PayOrderDO> list);
/**
* 订单DO转自定义分页对象
* 订单 DO 转自定义分页对象
*
* @param bean 订单DO
* @return 分页对象
*/
PayOrderPageItemRespVO pageConvertItemPage(PayOrderDO bean);
// TODO 芋艿:优化下 convert 逻辑
default PayOrderExcelVO excelConvert(PayOrderDO bean) {
if (bean == null) {
return null;
@@ -88,8 +91,11 @@ public interface PayOrderConvert {
PayOrderDO convert(PayOrderCreateReqDTO bean);
@Mapping(target = "id", ignore = true)
PayOrderExtensionDO convert(PayOrderSubmitReqDTO bean);
PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqDTO bean);
PayOrderRespDTO convert2(PayOrderDO bean);
}

View File

@@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.merchant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -26,7 +26,7 @@ import lombok.*;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayChannelDO extends BaseDO {
public class PayChannelDO extends TenantBaseDO {
/**
* 渠道编号,数据库自增

View File

@@ -93,7 +93,7 @@ public class PayOrderDO extends BaseDO {
/**
* 支付金额,单位:分
*/
private Long amount;
private Integer amount;
/**
* 渠道手续费,单位:百分比
*

View File

@@ -132,11 +132,11 @@ public class PayRefundDO extends BaseDO {
/**
* 支付金额,单位:分
*/
private Long payAmount;
private Integer payAmount;
/**
* 退款金额,单位:分
*/
private Long refundAmount;
private Integer refundAmount;
/**
* 退款原因
@@ -194,6 +194,4 @@ public class PayRefundDO extends BaseDO {
private LocalDateTime notifyTime;
}

View File

@@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.pay.enums.order;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付订单的状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayOrderStatusEnum implements IntArrayValuable {
WAITING(0, "未支付"),
SUCCESS(10, "支付成功"),
CLOSED(20, "支付关闭"), // 未付款交易超时关闭,或支付完成后全额退款 TODO 芋艿:需要优化下
;
private final Integer status;
private final String name;
@Override
public int[] array() {
return new int[0];
}
}

View File

@@ -1,17 +0,0 @@
package cn.iocoder.yudao.module.pay.enums.refund;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum PayRefundStatusEnum {
CREATE(0, "退款订单生成"),
SUCCESS(1, "退款成功"),
FAILURE(2, "退款失败"),
CLOSE(99, "退款关闭");
private final Integer status;
private final String name;
}

View File

@@ -15,8 +15,8 @@ import javax.annotation.Resource;
* @author 芋道源码
*/
@Component
@TenantJob // 多租户
@Slf4j
@TenantJob
public class PayNotifyJob implements JobHandler {
@Resource

View File

@@ -2,8 +2,8 @@
* pay 模块,我们放支付业务,提供业务的支付能力。
* 例如说:商户、应用、支付、退款等等
*
* 1. Controller URL以 /member/ 开头,避免和其它 Module 冲突
* 2. DataObject 表名:以 member_ 开头,方便在数据库中区分
* 1. Controller URL以 /pay/ 开头,避免和其它 Module 冲突
* 2. DataObject 表名:以 pay_ 开头,方便在数据库中区分
*
* 注意,由于 Pay 模块和 Trade 模块,容易重名,所以类名都加载 Pay 的前缀~
*/

View File

@@ -22,7 +22,7 @@ public interface PayChannelService {
/**
* 初始化支付客户端
*/
void initPayClients();
void initLocalCache();
/**
* 创建支付渠道

View File

@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.pay.service.merchant;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@@ -10,7 +9,7 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelPageReqVO;
@@ -20,7 +19,6 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.mysql.merchant.PayChannelMapper;
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -66,53 +64,47 @@ public class PayChannelServiceImpl implements PayChannelService {
@Resource
private Validator validator;
@Resource
@Lazy // 注入自己,所以延迟加载
private PayChannelService self;
/**
* 初始化 {@link #payClientFactory} 缓存
*/
@Override
@PostConstruct
@TenantIgnore // 忽略自动化租户,全局初始化本地缓存
public void initPayClients() {
// 获取支付渠道,如果有更新
List<PayChannelDO> payChannels = loadPayChannelIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(payChannels)) {
return;
}
// 创建或更新支付 Client
payChannels.forEach(payChannel -> payClientFactory.createOrUpdatePayClient(payChannel.getId(),
payChannel.getCode(), payChannel.getConfig()));
// 写入缓存
maxUpdateTime = CollectionUtils.getMaxValue(payChannels, PayChannelDO::getUpdateTime);
log.info("[initPayClients][初始化 PayChannel 数量为 {}]", payChannels.size());
public void initLocalCache() {
initLocalCacheIfUpdate(null);
}
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void schedulePeriodicRefresh() {
self.initPayClients();
initLocalCacheIfUpdate(this.maxUpdateTime);
}
/**
* 如果支付渠道发生变化,从数据库中获取最新的全量支付渠道。
* 如果未发生变化,则返回空
* 刷新本地缓存
*
* @param maxUpdateTime 当前支付渠道的最大更新时间
* @return 支付渠道列表
* @param maxUpdateTime 最大更新时间
* 1. 如果 maxUpdateTime 为 null则“强制”刷新缓存
* 2. 如果 maxUpdateTime 不为 null判断自 maxUpdateTime 是否有数据发生变化,有的情况下才刷新缓存
*/
private List<PayChannelDO> loadPayChannelIfUpdate(LocalDateTime maxUpdateTime) {
// 第一步,判断是否要更新。
if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
log.info("[loadPayChannelIfUpdate][首次加载全量支付渠道]");
} else { // 判断数据库中是否有更新的支付渠道
if (channelMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) {
return null;
private void initLocalCacheIfUpdate(LocalDateTime maxUpdateTime) {
// 注意:忽略自动多租户,因为要全局初始化缓存
TenantUtils.executeIgnore(() -> {
// 第一步:基于 maxUpdateTime 判断缓存是否刷新。
// 如果没有增量的数据变化,则不进行本地缓存的刷新
if (maxUpdateTime != null
&& channelMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) {
log.info("[initLocalCacheIfUpdate][数据未发生变化({}),本地缓存不刷新]", maxUpdateTime);
return;
}
log.info("[loadPayChannelIfUpdate][增量加载全量支付渠道]");
}
// 第二步,如果有更新,则从数据库加载所有支付渠道
return channelMapper.selectList();
List<PayChannelDO> channels = channelMapper.selectList();
log.info("[initLocalCacheIfUpdate][缓存支付渠道,数量为:{}]", channels.size());
// 第二步:构建缓存。创建或更新支付 Client
channels.forEach(payChannel -> payClientFactory.createOrUpdatePayClient(payChannel.getId(),
payChannel.getCode(), payChannel.getConfig()));
// 第三步:设置最新的 maxUpdateTime用于下次的增量判断。
this.maxUpdateTime = CollectionUtils.getMaxValue(channels, PayChannelDO::getUpdateTime);
});
}
@Override

View File

@@ -2,8 +2,13 @@ package cn.iocoder.yudao.module.pay.service.notify;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
@@ -14,11 +19,8 @@ import cn.iocoder.yudao.module.pay.dal.redis.notify.PayNotifyLockRedisDAO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import cn.iocoder.yudao.module.pay.service.notify.vo.PayNotifyOrderReqVO;
import cn.iocoder.yudao.module.pay.service.notify.vo.PayRefundOrderReqVO;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
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.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import lombok.extern.slf4j.Slf4j;
@@ -31,7 +33,9 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -166,7 +170,8 @@ public class PayNotifyServiceImpl implements PayNotifyService {
// 虽然已经通过分布式加锁,但是可能同时满足通知的条件,然后都去获得锁。此时,第一个执行完后,第二个还是能拿到锁,然后会再执行一次。
PayNotifyTaskDO dbTask = payNotifyTaskCoreMapper.selectById(task.getId());
if (LocalDateTimeUtils.afterNow(dbTask.getNextNotifyTime())) {
log.info("[executeNotify][dbTask({}) 任务被忽略,原因是未到达下次通知时间,可能是因为并发执行了]", JsonUtils.toJsonString(dbTask));
log.info("[executeNotifySync][dbTask({}) 任务被忽略,原因是未到达下次通知时间,可能是因为并发执行了]",
JsonUtils.toJsonString(dbTask));
return;
}
@@ -186,11 +191,12 @@ public class PayNotifyServiceImpl implements PayNotifyService {
invokeException = e;
}
// 处理
Integer newStatus = this.processNotifyResult(task, invokeResult, invokeException);
// 处理结果
Integer newStatus = processNotifyResult(task, invokeResult, invokeException);
// 记录 PayNotifyLog 日志
String response = invokeException != null ? ExceptionUtil.getRootCauseMessage(invokeException) : JsonUtils.toJsonString(invokeResult);
String response = invokeException != null ? ExceptionUtil.getRootCauseMessage(invokeException) :
JsonUtils.toJsonString(invokeResult);
payNotifyLogCoreMapper.insert(PayNotifyLogDO.builder().taskId(task.getId())
.notifyTimes(task.getNotifyTimes() + 1).status(newStatus).response(response).build());
}
@@ -202,22 +208,28 @@ public class PayNotifyServiceImpl implements PayNotifyService {
* @return HTTP 响应
*/
private CommonResult<?> executeNotifyInvoke(PayNotifyTaskDO task) {
// 拼接参数
// 拼接 body 参数
Object request;
if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
request = PayNotifyOrderReqVO.builder().merchantOrderId(task.getMerchantOrderId())
.payOrderId(task.getDataId()).build();
request = PayOrderNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
.payOrderId(task.getDataId()).build();
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
request = PayRefundOrderReqVO.builder().merchantOrderId(task.getMerchantOrderId())
request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
.payRefundId(task.getDataId()).build();
} else {
throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task));
}
// 请求地址
String response = HttpUtil.post(task.getNotifyUrl(), JsonUtils.toJsonString(request),
(int) NOTIFY_TIMEOUT_MILLIS);
// 解析结果
return JsonUtils.parseObject(response, CommonResult.class);
// 拼接 header 参数
Map<String, String> headers = new HashMap<>();
TenantUtils.addTenantHeader(headers);
// 发起请求
try (HttpResponse response = HttpUtil.createPost(task.getNotifyUrl())
.body(JsonUtils.toJsonString(request)).addHeaders(headers)
.timeout((int) NOTIFY_TIMEOUT_MILLIS).execute()) {
// 解析结果
return JsonUtils.parseObject(response.body(), CommonResult.class);
}
}
/**

View File

@@ -1,27 +0,0 @@
package cn.iocoder.yudao.module.pay.service.notify.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Schema(description = "支付单的通知 Request VO,业务方接入支付回调时,使用该 VO 对象")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayNotifyOrderReqVO {
@Schema(description = "商户订单编号", required = true, example = "10")
@NotEmpty(message = "商户订单号不能为空")
private String merchantOrderId;
@Schema(description = "支付订单编号", required = true, example = "20")
@NotNull(message = "支付订单编号不能为空")
private Long payOrderId;
}

View File

@@ -1,30 +0,0 @@
package cn.iocoder.yudao.module.pay.service.notify.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Schema(description = "退款单的通知 Request VO,业务方接入退款回调时,使用该 VO 对象")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayRefundOrderReqVO {
@Schema(description = "商户退款单编号", required = true, example = "10")
@NotEmpty(message = "商户退款单编号不能为空")
private String merchantOrderId;
@Schema(description = "支付退款编号", required = true, example = "20")
@NotNull(message = "支付退款编号不能为空")
private Long payRefundId;
@Schema(description = "退款状态(成功,失败)", required = true, example = "10")
private Integer status;
}

View File

@@ -1,6 +0,0 @@
/**
* 这里的 VO 包有点特殊,是提供给接入支付模块的业务,提供回调接口时,可以直接使用 VO
*
* 例如说,支付单的回调,使用 TODO 芋艿:想下怎么优化下
*/
package cn.iocoder.yudao.module.pay.service.notify.vo;

View File

@@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderSubmitReqDTO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderSubmitRespDTO;

View File

@@ -1,9 +1,10 @@
package cn.iocoder.yudao.module.pay.service.order;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.config.PayProperties;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
@@ -11,6 +12,8 @@ import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
@@ -28,8 +31,6 @@ import cn.iocoder.yudao.module.pay.service.merchant.PayAppService;
import cn.iocoder.yudao.module.pay.service.merchant.PayChannelService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderSubmitReqDTO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderSubmitRespDTO;
import lombok.extern.slf4j.Slf4j;
@@ -43,6 +44,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 支付订单 Service 实现类
*
@@ -133,16 +136,16 @@ public class PayOrderServiceImpl implements PayOrderService {
PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) {
log.error("[submitPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 获得 PayOrderDO ,并校验其是否存在
PayOrderDO order = orderMapper.selectById(reqDTO.getId());
if (order == null || !Objects.equals(order.getAppId(), reqDTO.getAppId())) { // 是否存在
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING);
throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING);
}
// 插入 PayOrderExtensionDO
@@ -177,7 +180,7 @@ public class PayOrderServiceImpl implements PayOrderService {
* @return 支付成功返回的地址。 配置地址 + "/" + channel id
*/
private String genChannelReturnUrl(PayChannelDO channel) {
return payProperties.getPayReturnUrl() + "/" + channel.getId();
return payProperties.getReturnUrl() + "/" + channel.getId();
}
/**
@@ -187,8 +190,7 @@ public class PayOrderServiceImpl implements PayOrderService {
* @return 支付渠道的回调地址 配置地址 + "/" + channel id
*/
private String genChannelPayNotifyUrl(PayChannelDO channel) {
//去掉channel code, 似乎没啥用, 用统一的回调地址
return payProperties.getPayNotifyUrl() + "/" + channel.getId();
return payProperties.getCallbackUrl() + "/" + channel.getId();
}
private String generateOrderExtensionNo() {
@@ -210,64 +212,99 @@ public class PayOrderServiceImpl implements PayOrderService {
}
@Override
@Transactional
public void notifyPayOrder(Long channelId, PayNotifyDataDTO notifyData) throws Exception {
@Transactional(rollbackFor = Exception.class)
public void notifyPayOrder(Long channelId, PayNotifyDataDTO notifyData) {
// TODO 芋艿,记录回调日志
log.info("[notifyPayOrder][channelId({}) 回调数据({})]", channelId, notifyData.getBody());
// 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(channelId);
TenantUtils.execute(channel.getTenantId(), () -> {
try {
notifyPayOrder(channel, notifyData);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
private void notifyPayOrder(PayChannelDO channel, PayNotifyDataDTO notifyData) throws Exception {
// 校验支付客户端是否正确初始化
PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 解析支付结果
// 0. 解析支付结果
PayOrderNotifyRespDTO notifyRespDTO = client.parseOrderNotify(notifyData);
// TODO 芋艿,先最严格的校验。即使调用方重复调用,实际哪个订单已经被重复回调的支付,也返回 false 。也没问题,因为实际已经回调成功了。
// 1.1 查询 PayOrderExtensionDO
PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notifyRespDTO.getOrderExtensionNo());
if (orderExtension == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND);
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(orderExtension.getStatus())) { // 校验状态,必须是待支付
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
// 1.2 更新 PayOrderExtensionDO
//TODO 支付宝交易超时 TRADE_FINISHED 需要更新交易关闭
int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(),
PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId())
.status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(notifyData.getBody()).build());
if (updateCounts == 0) { // 校验状态,必须是待支付
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
log.info("[notifyPayOrder][支付拓展单({}) 更新为已支付]", orderExtension.getId());
// 2.1 判断 PayOrderDO 是否处于待支付
PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId());
if (order == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING);
}
// 2.2 更新 PayOrderDO
updateCounts = orderMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(),
PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelId(channelId).channelCode(channel.getCode())
.successTime(notifyRespDTO.getSuccessTime()).successExtensionId(orderExtension.getId())
.channelOrderNo(notifyRespDTO.getChannelOrderNo()).channelUserId(notifyRespDTO.getChannelUserId())
.notifyTime(LocalDateTime.now()).build());
if (updateCounts == 0) { // 校验状态,必须是待支付
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING);
}
log.info("[notifyPayOrder][支付订单({}) 更新为已支付]", order.getId());
// 1. 更新 PayOrderExtensionDO 支付成功
PayOrderExtensionDO orderExtension = updatePayOrderExtensionSuccess(notifyRespDTO.getOrderExtensionNo(), notifyData.getBody());
// 2. 更新 PayOrderDO 支付成功
PayOrderDO order = updatePayOrderSuccess(channel, orderExtension, notifyRespDTO);
// 3. 插入支付通知记录
notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.ORDER.getType()).dataId(order.getId()).build());
}
/**
* 更新 PayOrderExtensionDO 支付成功
*
* @param no 支付订单号(支付模块)
* @param body 回调内容
* @return PayOrderExtensionDO 对象
*/
private PayOrderExtensionDO updatePayOrderExtensionSuccess(String no, String body) {
// 1.1 查询 PayOrderExtensionDO
PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(no);
if (orderExtension == null) {
throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND);
}
if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
// 1.2 更新 PayOrderExtensionDO
int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(),
PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId())
.status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(body).build());
if (updateCounts == 0) { // 校验状态,必须是待支付
throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
log.info("[updatePayOrderSuccess][支付拓展单({}) 更新为已支付]", orderExtension.getId());
return orderExtension;
}
/**
* 更新 PayOrderDO 支付成功
*
* @param channel 支付渠道
* @param orderExtension 支付拓展单
* @param notifyRespDTO 通知回调
* @return PayOrderDO 对象
*/
private PayOrderDO updatePayOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,
PayOrderNotifyRespDTO notifyRespDTO) {
// 2.1 判断 PayOrderDO 是否处于待支付
PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId());
if (order == null) {
throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING);
}
// 2.2 更新 PayOrderDO
int updateCounts = orderMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(),
PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus())
.channelId(channel.getId()).channelCode(channel.getCode())
.successTime(notifyRespDTO.getSuccessTime()).successExtensionId(orderExtension.getId())
.channelOrderNo(notifyRespDTO.getChannelOrderNo()).channelUserId(notifyRespDTO.getChannelUserId())
.notifyTime(LocalDateTime.now()).build());
if (updateCounts == 0) { // 校验状态,必须是待支付
throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING);
}
log.info("[updatePayOrderSuccess][支付订单({}) 更新为已支付]", order.getId());
return order;
}
}

View File

@@ -1,64 +0,0 @@
package cn.iocoder.yudao.module.pay.service.order.dto;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 支付单创建 Request DTO
*/
@Data
public class PayOrderCreateReqDTO implements Serializable {
/**
* 应用编号
*/
@NotNull(message = "应用编号不能为空")
private Long appId;
/**
* 用户 IP
*/
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
*/
@NotEmpty(message = "商户订单编号不能为空")
private String merchantOrderId;
/**
* 商品标题
*/
@NotEmpty(message = "商品标题不能为空")
@Length(max = 32, message = "商品标题不能超过 32")
private String subject;
/**
* 商品描述
*/
@NotEmpty(message = "商品描述信息不能为空")
@Length(max = 128, message = "商品描述信息长度不能超过128")
private String body;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer amount;
/**
* 支付过期时间
*/
@NotNull(message = "支付过期时间不能为空")
private LocalDateTime expireTime;
}

View File

@@ -31,7 +31,7 @@ public class PayRefundReqDTO {
*/
@NotNull(message = "退款金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "退款金额必须大于零")
private Long amount;
private Integer amount;
/**
* 退款原因

View File

@@ -1 +0,0 @@
package cn.iocoder.yudao.module.pay.service;

View File

@@ -49,7 +49,6 @@ public class PayChannelServiceTest extends BaseDbUnitTest {
@Test
public void testCreateWechatVersion2Channel_success() {
// 准备参数
WXPayClientConfig v2Config = getV2Config();
PayChannelCreateReqVO reqVO = randomPojo(PayChannelCreateReqVO.class, o -> {
o.setCode(PayChannelEnum.WX_PUB.getCode());

View File

@@ -76,7 +76,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
o.setBody("斌斌子送给灿灿子的炸弹猫");
o.setNotifyUrl("https://hc.com/lbh");
o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
o.setAmount(10000L);
o.setAmount(10000);
o.setChannelFeeRate(0.01);
o.setChannelFeeAmount(1L);
o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
@@ -148,7 +148,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
o.setBody("斌斌子送给灿灿子的炸弹猫");
o.setNotifyUrl("https://hc.com/lbh");
o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
o.setAmount(10000L);
o.setAmount(10000);
o.setChannelFeeRate(0.01);
o.setChannelFeeAmount(1L);
o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());

View File

@@ -67,8 +67,8 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
o.setType(PayRefundTypeEnum.SOME.getStatus());
o.setPayAmount(100L);
o.setRefundAmount(500L);
o.setPayAmount(100);
o.setRefundAmount(500);
o.setReason("就是想退款了,你有意见吗");
o.setUserIp("127.0.0.1");
o.setChannelOrderNo("CH0000001");
@@ -136,8 +136,8 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
o.setType(PayRefundTypeEnum.SOME.getStatus());
o.setPayAmount(100L);
o.setRefundAmount(500L);
o.setPayAmount(100);
o.setRefundAmount(500);
o.setReason("就是想退款了,你有意见吗");
o.setUserIp("127.0.0.1");
o.setChannelOrderNo("CH0000001");

View File

@@ -9,7 +9,7 @@ spring:
# 数据源配置项
datasource:
name: ruoyi-vue-pro
url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式DATABASE_TO_UPPER 配置表和字段使用小写
url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式DATABASE_TO_UPPER 配置表和字段使用小写
driver-class-name: org.h2.Driver
username: sa
password:

View File

@@ -43,6 +43,7 @@ CREATE TABLE IF NOT EXISTS "pay_channel" (
"updater" varchar(64) NULL DEFAULT '',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
"deleted" bit(1) NOT NULL DEFAULT FALSE,
"tenant_id" bigint not null default '0',
PRIMARY KEY ("id")
) COMMENT = '支付渠道';