多模块重构 7:pay 模块的初始化

This commit is contained in:
YunaiV
2022-01-31 21:51:23 +08:00
parent e7e3b18704
commit b757e1fccb
162 changed files with 1303 additions and 1714 deletions

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-module-pay</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>1.4.0-snapshot</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-pay-impl</artifactId>
<packaging>jar</packaging>
<name>${artifactId}</name>
<description>
pay 模块,我们放支付业务,提供业务的支付能力。
例如说:商户、应用、支付、退款等等
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-pay-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-redis</artifactId>
</dependency>
<!-- Job 定时任务相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-job</artifactId>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mq</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-excel</artifactId>
</dependency>
</dependencies>
<build>
<!-- 设置构建的 jar 包名 -->
<finalName>${artifactId}</finalName>
<plugins>
<!-- 打包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,163 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant;
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.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.*;
import cn.iocoder.yudao.module.pay.convert.app.PayAppConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.module.pay.service.merchant.PayAppService;
import cn.iocoder.yudao.module.pay.service.merchant.PayChannelService;
import cn.iocoder.yudao.module.pay.service.merchant.PayMerchantService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
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.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Slf4j
@Api(tags = "管理后台 - 支付应用信息")
@RestController
@RequestMapping("/pay/app")
@Validated
public class PayAppController {
@Resource
private PayAppService appService;
@Resource
private PayChannelService channelService;
@Resource
private PayMerchantService merchantService;
@PostMapping("/create")
@ApiOperation("创建支付应用信息")
@PreAuthorize("@ss.hasPermission('pay:app:create')")
public CommonResult<Long> createApp(@Valid @RequestBody PayAppCreateReqVO createReqVO) {
return success(appService.createApp(createReqVO));
}
@PutMapping("/update")
@ApiOperation("更新支付应用信息")
@PreAuthorize("@ss.hasPermission('pay:app:update')")
public CommonResult<Boolean> updateApp(@Valid @RequestBody PayAppUpdateReqVO updateReqVO) {
appService.updateApp(updateReqVO);
return success(true);
}
@PutMapping("/update-status")
@ApiOperation("更新支付应用状态")
@PreAuthorize("@ss.hasPermission('pay:app:update')")
public CommonResult<Boolean> updateAppStatus(@Valid @RequestBody PayAppUpdateStatusReqVO updateReqVO) {
appService.updateAppStatus(updateReqVO.getId(), updateReqVO.getStatus());
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除支付应用信息")
@ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('pay:app:delete')")
public CommonResult<Boolean> deleteApp(@RequestParam("id") Long id) {
appService.deleteApp(id);
return success(true);
}
@GetMapping("/get")
@ApiOperation("获得支付应用信息")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@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("/list")
@ApiOperation("获得支付应用信息列表")
@ApiImplicitParam(name = "ids", value = "编号列表", required = true, example = "1024,2048", dataTypeClass = List.class)
@PreAuthorize("@ss.hasPermission('pay:app:query')")
public CommonResult<List<PayAppRespVO>> getAppList(@RequestParam("ids") Collection<Long> ids) {
List<PayAppDO> list = appService.getAppList(ids);
return success(PayAppConvert.INSTANCE.convertList(list));
}
@GetMapping("/page")
@ApiOperation("获得支付应用信息分页")
@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(new PageResult<>(pageResult.getTotal()));
}
// 得到所有的应用编号,查出所有的渠道
Collection<Long> payAppIds = CollectionUtils.convertList(pageResult.getList(), PayAppDO::getId);
List<PayChannelDO> channels = channelService.getChannelListByAppIds(payAppIds);
// TODO @aquan可以基于 appId 简历一个 multiMap。这样下面直接 get 到之后CollUtil buildSet 即可
Iterator<PayChannelDO> iterator = channels.iterator();
// 得到所有的商户信息
Collection<Long> merchantIds = CollectionUtils.convertList(pageResult.getList(), PayAppDO::getMerchantId);
Map<Long, PayMerchantDO> deptMap = merchantService.getMerchantMap(merchantIds);
// 利用反射将渠道数据复制到返回的数据结构中去
List<PayAppPageItemRespVO> appList = new ArrayList<>(pageResult.getList().size());
pageResult.getList().forEach(app -> {
// 写入应用信息的数据
PayAppPageItemRespVO respVO = PayAppConvert.INSTANCE.pageConvert(app);
// 写入商户的数据
respVO.setPayMerchant(PayAppConvert.INSTANCE.convert(deptMap.get(app.getMerchantId())));
// 写入支付渠道信息的数据
Set<String> channelCodes = new HashSet<>(PayChannelEnum.values().length);
while (iterator.hasNext()) {
PayChannelDO channelDO = iterator.next();
if (channelDO.getAppId().equals(app.getId())) {
channelCodes.add(channelDO.getCode());
iterator.remove();
}
}
respVO.setChannelCodes(channelCodes);
appList.add(respVO);
});
return success(new PageResult<>(appList, pageResult.getTotal()));
}
@GetMapping("/export-excel")
@ApiOperation("导出支付应用信息 Excel")
@PreAuthorize("@ss.hasPermission('pay:app:export')")
@OperateLog(type = EXPORT)
public void exportAppExcel(@Valid PayAppExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
List<PayAppDO> list = appService.getAppList(exportReqVO);
// 导出 Excel
List<PayAppExcelVO> datas = PayAppConvert.INSTANCE.convertList02(list);
ExcelUtils.write(response, "支付应用信息.xls", "数据", PayAppExcelVO.class, datas);
}
@GetMapping("/list-merchant-id")
@ApiOperation("根据商户 ID 查询支付应用信息")
@ApiImplicitParam(name = "merchantId", value = "商户ID", required = true, example = "1", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('pay:merchant:query')")
public CommonResult<List<PayAppRespVO>> getMerchantListByName(@RequestParam String merchantId) {
List<PayAppDO> appListDO = appService.getListByMerchantId(merchantId);
return success(PayAppConvert.INSTANCE.convertList(appListDO));
}
}

View File

@@ -0,0 +1,124 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.*;
import cn.iocoder.yudao.module.pay.convert.channel.PayChannelConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.module.pay.service.merchant.PayChannelService;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
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.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api(tags = "管理后台 - 支付渠道")
@RestController
@RequestMapping("/pay/channel")
@Validated
public class PayChannelController {
@Resource
private PayChannelService channelService;
@PostMapping("/create")
@ApiOperation("创建支付渠道 ")
@PreAuthorize("@ss.hasPermission('pay:channel:create')")
public CommonResult<Long> createChannel(@Valid @RequestBody PayChannelCreateReqVO createReqVO) {
return success(channelService.createChannel(createReqVO));
}
@PutMapping("/update")
@ApiOperation("更新支付渠道 ")
@PreAuthorize("@ss.hasPermission('pay:channel:update')")
public CommonResult<Boolean> updateChannel(@Valid @RequestBody PayChannelUpdateReqVO updateReqVO) {
channelService.updateChannel(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除支付渠道 ")
@ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('pay:channel:delete')")
public CommonResult<Boolean> deleteChannel(@RequestParam("id") Long id) {
channelService.deleteChannel(id);
return success(true);
}
@GetMapping("/get")
@ApiOperation("获得支付渠道 ")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('pay:channel:query')")
public CommonResult<PayChannelRespVO> getChannel(@RequestParam("id") Long id) {
PayChannelDO channel = channelService.getChannel(id);
return success(PayChannelConvert.INSTANCE.convert(channel));
}
@GetMapping("/list")
@ApiOperation("获得支付渠道列表")
@ApiImplicitParam(name = "ids", value = "编号列表",
required = true, example = "1024,2048", dataTypeClass = List.class)
@PreAuthorize("@ss.hasPermission('pay:channel:query')")
public CommonResult<List<PayChannelRespVO>> getChannelList(@RequestParam("ids") Collection<Long> ids) {
List<PayChannelDO> list = channelService.getChannelList(ids);
return success(PayChannelConvert.INSTANCE.convertList(list));
}
@GetMapping("/page")
@ApiOperation("获得支付渠道分页")
@PreAuthorize("@ss.hasPermission('pay:channel:query')")
public CommonResult<PageResult<PayChannelRespVO>> getChannelPage(@Valid PayChannelPageReqVO pageVO) {
PageResult<PayChannelDO> pageResult = channelService.getChannelPage(pageVO);
return success(PayChannelConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/export-excel")
@ApiOperation("导出支付渠道Excel")
@PreAuthorize("@ss.hasPermission('pay:channel:export')")
@OperateLog(type = EXPORT)
public void exportChannelExcel(@Valid PayChannelExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
List<PayChannelDO> list = channelService.getChannelList(exportReqVO);
// 导出 Excel
List<PayChannelExcelVO> datas = PayChannelConvert.INSTANCE.convertList02(list);
ExcelUtils.write(response, "支付渠道.xls", "数据", PayChannelExcelVO.class, datas);
}
@GetMapping("/get-channel")
@ApiOperation("根据条件查询微信支付渠道")
@ApiImplicitParams({
@ApiImplicitParam(name = "merchantId", value = "商户编号",
required = true, example = "1", dataTypeClass = Long.class),
@ApiImplicitParam(name = "appId", value = "应用编号",
required = true, example = "1", dataTypeClass = Long.class),
@ApiImplicitParam(name = "code", value = "支付渠道编码",
required = true, example = "wx_pub", dataTypeClass = String.class)
})
@PreAuthorize("@ss.hasPermission('pay:channel:query')")
public CommonResult<PayChannelRespVO> getChannel(
@RequestParam Long merchantId, @RequestParam Long appId, @RequestParam String code) {
// 獲取渠道
PayChannelDO channel = channelService.getChannelByConditions(merchantId, appId, code);
if (channel == null) {
return success(new PayChannelRespVO());
}
// 拼凑数据
PayChannelRespVO respVo = PayChannelConvert.INSTANCE.convert(channel);
return success(respVo);
}
}

View File

@@ -0,0 +1,116 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.*;
import cn.iocoder.yudao.module.pay.convert.merchant.PayMerchantConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.module.pay.service.merchant.PayMerchantService;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
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.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api(tags = "支付商户信息")
@RestController
@RequestMapping("/pay/merchant")
@Validated
public class PayMerchantController {
@Resource
private PayMerchantService merchantService;
@PostMapping("/create")
@ApiOperation("创建支付商户信息")
@PreAuthorize("@ss.hasPermission('pay:merchant:create')")
public CommonResult<Long> createMerchant(@Valid @RequestBody PayMerchantCreateReqVO createReqVO) {
return success(merchantService.createMerchant(createReqVO));
}
@PutMapping("/update")
@ApiOperation("更新支付商户信息")
@PreAuthorize("@ss.hasPermission('pay:merchant:update')")
public CommonResult<Boolean> updateMerchant(@Valid @RequestBody PayMerchantUpdateReqVO updateReqVO) {
merchantService.updateMerchant(updateReqVO);
return success(true);
}
@PutMapping("/update-status")
@ApiOperation("修改支付商户状态")
@PreAuthorize("@ss.hasPermission('pay:merchant:update')")
public CommonResult<Boolean> updateMerchantStatus(@Valid @RequestBody PayMerchantUpdateStatusReqVO reqVO) {
merchantService.updateMerchantStatus(reqVO.getId(), reqVO.getStatus());
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除支付商户信息")
@ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('pay:merchant:delete')")
public CommonResult<Boolean> deleteMerchant(@RequestParam("id") Long id) {
merchantService.deleteMerchant(id);
return success(true);
}
@GetMapping("/get")
@ApiOperation("获得支付商户信息")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('pay:merchant:query')")
public CommonResult<PayMerchantRespVO> getMerchant(@RequestParam("id") Long id) {
PayMerchantDO merchant = merchantService.getMerchant(id);
return success(PayMerchantConvert.INSTANCE.convert(merchant));
}
@GetMapping("/list-by-name")
@ApiOperation("根据商户名称获得支付商户信息列表")
@ApiImplicitParam(name = "name", value = "商户名称", example = "芋道", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('pay:merchant:query')")
public CommonResult<List<PayMerchantRespVO>> getMerchantListByName(@RequestParam(required = false) String name) {
List<PayMerchantDO> merchantListDO = merchantService.getMerchantListByName(name);
return success(PayMerchantConvert.INSTANCE.convertList(merchantListDO));
}
@GetMapping("/list")
@ApiOperation("获得支付商户信息列表")
@ApiImplicitParam(name = "ids", value = "编号列表", required = true, example = "1024,2048", dataTypeClass = List.class)
@PreAuthorize("@ss.hasPermission('pay:merchant:query')")
public CommonResult<List<PayMerchantRespVO>> getMerchantList(@RequestParam("ids") Collection<Long> ids) {
List<PayMerchantDO> list = merchantService.getMerchantList(ids);
return success(PayMerchantConvert.INSTANCE.convertList(list));
}
@GetMapping("/page")
@ApiOperation("获得支付商户信息分页")
@PreAuthorize("@ss.hasPermission('pay:merchant:query')")
public CommonResult<PageResult<PayMerchantRespVO>> getMerchantPage(@Valid PayMerchantPageReqVO pageVO) {
PageResult<PayMerchantDO> pageResult = merchantService.getMerchantPage(pageVO);
return success(PayMerchantConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/export-excel")
@ApiOperation("导出支付商户信息 Excel")
@PreAuthorize("@ss.hasPermission('pay:merchant:export')")
@OperateLog(type = EXPORT)
public void exportMerchantExcel(@Valid PayMerchantExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
List<PayMerchantDO> list = merchantService.getMerchantList(exportReqVO);
// 导出 Excel
List<PayMerchantExcelVO> datas = PayMerchantConvert.INSTANCE.convertList02(list);
ExcelUtils.write(response, "支付商户信息.xls", "数据", PayMerchantExcelVO.class, datas);
}
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app;
import lombok.*;
import io.swagger.annotations.*;
import javax.validation.constraints.*;
/**
* 支付应用信息 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class PayAppBaseVO {
@ApiModelProperty(value = "应用名", required = true)
@NotNull(message = "应用名不能为空")
private String name;
@ApiModelProperty(value = "开启状态", required = true)
@NotNull(message = "开启状态不能为空")
private Integer status;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "支付结果的回调地址", required = true)
@NotNull(message = "支付结果的回调地址不能为空")
private String payNotifyUrl;
@ApiModelProperty(value = "退款结果的回调地址", required = true)
@NotNull(message = "退款结果的回调地址不能为空")
private String refundNotifyUrl;
@ApiModelProperty(value = "商户编号", required = true)
@NotNull(message = "商户编号不能为空")
private Long merchantId;
}

View File

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

View File

@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
/**
* 支付应用信息 Excel VO
*
* @author 芋艿
*/
@Data
public class PayAppExcelVO {
@ExcelProperty("应用编号")
private Long id;
@ExcelProperty("应用名")
private String name;
@ExcelProperty("开启状态")
private Integer status;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("支付结果的回调地址")
private String payNotifyUrl;
@ExcelProperty("退款结果的回调地址")
private String refundNotifyUrl;
@ExcelProperty("商户编号")
private Long merchantId;
@ExcelProperty("创建时间")
private Date createTime;
}

View File

@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(value = "管理后台 - 支付应用信息 Excel 导出 Request VO", description = "参数和 PayAppPageReqVO 是一致的")
@Data
public class PayAppExportReqVO {
@ApiModelProperty(value = "应用名")
private String name;
@ApiModelProperty(value = "开启状态")
private Integer status;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "支付结果的回调地址")
private String payNotifyUrl;
@ApiModelProperty(value = "退款结果的回调地址")
private String refundNotifyUrl;
@ApiModelProperty(value = "商户名称")
private String merchantName;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Date;
import java.util.Set;
@ApiModel(value = "管理后台 - 支付应用信息分页查询 Response VO", description = "相比于支付信息,还会多出应用渠道的开关信息")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppPageItemRespVO extends PayAppBaseVO {
@ApiModelProperty(value = "应用编号", required = true)
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
/**
* 所属商户
*/
private PayMerchant payMerchant;
@ApiModel("商户")
@Data
public static class PayMerchant {
@ApiModelProperty(value = "商户编号", required = true, example = "1")
private Long id;
@ApiModelProperty(value = "商户名称", required = true, example = "研发部")
private String name;
}
@ApiModelProperty(value = "渠道编码集合", required = true, example = "alipay_pc,alipay_wap...")
private Set<String> channelCodes;
}

View File

@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel("管理后台 - 支付应用信息分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppPageReqVO extends PageParam {
@ApiModelProperty(value = "应用名")
private String name;
@ApiModelProperty(value = "开启状态")
private Integer status;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "支付结果的回调地址")
private String payNotifyUrl;
@ApiModelProperty(value = "退款结果的回调地址")
private String refundNotifyUrl;
@ApiModelProperty(value = "商户名称")
private String merchantName;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
@ApiModel("管理后台 - 支付应用信息 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayAppRespVO extends PayAppBaseVO {
@ApiModelProperty(value = "应用编号", required = true)
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotBlank;
@ApiModel("管理后台 - 支付渠道 创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayChannelCreateReqVO extends PayChannelBaseVO {
@ApiModelProperty(value = "渠道配置的 json 字符串")
@NotBlank(message = "渠道配置不能为空")
private String config;
}

View File

@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
import com.alibaba.excel.annotation.ExcelProperty;
/**
* 支付渠道 Excel VO
*
* @author 芋艿
*/
@Data
public class PayChannelExcelVO {
@ExcelProperty("商户编号")
private Long id;
@ExcelProperty("渠道编码")
private String code;
@ExcelProperty("开启状态")
private Integer status;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("渠道费率,单位:百分比")
private Double feeRate;
@ExcelProperty("商户编号")
private Long merchantId;
@ExcelProperty("应用编号")
private Long appId;
/**
* todo @芋艿 mapStruct 存在转换问题
* java: Can't map property "PayClientConfig payChannelDO.config" to "String payChannelExcelVO.config".
* Consider to declare/implement a mapping method: "String map(PayClientConfig value)".
*/
/// @ExcelProperty("支付渠道配置")
/// private String config;
@ExcelProperty("创建时间")
private Date createTime;
}

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(value = "管理后台 - 支付渠道 Excel 导出 Request VO", description = "参数和 PayChannelPageReqVO 是一致的")
@Data
public class PayChannelExportReqVO {
@ApiModelProperty(value = "渠道编码")
private String code;
@ApiModelProperty(value = "开启状态")
private Integer status;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "渠道费率,单位:百分比")
private Double feeRate;
@ApiModelProperty(value = "商户编号")
private Long merchantId;
@ApiModelProperty(value = "应用编号")
private Long appId;
@ApiModelProperty(value = "支付渠道配置")
private String config;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel("管理后台 - 支付渠道 分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayChannelPageReqVO extends PageParam {
@ApiModelProperty(value = "渠道编码")
private String code;
@ApiModelProperty(value = "开启状态")
private Integer status;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "渠道费率,单位:百分比")
private Double feeRate;
@ApiModelProperty(value = "商户编号")
private Long merchantId;
@ApiModelProperty(value = "应用编号")
private Long appId;
@ApiModelProperty(value = "支付渠道配置")
private String config;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
@ApiModel("管理后台 - 支付渠道 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayChannelRespVO extends PayChannelBaseVO {
@ApiModelProperty(value = "商户编号", required = true)
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
@ApiModelProperty(value = "配置", required = true)
private String config;
}

View File

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

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 支付商户信息 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class PayMerchantBaseVO {
@ApiModelProperty(value = "商户全称", required = true)
@NotNull(message = "商户全称不能为空")
private String name;
@ApiModelProperty(value = "商户简称", required = true)
@NotNull(message = "商户简称不能为空")
private String shortName;
@ApiModelProperty(value = "开启状态", required = true)
@NotNull(message = "开启状态不能为空")
private Integer status;
@ApiModelProperty(value = "备注")
private String remark;
}

View File

@@ -0,0 +1,12 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant;
import lombok.*;
import io.swagger.annotations.*;
@ApiModel("管理后台 - 支付商户信息创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayMerchantCreateReqVO extends PayMerchantBaseVO {
}

View File

@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
/**
* 支付商户信息 Excel VO
*
* @author 芋艿
*/
@Data
public class PayMerchantExcelVO {
@ExcelProperty("商户编号")
private Long id;
@ExcelProperty("商户号")
private String no;
@ExcelProperty("商户全称")
private String name;
@ExcelProperty("商户简称")
private String shortName;
@ExcelProperty(value = "开启状态",converter = DictConvert.class)
@DictFormat("sys_common_status")
private Integer status;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建时间")
private Date createTime;
}

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(value = "管理后台 - 支付商户信息 Excel 导出 Request VO", description = "参数和 PayMerchantPageReqVO 是一致的")
@Data
public class PayMerchantExportReqVO {
@ApiModelProperty(value = "商户号")
private String no;
@ApiModelProperty(value = "商户全称")
private String name;
@ApiModelProperty(value = "商户简称")
private String shortName;
@ApiModelProperty(value = "开启状态")
private Integer status;
@ApiModelProperty(value = "备注")
private String remark;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant;
import lombok.*;
import java.util.*;
import io.swagger.annotations.*;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel("管理后台 - 支付商户信息分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayMerchantPageReqVO extends PageParam {
@ApiModelProperty(value = "商户号")
private String no;
@ApiModelProperty(value = "商户全称")
private String name;
@ApiModelProperty(value = "商户简称")
private String shortName;
@ApiModelProperty(value = "开启状态")
private Integer status;
@ApiModelProperty(value = "备注")
private String remark;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Date;
@ApiModel("管理后台 - 支付商户信息 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayMerchantRespVO extends PayMerchantBaseVO {
@ApiModelProperty(value = "商户编号", required = true)
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
@ApiModelProperty(value = "商户号", required = true, example = "M233666999")
private String no;
}

View File

@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant;
import lombok.*;
import io.swagger.annotations.*;
import javax.validation.constraints.*;
@ApiModel("管理后台 - 支付商户信息更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayMerchantUpdateReqVO extends PayMerchantBaseVO {
@ApiModelProperty(value = "商户编号", required = true)
@NotNull(message = "商户编号不能为空")
private Long id;
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
@ApiModel("管理后台 - 商户更新状态 Request VO")
@Data
public class PayMerchantUpdateStatusReqVO {
@ApiModelProperty(value = "商户编号", required = true, example = "1024")
@NotNull(message = "商户编号不能为空")
private Long id;
@ApiModelProperty(value = "状态", required = true, example = "1", notes = "见 SysCommonStatusEnum 枚举")
@NotNull(message = "状态不能为空")
private Integer status;
}

View File

@@ -0,0 +1,158 @@
package cn.iocoder.yudao.module.pay.controller.admin.order;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
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.merchant.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
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.merchant.PayAppService;
import cn.iocoder.yudao.module.pay.service.merchant.PayMerchantService;
import cn.iocoder.yudao.module.pay.service.order.PayOrderExtensionService;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
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.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
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.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api(tags = "管理后台 - 支付订单")
@RestController
@RequestMapping("/pay/order")
@Validated
public class PayOrderController {
@Resource
private PayOrderService orderService;
@Resource
private PayOrderExtensionService orderExtensionService;
@Resource
private PayMerchantService merchantService;
@Resource
private PayAppService appService;
@GetMapping("/get")
@ApiOperation("获得支付订单")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('pay:order:query')")
public CommonResult<PayOrderDetailsRespVO> getOrder(@RequestParam("id") Long id) {
PayOrderDO order = orderService.getOrder(id);
if (ObjectUtil.isNull(order)) {
return success(new PayOrderDetailsRespVO());
}
PayMerchantDO merchantDO = merchantService.getMerchant(order.getMerchantId());
PayAppDO appDO = appService.getApp(order.getAppId());
PayChannelEnum channelEnum = PayChannelEnum.getByCode(order.getChannelCode());
// TODO @aquan文案都是前端 format
PayOrderDetailsRespVO respVO = PayOrderConvert.INSTANCE.orderDetailConvert(order);
respVO.setMerchantName(ObjectUtil.isNotNull(merchantDO) ? merchantDO.getName() : "未知商户");
respVO.setAppName(ObjectUtil.isNotNull(appDO) ? appDO.getName() : "未知应用");
respVO.setChannelCodeName(ObjectUtil.isNotNull(channelEnum) ? channelEnum.getName() : "未知渠道");
PayOrderExtensionDO extensionDO = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
if (ObjectUtil.isNotNull(extensionDO)) {
respVO.setPayOrderExtension(PayOrderConvert.INSTANCE.orderDetailExtensionConvert(extensionDO));
}
return success(respVO);
}
@GetMapping("/page")
@ApiOperation("获得支付订单分页")
@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()));
}
// 处理商户ID数据
Map<Long, PayMerchantDO> merchantMap = merchantService.getMerchantMap(
CollectionUtils.convertList(pageResult.getList(), PayOrderDO::getMerchantId));
// 处理应用ID数据
Map<Long, PayAppDO> appMap = appService.getAppMap(
CollectionUtils.convertList(pageResult.getList(), PayOrderDO::getAppId));
List<PayOrderPageItemRespVO> pageList = new ArrayList<>(pageResult.getList().size());
pageResult.getList().forEach(c -> {
PayMerchantDO merchantDO = merchantMap.get(c.getMerchantId());
PayAppDO appDO = appMap.get(c.getAppId());
PayChannelEnum channelEnum = PayChannelEnum.getByCode(c.getChannelCode());
PayOrderPageItemRespVO orderItem = PayOrderConvert.INSTANCE.pageConvertItemPage(c);
orderItem.setMerchantName(ObjectUtil.isNotNull(merchantDO) ? merchantDO.getName() : "未知商户");
orderItem.setAppName(ObjectUtil.isNotNull(appDO) ? appDO.getName() : "未知应用");
orderItem.setChannelCodeName(ObjectUtil.isNotNull(channelEnum) ? channelEnum.getName() : "未知渠道");
pageList.add(orderItem);
});
return success(new PageResult<>(pageList, pageResult.getTotal()));
}
@GetMapping("/export-excel")
@ApiOperation("导出支付订单Excel")
@PreAuthorize("@ss.hasPermission('pay:order:export')")
@OperateLog(type = EXPORT)
public void exportOrderExcel(@Valid PayOrderExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
List<PayOrderDO> list = orderService.getOrderList(exportReqVO);
if (CollectionUtil.isEmpty(list)) {
ExcelUtils.write(response, "支付订单.xls", "数据",
PayOrderExcelVO.class, new ArrayList<>());
}
// 处理商户ID数据
Map<Long, PayMerchantDO> merchantMap = merchantService.getMerchantMap(
CollectionUtils.convertList(list, PayOrderDO::getMerchantId));
// 处理应用ID数据
Map<Long, PayAppDO> appMap = appService.getAppMap(
CollectionUtils.convertList(list, PayOrderDO::getAppId));
// 处理扩展订单数据
Map<Long, PayOrderExtensionDO> orderExtensionMap = orderExtensionService
.getOrderExtensionMap(CollectionUtils.convertList(list, PayOrderDO::getSuccessExtensionId));
List<PayOrderExcelVO> excelDatum = new ArrayList<>(list.size());
list.forEach(c -> {
PayMerchantDO merchantDO = merchantMap.get(c.getMerchantId());
PayAppDO appDO = appMap.get(c.getAppId());
PayChannelEnum channelEnum = PayChannelEnum.getByCode(c.getChannelCode());
PayOrderExtensionDO orderExtensionDO = orderExtensionMap.get(c.getSuccessExtensionId());
PayOrderExcelVO excelItem = PayOrderConvert.INSTANCE.excelConvert(c);
excelItem.setMerchantName(ObjectUtil.isNotNull(merchantDO) ? merchantDO.getName() : "未知商户");
excelItem.setAppName(ObjectUtil.isNotNull(appDO) ? appDO.getName() : "未知应用");
excelItem.setChannelCodeName(ObjectUtil.isNotNull(channelEnum) ? channelEnum.getName() : "未知渠道");
excelItem.setNo(ObjectUtil.isNotNull(orderExtensionDO) ? orderExtensionDO.getNo() : "");
excelDatum.add(excelItem);
});
// 导出 Excel
ExcelUtils.write(response, "支付订单.xls", "数据", PayOrderExcelVO.class, excelDatum);
}
}

View File

@@ -0,0 +1,107 @@
package cn.iocoder.yudao.module.pay.controller.admin.order.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import java.util.Date;
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 {
@ApiModelProperty(value = "商户编号", required = true)
@NotNull(message = "商户编号不能为空")
private Long merchantId;
@ApiModelProperty(value = "应用编号", required = true)
@NotNull(message = "应用编号不能为空")
private Long appId;
@ApiModelProperty(value = "渠道编号")
private Long channelId;
@ApiModelProperty(value = "渠道编码")
private String channelCode;
@ApiModelProperty(value = "商户订单编号", required = true)
@NotNull(message = "商户订单编号不能为空")
private String merchantOrderId;
@ApiModelProperty(value = "商品标题", required = true)
@NotNull(message = "商品标题不能为空")
private String subject;
@ApiModelProperty(value = "商品描述", required = true)
@NotNull(message = "商品描述不能为空")
private String body;
@ApiModelProperty(value = "异步通知地址", required = true)
@NotNull(message = "异步通知地址不能为空")
private String notifyUrl;
@ApiModelProperty(value = "通知商户支付结果的回调状态", required = true)
@NotNull(message = "通知商户支付结果的回调状态不能为空")
private Integer notifyStatus;
@ApiModelProperty(value = "支付金额,单位:分", required = true)
@NotNull(message = "支付金额,单位:分不能为空")
private Long amount;
@ApiModelProperty(value = "渠道手续费,单位:百分比")
private Double channelFeeRate;
@ApiModelProperty(value = "渠道手续金额,单位:分")
private Long channelFeeAmount;
@ApiModelProperty(value = "支付状态", required = true)
@NotNull(message = "支付状态不能为空")
private Integer status;
@ApiModelProperty(value = "用户 IP", required = true)
@NotNull(message = "用户 IP不能为空")
private String userIp;
@ApiModelProperty(value = "订单失效时间", required = true)
@NotNull(message = "订单失效时间不能为空")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private Date expireTime;
@ApiModelProperty(value = "订单支付成功时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private Date successTime;
@ApiModelProperty(value = "订单支付通知时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private Date notifyTime;
@ApiModelProperty(value = "支付成功的订单拓展单编号")
private Long successExtensionId;
@ApiModelProperty(value = "退款状态", required = true)
@NotNull(message = "退款状态不能为空")
private Integer refundStatus;
@ApiModelProperty(value = "退款次数", required = true)
@NotNull(message = "退款次数不能为空")
private Integer refundTimes;
@ApiModelProperty(value = "退款总金额,单位:分", required = true)
@NotNull(message = "退款总金额,单位:分不能为空")
private Long refundAmount;
@ApiModelProperty(value = "渠道用户编号")
private String channelUserId;
@ApiModelProperty(value = "渠道订单号")
private String channelOrderNo;
}

View File

@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.pay.controller.admin.order.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Date;
@ApiModel("管理后台 - 支付订单详细信息 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayOrderDetailsRespVO extends PayOrderBaseVO {
@ApiModelProperty(value = "支付订单编号")
private Long id;
@ApiModelProperty(value = "商户名称")
private String merchantName;
@ApiModelProperty(value = "应用名称")
private String appName;
@ApiModelProperty(value = "渠道编号名称")
private String channelCodeName;
@ApiModelProperty(value = "创建时间")
private Date createTime;
/**
* 支付订单扩展
*/
private PayOrderExtension payOrderExtension;
@Data
@ApiModel("支付订单扩展")
public static class PayOrderExtension {
@ApiModelProperty(value = "支付订单号")
private String no;
@ApiModelProperty(value = "支付异步通知的内容")
private String channelNotifyData;
}
}

View File

@@ -0,0 +1,91 @@
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.module.pay.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
/**
* 支付订单Excel VO
*
* @author aquan
*/
@Data
public class PayOrderExcelVO {
@ExcelProperty("支付订单编号")
private Long id;
@ExcelProperty(value = "商户名称")
private String merchantName;
@ExcelProperty(value = "应用名称")
private String appName;
@ExcelProperty("商品标题")
private String subject;
@ExcelProperty("商户订单编号")
private String merchantOrderId;
@ExcelProperty("渠道订单号")
private String channelOrderNo;
@ExcelProperty(value = "支付订单号")
private String no;
@ExcelProperty("支付金额,单位:元")
private String amount;
@ExcelProperty("渠道手续金额,单位:元")
private String channelFeeAmount;
@ExcelProperty("渠道手续费,单位:百分比")
private String channelFeeRate;
@DictFormat(DictTypeConstants.PAY_ORDER_STATUS)
@ExcelProperty(value = "支付状态", converter = DictConvert.class)
private Integer status;
@DictFormat(DictTypeConstants.PAY_ORDER_NOTIFY_STATUS)
@ExcelProperty(value = "通知商户支付结果的回调状态", converter = DictConvert.class)
private Integer notifyStatus;
@ExcelProperty("异步通知地址")
private String notifyUrl;
@ExcelProperty("创建时间")
private Date createTime;
@ExcelProperty("订单支付成功时间")
private Date successTime;
@ExcelProperty("订单失效时间")
private Date expireTime;
@ExcelProperty("订单支付通知时间")
private Date notifyTime;
@ExcelProperty(value = "渠道编号名称")
private String channelCodeName;
@ExcelProperty("用户 IP")
private String userIp;
@DictFormat(DictTypeConstants.PAY_ORDER_REFUND_STATUS)
@ExcelProperty(value = "退款状态", converter = DictConvert.class)
private Integer refundStatus;
@ExcelProperty("退款次数")
private Integer refundTimes;
@ExcelProperty("退款总金额,单位:元")
private String refundAmount;
@ExcelProperty("商品描述")
private String body;
}

View File

@@ -0,0 +1,108 @@
package cn.iocoder.yudao.module.pay.controller.admin.order.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(value = "管理后台 - 支付订单 Excel 导出 Request VO", description = "参数和 PayOrderPageReqVO 是一致的")
@Data
public class PayOrderExportReqVO {
@ApiModelProperty(value = "商户编号")
private Long merchantId;
@ApiModelProperty(value = "应用编号")
private Long appId;
@ApiModelProperty(value = "渠道编号")
private Long channelId;
@ApiModelProperty(value = "渠道编码")
private String channelCode;
@ApiModelProperty(value = "商户订单编号")
private String merchantOrderId;
@ApiModelProperty(value = "商品标题")
private String subject;
@ApiModelProperty(value = "商品描述")
private String body;
@ApiModelProperty(value = "异步通知地址")
private String notifyUrl;
@ApiModelProperty(value = "通知商户支付结果的回调状态")
private Integer notifyStatus;
@ApiModelProperty(value = "支付金额,单位:分")
private Long amount;
@ApiModelProperty(value = "渠道手续费,单位:百分比")
private Double channelFeeRate;
@ApiModelProperty(value = "渠道手续金额,单位:分")
private Long channelFeeAmount;
@ApiModelProperty(value = "支付状态")
private Integer status;
@ApiModelProperty(value = "用户 IP")
private String userIp;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始订单失效时间")
private Date beginExpireTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束订单失效时间")
private Date endExpireTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始订单支付成功时间")
private Date beginSuccessTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束订单支付成功时间")
private Date endSuccessTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始订单支付通知时间")
private Date beginNotifyTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束订单支付通知时间")
private Date endNotifyTime;
@ApiModelProperty(value = "支付成功的订单拓展单编号")
private Long successExtensionId;
@ApiModelProperty(value = "退款状态")
private Integer refundStatus;
@ApiModelProperty(value = "退款次数")
private Integer refundTimes;
@ApiModelProperty(value = "退款总金额,单位:分")
private Long refundAmount;
@ApiModelProperty(value = "渠道用户编号")
private String channelUserId;
@ApiModelProperty(value = "渠道订单号")
private String channelOrderNo;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.pay.controller.admin.order.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Date;
@ApiModel("管理后台 - 支付订单分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayOrderPageItemRespVO extends PayOrderBaseVO {
@ApiModelProperty(value = "支付订单编号", required = true)
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
@ApiModelProperty(value = "商户名称")
private String merchantName;
@ApiModelProperty(value = "应用名称")
private String appName;
@ApiModelProperty(value = "渠道名称")
private String channelCodeName;
@ApiModelProperty(value = "支付订单号")
private String no;
}

View File

@@ -0,0 +1,113 @@
package cn.iocoder.yudao.module.pay.controller.admin.order.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel("管理后台 - 支付订单分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayOrderPageReqVO extends PageParam {
@ApiModelProperty(value = "商户编号")
private Long merchantId;
@ApiModelProperty(value = "应用编号")
private Long appId;
@ApiModelProperty(value = "渠道编号")
private Long channelId;
@ApiModelProperty(value = "渠道编码")
private String channelCode;
@ApiModelProperty(value = "商户订单编号")
private String merchantOrderId;
@ApiModelProperty(value = "商品标题")
private String subject;
@ApiModelProperty(value = "商品描述")
private String body;
@ApiModelProperty(value = "异步通知地址")
private String notifyUrl;
@ApiModelProperty(value = "通知商户支付结果的回调状态")
private Integer notifyStatus;
@ApiModelProperty(value = "支付金额,单位:分")
private Long amount;
@ApiModelProperty(value = "渠道手续费,单位:百分比")
private Double channelFeeRate;
@ApiModelProperty(value = "渠道手续金额,单位:分")
private Long channelFeeAmount;
@ApiModelProperty(value = "支付状态")
private Integer status;
@ApiModelProperty(value = "用户 IP")
private String userIp;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始订单失效时间")
private Date beginExpireTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束订单失效时间")
private Date endExpireTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始订单支付成功时间")
private Date beginSuccessTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束订单支付成功时间")
private Date endSuccessTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始订单支付通知时间")
private Date beginNotifyTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束订单支付通知时间")
private Date endNotifyTime;
@ApiModelProperty(value = "支付成功的订单拓展单编号")
private Long successExtensionId;
@ApiModelProperty(value = "退款状态")
private Integer refundStatus;
@ApiModelProperty(value = "退款次数")
private Integer refundTimes;
@ApiModelProperty(value = "退款总金额,单位:分")
private Long refundAmount;
@ApiModelProperty(value = "渠道用户编号")
private String channelUserId;
@ApiModelProperty(value = "渠道订单号")
private String channelOrderNo;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

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

View File

@@ -0,0 +1,156 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
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.merchant.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
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.service.merchant.PayAppService;
import cn.iocoder.yudao.module.pay.service.merchant.PayMerchantService;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
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.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
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.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api(tags = "管理后台 - 退款订单")
@RestController
@RequestMapping("/pay/refund")
@Validated
public class PayRefundController {
@Resource
private PayRefundService refundService;
@Resource
private PayMerchantService merchantService;
@Resource
private PayAppService appService;
@Resource
private PayOrderService orderService;
@GetMapping("/get")
@ApiOperation("获得退款订单")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('pay:refund:query')")
public CommonResult<PayRefundDetailsRespVO> getRefund(@RequestParam("id") Long id) {
PayRefundDO refund = refundService.getRefund(id);
if (ObjectUtil.isNull(refund)) {
return success(new PayRefundDetailsRespVO());
}
PayMerchantDO merchantDO = merchantService.getMerchant(refund.getMerchantId());
PayAppDO appDO = appService.getApp(refund.getAppId());
PayChannelEnum channelEnum = PayChannelEnum.getByCode(refund.getChannelCode());
PayOrderDO orderDO = orderService.getOrder(refund.getOrderId());
PayRefundDetailsRespVO refundDetail = PayRefundConvert.INSTANCE.refundDetailConvert(refund);
refundDetail.setMerchantName(ObjectUtil.isNotNull(merchantDO) ? merchantDO.getName() : "未知商户");
refundDetail.setAppName(ObjectUtil.isNotNull(appDO) ? appDO.getName() : "未知应用");
refundDetail.setChannelCodeName(ObjectUtil.isNotNull(channelEnum) ? channelEnum.getName() : "未知渠道");
refundDetail.setSubject(orderDO.getSubject());
return success(refundDetail);
}
@GetMapping("/page")
@ApiOperation("获得退款订单分页")
@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, PayMerchantDO> merchantMap = merchantService.getMerchantMap(
CollectionUtils.convertList(pageResult.getList(), PayRefundDO::getMerchantId));
// 处理应用ID数据
Map<Long, PayAppDO> appMap = appService.getAppMap(
CollectionUtils.convertList(pageResult.getList(), PayRefundDO::getAppId));
List<PayRefundPageItemRespVO> list = new ArrayList<>(pageResult.getList().size());
pageResult.getList().forEach(c -> {
PayMerchantDO merchantDO = merchantMap.get(c.getMerchantId());
PayAppDO appDO = appMap.get(c.getAppId());
PayChannelEnum channelEnum = PayChannelEnum.getByCode(c.getChannelCode());
PayRefundPageItemRespVO item = PayRefundConvert.INSTANCE.pageItemConvert(c);
item.setMerchantName(ObjectUtil.isNotNull(merchantDO) ? merchantDO.getName() : "未知商户");
item.setAppName(ObjectUtil.isNotNull(appDO) ? appDO.getName() : "未知应用");
item.setChannelCodeName(ObjectUtil.isNotNull(channelEnum) ? channelEnum.getName() : "未知渠道");
list.add(item);
});
return success(new PageResult<>(list, pageResult.getTotal()));
}
@GetMapping("/export-excel")
@ApiOperation("导出退款订单 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<>());
}
// 处理商户ID数据
Map<Long, PayMerchantDO> merchantMap = merchantService.getMerchantMap(
CollectionUtils.convertList(list, PayRefundDO::getMerchantId));
// 处理应用ID数据
Map<Long, PayAppDO> appMap = appService.getAppMap(
CollectionUtils.convertList(list, PayRefundDO::getAppId));
List<PayRefundExcelVO> excelDatum = new ArrayList<>(list.size());
// 处理商品名称数据
Map<Long, PayOrderDO> orderMap = orderService.getOrderSubjectMap(
CollectionUtils.convertList(list, PayRefundDO::getOrderId));
list.forEach(c -> {
PayMerchantDO merchantDO = merchantMap.get(c.getMerchantId());
PayAppDO appDO = appMap.get(c.getAppId());
PayChannelEnum channelEnum = PayChannelEnum.getByCode(c.getChannelCode());
PayRefundExcelVO excelItem = PayRefundConvert.INSTANCE.excelConvert(c);
excelItem.setMerchantName(ObjectUtil.isNotNull(merchantDO) ? merchantDO.getName() : "未知商户");
excelItem.setAppName(ObjectUtil.isNotNull(appDO) ? appDO.getName() : "未知应用");
excelItem.setChannelCodeName(ObjectUtil.isNotNull(channelEnum) ? channelEnum.getName() : "未知渠道");
excelItem.setSubject(orderMap.get(c.getOrderId()).getSubject());
excelDatum.add(excelItem);
});
// 导出 Excel
ExcelUtils.write(response, "退款订单.xls", "数据", PayRefundExcelVO.class, excelDatum);
}
}

View File

@@ -0,0 +1,110 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import java.util.Date;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
/**
* 退款订单 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class PayRefundBaseVO {
@ApiModelProperty(value = "商户编号", required = true)
@NotNull(message = "商户编号不能为空")
private Long merchantId;
@ApiModelProperty(value = "应用编号", required = true)
@NotNull(message = "应用编号不能为空")
private Long appId;
@ApiModelProperty(value = "渠道编号", required = true)
@NotNull(message = "渠道编号不能为空")
private Long channelId;
@ApiModelProperty(value = "渠道编码", required = true)
@NotNull(message = "渠道编码不能为空")
private String channelCode;
@ApiModelProperty(value = "支付订单编号 pay_order 表id", required = true)
@NotNull(message = "支付订单编号 pay_order 表id不能为空")
private Long orderId;
@ApiModelProperty(value = "交易订单号 pay_extension 表no 字段", required = true)
@NotNull(message = "交易订单号 pay_extension 表no 字段不能为空")
private String tradeNo;
@ApiModelProperty(value = "商户订单编号(商户系统生成)", required = true)
@NotNull(message = "商户订单编号(商户系统生成)不能为空")
private String merchantOrderId;
@ApiModelProperty(value = "商户退款订单号(商户系统生成)", required = true)
@NotNull(message = "商户退款订单号(商户系统生成)不能为空")
private String merchantRefundNo;
@ApiModelProperty(value = "异步通知商户地址", required = true)
@NotNull(message = "异步通知商户地址不能为空")
private String notifyUrl;
@ApiModelProperty(value = "通知商户退款结果的回调状态", required = true)
@NotNull(message = "通知商户退款结果的回调状态不能为空")
private Integer notifyStatus;
@ApiModelProperty(value = "退款状态", required = true)
@NotNull(message = "退款状态不能为空")
private Integer status;
@ApiModelProperty(value = "退款类型(部分退款,全部退款)", required = true)
@NotNull(message = "退款类型(部分退款,全部退款)不能为空")
private Integer type;
@ApiModelProperty(value = "支付金额,单位分", required = true)
@NotNull(message = "支付金额,单位分不能为空")
private Long payAmount;
@ApiModelProperty(value = "退款金额,单位分", required = true)
@NotNull(message = "退款金额,单位分不能为空")
private Long refundAmount;
@ApiModelProperty(value = "退款原因", required = true)
@NotNull(message = "退款原因不能为空")
private String reason;
@ApiModelProperty(value = "用户 IP")
private String userIp;
@ApiModelProperty(value = "渠道订单号pay_order 中的channel_order_no 对应", required = true)
@NotNull(message = "渠道订单号pay_order 中的channel_order_no 对应不能为空")
private String channelOrderNo;
@ApiModelProperty(value = "渠道退款单号,渠道返回")
private String channelRefundNo;
@ApiModelProperty(value = "渠道调用报错时,错误码")
private String channelErrorCode;
@ApiModelProperty(value = "渠道调用报错时,错误信息")
private String channelErrorMsg;
@ApiModelProperty(value = "支付渠道的额外参数")
private String channelExtras;
@ApiModelProperty(value = "退款失效时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private Date expireTime;
@ApiModelProperty(value = "退款成功时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private Date successTime;
@ApiModelProperty(value = "退款通知时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private Date notifyTime;
}

View File

@@ -0,0 +1,12 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund.vo;
import lombok.*;
import io.swagger.annotations.*;
@ApiModel("管理后台 - 退款订单创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundCreateReqVO extends PayRefundBaseVO {
}

View File

@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotNull;
import java.util.Date;
@ApiModel("管理后台 - 退款订单详情 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundDetailsRespVO extends PayRefundBaseVO {
@ApiModelProperty(value = "支付退款编号", required = true)
private Long id;
@ApiModelProperty(value = "商户名称")
private String merchantName;
@ApiModelProperty(value = "应用名称")
private String appName;
@ApiModelProperty(value = "渠道编号名称")
private String channelCodeName;
@NotNull(message = "商品标题不能为空")
private String subject;
@ApiModelProperty(value = "创建时间")
private Date createTime;
@ApiModelProperty(value = "更新时间")
private Date updateTime;
}

View File

@@ -0,0 +1,88 @@
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.module.pay.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
/**
* 退款订单 Excel VO
*
* @author aquan
*/
@Data
public class PayRefundExcelVO {
@ExcelProperty("支付退款编号")
private Long id;
@ExcelProperty("商品名称")
private String subject;
@ExcelProperty(value = "商户名称")
private String merchantName;
@ExcelProperty(value = "应用名称")
private String appName;
@ExcelProperty(value = "渠道编号名称")
private String channelCodeName;
@ExcelProperty("交易订单号")
private String tradeNo;
@ExcelProperty("商户订单编号")
private String merchantOrderId;
@ExcelProperty("商户退款订单号")
private String merchantRefundNo;
@ExcelProperty("异步通知商户地址")
private String notifyUrl;
@DictFormat(DictTypeConstants.PAY_ORDER_NOTIFY_STATUS)
@ExcelProperty(value = "商户退款结果回调状态", converter = DictConvert.class)
private Integer notifyStatus;
@DictFormat(DictTypeConstants.PAY_REFUND_ORDER_STATUS)
@ExcelProperty(value = "退款状态", converter = DictConvert.class)
private Integer status;
@DictFormat(DictTypeConstants.PAY_REFUND_ORDER_TYPE)
@ExcelProperty(value = "退款类型", converter = DictConvert.class)
private Integer type;
@ExcelProperty("支付金额,单位:元")
private String payAmount;
@ExcelProperty("退款金额,单位:元")
private String refundAmount;
@ExcelProperty("退款原因")
private String reason;
@ExcelProperty("用户付款 IP")
private String userIp;
@ExcelProperty("渠道订单号")
private String channelOrderNo;
@ExcelProperty("渠道退款单号")
private String channelRefundNo;
@ExcelProperty("创建时间")
private Date createTime;
@ExcelProperty("退款成功时间")
private Date successTime;
@ExcelProperty("退款通知时间")
private Date notifyTime;
@ExcelProperty("退款失效时间")
private Date expireTime;
}

View File

@@ -0,0 +1,111 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(value = "管理后台 - 退款订单 Excel 导出 Request VO", description = "参数和 PayRefundPageReqVO 是一致的")
@Data
public class PayRefundExportReqVO {
@ApiModelProperty(value = "商户编号")
private Long merchantId;
@ApiModelProperty(value = "应用编号")
private Long appId;
@ApiModelProperty(value = "渠道编号")
private Long channelId;
@ApiModelProperty(value = "渠道编码")
private String channelCode;
@ApiModelProperty(value = "支付订单编号 pay_order 表id")
private Long orderId;
@ApiModelProperty(value = "交易订单号 pay_extension 表no 字段")
private String tradeNo;
@ApiModelProperty(value = "商户订单编号(商户系统生成)")
private String merchantOrderId;
@ApiModelProperty(value = "商户退款订单号(商户系统生成)")
private String merchantRefundNo;
@ApiModelProperty(value = "异步通知商户地址")
private String notifyUrl;
@ApiModelProperty(value = "通知商户退款结果的回调状态")
private Integer notifyStatus;
@ApiModelProperty(value = "退款状态")
private Integer status;
@ApiModelProperty(value = "退款类型(部分退款,全部退款)")
private Integer type;
@ApiModelProperty(value = "支付金额,单位分")
private Long payAmount;
@ApiModelProperty(value = "退款金额,单位分")
private Long refundAmount;
@ApiModelProperty(value = "退款原因")
private String reason;
@ApiModelProperty(value = "用户 IP")
private String userIp;
@ApiModelProperty(value = "渠道订单号pay_order 中的channel_order_no 对应")
private String channelOrderNo;
@ApiModelProperty(value = "渠道退款单号,渠道返回")
private String channelRefundNo;
@ApiModelProperty(value = "渠道调用报错时,错误码")
private String channelErrorCode;
@ApiModelProperty(value = "渠道调用报错时,错误信息")
private String channelErrorMsg;
@ApiModelProperty(value = "支付渠道的额外参数")
private String channelExtras;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始退款失效时间")
private Date beginExpireTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束退款失效时间")
private Date endExpireTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始退款成功时间")
private Date beginSuccessTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束退款成功时间")
private Date endSuccessTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始退款通知时间")
private Date beginNotifyTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束退款通知时间")
private Date endNotifyTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Date;
@ApiModel("管理后台 - 退款订单分页查询 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundPageItemRespVO extends PayRefundBaseVO {
@ApiModelProperty(value = "支付订单编号", required = true)
private Long id;
@ApiModelProperty(value = "商户名称")
private String merchantName;
@ApiModelProperty(value = "应用名称")
private String appName;
@ApiModelProperty(value = "渠道名称")
private String channelCodeName;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@@ -0,0 +1,116 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel("管理后台 - 退款订单分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundPageReqVO extends PageParam {
@ApiModelProperty(value = "商户编号")
private Long merchantId;
@ApiModelProperty(value = "应用编号")
private Long appId;
@ApiModelProperty(value = "渠道编号")
private Long channelId;
@ApiModelProperty(value = "渠道编码")
private String channelCode;
@ApiModelProperty(value = "支付订单编号 pay_order 表id")
private Long orderId;
@ApiModelProperty(value = "交易订单号 pay_extension 表no 字段")
private String tradeNo;
@ApiModelProperty(value = "商户订单编号(商户系统生成)")
private String merchantOrderId;
@ApiModelProperty(value = "商户退款订单号(商户系统生成)")
private String merchantRefundNo;
@ApiModelProperty(value = "异步通知商户地址")
private String notifyUrl;
@ApiModelProperty(value = "通知商户退款结果的回调状态")
private Integer notifyStatus;
@ApiModelProperty(value = "退款状态")
private Integer status;
@ApiModelProperty(value = "退款类型(部分退款,全部退款)")
private Integer type;
@ApiModelProperty(value = "支付金额,单位分")
private Long payAmount;
@ApiModelProperty(value = "退款金额,单位分")
private Long refundAmount;
@ApiModelProperty(value = "退款原因")
private String reason;
@ApiModelProperty(value = "用户 IP")
private String userIp;
@ApiModelProperty(value = "渠道订单号pay_order 中的channel_order_no 对应")
private String channelOrderNo;
@ApiModelProperty(value = "渠道退款单号,渠道返回")
private String channelRefundNo;
@ApiModelProperty(value = "渠道调用报错时,错误码")
private String channelErrorCode;
@ApiModelProperty(value = "渠道调用报错时,错误信息")
private String channelErrorMsg;
@ApiModelProperty(value = "支付渠道的额外参数")
private String channelExtras;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始退款失效时间")
private Date beginExpireTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束退款失效时间")
private Date endExpireTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始退款成功时间")
private Date beginSuccessTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束退款成功时间")
private Date endSuccessTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始退款通知时间")
private Date beginNotifyTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束退款通知时间")
private Date endNotifyTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Date;
@ApiModel("管理后台 - 退款订单 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundRespVO extends PayRefundBaseVO {
@ApiModelProperty(value = "支付退款编号", required = true)
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.pay.controller.admin.refund.vo;
import lombok.*;
import io.swagger.annotations.*;
import javax.validation.constraints.*;
@ApiModel("管理后台 - 退款订单更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayRefundUpdateReqVO extends PayRefundBaseVO {
@ApiModelProperty(value = "支付退款编号", required = true)
@NotNull(message = "支付退款编号不能为空")
private Long id;
}

View File

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

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.pay.controller.app.order.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Map;
@ApiModel("用户 APP - 支付订单提交 Request VO")
@Data
@Accessors(chain = true)
public class AppPayOrderSubmitReqVO {
@ApiModelProperty(value = "支付单编号", required = true, example = "1024")
@NotNull(message = "支付单编号不能为空")
private Long id;
@ApiModelProperty(value = "支付渠道", required = true, example = "wx_pub")
@NotEmpty(message = "支付渠道不能为空")
private String channelCode;
@ApiModelProperty(value = "支付渠道的额外参数", notes = "例如说,微信公众号需要传递 openid 参数")
private Map<String, String> channelExtras;
}

View File

@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.pay.controller.app.order.vo;
import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@ApiModel("用户 APP - 支付订单提交 Response VO")
@Data
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AppPayOrderSubmitRespVO {
/**
* 调用支付渠道的响应结果
*/
private Object invokeResponse;
}

View File

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

View File

@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.pay.controller.app.refund;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.pay.controller.app.refund.vo.AppPayRefundReqVO;
import cn.iocoder.yudao.module.pay.controller.app.refund.vo.AppPayRefundRespVO;
import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
import cn.iocoder.yudao.module.pay.service.order.dto.PayRefundReqDTO;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import cn.iocoder.yudao.module.pay.util.PaySeqUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
@Api(tags = "用户 APP - 退款订单")
@RestController
@RequestMapping("/pay/refund")
@Validated
@Slf4j
public class AppPayRefundController {
@Resource
private PayRefundService refundService;
@PostMapping("/refund")
@ApiOperation("提交退款订单")
public CommonResult<AppPayRefundRespVO> submitRefundOrder(@RequestBody AppPayRefundReqVO reqVO){
PayRefundReqDTO req = PayRefundConvert.INSTANCE.convert(reqVO);
req.setUserIp(getClientIP());
// TODO 测试暂时模拟生成商户退款订单
if(StrUtil.isEmpty(reqVO.getMerchantRefundId())) {
req.setMerchantRefundId(PaySeqUtils.genMerchantRefundNo());
}
return success(PayRefundConvert.INSTANCE.convert(refundService.submitRefundOrder(req)));
}
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.pay.controller.app.refund.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
@ApiModel("用户 APP - 退款订单 Req VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppPayRefundReqVO {
@ApiModelProperty(value = "支付订单编号自增", required = true, example = "10")
@NotEmpty(message = "支付订单编号自增")
private Long payOrderId;
@ApiModelProperty(value = "退款金额", required = true, example = "1")
@NotEmpty(message = "退款金额")
private Long amount;
@ApiModelProperty(value = "退款原因", required = true, example = "不喜欢")
@NotEmpty(message = "退款原因")
private String reason;
@ApiModelProperty(value = "商户退款订单号", required = true, example = "MR202111180000000001")
//TODO 测试暂时模拟生成
//@NotEmpty(message = "商户退款订单号")
private String merchantRefundId;
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.pay.controller.app.refund.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@ApiModel("用户 APP - 提交退款订单 Response VO")
@Data
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AppPayRefundRespVO {
@ApiModelProperty(value = "退款订单编号", required = true, example = "10")
private Long refundId;
}

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,39 @@
package cn.iocoder.yudao.module.pay.convert.app;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.*;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* 支付应用信息 Convert
*
* @author 芋艿
*/
@Mapper
public interface PayAppConvert {
PayAppConvert INSTANCE = Mappers.getMapper(PayAppConvert.class);
PayAppPageItemRespVO pageConvert (PayAppDO bean);
PayAppPageItemRespVO.PayMerchant convert(PayMerchantDO bean);
PayAppDO convert(PayAppCreateReqVO bean);
PayAppDO convert(PayAppUpdateReqVO bean);
PayAppRespVO convert(PayAppDO bean);
List<PayAppRespVO> convertList(List<PayAppDO> list);
PageResult<PayAppRespVO> convertPage(PageResult<PayAppDO> page);
List<PayAppExcelVO> convertList02(List<PayAppDO> list);
}

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.pay.convert.channel;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelExcelVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelUpdateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.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);
List<PayChannelRespVO> convertList(List<PayChannelDO> list);
PageResult<PayChannelRespVO> convertPage(PageResult<PayChannelDO> page);
List<PayChannelExcelVO> convertList02(List<PayChannelDO> list);
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.pay.convert.merchant;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantExcelVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantUpdateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface PayMerchantConvert {
PayMerchantConvert INSTANCE = Mappers.getMapper(PayMerchantConvert.class);
PayMerchantDO convert(PayMerchantCreateReqVO bean);
PayMerchantDO convert(PayMerchantUpdateReqVO bean);
PayMerchantRespVO convert(PayMerchantDO bean);
List<PayMerchantRespVO> convertList(List<PayMerchantDO> list);
PageResult<PayMerchantRespVO> convertPage(PageResult<PayMerchantDO> page);
List<PayMerchantExcelVO> convertList02(List<PayMerchantDO> list);
}

View File

@@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.pay.convert.order;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderDetailsRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExcelVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageItemRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderRespVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayOrderSubmitReqDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
/**
* 支付订单 Convert
*
* @author aquan
*/
@Mapper
public interface PayOrderConvert {
PayOrderConvert INSTANCE = Mappers.getMapper(PayOrderConvert.class);
PayOrderRespVO convert(PayOrderDO bean);
PayOrderDetailsRespVO orderDetailConvert(PayOrderDO bean);
PayOrderDetailsRespVO.PayOrderExtension orderDetailExtensionConvert(PayOrderExtensionDO bean);
List<PayOrderRespVO> convertList(List<PayOrderDO> list);
PageResult<PayOrderRespVO> convertPage(PageResult<PayOrderDO> page);
List<PayOrderExcelVO> convertList02(List<PayOrderDO> list);
/**
* 订单DO转自定义分页对象
*
* @param bean 订单DO
* @return 分页对象
*/
PayOrderPageItemRespVO pageConvertItemPage(PayOrderDO bean);
default PayOrderExcelVO excelConvert(PayOrderDO bean) {
if (bean == null) {
return null;
}
PayOrderExcelVO payOrderExcelVO = new PayOrderExcelVO();
payOrderExcelVO.setId(bean.getId());
payOrderExcelVO.setSubject(bean.getSubject());
payOrderExcelVO.setMerchantOrderId(bean.getMerchantOrderId());
payOrderExcelVO.setChannelOrderNo(bean.getChannelOrderNo());
payOrderExcelVO.setStatus(bean.getStatus());
payOrderExcelVO.setNotifyStatus(bean.getNotifyStatus());
payOrderExcelVO.setNotifyUrl(bean.getNotifyUrl());
payOrderExcelVO.setCreateTime(bean.getCreateTime());
payOrderExcelVO.setSuccessTime(bean.getSuccessTime());
payOrderExcelVO.setExpireTime(bean.getExpireTime());
payOrderExcelVO.setNotifyTime(bean.getNotifyTime());
payOrderExcelVO.setUserIp(bean.getUserIp());
payOrderExcelVO.setRefundStatus(bean.getRefundStatus());
payOrderExcelVO.setRefundTimes(bean.getRefundTimes());
payOrderExcelVO.setBody(bean.getBody());
BigDecimal multiple = new BigDecimal(100);
payOrderExcelVO.setAmount(BigDecimal.valueOf(bean.getAmount())
.divide(multiple, 2, RoundingMode.HALF_UP).toString());
payOrderExcelVO.setChannelFeeAmount(BigDecimal.valueOf(bean.getChannelFeeAmount())
.divide(multiple, 2, RoundingMode.HALF_UP).toString());
payOrderExcelVO.setChannelFeeRate(java.math.BigDecimal.valueOf(bean.getChannelFeeRate())
.multiply(multiple).toString());
payOrderExcelVO.setRefundAmount(BigDecimal.valueOf(bean.getRefundAmount())
.divide(multiple, 2, RoundingMode.HALF_UP).toString());
return payOrderExcelVO;
}
PayOrderDO convert(PayOrderCreateReqDTO bean);
PayOrderExtensionDO convert(PayOrderSubmitReqDTO bean);
PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqDTO bean);
}

View File

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

View File

@@ -0,0 +1,109 @@
package cn.iocoder.yudao.module.pay.convert.refund;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.*;
import cn.iocoder.yudao.module.pay.controller.app.refund.vo.AppPayRefundReqVO;
import cn.iocoder.yudao.module.pay.controller.app.refund.vo.AppPayRefundRespVO;
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.service.order.dto.PayRefundReqDTO;
import cn.iocoder.yudao.module.pay.service.order.dto.PayRefundRespDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
/**
* 退款订单 Convert
*
* @author aquan
*/
@Mapper
public interface PayRefundConvert {
PayRefundConvert INSTANCE = Mappers.getMapper(PayRefundConvert.class);
PayRefundDO convert(PayRefundCreateReqVO bean);
PayRefundDO convert(PayRefundUpdateReqVO bean);
PayRefundRespVO convert(PayRefundDO bean);
/**
* 退款订单 DO 转 退款详情订单 VO
*
* @param bean 退款订单 DO
* @return 退款详情订单 VO
*/
PayRefundDetailsRespVO refundDetailConvert(PayRefundDO bean);
/**
* 退款订单DO 转 分页退款条目VO
*
* @param bean 退款订单DO
* @return 分页退款条目VO
*/
PayRefundPageItemRespVO pageItemConvert(PayRefundDO bean);
List<PayRefundRespVO> convertList(List<PayRefundDO> list);
PageResult<PayRefundRespVO> convertPage(PageResult<PayRefundDO> page);
List<PayRefundExcelVO> convertList02(List<PayRefundDO> list);
/**
* 退款订单DO 转 导出excel VO
*
* @param bean 退款订单DO
* @return 导出 excel VO
*/
default PayRefundExcelVO excelConvert(PayRefundDO bean) {
if (bean == null) {
return null;
}
PayRefundExcelVO payRefundExcelVO = new PayRefundExcelVO();
payRefundExcelVO.setId(bean.getId());
payRefundExcelVO.setTradeNo(bean.getTradeNo());
payRefundExcelVO.setMerchantOrderId(bean.getMerchantOrderId());
payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
payRefundExcelVO.setNotifyUrl(bean.getNotifyUrl());
payRefundExcelVO.setNotifyStatus(bean.getNotifyStatus());
payRefundExcelVO.setStatus(bean.getStatus());
payRefundExcelVO.setType(bean.getType());
payRefundExcelVO.setReason(bean.getReason());
payRefundExcelVO.setUserIp(bean.getUserIp());
payRefundExcelVO.setChannelOrderNo(bean.getChannelOrderNo());
payRefundExcelVO.setChannelRefundNo(bean.getChannelRefundNo());
payRefundExcelVO.setExpireTime(bean.getExpireTime());
payRefundExcelVO.setSuccessTime(bean.getSuccessTime());
payRefundExcelVO.setNotifyTime(bean.getNotifyTime());
payRefundExcelVO.setCreateTime(bean.getCreateTime());
BigDecimal multiple = new BigDecimal(100);
payRefundExcelVO.setPayAmount(BigDecimal.valueOf(bean.getPayAmount())
.divide(multiple, 2, RoundingMode.HALF_UP).toString());
payRefundExcelVO.setRefundAmount(BigDecimal.valueOf(bean.getRefundAmount())
.divide(multiple, 2, RoundingMode.HALF_UP).toString());
return payRefundExcelVO;
}
//TODO 太多需要处理了, 暂时不用
@Mappings(value = {
@Mapping(source = "amount", target = "payAmount"),
@Mapping(source = "id", target = "orderId"),
@Mapping(target = "status",ignore = true)
})
PayRefundDO convert(PayOrderDO orderDO);
PayRefundReqDTO convert(AppPayRefundReqVO bean);
AppPayRefundRespVO convert(PayRefundRespDTO bean);
}

View File

@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.merchant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 支付应用 DO
* 一个商户下,可能会有多个支付应用。例如说,京东有京东商城、京东到家等等
* 不过一般来说,一个商户,只有一个应用哈~
*
* 即 PayMerchantDO : PayAppDO = 1 : n
*
* @author 芋道源码
*/
@TableName("pay_app")
@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 payNotifyUrl;
/**
* 退款结果的回调地址
*/
private String refundNotifyUrl;
/**
* 商户编号
*
* 关联 {@link PayMerchantDO#getId()}
*/
private Long merchantId;
}

View File

@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.merchant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import 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 芋道源码
*/
@Data
@TableName(value = "pay_channel", autoResultMap = true)
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayChannelDO extends BaseDO {
/**
* 渠道编号,数据库自增
*/
private Long id;
/**
* 渠道编码
*
* 枚举 {@link PayChannelEnum}
*/
private String code;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 渠道费率,单位:百分比
*/
private Double feeRate;
/**
* 备注
*/
private String remark;
/**
* 商户编号
*
* 关联 {@link PayMerchantDO#getId()}
*/
private Long merchantId;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 支付渠道配置
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private PayClientConfig config;
}

View File

@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.merchant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 支付商户信息 DO
* 目前暂时没有特别的用途,主要为未来多商户提供基础。
*
* @author 芋道源码
*/
@Data
@TableName("pay_merchant")
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayMerchantDO extends BaseDO {
/**
* 商户编号,数据库自增
*/
@TableId
private Long id;
/**
* 商户号
* 例如说M233666999
* 只有新增时插入,不允许修改
*/
private String no;
/**
* 商户全称
*/
private String name;
/**
* 商户简称
*/
private String shortName;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,49 @@
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.TableName;
import lombok.*;
/**
* 商户支付、退款等的通知 Log
* 每次通知时,都会在该表中,记录一次 Log方便排查问题
*
* @author 芋道源码
*/
@TableName("pay_notify_log")
@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,99 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.notify;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
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 cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 商户支付、退款等的通知
* 在支付系统收到支付渠道的支付、退款的结果后,需要不断的通知到业务系统,直到成功。
*
* @author 芋道源码
*/
@TableName("pay_notify_task")
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class PayNotifyTaskDO extends BaseDO {
/**
* 通知频率,单位为秒。
*
* 算上首次的通知,实际是一共 1 + 8 = 9 次。
*/
public static final Integer[] NOTIFY_FREQUENCY = new Integer[]{
15, 15, 30, 180,
1800, 1800, 1800, 3600
};
/**
* 编号,自增
*/
private Long id;
/**
* 商户编号
*
* 关联 {@link PayMerchantDO#getId()}
*/
private Long merchantId;
/**
* 应用编号
*
* 关联 {@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 Date nextNotifyTime;
/**
* 最后一次执行时间
*/
private Date lastExecuteTime;
/**
* 当前通知次数
*/
private Integer notifyTimes;
/**
* 最大可通知次数
*/
private Integer maxNotifyTimes;
/**
* 通知地址
*/
private String notifyUrl;
}

View File

@@ -0,0 +1,162 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.order;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.util.Date;
/**
* 支付订单 DO
*
* @author 芋道源码
*/
@TableName("pay_order")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderDO extends BaseDO {
/**
* 订单编号,数据库自增
*/
private Long id;
/**
* 商户编号
*
* 关联 {@link PayMerchantDO#getId()}
*/
private Long merchantId;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 渠道编号
*
* 关联 {@link PayChannelDO#getId()}
*/
private Long channelId;
/**
* 渠道编码
*
* 枚举 {@link PayChannelEnum}
*/
private String channelCode;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
* 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一
*/
private String merchantOrderId;
/**
* 商品标题
*/
private String subject;
/**
* 商品描述信息
*/
private String body;
/**
* 异步通知地址
*/
private String notifyUrl;
/**
* 通知商户支付结果的回调状态
*
* 枚举 {@link PayOrderNotifyStatusEnum}
*/
private Integer notifyStatus;
// /**
// * 商户拓展参数
// */
// private Map<String, String> merchantExtras;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
*/
private Long amount;
/**
* 渠道手续费,单位:百分比
*
* 冗余 {@link PayChannelDO#getFeeRate()}
*/
private Double channelFeeRate;
/**
* 渠道手续金额,单位:分
*/
private Long channelFeeAmount;
/**
* 支付状态
*
* 枚举 {@link PayOrderStatusEnum}
*/
private Integer status;
/**
* 用户 IP
*/
private String userIp;
/**
* 订单失效时间
*/
private Date expireTime;
/**
* 订单支付成功时间
*/
private Date successTime;
/**
* 订单支付通知时间,即支付渠道的通知时间
*/
private Date notifyTime;
/**
* 支付成功的订单拓展单编号
*
* 关联 {@link PayOrderDO#getId()}
*/
private Long successExtensionId;
// ========== 退款相关字段 ==========
/**
* 退款状态
*
* 枚举 {@link PayRefundTypeEnum}
*/
private Integer refundStatus;
/**
* 退款次数
*/
private Integer refundTimes;
/**
* 退款总金额,单位:分
*/
private Long refundAmount;
// ========== 渠道相关字段 ==========
/**
* 渠道用户编号
*
* 例如说,微信 openid、支付宝账号
*/
private String channelUserId;
/**
* 渠道订单号
*/
private String channelOrderNo;
}

View File

@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.order;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
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)
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderExtensionDO extends BaseDO {
/**
* 订单拓展编号,数据库自增
*/
private Long id;
/**
* 支付订单号,根据规则生成
* 调用支付渠道时,使用该字段作为对接的订单号。
* 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no
* 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 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}
* 注意,只包含上述枚举的 WAITING 和 SUCCESS
*/
private Integer status;
/**
* 支付渠道的额外参数
*
* 参见 https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, String> channelExtras;
/**
* 支付渠道异步通知的内容
*
* 在支持成功后,会记录回调的数据
*/
private String channelNotifyData;
}

View File

@@ -0,0 +1,197 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.refund;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.util.Date;
/**
* 支付退款单 DO
* 一个支付订单,可以拥有多个支付退款单
*
* 即 PayOrderDO : PayRefundDO = 1 : n
*
* @author 芋道源码
*/
@TableName("pay_refund")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayRefundDO extends BaseDO {
/**
* 退款单编号,数据库自增
*/
@TableId
private Long id;
/**
* 商户编号
*
* 关联 {@link PayMerchantDO#getId()}
*/
private Long merchantId;
/**
* 应用编号
*
* 关联 {@link PayAppDO#getId()}
*/
private Long appId;
/**
* 渠道编号
*
* 关联 {@link PayChannelDO#getId()}
*/
private Long channelId;
/**
* 商户编码
*
* 枚举 {@link PayChannelEnum}
*/
private String channelCode;
/**
* 订单编号
*
* 关联 {@link PayOrderDO#getId()}
*/
private Long orderId;
/**
* 交易订单号,根据规则生成
* 调用支付渠道时,使用该字段作为对接的订单号。
* 1. 调用微信支付 https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 时,使用该字段作为 out_trade_no
* 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no
* 这里对应 pay_extension 里面的 no
* 例如说P202110132239124200055
*/
private String tradeNo;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
*/
private String merchantOrderId;
/**
* 商户退款订单号, 由商户系统产生, 由他们保证唯一,不能为空,通知商户时会传该字段。
* 例如说,内部系统 A 的退款订单号。需要保证每个 PayMerchantDO 唯一
* 个商户退款订单,对应一条退款请求记录。可多次提交。 渠道保持幂等
* 使用商户退款单,作为退款请求号
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
* 退款请求号。
* 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。
* 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更,
* 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。
* 退款单请求号,根据规则生成
* 例如说R202109181134287570000
*/
// TODO @jasonmerchantRefundNo =》merchantRefundOId
private String merchantRefundNo;
/**
* 异步通知地址
*/
private String notifyUrl;
/**
* 通知商户退款结果的回调状态
* TODO 0 未发送 1 已发送
*/
private Integer notifyStatus;
// ========== 退款相关字段 ==========
/**
* 退款状态
*
* 枚举 {@link PayRefundStatusEnum}
*/
private Integer status;
/**
* 退款类型(部分退款,全部退款)
*
* 枚举 {@link PayRefundTypeEnum}
*/
private Integer type;
/**
* 支付金额,单位:分
*/
private Long payAmount;
/**
* 退款金额,单位:分
*/
private Long refundAmount;
/**
* 退款原因
*/
private String reason;
/**
* 用户 IP
*/
private String userIp;
// ========== 渠道相关字段 ==========
/**
* 渠道订单号pay_order 中的channel_order_no 对应
*/
private String channelOrderNo;
/**
* 微信中的 refund_id
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml
* 支付宝没有
* 渠道退款单号,渠道返回
*/
private String channelRefundNo;
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
/**
* 支付渠道的额外参数
* 参见 https://www.pingxx.com/api/Refunds%20退款概述.html
*/
private String channelExtras;
/**
* TODO
* 退款失效时间
*/
private Date expireTime;
/**
* 退款成功时间
*/
private Date successTime;
/**
* 退款通知时间
*/
private Date notifyTime;
}

View File

@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.pay.dal.mysql.merchant;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.PayAppExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.PayAppPageReqVO;
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.QueryWrapperX;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayAppDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface PayAppMapper extends BaseMapperX<PayAppDO> {
default PageResult<PayAppDO> selectPage(PayAppPageReqVO reqVO, Collection<Long> merchantIds) {
return selectPage(reqVO, new QueryWrapperX<PayAppDO>()
.likeIfPresent("name", reqVO.getName())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("remark", reqVO.getRemark())
.eqIfPresent("pay_notify_url", reqVO.getPayNotifyUrl())
.eqIfPresent("refund_notify_url", reqVO.getRefundNotifyUrl())
.inIfPresent("merchant_id", merchantIds)
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
default List<PayAppDO> selectList(PayAppExportReqVO reqVO, Collection<Long> merchantIds) {
return selectList(new QueryWrapperX<PayAppDO>()
.likeIfPresent("name", reqVO.getName())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("remark", reqVO.getRemark())
.eqIfPresent("pay_notify_url", reqVO.getPayNotifyUrl())
.eqIfPresent("refund_notify_url", reqVO.getRefundNotifyUrl())
.inIfPresent("merchant_id", merchantIds)
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
default List<PayAppDO> getListByMerchantId(String merchantId) {
return selectList(new LambdaQueryWrapper<PayAppDO>()
.select(PayAppDO::getId, PayAppDO::getName)
.eq(PayAppDO::getMerchantId, merchantId));
}
// TODO @aquan方法名补充 ByMerchantId
default Long selectCount(Long merchantId) {
return selectCount(new LambdaQueryWrapper<PayAppDO>().eq(PayAppDO::getMerchantId, merchantId));
}
}

View File

@@ -0,0 +1,96 @@
package cn.iocoder.yudao.module.pay.dal.mysql.merchant;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Collection;
import java.util.Date;
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);
}
@Select("SELECT id FROM pay_channel WHERE update_time > #{maxUpdateTime} LIMIT 1")
Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
default PageResult<PayChannelDO> selectPage(PayChannelPageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<PayChannelDO>()
.eqIfPresent("code", reqVO.getCode())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("remark", reqVO.getRemark())
.eqIfPresent("fee_rate", reqVO.getFeeRate())
.eqIfPresent("merchant_id", reqVO.getMerchantId())
.eqIfPresent("app_id", reqVO.getAppId())
// .eqIfPresent("config", reqVO.getConfig())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id") );
}
default List<PayChannelDO> selectList(PayChannelExportReqVO reqVO) {
return selectList(new QueryWrapperX<PayChannelDO>()
.eqIfPresent("code", reqVO.getCode())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("remark", reqVO.getRemark())
.eqIfPresent("fee_rate", reqVO.getFeeRate())
.eqIfPresent("merchant_id", reqVO.getMerchantId())
.eqIfPresent("app_id", reqVO.getAppId())
// .eqIfPresent("config", reqVO.getConfig())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id") );
}
/**
* 根据条件获取渠道数量
*
* @param merchantId 商户编号
* @param appid 应用编号
* @param code 渠道编码
* @return 数量
*/
default Integer selectCount(Long merchantId, Long appid, String code) {
return this.selectCount(new QueryWrapper<PayChannelDO>().lambda()
.eq(PayChannelDO::getMerchantId, merchantId)
.eq(PayChannelDO::getAppId, appid)
.eq(PayChannelDO::getCode, code)).intValue();
}
/**
* 根据条件获取渠道
*
* @param merchantId 商户编号
* @param appid 应用编号 // TODO @aquanappid =》appId
* @param code 渠道编码
* @return 数量
*/
default PayChannelDO selectOne(Long merchantId, Long appid, String code) {
return this.selectOne((new QueryWrapper<PayChannelDO>().lambda()
.eq(PayChannelDO::getMerchantId, merchantId)
.eq(PayChannelDO::getAppId, appid)
.eq(PayChannelDO::getCode, code)
));
}
// TODO @aquanselect 命名
/**
* 根据支付应用ID集合获得支付渠道列表
*
* @param appIds 应用编号集合
* @return 支付渠道列表
*/
default List<PayChannelDO> getChannelListByAppIds(Collection<Long> appIds){
return this.selectList(new QueryWrapper<PayChannelDO>().lambda()
.in(PayChannelDO::getAppId, appIds));
}
}

View File

@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.pay.dal.mysql.merchant;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantPageReqVO;
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.QueryWrapperX;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface PayMerchantMapper extends BaseMapperX<PayMerchantDO> {
default PageResult<PayMerchantDO> selectPage(PayMerchantPageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<PayMerchantDO>()
.likeIfPresent("no", reqVO.getNo())
.likeIfPresent("name", reqVO.getName())
.likeIfPresent("short_name", reqVO.getShortName())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("remark", reqVO.getRemark())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
default List<PayMerchantDO> selectList(PayMerchantExportReqVO reqVO) {
return selectList(new QueryWrapperX<PayMerchantDO>()
.likeIfPresent("no", reqVO.getNo())
.likeIfPresent("name", reqVO.getName())
.likeIfPresent("short_name", reqVO.getShortName())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("remark", reqVO.getRemark())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
/**
* 根据商户名称模糊查询商户集合
*
* @param merchantName 商户名称
* @return 商户集合
*/
default List<PayMerchantDO> getMerchantListByName(String merchantName) {
return this.selectList(new QueryWrapperX<PayMerchantDO>()
.likeIfPresent("name", merchantName));
}
}

View File

@@ -0,0 +1,9 @@
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;
@Mapper
public interface PayNotifyLogCoreMapper extends BaseMapperX<PayNotifyLogDO> {
}

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.pay.dal.mysql.notify;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Date;
import java.util.List;
@Mapper
public interface PayNotifyTaskCoreMapper extends BaseMapperX<PayNotifyTaskDO> {
/**
* 获得需要通知的 PayNotifyTaskDO 记录。需要满足如下条件:
*
* 1. status 非成功
* 2. nextNotifyTime 小于当前时间
*
* @return PayTransactionNotifyTaskDO 数组
*/
default List<PayNotifyTaskDO> selectListByNotify() {
return selectList(new QueryWrapper<PayNotifyTaskDO>()
.in("status", PayNotifyStatusEnum.WAITING.getStatus(), PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(),
PayNotifyStatusEnum.REQUEST_FAILURE.getStatus())
.le("next_notify_time", new Date()));
}
}

View File

@@ -0,0 +1,20 @@
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;
@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));
}
}

View File

@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.pay.dal.mysql.order;
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.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface PayOrderMapper extends BaseMapperX<PayOrderDO> {
default PageResult<PayOrderDO> selectPage(PayOrderPageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<PayOrderDO>()
.eqIfPresent("merchant_id", reqVO.getMerchantId())
.eqIfPresent("app_id", reqVO.getAppId())
.eqIfPresent("channel_id", reqVO.getChannelId())
.eqIfPresent("channel_code", reqVO.getChannelCode())
.likeIfPresent("merchant_order_id", reqVO.getMerchantOrderId())
.eqIfPresent("notify_status", reqVO.getNotifyStatus())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("refund_status", reqVO.getRefundStatus())
.likeIfPresent("channel_order_no", reqVO.getChannelOrderNo())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
default List<PayOrderDO> selectList(PayOrderExportReqVO reqVO) {
return selectList(new QueryWrapperX<PayOrderDO>()
.eqIfPresent("merchant_id", reqVO.getMerchantId())
.eqIfPresent("app_id", reqVO.getAppId())
.eqIfPresent("channel_id", reqVO.getChannelId())
.eqIfPresent("channel_code", reqVO.getChannelCode())
.likeIfPresent("merchant_order_id", reqVO.getMerchantOrderId())
.eqIfPresent("notify_status", reqVO.getNotifyStatus())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("refund_status", reqVO.getRefundStatus())
.likeIfPresent("channel_order_no", reqVO.getChannelOrderNo())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
default List<PayOrderDO> findByIdListQueryOrderSubject(Collection<Long> idList) {
return selectList(new LambdaQueryWrapper<PayOrderDO>()
.select(PayOrderDO::getId, PayOrderDO::getSubject)
.in(PayOrderDO::getId, idList));
}
/**
* 查询符合的订单数量
*
* @param appId 应用编号
* @param status 订单状态
* @return 条数
*/
default Long selectCount(Long appId, Integer status) {
return selectCount(new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getAppId, appId)
.in(PayOrderDO::getStatus, status));
}
default PayOrderDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) {
return selectOne(new QueryWrapper<PayOrderDO>().eq("app_id", appId)
.eq("merchant_order_id", merchantOrderId));
}
default int updateByIdAndStatus(Long id, Integer status, PayOrderDO update) {
return update(update, new QueryWrapper<PayOrderDO>()
.eq("id", id).eq("status", status));
}
}

View File

@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.pay.dal.mysql.refund;
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.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
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 PageResult<PayRefundDO> selectPage(PayRefundPageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<PayRefundDO>()
.eqIfPresent("merchant_id", reqVO.getMerchantId())
.eqIfPresent("app_id", reqVO.getAppId())
.eqIfPresent("channel_code", reqVO.getChannelCode())
.likeIfPresent("merchant_refund_no", reqVO.getMerchantRefundNo())
.eqIfPresent("type", reqVO.getType())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("notify_status", reqVO.getNotifyStatus())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
default List<PayRefundDO> selectList(PayRefundExportReqVO reqVO) {
return selectList(new QueryWrapperX<PayRefundDO>()
.eqIfPresent("merchant_id", reqVO.getMerchantId())
.eqIfPresent("app_id", reqVO.getAppId())
.eqIfPresent("channel_code", reqVO.getChannelCode())
.likeIfPresent("merchant_refund_no", reqVO.getMerchantRefundNo())
.eqIfPresent("type", reqVO.getType())
.eqIfPresent("status", reqVO.getStatus())
.eqIfPresent("notify_status", reqVO.getNotifyStatus())
.betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc("id"));
}
default Long selectCount(Long appId, Integer status) {
return selectCount(new LambdaQueryWrapper<PayRefundDO>()
.eq(PayRefundDO::getAppId, appId)
.eq(PayRefundDO::getStatus, status));
}
default PayRefundDO selectByReqNo(String reqNo) {
return selectOne("req_no", reqNo);
}
default PayRefundDO selectByTradeNoAndMerchantRefundNo(String tradeNo, String merchantRefundNo){
return selectOne("trade_no", tradeNo, "merchant_refund_no", merchantRefundNo);
}
}

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.pay.dal.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import org.redisson.api.RLock;
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
/**
* 支付 Redis Key 枚举类
*
* @author 芋道源码
*/
public interface PayRedisKeyCoreConstants {
RedisKeyDefine PAY_NOTIFY_LOCK = new RedisKeyDefine("通知任务的分布式锁",
"pay_notify:lock:", // 参数来自 DefaultLockKeyBuilder 类
RedisKeyDefine.KeyTypeEnum.HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构
}

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.PayRedisKeyCoreConstants.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,32 @@
package cn.iocoder.yudao.module.pay.enums.notify;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付通知状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayNotifyStatusEnum {
WAITING(1, "等待通知"),
SUCCESS(2, "通知成功"),
FAILURE(3, "通知失败"), // 多次尝试,彻底失败
REQUEST_SUCCESS(4, "请求成功,但是结果失败"),
REQUEST_FAILURE(5, "请求失败"),
;
/**
* 状态
*/
private final Integer status;
/**
* 名字
*/
private final String name;
}

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.pay.enums.notify;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付通知类型
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayNotifyTypeEnum {
ORDER(1, "支付单"),
REFUND(2, "退款单"),
;
/**
* 类型
*/
private final Integer type;
/**
* 名字
*/
private final String name;
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.pay.enums.order;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付订单的通知状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayOrderNotifyStatusEnum implements IntArrayValuable {
NO(0, "未通知"),
SUCCESS(10, "通知成功"),
FAILURE(20, "通知失败")
;
private final Integer status;
private final String name;
@Override
public int[] array() {
return new int[0];
}
}

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.pay.enums.refund;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付订单的退款状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayRefundTypeEnum implements IntArrayValuable {
NO(0, "未退款"),
SOME(10, "部分退款"),
ALL(20, "全部退款")
;
private final Integer status;
private final String name;
@Override
public int[] array() {
return new int[0];
}
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.pay.job.notify;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 支付通知 Job
* 通过不断扫描待通知的 PayNotifyTaskDO 记录,回调业务线的回调接口
*
* @author 芋道源码
*/
@Component
@Slf4j
public class PayNotifyJob implements JobHandler {
@Resource
private PayNotifyService payNotifyCoreService;
@Override
public String execute(String param) throws Exception {
int notifyCount = payNotifyCoreService.executeNotify();
return String.format("执行支付通知 %s 个", notifyCount);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,116 @@
package cn.iocoder.yudao.module.pay.service.merchant;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.PayAppCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.PayAppExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.PayAppPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.PayAppUpdateReqVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.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 编号
*/
void deleteApp(Long id);
/**
* 获得支付应用信息
*
* @param id 编号
* @return 支付应用信息
*/
PayAppDO getApp(Long id);
/**
* 获得支付应用信息列表
*
* @param ids 编号
* @return 支付应用信息列表
*/
List<PayAppDO> getAppList(Collection<Long> ids);
/**
* 获得支付应用信息分页
*
* @param pageReqVO 分页查询
* @return 支付应用信息分页
*/
PageResult<PayAppDO> getAppPage(PayAppPageReqVO pageReqVO);
/**
* 获得支付应用信息列表, 用于 Excel 导出
*
* @param exportReqVO 查询条件
* @return 支付应用信息列表
*/
List<PayAppDO> getAppList(PayAppExportReqVO exportReqVO);
/**
* 修改应用信息状态
*
* @param id 应用编号
* @param status 状态{@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
*/
void updateAppStatus(Long id, Integer status);
/**
* 根据商户 ID 获得支付应用信息列表,
*
* @param merchantId 商户 ID
* @return 支付应用信息列表
*/
List<PayAppDO> getListByMerchantId(String merchantId);
/**
* 获得指定编号的商户 Map
*
* @param appIdList 应用编号集合
* @return 商户 Map
*/
default Map<Long, PayAppDO> getAppMap(Collection<Long> appIdList) {
List<PayAppDO> list = this.getAppList(appIdList);
return CollectionUtils.convertMap(list, PayAppDO::getId);
}
/**
* 支付应用的合法性
*
* 如果不合法,抛出 {@link ServiceException} 业务异常
*
* @param id 应用编号
* @return 应用信息
*/
PayAppDO validPayApp(Long id);
}

View File

@@ -0,0 +1,189 @@
package cn.iocoder.yudao.module.pay.service.merchant;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.PayAppCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.PayAppExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.PayAppPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.app.PayAppUpdateReqVO;
import cn.iocoder.yudao.module.pay.convert.app.PayAppConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.module.pay.dal.mysql.merchant.PayAppMapper;
import cn.iocoder.yudao.module.pay.dal.mysql.merchant.PayMerchantMapper;
import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import com.google.common.annotations.VisibleForTesting;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
/**
* 支付应用信息 Service 实现类
*
* @author aquan
*/
@Service
@Validated
public class PayAppServiceImpl implements PayAppService {
@Resource
private PayAppMapper appMapper;
// TODO @aquan使用对方的 Service。模块与模块之间避免直接调用对方的 mapper
@Resource
private PayMerchantMapper merchantMapper;
@Resource
private PayOrderMapper orderMapper;
@Resource
private PayRefundMapper refundMapper;
@Override
public Long createApp(PayAppCreateReqVO createReqVO) {
// 插入
PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO);
appMapper.insert(app);
// 返回
return app.getId();
}
@Override
public void updateApp(PayAppUpdateReqVO updateReqVO) {
// 校验存在
this.validateAppExists(updateReqVO.getId());
// 更新
PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO);
appMapper.updateById(updateObj);
}
@Override
public void deleteApp(Long id) {
// 校验存在
this.validateAppExists(id);
this.validateOrderTransactionExist(id);
// 删除
appMapper.deleteById(id);
}
private void validateAppExists(Long id) {
if (appMapper.selectById(id) == null) {
throw exception(PAY_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 PageResult<PayAppDO> getAppPage(PayAppPageReqVO pageReqVO) {
Set<Long> merchantIdList = this.getMerchantCondition(pageReqVO.getMerchantName());
if (StrUtil.isNotBlank(pageReqVO.getMerchantName()) && CollectionUtil.isEmpty(merchantIdList)) {
return new PageResult<>();
}
return appMapper.selectPage(pageReqVO, merchantIdList);
}
@Override
public List<PayAppDO> getAppList(PayAppExportReqVO exportReqVO) {
Set<Long> merchantIdList = this.getMerchantCondition(exportReqVO.getMerchantName());
if (StrUtil.isNotBlank(exportReqVO.getMerchantName()) && CollectionUtil.isEmpty(merchantIdList)) {
return new ArrayList<>();
}
return appMapper.selectList(exportReqVO, merchantIdList);
}
/**
* 获取商户编号集合,根据商户名称模糊查询得到所有的商户编号集合
*
* @param merchantName 商户名称
* @return 商户编号集合
*/
private Set<Long> getMerchantCondition(String merchantName) {
if (StrUtil.isBlank(merchantName)) {
return Collections.emptySet();
}
return convertSet(merchantMapper.getMerchantListByName(merchantName), PayMerchantDO::getId);
}
@Override
public void updateAppStatus(Long id, Integer status) {
// 校验商户存在
this.checkAppExists(id);
// 更新状态
PayAppDO app = new PayAppDO();
app.setId(id);
app.setStatus(status);
appMapper.updateById(app);
}
@Override
public List<PayAppDO> getListByMerchantId(String merchantId) {
return appMapper.getListByMerchantId(merchantId);
}
/**
* 检查商户是否存在
*
* @param id 商户编号
*/
@VisibleForTesting
public void checkAppExists(Long id) {
if (id == null) {
return;
}
PayAppDO payApp = appMapper.selectById(id);
if (payApp == null) {
throw exception(PAY_APP_NOT_FOUND);
}
}
/**
* 验证是否存在交易中或者退款中等处理中状态的订单
*
* @param appId 应用 ID
*/
private void validateOrderTransactionExist(Long appId) {
// 查看交易订单
if (orderMapper.selectCount(appId, PayOrderStatusEnum.WAITING.getStatus()) > 0) {
throw exception(PAY_APP_EXIST_TRANSACTION_ORDER_CANT_DELETE);
}
// 查看退款订单
if (refundMapper.selectCount(appId, PayRefundStatusEnum.CREATE.getStatus()) > 0) {
throw exception(PAY_APP_EXIST_TRANSACTION_ORDER_CANT_DELETE);
}
}
@Override
public PayAppDO validPayApp(Long id) {
PayAppDO app = appMapper.selectById(id);
// 校验是否存在
if (app == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_APP_NOT_FOUND);
}
// 校验是否禁用
if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_APP_IS_DISABLE);
}
return app;
}
}

View File

@@ -0,0 +1,133 @@
package cn.iocoder.yudao.module.pay.service.merchant;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelUpdateReqVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* 支付渠道 Service 接口
*
* @author aquan
*/
public interface PayChannelService {
/**
* 初始化支付客户端
*/
void initPayClients();
/**
* 创建支付渠道
*
* @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);
/**
* 获得支付渠道列表
*
* @param ids 编号
* @return 支付渠道
* 列表
*/
List<PayChannelDO> getChannelList(Collection<Long> ids);
/**
* 获得支付渠道分页
*
* @param pageReqVO 分页查询
* @return 支付渠道
* 分页
*/
PageResult<PayChannelDO> getChannelPage(PayChannelPageReqVO pageReqVO);
/**
* 获得支付渠道
* 列表, 用于 Excel 导出
*
* @param exportReqVO 查询条件
* @return 支付渠道列表
*/
List<PayChannelDO> getChannelList(PayChannelExportReqVO exportReqVO);
/**
* 根据支付应用ID集合获得支付渠道列表
*
* @param appIds 应用编号集合
* @return 支付渠道列表
*/
List<PayChannelDO> getChannelListByAppIds(Collection<Long> appIds);
/**
* 根据条件获取渠道数量
*
* @param merchantId 商户编号
* @param appid 应用编号
* @param code 渠道编码
* @return 数量
*/
Integer getChannelCountByConditions(Long merchantId, Long appid, String code);
/**
* 根据条件获取渠道
*
* @param merchantId 商户编号
* @param appid 应用编号
* @param code 渠道编码
* @return 数量
*/
PayChannelDO getChannelByConditions(Long merchantId, 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);
}

View File

@@ -0,0 +1,250 @@
package cn.iocoder.yudao.module.pay.service.merchant;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelUpdateReqVO;
import cn.iocoder.yudao.module.pay.convert.channel.PayChannelConvert;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.mysql.merchant.PayChannelMapper;
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.validation.Validator;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
/**
* 支付渠道 Service 实现类
*
* @author aquan
*/
@Service
@Slf4j
@Validated
public class PayChannelServiceImpl implements PayChannelService {
/**
* 定时执行 {@link #schedulePeriodicRefresh()} 的周期
* 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
/**
* 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
*/
private volatile Date maxUpdateTime;
@Resource
private PayClientFactory payClientFactory;
@Resource
private PayChannelMapper channelMapper;
@Resource
private Validator validator;
@Override
@PostConstruct
public void initPayClients() {
// 获取支付渠道,如果有更新
List<PayChannelDO> payChannels = this.loadPayChannelIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(payChannels)) {
return;
}
// 创建或更新支付 Client
payChannels.forEach(payChannel -> payClientFactory.createOrUpdatePayClient(payChannel.getId(),
payChannel.getCode(), payChannel.getConfig()));
// 写入缓存
assert payChannels.size() > 0; // 断言,避免告警
maxUpdateTime = payChannels.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
log.info("[initPayClients][初始化 PayChannel 数量为 {}]", payChannels.size());
}
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void schedulePeriodicRefresh() {
initPayClients();
}
/**
* 如果支付渠道发生变化,从数据库中获取最新的全量支付渠道。
* 如果未发生变化,则返回空
*
* @param maxUpdateTime 当前支付渠道的最大更新时间
* @return 支付渠道列表
*/
private List<PayChannelDO> loadPayChannelIfUpdate(Date maxUpdateTime) {
// 第一步,判断是否要更新。
if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
log.info("[loadPayChannelIfUpdate][首次加载全量支付渠道]");
} else { // 判断数据库中是否有更新的支付渠道
if (channelMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
return null;
}
log.info("[loadPayChannelIfUpdate][增量加载全量支付渠道]");
}
// 第二步,如果有更新,则从数据库加载所有支付渠道
return channelMapper.selectList();
}
@Override
public Long createChannel(PayChannelCreateReqVO reqVO) {
// 断言是否有重复的
PayChannelDO channelDO = this.getChannelByConditions(reqVO.getMerchantId(), reqVO.getAppId(), reqVO.getCode());
if (ObjectUtil.isNotNull(channelDO)) {
throw exception(CHANNEL_EXIST_SAME_CHANNEL_ERROR);
}
// 新增渠道
PayChannelDO channel = PayChannelConvert.INSTANCE.convert(reqVO);
settingConfigAndCheckParam(channel, reqVO.getConfig());
channelMapper.insert(channel);
return channel.getId();
}
@Override
public void updateChannel(PayChannelUpdateReqVO updateReqVO) {
// 校验存在
this.validateChannelExists(updateReqVO.getId());
// 更新
PayChannelDO channel = PayChannelConvert.INSTANCE.convert(updateReqVO);
settingConfigAndCheckParam(channel, updateReqVO.getConfig());
channelMapper.updateById(channel);
}
@Override
public void deleteChannel(Long id) {
// 校验存在
this.validateChannelExists(id);
// 删除
channelMapper.deleteById(id);
}
private void validateChannelExists(Long id) {
if (channelMapper.selectById(id) == null) {
throw exception(CHANNEL_NOT_EXISTS);
}
}
@Override
public PayChannelDO getChannel(Long id) {
return channelMapper.selectById(id);
}
@Override
public List<PayChannelDO> getChannelList(Collection<Long> ids) {
return channelMapper.selectBatchIds(ids);
}
@Override
public PageResult<PayChannelDO> getChannelPage(PayChannelPageReqVO pageReqVO) {
return channelMapper.selectPage(pageReqVO);
}
@Override
public List<PayChannelDO> getChannelList(PayChannelExportReqVO exportReqVO) {
return channelMapper.selectList(exportReqVO);
}
/**
* 根据支付应用ID集合获得支付渠道列表
*
* @param appIds 应用编号集合
* @return 支付渠道列表
*/
@Override
public List<PayChannelDO> getChannelListByAppIds(Collection<Long> appIds) {
return channelMapper.getChannelListByAppIds(appIds);
}
/**
* 根据条件获取渠道数量
*
* @param merchantId 商户编号
* @param appid 应用编号
* @param code 渠道编码
* @return 数量
*/
@Override
public Integer getChannelCountByConditions(Long merchantId, Long appid, String code) {
return this.channelMapper.selectCount(merchantId, appid, code);
}
/**
* 根据条件获取渠道
*
* @param merchantId 商户编号
* @param appid 应用编号
* @param code 渠道编码
* @return 数量
*/
@Override
public PayChannelDO getChannelByConditions(Long merchantId, Long appid, String code) {
return this.channelMapper.selectOne(merchantId, appid, code);
}
/**
* 设置渠道配置以及参数校验
*
* @param channel 渠道
* @param configStr 配置
*/
private void settingConfigAndCheckParam(PayChannelDO channel, String configStr) {
// 得到这个渠道是微信的还是支付宝的
Class<? extends PayClientConfig> payClass = PayChannelEnum.getByCode(channel.getCode()).getConfigClass();
if (ObjectUtil.isNull(payClass)) {
throw exception(CHANNEL_NOT_EXISTS);
}
PayClientConfig config = JSONUtil.toBean(configStr, payClass);
// 验证参数
config.validate(validator);
channel.setConfig(config);
}
@Override
public PayChannelDO validPayChannel(Long id) {
PayChannelDO channel = channelMapper.selectById(id);
this.validPayChannel(channel);
return channel;
}
@Override
public PayChannelDO validPayChannel(Long appId, String code) {
PayChannelDO channel = channelMapper.selectByAppIdAndCode(appId, code);
this.validPayChannel(channel);
return channel;
}
private void validPayChannel(PayChannelDO channel) {
if (channel == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_CHANNEL_NOT_FOUND);
}
if (CommonStatusEnum.DISABLE.getStatus().equals(channel.getStatus())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_CHANNEL_IS_DISABLE);
}
}
}

View File

@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.pay.service.merchant;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantUpdateReqVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 支付商户信息 Service 接口
*
* @author aquan
*/
public interface PayMerchantService {
/**
* 创建支付商户信息
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createMerchant(@Valid PayMerchantCreateReqVO createReqVO);
/**
* 更新支付商户信息
*
* @param updateReqVO 更新信息
*/
void updateMerchant(@Valid PayMerchantUpdateReqVO updateReqVO);
/**
* 删除支付商户信息
*
* @param id 编号
*/
void deleteMerchant(Long id);
/**
* 获得支付商户信息
*
* @param id 编号
* @return 支付商户信息
*/
PayMerchantDO getMerchant(Long id);
/**
* 获得支付商户信息列表
*
* @param ids 编号
* @return 支付商户信息列表
*/
List<PayMerchantDO> getMerchantList(Collection<Long> ids);
/**
* 获得支付商户信息分页
*
* @param pageReqVO 分页查询
* @return 支付商户信息分页
*/
PageResult<PayMerchantDO> getMerchantPage(PayMerchantPageReqVO pageReqVO);
/**
* 获得支付商户信息列表, 用于 Excel 导出
*
* @param exportReqVO 查询条件
* @return 支付商户信息列表
*/
List<PayMerchantDO> getMerchantList(PayMerchantExportReqVO exportReqVO);
/**
* 修改商户状态
*
* @param id 商户编号
* @param status 状态
*/
void updateMerchantStatus(Long id, Integer status);
/**
* 根据商户名称模糊查询商户集合
*
* @param merchantName 商户名称
* @return 商户集合
*/
List<PayMerchantDO> getMerchantListByName(String merchantName);
/**
* 获得指定编号的商户 Map
*
* @param merchantIds 商户编号数组
* @return 商户 Map
*/
default Map<Long, PayMerchantDO> getMerchantMap(Collection<Long> merchantIds) {
List<PayMerchantDO> list = this.getMerchantList(merchantIds);
return CollectionUtils.convertMap(list, PayMerchantDO::getId);
}
}

View File

@@ -0,0 +1,148 @@
package cn.iocoder.yudao.module.pay.service.merchant;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.merchant.PayMerchantUpdateReqVO;
import cn.iocoder.yudao.module.pay.convert.merchant.PayMerchantConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.module.pay.dal.mysql.merchant.PayAppMapper;
import cn.iocoder.yudao.module.pay.dal.mysql.merchant.PayMerchantMapper;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import com.google.common.annotations.VisibleForTesting;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
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 PayMerchantServiceImpl implements PayMerchantService {
@Resource
private PayMerchantMapper merchantMapper;
@Resource
private PayAppMapper appMapper;
@Override
public Long createMerchant(PayMerchantCreateReqVO createReqVO) {
// 插入
PayMerchantDO merchant = PayMerchantConvert.INSTANCE.convert(createReqVO);
merchant.setNo(this.generateMerchantNo());
merchantMapper.insert(merchant);
// 返回
return merchant.getId();
}
@Override
public void updateMerchant(PayMerchantUpdateReqVO updateReqVO) {
// 校验存在
this.validateMerchantExists(updateReqVO.getId());
// 更新
PayMerchantDO updateObj = PayMerchantConvert.INSTANCE.convert(updateReqVO);
merchantMapper.updateById(updateObj);
}
@Override
public void deleteMerchant(Long id) {
// 校验
this.validateMerchantExists(id);
this.validateAppExists(id);
// 删除
merchantMapper.deleteById(id);
}
@Override
public PayMerchantDO getMerchant(Long id) {
return merchantMapper.selectById(id);
}
@Override
public List<PayMerchantDO> getMerchantList(Collection<Long> ids) {
return merchantMapper.selectBatchIds(ids);
}
@Override
public PageResult<PayMerchantDO> getMerchantPage(PayMerchantPageReqVO pageReqVO) {
return merchantMapper.selectPage(pageReqVO);
}
@Override
public List<PayMerchantDO> getMerchantList(PayMerchantExportReqVO exportReqVO) {
return merchantMapper.selectList(exportReqVO);
}
@Override
public void updateMerchantStatus(Long id, Integer status) {
// 校验商户存在
this.checkMerchantExists(id);
// 更新状态
PayMerchantDO merchant = new PayMerchantDO();
merchant.setId(id);
merchant.setStatus(status);
merchantMapper.updateById(merchant);
}
@Override
public List<PayMerchantDO> getMerchantListByName(String merchantName) {
return this.merchantMapper.getMerchantListByName(merchantName);
}
@VisibleForTesting
public void checkMerchantExists(Long id) {
if (id == null) {
return;
}
PayMerchantDO merchant = merchantMapper.selectById(id);
if (merchant == null) {
throw exception(PAY_MERCHANT_NOT_EXISTS);
}
}
/**
* 校验商户是否存在
*
* @param id 商户 ID
*/
private void validateMerchantExists(Long id) {
if (ObjectUtil.isNull(merchantMapper.selectById(id))) {
throw exception(PAY_MERCHANT_NOT_EXISTS);
}
}
/**
* 校验商户是否还存在支付应用
*
* @param id 商户ID
*/
private void validateAppExists(Long id) {
if (appMapper.selectCount(id) > 0) {
throw exception(PAY_MERCHANT_EXIST_APP_CANT_DELETE);
}
}
// TODO @芋艿:后续增加下合适的算法
/**
* 根据年月日时分秒毫秒生成商户号
*
* @return 商户号
*/
private String generateMerchantNo() {
return "M" + DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmssSSS");
}
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.pay.service.notify;
import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import javax.validation.Valid;
/**
* 支付通知 Service 接口
*
* @author 芋道源码
*/
public interface PayNotifyService {
/**
* 创建支付通知任务
*
* @param reqDTO 任务信息
*/
void createPayNotifyTask(@Valid PayNotifyTaskCreateReqDTO reqDTO);
/**
* 执行支付通知
*
* 注意,该方法提供给定时任务调用。目前是 yudao-admin-server 进行调用
* @return 通知数量
*/
int executeNotify() throws InterruptedException;
}

View File

@@ -0,0 +1,258 @@
package cn.iocoder.yudao.module.pay.service.notify;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyLogCoreMapper;
import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskCoreMapper;
import cn.iocoder.yudao.module.pay.dal.redis.notify.PayNotifyLockRedisDAO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.module.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import cn.iocoder.yudao.module.pay.service.notify.vo.PayNotifyOrderReqVO;
import cn.iocoder.yudao.module.pay.service.notify.vo.PayRefundOrderReqVO;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 支付通知 Core Service 实现类
*
* @author 芋道源码
*/
@Service
@Valid
@Slf4j
public class PayNotifyServiceImpl implements PayNotifyService {
/**
* 通知超时时间,单位:秒
*/
public static final int NOTIFY_TIMEOUT = 120;
/**
* {@link #NOTIFY_TIMEOUT} 的毫秒
*/
public static final long NOTIFY_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS;
@Resource
@Lazy // 循环依赖,避免报错
private PayOrderService orderService;
@Resource
@Lazy // 循环依赖,避免报错
private PayRefundService refundService;
@Resource
private PayNotifyTaskCoreMapper payNotifyTaskCoreMapper;
@Resource
private PayNotifyLogCoreMapper payNotifyLogCoreMapper;
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor; // TODO 芋艿:未来提供独立的线程池
@Resource
private PayNotifyLockRedisDAO payNotifyLockCoreRedisDAO;
@Resource
@Lazy // 循环依赖(自己依赖自己),避免报错
private PayNotifyServiceImpl self;
@Override
public void createPayNotifyTask(PayNotifyTaskCreateReqDTO reqDTO) {
PayNotifyTaskDO task = new PayNotifyTaskDO();
task.setType(reqDTO.getType()).setDataId(reqDTO.getDataId());
task.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setNextNotifyTime(new Date())
.setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1);
// 补充 merchantId + appId + notifyUrl 字段
if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
PayOrderDO order = orderService.getOrder(task.getDataId()); // 不进行非空判断,有问题直接异常
task.setMerchantId(order.getMerchantId()).setAppId(order.getAppId()).
setMerchantOrderId(order.getMerchantOrderId()).setNotifyUrl(order.getNotifyUrl());
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
PayRefundDO refundDO = refundService.getRefund(task.getDataId());
task.setMerchantId(refundDO.getMerchantId()).setAppId(refundDO.getAppId())
.setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl());
}
// 执行插入
payNotifyTaskCoreMapper.insert(task);
// 异步直接发起任务。虽然会有定时任务扫描,但是会导致延迟
self.executeNotifyAsync(task);
}
@Override
public int executeNotify() throws InterruptedException {
// 获得需要通知的任务
List<PayNotifyTaskDO> tasks = payNotifyTaskCoreMapper.selectListByNotify();
if (CollUtil.isEmpty(tasks)) {
return 0;
}
// 遍历,逐个通知
CountDownLatch latch = new CountDownLatch(tasks.size());
tasks.forEach(task -> threadPoolTaskExecutor.execute(() -> {
try {
executeNotifySync(task);
} finally {
latch.countDown();
}
}));
// 等待完成
this.awaitExecuteNotify(latch);
// 返回执行完成的任务数(成功 + 失败)
return tasks.size();
}
/**
* 等待全部支付通知的完成
* 每 1 秒会打印一次剩余任务数量
*
* @param latch Latch
* @throws InterruptedException 如果被打断
*/
private void awaitExecuteNotify(CountDownLatch latch) throws InterruptedException {
long size = latch.getCount();
for (int i = 0; i < NOTIFY_TIMEOUT; i++) {
if (latch.await(1L, TimeUnit.SECONDS)) {
return;
}
log.info("[awaitExecuteNotify][任务处理中, 总任务数({}) 剩余任务数({})]", size, latch.getCount());
}
log.error("[awaitExecuteNotify][任务未处理完,总任务数({}) 剩余任务数({})]", size, latch.getCount());
}
/**
* 异步执行单个支付通知
*
* @param task 通知任务
*/
@Async
public void executeNotifyAsync(PayNotifyTaskDO task) {
self.executeNotifySync(task); // 使用 self避免事务不发起
}
/**
* 同步执行单个支付通知
*
* @param task 通知任务
*/
public void executeNotifySync(PayNotifyTaskDO task) {
// 分布式锁,避免并发问题
payNotifyLockCoreRedisDAO.lock(task.getId(), NOTIFY_TIMEOUT_MILLIS, () -> {
// 校验,当前任务是否已经被通知过
// 虽然已经通过分布式加锁,但是可能同时满足通知的条件,然后都去获得锁。此时,第一个执行完后,第二个还是能拿到锁,然后会再执行一次。
PayNotifyTaskDO dbTask = payNotifyTaskCoreMapper.selectById(task.getId());
if (DateUtils.afterNow(dbTask.getNextNotifyTime())) {
log.info("[executeNotify][dbTask({}) 任务被忽略,原因是未到达下次通知时间,可能是因为并发执行了]", JsonUtils.toJsonString(dbTask));
return;
}
// 执行通知
executeNotify(dbTask);
});
}
@Transactional
public void executeNotify(PayNotifyTaskDO task) {
// 发起回调
CommonResult<?> invokeResult = null;
Throwable invokeException = null;
try {
invokeResult = executeNotifyInvoke(task);
} catch (Throwable e) {
invokeException = e;
}
// 处理
Integer newStatus = this.processNotifyResult(task, invokeResult, invokeException);
// 记录 PayNotifyLog 日志
String response = invokeException != null ? ExceptionUtil.getRootCauseMessage(invokeException) : JsonUtils.toJsonString(invokeResult);
payNotifyLogCoreMapper.insert(PayNotifyLogDO.builder().taskId(task.getId())
.notifyTimes(task.getNotifyTimes() + 1).status(newStatus).response(response).build());
}
/**
* 执行单个支付任务的 HTTP 调用
*
* @param task 通知任务
* @return HTTP 响应
*/
private CommonResult<?> executeNotifyInvoke(PayNotifyTaskDO task) {
// 拼接参数
Object request;
if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
request = PayNotifyOrderReqVO.builder().merchantOrderId(task.getMerchantOrderId())
.payOrderId(task.getDataId()).build();
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
request = PayRefundOrderReqVO.builder().merchantOrderId(task.getMerchantOrderId())
.payRefundId(task.getDataId()).build();
} else {
throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task));
}
// 请求地址
String response = HttpUtil.post(task.getNotifyUrl(), JsonUtils.toJsonString(request),
(int) NOTIFY_TIMEOUT_MILLIS);
// 解析结果
return JsonUtils.parseObject(response, CommonResult.class);
}
/**
* 处理并更新通知结果
*
* @param task 通知任务
* @param invokeResult 通知结果
* @param invokeException 通知异常
* @return 最终任务的状态
*/
private Integer processNotifyResult(PayNotifyTaskDO task, CommonResult<?> invokeResult, Throwable invokeException) {
// 设置通用的更新 PayNotifyTaskDO 的字段
PayNotifyTaskDO updateTask = new PayNotifyTaskDO()
.setId(task.getId())
.setLastExecuteTime(new Date())
.setNotifyTimes(task.getNotifyTimes() + 1);
// 情况一:调用成功
if (invokeResult != null && invokeResult.isSuccess()) {
updateTask.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus());
return updateTask.getStatus();
}
// 情况二:调用失败、调用异常
// 2.1 超过最大回调次数
if (updateTask.getNotifyTimes() >= PayNotifyTaskDO.NOTIFY_FREQUENCY.length) {
updateTask.setStatus(PayNotifyStatusEnum.FAILURE.getStatus());
return updateTask.getStatus();
}
// 2.2 未超过最大回调次数
updateTask.setNextNotifyTime(DateUtils.addDate(Calendar.SECOND, PayNotifyTaskDO.NOTIFY_FREQUENCY[updateTask.getNotifyTimes()]));
updateTask.setStatus(invokeException != null ? PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()
: PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus());
return updateTask.getStatus();
}
private void processNotifySuccess(PayNotifyTaskDO task, PayNotifyTaskDO updateTask) {
payNotifyTaskCoreMapper.updateById(updateTask);
}
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.pay.service.notify.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
/**
* 支付通知创建 DTO
*
* @author 芋道源码
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayNotifyTaskCreateReqDTO {
/**
* 类型
*/
@NotNull(message = "类型不能为空")
private Integer type;
/**
* 数据编号
*/
@NotNull(message = "数据编号不能为空")
private Long dataId;
}

View File

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

View File

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

View File

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

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