增加 pay 支付服务

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

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.pay;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 项目的启动类
*
* 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
* 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
* 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
*
* @author 芋道源码
*/
@SpringBootApplication
public class PayServerApplication {
public static void main(String[] args) {
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
SpringApplication.run(PayServerApplication.class, args);
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
}
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.pay.api.order;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import static cn.iocoder.yudao.module.system.enums.ApiConstants.VERSION;
@Service
@RestController // 提供 RESTful API 接口,给 Feign 调用
@DubboService(version = VERSION) // 提供 Dubbo RPC 接口,给 Dubbo Consumer 调用
@Validated
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) {
PayOrderDO order = payOrderService.getOrder(id);
return PayOrderConvert.INSTANCE.convert2(order);
}
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.pay.api.refund;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import static cn.iocoder.yudao.module.system.enums.ApiConstants.VERSION;
@Service
@RestController // 提供 RESTful API 接口,给 Feign 调用
@DubboService(version = VERSION) // 提供 Dubbo RPC 接口,给 Dubbo Consumer 调用
@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 cn.iocoder.yudao.module.pay.controller.admin.app;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.*;
import cn.iocoder.yudao.module.pay.convert.app.PayAppConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import 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.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@Slf4j
@Tag(name = "管理后台 - 支付应用信息")
@RestController
@RequestMapping("/pay/app")
@Validated
public class PayAppController {
@Resource
private PayAppService appService;
@Resource
private PayChannelService channelService;
@PostMapping("/create")
@Operation(summary = "创建支付应用信息")
@PreAuthorize("@ss.hasPermission('pay:app:create')")
public CommonResult<Long> createApp(@Valid @RequestBody PayAppCreateReqVO createReqVO) {
return success(appService.createApp(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新支付应用信息")
@PreAuthorize("@ss.hasPermission('pay:app:update')")
public CommonResult<Boolean> updateApp(@Valid @RequestBody PayAppUpdateReqVO updateReqVO) {
appService.updateApp(updateReqVO);
return success(true);
}
@PutMapping("/update-status")
@Operation(summary = "更新支付应用状态")
@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")
@Operation(summary = "删除支付应用信息")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('pay:app:delete')")
public CommonResult<Boolean> deleteApp(@RequestParam("id") Long id) {
appService.deleteApp(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得支付应用信息")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('pay:app:query')")
public CommonResult<PayAppRespVO> getApp(@RequestParam("id") Long id) {
PayAppDO app = appService.getApp(id);
return success(PayAppConvert.INSTANCE.convert(app));
}
@GetMapping("/page")
@Operation(summary = "获得支付应用信息分页")
@PreAuthorize("@ss.hasPermission('pay:app:query')")
public CommonResult<PageResult<PayAppPageItemRespVO>> getAppPage(@Valid PayAppPageReqVO pageVO) {
// 得到应用分页列表
PageResult<PayAppDO> pageResult = appService.getAppPage(pageVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty());
}
// 得到所有的应用编号,查出所有的渠道
Collection<Long> appIds = convertList(pageResult.getList(), PayAppDO::getId);
List<PayChannelDO> channels = channelService.getChannelListByAppIds(appIds);
// 拼接后返回
return success(PayAppConvert.INSTANCE.convertPage(pageResult, channels));
}
@GetMapping("/list")
@Operation(summary = "获得应用列表")
@PreAuthorize("@ss.hasPermission('pay:merchant:query')")
public CommonResult<List<PayAppRespVO>> getAppList() {
List<PayAppDO> appListDO = appService.getAppList();
return success(PayAppConvert.INSTANCE.convertList(appListDO));
}
}

View File

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

View File

@@ -0,0 +1,11 @@
package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
@Schema(description = "管理后台 - 支付应用信息创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppCreateReqVO extends PayAppBaseVO {
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 支付应用信息 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppRespVO extends PayAppBaseVO {
@Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import javax.validation.constraints.*;
@Schema(description = "管理后台 - 支付应用信息更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppUpdateReqVO extends PayAppBaseVO {
@Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "应用编号不能为空")
private Long id;
}

View File

@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 应用更新状态 Request VO")
@Data
public class PayAppUpdateStatusReqVO {
@Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "应用编号不能为空")
private Long id;
@Schema(description = "状态,见 SysCommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "状态不能为空")
private Integer status;
}

View File

@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.pay.controller.admin.channel;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO;
import cn.iocoder.yudao.module.pay.convert.channel.PayChannelConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import 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.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - 支付渠道")
@RestController
@RequestMapping("/pay/channel")
@Validated
public class PayChannelController {
@Resource
private PayChannelService channelService;
@PostMapping("/create")
@Operation(summary = "创建支付渠道 ")
@PreAuthorize("@ss.hasPermission('pay:channel:create')")
public CommonResult<Long> createChannel(@Valid @RequestBody PayChannelCreateReqVO createReqVO) {
return success(channelService.createChannel(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新支付渠道 ")
@PreAuthorize("@ss.hasPermission('pay:channel:update')")
public CommonResult<Boolean> updateChannel(@Valid @RequestBody PayChannelUpdateReqVO updateReqVO) {
channelService.updateChannel(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除支付渠道 ")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('pay:channel:delete')")
public CommonResult<Boolean> deleteChannel(@RequestParam("id") Long id) {
channelService.deleteChannel(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得支付渠道")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('pay:channel:query')")
public CommonResult<PayChannelRespVO> getChannel(@RequestParam(value = "id", required = false) Long id,
@RequestParam(value = "appId", required = false) Long appId,
@RequestParam(value = "code", required = false) String code) {
PayChannelDO channel = null;
if (id != null) {
channel = channelService.getChannel(id);
} else if (appId != null && code != null) {
channel = channelService.getChannelByAppIdAndCode(appId, code);
}
return success(PayChannelConvert.INSTANCE.convert(channel));
}
@GetMapping("/get-enable-code-list")
@Operation(summary = "获得指定应用的开启的支付渠道编码列表")
@Parameter(name = "appId", description = "应用编号", required = true, example = "1")
public CommonResult<Set<String>> getEnableChannelCodeList(@RequestParam("appId") Long appId) {
List<PayChannelDO> channels = channelService.getEnableChannelList(appId);
return success(convertSet(channels, PayChannelDO::getCode));
}
}

View File

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

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.pay.controller.admin.channel.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 支付渠道 创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayChannelCreateReqVO extends PayChannelBaseVO {
@Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_pc")
@NotNull(message = "渠道编码不能为空")
private String code;
@Schema(description = "渠道配置的 json 字符串")
@NotBlank(message = "渠道配置不能为空")
private String config;
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.pay.controller.admin.channel.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 支付渠道 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayChannelRespVO extends PayChannelBaseVO {
@Schema(description = "商户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private LocalDateTime createTime;
@Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_pc")
private String code;
@Schema(description = "配置", requiredMode = Schema.RequiredMode.REQUIRED)
private String config;
}

View File

@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.pay.controller.admin.channel.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import javax.validation.constraints.*;
@Schema(description = "管理后台 - 支付渠道 更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayChannelUpdateReqVO extends PayChannelBaseVO {
@Schema(description = "商户编号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "商户编号不能为空")
private Long id;
@Schema(description = "渠道配置的json字符串")
@NotBlank(message = "渠道配置不能为空")
private String config;
}

View File

@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.pay.controller.admin.demo;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO;
import cn.iocoder.yudao.module.pay.convert.demo.PayDemoOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import cn.iocoder.yudao.module.pay.service.demo.PayDemoOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - 示例订单")
@RestController
@RequestMapping("/pay/demo-order")
@Validated
public class PayDemoOrderController {
@Resource
private PayDemoOrderService payDemoOrderService;
@PostMapping("/create")
@Operation(summary = "创建示例订单")
public CommonResult<Long> createDemoOrder(@Valid @RequestBody PayDemoOrderCreateReqVO createReqVO) {
return success(payDemoOrderService.createDemoOrder(getLoginUserId(), createReqVO));
}
@GetMapping("/page")
@Operation(summary = "获得示例订单分页")
public CommonResult<PageResult<PayDemoOrderRespVO>> getDemoOrderPage(@Valid PageParam pageVO) {
PageResult<PayDemoOrderDO> pageResult = payDemoOrderService.getDemoOrderPage(pageVO);
return success(PayDemoOrderConvert.INSTANCE.convertPage(pageResult));
}
@PostMapping("/update-paid")
@Operation(summary = "更新示例订单为已支付") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob
@PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现
@OperateLog(enable = false) // 禁用操作日志,因为没有操作人
public CommonResult<Boolean> updateDemoOrderPaid(@RequestBody PayOrderNotifyReqDTO notifyReqDTO) {
payDemoOrderService.updateDemoOrderPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()),
notifyReqDTO.getPayOrderId());
return success(true);
}
@PutMapping("/refund")
@Operation(summary = "发起示例订单的退款")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<Boolean> refundDemoOrder(@RequestParam("id") Long id) {
payDemoOrderService.refundDemoOrder(id, getClientIP());
return success(true);
}
@PostMapping("/update-refunded")
@Operation(summary = "更新示例订单为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob
@PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现
@OperateLog(enable = false) // 禁用操作日志,因为没有操作人
public CommonResult<Boolean> updateDemoOrderRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
payDemoOrderService.updateDemoOrderRefunded(Long.valueOf(notifyReqDTO.getMerchantOrderId()),
notifyReqDTO.getPayRefundId());
return success(true);
}
}

View File

@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.pay.controller.admin.demo.vo;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 示例订单创建 Request VO")
@Data
public class PayDemoOrderCreateReqVO {
@Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17682")
@NotNull(message = "商品编号不能为空")
private Long spuId;
}

View File

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

View File

@@ -0,0 +1,126 @@
package cn.iocoder.yudao.module.pay.controller.admin.notify;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO;
import cn.iocoder.yudao.module.pay.convert.notify.PayNotifyTaskConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import 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.annotation.security.PermitAll;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.CHANNEL_NOT_FOUND;
@Tag(name = "管理后台 - 回调通知")
@RestController
@RequestMapping("/pay/notify")
@Validated
@Slf4j
public class PayNotifyController {
@Resource
private PayOrderService orderService;
@Resource
private PayRefundService refundService;
@Resource
private PayNotifyService notifyService;
@Resource
private PayAppService appService;
@Resource
private PayClientFactory payClientFactory;
@PostMapping(value = "/order/{channelId}")
@Operation(summary = "支付渠道的统一【支付】回调")
@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 "success";
}
@PostMapping(value = "/refund/{channelId}")
@Operation(summary = "支付渠道的统一【退款】回调")
@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 "success";
}
@GetMapping("/get-detail")
@Operation(summary = "获得回调通知的明细")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('pay:notify:query')")
public CommonResult<PayNotifyTaskDetailRespVO> getNotifyTaskDetail(@RequestParam("id") Long id) {
PayNotifyTaskDO task = notifyService.getNotifyTask(id);
if (task == null) {
return success(null);
}
// 拼接返回
PayAppDO app = appService.getApp(task.getAppId());
List<PayNotifyLogDO> logs = notifyService.getNotifyLogList(id);
return success(PayNotifyTaskConvert.INSTANCE.convert(task, app, logs));
}
@GetMapping("/page")
@Operation(summary = "获得回调通知分页")
@PreAuthorize("@ss.hasPermission('pay:notify:query')")
public CommonResult<PageResult<PayNotifyTaskRespVO>> getNotifyTaskPage(@Valid PayNotifyTaskPageReqVO pageVO) {
PageResult<PayNotifyTaskDO> pageResult = notifyService.getNotifyTaskPage(pageVO);
// 拼接返回
Map<Long, PayAppDO> appMap = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTaskDO::getAppId));
return success(PayNotifyTaskConvert.INSTANCE.convertPage(pageResult, appMap));
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.pay.controller.admin.notify.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 回调通知 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayNotifyTaskRespVO extends PayNotifyTaskBaseVO {
@Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3380")
private Long id;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "应用名称", example = "wx_pay")
private String appName;
}

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.pay.controller.admin.order.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 支付订单详细信息 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayOrderDetailsRespVO extends PayOrderBaseVO {
@Schema(description = "支付订单编号", required = true, example = "1024")
private Long id;
@Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
private String appName;
@Schema(description = "创建时间", required = true)
private LocalDateTime createTime;
@Schema(description = "更新时间", required = true)
private LocalDateTime updateTime;
/**
* 支付订单扩展
*/
private PayOrderExtension extension;
@Data
@Schema(description = "支付订单扩展")
public static class PayOrderExtension {
@Schema(description = "支付订单号", required = true, example = "1024")
private String no;
@Schema(description = "支付异步通知的内容")
private String channelNotifyData;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.pay.controller.admin.order.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 支付订单分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayOrderPageItemRespVO extends PayOrderBaseVO {
@Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "应用名称", example = "wx_pay")
private String appName;
}

View File

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

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.pay.controller.admin.order.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 支付订单 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayOrderRespVO extends PayOrderBaseVO {
@Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long id;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

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

View File

@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.pay.controller.admin.order.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 支付订单提交 Response VO")
@Data
public class PayOrderSubmitRespVO {
@Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") // 参见 PayOrderStatusEnum 枚举
private Integer status;
@Schema(description = "展示模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "url") // 参见 PayDisplayModeEnum 枚举
private String displayMode;
@Schema(description = "展示内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String displayContent;
}

View File

@@ -0,0 +1,96 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund;
import cn.hutool.core.collection.CollectionUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.*;
import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import 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.ArrayList;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Tag(name = "管理后台 - 退款订单")
@RestController
@RequestMapping("/pay/refund")
@Validated
public class PayRefundController {
@Resource
private PayRefundService refundService;
@Resource
private PayAppService appService;
@GetMapping("/get")
@Operation(summary = "获得退款订单")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('pay:refund:query')")
public CommonResult<PayRefundDetailsRespVO> getRefund(@RequestParam("id") Long id) {
PayRefundDO refund = refundService.getRefund(id);
if (refund == null) {
return success(new PayRefundDetailsRespVO());
}
// 拼接数据
PayAppDO app = appService.getApp(refund.getAppId());
return success(PayRefundConvert.INSTANCE.convert(refund, app));
}
@GetMapping("/page")
@Operation(summary = "获得退款订单分页")
@PreAuthorize("@ss.hasPermission('pay:refund:query')")
public CommonResult<PageResult<PayRefundPageItemRespVO>> getRefundPage(@Valid PayRefundPageReqVO pageVO) {
PageResult<PayRefundDO> pageResult = refundService.getRefundPage(pageVO);
if (CollectionUtil.isEmpty(pageResult.getList())) {
return success(new PageResult<>(pageResult.getTotal()));
}
// 处理应用ID数据
Map<Long, PayAppDO> appMap = appService.getAppMap(convertList(pageResult.getList(), PayRefundDO::getAppId));
return success(PayRefundConvert.INSTANCE.convertPage(pageResult, appMap));
}
@GetMapping("/export-excel")
@Operation(summary = "导出退款订单 Excel")
@PreAuthorize("@ss.hasPermission('pay:refund:export')")
@OperateLog(type = EXPORT)
public void exportRefundExcel(@Valid PayRefundExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
List<PayRefundDO> list = refundService.getRefundList(exportReqVO);
if (CollectionUtil.isEmpty(list)) {
ExcelUtils.write(response, "退款订单.xls", "数据",
PayRefundExcelVO.class, new ArrayList<>());
return;
}
// 拼接返回
Map<Long, PayAppDO> appMap = appService.getAppMap(convertList(list, PayRefundDO::getAppId));
List<PayRefundExcelVO> excelList = PayRefundConvert.INSTANCE.convertList(list, appMap);
// 导出 Excel
ExcelUtils.write(response, "退款订单.xls", "数据", PayRefundExcelVO.class, excelList);
}
}

View File

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

View File

@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 退款订单详情 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundDetailsRespVO extends PayRefundBaseVO {
@Schema(description = "支付退款编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long id;
@Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是芋艿")
private String appName;
@Schema(description = "支付订单", requiredMode = Schema.RequiredMode.REQUIRED)
private Order order;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "管理后台 - 支付订单")
@Data
public static class Order {
@Schema(description = "商品标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆")
private String subject;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 退款订单分页查询 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundPageItemRespVO extends PayRefundBaseVO {
@Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是芋艿")
private String appName;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

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

View File

@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.pay.controller.app.channel;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import 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 cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "用户 App - 支付渠道")
@RestController
@RequestMapping("/pay/channel")
@Validated
public class AppPayChannelController {
@Resource
private PayChannelService channelService;
@GetMapping("/get-enable-code-list")
@Operation(summary = "获得指定应用的开启的支付渠道编码列表")
@Parameter(name = "appId", description = "应用编号", required = true, example = "1")
public CommonResult<Set<String>> getEnableChannelCodeList(@RequestParam("appId") Long appId) {
List<PayChannelDO> channels = channelService.getEnableChannelList(appId);
return success(convertSet(channels, PayChannelDO::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,47 @@
package cn.iocoder.yudao.module.pay.controller.app.order;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO;
import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO;
import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
@Tag(name = "用户 APP - 支付订单")
@RestController
@RequestMapping("/pay/order")
@Validated
@Slf4j
public class AppPayOrderController {
@Resource
private PayOrderService payOrderService;
// TODO 芋艿:临时 demo技术打样。
@GetMapping("/get")
@Operation(summary = "获得支付订单")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<PayOrderRespVO> getOrder(@RequestParam("id") Long id) {
return success(PayOrderConvert.INSTANCE.convert(payOrderService.getOrder(id)));
}
@PostMapping("/submit")
@Operation(summary = "提交支付订单")
public CommonResult<AppPayOrderSubmitRespVO> submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) {
PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
return success(PayOrderConvert.INSTANCE.convert3(respVO));
}
}

View File

@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.pay.controller.app.order.vo;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Map;
@Schema(description = "用户 APP - 支付订单提交 Request VO")
@Data
public class AppPayOrderSubmitReqVO extends PayOrderSubmitReqVO {
}

View File

@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.pay.controller.app.order.vo;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Schema(description = "用户 APP - 支付订单提交 Response VO")
@Data
public class AppPayOrderSubmitRespVO extends PayOrderSubmitRespVO {
}

View File

@@ -0,0 +1,4 @@
/**
* TODO 芋艿:占个位置,没啥用
*/
package cn.iocoder.yudao.module.pay.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 cn.iocoder.yudao.module.pay.controller;

View File

@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.pay.convert.app;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageItemRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
/**
* 支付应用信息 Convert
*
* @author 芋艿
*/
@Mapper
public interface PayAppConvert {
PayAppConvert INSTANCE = Mappers.getMapper(PayAppConvert.class);
PayAppPageItemRespVO pageConvert (PayAppDO bean);
PayAppDO convert(PayAppCreateReqVO bean);
PayAppDO convert(PayAppUpdateReqVO bean);
PayAppRespVO convert(PayAppDO bean);
List<PayAppRespVO> convertList(List<PayAppDO> list);
PageResult<PayAppPageItemRespVO> convertPage(PageResult<PayAppDO> page);
default PageResult<PayAppPageItemRespVO> convertPage(PageResult<PayAppDO> pageResult, List<PayChannelDO> channels) {
PageResult<PayAppPageItemRespVO> voPageResult = convertPage(pageResult);
// 处理 channel 关系
Map<Long, Set<String>> appIdChannelMap = CollectionUtils.convertMultiMap2(channels, PayChannelDO::getAppId, PayChannelDO::getCode);
voPageResult.getList().forEach(app -> app.setChannelCodes(appIdChannelMap.get(app.getId())));
return voPageResult;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
/**
* 提供 POJO 类的实体转换
*
* 目前使用 MapStruct 框架
*/
package cn.iocoder.yudao.module.pay.convert;

View File

@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.pay.convert.refund;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundDetailsRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExcelVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageItemRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.Map;
@Mapper
public interface PayRefundConvert {
PayRefundConvert INSTANCE = Mappers.getMapper(PayRefundConvert.class);
default PayRefundDetailsRespVO convert(PayRefundDO refund, PayAppDO app) {
PayRefundDetailsRespVO respVO = convert(refund);
if (app != null) {
respVO.setAppName(app.getName());
}
return respVO;
}
PayRefundDetailsRespVO convert(PayRefundDO bean);
PayRefundDetailsRespVO.Order convert(PayOrderDO bean);
default PageResult<PayRefundPageItemRespVO> convertPage(PageResult<PayRefundDO> page, Map<Long, PayAppDO> 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<PayRefundDO> page);
PayRefundDO convert(PayRefundCreateReqDTO bean);
PayRefundRespDTO convert02(PayRefundDO bean);
default List<PayRefundExcelVO> convertList(List<PayRefundDO> list, Map<Long, PayAppDO> appMap) {
return CollectionUtils.convertList(list, order -> {
PayRefundExcelVO excelVO = convertExcel(order);
MapUtils.findAndThen(appMap, order.getAppId(), app -> excelVO.setAppName(app.getName()));
return excelVO;
});
}
PayRefundExcelVO convertExcel(PayRefundDO bean);
}

View File

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

View File

@@ -0,0 +1,69 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.channel;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
/**
* 支付渠道 DO
* 一个应用下,会有多种支付渠道,例如说微信支付、支付宝支付等等
*
* 即 PayAppDO : PayChannelDO = 1 : n
*
* @author 芋道源码
*/
@TableName(value = "pay_channel", autoResultMap = true)
@KeySequence("pay_channel_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayChannelDO extends TenantBaseDO {
/**
* 渠道编号,数据库自增
*/
private Long id;
/**
* 渠道编码
*
* 枚举 {@link PayChannelEnum}
*/
private String code;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 渠道费率,单位:百分比
*/
private Double feeRate;
/**
* 备注
*/
private String remark;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 支付渠道配置
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private PayClientConfig config;
}

View File

@@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.demo;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 示例订单
*
* 演示业务系统的订单,如何接入 pay 系统的支付与退款
*
* @author 芋道源码
*/
@TableName("pay_demo_order")
@KeySequence("pay_demo_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayDemoOrderDO extends BaseDO {
/**
* 订单编号,自增
*/
@TableId
private Long id;
/**
* 用户编号
*/
private Long userId;
/**
* 商品编号
*/
private Long spuId;
/**
* 商品名称
*/
private String spuName;
/**
* 价格,单位:分
*/
private Integer price;
// ========== 支付相关字段 ==========
/**
* 是否支付
*/
private Boolean payStatus;
/**
* 支付订单编号
*
* 对接 pay-module-biz 支付服务的支付订单编号,即 PayOrderDO 的 id 编号
*/
private Long payOrderId;
/**
* 付款时间
*/
private LocalDateTime payTime;
/**
* 支付渠道
*
* 对应 PayChannelEnum 枚举
*/
private String payChannelCode;
// ========== 退款相关字段 ==========
/**
* 支付退款单号
*/
private Long payRefundId;
/**
* 退款金额,单位:分
*/
private Integer refundPrice;
/**
* 退款完成时间
*/
private LocalDateTime refundTime;
}

View File

@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.member;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
// TODO @jason修改 MemberWalletDO 为 PayWalletDO
/**
* 支付 - 会员钱包 DO
*
* @author jason
*/
@TableName(value ="pay_member_wallet")
@KeySequence("pay_member_wallet_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
public class MemberWalletDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
// TODO @jaosn增加 userType 字段;
/**
* 用户 id
*
* 关联 MemberUserDO 的 id 编号
* 关联 AdminUserDO 的 id 编号
*/
private Long userId;
/**
* 余额, 单位分
*/
private Integer balance;
/**
* 累计支出, 单位分
*/
private Integer totalSpending;
/**
* 累计充值, 单位分
*/
private Integer totalTopUp;
}

View File

@@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.member;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.pay.enums.member.WalletOperateTypeEnum;
import cn.iocoder.yudao.module.pay.enums.member.WalletTransactionGategoryEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 支付-会员钱包明细 DO
*
* @author jason
*/
@TableName(value ="pay_member_wallet_transaction")
@KeySequence("pay_member_wallet_transaction_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
public class MemberWalletTransactionDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 会员钱包 id
*
* 关联 {@link MemberWalletDO#getId()}
*/
private Long walletId;
/**
* 用户 id
*
* 关联 MemberUserDO 的 id 编号
*/
private Long userId;
/**
* 交易单号 @芋艿 这里是关联交易单号, 还是订单号 , 退款单号! ??
*/
private String tradeNo;
/**
* 交易分类
*
* 枚举 {@link WalletTransactionGategoryEnum#getCategory()}
*/
private Integer category;
/**
* 操作分类
*
* 枚举 {@link WalletOperateTypeEnum#getType()}
*/
private Integer operateType;
/**
* 操作详情
*/
private String operateDesc;
/**
* 交易金额, 单位分
*/
private Integer price;
/**
* 余额, 单位分
*/
private Integer balance;
/**
* 备注
*/
private String mark;
/**
* 交易时间
*/
private LocalDateTime transactionTime;
}

View File

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

View File

@@ -0,0 +1,96 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.notify;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 支付通知
* 在支付系统收到支付渠道的支付、退款的结果后,需要不断的通知到业务系统,直到成功。
*
* @author 芋道源码
*/
@TableName("pay_notify_task")
@KeySequence("pay_notify_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class PayNotifyTaskDO extends TenantBaseDO {
/**
* 通知频率,单位为秒。
*
* 算上首次的通知,实际是一共 1 + 8 = 9 次。
*/
public static final Integer[] NOTIFY_FREQUENCY = new Integer[]{
15, 15, 30, 180,
1800, 1800, 1800, 3600
};
/**
* 编号,自增
*/
@TableId
private Long id;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 通知类型
*
* 外键 {@link PayNotifyTypeEnum}
*/
private Integer type;
/**
* 数据编号,根据不同 type 进行关联:
*
* 1. {@link PayNotifyTypeEnum#ORDER} 时,关联 {@link PayOrderDO#getId()}
* 2. {@link PayNotifyTypeEnum#REFUND} 时,关联 {@link PayRefundDO#getId()}
*/
private Long dataId;
/**
* 商户订单编号
*/
private String merchantOrderId;
/**
* 通知状态
*
* 外键 {@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,138 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.order;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 支付订单 DO
*
* @author 芋道源码
*/
@TableName("pay_order")
@KeySequence("pay_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderDO extends BaseDO {
/**
* 订单编号,数据库自增
*/
private Long id;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 渠道编号
*
* 关联 {@link PayChannelDO#getId()}
*/
private Long channelId;
/**
* 渠道编码
*
* 枚举 {@link PayChannelEnum}
*/
private String channelCode;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
*
* 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
*/
private String merchantOrderId;
/**
* 商品标题
*/
private String subject;
/**
* 商品描述信息
*/
private String body;
/**
* 异步通知地址
*/
private String notifyUrl;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
*/
private Integer price;
/**
* 渠道手续费,单位:百分比
*
* 冗余 {@link PayChannelDO#getFeeRate()}
*/
private Double channelFeeRate;
/**
* 渠道手续金额,单位:分
*/
private Integer channelFeePrice;
/**
* 支付状态
*
* 枚举 {@link PayOrderStatusEnum}
*/
private Integer status;
/**
* 用户 IP
*/
private String userIp;
/**
* 订单失效时间
*/
private LocalDateTime expireTime;
/**
* 订单支付成功时间
*/
private LocalDateTime successTime;
/**
* 支付成功的订单拓展单编号
*
* 关联 {@link PayOrderExtensionDO#getId()}
*/
private Long extensionId;
/**
* 支付成功的外部订单号
*
* 关联 {@link PayOrderExtensionDO#getNo()}
*/
private String no;
// ========== 退款相关字段 ==========
/**
* 退款总金额,单位:分
*/
private Integer refundPrice;
// ========== 渠道相关字段 ==========
/**
* 渠道用户编号
*
* 例如说,微信 openid、支付宝账号
*/
private String channelUserId;
/**
* 渠道订单号
*/
private String channelOrderNo;
}

View File

@@ -0,0 +1,96 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.order;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import java.util.Map;
/**
* 支付订单拓展 DO
*
* 每次调用支付渠道,都会生成一条对应记录
*
* @author 芋道源码
*/
@TableName(value = "pay_order_extension",autoResultMap = true)
@KeySequence("pay_order_extension_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderExtensionDO extends BaseDO {
/**
* 订单拓展编号,数据库自增
*/
private Long id;
/**
* 外部订单号,根据规则生成
*
* 调用支付渠道时,使用该字段作为对接的订单号:
* 1. 微信支付:对应 <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 PayOrderDO#getId()}
*/
private Long orderId;
/**
* 渠道编号
*
* 关联 {@link PayChannelDO#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,160 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.refund;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 支付退款单 DO
* 一个支付订单,可以拥有多个支付退款单
*
* 即 PayOrderDO : PayRefundDO = 1 : n
*
* @author 芋道源码
*/
@TableName("pay_refund")
@KeySequence("pay_refund_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayRefundDO extends BaseDO {
/**
* 退款单编号,数据库自增
*/
@TableId
private Long id;
/**
* 外部退款号,根据规则生成
*
* 调用支付渠道时,使用该字段作为对接的退款号:
* 1. 微信退款:对应 <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 PayAppDO#getId()}
*/
private Long appId;
/**
* 渠道编号
*
* 关联 {@link PayChannelDO#getId()}
*/
private Long channelId;
/**
* 商户编码
*
* 枚举 {@link PayChannelEnum}
*/
private String channelCode;
/**
* 订单编号
*
* 关联 {@link PayOrderDO#getId()}
*/
private Long orderId;
/**
* 支付订单编号
*
* 冗余 {@link PayOrderDO#getNo()}
*/
private String orderNo;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
*
* 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
*/
private String merchantOrderId;
/**
* 商户退款订单号
*
* 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
*/
private String merchantRefundId;
/**
* 异步通知地址
*/
private String notifyUrl;
// ========== 退款相关字段 ==========
/**
* 退款状态
*
* 枚举 {@link PayRefundStatusEnum}
*/
private Integer status;
/**
* 支付金额,单位:分
*/
private Integer payPrice;
/**
* 退款金额,单位:分
*/
private Integer refundPrice;
/**
* 退款原因
*/
private String reason;
/**
* 用户 IP
*/
private String userIp;
// ========== 渠道相关字段 ==========
/**
* 渠道订单号
*
* 冗余 {@link PayOrderDO#getChannelOrderNo()}
*/
private String channelOrderNo;
/**
* 渠道退款单号
*
* 1. 微信退款:对应 <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,22 @@
package cn.iocoder.yudao.module.pay.dal.mysql.app;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayAppMapper extends BaseMapperX<PayAppDO> {
default PageResult<PayAppDO> selectPage(PayAppPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PayAppDO>()
.likeIfPresent(PayAppDO::getName, reqVO.getName())
.eqIfPresent(PayAppDO::getStatus, reqVO.getStatus())
.betweenIfPresent(PayAppDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayAppDO::getId));
}
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.pay.dal.mysql.channel;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
@Mapper
public interface PayChannelMapper extends BaseMapperX<PayChannelDO> {
default PayChannelDO selectByAppIdAndCode(Long appId, String code) {
return selectOne(PayChannelDO::getAppId, appId, PayChannelDO::getCode, code);
}
default List<PayChannelDO> selectListByAppIds(Collection<Long> appIds){
return selectList(PayChannelDO::getAppId, appIds);
}
default List<PayChannelDO> selectListByAppId(Long appId, Integer status) {
return selectList(new LambdaQueryWrapperX<PayChannelDO>()
.eq(PayChannelDO::getAppId, appId)
.eq(PayChannelDO::getStatus, status));
}
@Select("SELECT COUNT(*) FROM pay_channel WHERE update_time > #{maxUpdateTime}")
Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime);
}

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.pay.dal.mysql.demo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 示例订单 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface PayDemoOrderMapper extends BaseMapperX<PayDemoOrderDO> {
default PageResult<PayDemoOrderDO> selectPage(PageParam reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PayDemoOrderDO>()
.orderByDesc(PayDemoOrderDO::getId));
}
default int updateByIdAndPayed(Long id, boolean wherePayed, PayDemoOrderDO updateObj) {
return update(updateObj, new LambdaQueryWrapperX<PayDemoOrderDO>()
.eq(PayDemoOrderDO::getId, id).eq(PayDemoOrderDO::getPayStatus, wherePayed));
}
}

View File

@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.pay.dal.mysql.member;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.pay.dal.dataobject.member.MemberWalletDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MemberWalletMapper extends BaseMapperX<MemberWalletDO> {
}

View File

@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.pay.dal.mysql.member;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.pay.dal.dataobject.member.MemberWalletTransactionDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MemberWalletTransactionMapper extends BaseMapperX<MemberWalletTransactionDO> {
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.pay.dal.mysql.notify;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface PayNotifyLogMapper extends BaseMapperX<PayNotifyLogDO> {
default List<PayNotifyLogDO> selectListByTaskId(Long taskId) {
return selectList(PayNotifyLogDO::getTaskId, taskId);
}
}

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.pay.dal.mysql.notify;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface PayNotifyTaskMapper extends BaseMapperX<PayNotifyTaskDO> {
/**
* 获得需要通知的 PayNotifyTaskDO 记录。需要满足如下条件:
*
* 1. status 非成功
* 2. nextNotifyTime 小于当前时间
*
* @return PayTransactionNotifyTaskDO 数组
*/
default List<PayNotifyTaskDO> selectListByNotify() {
return selectList(new LambdaQueryWrapper<PayNotifyTaskDO>()
.in(PayNotifyTaskDO::getStatus, PayNotifyStatusEnum.WAITING.getStatus(),
PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus())
.le(PayNotifyTaskDO::getNextNotifyTime, LocalDateTime.now()));
}
default PageResult<PayNotifyTaskDO> selectPage(PayNotifyTaskPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PayNotifyTaskDO>()
.eqIfPresent(PayNotifyTaskDO::getAppId, reqVO.getAppId())
.eqIfPresent(PayNotifyTaskDO::getType, reqVO.getType())
.eqIfPresent(PayNotifyTaskDO::getDataId, reqVO.getDataId())
.eqIfPresent(PayNotifyTaskDO::getStatus, reqVO.getStatus())
.eqIfPresent(PayNotifyTaskDO::getMerchantOrderId, reqVO.getMerchantOrderId())
.betweenIfPresent(PayNotifyTaskDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayNotifyTaskDO::getId));
}
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.pay.dal.mysql.order;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO> {
default PayOrderExtensionDO selectByNo(String no) {
return selectOne(PayOrderExtensionDO::getNo, no);
}
default int updateByIdAndStatus(Long id, Integer status, PayOrderExtensionDO update) {
return update(update, new LambdaQueryWrapper<PayOrderExtensionDO>()
.eq(PayOrderExtensionDO::getId, id).eq(PayOrderExtensionDO::getStatus, status));
}
default List<PayOrderExtensionDO> selectListByOrderId(Long orderId) {
return selectList(PayOrderExtensionDO::getOrderId, orderId);
}
default List<PayOrderExtensionDO> selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime) {
return selectList(new LambdaQueryWrapper<PayOrderExtensionDO>()
.eq(PayOrderExtensionDO::getStatus, status)
.ge(PayOrderExtensionDO::getCreateTime, minCreateTime));
}
}

View File

@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.pay.dal.mysql.order;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface PayOrderMapper extends BaseMapperX<PayOrderDO> {
default PageResult<PayOrderDO> selectPage(PayOrderPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PayOrderDO>()
.eqIfPresent(PayOrderDO::getAppId, reqVO.getAppId())
.eqIfPresent(PayOrderDO::getChannelCode, reqVO.getChannelCode())
.likeIfPresent(PayOrderDO::getMerchantOrderId, reqVO.getMerchantOrderId())
.likeIfPresent(PayOrderDO::getChannelOrderNo, reqVO.getChannelOrderNo())
.likeIfPresent(PayOrderDO::getNo, reqVO.getNo())
.eqIfPresent(PayOrderDO::getStatus, reqVO.getStatus())
.betweenIfPresent(PayOrderDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayOrderDO::getId));
}
default List<PayOrderDO> selectList(PayOrderExportReqVO reqVO) {
return selectList(new LambdaQueryWrapperX<PayOrderDO>()
.eqIfPresent(PayOrderDO::getAppId, reqVO.getAppId())
.eqIfPresent(PayOrderDO::getChannelCode, reqVO.getChannelCode())
.likeIfPresent(PayOrderDO::getMerchantOrderId, reqVO.getMerchantOrderId())
.likeIfPresent(PayOrderDO::getChannelOrderNo, reqVO.getChannelOrderNo())
.likeIfPresent(PayOrderDO::getNo, reqVO.getNo())
.eqIfPresent(PayOrderDO::getStatus, reqVO.getStatus())
.betweenIfPresent(PayOrderDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayOrderDO::getId));
}
default Long selectCountByAppId(Long appId) {
return selectCount(PayOrderDO::getAppId, appId);
}
default PayOrderDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) {
return selectOne(PayOrderDO::getAppId, appId,
PayOrderDO::getMerchantOrderId, merchantOrderId);
}
default int updateByIdAndStatus(Long id, Integer status, PayOrderDO update) {
return update(update, new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getId, id).eq(PayOrderDO::getStatus, status));
}
default List<PayOrderDO> selectListByStatusAndExpireTimeLt(Integer status, LocalDateTime expireTime) {
return selectList(new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getStatus, status)
.lt(PayOrderDO::getExpireTime, expireTime));
}
}

View File

@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.pay.dal.mysql.refund;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
default Long selectCountByAppId(Long appId) {
return selectCount(PayRefundDO::getAppId, appId);
}
default PayRefundDO selectByAppIdAndMerchantRefundId(Long appId, String merchantRefundId) {
return selectOne(new LambdaQueryWrapperX<PayRefundDO>()
.eq(PayRefundDO::getAppId, appId)
.eq(PayRefundDO::getMerchantRefundId, merchantRefundId));
}
default Long selectCountByAppIdAndOrderId(Long appId, Long orderId, Integer status) {
return selectCount(new LambdaQueryWrapperX<PayRefundDO>()
.eq(PayRefundDO::getAppId, appId)
.eq(PayRefundDO::getOrderId, orderId)
.eq(PayRefundDO::getStatus, status));
}
default PayRefundDO selectByAppIdAndNo(Long appId, String no) {
return selectOne(new LambdaQueryWrapperX<PayRefundDO>()
.eq(PayRefundDO::getAppId, appId)
.eq(PayRefundDO::getNo, no));
}
default int updateByIdAndStatus(Long id, Integer status, PayRefundDO update) {
return update(update, new LambdaQueryWrapper<PayRefundDO>()
.eq(PayRefundDO::getId, id).eq(PayRefundDO::getStatus, status));
}
default PageResult<PayRefundDO> selectPage(PayRefundPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PayRefundDO>()
.eqIfPresent(PayRefundDO::getAppId, reqVO.getAppId())
.eqIfPresent(PayRefundDO::getChannelCode, reqVO.getChannelCode())
.likeIfPresent(PayRefundDO::getMerchantOrderId, reqVO.getMerchantOrderId())
.likeIfPresent(PayRefundDO::getMerchantRefundId, reqVO.getMerchantRefundId())
.likeIfPresent(PayRefundDO::getChannelOrderNo, reqVO.getChannelOrderNo())
.likeIfPresent(PayRefundDO::getChannelRefundNo, reqVO.getChannelRefundNo())
.eqIfPresent(PayRefundDO::getStatus, reqVO.getStatus())
.betweenIfPresent(PayRefundDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayRefundDO::getId));
}
default List<PayRefundDO> selectList(PayRefundExportReqVO reqVO) {
return selectList(new LambdaQueryWrapperX<PayRefundDO>()
.eqIfPresent(PayRefundDO::getAppId, reqVO.getAppId())
.eqIfPresent(PayRefundDO::getChannelCode, reqVO.getChannelCode())
.likeIfPresent(PayRefundDO::getMerchantOrderId, reqVO.getMerchantOrderId())
.likeIfPresent(PayRefundDO::getMerchantRefundId, reqVO.getMerchantRefundId())
.likeIfPresent(PayRefundDO::getChannelOrderNo, reqVO.getChannelOrderNo())
.likeIfPresent(PayRefundDO::getChannelRefundNo, reqVO.getChannelRefundNo())
.eqIfPresent(PayRefundDO::getStatus, reqVO.getStatus())
.betweenIfPresent(PayRefundDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayRefundDO::getId));
}
default List<PayRefundDO> selectListByStatus(Integer status) {
return selectList(PayRefundDO::getStatus, status);
}
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.pay.dal.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import org.redisson.api.RLock;
/**
* 支付 Redis Key 枚举类
*
* @author 芋道源码
*/
public interface RedisKeyConstants {
RedisKeyDefine PAY_NOTIFY_LOCK = new RedisKeyDefine("通知任务的分布式锁",
"pay_notify:lock:%d", // 参数来自 DefaultLockKeyBuilder 类
RedisKeyDefine.KeyTypeEnum.HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构
/**
* 支付序号的缓存
*
* KEY 格式pay_no:{prefix}
* VALUE 数据格式:编号自增
*/
String PAY_NO = "pay_no";
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.pay.dal.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 芋道源码
*/
@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,39 @@
package cn.iocoder.yudao.module.pay.dal.redis.notify;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.module.pay.dal.redis.RedisKeyConstants.PAY_NOTIFY_LOCK;
/**
* 支付通知的锁 Redis DAO
*
* @author 芋道源码
*/
@Repository
public class PayNotifyLockRedisDAO {
@Resource
private RedissonClient redissonClient;
public void lock(Long id, Long timeoutMillis, Runnable runnable) {
String lockKey = formatKey(id);
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
// 执行逻辑
runnable.run();
} finally {
lock.unlock();
}
}
private static String formatKey(Long id) {
return String.format(PAY_NOTIFY_LOCK.getKeyTemplate(), id);
}
}

View File

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

View File

@@ -0,0 +1,4 @@
/**
* 占位
*/
package cn.iocoder.yudao.module.pay.framework.job.core;

View File

@@ -0,0 +1,6 @@
/**
* 属于 pay 模块的 framework 封装
*
* @author 芋道源码
*/
package cn.iocoder.yudao.module.pay.framework;

View File

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

View File

@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.pay.framework.pay.config;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import 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 地址 => 业务的 PayAppDO.orderNotifyUrl 地址
*/
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String orderNotifyUrl;
/**
* 退款回调地址
*
* 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL
*
* 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址
*/
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String refundNotifyUrl;
/**
* 支付订单 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 cn.iocoder.yudao.module.pay.framework.pay.core;

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.pay.framework.security.config;
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
/**
* Pay 模块的 Security 配置
*/
@Configuration("paySecurityConfiguration")
public class SecurityConfiguration {
@Bean("payAuthorizeRequestsCustomizer")
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
return new AuthorizeRequestsCustomizer() {
@Override
public void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
// Swagger 接口文档
registry.antMatchers("/v3/api-docs/**").permitAll() // 元数据
.antMatchers("/swagger-ui.html").permitAll(); // Swagger UI
// Spring Boot Actuator 的安全配置
registry.antMatchers("/actuator").anonymous()
.antMatchers("/actuator/**").anonymous();
// Druid 监控
registry.antMatchers("/druid/**").anonymous();
}
};
}
}

View File

@@ -0,0 +1,4 @@
/**
* 占位
*/
package cn.iocoder.yudao.module.pay.framework.security.core;

View File

@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.pay.framework.web.config;
import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* pay 模块的 web 组件的 Configuration
*
* @author 芋道源码
*/
@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 cn.iocoder.yudao.module.pay.framework.web;

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.pay.job.notify;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 支付通知 Job
* 通过不断扫描待通知的 PayNotifyTaskDO 记录,回调业务线的回调接口
*
* @author 芋道源码
*/
@Component
@TenantJob // 多租户
@Slf4j
public class PayNotifyJob {
@Resource
private PayNotifyService payNotifyService;
@XxlJob("payNotifyJob")
public void execute() throws Exception {
int notifyCount = payNotifyService.executeNotify();
log.info("[execute][执行支付通知 ({}) 个]", notifyCount);
}
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.pay.job.order;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 支付订单的过期 Job
*
* 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。
*
* @author 芋道源码
*/
@Component
@TenantJob
@Slf4j
public class PayOrderExpireJob {
@Resource
private PayOrderService orderService;
@XxlJob("payOrderExpireJob")
public void execute(String param) {
int count = orderService.expireOrder();
log.info("[execute][支付过期 ({}) 个]", count);
}
}

View File

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

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.pay.job.refund;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 退款订单的同步 Job
*
* 由于退款订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
*
* @author 芋道源码
*/
@Component
@TenantJob
@Slf4j
public class PayRefundSyncJob {
@Resource
private PayRefundService refundService;
@XxlJob("payRefundSyncJob")
public void execute() {
int count = refundService.syncRefund();
log.info("[execute][同步退款订单 ({}) 个]", count);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,126 @@
package cn.iocoder.yudao.module.pay.service.app;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO;
import cn.iocoder.yudao.module.pay.convert.app.PayAppConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.mysql.app.PayAppMapper;
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.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 cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
/**
* 支付应用 Service 实现类
*
* @author aquan
*/
@Service
@Validated
public class PayAppServiceImpl implements PayAppService {
@Resource
private PayAppMapper appMapper;
@Resource
@Lazy // 延迟加载,避免循环依赖报错
private PayOrderService orderService;
@Resource
@Lazy // 延迟加载,避免循环依赖报错
private PayRefundService refundService;
@Override
public Long createApp(PayAppCreateReqVO createReqVO) {
// 插入
PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO);
appMapper.insert(app);
// 返回
return app.getId();
}
@Override
public void updateApp(PayAppUpdateReqVO updateReqVO) {
// 校验存在
validateAppExists(updateReqVO.getId());
// 更新
PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO);
appMapper.updateById(updateObj);
}
@Override
public void updateAppStatus(Long id, Integer status) {
// 校验商户存在
validateAppExists(id);
// 更新状态
appMapper.updateById(new PayAppDO().setId(id).setStatus(status));
}
@Override
public void deleteApp(Long id) {
// 校验存在
validateAppExists(id);
// 校验关联数据是否存在
if (orderService.getOrderCountByAppId(id) > 0) {
throw exception(APP_EXIST_ORDER_CANT_DELETE);
}
if (refundService.getRefundCountByAppId(id) > 0) {
throw exception(APP_EXIST_REFUND_CANT_DELETE);
}
// 删除
appMapper.deleteById(id);
}
private void validateAppExists(Long id) {
if (appMapper.selectById(id) == null) {
throw exception(APP_NOT_FOUND);
}
}
@Override
public PayAppDO getApp(Long id) {
return appMapper.selectById(id);
}
@Override
public List<PayAppDO> getAppList(Collection<Long> ids) {
return appMapper.selectBatchIds(ids);
}
@Override
public List<PayAppDO> getAppList() {
return appMapper.selectList();
}
@Override
public PageResult<PayAppDO> getAppPage(PayAppPageReqVO pageReqVO) {
return appMapper.selectPage(pageReqVO);
}
@Override
public PayAppDO validPayApp(Long id) {
PayAppDO app = appMapper.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,95 @@
package cn.iocoder.yudao.module.pay.service.channel;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
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 支付渠道
*/
PayChannelDO getChannel(Long id);
/**
* 根据支付应用 ID 集合,获得支付渠道列表
*
* @param appIds 应用编号集合
* @return 支付渠道列表
*/
List<PayChannelDO> getChannelListByAppIds(Collection<Long> appIds);
/**
* 根据条件获取渠道
*
* @param appId 应用编号
* @param code 渠道编码
* @return 数量
*/
PayChannelDO getChannelByAppIdAndCode(Long appId, String code);
/**
* 支付渠道的合法性
*
* 如果不合法,抛出 {@link ServiceException} 业务异常
*
* @param id 渠道编号
* @return 渠道信息
*/
PayChannelDO validPayChannel(Long id);
/**
* 支付渠道的合法性
*
* 如果不合法,抛出 {@link ServiceException} 业务异常
*
* @param appId 应用编号
* @param code 支付渠道
* @return 渠道信息
*/
PayChannelDO validPayChannel(Long appId, String code);
/**
* 获得指定应用的开启的渠道列表
*
* @param appId 应用编号
* @return 渠道列表
*/
List<PayChannelDO> getEnableChannelList(Long appId);
}

View File

@@ -0,0 +1,216 @@
package cn.iocoder.yudao.module.pay.service.channel;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO;
import cn.iocoder.yudao.module.pay.convert.channel.PayChannelConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.mysql.channel.PayChannelMapper;
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.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
/**
* 支付渠道 Service 实现类
*
* @author aquan
*/
@Service
@Slf4j
@Validated
public class PayChannelServiceImpl implements PayChannelService {
@Getter // 为了方便测试,这里提供 getter 方法
@Setter
private volatile List<PayChannelDO> channelCache;
@Resource
private PayClientFactory payClientFactory;
@Resource
private PayChannelMapper channelMapper;
@Resource
private Validator validator;
/**
* 初始化 {@link #payClientFactory} 缓存
*/
@PostConstruct
public void initLocalCache() {
// 注意:忽略自动多租户,因为要全局初始化缓存
TenantUtils.executeIgnore(() -> {
// 第一步:查询数据
List<PayChannelDO> channels = channelMapper.selectList();
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, PayChannelDO::getUpdateTime);
if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
initLocalCache();
}
});
}
@Override
public Long createChannel(PayChannelCreateReqVO reqVO) {
// 断言是否有重复的
PayChannelDO dbChannel = getChannelByAppIdAndCode(reqVO.getAppId(), reqVO.getCode());
if (dbChannel != null) {
throw exception(CHANNEL_EXIST_SAME_CHANNEL_ERROR);
}
// 新增渠道
PayChannelDO channel = PayChannelConvert.INSTANCE.convert(reqVO)
.setConfig(parseConfig(reqVO.getCode(), reqVO.getConfig()));
channelMapper.insert(channel);
// 刷新缓存
initLocalCache();
return channel.getId();
}
@Override
public void updateChannel(PayChannelUpdateReqVO updateReqVO) {
// 校验存在
PayChannelDO dbChannel = validateChannelExists(updateReqVO.getId());
// 更新
PayChannelDO channel = PayChannelConvert.INSTANCE.convert(updateReqVO)
.setConfig(parseConfig(dbChannel.getCode(), updateReqVO.getConfig()));
channelMapper.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);
// 删除
channelMapper.deleteById(id);
// 刷新缓存
initLocalCache();
}
private PayChannelDO validateChannelExists(Long id) {
PayChannelDO channel = channelMapper.selectById(id);
if (channel == null) {
throw exception(CHANNEL_NOT_FOUND);
}
return channel;
}
@Override
public PayChannelDO getChannel(Long id) {
return channelMapper.selectById(id);
}
@Override
public List<PayChannelDO> getChannelListByAppIds(Collection<Long> appIds) {
return channelMapper.selectListByAppIds(appIds);
}
@Override
public PayChannelDO getChannelByAppIdAndCode(Long appId, String code) {
return channelMapper.selectByAppIdAndCode(appId, code);
}
@Override
public PayChannelDO validPayChannel(Long id) {
PayChannelDO channel = channelMapper.selectById(id);
validPayChannel(channel);
return channel;
}
@Override
public PayChannelDO validPayChannel(Long appId, String code) {
PayChannelDO channel = channelMapper.selectByAppIdAndCode(appId, code);
validPayChannel(channel);
return channel;
}
private void validPayChannel(PayChannelDO channel) {
if (channel == null) {
throw exception(CHANNEL_NOT_FOUND);
}
if (CommonStatusEnum.DISABLE.getStatus().equals(channel.getStatus())) {
throw exception(CHANNEL_IS_DISABLE);
}
}
@Override
public List<PayChannelDO> getEnableChannelList(Long appId) {
return channelMapper.selectListByAppId(appId, CommonStatusEnum.ENABLE.getStatus());
}
}

View File

@@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.pay.service.demo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import javax.validation.Valid;
/**
* 示例订单 Service 接口
*
* @author 芋道源码
*/
public interface PayDemoOrderService {
/**
* 创建示例订单
*
* @param userId 用户编号
* @param createReqVO 创建信息
* @return 编号
*/
Long createDemoOrder(Long userId, @Valid PayDemoOrderCreateReqVO createReqVO);
/**
* 获得示例订单
*
* @param id 编号
* @return 示例订单
*/
PayDemoOrderDO getDemoOrder(Long id);
/**
* 获得示例订单分页
*
* @param pageReqVO 分页查询
* @return 示例订单分页
*/
PageResult<PayDemoOrderDO> getDemoOrderPage(PageParam pageReqVO);
/**
* 更新示例订单为已支付
*
* @param id 编号
* @param payOrderId 支付订单号
*/
void updateDemoOrderPaid(Long id, Long payOrderId);
/**
* 发起示例订单的退款
*
* @param id 编号
* @param userIp 用户编号
*/
void refundDemoOrder(Long id, String userIp);
/**
* 更新示例订单为已退款
*
* @param id 编号
* @param payRefundId 退款订单号
*/
void updateDemoOrderRefunded(Long id, Long payRefundId);
}

View File

@@ -0,0 +1,265 @@
package cn.iocoder.yudao.module.pay.service.demo;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoOrderMapper;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
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.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
/**
* 示例订单 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class PayDemoOrderServiceImpl implements PayDemoOrderService {
/**
* 接入的实力应用编号
*
* 从 [支付管理 -> 应用信息] 里添加
*/
private static final 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 PayDemoOrderMapper payDemoOrderMapper;
public PayDemoOrderServiceImpl() {
spuNames.put(1L, new Object[]{"华为手机", 1});
spuNames.put(2L, new Object[]{"小米电视", 10});
spuNames.put(3L, new Object[]{"苹果手表", 100});
spuNames.put(4L, new Object[]{"华硕笔记本", 1000});
spuNames.put(5L, new Object[]{"蔚来汽车", 200000});
}
@Override
public Long createDemoOrder(Long userId, PayDemoOrderCreateReqVO createReqVO) {
// 1.1 获得商品
Object[] spu = spuNames.get(createReqVO.getSpuId());
Assert.notNull(spu, "商品({}) 不存在", createReqVO.getSpuId());
String spuName = (String) spu[0];
Integer price = (Integer) spu[1];
// 1.2 插入 demo 订单
PayDemoOrderDO demoOrder = new PayDemoOrderDO().setUserId(userId)
.setSpuId(createReqVO.getSpuId()).setSpuName(spuName)
.setPrice(price).setPayStatus(false).setRefundPrice(0);
payDemoOrderMapper.insert(demoOrder);
// 2.1 创建支付单
Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO()
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
.setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号
.setSubject(spuName).setBody("").setPrice(price) // 价格信息
.setExpireTime(addTime(Duration.ofHours(2L)))); // 支付的过期时间
// 2.2 更新支付单到 demo 订单
payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(demoOrder.getId())
.setPayOrderId(payOrderId));
// 返回
return demoOrder.getId();
}
@Override
public PayDemoOrderDO getDemoOrder(Long id) {
return payDemoOrderMapper.selectById(id);
}
@Override
public PageResult<PayDemoOrderDO> getDemoOrderPage(PageParam pageReqVO) {
return payDemoOrderMapper.selectPage(pageReqVO);
}
@Override
public void updateDemoOrderPaid(Long id, Long payOrderId) {
// 校验并获得支付订单(可支付)
PayOrderRespDTO payOrder = validateDemoOrderCanPaid(id, payOrderId);
// 更新 PayDemoOrderDO 状态为已支付
int updateCount = payDemoOrderMapper.updateByIdAndPayed(id, false,
new PayDemoOrderDO().setPayStatus(true).setPayTime(LocalDateTime.now())
.setPayChannelCode(payOrder.getChannelCode()));
if (updateCount == 0) {
throw exception(DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID);
}
}
/**
* 校验交易订单满足被支付的条件
*
* 1. 交易订单未支付
* 2. 支付单已支付
*
* @param id 交易订单编号
* @param payOrderId 支付订单编号
* @return 交易订单
*/
private PayOrderRespDTO validateDemoOrderCanPaid(Long id, Long payOrderId) {
// 1.1 校验订单是否存在
PayDemoOrderDO order = payDemoOrderMapper.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 refundDemoOrder(Long id, String userIp) {
// 1. 校验订单是否可以退款
PayDemoOrderDO order = validateDemoOrderCanRefund(id);
// 2.1 生成退款单号
// 一般来说,用户发起退款的时候,都会单独插入一个售后维权表,然后使用该表的 id 作为 refundId
// 这里我们是个简单的 demo所以没有售后维权表直接使用订单 id + "-refund" 来演示
String refundId = order.getId() + "-refund";
// 2.2 创建退款单
Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
.setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
.setMerchantRefundId(refundId)
.setReason("想退钱").setPrice(order.getPrice()));// 价格信息
// 2.3 更新退款单到 demo 订单
payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id)
.setPayRefundId(payRefundId).setRefundPrice(order.getPrice()));
}
private PayDemoOrderDO validateDemoOrderCanRefund(Long id) {
// 校验订单是否存在
PayDemoOrderDO order = payDemoOrderMapper.selectById(id);
if (order == null) {
throw exception(DEMO_ORDER_NOT_FOUND);
}
// 校验订单是否支付
if (!order.getPayStatus()) {
throw exception(DEMO_ORDER_REFUND_FAIL_NOT_PAID);
}
// 校验订单是否已退款
if (order.getPayRefundId() != null) {
throw exception(DEMO_ORDER_REFUND_FAIL_REFUNDED);
}
return order;
}
@Override
public void updateDemoOrderRefunded(Long id, Long payRefundId) {
// 1. 校验并获得退款订单(可退款)
PayRefundRespDTO payRefund = validateDemoOrderCanRefunded(id, payRefundId);
// 2.2 更新退款单到 demo 订单
payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id)
.setRefundTime(payRefund.getSuccessTime()));
}
private PayRefundRespDTO validateDemoOrderCanRefunded(Long id, Long payRefundId) {
// 1.1 校验示例订单
PayDemoOrderDO order = payDemoOrderMapper.selectById(id);
if (order == null) {
throw exception(DEMO_ORDER_NOT_FOUND);
}
// 1.2 校验退款订单匹配
if (Objects.equals(order.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;
}
}

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