第一次提交

This commit is contained in:
wyw
2024-08-08 00:31:26 +08:00
commit c202e2b63d
1819 changed files with 221890 additions and 0 deletions

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.fastbee.pay</groupId>
<artifactId>fastbee-pay</artifactId>
<version>3.8.5</version>
</parent>
<groupId>com.fastbee.pay.core</groupId>
<artifactId>fastbee-pay-core</artifactId>
<!-- <properties>-->
<!-- <maven.compiler.source>8</maven.compiler.source>-->
<!-- <maven.compiler.target>8</maven.compiler.target>-->
<!-- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>-->
<!-- </properties>-->
<dependencies>
<!-- <dependency>-->
<!-- <groupId>org.projectlombok</groupId>-->
<!-- <artifactId>lombok</artifactId>-->
<!-- <version>1.18.22</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.fastbee</groupId>-->
<!-- <artifactId>fastbee-common</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.fastbee.pay.framework</groupId>-->
<!-- <artifactId>fastbee-pay-framework</artifactId>-->
<!-- <version>3.8.5</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.fastbee.pay.api</groupId>
<artifactId>fastbee-pay-api</artifactId>
<version>3.8.5</version>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>13.0</version>
<scope>compile</scope>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>easyexcel-core</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.mapstruct</groupId>-->
<!-- <artifactId>mapstruct</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.google.guava</groupId>-->
<!-- <artifactId>guava</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>fastbee-framework</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,35 @@
package com.fastbee.pay.core.api.order;
import com.fastbee.pay.api.api.order.PayOrderApi;
import com.fastbee.pay.api.api.order.dto.PayOrderCreateReqDTO;
import com.fastbee.pay.api.api.order.dto.PayOrderRespDTO;
import com.fastbee.pay.core.convert.order.PayOrderConvert;
import com.fastbee.pay.core.domain.dataobject.order.PayOrder;
import com.fastbee.pay.core.service.order.PayOrderService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 支付单 API 实现类
*
* @author fastbee
*/
@Service
public class PayOrderApiImpl implements PayOrderApi {
@Resource
private PayOrderService payOrderService;
@Override
public Long createOrder(PayOrderCreateReqDTO reqDTO) {
return payOrderService.createOrder(reqDTO);
}
@Override
public PayOrderRespDTO getOrder(Long id) {
PayOrder order = payOrderService.getOrder(id);
return PayOrderConvert.INSTANCE.convert2(order);
}
}

View File

@ -0,0 +1,35 @@
package com.fastbee.pay.core.api.refund;
import com.fastbee.pay.api.api.refund.PayRefundApi;
import com.fastbee.pay.api.api.refund.dto.PayRefundCreateReqDTO;
import com.fastbee.pay.api.api.refund.dto.PayRefundRespDTO;
import com.fastbee.pay.core.convert.refund.PayRefundConvert;
import com.fastbee.pay.core.service.refund.PayRefundService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
/**
* 退款单 API 实现类
*
* @author fastbee
*/
@Service
@Validated
public class PayRefundApiImpl implements PayRefundApi {
@Resource
private PayRefundService payRefundService;
@Override
public Long createRefund(PayRefundCreateReqDTO reqDTO) {
return payRefundService.createPayRefund(reqDTO);
}
@Override
public PayRefundRespDTO getRefund(Long id) {
return PayRefundConvert.INSTANCE.convert02(payRefundService.getRefund(id));
}
}

View File

@ -0,0 +1,108 @@
package com.fastbee.pay.core.controller.admin.app;
import cn.hutool.core.collection.CollUtil;
import com.fastbee.common.core.domain.CommonResult;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.pay.core.controller.admin.app.vo.*;
import com.fastbee.pay.core.convert.app.PayAppConvert;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import com.fastbee.pay.core.service.app.PayAppService;
import com.fastbee.pay.core.service.channel.PayChannelService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
import static com.fastbee.common.core.domain.CommonResult.success;
import static com.fastbee.common.utils.collection.CollectionUtils.convertList;
@Slf4j
@Api(tags = "管理后台 - 支付应用信息")
@RestController
@RequestMapping("/pay/app")
@Validated
public class PayAppController {
@Resource
private PayAppService appService;
@Resource
private PayChannelService channelService;
@PostMapping("/create")
@ApiOperation("创建支付应用信息")
@PreAuthorize("@ss.hasPermission('pay:app:create')")
public CommonResult<Long> createApp(@Valid @RequestBody PayAppCreateReqVO createReqVO) {
return success(appService.createApp(createReqVO));
}
@PutMapping("/update")
@ApiOperation("更新支付应用信息")
@PreAuthorize("@ss.hasPermission('pay:app:update')")
public CommonResult<Boolean> updateApp(@Valid @RequestBody PayAppUpdateReqVO updateReqVO) {
appService.updateApp(updateReqVO);
return success(true);
}
@PutMapping("/update-status")
@ApiOperation("更新支付应用状态")
@PreAuthorize("@ss.hasPermission('pay:app:update')")
public CommonResult<Boolean> updateAppStatus(@Valid @RequestBody PayAppUpdateStatusReqVO updateReqVO) {
appService.updateAppStatus(updateReqVO.getId(), updateReqVO.getStatus());
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除支付应用信息")
@ApiParam(name = "id", value = "编号", required = true)
@PreAuthorize("@ss.hasPermission('pay:app:delete')")
public CommonResult<Boolean> deleteApp(@RequestParam("id") Long id) {
appService.deleteApp(id);
return success(true);
}
@GetMapping("/get")
@ApiOperation("获得支付应用信息")
@ApiParam(name = "id", value = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('pay:app:query')")
public CommonResult<PayAppRespVO> getApp(@RequestParam("id") Long id) {
PayApp app = appService.getApp(id);
return success(PayAppConvert.INSTANCE.convert(app));
}
@GetMapping("/page")
@ApiOperation("获得支付应用信息分页")
@PreAuthorize("@ss.hasPermission('pay:app:query')")
public CommonResult<PageResult<PayAppPageItemRespVO>> getAppPage(@Valid PayAppPageReqVO pageVO) {
// 得到应用分页列表
PageResult<PayApp> pageResult = appService.getAppPage(pageVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty());
}
// 得到所有的应用编号,查出所有的渠道
Collection<Long> appIds = convertList(pageResult.getList(), PayApp::getId);
List<PayChannel> channels = channelService.getChannelListByAppIds(appIds);
// 拼接后返回
return success(PayAppConvert.INSTANCE.convertPage(pageResult, channels));
}
@GetMapping("/list")
@ApiOperation("获得应用列表")
@PreAuthorize("@ss.hasPermission('pay:merchant:query')")
public CommonResult<List<PayAppRespVO>> getAppList() {
List<PayApp> appListDO = appService.getAppList();
return success(PayAppConvert.INSTANCE.convertList(appListDO));
}
}

View File

@ -0,0 +1,38 @@
package com.fastbee.pay.core.controller.admin.app.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.NotNull;
/**
* 支付应用信息 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class PayAppBaseVO {
@ApiModelProperty(value = "应用名", required = true, example = "小豆")
@NotNull(message = "应用名不能为空")
private String name;
@ApiModelProperty(value = "开启状态", required = true, example = "0")
@NotNull(message = "开启状态不能为空")
// @InEnum(CommonStatusEnum.class)
private Integer status;
@ApiModelProperty(value = "备注", example = "我是一个测试应用")
private String remark;
@ApiModelProperty(value = "支付结果的回调地址", required = true, example = "http://127.0.0.1:48080/pay-callback")
@NotNull(message = "支付结果的回调地址不能为空")
@URL(message = "支付结果的回调地址必须为 URL 格式")
private String orderNotifyUrl;
@ApiModelProperty(value = "退款结果的回调地址", required = true, example = "http://127.0.0.1:48080/refund-callback")
@NotNull(message = "退款结果的回调地址不能为空")
@URL(message = "退款结果的回调地址必须为 URL 格式")
private String refundNotifyUrl;
}

View File

@ -0,0 +1,14 @@
package com.fastbee.pay.core.controller.admin.app.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ApiModel(description = "管理后台 - 支付应用信息创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppCreateReqVO extends PayAppBaseVO {
}

View File

@ -0,0 +1,27 @@
package com.fastbee.pay.core.controller.admin.app.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.Set;
@ApiModel(description = "管理后台 - 支付应用信息分页查询 Response VO,相比于支付信息,还会多出应用渠道的开关信息")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppPageItemRespVO extends PayAppBaseVO {
@ApiModelProperty(value = "应用编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
@ApiModelProperty(value = "已配置的支付渠道编码", required = true, example = "[alipay_pc, alipay_wap]")
private Set<String> channelCodes;
}

View File

@ -0,0 +1,32 @@
package com.fastbee.pay.core.controller.admin.app.vo;
import com.fastbee.common.core.domain.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.fastbee.common.utils.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(description = "管理后台 - 支付应用信息分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppPageReqVO extends PageParam {
@ApiModelProperty(value = "应用名", example = "小豆")
private String name;
@ApiModelProperty(value = "开启状态", example = "0")
private Integer status;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "创建时间")
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,23 @@
package com.fastbee.pay.core.controller.admin.app.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@ApiModel(description = "管理后台 - 支付应用信息 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppRespVO extends PayAppBaseVO {
@ApiModelProperty(value = "应用编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
}

View File

@ -0,0 +1,21 @@
package com.fastbee.pay.core.controller.admin.app.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotNull;
@ApiModel(description = "管理后台 - 支付应用信息更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppUpdateReqVO extends PayAppBaseVO {
@ApiModelProperty(value = "应用编号", required = true, example = "1024")
@NotNull(message = "应用编号不能为空")
private Long id;
}

View File

@ -0,0 +1,21 @@
package com.fastbee.pay.core.controller.admin.app.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
@ApiModel(description = "管理后台 - 应用更新状态 Request VO")
@Data
public class PayAppUpdateStatusReqVO {
@ApiModelProperty(value = "应用编号", required = true, example = "1024")
@NotNull(message = "应用编号不能为空")
private Long id;
@ApiModelProperty(value = "状态,见 SysCommonStatusEnum 枚举", required = true, example = "1")
@NotNull(message = "状态不能为空")
private Integer status;
}

View File

@ -0,0 +1,90 @@
package com.fastbee.pay.core.controller.admin.channel;
import com.fastbee.common.core.domain.CommonResult;
import com.fastbee.pay.core.controller.admin.channel.vo.PayChannelCreateReqVO;
import com.fastbee.pay.core.controller.admin.channel.vo.PayChannelRespVO;
import com.fastbee.pay.core.controller.admin.channel.vo.PayChannelUpdateReqVO;
import com.fastbee.pay.core.convert.channel.PayChannelConvert;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import com.fastbee.pay.core.service.channel.PayChannelService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
import java.util.Set;
import static com.fastbee.common.core.domain.CommonResult.success;
import static com.fastbee.common.utils.collection.CollectionUtils.convertSet;
@Api(tags = "管理后台 - 支付渠道")
@RestController
@RequestMapping("/pay/channel")
@Validated
public class PayChannelController {
@Resource
private PayChannelService channelService;
@PostMapping("/create")
@ApiOperation("创建支付渠道")
@PreAuthorize("@ss.hasPermission('pay:channel:create')")
public CommonResult<Long> createChannel(@Valid @RequestBody PayChannelCreateReqVO createReqVO) {
return success(channelService.createChannel(createReqVO));
}
@PutMapping("/update")
@ApiOperation("更新支付渠道")
@PreAuthorize("@ss.hasPermission('pay:channel:update')")
public CommonResult<Boolean> updateChannel(@Valid @RequestBody PayChannelUpdateReqVO updateReqVO) {
channelService.updateChannel(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除支付渠道")
@ApiParam(name = "id", value = "编号", required = true)
@PreAuthorize("@ss.hasPermission('pay:channel:delete')")
public CommonResult<Boolean> deleteChannel(@RequestParam("id") Long id) {
channelService.deleteChannel(id);
return success(true);
}
@GetMapping("/get")
@ApiOperation("获得支付渠道")
@ApiParam(name = "id", value = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('pay:channel:query')")
public CommonResult<PayChannelRespVO> getChannel(@RequestParam(value = "id", required = false) Long id,
@RequestParam(value = "appId", required = false) Long appId,
@RequestParam(value = "code", required = false) String code) {
PayChannel channel = null;
if (id != null) {
channel = channelService.getChannel(id);
} else if (appId != null && code != null) {
channel = channelService.getChannelByAppIdAndCode(appId, code);
}
return success(new PayChannelRespVO());
// return success(PayChannelConvert.INSTANCE.convert(channel));
}
@GetMapping("/get-enable-code-list")
@ApiOperation("获得指定应用的开启的支付渠道编码列表")
@ApiParam(name = "appId", value = "应用编号", required = true, example = "1")
public CommonResult<Set<String>> getEnableChannelCodeList(@RequestParam("appId") Long appId) {
List<PayChannel> channels = channelService.getEnableChannelList(appId);
return success(convertSet(channels, PayChannel::getCode));
}
}

View File

@ -0,0 +1,32 @@
package com.fastbee.pay.core.controller.admin.channel.vo;
import com.fastbee.common.enums.CommonStatusEnum;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 支付渠道 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class PayChannelBaseVO {
@ApiModelProperty(value = "开启状态", required = true, example = "1")
@NotNull(message = "开启状态不能为空")
// @InEnum(CommonStatusEnum.class)
private Integer status;
@ApiModelProperty(value = "备注", example = "我是小备注")
private String remark;
@ApiModelProperty(value = "渠道费率,单位:百分比", required = true, example = "10")
@NotNull(message = "渠道费率,单位:百分比不能为空")
private Double feeRate;
@ApiModelProperty(value = "应用编号", required = true, example = "1024")
@NotNull(message = "应用编号不能为空")
private Long appId;
}

View File

@ -0,0 +1,26 @@
package com.fastbee.pay.core.controller.admin.channel.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@ApiModel(description = "管理后台 - 支付渠道 创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayChannelCreateReqVO extends PayChannelBaseVO {
@ApiModelProperty(value = "渠道编码", required = true, example = "alipay_pc")
@NotNull(message = "渠道编码不能为空")
private String code;
@ApiModelProperty(value = "渠道配置的 json 字符串")
@NotBlank(message = "渠道配置不能为空")
private String config;
}

View File

@ -0,0 +1,29 @@
package com.fastbee.pay.core.controller.admin.channel.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@ApiModel(description = "管理后台 - 支付渠道 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayChannelRespVO extends PayChannelBaseVO {
@ApiModelProperty(value = "商户编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "创建时间", required = true, example = "1024")
private LocalDateTime createTime;
@ApiModelProperty(value = "渠道编码", required = true, example = "alipay_pc")
private String code;
@ApiModelProperty(value = "配置", required = true)
private String config;
}

View File

@ -0,0 +1,26 @@
package com.fastbee.pay.core.controller.admin.channel.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@ApiModel(description = "管理后台 - 支付渠道 更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayChannelUpdateReqVO extends PayChannelBaseVO {
@ApiModelProperty(value = "商户编号", required = true)
@NotNull(message = "商户编号不能为空")
private Long id;
@ApiModelProperty(value = "渠道配置的json字符串")
@NotBlank(message = "渠道配置不能为空")
private String config;
}

View File

@ -0,0 +1,83 @@
package com.fastbee.pay.core.controller.admin.demo;
import com.fastbee.common.core.domain.CommonResult;
import com.fastbee.common.core.domain.PageParam;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.pay.api.api.notify.dto.PayOrderNotifyReqDTO;
import com.fastbee.pay.api.api.notify.dto.PayRefundNotifyReqDTO;
import com.fastbee.pay.core.controller.admin.demo.vo.OrderInfoCreateReqVO;
import com.fastbee.pay.core.controller.admin.demo.vo.OrderInfoRespVO;
import com.fastbee.pay.core.convert.demo.OrderInfoConvert;
import com.fastbee.pay.core.domain.dataobject.demo.OrderInfo;
import com.fastbee.pay.core.service.demo.OrderInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.validation.Valid;
import static com.fastbee.common.core.domain.CommonResult.success;
import static com.fastbee.common.utils.SecurityUtils.getLoginUser;
import static com.fastbee.common.utils.ServletUtils.getClientIP;
@Api(tags = "管理后台 - 示例订单")
@RestController
@RequestMapping("/order")
@Validated
public class OrderInfoController {
@Resource
private OrderInfoService OrderInfoService;
@PostMapping("/create")
@ApiOperation("创建示例订单")
public CommonResult<Long> createOrder(@Valid @RequestBody OrderInfoCreateReqVO createReqVO) {
return success(OrderInfoService.createOrder(getLoginUser().getUserId(), createReqVO));
}
@GetMapping("/page")
@ApiOperation("获得示例订单分页")
public CommonResult<PageResult<OrderInfoRespVO>> getOrderPage(@Valid PageParam pageVO) {
PageResult<OrderInfo> pageResult = OrderInfoService.getOrderPage(pageVO);
return success(OrderInfoConvert.INSTANCE.convertPage(pageResult));
}
@PostMapping("/update-paid")
@ApiOperation("更新示例订单为已支付") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob
@PermitAll // 无需登录,安全由 OrderInfoService 内部校验实现
// @OperateLog(enable = false) // 禁用操作日志,因为没有操作人
public CommonResult<Boolean> updateOrderPaid(@RequestBody PayOrderNotifyReqDTO notifyReqDTO) {
OrderInfoService.updateOrderPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()),
notifyReqDTO.getPayOrderId());
return success(true);
}
@PutMapping("/refund")
@ApiOperation("发起示例订单的退款")
@ApiParam(name = "id", value = "编号", required = true, example = "1024")
public CommonResult<Boolean> refundOrder(@RequestParam("id") Long id) {
OrderInfoService.refundOrder(id, getClientIP());
return success(true);
}
@PostMapping("/update-refunded")
@ApiOperation("更新示例订单为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob
@PermitAll // 无需登录,安全由 OrderInfoService 内部校验实现
// @OperateLog(enable = false) // 禁用操作日志,因为没有操作人
public CommonResult<Boolean> updateOrderRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
OrderInfoService.updateOrderRefunded(Long.valueOf(notifyReqDTO.getMerchantOrderId()),
notifyReqDTO.getPayRefundId());
return success(true);
}
}

View File

@ -0,0 +1,17 @@
package com.fastbee.pay.core.controller.admin.demo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
@ApiModel(description = "管理后台 - 示例订单创建 Request VO")
@Data
public class OrderInfoCreateReqVO {
@ApiModelProperty(value = "商品编号", required = true, example = "17682")
@NotNull(message = "商品编号不能为空")
private Long spuId;
}

View File

@ -0,0 +1,54 @@
package com.fastbee.pay.core.controller.admin.demo.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 示例订单 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class OrderInfoRespVO {
@ApiModelProperty(value = "订单编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "用户编号", required = true, example = "23199")
private Long userId;
@ApiModelProperty(value = "商品编号", required = true, example = "17682")
private Long spuId;
@ApiModelProperty(value = "商家备注", example = "李四")
private String spuName;
@ApiModelProperty(value = "价格,单位:分", required = true, example = "30381")
private Integer price;
@ApiModelProperty(value = "是否已支付", required = true)
private Boolean payStatus;
@ApiModelProperty(value = "支付订单编号", example = "16863")
private Long payOrderId;
@ApiModelProperty(value = "订单支付时间")
private LocalDateTime payTime;
@ApiModelProperty(value = "支付渠道", example = "alipay_qr")
private String payChannelCode;
@ApiModelProperty(value = "支付退款编号", example = "23366")
private Long payRefundId;
@ApiModelProperty(value = "退款金额,单位:分", required = true, example = "14039")
private Integer refundPrice;
@ApiModelProperty(value = "退款时间")
private LocalDateTime refundTime;
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
}

View File

@ -0,0 +1,137 @@
package com.fastbee.pay.core.controller.admin.notify;
import cn.hutool.core.collection.CollUtil;
import com.fastbee.common.core.domain.CommonResult;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.common.utils.MessageUtils;
import com.fastbee.pay.core.controller.admin.notify.vo.PayNotifyTaskDetailRespVO;
import com.fastbee.pay.core.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
import com.fastbee.pay.core.controller.admin.notify.vo.PayNotifyTaskRespVO;
import com.fastbee.pay.core.convert.notify.PayNotifyTaskConvert;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.notify.PayNotifyLog;
import com.fastbee.pay.core.domain.dataobject.notify.PayNotifyTask;
import com.fastbee.pay.core.service.app.PayAppService;
import com.fastbee.pay.core.service.notify.PayNotifyService;
import com.fastbee.pay.core.service.order.PayOrderService;
import com.fastbee.pay.core.service.refund.PayRefundService;
import com.fastbee.pay.framework.client.PayClient;
import com.fastbee.pay.framework.client.PayClientFactory;
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
import com.fastbee.pay.framework.client.dto.refund.PayRefundRespDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
import static com.fastbee.common.core.domain.CommonResult.success;
import static com.fastbee.common.exception.ServiceExceptionUtil.exception;
import static com.fastbee.common.utils.collection.CollectionUtils.convertList;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.CHANNEL_NOT_FOUND;
@Api(tags = "管理后台 - 回调通知")
@RestController
@RequestMapping("/pay/notify")
@Validated
@Slf4j
public class PayNotifyController {
@Resource
private PayOrderService orderService;
@Resource
private PayRefundService refundService;
@Resource
private PayNotifyService notifyService;
@Resource
private PayAppService appService;
@Resource
private PayClientFactory payClientFactory;
@PostMapping(value = "/order/{channelId}")
@ApiOperation("支付渠道的统一【支付】回调")
@PermitAll
// @OperateLog(enable = false) // 回调地址,无需记录操作日志
public String notifyOrder(@PathVariable("channelId") Long channelId,
@RequestParam(required = false) Map<String, String> params,
@RequestBody(required = false) String body) {
log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body);
// 1. 校验支付渠道是否存在
PayClient payClient = payClientFactory.getPayClient(channelId);
if (payClient == null) {
log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId);
throw exception(CHANNEL_NOT_FOUND);
}
// 2. 解析通知数据
PayOrderRespDTO notify = payClient.parseOrderNotify(params, body);
orderService.notifyOrder(channelId, notify);
return MessageUtils.message("success");
}
@PostMapping(value = "/refund/{channelId}")
@ApiOperation("支付渠道的统一【退款】回调")
@PermitAll
// @OperateLog(enable = false) // 回调地址,无需记录操作日志
public String notifyRefund(@PathVariable("channelId") Long channelId,
@RequestParam(required = false) Map<String, String> params,
@RequestBody(required = false) String body) {
log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body);
// 1. 校验支付渠道是否存在
PayClient payClient = payClientFactory.getPayClient(channelId);
if (payClient == null) {
log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId);
throw exception(CHANNEL_NOT_FOUND);
}
// 2. 解析通知数据
PayRefundRespDTO notify = payClient.parseRefundNotify(params, body);
refundService.notifyRefund(channelId, notify);
return MessageUtils.message("success");
}
@GetMapping("/get-detail")
@ApiOperation("获得回调通知的明细")
@ApiParam(name = "id", value = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('pay:notify:query')")
public CommonResult<PayNotifyTaskDetailRespVO> getNotifyTaskDetail(@RequestParam("id") Long id) {
PayNotifyTask task = notifyService.getNotifyTask(id);
if (task == null) {
return success(null);
}
// 拼接返回
PayApp app = appService.getApp(task.getAppId());
List<PayNotifyLog> logs = notifyService.getNotifyLogList(id);
return success(PayNotifyTaskConvert.INSTANCE.convert(task, app, logs));
}
@GetMapping("/page")
@ApiOperation("获得回调通知分页")
@PreAuthorize("@ss.hasPermission('pay:notify:query')")
public CommonResult<PageResult<PayNotifyTaskRespVO>> getNotifyTaskPage(@Valid PayNotifyTaskPageReqVO pageVO) {
PageResult<PayNotifyTask> pageResult = notifyService.getNotifyTaskPage(pageVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty());
}
// 拼接返回
Map<Long, PayApp> appMap = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTask::getAppId));
return success(PayNotifyTaskConvert.INSTANCE.convertPage(pageResult, appMap));
}
}

View File

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

View File

@ -0,0 +1,55 @@
package com.fastbee.pay.core.controller.admin.notify.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.List;
@ApiModel(description = "管理后台 - 回调通知的明细 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayNotifyTaskDetailRespVO extends PayNotifyTaskBaseVO {
@ApiModelProperty(value = "任务编号", required = true, example = "3380")
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
@ApiModelProperty(value = "更新时间", required = true)
private LocalDateTime updateTime;
@ApiModelProperty(value = "应用名称", example = "wx_pay")
private String appName;
@ApiModelProperty(value = "回调日志列表")
private List<Log> logs;
@ApiModel(description = "管理后台 - 回调日志")
@Data
public static class Log {
@ApiModelProperty(value = "日志编号", required = true, example = "8848")
private Long id;
@ApiModelProperty(value = "通知状态", required = true, example = "1")
private Byte status;
@ApiModelProperty(value = "当前通知次数", required = true)
private Byte notifyTimes;
@ApiModelProperty(value = "HTTP 响应结果", required = true)
private String response;
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
}
}

View File

@ -0,0 +1,41 @@
package com.fastbee.pay.core.controller.admin.notify.vo;
import com.fastbee.common.core.domain.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.fastbee.common.utils.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(description = "管理后台 - 回调通知分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayNotifyTaskPageReqVO extends PageParam {
@ApiModelProperty(value = "应用编号", example = "10636")
private Long appId;
@ApiModelProperty(value = "通知类型", example = "2")
private Integer type;
@ApiModelProperty(value = "数据编号", example = "6722")
private Long dataId;
@ApiModelProperty(value = "通知状态", example = "1")
private Integer status;
@ApiModelProperty(value = "商户订单编号", example = "26697")
private String merchantOrderId;
@ApiModelProperty(value = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,26 @@
package com.fastbee.pay.core.controller.admin.notify.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@ApiModel(description = "管理后台 - 回调通知 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayNotifyTaskRespVO extends PayNotifyTaskBaseVO {
@ApiModelProperty(value = "任务编号", required = true, example = "3380")
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
@ApiModelProperty(value = "应用名称", example = "wx_pay")
private String appName;
}

View File

@ -0,0 +1,120 @@
package com.fastbee.pay.core.controller.admin.order;
import cn.hutool.core.collection.CollectionUtil;
import com.fastbee.common.core.domain.CommonResult;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderDetailsRespVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderExcelVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderExportReqVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderPageItemRespVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderPageReqVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderRespVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitReqVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitRespVO;
import com.fastbee.pay.core.convert.order.PayOrderConvert;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.order.PayOrder;
import com.fastbee.pay.core.domain.dataobject.order.PayOrderExtension;
import com.fastbee.pay.core.service.app.PayAppService;
import com.fastbee.pay.core.service.order.PayOrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import static com.fastbee.common.core.domain.CommonResult.success;
import static com.fastbee.common.utils.ServletUtils.getClientIP;
import static com.fastbee.common.utils.collection.CollectionUtils.convertList;
@Api(tags = "管理后台 - 支付订单")
@RestController
@RequestMapping("/pay/order")
@Validated
public class PayOrderController {
@Resource
private PayOrderService orderService;
@Resource
private PayAppService appService;
@GetMapping("/get")
@ApiOperation("获得支付订单")
@ApiParam(name = "id", value = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('pay:order:query')")
public CommonResult<PayOrderRespVO> getOrder(@RequestParam("id") Long id) {
return success(PayOrderConvert.INSTANCE.convert(orderService.getOrder(id)));
}
@GetMapping("/get-detail")
@ApiOperation("获得支付订单详情")
@ApiParam(name = "id", value = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('pay:order:query')")
public CommonResult<PayOrderDetailsRespVO> getOrderDetail(@RequestParam("id") Long id) {
PayOrder order = orderService.getOrder(id);
if (order == null) {
return success(null);
}
// 拼接返回
PayApp app = appService.getApp(order.getAppId());
PayOrderExtension orderExtension = orderService.getOrderExtension(order.getExtensionId());
return success(PayOrderConvert.INSTANCE.convert(order, orderExtension, app));
}
@PostMapping("/submit")
@ApiOperation("提交支付订单")
public CommonResult<PayOrderSubmitRespVO> submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) {
PayOrderSubmitRespVO respVO = orderService.submitOrder(reqVO, getClientIP());
return success(respVO);
}
@GetMapping("/page")
@ApiOperation("获得支付订单分页")
@PreAuthorize("@ss.hasPermission('pay:order:query')")
public CommonResult<PageResult<PayOrderPageItemRespVO>> getOrderPage(@Valid PayOrderPageReqVO pageVO) {
PageResult<PayOrder> pageResult = orderService.getOrderPage(pageVO);
if (CollectionUtil.isEmpty(pageResult.getList())) {
return success(new PageResult<>(pageResult.getTotal()));
}
// 拼接返回
Map<Long, PayApp> appMap = appService.getAppMap(convertList(pageResult.getList(), PayOrder::getAppId));
return success(PayOrderConvert.INSTANCE.convertPage(pageResult, appMap));
}
@GetMapping("/export-excel")
@ApiOperation("导出支付订单 Excel")
@PreAuthorize("@ss.hasPermission('pay:order:export')")
// @OperateLog(type = EXPORT)
public void exportOrderExcel(@Valid PayOrderExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
List<PayOrder> list = orderService.getOrderList(exportReqVO);
if (CollectionUtil.isEmpty(list)) {
// ExcelUtils.write(response, "支付订单.xls", "数据",
// PayOrderExcelVO.class, new ArrayList<>());
return;
}
// 拼接返回
Map<Long, PayApp> appMap = appService.getAppMap(convertList(list, PayOrder::getAppId));
List<PayOrderExcelVO> excelList = PayOrderConvert.INSTANCE.convertList(list, appMap);
// 导出 Excel
// ExcelUtils.write(response, "支付订单.xls", "数据", PayOrderExcelVO.class, excelList);
}
}

View File

@ -0,0 +1,90 @@
package com.fastbee.pay.core.controller.admin.order.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import static com.fastbee.common.utils.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
/**
* 支付订单 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*
* @author aquan
*/
@Data
public class PayOrderBaseVO {
@ApiModelProperty(value = "应用编号", required = true, example = "1024")
@NotNull(message = "应用编号不能为空")
private Long appId;
@ApiModelProperty(value = "渠道编号", example = "2048")
private Long channelId;
@ApiModelProperty(value = "渠道编码", example = "wx_app")
private String channelCode;
@ApiModelProperty(value = "商户订单编号", required = true, example = "888")
@NotNull(message = "商户订单编号不能为空")
private String merchantOrderId;
@ApiModelProperty(value = "商品标题", required = true, example = "土豆")
@NotNull(message = "商品标题不能为空")
private String subject;
@ApiModelProperty(value = "商品描述", required = true, example = "我是土豆")
@NotNull(message = "商品描述不能为空")
private String body;
@ApiModelProperty(value = "异步通知地址", required = true, example = "http://127.0.0.1:48080/pay/notify")
@NotNull(message = "异步通知地址不能为空")
private String notifyUrl;
@ApiModelProperty(value = "支付金额,单位:分", required = true, example = "10")
@NotNull(message = "支付金额,单位:分不能为空")
private Long price;
@ApiModelProperty(value = "渠道手续费,单位:百分比", example = "10")
private Double channelFeeRate;
@ApiModelProperty(value = "渠道手续金额,单位:分", example = "100")
private Integer channelFeePrice;
@ApiModelProperty(value = "支付状态", required = true, example = "1")
@NotNull(message = "支付状态不能为空")
private Integer status;
@ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1")
@NotNull(message = "用户 IP不能为空")
private String userIp;
@ApiModelProperty(value = "订单失效时间", required = true)
@NotNull(message = "订单失效时间不能为空")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime expireTime;
@ApiModelProperty(value = "订单支付成功时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime successTime;
@ApiModelProperty(value = "支付成功的订单拓展单编号", example = "50")
private Long extensionId;
@ApiModelProperty(value = "支付订单号", example = "2048888")
private String no;
@ApiModelProperty(value = "退款总金额,单位:分", required = true, example = "10")
@NotNull(message = "退款总金额,单位:分不能为空")
private Long refundPrice;
@ApiModelProperty(value = "渠道用户编号", example = "2048")
private String channelUserId;
@ApiModelProperty(value = "渠道订单号", example = "4096")
private String channelOrderNo;
}

View File

@ -0,0 +1,46 @@
package com.fastbee.pay.core.controller.admin.order.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@ApiModel(description = "管理后台 - 支付订单详细信息 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayOrderDetailsRespVO extends PayOrderBaseVO {
@ApiModelProperty(value = "支付订单编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "应用名称", required = true, example = "fastbee")
private String appName;
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
@ApiModelProperty(value = "更新时间", required = true)
private LocalDateTime updateTime;
/**
* 支付订单扩展
*/
private PayOrderExtension extension;
@Data
@ApiModel(description = "支付订单扩展")
public static class PayOrderExtension {
@ApiModelProperty(value = "支付订单号", required = true, example = "1024")
private String no;
@ApiModelProperty(value = "支付异步通知的内容")
private String channelNotifyData;
}
}

View File

@ -0,0 +1,62 @@
package com.fastbee.pay.core.controller.admin.order.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import com.fastbee.pay.api.enums.DictTypeConstants;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 支付订单 Excel VO
*
* @author aquan
*/
@Data
public class PayOrderExcelVO {
@ExcelProperty("编号")
private Long id;
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@ExcelProperty(value = "支付金额")
private Integer price;
@ExcelProperty(value = "退款金额")
private Integer refundPrice;
@ExcelProperty(value = "手续金额")
private Integer channelFeePrice;
@ExcelProperty("商户单号")
private String merchantOrderId;
@ExcelProperty(value = "支付单号")
private String no;
@ExcelProperty("渠道单号")
private String channelOrderNo;
@ExcelProperty(value = "支付状态")
private Integer status;
@ExcelProperty(value = "渠道编号名称")
private String channelCode;
@ExcelProperty("订单支付成功时间")
private LocalDateTime successTime;
@ExcelProperty("订单失效时间")
private LocalDateTime expireTime;
@ExcelProperty(value = "应用名称")
private String appName;
@ExcelProperty("商品标题")
private String subject;
@ExcelProperty("商品描述")
private String body;
}

View File

@ -0,0 +1,39 @@
package com.fastbee.pay.core.controller.admin.order.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.fastbee.common.utils.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(description = "管理后台 - 支付订单 Excel 导出 Request VO参数和 PayOrderPageReqVO 是一致的")
@Data
public class PayOrderExportReqVO {
@ApiModelProperty(value = "应用编号", example = "1024")
private Long appId;
@ApiModelProperty(value = "渠道编码", example = "wx_app")
private String channelCode;
@ApiModelProperty(value = "商户订单编号", example = "4096")
private String merchantOrderId;
@ApiModelProperty(value = "渠道编号", example = "1888")
private String channelOrderNo;
@ApiModelProperty(value = "支付单号", example = "2014888")
private String no;
@ApiModelProperty(value = "支付状态", example = "0")
private Integer status;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "创建时间")
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,26 @@
package com.fastbee.pay.core.controller.admin.order.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@ApiModel(description = "管理后台 - 支付订单分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayOrderPageItemRespVO extends PayOrderBaseVO {
@ApiModelProperty(value = "支付订单编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
@ApiModelProperty(value = "应用名称", example = "wx_pay")
private String appName;
}

View File

@ -0,0 +1,44 @@
package com.fastbee.pay.core.controller.admin.order.vo;
import com.fastbee.common.core.domain.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.fastbee.common.utils.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(description = "管理后台 - 支付订单分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayOrderPageReqVO extends PageParam {
@ApiModelProperty(value = "应用编号", example = "1024")
private Long appId;
@ApiModelProperty(value = "渠道编码", example = "wx_app")
private String channelCode;
@ApiModelProperty(value = "商户订单编号", example = "4096")
private String merchantOrderId;
@ApiModelProperty(value = "渠道编号", example = "1888")
private String channelOrderNo;
@ApiModelProperty(value = "支付单号", example = "2014888")
private String no;
@ApiModelProperty(value = "支付状态", example = "0")
private Integer status;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "创建时间")
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,23 @@
package com.fastbee.pay.core.controller.admin.order.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@ApiModel(description = "管理后台 - 支付订单 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayOrderRespVO extends PayOrderBaseVO {
@ApiModelProperty(value = "支付订单编号", required = true)
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
}

View File

@ -0,0 +1,34 @@
package com.fastbee.pay.core.controller.admin.order.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Map;
@ApiModel(description = "管理后台 - 支付订单提交 Request VO")
@Data
public class PayOrderSubmitReqVO {
@ApiModelProperty(value = "支付单编号", required = true, example = "1024")
@NotNull(message = "支付单编号不能为空")
private Long id;
@ApiModelProperty(value = "支付渠道", required = true, example = "wx_pub")
@NotEmpty(message = "支付渠道不能为空")
private String channelCode;
@ApiModelProperty(value = "支付渠道的额外参数,例如说,微信公众号需要传递 openid 参数")
private Map<String, String> channelExtras;
@ApiModelProperty(value = "展示模式", example = "url") // 参见 {@link PayDisplayModeEnum} 枚举。如果不传递,则每个支付渠道使用默认的方式
private String displayMode;
@ApiModelProperty(value = "回跳地址")
@URL(message = "回跳地址的格式必须是 URL")
private String returnUrl;
}

View File

@ -0,0 +1,21 @@
package com.fastbee.pay.core.controller.admin.order.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ApiModel(description = "管理后台 - 支付订单提交 Response VO")
@Data
public class PayOrderSubmitRespVO {
@ApiModelProperty(value = "支付状态", required = true, example = "10") // 参见 PayOrderStatusEnum 枚举
private Integer status;
@ApiModelProperty(value = "展示模式", required = true, example = "url") // 参见 PayDisplayModeEnum 枚举
private String displayMode;
@ApiModelProperty(value = "展示内容", required = true)
private String displayContent;
}

View File

@ -0,0 +1,94 @@
package com.fastbee.pay.core.controller.admin.refund;
import cn.hutool.core.collection.CollectionUtil;
import com.fastbee.common.core.domain.CommonResult;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.pay.core.controller.admin.refund.vo.*;
import com.fastbee.pay.core.convert.refund.PayRefundConvert;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.refund.PayRefund;
import com.fastbee.pay.core.service.app.PayAppService;
import com.fastbee.pay.core.service.refund.PayRefundService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import static com.fastbee.common.core.domain.CommonResult.success;
import static com.fastbee.common.utils.collection.CollectionUtils.convertList;
@Api(tags = "管理后台 - 退款订单")
@RestController
@RequestMapping("/pay/refund")
@Validated
public class PayRefundController {
@Resource
private PayRefundService refundService;
@Resource
private PayAppService appService;
@GetMapping("/get")
@ApiOperation("获得退款订单")
@ApiParam(name = "id", value = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('pay:refund:query')")
public CommonResult<PayRefundDetailsRespVO> getRefund(@RequestParam("id") Long id) {
PayRefund refund = refundService.getRefund(id);
if (refund == null) {
return success(new PayRefundDetailsRespVO());
}
// 拼接数据
PayApp app = appService.getApp(refund.getAppId());
return success(PayRefundConvert.INSTANCE.convert(refund, app));
}
@GetMapping("/page")
@ApiModelProperty("获得退款订单分页")
@PreAuthorize("@ss.hasPermission('pay:refund:query')")
public CommonResult<PageResult<PayRefundPageItemRespVO>> getRefundPage(@Valid PayRefundPageReqVO pageVO) {
PageResult<PayRefund> pageResult = refundService.getRefundPage(pageVO);
if (CollectionUtil.isEmpty(pageResult.getList())) {
return success(new PageResult<>(pageResult.getTotal()));
}
// 处理应用ID数据
Map<Long, PayApp> appMap = appService.getAppMap(convertList(pageResult.getList(), PayRefund::getAppId));
return success(PayRefundConvert.INSTANCE.convertPage(pageResult, appMap));
}
@GetMapping("/export-excel")
@ApiModelProperty("导出退款订单 Excel")
@PreAuthorize("@ss.hasPermission('pay:refund:export')")
// @OperateLog(type = EXPORT)
public void exportRefundExcel(@Valid PayRefundExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
List<PayRefund> list = refundService.getRefundList(exportReqVO);
if (CollectionUtil.isEmpty(list)) {
// ExcelUtils.write(response, "退款订单.xls", "数据",
// PayRefundExcelVO.class, new ArrayList<>());
return;
}
// 拼接返回
Map<Long, PayApp> appMap = appService.getAppMap(convertList(list, PayRefund::getAppId));
List<PayRefundExcelVO> excelList = PayRefundConvert.INSTANCE.convertList(list, appMap);
// 导出 Excel
// ExcelUtils.write(response, "退款订单.xls", "数据", PayRefundExcelVO.class, excelList);
}
}

View File

@ -0,0 +1,78 @@
package com.fastbee.pay.core.controller.admin.refund.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 退款订单 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class PayRefundBaseVO {
@ApiModelProperty(value = "外部退款号", required = true, example = "110")
private String no;
@ApiModelProperty(value = "应用编号", required = true, example = "1024")
private Long appId;
@ApiModelProperty(value = "渠道编号", required = true, example = "2048")
private Long channelId;
@ApiModelProperty(value = "渠道编码", required = true, example = "wx_app")
private String channelCode;
@ApiModelProperty(value = "订单编号", required = true, example = "1024")
private Long orderId;
// ========== 商户相关字段 ==========
@ApiModelProperty(value = "商户订单编号", required = true, example = "225")
private String merchantOrderId;
@ApiModelProperty(value = "商户退款订单号", required = true, example = "512")
private String merchantRefundId;
@ApiModelProperty(value = "异步通知地址", required = true)
private String notifyUrl;
// ========== 退款相关字段 ==========
@ApiModelProperty(value = "退款状态", required = true, example = "0")
private Integer status;
@ApiModelProperty(value = "支付金额", required = true, example = "100")
private Long payPrice;
@ApiModelProperty(value = "退款金额,单位分", required = true, example = "200")
private Long refundPrice;
@ApiModelProperty(value = "退款原因", required = true, example = "我要退了")
private String reason;
@ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1")
private String userIp;
// ========== 渠道相关字段 ==========
@ApiModelProperty(value = "渠道订单号", required = true, example = "233")
private String channelOrderNo;
@ApiModelProperty(value = "渠道退款单号", example = "2022")
private String channelRefundNo;
@ApiModelProperty(value = "退款成功时间")
private LocalDateTime successTime;
@ApiModelProperty(value = "调用渠道的错误码")
private String channelErrorCode;
@ApiModelProperty(value = "调用渠道的错误提示")
private String channelErrorMsg;
@ApiModelProperty(value = "支付渠道的额外参数")
private String channelNotifyData;
}

View File

@ -0,0 +1,41 @@
package com.fastbee.pay.core.controller.admin.refund.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@ApiModel(description = "管理后台 - 退款订单详情 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundDetailsRespVO extends PayRefundBaseVO {
@ApiModelProperty(value = "支付退款编号", required = true)
private Long id;
@ApiModelProperty(value = "应用名称", required = true, example = "我是fastbee")
private String appName;
@ApiModelProperty(value = "支付订单", required = true)
private Order order;
@ApiModelProperty(value = "创建时间")
private LocalDateTime createTime;
@ApiModelProperty(value = "更新时间")
private LocalDateTime updateTime;
@ApiModel(value = "管理后台 - 支付订单")
@Data
public static class Order {
@ApiModelProperty(value = "商品标题", required = true, example = "土豆")
private String subject;
}
}

View File

@ -0,0 +1,59 @@
package com.fastbee.pay.core.controller.admin.refund.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import com.fastbee.common.annotation.DictFormat;
import com.fastbee.pay.api.enums.DictTypeConstants;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 退款订单 Excel VO
*
* @author aquan
*/
@Data
public class PayRefundExcelVO {
@ExcelProperty("支付退款编号")
private Long id;
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@ExcelProperty(value = "支付金额")
private Integer payPrice;
@ExcelProperty(value = "退款金额")
private Integer refundPrice;
@ExcelProperty("商户退款单号")
private String merchantRefundId;
@ExcelProperty("退款单号")
private String no;
@ExcelProperty("渠道退款单号")
private String channelRefundNo;
@ExcelProperty("商户支付单号")
private String merchantOrderId;
@ExcelProperty("渠道支付单号")
private String channelOrderNo;
@ExcelProperty(value = "退款状态")
// @DictFormat(DictTypeConstants.REFUND_STATUS)
private Integer status;
@ExcelProperty(value = "退款渠道")
// @DictFormat(DictTypeConstants.CHANNEL_CODE)
private String channelCode;
@ExcelProperty("成功时间")
private LocalDateTime successTime;
@ExcelProperty(value = "支付应用")
private String appName;
@ExcelProperty("退款原因")
private String reason;
}

View File

@ -0,0 +1,42 @@
package com.fastbee.pay.core.controller.admin.refund.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.fastbee.common.utils.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(description = "管理后台 - 退款订单 Excel 导出 Request VO参数和 PayRefundPageReqVO 是一致的")
@Data
public class PayRefundExportReqVO {
@ApiModelProperty(value = "应用编号", example = "1024")
private Long appId;
@ApiModelProperty(value = "渠道编码", example = "wx_app")
private String channelCode;
@ApiModelProperty(value = "商户支付单号", example = "10")
private String merchantOrderId;
@ApiModelProperty(value = "商户退款单号", example = "20")
private String merchantRefundId;
@ApiModelProperty(value = "渠道支付单号", example = "30")
private String channelOrderNo;
@ApiModelProperty(value = "渠道退款单号", example = "40")
private String channelRefundNo;
@ApiModelProperty(value = "退款状态", example = "0")
private Integer status;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "创建时间")
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,27 @@
package com.fastbee.pay.core.controller.admin.refund.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@ApiModel(description = "管理后台 - 退款订单分页查询 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundPageItemRespVO extends PayRefundBaseVO {
@ApiModelProperty(value = "支付订单编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "应用名称", required = true, example = "我是fastbee")
private String appName;
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
}

View File

@ -0,0 +1,47 @@
package com.fastbee.pay.core.controller.admin.refund.vo;
import com.fastbee.common.core.domain.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.fastbee.common.utils.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(description = "管理后台 - 退款订单分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundPageReqVO extends PageParam {
@ApiModelProperty(value = "应用编号", example = "1024")
private Long appId;
@ApiModelProperty(value = "渠道编码", example = "wx_app")
private String channelCode;
@ApiModelProperty(value = "商户支付单号", example = "10")
private String merchantOrderId;
@ApiModelProperty(value = "商户退款单号", example = "20")
private String merchantRefundId;
@ApiModelProperty(value = "渠道支付单号", example = "30")
private String channelOrderNo;
@ApiModelProperty(value = "渠道退款单号", example = "40")
private String channelRefundNo;
@ApiModelProperty(value = "退款状态", example = "0")
private Integer status;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "创建时间")
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,40 @@
package com.fastbee.pay.core.controller.app.channel;
import com.fastbee.common.core.domain.CommonResult;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import com.fastbee.pay.core.service.channel.PayChannelService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
import java.util.Set;
import static com.fastbee.common.core.domain.CommonResult.success;
import static com.fastbee.common.utils.collection.CollectionUtils.convertSet;
@Api(tags = "用户 App - 支付渠道")
@RestController
@RequestMapping("/pay/channel")
@Validated
public class AppPayChannelController {
@Resource
private PayChannelService channelService;
@GetMapping("/get-enable-code-list")
@ApiOperation("获得指定应用的开启的支付渠道编码列表")
@ApiParam(name = "appId", value = "应用编号", required = true, example = "1")
public CommonResult<Set<String>> getEnableChannelCodeList(@RequestParam("appId") Long appId) {
List<PayChannel> channels = channelService.getEnableChannelList(appId);
return success(convertSet(channels, PayChannel::getCode));
}
}

View File

@ -0,0 +1,63 @@
### /pay/create 提交支付订单【alipay_pc】
POST {{appApi}}/pay/order/submit
Content-Type: application/json
Authorization: Bearer {{appToken}}
tenant-id: {{appTenentId}}
{
"id": 174,
"channelCode": "alipay_pc"
}
### /pay/create 提交支付订单【wx_bar】
POST {{appApi}}/pay/order/submit
Content-Type: application/json
Authorization: Bearer {{appToken}}
tenant-id: {{appTenentId}}
{
"id": 202,
"channelCode": "wx_bar",
"channelExtras": {
"authCode": "134042110834344848"
}
}
### /pay/create 提交支付订单【wx_pub】
POST {{appApi}}/pay/order/submit
Content-Type: application/json
Authorization: Bearer {{appToken}}
tenant-id: {{appTenentId}}
{
"id": 202,
"channelCode": "wx_pub",
"channelExtras": {
"openid": "ockUAwIZ-0OeMZl9ogcZ4ILrGba0"
}
}
### /pay/create 提交支付订单【wx_lite】
POST {{appApi}}/pay/order/submit
Content-Type: application/json
Authorization: Bearer {{appToken}}
tenant-id: {{appTenentId}}
{
"id": 202,
"channelCode": "wx_lite",
"channelExtras": {
"openid": "oLefc4g5GjKWHJjLjMSXB3wX0fD0"
}
}
### /pay/create 提交支付订单【wx_native】
POST {{appApi}}/pay/order/submit
Content-Type: application/json
Authorization: Bearer {{appToken}}
tenant-id: {{appTenentId}}
{
"id": 202,
"channelCode": "wx_native"
}

View File

@ -0,0 +1,48 @@
package com.fastbee.pay.core.controller.app.order;
import com.fastbee.common.core.domain.CommonResult;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderRespVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitRespVO;
import com.fastbee.pay.core.controller.app.order.vo.AppPayOrderSubmitReqVO;
import com.fastbee.pay.core.controller.app.order.vo.AppPayOrderSubmitRespVO;
import com.fastbee.pay.core.convert.order.PayOrderConvert;
import com.fastbee.pay.core.service.order.PayOrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import static com.fastbee.common.core.domain.CommonResult.success;
import static com.fastbee.common.utils.ServletUtils.getClientIP;
@Api(tags = "用户 APP - 支付订单")
@RestController
@RequestMapping("/pay/order")
@Validated
@Slf4j
public class AppPayOrderController {
@Resource
private PayOrderService payOrderService;
// TODO fastbee临时 demo技术打样。
@GetMapping("/get")
@ApiOperation("获得支付订单")
@ApiParam(name = "id", value = "编号", required = true, example = "1024")
public CommonResult<PayOrderRespVO> getOrder(@RequestParam("id") Long id) {
return success(PayOrderConvert.INSTANCE.convert(payOrderService.getOrder(id)));
}
@PostMapping("/submit")
@ApiOperation("提交支付订单")
public CommonResult<AppPayOrderSubmitRespVO> submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) {
PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
return success(PayOrderConvert.INSTANCE.convert3(respVO));
}
}

View File

@ -0,0 +1,10 @@
package com.fastbee.pay.core.controller.app.order.vo;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitReqVO;
import io.swagger.annotations.ApiModel;
import lombok.Data;
@ApiModel(description = "用户 APP - 支付订单提交 Request VO")
@Data
public class AppPayOrderSubmitReqVO extends PayOrderSubmitReqVO {
}

View File

@ -0,0 +1,11 @@
package com.fastbee.pay.core.controller.app.order.vo;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitRespVO;
import io.swagger.annotations.ApiModel;
import lombok.Data;
@ApiModel(description = "用户 APP - 支付订单提交 Response VO")
@Data
public class AppPayOrderSubmitRespVO extends PayOrderSubmitRespVO {
}

View File

@ -0,0 +1,4 @@
/**
* TODO fastbee占个位置没啥用
*/
package com.fastbee.pay.core.controller.app.refund;

View File

@ -0,0 +1,6 @@
/**
* 提供 RESTful API 给前端:
* 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目
* 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分
*/
package com.fastbee.pay.core.controller;

View File

@ -0,0 +1,48 @@
package com.fastbee.pay.core.convert.app;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.common.utils.collection.CollectionUtils;
import com.fastbee.pay.core.controller.admin.app.vo.PayAppCreateReqVO;
import com.fastbee.pay.core.controller.admin.app.vo.PayAppPageItemRespVO;
import com.fastbee.pay.core.controller.admin.app.vo.PayAppRespVO;
import com.fastbee.pay.core.controller.admin.app.vo.PayAppUpdateReqVO;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 支付应用信息 Convert
*
* @author fastbee
*/
@Mapper
public interface PayAppConvert {
PayAppConvert INSTANCE = Mappers.getMapper(PayAppConvert.class);
PayAppPageItemRespVO pageConvert (PayApp bean);
PayApp convert(PayAppCreateReqVO bean);
PayApp convert(PayAppUpdateReqVO bean);
PayAppRespVO convert(PayApp bean);
List<PayAppRespVO> convertList(List<PayApp> list);
PageResult<PayAppPageItemRespVO> convertPage(PageResult<PayApp> page);
default PageResult<PayAppPageItemRespVO> convertPage(PageResult<PayApp> pageResult, List<PayChannel> channels) {
PageResult<PayAppPageItemRespVO> voPageResult = convertPage(pageResult);
// 处理 channel 关系
Map<Long, Set<String>> appIdChannelMap = CollectionUtils.convertMultiMap2(channels, PayChannel::getAppId, PayChannel::getCode);
voPageResult.getList().forEach(app -> app.setChannelCodes(appIdChannelMap.get(app.getId())));
return voPageResult;
}
}

View File

@ -0,0 +1,28 @@
package com.fastbee.pay.core.convert.channel;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.pay.core.controller.admin.channel.vo.PayChannelCreateReqVO;
import com.fastbee.pay.core.controller.admin.channel.vo.PayChannelRespVO;
import com.fastbee.pay.core.controller.admin.channel.vo.PayChannelUpdateReqVO;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface PayChannelConvert {
PayChannelConvert INSTANCE = Mappers.getMapper(PayChannelConvert.class);
@Mapping(target = "config",ignore = true)
PayChannel convert(PayChannelCreateReqVO bean);
@Mapping(target = "config",ignore = true)
PayChannel convert(PayChannelUpdateReqVO bean);
// @Mapping(target = "config",expression = "java(cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString(bean.getConfig()))")
// PayChannelRespVO convert(PayChannel bean);
// PageResult<PayChannelRespVO> convertPage(PageResult<PayChannel> page);
}

View File

@ -0,0 +1,26 @@
package com.fastbee.pay.core.convert.demo;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.pay.core.controller.admin.demo.vo.OrderInfoCreateReqVO;
import com.fastbee.pay.core.controller.admin.demo.vo.OrderInfoRespVO;
import com.fastbee.pay.core.domain.dataobject.demo.OrderInfo;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* 示例订单 Convert
*
* @author fastbee
*/
@Mapper
public interface OrderInfoConvert {
OrderInfoConvert INSTANCE = Mappers.getMapper(OrderInfoConvert.class);
OrderInfo convert(OrderInfoCreateReqVO bean);
OrderInfoRespVO convert(OrderInfo bean);
PageResult<OrderInfoRespVO> convertPage(PageResult<OrderInfo> page);
}

View File

@ -0,0 +1,43 @@
package com.fastbee.pay.core.convert.notify;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.common.utils.MapUtils;
import com.fastbee.pay.core.controller.admin.notify.vo.PayNotifyTaskDetailRespVO;
import com.fastbee.pay.core.controller.admin.notify.vo.PayNotifyTaskRespVO;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.notify.PayNotifyLog;
import com.fastbee.pay.core.domain.dataobject.notify.PayNotifyTask;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.Map;
/**
* 支付通知 Convert
*
* @author fastbee
*/
@Mapper
public interface PayNotifyTaskConvert {
PayNotifyTaskConvert INSTANCE = Mappers.getMapper(PayNotifyTaskConvert.class);
PayNotifyTaskRespVO convert(PayNotifyTask bean);
default PageResult<PayNotifyTaskRespVO> convertPage(PageResult<PayNotifyTask> page, Map<Long, PayApp> 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<PayNotifyTask> page);
default PayNotifyTaskDetailRespVO convert(PayNotifyTask task, PayApp app, List<PayNotifyLog> logs) {
PayNotifyTaskDetailRespVO respVO = convert(task, logs);
if (app != null) {
respVO.setAppName(app.getName());
}
return respVO;
}
PayNotifyTaskDetailRespVO convert(PayNotifyTask task, List<PayNotifyLog> logs);
}

View File

@ -0,0 +1,79 @@
package com.fastbee.pay.core.convert.order;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.common.utils.MapUtils;
import com.fastbee.common.utils.collection.CollectionUtils;
import com.fastbee.pay.api.api.order.dto.PayOrderCreateReqDTO;
import com.fastbee.pay.api.api.order.dto.PayOrderRespDTO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderDetailsRespVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderExcelVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderPageItemRespVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderRespVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitReqVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitRespVO;
import com.fastbee.pay.core.controller.app.order.vo.AppPayOrderSubmitRespVO;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.order.PayOrder;
import com.fastbee.pay.core.domain.dataobject.order.PayOrderExtension;
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.Map;
/**
* 支付订单 Convert
*
* @author aquan
*/
@Mapper
public interface PayOrderConvert {
PayOrderConvert INSTANCE = Mappers.getMapper(PayOrderConvert.class);
PayOrderRespVO convert(PayOrder bean);
PayOrderRespDTO convert2(PayOrder order);
default PayOrderDetailsRespVO convert(PayOrder order, PayOrderExtension orderExtension, PayApp app) {
PayOrderDetailsRespVO respVO = convertDetail(order);
respVO.setExtension(convert(orderExtension));
if (app != null) {
respVO.setAppName(app.getName());
}
return respVO;
}
PayOrderDetailsRespVO convertDetail(PayOrder bean);
PayOrderDetailsRespVO.PayOrderExtension convert(PayOrderExtension bean);
default PageResult<PayOrderPageItemRespVO> convertPage(PageResult<PayOrder> page, Map<Long, PayApp> appMap) {
PageResult<PayOrderPageItemRespVO> result = convertPage(page);
result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName())));
return result;
}
PageResult<PayOrderPageItemRespVO> convertPage(PageResult<PayOrder> page);
default List<PayOrderExcelVO> convertList(List<PayOrder> list, Map<Long, PayApp> appMap) {
return CollectionUtils.convertList(list, order -> {
PayOrderExcelVO excelVO = convertExcel(order);
MapUtils.findAndThen(appMap, order.getAppId(), app -> excelVO.setAppName(app.getName()));
return excelVO;
});
}
PayOrderExcelVO convertExcel(PayOrder bean);
PayOrder convert(PayOrderCreateReqDTO bean);
@Mapping(target = "id", ignore = true)
PayOrderExtension convert(PayOrderSubmitReqVO bean, String userIp);
PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqVO reqVO, String userIp);
@Mapping(source = "order.status", target = "status")
PayOrderSubmitRespVO convert(PayOrder order, com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO respDTO);
AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean);
}

View File

@ -0,0 +1,5 @@
/**
* 提供 POJO 类的实体转换
* 目前使用 MapStruct 框架
*/
package com.fastbee.pay.core.convert;

View File

@ -0,0 +1,56 @@
package com.fastbee.pay.core.convert.refund;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.common.utils.MapUtils;
import com.fastbee.common.utils.collection.CollectionUtils;
import com.fastbee.pay.api.api.refund.dto.PayRefundCreateReqDTO;
import com.fastbee.pay.api.api.refund.dto.PayRefundRespDTO;
import com.fastbee.pay.core.controller.admin.refund.vo.PayRefundDetailsRespVO;
import com.fastbee.pay.core.controller.admin.refund.vo.PayRefundExcelVO;
import com.fastbee.pay.core.controller.admin.refund.vo.PayRefundPageItemRespVO;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.order.PayOrder;
import com.fastbee.pay.core.domain.dataobject.refund.PayRefund;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.Map;
@Mapper
public interface PayRefundConvert {
PayRefundConvert INSTANCE = Mappers.getMapper(PayRefundConvert.class);
default PayRefundDetailsRespVO convert(PayRefund refund, PayApp app) {
PayRefundDetailsRespVO respVO = convert(refund);
if (app != null) {
respVO.setAppName(app.getName());
}
return respVO;
}
PayRefundDetailsRespVO convert(PayRefund bean);
PayRefundDetailsRespVO.Order convert(PayOrder bean);
default PageResult<PayRefundPageItemRespVO> convertPage(PageResult<PayRefund> page, Map<Long, PayApp> appMap) {
PageResult<PayRefundPageItemRespVO> result = convertPage(page);
result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName())));
return result;
}
PageResult<PayRefundPageItemRespVO> convertPage(PageResult<PayRefund> page);
PayRefund convert(PayRefundCreateReqDTO bean);
PayRefundRespDTO convert02(PayRefund bean);
default List<PayRefundExcelVO> convertList(List<PayRefund> list, Map<Long, PayApp> appMap) {
return CollectionUtils.convertList(list, order -> {
PayRefundExcelVO excelVO = convertExcel(order);
MapUtils.findAndThen(appMap, order.getAppId(), app -> excelVO.setAppName(app.getName()));
return excelVO;
});
}
PayRefundExcelVO convertExcel(PayRefund bean);
}

View File

@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/MapStruct/>

View File

@ -0,0 +1,56 @@
package com.fastbee.pay.core.domain.dataobject.app;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fastbee.common.core.domain.BaseDO;
import com.fastbee.common.enums.CommonStatusEnum;
import lombok.*;
import lombok.experimental.Accessors;
/**
* 支付应用 DO
* 一个商户下,可能会有多个支付应用。例如说,京东有京东商城、京东到家等等
* 不过一般来说,一个商户,只有一个应用哈~
* 即 PayMerchantDO : PayApp = 1 : n
*
* @author fastbee
*/
@TableName("pay_app")
//@KeySequence("pay_app_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class PayApp extends BaseDO {
/**
* 应用编号,数据库自增
*/
@TableId
private Long id;
/**
* 应用名
*/
private String name;
/**
* 状态
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 支付结果的回调地址
*/
private String orderNotifyUrl;
/**
* 退款结果的回调地址
*/
private String refundNotifyUrl;
}

View File

@ -0,0 +1,70 @@
package com.fastbee.pay.core.domain.dataobject.channel;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fastbee.common.core.domain.TenantBaseDO;
import com.fastbee.common.enums.CommonStatusEnum;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.framework.client.PayClientConfig;
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
/**
* 支付渠道 DO
* 一个应用下,会有多种支付渠道,例如说微信支付、支付宝支付等等
* 即 PayApp : PayChannel = 1 : n
*
* @author fastbee
*/
@TableName(value = "pay_channel", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class PayChannel extends TenantBaseDO {
/**
* 渠道编号,数据库自增
*/
private Long id;
/**
* 渠道编码
* 枚举 {@link PayChannelEnum}
*/
private String code;
/**
* 状态
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 渠道费率,单位:百分比
*/
private Double feeRate;
/**
* 备注
*/
private String remark;
/**
* 应用编号
* 关联 {@link PayApp#getId()}
*/
private Long appId;
/**
* 支付渠道配置
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private PayClientConfig config;
}

View File

@ -0,0 +1,87 @@
package com.fastbee.pay.core.domain.dataobject.demo;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fastbee.common.core.domain.BaseDO;
import lombok.*;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 订单信息, 实际根据自己的业务系统设计
* 用id关联 pay 系统的支付与退款
*
* @author fastbee
*/
@TableName("order_info")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class OrderInfo extends BaseDO {
/**
* 订单编号,自增
*/
@TableId
private Long id;
/**
* 用户编号
*/
private Long userId;
/**
* 商户显示订单编号
*/
private String orderNo;
/**
* 商品编号
*/
private Long spuId;
/**
* 商品名称
*/
private String spuName;
/**
* 价格,单位:分
*/
private Integer price;
// ========== 支付相关字段 ==========
/**
* 是否支付
*/
private Boolean payStatus;
/**
* 支付订单编号
* 对接 pay-module-biz 支付服务的支付订单编号,即 PayOrder 的 id 编号
*/
private Long payOrderId;
/**
* 付款时间
*/
private LocalDateTime payTime;
/**
* 支付渠道
* 对应 PayChannelEnum 枚举
*/
private String payChannelCode;
// ========== 退款相关字段 ==========
/**
* 支付退款单号
*/
private Long payRefundId;
/**
* 退款金额,单位:分
*/
private Integer refundPrice;
/**
* 退款完成时间
*/
private LocalDateTime refundTime;
}

View File

@ -0,0 +1,48 @@
package com.fastbee.pay.core.domain.dataobject.notify;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fastbee.common.core.domain.BaseDO;
import com.fastbee.pay.api.enums.notify.PayNotifyStatusEnum;
import lombok.*;
/**
* 商户支付、退款等的通知 Log
* 每次通知时,都会在该表中,记录一次 Log方便排查问题
*
* @author fastbee
*/
@TableName("pay_notify_log")
@KeySequence("pay_notify_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayNotifyLog extends BaseDO {
/**
* 日志编号,自增
*/
private Long id;
/**
* 通知任务编号
* 关联 {@link PayNotifyTask#getId()}
*/
private Long taskId;
/**
* 第几次被通知
* 对应到 {@link PayNotifyTask#getNotifyTimes()}
*/
private Integer notifyTimes;
/**
* HTTP 响应结果
*/
private String response;
/**
* 支付通知状态
* 外键 {@link PayNotifyStatusEnum}
*/
private Integer status;
}

View File

@ -0,0 +1,92 @@
package com.fastbee.pay.core.domain.dataobject.notify;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fastbee.common.core.domain.TenantBaseDO;
import com.fastbee.pay.api.enums.notify.PayNotifyStatusEnum;
import com.fastbee.pay.api.enums.notify.PayNotifyTypeEnum;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.order.PayOrder;
import com.fastbee.pay.core.domain.dataobject.refund.PayRefund;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 支付通知
* 在支付系统收到支付渠道的支付、退款的结果后,需要不断的通知到业务系统,直到成功。
*
* @author fastbee
*/
@TableName("pay_notify_task")
@KeySequence("pay_notify_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class PayNotifyTask extends TenantBaseDO {
/**
* 通知频率,单位为秒。
*
* 算上首次的通知,实际是一共 1 + 8 = 9 次。
*/
public static final Integer[] NOTIFY_FREQUENCY = new Integer[]{
15, 15, 30, 180,
1800, 1800, 1800, 3600
};
/**
* 编号,自增
*/
@TableId
private Long id;
/**
* 应用编号
* 关联 {@link PayApp#getId()}
*/
private Long appId;
/**
* 通知类型
* 外键 {@link PayNotifyTypeEnum}
*/
private Integer type;
/**
* 数据编号,根据不同 type 进行关联:
* 1. {@link PayNotifyTypeEnum#ORDER} 时,关联 {@link PayOrder#getId()}
* 2. {@link PayNotifyTypeEnum#REFUND} 时,关联 {@link PayRefund#getId()}
*/
private Long dataId;
/**
* 商户订单编号
*/
private String merchantOrderId;
/**
* 通知状态
* 外键 {@link PayNotifyStatusEnum}
*/
private Integer status;
/**
* 下一次通知时间
*/
private LocalDateTime nextNotifyTime;
/**
* 最后一次执行时间
*/
private LocalDateTime lastExecuteTime;
/**
* 当前通知次数
*/
private Integer notifyTimes;
/**
* 最大可通知次数
*/
private Integer maxNotifyTimes;
/**
* 通知地址
*/
private String notifyUrl;
}

View File

@ -0,0 +1,134 @@
package com.fastbee.pay.core.domain.dataobject.order;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fastbee.common.core.domain.BaseDO;
import com.fastbee.pay.api.enums.order.PayOrderStatusEnum;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 支付订单 DO
*
* @author fastbee
*/
@TableName("pay_order")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class PayOrder extends BaseDO {
/**
* 订单编号,数据库自增
*/
private Long id;
/**
* 应用编号
* 关联 {@link PayApp#getId()}
*/
private Long appId;
/**
* 渠道编号
* 关联 {@link PayChannel#getId()}
*/
private Long channelId;
/**
* 渠道编码
* 枚举 {@link PayChannelEnum}
*/
private String channelCode;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
* 例如说,内部系统 A 的订单号,需要保证每个 PayApp 唯一
*/
private String merchantOrderId;
/**
* 商品标题
*/
private String subject;
/**
* 商品描述信息
*/
private String body;
/**
* 异步通知地址
*/
private String notifyUrl;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
*/
private Integer price;
/**
* 渠道手续费,单位:百分比
* 冗余 {@link PayChannel#getFeeRate()}
*/
private Double channelFeeRate;
/**
* 渠道手续金额,单位:分
*/
private Integer channelFeePrice;
/**
* 支付状态
* 枚举 {@link PayOrderStatusEnum}
*/
private Integer status;
/**
* 用户 IP
*/
private String userIp;
/**
* 订单失效时间
*/
private LocalDateTime expireTime;
/**
* 订单支付成功时间
*/
private LocalDateTime successTime;
/**
* 支付成功的订单拓展单编号
* 关联 {@link PayOrderExtension#getId()}
*/
private Long extensionId;
/**
* 支付成功的外部订单号
* 关联 {@link PayOrderExtension#getNo()}
*/
private String no;
// ========== 退款相关字段 ==========
/**
* 退款总金额,单位:分
*/
private Integer refundPrice;
// ========== 渠道相关字段 ==========
/**
* 渠道用户编号
* 例如说,微信 openid、支付宝账号
*/
private String channelUserId;
/**
* 渠道订单号
*/
private String channelOrderNo;
}

View File

@ -0,0 +1,93 @@
package com.fastbee.pay.core.domain.dataobject.order;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fastbee.common.core.domain.BaseDO;
import com.fastbee.pay.api.api.order.dto.PayOrderRespDTO;
import com.fastbee.pay.api.enums.order.PayOrderStatusEnum;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.util.Map;
/**
* 支付订单拓展 DO
* 每次调用支付渠道,都会生成一条对应记录
*
* @author fastbee
*/
@TableName(value = "pay_order_extension",autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class PayOrderExtension extends BaseDO {
/**
* 订单拓展编号,数据库自增
*/
private Long id;
/**
* 外部订单号,根据规则生成
* 调用支付渠道时,使用该字段作为对接的订单号:
* 1. 微信支付:对应 <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 支付</a> 的 out_trade_no 字段
* 2. 支付宝支付:对应 <a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a> 的 out_trade_no 字段
* 例如说P202110132239124200055
*/
private String no;
/**
* 订单号
* 关联 {@link PayOrder#getId()}
*/
private Long orderId;
/**
* 渠道编号
* 关联 {@link PayChannel#getId()}
*/
private Long channelId;
/**
* 渠道编码
*/
private String channelCode;
/**
* 用户 IP
*/
private String userIp;
/**
* 支付状态
* 枚举 {@link PayOrderStatusEnum}
*/
private Integer status;
/**
* 支付渠道的额外参数
* 参见 <a href="https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html">参数说明</>
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, String> channelExtras;
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
/**
* 支付渠道的同步/异步通知的内容
* 对应 {@link PayOrderRespDTO#getRawData()}
*/
private String channelNotifyData;
}

View File

@ -0,0 +1,152 @@
package com.fastbee.pay.core.domain.dataobject.refund;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fastbee.common.core.domain.BaseDO;
import com.fastbee.pay.api.api.refund.dto.PayRefundRespDTO;
import com.fastbee.pay.api.enums.refund.PayRefundStatusEnum;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import com.fastbee.pay.core.domain.dataobject.order.PayOrder;
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 支付退款单 DO
* 一个支付订单,可以拥有多个支付退款单
* 即 PayOrder : PayRefund = 1 : n
*
* @author fastbee
*/
@TableName("pay_refund")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class PayRefund extends BaseDO {
/**
* 退款单编号,数据库自增
*/
@TableId
private Long id;
/**
* 外部退款号,根据规则生成
* 调用支付渠道时,使用该字段作为对接的退款号:
* 1. 微信退款:对应 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_4">申请退款</a> 的 out_refund_no 字段
* 2. 支付宝退款:对应 <a href="https://opendocs.alipay.com/open/02e7go"统一收单交易退款接口></a> 的 out_request_no 字段
*/
private String no;
/**
* 应用编号
* 关联 {@link PayApp#getId()}
*/
private Long appId;
/**
* 渠道编号
* 关联 {@link PayChannel#getId()}
*/
private Long channelId;
/**
* 商户编码
* 枚举 {@link PayChannelEnum}
*/
private String channelCode;
/**
* 订单编号
* 关联 {@link PayOrder#getId()}
*/
private Long orderId;
/**
* 支付订单编号
* 冗余 {@link PayOrder#getNo()}
*/
private String orderNo;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
* 例如说,内部系统 A 的订单号,需要保证每个 PayApp 唯一
*/
private String merchantOrderId;
/**
* 商户退款订单号
* 例如说,内部系统 A 的订单号,需要保证每个 PayApp 唯一
*/
private String merchantRefundId;
/**
* 异步通知地址
*/
private String notifyUrl;
// ========== 退款相关字段 ==========
/**
* 退款状态
* 枚举 {@link PayRefundStatusEnum}
*/
private Integer status;
/**
* 支付金额,单位:分
*/
private Integer payPrice;
/**
* 退款金额,单位:分
*/
private Integer refundPrice;
/**
* 退款原因
*/
private String reason;
/**
* 用户 IP
*/
private String userIp;
// ========== 渠道相关字段 ==========
/**
* 渠道订单号
* 冗余 {@link PayOrder#getChannelOrderNo()}
*/
private String channelOrderNo;
/**
* 渠道退款单号
* 1. 微信退款:对应 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_4">申请退款</a> 的 refund_id 字段
* 2. 支付宝退款:没有字段
*/
private String channelRefundNo;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道的错误提示
*/
private String channelErrorMsg;
/**
* 支付渠道的同步/异步通知的内容
* 对应 {@link PayRefundRespDTO#getRawData()}
*/
private String channelNotifyData;
}

View File

@ -0,0 +1,11 @@
package com.fastbee.pay.core.domain.mapper.app;
import com.fastbee.framework.mybatis.mapper.BaseMapperX;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayAppMapper extends BaseMapperX<PayApp> {
}

View File

@ -0,0 +1,10 @@
package com.fastbee.pay.core.domain.mapper.channel;
import com.fastbee.framework.mybatis.mapper.BaseMapperX;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayChannelMapper extends BaseMapperX<PayChannel> {
}

View File

@ -0,0 +1,15 @@
package com.fastbee.pay.core.domain.mapper.demo;
import com.fastbee.framework.mybatis.mapper.BaseMapperX;
import com.fastbee.pay.core.domain.dataobject.demo.OrderInfo;
import org.apache.ibatis.annotations.Mapper;
/**
* 示例订单 Mapper
*
* @author fastbee
*/
@Mapper
public interface OrderInfoMapper extends BaseMapperX<OrderInfo> {
}

View File

@ -0,0 +1,10 @@
package com.fastbee.pay.core.domain.mapper.notify;
import com.fastbee.framework.mybatis.mapper.BaseMapperX;
import com.fastbee.pay.core.domain.dataobject.notify.PayNotifyLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayNotifyLogMapper extends BaseMapperX<PayNotifyLog> {
}

View File

@ -0,0 +1,10 @@
package com.fastbee.pay.core.domain.mapper.notify;
import com.fastbee.framework.mybatis.mapper.BaseMapperX;
import com.fastbee.pay.core.domain.dataobject.notify.PayNotifyTask;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayNotifyTaskMapper extends BaseMapperX<PayNotifyTask> {
}

View File

@ -0,0 +1,10 @@
package com.fastbee.pay.core.domain.mapper.order;
import com.fastbee.framework.mybatis.mapper.BaseMapperX;
import com.fastbee.pay.core.domain.dataobject.order.PayOrderExtension;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtension> {
}

View File

@ -0,0 +1,10 @@
package com.fastbee.pay.core.domain.mapper.order;
import com.fastbee.framework.mybatis.mapper.BaseMapperX;
import com.fastbee.pay.core.domain.dataobject.order.PayOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayOrderMapper extends BaseMapperX<PayOrder> {
}

View File

@ -0,0 +1,11 @@
package com.fastbee.pay.core.domain.mapper.refund;
import com.fastbee.framework.mybatis.mapper.BaseMapperX;
import com.fastbee.pay.core.domain.dataobject.refund.PayRefund;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayRefundMapper extends BaseMapperX<PayRefund> {
}

View File

@ -0,0 +1,27 @@
package com.fastbee.pay.core.domain.redis;
/**
* 支付 Redis Key 枚举类
*
* @author fastbee
*/
public interface RedisKeyConstants {
/**
* 通知任务的分布式锁
*
* KEY 格式pay_notify:lock:%d // 参数来自 DefaultLockKeyBuilder 类
* VALUE 数据格式HASH // RLock.classRedisson 的 Lock 锁,使用 Hash 数据结构
* 过期时间:不固定
*/
String PAY_NOTIFY_LOCK = "pay_notify:lock:%d";
/**
* 支付序号的缓存
*
* KEY 格式pay_no:{prefix}
* VALUE 数据格式:编号自增
*/
String PAY_NO = "pay_no";
}

View File

@ -0,0 +1,34 @@
package com.fastbee.pay.core.domain.redis.no;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* 支付序号的 Redis DAO
*
* @author fastbee
*/
@Repository
public class PayNoRedisDAO {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 生成序号
*
* @param prefix 前缀
* @return 序号
*/
public String generate(String prefix) {
String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN);
Long no = stringRedisTemplate.opsForValue().increment(noPrefix);
return noPrefix + no;
}
}

View File

@ -0,0 +1,33 @@
package com.fastbee.pay.core.domain.redis.notify;
import org.springframework.stereotype.Repository;
/**
* 支付通知的锁 Redis DAO
*
* @author fastbee
*/
@Repository
public class PayNotifyLockRedisDAO {
// @Resource
// private RedissonClient redissonClient;
//
// public void lock(Long id, Long timeoutMillis, Runnable runnable) {
// String lockKey = formatKey(id);
// RLock lock = redissonClient.getLock(lockKey);
// try {
// lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
// // 执行逻辑
// runnable.run();
// } finally {
// lock.unlock();
// }
// }
//
// private static String formatKey(Long id) {
// return String.format(PAY_NOTIFY_LOCK, id);
// }
}

View File

@ -0,0 +1,28 @@
package com.fastbee.pay.core.framework.job.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration(proxyBeanMethods = false)
public class PayJobConfiguration {
public static final String NOTIFY_THREAD_POOL_TASK_EXECUTOR = "NOTIFY_THREAD_POOL_TASK_EXECUTOR";
@Bean(NOTIFY_THREAD_POOL_TASK_EXECUTOR)
public ThreadPoolTaskExecutor notifyThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8); // 设置核心线程数
executor.setMaxPoolSize(16); // 设置最大线程数
executor.setKeepAliveSeconds(60); // 设置空闲时间
executor.setQueueCapacity(100); // 设置队列大小
executor.setThreadNamePrefix("notify-task-"); // 配置线程池的前缀
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 进行加载
executor.initialize();
return executor;
}
}

View File

@ -0,0 +1,4 @@
/**
* 占位
*/
package com.fastbee.pay.core.framework.job.core;

View File

@ -0,0 +1,6 @@
/**
* 属于 pay 模块的 framework 封装
*
* @author fastbee
*/
package com.fastbee.pay.core.framework;

View File

@ -0,0 +1,9 @@
package com.fastbee.pay.core.framework.pay.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PayProperties.class)
public class PayConfiguration {
}

View File

@ -0,0 +1,48 @@
package com.fastbee.pay.core.framework.pay.config;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
@ConfigurationProperties(prefix = "yudao.pay")
@Validated
@Data
public class PayProperties {
private static final String ORDER_NO_PREFIX = "P";
private static final String REFUND_NO_PREFIX = "R";
/**
* 支付回调地址
* 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL
* 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 orderNotifyUrl 地址 => 业务的 PayApp.orderNotifyUrl 地址
*/
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String orderNotifyUrl;
/**
* 退款回调地址
* 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL
* 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 refundNotifyUrl 地址 => 业务的 PayApp.notifyRefundUrl 地址
*/
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String refundNotifyUrl;
/**
* 支付订单 no 的前缀
*/
@NotEmpty(message = "支付订单 no 的前缀不能为空")
private String orderNoPrefix = ORDER_NO_PREFIX;
/**
* 退款订单 no 的前缀
*/
@NotEmpty(message = "退款订单 no 的前缀不能为空")
private String refundNoPrefix = REFUND_NO_PREFIX;
}

View File

@ -0,0 +1,4 @@
/**
* 占位,无实际作用
*/
package com.fastbee.pay.core.framework.pay.core;

View File

@ -0,0 +1,22 @@
package com.fastbee.pay.core.framework.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* pay 模块的 web 组件的 Configuration
*
* @author fastbee
*/
@Configuration(proxyBeanMethods = false)
public class PayWebConfiguration {
/**
* pay 模块的 API 分组
*/
// @Bean
// public GroupedOpenApi payGroupedOpenApi() {
// return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("pay");
// }
}

View File

@ -0,0 +1,4 @@
/**
* pay 模块的 web 配置
*/
package com.fastbee.pay.core.framework.web;

View File

@ -0,0 +1,19 @@
package com.fastbee.pay.core.job.notify;
/**
* 任务处理器
*
* @author 芋道源码
*/
public interface JobHandler {
/**
* 执行任务
*
* @param param 参数
* @return 结果
* @throws Exception 异常
*/
String execute(String param) throws Exception;
}

View File

@ -0,0 +1,29 @@
package com.fastbee.pay.core.job.notify;
import com.fastbee.pay.core.service.notify.PayNotifyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 支付通知 Job
* 通过不断扫描待通知的 PayNotifyTask 记录,回调业务线的回调接口
*
* @author fastbee
*/
@Component
//@TenantJob // 多租户
@Slf4j
public class PayNotifyJob implements JobHandler {
@Resource
private PayNotifyService payNotifyService;
@Override
public String execute(String param) throws Exception {
int notifyCount = payNotifyService.executeNotify();
return String.format("执行支付通知 %s 个", notifyCount);
}
}

View File

@ -0,0 +1,29 @@
package com.fastbee.pay.core.job.order;
import cn.hutool.core.util.StrUtil;
import com.fastbee.pay.core.job.notify.JobHandler;
import com.fastbee.pay.core.service.order.PayOrderService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 支付订单的过期 Job
* 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。
*
* @author fastbee
*/
@Component
//@TenantJob
public class PayOrderExpireJob implements JobHandler {
@Resource
private PayOrderService orderService;
@Override
public String execute(String param) {
int count = orderService.expireOrder();
return StrUtil.format("支付过期 {} 个", count);
}
}

View File

@ -0,0 +1,41 @@
package com.fastbee.pay.core.job.order;
import cn.hutool.core.util.StrUtil;
import com.fastbee.pay.core.job.notify.JobHandler;
import com.fastbee.pay.core.service.order.PayOrderService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 支付订单的同步 Job
* 由于支付订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
*
* @author fastbee
*/
@Component
//@TenantJob
public class PayOrderSyncJob implements JobHandler {
/**
* 同步创建时间在 N 分钟之前的订单
*
* 为什么同步 10 分钟之前的订单?
* 因为一个订单发起支付,到支付成功,大多数在 10 分钟内,需要保证轮询到。
* 如果设置为 30、60 或者更大时间范围,会导致轮询的订单太多,影响性能。当然,你也可以根据自己的业务情况来处理。
*/
private static final Duration CREATE_TIME_DURATION_BEFORE = Duration.ofMinutes(10);
@Resource
private PayOrderService orderService;
@Override
public String execute(String param) {
LocalDateTime minCreateTime = LocalDateTime.now().minus(CREATE_TIME_DURATION_BEFORE);
int count = orderService.syncOrder(minCreateTime);
return StrUtil.format("同步支付订单 {} 个", count);
}
}

View File

@ -0,0 +1,29 @@
package com.fastbee.pay.core.job.refund;
import cn.hutool.core.util.StrUtil;
import com.fastbee.pay.core.job.notify.JobHandler;
import com.fastbee.pay.core.service.refund.PayRefundService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 退款订单的同步 Job
* 由于退款订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
*
* @author fastbee
*/
@Component
//@TenantJob
public class PayRefundSyncJob implements JobHandler {
@Resource
private PayRefundService refundService;
@Override
public String execute(String param) {
int count = refundService.syncRefund();
return StrUtil.format("同步退款订单 {} 个", count);
}
}

View File

@ -0,0 +1,105 @@
package com.fastbee.pay.core.service.app;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.common.exception.ServiceException;
import com.fastbee.common.utils.collection.CollectionUtils;
import com.fastbee.pay.core.controller.admin.app.vo.PayAppCreateReqVO;
import com.fastbee.pay.core.controller.admin.app.vo.PayAppPageReqVO;
import com.fastbee.pay.core.controller.admin.app.vo.PayAppUpdateReqVO;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 支付应用 Service 接口
*
* @author fastbee
*/
public interface PayAppService {
/**
* 创建支付应用
*
* @param createReqVO 创建
* @return 编号
*/
Long createApp(@Valid PayAppCreateReqVO createReqVO);
/**
* 更新支付应用
*
* @param updateReqVO 更新
*/
void updateApp(@Valid PayAppUpdateReqVO updateReqVO);
/**
* 修改应用状态
*
* @param id 应用编号
* @param status 状态
*/
void updateAppStatus(Long id, Integer status);
/**
* 删除支付应用
*
* @param id 编号
*/
void deleteApp(Long id);
/**
* 获得支付应用
*
* @param id 编号
* @return 支付应用
*/
PayApp getApp(Long id);
/**
* 获得支付应用列表
*
* @param ids 编号
* @return 支付应用列表
*/
List<PayApp> getAppList(Collection<Long> ids);
/**
* 获得支付应用列表
*
* @return 支付应用列表
*/
List<PayApp> getAppList();
/**
* 获得支付应用分页
*
* @param pageReqVO 分页查询
* @return 支付应用分页
*/
PageResult<PayApp> getAppPage(PayAppPageReqVO pageReqVO);
/**
* 获得指定编号的商户 Map
*
* @param ids 应用编号集合
* @return 商户 Map
*/
default Map<Long, PayApp> getAppMap(Collection<Long> ids) {
List<PayApp> list = getAppList(ids);
return CollectionUtils.convertMap(list, PayApp::getId);
}
/**
* 支付应用的合法性
*
* 如果不合法,抛出 {@link ServiceException} 业务异常
*
* @param id 应用编号
* @return 应用
*/
PayApp validPayApp(Long id);
}

View File

@ -0,0 +1,131 @@
package com.fastbee.pay.core.service.app;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.common.enums.CommonStatusEnum;
import com.fastbee.framework.mybatis.LambdaQueryWrapperX;
import com.fastbee.pay.api.enums.ErrorCodeConstants;
import com.fastbee.pay.core.controller.admin.app.vo.PayAppCreateReqVO;
import com.fastbee.pay.core.controller.admin.app.vo.PayAppPageReqVO;
import com.fastbee.pay.core.controller.admin.app.vo.PayAppUpdateReqVO;
import com.fastbee.pay.core.convert.app.PayAppConvert;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.mapper.app.PayAppMapper;
import com.fastbee.pay.core.service.order.PayOrderService;
import com.fastbee.pay.core.service.refund.PayRefundService;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import static com.fastbee.common.exception.ServiceExceptionUtil.exception;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.*;
/**
* 支付应用 Service 实现类
*
* @author aquan
*/
@Service
@Validated
public class PayAppServiceImpl implements PayAppService {
@Resource
private PayAppMapper payAppMapper;
@Resource
@Lazy // 延迟加载,避免循环依赖报错
private PayOrderService payOrderService;
@Resource
@Lazy // 延迟加载,避免循环依赖报错
private PayRefundService payRefundService;
@Override
public Long createApp(PayAppCreateReqVO createReqVO) {
// 插入
PayApp app = PayAppConvert.INSTANCE.convert(createReqVO);
payAppMapper.insert(app);
// 返回
return app.getId();
}
@Override
public void updateApp(PayAppUpdateReqVO updateReqVO) {
// 校验存在
validateAppExists(updateReqVO.getId());
// 更新
PayApp updateObj = PayAppConvert.INSTANCE.convert(updateReqVO);
payAppMapper.updateById(updateObj);
}
@Override
public void updateAppStatus(Long id, Integer status) {
// 校验商户存在
validateAppExists(id);
// 更新状态
payAppMapper.updateById(new PayApp().setId(id).setStatus(status));
}
@Override
public void deleteApp(Long id) {
// 校验存在
validateAppExists(id);
// 校验关联数据是否存在
if (payOrderService.getOrderCountByAppId(id) > 0) {
throw exception(APP_EXIST_ORDER_CANT_DELETE);
}
if (payRefundService.getRefundCountByAppId(id) > 0) {
throw exception(APP_EXIST_REFUND_CANT_DELETE);
}
// 删除
payAppMapper.deleteById(id);
}
private void validateAppExists(Long id) {
if (payAppMapper.selectById(id) == null) {
throw exception(APP_NOT_FOUND);
}
}
@Override
public PayApp getApp(Long id) {
return payAppMapper.selectById(id);
}
@Override
public List<PayApp> getAppList(Collection<Long> ids) {
return payAppMapper.selectBatchIds(ids);
}
@Override
public List<PayApp> getAppList() {
return payAppMapper.selectList();
}
@Override
public PageResult<PayApp> getAppPage(PayAppPageReqVO pageReqVO) {
return payAppMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<PayApp>()
.likeIfPresent(PayApp::getName, pageReqVO.getName())
.eqIfPresent(PayApp::getStatus, pageReqVO.getStatus())
.betweenIfPresent(PayApp::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(PayApp::getId));
}
@Override
public PayApp validPayApp(Long id) {
PayApp app = payAppMapper.selectById(id);
// 校验是否存在
if (app == null) {
throw exception(ErrorCodeConstants.APP_NOT_FOUND);
}
// 校验是否禁用
if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) {
throw exception(ErrorCodeConstants.APP_IS_DISABLE);
}
return app;
}
}

View File

@ -0,0 +1,97 @@
package com.fastbee.pay.core.service.channel;
import com.fastbee.common.exception.ServiceException;
import com.fastbee.pay.core.controller.admin.channel.vo.PayChannelCreateReqVO;
import com.fastbee.pay.core.controller.admin.channel.vo.PayChannelUpdateReqVO;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* 支付渠道 Service 接口
*
* @author aquan
*/
public interface PayChannelService {
/**
* 创建支付渠道
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createChannel(@Valid PayChannelCreateReqVO createReqVO);
/**
* 更新支付渠道
*
* @param updateReqVO 更新信息
*/
void updateChannel(@Valid PayChannelUpdateReqVO updateReqVO);
/**
* 删除支付渠道
*
* @param id 编号
*/
void deleteChannel(Long id);
/**
* 获得支付渠道
*
* @param id 编号
* @return 支付渠道
*/
PayChannel getChannel(Long id);
/**
* 根据支付应用 ID 集合,获得支付渠道列表
*
* @param appIds 应用编号集合
* @return 支付渠道列表
*/
List<PayChannel> getChannelListByAppIds(Collection<Long> appIds);
/**
* 根据条件获取渠道
*
* @param appId 应用编号
* @param code 渠道编码
* @return 数量
*/
PayChannel getChannelByAppIdAndCode(Long appId, String code);
/**
* 支付渠道的合法性
*
* 如果不合法,抛出 {@link ServiceException} 业务异常
*
* @param id 渠道编号
* @return 渠道信息
*/
PayChannel validPayChannel(Long id);
/**
* 支付渠道的合法性
*
* 如果不合法,抛出 {@link ServiceException} 业务异常
*
* @param appId 应用编号
* @param code 支付渠道
* @return 渠道信息
*/
PayChannel validPayChannel(Long appId, String code);
/**
* 获得指定应用的开启的渠道列表
*
* @param appId 应用编号
* @return 渠道列表
*/
List<PayChannel> getEnableChannelList(Long appId);
PayChannel selectByAppIdAndCode(Long appId, String code);
}

View File

@ -0,0 +1,231 @@
package com.fastbee.pay.core.service.channel;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import com.fastbee.common.enums.CommonStatusEnum;
import com.fastbee.common.utils.json.JsonUtils;
import com.fastbee.framework.mybatis.LambdaQueryWrapperX;
import com.fastbee.pay.core.controller.admin.channel.vo.PayChannelCreateReqVO;
import com.fastbee.pay.core.controller.admin.channel.vo.PayChannelUpdateReqVO;
import com.fastbee.pay.core.convert.channel.PayChannelConvert;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import com.fastbee.pay.core.domain.mapper.channel.PayChannelMapper;
import com.fastbee.pay.framework.client.PayClientConfig;
import com.fastbee.pay.framework.client.PayClientFactory;
import com.fastbee.pay.framework.enums.channel.PayChannelEnum;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.validation.Validator;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.fastbee.common.exception.ServiceExceptionUtil.exception;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.CHANNEL_EXIST_SAME_CHANNEL_ERROR;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.CHANNEL_IS_DISABLE;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.CHANNEL_NOT_FOUND;
/**
* 支付渠道 Service 实现类
*
* @author aquan
*/
@Service
@Slf4j
@Validated
public class PayChannelServiceImpl implements PayChannelService {
@Getter // 为了方便测试,这里提供 getter 方法
@Setter
private volatile List<PayChannel> channelCache;
@Resource
private PayClientFactory payClientFactory;
@Resource
private PayChannelMapper payChannelMapper;
@Resource
private Validator validator;
/**
* 初始化 {@link #payClientFactory} 缓存
*/
@PostConstruct
public void initLocalCache() {
// 注意:忽略自动多租户,因为要全局初始化缓存
// TenantUtils.executeIgnore(() -> {
// // 第一步:查询数据
// List<PayChannel> channels = Collections.emptyList();
// try {
// channels = channelMapper.selectList();
// } catch (Throwable ex) {
// if (!ex.getMessage().contains("doesn't exist")) {
// throw ex;
// }
// log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]");
// }
// log.info("[initLocalCache][缓存支付渠道,数量为:{}]", channels.size());
//
// // 第二步:构建缓存:创建或更新支付 Client
// channels.forEach(payChannel -> payClientFactory.createOrUpdatePayClient(payChannel.getId(),
// payChannel.getCode(), payChannel.getConfig()));
// this.channelCache = channels;
// });
}
/**
* 通过定时任务轮询,刷新缓存
*
* 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新
*/
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void refreshLocalCache() {
// 注意:忽略自动多租户,因为要全局初始化缓存
// TenantUtils.executeIgnore(() -> {
// // 情况一:如果缓存里没有数据,则直接刷新缓存
// if (CollUtil.isEmpty(channelCache)) {
// initLocalCache();
// return;
// }
//
// // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存
// LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannel::getUpdateTime);
// if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
// initLocalCache();
// }
// });
}
@Override
public Long createChannel(PayChannelCreateReqVO reqVO) {
// 断言是否有重复的
PayChannel dbChannel = getChannelByAppIdAndCode(reqVO.getAppId(), reqVO.getCode());
if (dbChannel != null) {
throw exception(CHANNEL_EXIST_SAME_CHANNEL_ERROR);
}
// 新增渠道
PayChannel channel = PayChannelConvert.INSTANCE.convert(reqVO)
.setConfig(parseConfig(reqVO.getCode(), reqVO.getConfig()));
payChannelMapper.insert(channel);
// 刷新缓存
initLocalCache();
return channel.getId();
}
@Override
public void updateChannel(PayChannelUpdateReqVO updateReqVO) {
// 校验存在
PayChannel dbChannel = validateChannelExists(updateReqVO.getId());
// 更新
PayChannel channel = PayChannelConvert.INSTANCE.convert(updateReqVO)
.setConfig(parseConfig(dbChannel.getCode(), updateReqVO.getConfig()));
payChannelMapper.updateById(channel);
// 刷新缓存
initLocalCache();
}
/**
* 解析并校验配置
*
* @param code 渠道编码
* @param configStr 配置
* @return 支付配置
*/
private PayClientConfig parseConfig(String code, String configStr) {
// 解析配置
Class<? extends PayClientConfig> payClass = PayChannelEnum.getByCode(code).getConfigClass();
if (ObjectUtil.isNull(payClass)) {
throw exception(CHANNEL_NOT_FOUND);
}
PayClientConfig config = JsonUtils.parseObject2(configStr, payClass);
Assert.notNull(config);
// 验证参数
config.validate(validator);
return config;
}
@Override
public void deleteChannel(Long id) {
// 校验存在
validateChannelExists(id);
// 删除
payChannelMapper.deleteById(id);
// 刷新缓存
initLocalCache();
}
private PayChannel validateChannelExists(Long id) {
PayChannel channel = payChannelMapper.selectById(id);
if (channel == null) {
throw exception(CHANNEL_NOT_FOUND);
}
return channel;
}
@Override
public PayChannel getChannel(Long id) {
return payChannelMapper.selectById(id);
}
@Override
public List<PayChannel> getChannelListByAppIds(Collection<Long> appIds) {
return payChannelMapper.selectList(PayChannel::getAppId, appIds);
}
@Override
public PayChannel getChannelByAppIdAndCode(Long appId, String code) {
return this.selectByAppIdAndCode(appId, code);
}
@Override
public PayChannel validPayChannel(Long id) {
PayChannel channel = payChannelMapper.selectById(id);
validPayChannel(channel);
return channel;
}
@Override
public PayChannel validPayChannel(Long appId, String code) {
PayChannel channel = this.selectByAppIdAndCode(appId, code);
validPayChannel(channel);
return channel;
}
private void validPayChannel(PayChannel channel) {
if (channel == null) {
throw exception(CHANNEL_NOT_FOUND);
}
if (CommonStatusEnum.DISABLE.getStatus().equals(channel.getStatus())) {
throw exception(CHANNEL_IS_DISABLE);
}
}
@Override
public List<PayChannel> getEnableChannelList(Long appId) {
return payChannelMapper.selectList(new LambdaQueryWrapperX<PayChannel>()
.eq(PayChannel::getAppId, appId)
.eq(PayChannel::getStatus, CommonStatusEnum.ENABLE.getStatus()));
}
@Override
public PayChannel selectByAppIdAndCode(Long appId, String code) {
return payChannelMapper.selectOne(PayChannel::getAppId, appId, PayChannel::getCode, code);
}
}

View File

@ -0,0 +1,66 @@
package com.fastbee.pay.core.service.demo;
import com.fastbee.common.core.domain.PageParam;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.pay.core.controller.admin.demo.vo.OrderInfoCreateReqVO;
import com.fastbee.pay.core.domain.dataobject.demo.OrderInfo;
import javax.validation.Valid;
/**
* 示例订单 Service 接口
*
* @author fastbee
*/
public interface OrderInfoService {
/**
* 创建示例订单
*
* @param userId 用户编号
* @param createReqVO 创建信息
* @return 编号
*/
Long createOrder(Long userId, @Valid OrderInfoCreateReqVO createReqVO);
/**
* 获得示例订单
*
* @param id 编号
* @return 示例订单
*/
OrderInfo getOrder(Long id);
/**
* 获得示例订单分页
*
* @param pageReqVO 分页查询
* @return 示例订单分页
*/
PageResult<OrderInfo> getOrderPage(PageParam pageReqVO);
/**
* 更新示例订单为已支付
*
* @param id 编号
* @param payOrderId 支付订单号
*/
void updateOrderPaid(Long id, Long payOrderId);
/**
* 发起示例订单的退款
*
* @param id 编号
* @param userIp 用户编号
*/
void refundOrder(Long id, String userIp);
/**
* 更新示例订单为已退款
*
* @param id 编号
* @param payRefundId 退款订单号
*/
void updateOrderRefunded(Long id, Long payRefundId);
}

View File

@ -0,0 +1,264 @@
package com.fastbee.pay.core.service.demo;
import cn.hutool.core.lang.Assert;
import com.fastbee.common.core.domain.PageParam;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.framework.mybatis.LambdaQueryWrapperX;
import com.fastbee.pay.api.api.order.PayOrderApi;
import com.fastbee.pay.api.api.order.dto.PayOrderCreateReqDTO;
import com.fastbee.pay.api.api.order.dto.PayOrderRespDTO;
import com.fastbee.pay.api.api.refund.PayRefundApi;
import com.fastbee.pay.api.api.refund.dto.PayRefundCreateReqDTO;
import com.fastbee.pay.api.api.refund.dto.PayRefundRespDTO;
import com.fastbee.pay.api.enums.order.PayOrderStatusEnum;
import com.fastbee.pay.api.enums.refund.PayRefundStatusEnum;
import com.fastbee.pay.core.controller.admin.demo.vo.OrderInfoCreateReqVO;
import com.fastbee.pay.core.domain.dataobject.demo.OrderInfo;
import com.fastbee.pay.core.domain.mapper.demo.OrderInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static cn.hutool.core.util.ObjectUtil.notEqual;
import static com.fastbee.common.exception.ServiceExceptionUtil.exception;
import static com.fastbee.common.utils.ServletUtils.getClientIP;
import static com.fastbee.common.utils.date.LocalDateTimeUtils.addTime;
import static com.fastbee.common.utils.json.JsonUtils.toJsonString;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.*;
/**
* 示例订单 Service 实现类
*
* @author fastbee
*/
@Service
@Validated
@Slf4j
public class OrderInfoServiceImpl implements OrderInfoService {
/**
* 接入的实力应用编号
* 从 [支付管理 -> 应用信息] 里添加
*/
private static final Long PAY_APP_ID = 7L;
/**
* 商品信息 Map
* key商品编号
* value[商品名、商品价格]
*/
private final Map<Long, Object[]> spuNames = new HashMap<>();
@Resource
private PayOrderApi payOrderApi;
@Resource
private PayRefundApi payRefundApi;
@Resource
private OrderInfoMapper OrderInfoMapper;
public OrderInfoServiceImpl() {
spuNames.put(1L, new Object[]{"华为手机", 1});
spuNames.put(2L, new Object[]{"小米电视", 10});
spuNames.put(3L, new Object[]{"苹果手表", 100});
spuNames.put(4L, new Object[]{"华硕笔记本", 1000});
spuNames.put(5L, new Object[]{"蔚来汽车", 200000});
}
@Override
public Long createOrder(Long userId, OrderInfoCreateReqVO createReqVO) {
// 1.1 获得商品
Object[] spu = spuNames.get(createReqVO.getSpuId());
Assert.notNull(spu, "商品({}) 不存在", createReqVO.getSpuId());
String spuName = (String) spu[0];
Integer price = (Integer) spu[1];
// 1.2 插入 demo 订单
OrderInfo demoOrder = new OrderInfo().setUserId(userId)
.setSpuId(createReqVO.getSpuId()).setSpuName(spuName)
.setPrice(price).setPayStatus(false).setRefundPrice(0);
OrderInfoMapper.insert(demoOrder);
// 2.1 创建支付单
Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO()
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
.setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号
.setSubject(spuName).setBody("").setPrice(price) // 价格信息
.setExpireTime(addTime(Duration.ofHours(2L)))); // 支付的过期时间
// 2.2 更新支付单到 demo 订单
OrderInfoMapper.updateById(new OrderInfo().setId(demoOrder.getId())
.setPayOrderId(payOrderId));
// 返回
return demoOrder.getId();
}
@Override
public OrderInfo getOrder(Long id) {
return OrderInfoMapper.selectById(id);
}
@Override
public PageResult<OrderInfo> getOrderPage(PageParam pageReqVO) {
return OrderInfoMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<OrderInfo>()
.orderByDesc(OrderInfo::getId));
}
@Override
public void updateOrderPaid(Long id, Long payOrderId) {
// 校验并获得支付订单(可支付)
PayOrderRespDTO payOrder = validateDemoOrderCanPaid(id, payOrderId);
// 更新 OrderInfo 状态为已支付
int updateCount = OrderInfoMapper.update(new OrderInfo().setPayStatus(true).setPayTime(LocalDateTime.now())
.setPayChannelCode(payOrder.getChannelCode()), new LambdaQueryWrapperX<OrderInfo>()
.eq(OrderInfo::getId, id).eq(OrderInfo::getPayStatus, false));
if (updateCount == 0) {
throw exception(DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID);
}
}
/**
* 校验交易订单满足被支付的条件
* 1. 交易订单未支付
* 2. 支付单已支付
*
* @param id 交易订单编号
* @param payOrderId 支付订单编号
* @return 交易订单
*/
private PayOrderRespDTO validateDemoOrderCanPaid(Long id, Long payOrderId) {
// 1.1 校验订单是否存在
OrderInfo order = OrderInfoMapper.selectById(id);
if (order == null) {
throw exception(DEMO_ORDER_NOT_FOUND);
}
// 1.2 校验订单未支付
if (order.getPayStatus()) {
log.error("[validateDemoOrderCanPaid][order({}) 不处于待支付状态请进行处理order 数据是:{}]",
id, toJsonString(order));
throw exception(DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID);
}
// 1.3 校验支付订单匹配
if (notEqual(order.getPayOrderId(), payOrderId)) { // 支付单号
log.error("[validateDemoOrderCanPaid][order({}) 支付单不匹配({})请进行处理order 数据是:{}]",
id, payOrderId, toJsonString(order));
throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR);
}
// 2.1 校验支付单是否存在
PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId);
if (payOrder == null) {
log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 不存在,请进行处理!]", id, payOrderId);
throw exception(ORDER_NOT_FOUND);
}
// 2.2 校验支付单已支付
if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 未支付请进行处理payOrder 数据是:{}]",
id, payOrderId, toJsonString(payOrder));
throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS);
}
// 2.3 校验支付金额一致
if (notEqual(payOrder.getPrice(), order.getPrice())) {
log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 支付金额不匹配请进行处理order 数据是:{}payOrder 数据是:{}]",
id, payOrderId, toJsonString(order), toJsonString(payOrder));
throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH);
}
// 2.4 校验支付订单匹配(二次)
if (notEqual(payOrder.getMerchantOrderId(), id.toString())) {
log.error("[validateDemoOrderCanPaid][order({}) 支付单不匹配({})请进行处理payOrder 数据是:{}]",
id, payOrderId, toJsonString(payOrder));
throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR);
}
return payOrder;
}
@Override
public void refundOrder(Long id, String userIp) {
// 1. 校验订单是否可以退款
OrderInfo order = validateDemoOrderCanRefund(id);
// 2.1 生成退款单号
// 一般来说,用户发起退款的时候,都会单独插入一个售后维权表,然后使用该表的 id 作为 refundId
// 这里我们是个简单的 demo所以没有售后维权表直接使用订单 id + "-refund" 来演示
String refundId = order.getId() + "-refund";
// 2.2 创建退款单
Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
.setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
.setMerchantRefundId(refundId)
.setReason("想退钱").setPrice(order.getPrice()));// 价格信息
// 2.3 更新退款单到 demo 订单
OrderInfoMapper.updateById(new OrderInfo().setId(id)
.setPayRefundId(payRefundId).setRefundPrice(order.getPrice()));
}
private OrderInfo validateDemoOrderCanRefund(Long id) {
// 校验订单是否存在
OrderInfo order = OrderInfoMapper.selectById(id);
if (order == null) {
throw exception(DEMO_ORDER_NOT_FOUND);
}
// 校验订单是否支付
if (!order.getPayStatus()) {
throw exception(DEMO_ORDER_REFUND_FAIL_NOT_PAID);
}
// 校验订单是否已退款
if (order.getPayRefundId() != null) {
throw exception(DEMO_ORDER_REFUND_FAIL_REFUNDED);
}
return order;
}
@Override
public void updateOrderRefunded(Long id, Long payRefundId) {
// 1. 校验并获得退款订单(可退款)
PayRefundRespDTO payRefund = validateDemoOrderCanRefunded(id, payRefundId);
// 2.2 更新退款单到 demo 订单
OrderInfoMapper.updateById(new OrderInfo().setId(id)
.setRefundTime(payRefund.getSuccessTime()));
}
private PayRefundRespDTO validateDemoOrderCanRefunded(Long id, Long payRefundId) {
// 1.1 校验示例订单
OrderInfo order = OrderInfoMapper.selectById(id);
if (order == null) {
throw exception(DEMO_ORDER_NOT_FOUND);
}
// 1.2 校验退款订单匹配
if (Objects.equals(order.getPayOrderId(), payRefundId)) {
log.error("[validateDemoOrderCanRefunded][order({}) 退款单不匹配({})请进行处理order 数据是:{}]",
id, payRefundId, toJsonString(order));
throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR);
}
// 2.1 校验退款订单
PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId);
if (payRefund == null) {
throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND);
}
// 2.2
if (!PayRefundStatusEnum.isSuccess(payRefund.getStatus())) {
throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS);
}
// 2.3 校验退款金额一致
if (notEqual(payRefund.getRefundPrice(), order.getPrice())) {
log.error("[validateDemoOrderCanRefunded][order({}) payRefund({}) 退款金额不匹配请进行处理order 数据是:{}payRefund 数据是:{}]",
id, payRefundId, toJsonString(order), toJsonString(payRefund));
throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH);
}
// 2.4 校验退款订单匹配(二次)
if (notEqual(payRefund.getMerchantOrderId(), id.toString())) {
log.error("[validateDemoOrderCanRefunded][order({}) 退款单不匹配({})请进行处理payRefund 数据是:{}]",
id, payRefundId, toJsonString(payRefund));
throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR);
}
return payRefund;
}
}

View File

@ -0,0 +1,56 @@
package com.fastbee.pay.core.service.notify;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.pay.core.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
import com.fastbee.pay.core.domain.dataobject.notify.PayNotifyLog;
import com.fastbee.pay.core.domain.dataobject.notify.PayNotifyTask;
import java.util.List;
/**
* 回调通知 Service 接口
*
* @author fastbee
*/
public interface PayNotifyService {
/**
* 创建回调通知任务
*
* @param type 类型
* @param dataId 数据编号
*/
void createPayNotifyTask(Integer type, Long dataId);
/**
* 执行回调通知
* 注意,该方法提供给定时任务调用。目前是 yudao-server 进行调用
* @return 通知数量
*/
int executeNotify() throws InterruptedException;
/**
* 获得回调通知
*
* @param id 编号
* @return 回调通知
*/
PayNotifyTask getNotifyTask(Long id);
/**
* 获得回调通知分页
*
* @param pageReqVO 分页查询
* @return 回调通知分页
*/
PageResult<PayNotifyTask> getNotifyTaskPage(PayNotifyTaskPageReqVO pageReqVO);
/**
* 获得回调日志列表
*
* @param taskId 任务编号
* @return 日志列表
*/
List<PayNotifyLog> getNotifyLogList(Long taskId);
}

View File

@ -0,0 +1,305 @@
package com.fastbee.pay.core.service.notify;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fastbee.common.core.domain.CommonResult;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.common.utils.date.DateUtils;
import com.fastbee.common.utils.json.JsonUtils;
import com.fastbee.framework.mybatis.LambdaQueryWrapperX;
import com.fastbee.pay.api.api.notify.dto.PayOrderNotifyReqDTO;
import com.fastbee.pay.api.api.notify.dto.PayRefundNotifyReqDTO;
import com.fastbee.pay.api.enums.notify.PayNotifyStatusEnum;
import com.fastbee.pay.api.enums.notify.PayNotifyTypeEnum;
import com.fastbee.pay.core.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
import com.fastbee.pay.core.domain.dataobject.notify.PayNotifyLog;
import com.fastbee.pay.core.domain.dataobject.notify.PayNotifyTask;
import com.fastbee.pay.core.domain.dataobject.order.PayOrder;
import com.fastbee.pay.core.domain.dataobject.refund.PayRefund;
import com.fastbee.pay.core.domain.mapper.notify.PayNotifyLogMapper;
import com.fastbee.pay.core.domain.mapper.notify.PayNotifyTaskMapper;
import com.fastbee.pay.core.service.order.PayOrderService;
import com.fastbee.pay.core.service.refund.PayRefundService;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static com.fastbee.common.utils.date.LocalDateTimeUtils.addTime;
import static com.fastbee.pay.core.framework.job.config.PayJobConfiguration.NOTIFY_THREAD_POOL_TASK_EXECUTOR;
/**
* 支付通知 Core Service 实现类
*
* @author fastbee
*/
@Service
@Valid
@Slf4j
public class PayNotifyServiceImpl implements PayNotifyService {
/**
* 通知超时时间,单位:秒
*/
public static final int NOTIFY_TIMEOUT = 120;
/**
* {@link #NOTIFY_TIMEOUT} 的毫秒
*/
public static final long NOTIFY_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS;
@Resource
@Lazy // 循环依赖,避免报错
private PayOrderService payOrderService;
@Resource
@Lazy // 循环依赖,避免报错
private PayRefundService payRefundService;
@Resource
private PayNotifyTaskMapper payNotifyTaskMapper;
@Resource
private PayNotifyLogMapper payNotifyLogMapper;
@Resource(name = NOTIFY_THREAD_POOL_TASK_EXECUTOR)
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
// @Resource
// private PayNotifyLockRedisDAO notifyLockCoreRedisDAO;
@Override
@Transactional(rollbackFor = Exception.class)
public void createPayNotifyTask(Integer type, Long dataId) {
PayNotifyTask task = new PayNotifyTask().setType(type).setDataId(dataId);
task.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setNextNotifyTime(LocalDateTime.now())
.setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTask.NOTIFY_FREQUENCY.length + 1);
// 补充 appId + notifyUrl 字段
if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
PayOrder order = payOrderService.getOrder(task.getDataId()); // 不进行非空判断,有问题直接异常
task.setAppId(order.getAppId()).
setMerchantOrderId(order.getMerchantOrderId()).setNotifyUrl(order.getNotifyUrl());
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
PayRefund refundDO = payRefundService.getRefund(task.getDataId());
task.setAppId(refundDO.getAppId())
.setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl());
}
// 执行插入
payNotifyTaskMapper.insert(task);
// 必须在事务提交后,在发起任务,否则 PayNotifyTask 还没入库,就提前回调接入的业务
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
// executeNotify(task);
}
});
}
@Override
public int executeNotify() throws InterruptedException {
// 获得需要通知的任务
List<PayNotifyTask> tasks = payNotifyTaskMapper.selectList(new LambdaQueryWrapper<PayNotifyTask>()
.in(PayNotifyTask::getStatus, PayNotifyStatusEnum.WAITING.getStatus(),
PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus())
.le(PayNotifyTask::getNextNotifyTime, LocalDateTime.now()));
if (CollUtil.isEmpty(tasks)) {
return 0;
}
// 遍历,逐个通知
CountDownLatch latch = new CountDownLatch(tasks.size());
tasks.forEach(task -> threadPoolTaskExecutor.execute(() -> {
try {
// executeNotify(task);
} finally {
latch.countDown();
}
}));
// 等待完成
awaitExecuteNotify(latch);
// 返回执行完成的任务数(成功 + 失败)
return tasks.size();
}
/**
* 等待全部支付通知的完成
* 每 1 秒会打印一次剩余任务数量
*
* @param latch Latch
* @throws InterruptedException 如果被打断
*/
private void awaitExecuteNotify(CountDownLatch latch) throws InterruptedException {
long size = latch.getCount();
for (int i = 0; i < NOTIFY_TIMEOUT; i++) {
if (latch.await(1L, TimeUnit.SECONDS)) {
return;
}
log.info("[awaitExecuteNotify][任务处理中, 总任务数({}) 剩余任务数({})]", size, latch.getCount());
}
log.error("[awaitExecuteNotify][任务未处理完,总任务数({}) 剩余任务数({})]", size, latch.getCount());
}
/**
* 同步执行单个支付通知
*
* @param task 通知任务
*/
// public void executeNotify(PayNotifyTask task) {
// // 分布式锁,避免并发问题
// notifyLockCoreRedisDAO.lock(task.getId(), NOTIFY_TIMEOUT_MILLIS, () -> {
// // 校验,当前任务是否已经被通知过
// // 虽然已经通过分布式加锁,但是可能同时满足通知的条件,然后都去获得锁。此时,第一个执行完后,第二个还是能拿到锁,然后会再执行一次。
// // 因此,此处我们通过第 notifyTimes 通知次数是否匹配来判断
// PayNotifyTask dbTask = payNotifyTaskMapper.selectById(task.getId());
// if (ObjectUtil.notEqual(task.getNotifyTimes(), dbTask.getNotifyTimes())) {
// log.warn("[executeNotifySync][task({}) 任务被忽略,原因是它的通知不是第 ({}) 次,可能是因为并发执行了]",
// JsonUtils.toJsonString(task), dbTask.getNotifyTimes());
// return;
// }
//
// // 执行通知
// getSelf().executeNotify0(dbTask);
// });
// }
@Transactional(rollbackFor = Exception.class)
public void executeNotify0(PayNotifyTask task) {
// 发起回调
CommonResult<?> invokeResult = null;
Throwable invokeException = null;
try {
invokeResult = executeNotifyInvoke(task);
} catch (Throwable e) {
invokeException = e;
}
// 处理结果
Integer newStatus = processNotifyResult(task, invokeResult, invokeException);
// 记录 PayNotifyLog 日志
String response = invokeException != null ? ExceptionUtil.getRootCauseMessage(invokeException) :
JsonUtils.toJsonString(invokeResult);
payNotifyLogMapper.insert(PayNotifyLog.builder().taskId(task.getId())
.notifyTimes(task.getNotifyTimes() + 1).status(newStatus).response(response).build());
}
/**
* 执行单个支付任务的 HTTP 调用
*
* @param task 通知任务
* @return HTTP 响应
*/
private CommonResult<?> executeNotifyInvoke(PayNotifyTask task) {
// 拼接 body 参数
Object request;
if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
request = PayOrderNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
.payOrderId(task.getDataId()).build();
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
.payRefundId(task.getDataId()).build();
} else {
throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task));
}
// 拼接 header 参数
Map<String, String> headers = new HashMap<>();
// TenantUtils.addTenantHeader(headers, task.getTenantId());
// 发起请求
try (HttpResponse response = HttpUtil.createPost(task.getNotifyUrl())
.body(JsonUtils.toJsonString(request)).addHeaders(headers)
.timeout((int) NOTIFY_TIMEOUT_MILLIS).execute()) {
// 解析结果
return JsonUtils.parseObject(response.body(), CommonResult.class);
}
}
/**
* 处理并更新通知结果
*
* @param task 通知任务
* @param invokeResult 通知结果
* @param invokeException 通知异常
* @return 最终任务的状态
*/
@VisibleForTesting
Integer processNotifyResult(PayNotifyTask task, CommonResult<?> invokeResult, Throwable invokeException) {
// 设置通用的更新 PayNotifyTask 的字段
PayNotifyTask updateTask = new PayNotifyTask()
.setId(task.getId())
.setLastExecuteTime(LocalDateTime.now())
.setNotifyTimes(task.getNotifyTimes() + 1);
// 情况一:调用成功
if (invokeResult != null && invokeResult.isSuccess()) {
updateTask.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus());
payNotifyTaskMapper.updateById(updateTask);
return updateTask.getStatus();
}
// 情况二:调用失败、调用异常
// 2.1 超过最大回调次数
if (updateTask.getNotifyTimes() >= PayNotifyTask.NOTIFY_FREQUENCY.length) {
updateTask.setStatus(PayNotifyStatusEnum.FAILURE.getStatus());
payNotifyTaskMapper.updateById(updateTask);
return updateTask.getStatus();
}
// 2.2 未超过最大回调次数
updateTask.setNextNotifyTime(addTime(Duration.ofSeconds(PayNotifyTask.NOTIFY_FREQUENCY[updateTask.getNotifyTimes()])));
updateTask.setStatus(invokeException != null ? PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()
: PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus());
payNotifyTaskMapper.updateById(updateTask);
return updateTask.getStatus();
}
@Override
public PayNotifyTask getNotifyTask(Long id) {
return payNotifyTaskMapper.selectById(id);
}
@Override
public PageResult<PayNotifyTask> getNotifyTaskPage(PayNotifyTaskPageReqVO pageReqVO) {
return payNotifyTaskMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<PayNotifyTask>()
.eqIfPresent(PayNotifyTask::getAppId, pageReqVO.getAppId())
.eqIfPresent(PayNotifyTask::getType, pageReqVO.getType())
.eqIfPresent(PayNotifyTask::getDataId, pageReqVO.getDataId())
.eqIfPresent(PayNotifyTask::getStatus, pageReqVO.getStatus())
.eqIfPresent(PayNotifyTask::getMerchantOrderId, pageReqVO.getMerchantOrderId())
.betweenIfPresent(PayNotifyTask::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(PayNotifyTask::getId));
}
@Override
public List<PayNotifyLog> getNotifyLogList(Long taskId) {
return payNotifyLogMapper.selectList(PayNotifyLog::getTaskId, taskId);
}
/**
* 获得自身的代理对象,解决 AOP 生效问题
*
* @return 自己
*/
private PayNotifyServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}

View File

@ -0,0 +1,27 @@
package com.fastbee.pay.core.service.order;
import com.fastbee.pay.core.domain.dataobject.order.PayOrderExtension;
import java.time.LocalDateTime;
import java.util.List;
/**
* 支付订单扩展 Service 接口
*
* @author aquan
*/
public interface PayOrderExtensionService {
PayOrderExtension selectByNo(String no);
int updateByIdAndStatus(Long id, Integer status, PayOrderExtension update);
List<PayOrderExtension> selectListByOrderId(Long orderId);
List<PayOrderExtension> selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime);
PayOrderExtension selectById(Long id);
void insert(PayOrderExtension payOrderExtension);
}

View File

@ -0,0 +1,60 @@
package com.fastbee.pay.core.service.order;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fastbee.pay.core.domain.dataobject.order.PayOrderExtension;
import com.fastbee.pay.core.domain.mapper.order.PayOrderExtensionMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
/**
* 支付订单 Service 实现类
*
* @author aquan
*/
@Service
@Validated
@Slf4j
public class PayOrderExtensionServiceImpl implements PayOrderExtensionService {
@Resource
private PayOrderExtensionMapper payOrderExtensionMapper;
@Override
public PayOrderExtension selectByNo(String no) {
return payOrderExtensionMapper.selectOne(PayOrderExtension::getNo, no);
}
@Override
public int updateByIdAndStatus(Long id, Integer status, PayOrderExtension update) {
return payOrderExtensionMapper.update(update, new LambdaQueryWrapper<PayOrderExtension>()
.eq(PayOrderExtension::getId, id).eq(PayOrderExtension::getStatus, status));
}
@Override
public List<PayOrderExtension> selectListByOrderId(Long orderId) {
return payOrderExtensionMapper.selectList(PayOrderExtension::getOrderId, orderId);
}
@Override
public List<PayOrderExtension> selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime) {
return payOrderExtensionMapper.selectList(new LambdaQueryWrapper<PayOrderExtension>()
.eq(PayOrderExtension::getStatus, status)
.ge(PayOrderExtension::getCreateTime, minCreateTime));
}
@Override
public PayOrderExtension selectById(Long id) {
return payOrderExtensionMapper.selectById(id);
}
@Override
public void insert(PayOrderExtension payOrderExtension) {
payOrderExtensionMapper.insert(payOrderExtension);
}
}

View File

@ -0,0 +1,129 @@
package com.fastbee.pay.core.service.order;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.pay.api.api.order.dto.PayOrderCreateReqDTO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderExportReqVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderPageReqVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitReqVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitRespVO;
import com.fastbee.pay.core.domain.dataobject.order.PayOrder;
import com.fastbee.pay.core.domain.dataobject.order.PayOrderExtension;
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.time.LocalDateTime;
import java.util.List;
/**
* 支付订单 Service 接口
*
* @author aquan
*/
public interface PayOrderService {
/**
* 获得支付订单
*
* @param id 编号
* @return 支付订单
*/
PayOrder getOrder(Long id);
/**
* 获得支付订单
*
* @param appId 应用编号
* @param merchantOrderId 商户订单编号
* @return 支付订单
*/
PayOrder getOrder(Long appId, String merchantOrderId);
/**
* 获得指定应用的订单数量
*
* @param appId 应用编号
* @return 订单数量
*/
Long getOrderCountByAppId(Long appId);
/**
* 获得支付订单分页
*
* @param pageReqVO 分页查询
* @return 支付订单分页
*/
PageResult<PayOrder> getOrderPage(PayOrderPageReqVO pageReqVO);
/**
* 获得支付订单列表, 用于 Excel 导出
*
* @param exportReqVO 查询条件
* @return 支付订单列表
*/
List<PayOrder> getOrderList(PayOrderExportReqVO exportReqVO);
/**
* 创建支付单
*
* @param reqDTO 创建请求
* @return 支付单编号
*/
Long createOrder(@Valid PayOrderCreateReqDTO reqDTO);
/**
* 提交支付
* 此时,会发起支付渠道的调用
*
* @param reqVO 提交请求
* @param userIp 提交 IP
* @return 提交结果
*/
PayOrderSubmitRespVO submitOrder(@Valid PayOrderSubmitReqVO reqVO,
@NotEmpty(message = "提交 IP 不能为空") String userIp);
/**
* 通知支付单成功
*
* @param channelId 渠道编号
* @param notify 通知
*/
void notifyOrder(Long channelId, PayOrderRespDTO notify);
/**
* 更新支付订单的退款金额
*
* @param id 编号
* @param incrRefundPrice 增加的退款金额
*/
void updateOrderRefundPrice(Long id, Integer incrRefundPrice);
/**
* 获得支付订单
*
* @param id 编号
* @return 支付订单
*/
PayOrderExtension getOrderExtension(Long id);
/**
* 同步订单的支付状态
*
* @param minCreateTime 最小创建时间
* @return 同步到已支付的订单数量
*/
int syncOrder(LocalDateTime minCreateTime);
/**
* 将已过期的订单,状态修改为已关闭
*
* @return 过期的订单数量
*/
int expireOrder();
PayOrder selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId);
int updateByIdAndStatus(Long id, Integer status, PayOrder update);
}

View File

@ -0,0 +1,589 @@
package com.fastbee.pay.core.service.order;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fastbee.common.core.domain.PageResult;
import com.fastbee.common.utils.date.LocalDateTimeUtils;
import com.fastbee.framework.mybatis.LambdaQueryWrapperX;
import com.fastbee.pay.api.api.order.dto.PayOrderCreateReqDTO;
import com.fastbee.pay.api.enums.notify.PayNotifyTypeEnum;
import com.fastbee.pay.api.enums.order.PayOrderStatusEnum;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderExportReqVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderPageReqVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitReqVO;
import com.fastbee.pay.core.controller.admin.order.vo.PayOrderSubmitRespVO;
import com.fastbee.pay.core.convert.order.PayOrderConvert;
import com.fastbee.pay.core.domain.dataobject.app.PayApp;
import com.fastbee.pay.core.domain.dataobject.channel.PayChannel;
import com.fastbee.pay.core.domain.dataobject.order.PayOrder;
import com.fastbee.pay.core.domain.dataobject.order.PayOrderExtension;
import com.fastbee.pay.core.domain.mapper.order.PayOrderMapper;
import com.fastbee.pay.core.domain.redis.no.PayNoRedisDAO;
import com.fastbee.pay.core.framework.pay.config.PayProperties;
import com.fastbee.pay.core.service.app.PayAppService;
import com.fastbee.pay.core.service.channel.PayChannelService;
import com.fastbee.pay.core.service.notify.PayNotifyService;
import com.fastbee.pay.core.util.MoneyUtils;
import com.fastbee.pay.framework.client.PayClient;
import com.fastbee.pay.framework.client.PayClientFactory;
import com.fastbee.pay.framework.client.dto.order.PayOrderRespDTO;
import com.fastbee.pay.framework.client.dto.order.PayOrderUnifiedReqDTO;
import com.fastbee.pay.framework.enums.order.PayOrderStatusRespEnum;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import static com.fastbee.common.exception.ServiceExceptionUtil.exception;
import static com.fastbee.common.utils.json.JsonUtils.toJsonString;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.CHANNEL_NOT_FOUND;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.ORDER_EXTENSION_IS_PAID;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.ORDER_EXTENSION_NOT_FOUND;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.ORDER_EXTENSION_STATUS_IS_NOT_WAITING;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.ORDER_IS_EXPIRED;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.ORDER_NOT_FOUND;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.ORDER_REFUND_FAIL_STATUS_ERROR;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.ORDER_STATUS_IS_NOT_WAITING;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.ORDER_STATUS_IS_SUCCESS;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.ORDER_SUBMIT_CHANNEL_ERROR;
import static com.fastbee.pay.api.enums.ErrorCodeConstants.REFUND_PRICE_EXCEED;
/**
* 支付订单 Service 实现类
*
* @author aquan
*/
@Service
@Validated
@Slf4j
public class PayOrderServiceImpl implements PayOrderService {
@Resource
private PayProperties payProperties;
@Resource
private PayClientFactory payClientFactory;
@Resource
private PayOrderMapper payOrderMapper;
@Resource
private PayOrderExtensionService payOrderExtensionService;
@Resource
private PayNoRedisDAO noRedisDAO;
@Resource
private PayAppService appService;
@Resource
private PayChannelService channelService;
@Resource
private PayNotifyService notifyService;
@Override
public PayOrder getOrder(Long id) {
return payOrderMapper.selectById(id);
}
@Override
public PayOrder getOrder(Long appId, String merchantOrderId) {
return this.selectByAppIdAndMerchantOrderId(appId, merchantOrderId);
}
@Override
public Long getOrderCountByAppId(Long appId) {
return payOrderMapper.selectCount(PayOrder::getAppId, appId);
}
@Override
public PageResult<PayOrder> getOrderPage(PayOrderPageReqVO pageReqVO) {
return payOrderMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<PayOrder>()
.eqIfPresent(PayOrder::getAppId, pageReqVO.getAppId())
.eqIfPresent(PayOrder::getChannelCode, pageReqVO.getChannelCode())
.likeIfPresent(PayOrder::getMerchantOrderId, pageReqVO.getMerchantOrderId())
.likeIfPresent(PayOrder::getChannelOrderNo, pageReqVO.getChannelOrderNo())
.likeIfPresent(PayOrder::getNo, pageReqVO.getNo())
.eqIfPresent(PayOrder::getStatus, pageReqVO.getStatus())
.betweenIfPresent(PayOrder::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(PayOrder::getId));
}
@Override
public List<PayOrder> getOrderList(PayOrderExportReqVO exportReqVO) {
return payOrderMapper.selectList(new LambdaQueryWrapperX<PayOrder>()
.eqIfPresent(PayOrder::getAppId, exportReqVO.getAppId())
.eqIfPresent(PayOrder::getChannelCode, exportReqVO.getChannelCode())
.likeIfPresent(PayOrder::getMerchantOrderId, exportReqVO.getMerchantOrderId())
.likeIfPresent(PayOrder::getChannelOrderNo, exportReqVO.getChannelOrderNo())
.likeIfPresent(PayOrder::getNo, exportReqVO.getNo())
.eqIfPresent(PayOrder::getStatus, exportReqVO.getStatus())
.betweenIfPresent(PayOrder::getCreateTime, exportReqVO.getCreateTime())
.orderByDesc(PayOrder::getId));
}
@Override
public Long createOrder(PayOrderCreateReqDTO reqDTO) {
// 校验 App
PayApp app = appService.validPayApp(reqDTO.getAppId());
// 查询对应的支付交易单是否已经存在。如果是,则直接返回
PayOrder order = this.selectByAppIdAndMerchantOrderId(
reqDTO.getAppId(), reqDTO.getMerchantOrderId());
if (order != null) {
log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况
return order.getId();
}
// 创建支付交易单
order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId())
// 商户相关字段
.setNotifyUrl(app.getOrderNotifyUrl())
// 订单相关字段
.setStatus(PayOrderStatusEnum.WAITING.getStatus())
// 退款相关字段
.setRefundPrice(0);
payOrderMapper.insert(order);
return order.getId();
}
@Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtension 回滚了
public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) {
// 1.1 获得 PayOrder ,并校验其是否存在
PayOrder order = validateOrderCanSubmit(reqVO.getId());
// 1.32 校验支付渠道是否有效
PayChannel channel = validateChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
PayClient client = payClientFactory.getPayClient(channel.getId());
// 2. 插入 PayOrderExtension
String no = noRedisDAO.generate(payProperties.getOrderNoPrefix());
PayOrderExtension orderExtension = PayOrderConvert.INSTANCE.convert(reqVO, userIp)
.setOrderId(order.getId()).setNo(no)
.setChannelId(channel.getId()).setChannelCode(channel.getCode())
.setStatus(PayOrderStatusEnum.WAITING.getStatus());
payOrderExtensionService.insert(orderExtension);
// 3. 调用三方接口
PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp)
// 商户相关的字段
.setOutTradeNo(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtension.no 属性!
.setSubject(order.getSubject()).setBody(order.getBody())
.setNotifyUrl(genChannelOrderNotifyUrl(channel))
.setReturnUrl(reqVO.getReturnUrl())
// 订单相关字段
.setPrice(order.getPrice()).setExpireTime(order.getExpireTime());
PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO);
// 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功
if (unifiedOrderResp != null) {
getSelf().notifyOrder(channel, unifiedOrderResp);
// 如有渠道错误码,则抛出业务异常,提示用户
if (StrUtil.isNotEmpty(unifiedOrderResp.getChannelErrorCode())) {
throw exception(ORDER_SUBMIT_CHANNEL_ERROR, unifiedOrderResp.getChannelErrorCode(),
unifiedOrderResp.getChannelErrorMsg());
}
// 此处需要读取最新的状态
order = payOrderMapper.selectById(order.getId());
}
return PayOrderConvert.INSTANCE.convert(order, unifiedOrderResp);
}
private PayOrder validateOrderCanSubmit(Long id) {
PayOrder order = payOrderMapper.selectById(id);
if (order == null) { // 是否存在
throw exception(ORDER_NOT_FOUND);
}
if (PayOrderStatusEnum.isSuccess(order.getStatus())) { // 校验状态,发现已支付
throw exception(ORDER_STATUS_IS_SUCCESS);
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
throw exception(ORDER_STATUS_IS_NOT_WAITING);
}
if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期
throw exception(ORDER_IS_EXPIRED);
}
// 【重要】校验是否支付拓展单已支付,只是没有回调、或者数据不正常
validateOrderActuallyPaid(id);
return order;
}
/**
* 校验支付订单实际已支付
*
* @param id 支付编号
*/
@VisibleForTesting
void validateOrderActuallyPaid(Long id) {
List<PayOrderExtension> orderExtensions = payOrderExtensionService.selectListByOrderId(id);
orderExtensions.forEach(orderExtension -> {
// 情况一:校验数据库中的 orderExtension 是不是已支付
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
log.warn("[validateOrderCanSubmit][order({}) 的 extension({}) 已支付,可能是数据不一致]",
id, orderExtension.getId());
throw exception(ORDER_EXTENSION_IS_PAID);
}
// 情况二:调用三方接口,查询支付单状态,是不是已支付
PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
if (payClient == null) {
log.error("[validateOrderCanSubmit][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
return;
}
PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
if (respDTO != null && PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) {
log.warn("[validateOrderCanSubmit][order({}) 的 PayOrderRespDTO({}) 已支付,可能是回调延迟]",
id, toJsonString(respDTO));
throw exception(ORDER_EXTENSION_IS_PAID);
}
});
}
private PayChannel validateChannelCanSubmit(Long appId, String channelCode) {
// 校验 App
appService.validPayApp(appId);
// 校验支付渠道是否有效
PayChannel channel = channelService.validPayChannel(appId, channelCode);
PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) {
log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(CHANNEL_NOT_FOUND);
}
return channel;
}
/**
* 根据支付渠道的编码,生成支付渠道的回调地址
*
* @param channel 支付渠道
* @return 支付渠道的回调地址 配置地址 + "/" + channel id
*/
private String genChannelOrderNotifyUrl(PayChannel channel) {
return payProperties.getOrderNotifyUrl() + "/" + channel.getId();
}
@Override
public void notifyOrder(Long channelId, PayOrderRespDTO notify) {
// 校验支付渠道是否有效
PayChannel channel = channelService.validPayChannel(channelId);
// 更新支付订单为已支付
// TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyOrder(channel, notify));
}
/**
* 通知并更新订单的支付结果
*
* @param channel 支付渠道
* @param notify 通知
*/
@Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyPayOrder(channel, notify) 调用,否则事务不生效
public void notifyOrder(PayChannel channel, PayOrderRespDTO notify) {
// 情况一:支付成功的回调
if (PayOrderStatusRespEnum.isSuccess(notify.getStatus())) {
notifyOrderSuccess(channel, notify);
return;
}
// 情况二:支付失败的回调
if (PayOrderStatusRespEnum.isClosed(notify.getStatus())) {
notifyOrderClosed(channel, notify);
}
// 情况三WAITING无需处理
// 情况四REFUND通过退款回调处理
}
private void notifyOrderSuccess(PayChannel channel, PayOrderRespDTO notify) {
// 1. 更新 PayOrderExtension 支付成功
PayOrderExtension orderExtension = updateOrderSuccess(notify);
// 2. 更新 PayOrder 支付成功
Boolean paid = updateOrderSuccess(channel, orderExtension, notify);
if (paid) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调
return;
}
// 3. 插入支付通知记录
notifyService.createPayNotifyTask(PayNotifyTypeEnum.ORDER.getType(),
orderExtension.getOrderId());
}
/**
* 更新 PayOrderExtension 支付成功
*
* @param notify 通知
* @return PayOrderExtension 对象
*/
private PayOrderExtension updateOrderSuccess(PayOrderRespDTO notify) {
// 1. 查询 PayOrderExtension
PayOrderExtension orderExtension = payOrderExtensionService.selectByNo(notify.getOutTradeNo());
if (orderExtension == null) {
throw exception(ORDER_EXTENSION_NOT_FOUND);
}
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
log.info("[updateOrderExtensionSuccess][orderExtension({}) 已经是已支付,无需更新]", orderExtension.getId());
return orderExtension;
}
if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
// 2. 更新 PayOrderExtension
int updateCounts = payOrderExtensionService.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(),
PayOrderExtension.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(toJsonString(notify)).build());
if (updateCounts == 0) { // 校验状态,必须是待支付
throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
log.info("[updateOrderExtensionSuccess][orderExtension({}) 更新为已支付]", orderExtension.getId());
return orderExtension;
}
/**
* 更新 PayOrder 支付成功
*
* @param channel 支付渠道
* @param orderExtension 支付拓展单
* @param notify 通知回调
* @return 是否之前已经成功回调
*/
private Boolean updateOrderSuccess(PayChannel channel, PayOrderExtension orderExtension,
PayOrderRespDTO notify) {
// 1. 判断 PayOrder 是否处于待支付
PayOrder order = payOrderMapper.selectById(orderExtension.getOrderId());
if (order == null) {
throw exception(ORDER_NOT_FOUND);
}
if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新
&& Objects.equals(order.getExtensionId(), orderExtension.getId())) {
log.info("[updateOrderExtensionSuccess][order({}) 已经是已支付,无需更新]", order.getId());
return true;
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
throw exception(ORDER_STATUS_IS_NOT_WAITING);
}
// 2. 更新 PayOrder
int updateCounts = this.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(),
PayOrder.builder().status(PayOrderStatusEnum.SUCCESS.getStatus())
.channelId(channel.getId()).channelCode(channel.getCode())
.successTime(notify.getSuccessTime()).extensionId(orderExtension.getId()).no(orderExtension.getNo())
.channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId())
.channelFeeRate(channel.getFeeRate()).channelFeePrice(MoneyUtils.calculateRatePrice(order.getPrice(), channel.getFeeRate()))
.build());
if (updateCounts == 0) { // 校验状态,必须是待支付
throw exception(ORDER_STATUS_IS_NOT_WAITING);
}
log.info("[updateOrderExtensionSuccess][order({}) 更新为已支付]", order.getId());
return false;
}
private void notifyOrderClosed(PayChannel channel, PayOrderRespDTO notify) {
updateOrderExtensionClosed(channel, notify);
}
private void updateOrderExtensionClosed(PayChannel channel, PayOrderRespDTO notify) {
// 1. 查询 PayOrderExtension
PayOrderExtension orderExtension = payOrderExtensionService.selectByNo(notify.getOutTradeNo());
if (orderExtension == null) {
throw exception(ORDER_EXTENSION_NOT_FOUND);
}
if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭,直接返回,不用重复更新
log.info("[updateOrderExtensionClosed][orderExtension({}) 已经是支付关闭,无需更新]", orderExtension.getId());
return;
}
// 一般出现先是支付成功,然后支付关闭,都是全部退款导致关闭的场景。这个情况,我们不更新支付拓展单,只通过退款流程,更新支付单
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
log.info("[updateOrderExtensionClosed][orderExtension({}) 是已支付,无需更新为支付关闭]", orderExtension.getId());
return;
}
if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
// 2. 更新 PayOrderExtension
int updateCounts = payOrderExtensionService.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(),
PayOrderExtension.builder().status(PayOrderStatusEnum.CLOSED.getStatus()).channelNotifyData(toJsonString(notify))
.channelErrorCode(notify.getChannelErrorCode()).channelErrorMsg(notify.getChannelErrorMsg()).build());
if (updateCounts == 0) { // 校验状态,必须是待支付
throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
log.info("[updateOrderExtensionClosed][orderExtension({}) 更新为支付关闭]", orderExtension.getId());
}
@Override
public void updateOrderRefundPrice(Long id, Integer incrRefundPrice) {
PayOrder order = payOrderMapper.selectById(id);
if (order == null) {
throw exception(ORDER_NOT_FOUND);
}
if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) {
throw exception(ORDER_REFUND_FAIL_STATUS_ERROR);
}
if (order.getRefundPrice() + incrRefundPrice > order.getPrice()) {
throw exception(REFUND_PRICE_EXCEED);
}
// 更新订单
PayOrder updateObj = new PayOrder()
.setRefundPrice(order.getRefundPrice() + incrRefundPrice)
.setStatus(PayOrderStatusEnum.REFUND.getStatus());
int updateCount = this.updateByIdAndStatus(id, order.getStatus(), updateObj);
if (updateCount == 0) {
throw exception(ORDER_REFUND_FAIL_STATUS_ERROR);
}
}
@Override
public PayOrderExtension getOrderExtension(Long id) {
return payOrderExtensionService.selectById(id);
}
@Override
public int syncOrder(LocalDateTime minCreateTime) {
// 1. 查询指定创建时间内的待支付订单
List<PayOrderExtension> orderExtensions = payOrderExtensionService.selectListByStatusAndCreateTimeGe(
PayOrderStatusEnum.WAITING.getStatus(), minCreateTime);
if (CollUtil.isEmpty(orderExtensions)) {
return 0;
}
// 2. 遍历执行
int count = 0;
for (PayOrderExtension orderExtension : orderExtensions) {
count += syncOrder(orderExtension) ? 1 : 0;
}
return count;
}
/**
* 同步单个支付拓展单
*
* @param orderExtension 支付拓展单
* @return 是否已支付
*/
private boolean syncOrder(PayOrderExtension orderExtension) {
try {
// 1.1 查询支付订单信息
PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
if (payClient == null) {
log.error("[syncOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
return false;
}
PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
// 1.2 回调支付结果
notifyOrder(orderExtension.getChannelId(), respDTO);
// 2. 如果是已支付,则返回 true
return PayOrderStatusRespEnum.isSuccess(respDTO.getStatus());
} catch (Throwable e) {
log.error("[syncOrder][orderExtension({}) 同步支付状态异常]", orderExtension.getId(), e);
return false;
}
}
@Override
public int expireOrder() {
// 1. 查询过期的待支付订单
List<PayOrder> orders = payOrderMapper.selectList(new LambdaQueryWrapper<PayOrder>()
.eq(PayOrder::getStatus, PayOrderStatusEnum.WAITING.getStatus())
.lt(PayOrder::getExpireTime, LocalDateTime.now()));
if (CollUtil.isEmpty(orders)) {
return 0;
}
// 2. 遍历执行
int count = 0;
for (PayOrder order : orders) {
count += expireOrder(order) ? 1 : 0;
}
return count;
}
@Override
public PayOrder selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) {
return payOrderMapper.selectOne(PayOrder::getAppId, appId,
PayOrder::getMerchantOrderId, merchantOrderId);
}
@Override
public int updateByIdAndStatus(Long id, Integer status, PayOrder update) {
return payOrderMapper.update(update, new LambdaQueryWrapper<PayOrder>()
.eq(PayOrder::getId, id).eq(PayOrder::getStatus, status));
}
/**
* 同步单个支付单
*
* @param order 支付单
* @return 是否已过期
*/
private boolean expireOrder(PayOrder order) {
try {
// 1. 需要先处理关联的支付拓展单,避免错误的过期已支付 or 已退款的订单
List<PayOrderExtension> orderExtensions = payOrderExtensionService.selectListByOrderId(order.getId());
for (PayOrderExtension orderExtension : orderExtensions) {
if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) {
continue;
}
// 情况一:校验数据库中的 orderExtension 是不是已支付
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
log.error("[expireOrder][order({}) 的 extension({}) 已支付,可能是数据不一致]",
order.getId(), orderExtension.getId());
return false;
}
// 情况二:调用三方接口,查询支付单状态,是不是已支付/已退款
PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
if (payClient == null) {
log.error("[expireOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
return false;
}
PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
if (PayOrderStatusRespEnum.isRefund(respDTO.getStatus())) {
// 补充说明:按道理,应该是 WAITING => SUCCESS => REFUND 状态,如果直接 WAITING => REFUND 状态,说明中间丢了过程
// 此时,需要人工介入,手工补齐数据,保持 WAITING => SUCCESS => REFUND 的过程
log.error("[expireOrder][extension({}) 的 PayOrderRespDTO({}) 已退款,可能是回调延迟]",
orderExtension.getId(), toJsonString(respDTO));
return false;
}
if (PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) {
notifyOrder(orderExtension.getChannelId(), respDTO);
return false;
}
// 兜底逻辑:将支付拓展单更新为已关闭
PayOrderExtension updateObj = new PayOrderExtension().setStatus(PayOrderStatusEnum.CLOSED.getStatus())
.setChannelNotifyData(toJsonString(respDTO));
if (payOrderExtensionService.updateByIdAndStatus(orderExtension.getId(), PayOrderStatusEnum.WAITING.getStatus(),
updateObj) == 0) {
log.error("[expireOrder][extension({}) 更新为支付关闭失败]", orderExtension.getId());
return false;
}
log.info("[expireOrder][extension({}) 更新为支付关闭成功]", orderExtension.getId());
}
// 2. 都没有上述情况,可以安心更新为已关闭
PayOrder updateObj = new PayOrder().setStatus(PayOrderStatusEnum.CLOSED.getStatus());
if (this.updateByIdAndStatus(order.getId(), order.getStatus(), updateObj) == 0) {
log.error("[expireOrder][order({}) 更新为支付关闭失败]", order.getId());
return false;
}
log.info("[expireOrder][order({}) 更新为支付关闭失败]", order.getId());
return true;
} catch (Throwable e) {
log.error("[expireOrder][order({}) 过期订单异常]", order.getId(), e);
return false;
}
}
/**
* 获得自身的代理对象,解决 AOP 生效问题
*
* @return 自己
*/
private PayOrderServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}

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