Commit e40c0e3057c4e89b42a93cebdf4e39e107aa2ab8

Authored by YunaiV
2 parents 7f75f0ab c7a21b2a

Merge branch 'feature/mall_product' of https://gitee.com/zhijiantianya/ruoyi-vue-pro

# Conflicts:
#	pom.xml
#	sql/mysql/pay_wallet.sql
#	yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
Showing 50 changed files with 718 additions and 158 deletions
sql/mysql/pay_wallet.sql
... ... @@ -246,3 +246,11 @@ VALUES (
246 246 '转账订单', '', 2, 3, 1117,
247 247 'transfer', 'ep:credit-card', 'pay/transfer/index', 0, 'PayTransfer'
248 248 );
  249 +
  250 +-- 转账通知脚本
  251 +
  252 +ALTER TABLE `pay_app`
  253 + ADD COLUMN `transfer_notify_url` varchar(1024) NOT NULL COMMENT '转账结果的回调地址' AFTER `refund_notify_url`;
  254 +ALTER TABLE `pay_notify_task`
  255 + MODIFY COLUMN `merchant_order_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '商户订单编号' AFTER `status`,
  256 + ADD COLUMN `merchant_transfer_id` varchar(64) COMMENT '商户转账单编号' AFTER `merchant_order_id`;
249 257 \ No newline at end of file
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
... ... @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
6 6 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
7 7 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
8 8 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
  9 +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
9 10  
10 11 import java.util.Map;
11 12  
... ... @@ -86,4 +87,12 @@ public interface PayClient {
86 87 */
87 88 PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
88 89  
  90 + /**
  91 + * 获得转账订单信息
  92 + *
  93 + * @param outTradeNo 外部订单号
  94 + * @param type 转账类型
  95 + * @return 转账信息
  96 + */
  97 + PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
89 98 }
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
... ... @@ -53,11 +53,24 @@ public class PayTransferRespDTO {
53 53 /**
54 54 * 创建【WAITING】状态的转账返回
55 55 */
56   - public static PayTransferRespDTO waitingOf(String channelOrderNo,
  56 + public static PayTransferRespDTO waitingOf(String channelTransferNo,
57 57 String outTransferNo, Object rawData) {
58 58 PayTransferRespDTO respDTO = new PayTransferRespDTO();
59 59 respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
60   - respDTO.channelTransferNo = channelOrderNo;
  60 + respDTO.channelTransferNo = channelTransferNo;
  61 + respDTO.outTransferNo = outTransferNo;
  62 + respDTO.rawData = rawData;
  63 + return respDTO;
  64 + }
  65 +
  66 + /**
  67 + * 创建【IN_PROGRESS】状态的转账返回
  68 + */
  69 + public static PayTransferRespDTO dealingOf(String channelTransferNo,
  70 + String outTransferNo, Object rawData) {
  71 + PayTransferRespDTO respDTO = new PayTransferRespDTO();
  72 + respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
  73 + respDTO.channelTransferNo = channelTransferNo;
61 74 respDTO.outTransferNo = outTransferNo;
62 75 respDTO.rawData = rawData;
63 76 return respDTO;
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
... ... @@ -188,11 +188,11 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
188 188  
189 189 @Override
190 190 public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
  191 + validatePayTransferReqDTO(reqDTO);
191 192 PayTransferRespDTO resp;
192   - try{
193   - validatePayTransferReqDTO(reqDTO);
  193 + try {
194 194 resp = doUnifiedTransfer(reqDTO);
195   - }catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
  195 + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
196 196 throw ex;
197 197 } catch (Throwable ex) {
198 198 // 系统异常,则包装成 PayException 异常抛出
... ... @@ -219,9 +219,25 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
219 219 }
220 220 }
221 221  
  222 + @Override
  223 + public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
  224 + try {
  225 + return doGetTransfer(outTradeNo, type);
  226 + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
  227 + throw ex;
  228 + } catch (Throwable ex) {
  229 + log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
  230 + getId(), outTradeNo, type, ex);
  231 + throw buildPayException(ex);
  232 + }
  233 + }
  234 +
222 235 protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
223 236 throws Throwable;
224 237  
  238 + protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
  239 + throws Throwable;
  240 +
225 241 // ========== 各种工具方法 ==========
226 242  
227 243 private PayException buildPayException(Throwable ex) {
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
... ... @@ -23,14 +23,8 @@ import com.alipay.api.AlipayResponse;
23 23 import com.alipay.api.DefaultAlipayClient;
24 24 import com.alipay.api.domain.*;
25 25 import com.alipay.api.internal.util.AlipaySignature;
26   -import com.alipay.api.request.AlipayFundTransUniTransferRequest;
27   -import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
28   -import com.alipay.api.request.AlipayTradeQueryRequest;
29   -import com.alipay.api.request.AlipayTradeRefundRequest;
30   -import com.alipay.api.response.AlipayFundTransUniTransferResponse;
31   -import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
32   -import com.alipay.api.response.AlipayTradeQueryResponse;
33   -import com.alipay.api.response.AlipayTradeRefundResponse;
  26 +import com.alipay.api.request.*;
  27 +import com.alipay.api.response.*;
34 28 import lombok.Getter;
35 29 import lombok.SneakyThrows;
36 30 import lombok.extern.slf4j.Slf4j;
... ... @@ -126,7 +120,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
126 120 }
127 121 // 2.2 解析订单的状态
128 122 Integer status = parseStatus(response.getTradeStatus());
129   - Assert.notNull(status, () -> {
  123 + Assert.notNull(status, () -> {
130 124 throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
131 125 });
132 126 return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
... ... @@ -228,7 +222,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
228 222 protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
229 223 // 1.1 校验公钥类型 必须使用公钥证书模式
230 224 if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
231   - throw exception0(ERROR_CONFIGURATION.getCode(),"支付宝单笔转账必须使用公钥证书模式");
  225 + throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式");
232 226 }
233 227 // 1.2 构建 AlipayFundTransUniTransferModel
234 228 AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
... ... @@ -238,44 +232,96 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
238 232 model.setOutBizNo(reqDTO.getOutTransferNo());
239 233 model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
240 234 model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
241   - model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
  235 + if (reqDTO.getChannelExtras() != null) {
  236 + model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
  237 + }
  238 + // ② 个性化的参数
  239 + Participant payeeInfo = new Participant();
242 240 PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
243 241 switch (transferType) {
244 242 // TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦?
245 243 // @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试??
246 244 case ALIPAY_BALANCE: {
247   - // ② 个性化的参数
248   - Participant payeeInfo = new Participant();
249 245 payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
250 246 payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
251 247 payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
252 248 model.setPayeeInfo(payeeInfo);
253   - // 1.3 构建 AlipayFundTransUniTransferRequest
254   - AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
255   - request.setBizModel(model);
256   - // 执行请求
257   - AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
258   - // 处理结果
259   - if (!response.isSuccess()) {
260   - // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
261   - if (ObjectUtils.equalsAny(response.getSubCode(), "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
262   - return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
263   - }
264   - return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
265   - reqDTO.getOutTransferNo(), response);
266   - }
267   - return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
268   - response.getOutBizNo(), response);
  249 + break;
269 250 }
270 251 case BANK_CARD: {
271   - Participant payeeInfo = new Participant();
272 252 payeeInfo.setIdentityType("BANKCARD_ACCOUNT");
273 253 // TODO 待实现
274 254 throw exception(NOT_IMPLEMENTED);
275 255 }
276 256 default: {
277   - throw exception0(BAD_REQUEST.getCode(),"不正确的转账类型: {}",transferType);
  257 + throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType);
  258 + }
  259 + }
  260 + // 1.3 构建 AlipayFundTransUniTransferRequest
  261 + AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
  262 + request.setBizModel(model);
  263 + // 执行请求
  264 + AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
  265 + // 处理结果
  266 + if (!response.isSuccess()) {
  267 + // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账
  268 + // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
  269 + if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
  270 + return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
  271 + }
  272 + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
  273 + reqDTO.getOutTransferNo(), response);
  274 + } else {
  275 + if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
  276 + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
  277 + reqDTO.getOutTransferNo(), response);
  278 + }
  279 + if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
  280 + return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
  281 + }
  282 + return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
  283 + response.getOutBizNo(), response);
  284 + }
  285 +
  286 + }
  287 +
  288 + @Override
  289 + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable {
  290 + // 1.1 构建 AlipayFundTransCommonQueryModel
  291 + AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
  292 + model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD");
  293 + model.setBizScene("DIRECT_TRANSFER"); //业务场景
  294 + model.setOutBizNo(outTradeNo);
  295 + // 1.2 构建 AlipayFundTransCommonQueryRequest
  296 + AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
  297 + request.setBizModel(model);
  298 +
  299 + // 2.1 执行请求
  300 + AlipayFundTransCommonQueryResponse response;
  301 + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
  302 + response = client.certificateExecute(request);
  303 + } else {
  304 + response = client.execute(request);
  305 + }
  306 + // 2.2 处理返回结果
  307 + if (response.isSuccess()) {
  308 + if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
  309 + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
  310 + outTradeNo, response);
  311 + }
  312 + if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
  313 + return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response);
278 314 }
  315 + return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
  316 + response.getOutBizNo(), response);
  317 + } else {
  318 + // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
  319 + // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
  320 + if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
  321 + return PayTransferRespDTO.waitingOf(null, outTradeNo, response);
  322 + }
  323 + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
  324 + outTradeNo, response);
279 325 }
280 326 }
281 327  
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
... ... @@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifie
9 9 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
10 10 import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
11 11 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
  12 +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
12 13  
13 14 import java.time.LocalDateTime;
14 15 import java.util.Map;
... ... @@ -71,4 +72,9 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
71 72 throw new UnsupportedOperationException("待实现");
72 73 }
73 74  
  75 + @Override
  76 + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
  77 + throw new UnsupportedOperationException("待实现");
  78 + }
  79 +
74 80 }
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
... ... @@ -16,6 +16,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDT
16 16 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
17 17 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
18 18 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
  19 +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
19 20 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
20 21 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
21 22 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
... ... @@ -431,6 +432,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
431 432 protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
432 433 throw new UnsupportedOperationException("待实现");
433 434 }
  435 +
  436 + @Override
  437 + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
  438 + throw new UnsupportedOperationException("待实现");
  439 + }
  440 +
434 441 // ========== 各种工具方法 ==========
435 442  
436 443 static String formatDateV2(LocalDateTime time) {
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
... ... @@ -38,4 +38,8 @@ public enum PayTransferStatusRespEnum {
38 38 public static boolean isClosed(Integer status) {
39 39 return Objects.equals(status, CLOSED.getStatus());
40 40 }
  41 +
  42 + public static boolean isInProgress(Integer status) {
  43 + return Objects.equals(status, IN_PROGRESS.getStatus());
  44 + }
41 45 }
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
... ... @@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
12 12 import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
13 13 import com.baomidou.mybatisplus.extension.toolkit.Db;
14 14 import com.github.yulichang.base.MPJBaseMapper;
  15 +import com.github.yulichang.interfaces.MPJBaseJoin;
15 16 import org.apache.ibatis.annotations.Param;
16 17  
17 18 import java.util.Collection;
... ... @@ -39,6 +40,13 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
39 40 return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
40 41 }
41 42  
  43 + default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
  44 + IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
  45 + selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
  46 + // 转换返回
  47 + return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
  48 + }
  49 +
42 50 default T selectOne(String field, Object value) {
43 51 return selectOne(new QueryWrapper<T>().eq(field, value));
44 52 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.admin.favorite;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
  7 +import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
  8 +import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
  9 +import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
  10 +import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
  11 +import cn.iocoder.yudao.module.product.service.favorite.ProductFavoriteService;
  12 +import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
  13 +import io.swagger.v3.oas.annotations.Operation;
  14 +import io.swagger.v3.oas.annotations.tags.Tag;
  15 +import org.springframework.security.access.prepost.PreAuthorize;
  16 +import org.springframework.validation.annotation.Validated;
  17 +import org.springframework.web.bind.annotation.GetMapping;
  18 +import org.springframework.web.bind.annotation.RequestMapping;
  19 +import org.springframework.web.bind.annotation.RestController;
  20 +
  21 +import javax.annotation.Resource;
  22 +import javax.validation.Valid;
  23 +import java.util.List;
  24 +
  25 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  26 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  27 +
  28 +@Tag(name = "管理后台 - 商品收藏")
  29 +@RestController
  30 +@RequestMapping("/product/favorite")
  31 +@Validated
  32 +public class ProductFavoriteController {
  33 +
  34 + @Resource
  35 + private ProductFavoriteService productFavoriteService;
  36 +
  37 + @Resource
  38 + private ProductSpuService productSpuService;
  39 +
  40 + @GetMapping("/page")
  41 + @Operation(summary = "获得商品收藏分页")
  42 + @PreAuthorize("@ss.hasPermission('product:favorite:query')")
  43 + public CommonResult<PageResult<ProductFavoriteRespVO>> getFavoritePage(@Valid ProductFavoritePageReqVO pageVO) {
  44 + PageResult<ProductFavoriteDO> pageResult = productFavoriteService.getFavoritePage(pageVO);
  45 + if (CollUtil.isEmpty(pageResult.getList())) {
  46 + return success(PageResult.empty());
  47 + }
  48 + // 拼接数据
  49 + List<ProductSpuDO> spuList = productSpuService.getSpuList(convertSet(pageResult.getList(), ProductFavoriteDO::getSpuId));
  50 + return success(ProductFavoriteConvert.INSTANCE.convertPage(pageResult, spuList));
  51 + }
  52 +
  53 +}
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import javax.validation.constraints.NotNull;
  7 +
  8 +/**
  9 + * 商品收藏 Base VO,提供给添加、修改、详细的子 VO 使用
  10 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
  11 + */
  12 +@Data
  13 +public class ProductFavoriteBaseVO {
  14 +
  15 + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5036")
  16 + @NotNull(message = "用户编号不能为空")
  17 + private Long userId;
  18 +
  19 +}
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +@Schema(description = "管理后台 - 商品收藏分页 Request VO")
  10 +@Data
  11 +@EqualsAndHashCode(callSuper = true)
  12 +@ToString(callSuper = true)
  13 +public class ProductFavoritePageReqVO extends PageParam {
  14 +
  15 + @Schema(description = "用户编号", example = "5036")
  16 + private Long userId;
  17 +
  18 +}
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +import lombok.ToString;
  6 +
  7 +import javax.validation.constraints.NotNull;
  8 +
  9 +@Schema(description = "管理后台 - 商品收藏的单个 Response VO")
  10 +@Data
  11 +@ToString(callSuper = true)
  12 +public class ProductFavoriteReqVO extends ProductFavoriteBaseVO {
  13 +
  14 + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32734")
  15 + @NotNull(message = "商品 SPU 编号不能为空")
  16 + private Long spuId;
  17 +}
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
  2 +
  3 +import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuRespVO;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.ToString;
  7 +
  8 +@Schema(description = "管理后台 - 商品收藏 Response VO")
  9 +@Data
  10 +@ToString(callSuper = true)
  11 +public class ProductFavoriteRespVO extends ProductSpuRespVO {
  12 +
  13 + @Schema(description = "userId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
  14 + private Long userId;
  15 +
  16 + @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
  17 + private Long spuId;
  18 +
  19 +}
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java
... ... @@ -12,19 +12,7 @@ import java.util.List;
12 12 @Data
13 13 @EqualsAndHashCode(callSuper = true)
14 14 @ToString(callSuper = true)
15   -public class ProductSpuDetailRespVO extends ProductSpuBaseVO {
16   -
17   - @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1212")
18   - private Long id;
19   -
20   - @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
21   - private Integer salesCount;
22   -
23   - @Schema(description = "浏览量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20000")
24   - private Integer browseCount;
25   -
26   - @Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
27   - private Integer status;
  15 +public class ProductSpuDetailRespVO extends ProductSpuRespVO {
28 16  
29 17 // ========== SKU 相关字段 =========
30 18  
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
... ... @@ -13,7 +13,7 @@ import java.time.LocalDateTime;
13 13 @ToString(callSuper = true)
14 14 public class ProductSpuRespVO extends ProductSpuBaseVO {
15 15  
16   - @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
  16 + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
17 17 private Long id;
18 18  
19 19 @Schema(description = "商品价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "1999")
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
... ... @@ -30,6 +30,7 @@ import javax.annotation.Resource;
30 30 import javax.validation.Valid;
31 31 import java.util.Collections;
32 32 import java.util.List;
  33 +import java.util.Set;
33 34  
34 35 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
35 36 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
... ... @@ -75,6 +76,25 @@ public class AppProductSpuController {
75 76 return success(voList);
76 77 }
77 78  
  79 + @GetMapping("/list-by-ids")
  80 + @Operation(summary = "获得商品 SPU 列表")
  81 + @Parameters({
  82 + @Parameter(name = "ids", description = "编号列表", required = true)
  83 + })
  84 + public CommonResult<List<AppProductSpuPageRespVO>> getSpuList(@RequestParam("ids") Set<Long> ids) {
  85 + List<ProductSpuDO> list = productSpuService.getSpuList(ids);
  86 + if (CollUtil.isEmpty(list)) {
  87 + return success(Collections.emptyList());
  88 + }
  89 +
  90 + // 拼接返回
  91 + List<AppProductSpuPageRespVO> voList = ProductSpuConvert.INSTANCE.convertListForGetSpuList(list);
  92 + // 处理 vip 价格
  93 + MemberLevelRespDTO memberLevel = getMemberLevel();
  94 + voList.forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
  95 + return success(voList);
  96 + }
  97 +
78 98 @GetMapping("/page")
79 99 @Operation(summary = "获得商品 SPU 分页")
80 100 public CommonResult<PageResult<AppProductSpuPageRespVO>> getSpuPage(@Valid AppProductSpuPageReqVO pageVO) {
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java
... ... @@ -15,6 +15,9 @@ public class AppProductSpuPageRespVO {
15 15 @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
16 16 private String name;
17 17  
  18 + @Schema(description = "商品简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉小短袖简介")
  19 + private String introduction;
  20 +
18 21 @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED)
19 22 private Long categoryId;
20 23  
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
1 1 package cn.iocoder.yudao.module.product.convert.favorite;
2 2  
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  5 +import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
3 6 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoriteRespVO;
4 7 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
5 8 import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
... ... @@ -34,4 +37,18 @@ public interface ProductFavoriteConvert {
34 37 return resultList;
35 38 }
36 39  
  40 + default PageResult<ProductFavoriteRespVO> convertPage(PageResult<ProductFavoriteDO> pageResult, List<ProductSpuDO> spuList) {
  41 + Map<Long, ProductSpuDO> spuMap = convertMap(spuList, ProductSpuDO::getId);
  42 + List<ProductFavoriteRespVO> voList = CollectionUtils.convertList(pageResult.getList(), favorite -> {
  43 + ProductSpuDO spu = spuMap.get(favorite.getSpuId());
  44 + return convert02(spu, favorite);
  45 + });
  46 + return new PageResult<>(voList, pageResult.getTotal());
  47 + }
  48 + @Mapping(target = "id", source = "favorite.id")
  49 + @Mapping(target = "userId", source = "favorite.userId")
  50 + @Mapping(target = "spuId", source = "favorite.spuId")
  51 + @Mapping(target = "createTime", source = "favorite.createTime")
  52 + ProductFavoriteRespVO convert02(ProductSpuDO spu, ProductFavoriteDO favorite);
  53 +
37 54 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
... ... @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.product.dal.mysql.favorite;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
5 7 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
6 8 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
7 9 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
... ... @@ -21,6 +23,12 @@ public interface ProductFavoriteMapper extends BaseMapperX&lt;ProductFavoriteDO&gt; {
21 23 .orderByDesc(ProductFavoriteDO::getId));
22 24 }
23 25  
  26 + default PageResult<ProductFavoriteDO> selectPageByUserId(ProductFavoritePageReqVO reqVO) {
  27 + return selectPage(reqVO, new LambdaQueryWrapperX<ProductFavoriteDO>()
  28 + .eqIfPresent(ProductFavoriteDO::getUserId, reqVO.getUserId())
  29 + .orderByDesc(ProductFavoriteDO::getId));
  30 + }
  31 +
24 32 default Long selectCountByUserId(Long userId) {
25 33 return selectCount(ProductFavoriteDO::getUserId, userId);
26 34 }
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
1 1 package cn.iocoder.yudao.module.product.service.favorite;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
4 5 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
5 6 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
6 7  
... ... @@ -38,6 +39,13 @@ public interface ProductFavoriteService {
38 39 PageResult<ProductFavoriteDO> getFavoritePage(Long userId, @Valid AppFavoritePageReqVO reqVO);
39 40  
40 41 /**
  42 + * 分页查询用户收藏列表
  43 + *
  44 + * @param reqVO 请求 vo
  45 + */
  46 + PageResult<ProductFavoriteDO> getFavoritePage(@Valid ProductFavoritePageReqVO reqVO);
  47 +
  48 + /**
41 49 * 获取收藏过商品
42 50 *
43 51 * @param userId 用户编号
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
1 1 package cn.iocoder.yudao.module.product.service.favorite;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
4 5 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
5 6 import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
6 7 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
... ... @@ -55,6 +56,11 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
55 56 }
56 57  
57 58 @Override
  59 + public PageResult<ProductFavoriteDO> getFavoritePage(@Valid ProductFavoritePageReqVO reqVO) {
  60 + return productFavoriteMapper.selectPageByUserId(reqVO);
  61 + }
  62 +
  63 + @Override
58 64 public ProductFavoriteDO getFavorite(Long userId, Long spuId) {
59 65 return productFavoriteMapper.selectByUserIdAndSpuId(userId, spuId);
60 66 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/diy/vo/template/DiyTemplateBaseVO.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.promotion.controller.admin.diy.vo.template;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.Data;
5 5  
6   -import javax.validation.constraints.NotNull;
  6 +import javax.validation.constraints.NotEmpty;
7 7 import java.util.List;
8 8  
9 9 /**
... ... @@ -14,7 +14,7 @@ import java.util.List;
14 14 public class DiyTemplateBaseVO {
15 15  
16 16 @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "默认主题")
17   - @NotNull(message = "模板名称不能为空")
  17 + @NotEmpty(message = "模板名称不能为空")
18 18 private String name;
19 19  
20 20 @Schema(description = "备注", example = "默认主题")
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyPageDO.java
... ... @@ -32,6 +32,8 @@ public class DiyPageDO extends BaseDO {
32 32 private Long id;
33 33 /**
34 34 * 装修模板编号
  35 + *
  36 + * 关联 {@link DiyTemplateDO#getId()}
35 37 */
36 38 private Long templateId;
37 39 /**
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyTemplateDO.java
... ... @@ -14,6 +14,9 @@ import java.util.List;
14 14 /**
15 15 * 装修模板 DO
16 16 *
  17 + * 1. 新建一个模版,下面可以包含多个 {@link DiyPageDO} 页面,例如说首页、我的
  18 + * 2. 如果需要使用某个模版,则将 {@link #used} 设置为 true,表示已使用,有且仅有一个
  19 + *
17 20 * @author owen
18 21 */
19 22 @TableName(value = "promotion_diy_template", autoResultMap = true)
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyPageServiceImpl.java
... ... @@ -42,7 +42,6 @@ public class DiyPageServiceImpl implements DiyPageService {
42 42 DiyPageDO diyPage = DiyPageConvert.INSTANCE.convert(createReqVO);
43 43 diyPage.setProperty("{}");
44 44 diyPageMapper.insert(diyPage);
45   - // 返回
46 45 return diyPage.getId();
47 46 }
48 47  
... ... @@ -57,6 +56,13 @@ public class DiyPageServiceImpl implements DiyPageService {
57 56 diyPageMapper.updateById(updateObj);
58 57 }
59 58  
  59 + /**
  60 + * 校验 Page 页面,在一个 template 模版下的名字是唯一的
  61 + *
  62 + * @param id Page 编号
  63 + * @param templateId 模版编号
  64 + * @param name Page 名字
  65 + */
60 66 void validateNameUnique(Long id, Long templateId, String name) {
61 67 if (templateId != null || StrUtil.isBlank(name)) {
62 68 return;
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImpl.java
... ... @@ -32,9 +32,11 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
32 32  
33 33 @Resource
34 34 private DiyTemplateMapper diyTemplateMapper;
  35 +
35 36 @Resource
36 37 private DiyPageService diyPageService;
37 38  
  39 + // TODO @疯狂:事务;
38 40 @Override
39 41 public Long createDiyTemplate(DiyTemplateCreateReqVO createReqVO) {
40 42 // 校验名称唯一
... ... @@ -120,9 +122,11 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
120 122 }
121 123  
122 124 @Override
  125 + // TODO @疯狂:事务;
123 126 public void useDiyTemplate(Long id) {
124 127 // 校验存在
125 128 validateDiyTemplateExists(id);
  129 + // TODO @疯狂:要不已使用的情况,抛个业务异常?
126 130 // 已使用的更新为未使用
127 131 DiyTemplateDO used = diyTemplateMapper.selectByUsed(true);
128 132 if (used != null) {
... ... @@ -136,8 +140,8 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
136 140 this.updateUsed(id, true, LocalDateTime.now());
137 141 }
138 142  
139   - @Transactional(rollbackFor = Exception.class)
140 143 @Override
  144 + @Transactional(rollbackFor = Exception.class)
141 145 public void updateDiyTemplateProperty(DiyTemplatePropertyUpdateRequestVO updateReqVO) {
142 146 // 校验存在
143 147 validateDiyTemplateExists(updateReqVO.getId());
... ... @@ -151,6 +155,7 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
151 155 return diyTemplateMapper.selectByUsed(true);
152 156 }
153 157  
  158 + // TODO @疯狂:挪到 useDiyTemplate 下面,改名 updateTemplateUsed 会不会好点哈;
154 159 /**
155 160 * 更新模板是否使用
156 161 *
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.pay.api.notify.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import javax.validation.constraints.NotEmpty;
  6 +import javax.validation.constraints.NotNull;
  7 +
  8 +/**
  9 + * 转账单的通知 Request DTO
  10 + *
  11 + * @author jason
  12 + */
  13 +@Data
  14 +public class PayTransferNotifyReqDTO {
  15 +
  16 + /**
  17 + * 商户转账单号
  18 + */
  19 + @NotEmpty(message = "商户转账单号不能为空")
  20 + private String merchantTransferId;
  21 +
  22 + /**
  23 + * 转账订单编号
  24 + */
  25 + @NotNull(message = "转账订单编号不能为空")
  26 + private Long payTransferId;
  27 +}
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
... ... @@ -65,12 +65,13 @@ public interface ErrorCodeConstants {
65 65  
66 66 // ========== 转账模块 1-007-009-000 ==========
67 67 ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_000, "发起转账报错,错误码:{},错误提示:{}");
68   - ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账交易单不存在");
69   - ErrorCode PAY_TRANSFER_STATUS_IS_SUCCESS = new ErrorCode(1_007_009_002, "转账单已成功转账");
70   - ErrorCode PAY_TRANSFER_EXISTS = new ErrorCode(1_007_009_003, "已经存在转账单");
71   - ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经存在,请查询转账订单相关状态");
  68 + ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在");
  69 + ErrorCode PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH = new ErrorCode(1_007_009_002, "两次相同转账请求的类型不匹配");
  70 + ErrorCode PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "两次相同转账请求的金额不匹配");
  71 + ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经发起,请查询转账订单相关状态");
72 72 ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_005, "转账单不处于待转账");
73 73 ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中");
  74 +
74 75 // ========== 示例订单 1-007-900-000 ==========
75 76 ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1_007_900_000, "示例订单不存在");
76 77 ErrorCode DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1_007_900_001, "示例订单更新支付状态失败,订单不是【未支付】状态");
... ... @@ -84,4 +85,8 @@ public interface ErrorCodeConstants {
84 85 ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1_007_900_009, "发起退款失败,退款单编号不匹配");
85 86 ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1_007_900_010, "发起退款失败,退款单金额不匹配");
86 87  
  88 + // ========== 示例转账订单 1-007-901-001 ==========
  89 + ErrorCode DEMO_TRANSFER_NOT_FOUND = new ErrorCode(1_007_901_001, "示例转账单不存在");
  90 + ErrorCode DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR = new ErrorCode(1_007_901_002, "转账失败,转账单编号不匹配");
  91 + ErrorCode DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH = new ErrorCode(1_007_901_003, "转账失败,转账单金额不匹配");
87 92 }
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java
... ... @@ -14,6 +14,7 @@ public enum PayNotifyTypeEnum {
14 14  
15 15 ORDER(1, "支付单"),
16 16 REFUND(2, "退款单"),
  17 + TRANSFER(3, "转账单")
17 18 ;
18 19  
19 20 /**
... ...
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java
... ... @@ -40,9 +40,13 @@ public enum PayTransferStatusEnum {
40 40 public static boolean isClosed(Integer status) {
41 41 return Objects.equals(status, CLOSED.getStatus());
42 42 }
  43 +
43 44 public static boolean isWaiting(Integer status) {
44 45 return Objects.equals(status, WAITING.getStatus());
45 46 }
  47 + public static boolean isInProgress(Integer status) {
  48 + return Objects.equals(status, IN_PROGRESS.getStatus());
  49 + }
46 50  
47 51 /**
48 52 * 是否处于待转账或者转账中的状态
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java
... ... @@ -6,8 +6,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
7 7 import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
8 8 import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
9   -import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
10   -import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO;
  9 +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
  10 +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
11 11 import cn.iocoder.yudao.module.pay.convert.demo.PayDemoOrderConvert;
12 12 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
13 13 import cn.iocoder.yudao.module.pay.service.demo.PayDemoOrderService;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoTransferController.java
... ... @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.demo;
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
  7 +import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
6 8 import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
7 9 import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferRespVO;
8 10 import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
... ... @@ -14,6 +16,7 @@ import org.springframework.validation.annotation.Validated;
14 16 import org.springframework.web.bind.annotation.*;
15 17  
16 18 import javax.annotation.Resource;
  19 +import javax.annotation.security.PermitAll;
17 20 import javax.validation.Valid;
18 21  
19 22 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
... ... @@ -33,9 +36,19 @@ public class PayDemoTransferController {
33 36 }
34 37  
35 38 @GetMapping("/page")
36   - @Operation(summary = "获得示例订单分页")
  39 + @Operation(summary = "获得示例转账订单分页")
37 40 public CommonResult<PageResult<PayDemoTransferRespVO>> getDemoTransferPage(@Valid PageParam pageVO) {
38 41 PageResult<PayDemoTransferDO> pageResult = demoTransferService.getDemoTransferPage(pageVO);
39 42 return success(PayDemoTransferConvert.INSTANCE.convertPage(pageResult));
40 43 }
  44 +
  45 + @PostMapping("/update-status")
  46 + @Operation(summary = "更新示例转账订单的转账状态") // 由 pay-module 转账服务,进行回调
  47 + @PermitAll // 无需登录,安全由 PayDemoTransferService 内部校验实现
  48 + @OperateLog(enable = false) // 禁用操作日志,因为没有操作人
  49 + public CommonResult<Boolean> updateDemoTransferStatus(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
  50 + demoTransferService.updateDemoTransferStatus(Long.valueOf(notifyReqDTO.getMerchantTransferId()),
  51 + notifyReqDTO.getPayTransferId());
  52 + return success(true);
  53 + }
41 54 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java renamed to yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java
1   -package cn.iocoder.yudao.module.pay.controller.admin.demo.vo;
  1 +package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order;
2 2  
3   -import lombok.*;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
5 5  
6   -import javax.validation.constraints.NotEmpty;
7 6 import javax.validation.constraints.NotNull;
8 7  
9 8 @Schema(description = "管理后台 - 示例订单创建 Request VO")
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java renamed to yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java
1   -package cn.iocoder.yudao.module.pay.controller.admin.demo.vo;
  1 +package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order;
2 2  
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.*;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java
1 1 package cn.iocoder.yudao.module.pay.convert.demo;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4   -import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
5   -import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO;
  4 +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
  5 +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
6 6 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
7 7 import org.mapstruct.Mapper;
8 8 import org.mapstruct.factory.Mappers;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
... ... @@ -18,7 +18,7 @@ public interface PayTransferConvert {
18 18  
19 19 PayTransferDO convert(PayTransferCreateReqDTO dto);
20 20  
21   - PayTransferUnifiedReqDTO convert2(PayTransferCreateReqDTO dto);
  21 + PayTransferUnifiedReqDTO convert2(PayTransferDO dto);
22 22  
23 23 PayTransferCreateReqDTO convert(PayTransferCreateReqVO vo);
24 24  
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
... ... @@ -54,4 +54,9 @@ public class PayAppDO extends BaseDO {
54 54 */
55 55 private String refundNotifyUrl;
56 56  
  57 + /**
  58 + * 转账结果的回调地址
  59 + */
  60 + private String transferNotifyUrl;
  61 +
57 62 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java
... ... @@ -67,6 +67,10 @@ public class PayNotifyTaskDO extends TenantBaseDO {
67 67 */
68 68 private String merchantOrderId;
69 69 /**
  70 + * 商户转账单编号
  71 + */
  72 + private String merchantTransferId;
  73 + /**
70 74 * 通知状态
71 75 *
72 76 * 外键 {@link PayNotifyStatusEnum}
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
1 1 package cn.iocoder.yudao.module.pay.dal.mysql.transfer;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 6 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
6 7 import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
7   -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
8 8 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
9 9 import org.apache.ibatis.annotations.Mapper;
10 10  
... ... @@ -40,6 +40,10 @@ public interface PayTransferMapper extends BaseMapperX&lt;PayTransferDO&gt; {
40 40 .betweenIfPresent(PayTransferDO::getCreateTime, reqVO.getCreateTime())
41 41 .orderByDesc(PayTransferDO::getId));
42 42 }
  43 +
  44 + default List<PayTransferDO> selectListByStatus(Integer status){
  45 + return selectList(PayTransferDO::getStatus, status);
  46 + }
43 47 }
44 48  
45 49  
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
... ... @@ -14,6 +14,7 @@ import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
14 14 import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
15 15 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
16 16 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
  17 +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
17 18 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
18 19 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
19 20 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
... ... @@ -181,4 +182,9 @@ public class WalletPayClient extends AbstractPayClient&lt;NonePayClientConfig&gt; {
181 182 throw new UnsupportedOperationException("待实现");
182 183 }
183 184  
  185 + @Override
  186 + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
  187 + throw new UnsupportedOperationException("待实现");
  188 + }
  189 +
184 190 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/transfer/PayTransferSyncJob.java 0 → 100644
  1 +package cn.iocoder.yudao.module.pay.job.transfer;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
  5 +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
  6 +import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import javax.annotation.Resource;
  10 +
  11 +/**
  12 + * 转账订单的同步 Job
  13 + *
  14 + * 由于转账订单的转账结果,有些渠道是异步通知进行同步的,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
  15 + *
  16 + * @author jason
  17 + */
  18 +@Component
  19 +public class PayTransferSyncJob implements JobHandler {
  20 +
  21 + @Resource
  22 + private PayTransferService transferService;
  23 +
  24 + @Override
  25 + @TenantJob
  26 + public String execute(String param) {
  27 + int count = transferService.syncTransfer();
  28 + return StrUtil.format("同步转账订单 {} 个", count);
  29 + }
  30 +}
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.pay.service.demo;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageParam;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5   -import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
  5 +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
6 6 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
7 7  
8 8 import javax.validation.Valid;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
... ... @@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
9 9 import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
10 10 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
11 11 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
12   -import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
  12 +import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
13 13 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
14 14 import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoOrderMapper;
15 15 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferService.java
... ... @@ -28,4 +28,12 @@ public interface PayDemoTransferService {
28 28 * @param pageVO 分页查询参数
29 29 */
30 30 PageResult<PayDemoTransferDO> getDemoTransferPage(PageParam pageVO);
  31 +
  32 + /**
  33 + * 更新转账业务示例订单的转账状态
  34 + *
  35 + * @param id 编号
  36 + * @param payTransferId 转账单编号
  37 + */
  38 + void updateDemoTransferStatus(Long id, Long payTransferId);
31 39 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java
1 1 package cn.iocoder.yudao.module.pay.service.demo;
2 2  
  3 +import cn.hutool.core.util.ObjectUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 6 import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
6 7 import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
7 8 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoTransferDO;
  9 +import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
8 10 import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoTransferMapper;
  11 +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
  12 +import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
9 13 import org.springframework.stereotype.Service;
10 14 import org.springframework.validation.annotation.Validated;
11 15  
12 16 import javax.annotation.Resource;
13 17 import javax.validation.Valid;
14 18 import javax.validation.Validator;
  19 +import java.util.Objects;
15 20  
  21 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  22 +import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
16 23 import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.WAITING;
17 24  
18 25 /**
... ... @@ -33,6 +40,8 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
33 40 @Resource
34 41 private PayDemoTransferMapper demoTransferMapper;
35 42 @Resource
  43 + private PayTransferService payTransferService;
  44 + @Resource
36 45 private Validator validator;
37 46  
38 47 @Override
... ... @@ -50,4 +59,41 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
50 59 public PageResult<PayDemoTransferDO> getDemoTransferPage(PageParam pageVO) {
51 60 return demoTransferMapper.selectPage(pageVO);
52 61 }
  62 +
  63 + @Override
  64 + public void updateDemoTransferStatus(Long id, Long payTransferId) {
  65 + PayTransferDO payTransfer = validateDemoTransferStatusCanUpdate(id, payTransferId);
  66 + // 更新示例订单状态
  67 + if (payTransfer != null) {
  68 + demoTransferMapper.updateById(new PayDemoTransferDO().setId(id)
  69 + .setPayTransferId(payTransferId)
  70 + .setPayChannelCode(payTransfer.getChannelCode())
  71 + .setTransferStatus(payTransfer.getStatus())
  72 + .setTransferTime(payTransfer.getSuccessTime()));
  73 + }
  74 + }
  75 +
  76 + private PayTransferDO validateDemoTransferStatusCanUpdate(Long id, Long payTransferId) {
  77 + PayDemoTransferDO demoTransfer = demoTransferMapper.selectById(id);
  78 + if (demoTransfer == null) {
  79 + throw exception(DEMO_TRANSFER_NOT_FOUND);
  80 + }
  81 + if (PayTransferStatusEnum.isSuccess(demoTransfer.getTransferStatus())
  82 + || PayTransferStatusEnum.isClosed(demoTransfer.getTransferStatus())) {
  83 + // 无需更新返回 null
  84 + return null;
  85 + }
  86 + PayTransferDO transfer = payTransferService.getTransfer(payTransferId);
  87 + if (transfer == null) {
  88 + throw exception(PAY_TRANSFER_NOT_FOUND);
  89 + }
  90 + if (!Objects.equals(demoTransfer.getPrice(), transfer.getPrice())) {
  91 + throw exception(DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH);
  92 + }
  93 + if (ObjectUtil.notEqual(transfer.getMerchantTransferId(), id.toString())) {
  94 + throw exception(DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR);
  95 + }
  96 + // TODO 校验账号
  97 + return transfer;
  98 + }
53 99 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java
... ... @@ -13,11 +13,13 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
13 13 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
14 14 import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
15 15 import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
  16 +import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
16 17 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
17 18 import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
18 19 import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
19 20 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
20 21 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
  22 +import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
21 23 import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyLogMapper;
22 24 import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper;
23 25 import cn.iocoder.yudao.module.pay.dal.redis.notify.PayNotifyLockRedisDAO;
... ... @@ -25,6 +27,7 @@ import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
25 27 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
26 28 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
27 29 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
  30 +import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
28 31 import com.google.common.annotations.VisibleForTesting;
29 32 import lombok.extern.slf4j.Slf4j;
30 33 import org.springframework.context.annotation.Lazy;
... ... @@ -73,6 +76,9 @@ public class PayNotifyServiceImpl implements PayNotifyService {
73 76 @Resource
74 77 @Lazy // 循环依赖,避免报错
75 78 private PayRefundService refundService;
  79 + @Resource
  80 + @Lazy // 循环依赖,避免报错
  81 + private PayTransferService transferService;
76 82  
77 83 @Resource
78 84 private PayNotifyTaskMapper notifyTaskMapper;
... ... @@ -100,6 +106,10 @@ public class PayNotifyServiceImpl implements PayNotifyService {
100 106 PayRefundDO refundDO = refundService.getRefund(task.getDataId());
101 107 task.setAppId(refundDO.getAppId())
102 108 .setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl());
  109 + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
  110 + PayTransferDO transfer = transferService.getTransfer(task.getDataId());
  111 + task.setAppId(transfer.getAppId()).setMerchantTransferId(transfer.getMerchantTransferId())
  112 + .setNotifyUrl(transfer.getNotifyUrl());
103 113 }
104 114  
105 115 // 执行插入
... ... @@ -214,6 +224,9 @@ public class PayNotifyServiceImpl implements PayNotifyService {
214 224 } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
215 225 request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
216 226 .payRefundId(task.getDataId()).build();
  227 + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
  228 + request = new PayTransferNotifyReqDTO().setMerchantTransferId(task.getMerchantTransferId())
  229 + .setPayTransferId(task.getDataId());
217 230 } else {
218 231 throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task));
219 232 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
... ... @@ -48,4 +48,10 @@ public interface PayTransferService {
48 48 */
49 49 PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO);
50 50  
  51 + /**
  52 + * 同步渠道转账单状态
  53 + *
  54 + * @return 同步到状态的转账数量,包括转账成功、转账失败、转账中的
  55 + */
  56 + int syncTransfer();
51 57 }
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
1 1 package cn.iocoder.yudao.module.pay.service.transfer;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjectUtil;
4 5 import cn.hutool.extra.spring.SpringUtil;
5   -import cn.iocoder.yudao.framework.common.exception.ServiceException;
6 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8 8 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
9 9 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
10 10 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
11 11 import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
  12 +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
  13 +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
12 14 import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
13 15 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
14 16 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
  17 +import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
15 18 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
16 19 import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
17 20 import cn.iocoder.yudao.module.pay.dal.mysql.transfer.PayTransferMapper;
18 21 import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
  22 +import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
  23 +import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
19 24 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
20 25 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
  26 +import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
21 27 import lombok.extern.slf4j.Slf4j;
22 28 import org.springframework.stereotype.Service;
23 29 import org.springframework.transaction.annotation.Transactional;
24 30  
25 31 import javax.annotation.Resource;
26 32 import javax.validation.Validator;
27   -import java.util.Objects;
  33 +import java.util.List;
28 34  
29 35 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
30 36 import static cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert.INSTANCE;
... ... @@ -51,6 +57,8 @@ public class PayTransferServiceImpl implements PayTransferService {
51 57 @Resource
52 58 private PayChannelService channelService;
53 59 @Resource
  60 + private PayNotifyService notifyService;
  61 + @Resource
54 62 private PayNoRedisDAO noRedisDAO;
55 63 @Resource
56 64 private Validator validator;
... ... @@ -70,56 +78,60 @@ public class PayTransferServiceImpl implements PayTransferService {
70 78  
71 79 @Override
72 80 public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
73   - // 1.1 校验转账单是否可以提交
74   - validateTransferCanCreate(reqDTO.getAppId(), reqDTO.getMerchantTransferId());
75   - // 1.2 校验 App
76   - appService.validPayApp(reqDTO.getAppId());
77   - // 1.3 校验支付渠道是否有效
  81 + // 1.1 校验 App
  82 + PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
  83 + // 1.2 校验支付渠道是否有效
78 84 PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
79 85 PayClient client = channelService.getPayClient(channel.getId());
80 86 if (client == null) {
81 87 log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
82 88 throw exception(CHANNEL_NOT_FOUND);
83 89 }
84   - // 2.创建转账单
85   - String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
86   - PayTransferDO transfer = INSTANCE.convert(reqDTO)
87   - .setChannelId(channel.getId())
88   - .setNo(no).setStatus(WAITING.getStatus())
89   - .setNotifyUrl("http://127.0.0.1:48080/admin-api/pay/todo"); // TODO 需要加个transfer Notify url
90   - transferMapper.insert(transfer);
91   - PayTransferRespDTO unifiedTransferResp = null;
  90 + // 1.3 校验转账单已经发起过转账。
  91 + PayTransferDO transfer = validateTransferCanCreate(reqDTO);
  92 +
  93 + if (transfer == null) {
  94 + // 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账
  95 + String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
  96 + transfer = INSTANCE.convert(reqDTO)
  97 + .setChannelId(channel.getId())
  98 + .setNo(no).setStatus(WAITING.getStatus())
  99 + .setNotifyUrl(payApp.getTransferNotifyUrl());
  100 + transferMapper.insert(transfer);
  101 + }
92 102 try {
93 103 // 3. 调用三方渠道发起转账
94   - PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(reqDTO)
95   - .setOutTransferNo(no);
96   - unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
97   - } catch (ServiceException ex) {
98   - // 业务异常.直接返回转账失败的结果
99   - log.error("[createTransfer][转账 id({}) requestDTO({}) 发生业务异常]", transfer.getId(), reqDTO, ex);
100   - unifiedTransferResp = PayTransferRespDTO.closedOf("", "", no, ex);
  104 + PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer)
  105 + .setOutTransferNo(transfer.getNo());
  106 + PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
  107 + // 4. 通知转账结果
  108 + getSelf().notifyTransfer(channel, unifiedTransferResp);
101 109 } catch (Throwable e) {
102 110 // 注意这里仅打印异常,不进行抛出。
103   - // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续通过转账回调、或者转账轮询可以拿到。
104   - // TODO 需要加转账回调业务接口 和 转账轮询未实现
105   - // 最终,在异常的情况下,支付中心会异步回调业务的转账回调接口,提供转账结果
  111 + // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续转账轮询可以拿到。
  112 + // 或者使用相同 no 再次发起转账请求
106 113 log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
107 114 }
108   - if (Objects.nonNull(unifiedTransferResp)) {
109   - // 4. 通知转账结果
110   - getSelf().notifyTransfer(channel, unifiedTransferResp);
111   - }
112   - return transfer.getId();
113   - }
114 115  
115   - @Override
116   - public PayTransferDO getTransfer(Long id) {
117   - return transferMapper.selectById(id);
  116 + return transfer.getId();
118 117 }
119 118  
120   - @Override
121   - public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
122   - return transferMapper.selectPage(pageReqVO);
  119 + private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto) {
  120 + PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(dto.getAppId(), dto.getMerchantTransferId());
  121 + if (transfer != null) {
  122 + // 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果.
  123 + if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
  124 + throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
  125 + }
  126 + if (ObjectUtil.notEqual(dto.getPrice(), transfer.getPrice())) {
  127 + throw exception(PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH);
  128 + }
  129 + if (ObjectUtil.notEqual(dto.getType(), transfer.getType())) {
  130 + throw exception(PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH);
  131 + }
  132 + }
  133 + // 如果状态为等待状态。不知道渠道转账是否发起成功。 允许使用相同的 no 再次发起转账,渠道会保证幂等
  134 + return transfer;
123 135 }
124 136  
125 137 @Transactional(rollbackFor = Exception.class)
... ... @@ -133,34 +145,48 @@ public class PayTransferServiceImpl implements PayTransferService {
133 145 if (PayTransferStatusRespEnum.isClosed(notify.getStatus())) {
134 146 notifyTransferClosed(channel, notify);
135 147 }
  148 + // 转账处理中的回调
  149 + if (PayTransferStatusRespEnum.isInProgress(notify.getStatus())) {
  150 + notifyTransferInProgress(channel, notify);
  151 + }
136 152 // WAITING 状态无需处理
137   - // TODO IN_PROGRESS 待处理
138 153 }
139 154  
140   - private void validateTransferCanCreate(Long appId, String merchantTransferId) {
141   - PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, merchantTransferId);
142   - if (transfer != null) { // 是否存在
143   - throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
  155 + private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) {
  156 + // 1.校验
  157 + PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
  158 + if (transfer == null) {
  159 + throw exception(PAY_TRANSFER_NOT_FOUND);
144 160 }
145   - }
146   -
147   - private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
148   - // 1. 更新 PayTransferDO 转账成功
149   - Boolean transferred = updateTransferSuccess(channel, notify);
150   - if (transferred) {
  161 + if (isInProgress(transfer.getStatus())) { // 如果已经是转账中,直接返回,不用重复更新
151 162 return;
152 163 }
153   - // 2. TODO 插入转账通知记录
  164 + if (!isWaiting(transfer.getStatus())) {
  165 + throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
  166 + }
  167 + // 2.更新
  168 + int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(),
  169 + CollUtil.newArrayList(WAITING.getStatus()),
  170 + new PayTransferDO().setStatus(IN_PROGRESS.getStatus()));
  171 + if (updateCounts == 0) {
  172 + throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
  173 + }
  174 + log.info("[notifyTransferInProgress][transfer({}) 更新为转账进行中状态]", transfer.getId());
  175 +
  176 + // 3. 插入转账通知记录
  177 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
  178 + transfer.getId());
154 179 }
155 180  
156   - private Boolean updateTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
  181 +
  182 + private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
157 183 // 1.校验
158 184 PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
159 185 if (transfer == null) {
160 186 throw exception(PAY_TRANSFER_NOT_FOUND);
161 187 }
162 188 if (isSuccess(transfer.getStatus())) { // 如果已成功,直接返回,不用重复更新
163   - return Boolean.TRUE;
  189 + return;
164 190 }
165 191 if (!isPendingStatus(transfer.getStatus())) {
166 192 throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
... ... @@ -176,10 +202,13 @@ public class PayTransferServiceImpl implements PayTransferService {
176 202 throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
177 203 }
178 204 log.info("[updateTransferSuccess][transfer({}) 更新为已转账]", transfer.getId());
179   - return Boolean.FALSE;
  205 +
  206 + // 3. 插入转账通知记录
  207 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
  208 + transfer.getId());
180 209 }
181 210  
182   - private void updateTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
  211 + private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
183 212 // 1.校验
184 213 PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
185 214 if (transfer == null) {
... ... @@ -192,6 +221,7 @@ public class PayTransferServiceImpl implements PayTransferService {
192 221 if (!isPendingStatus(transfer.getStatus())) {
193 222 throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
194 223 }
  224 +
195 225 // 2.更新
196 226 int updateCount = transferMapper.updateByIdAndStatus(transfer.getId(),
197 227 CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()),
... ... @@ -203,11 +233,61 @@ public class PayTransferServiceImpl implements PayTransferService {
203 233 throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
204 234 }
205 235 log.info("[updateTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId());
  236 +
  237 + // 3. 插入转账通知记录
  238 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
  239 + transfer.getId());
  240 +
206 241 }
207 242  
208   - private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
209   - // 更新 PayTransferDO 转账关闭
210   - updateTransferClosed(channel, notify);
  243 + @Override
  244 + public PayTransferDO getTransfer(Long id) {
  245 + return transferMapper.selectById(id);
  246 + }
  247 +
  248 + @Override
  249 + public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
  250 + return transferMapper.selectPage(pageReqVO);
  251 + }
  252 +
  253 + @Override
  254 + public int syncTransfer() {
  255 + List<PayTransferDO> list = transferMapper.selectListByStatus(WAITING.getStatus());
  256 + if (CollUtil.isEmpty(list)) {
  257 + return 0;
  258 + }
  259 + int count = 0;
  260 + for (PayTransferDO transfer : list) {
  261 + count += syncTransfer(transfer) ? 1 : 0;
  262 + }
  263 + return count;
  264 + }
  265 +
  266 + private boolean syncTransfer(PayTransferDO transfer) {
  267 + try {
  268 + // 1. 查询转账订单信息
  269 + PayClient payClient = channelService.getPayClient(transfer.getChannelId());
  270 + if (payClient == null) {
  271 + log.error("[syncTransfer][渠道编号({}) 找不到对应的支付客户端]", transfer.getChannelId());
  272 + return false;
  273 + }
  274 + PayTransferRespDTO resp = payClient.getTransfer(transfer.getNo(),
  275 + PayTransferTypeEnum.typeOf(transfer.getType()));
  276 +
  277 + // 2. 回调转账结果
  278 + notifyTransfer(transfer.getChannelId(), resp);
  279 + return true;
  280 + } catch (Throwable ex) {
  281 + log.error("[syncTransfer][transfer({}) 同步转账单状态异常]", transfer.getId(), ex);
  282 + return false;
  283 + }
  284 + }
  285 +
  286 + private void notifyTransfer(Long channelId, PayTransferRespDTO notify) {
  287 + // 校验渠道是否有效
  288 + PayChannelDO channel = channelService.validPayChannel(channelId);
  289 + // 通知转账结果给对应的业务
  290 + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyTransfer(channel, notify));
211 291 }
212 292  
213 293 /**
... ...
yudao-server/pom.xml
... ... @@ -37,11 +37,11 @@
37 37 </dependency>
38 38  
39 39 <!-- 会员中心。默认注释,保证编译速度 -->
40   -<!-- <dependency>-->
41   -<!-- <groupId>cn.iocoder.boot</groupId>-->
42   -<!-- <artifactId>yudao-module-member-biz</artifactId>-->
43   -<!-- <version>${revision}</version>-->
44   -<!-- </dependency>-->
  40 + <dependency>
  41 + <groupId>cn.iocoder.boot</groupId>
  42 + <artifactId>yudao-module-member-biz</artifactId>
  43 + <version>${revision}</version>
  44 + </dependency>
45 45  
46 46 <!-- 数据报表。默认注释,保证编译速度 -->
47 47 <!-- <dependency>-->
... ... @@ -56,11 +56,11 @@
56 56 <!-- <version>${revision}</version>-->
57 57 <!-- </dependency>-->
58 58 <!-- 支付服务。默认注释,保证编译速度 -->
59   -<!-- <dependency>-->
60   -<!-- <groupId>cn.iocoder.boot</groupId>-->
61   -<!-- <artifactId>yudao-module-pay-biz</artifactId>-->
62   -<!-- <version>${revision}</version>-->
63   -<!-- </dependency>-->
  59 + <dependency>
  60 + <groupId>cn.iocoder.boot</groupId>
  61 + <artifactId>yudao-module-pay-biz</artifactId>
  62 + <version>${revision}</version>
  63 + </dependency>
64 64  
65 65 <!-- 微信公众号模块。默认注释,保证编译速度 -->
66 66 <!-- <dependency>-->
... ... @@ -69,27 +69,27 @@
69 69 <!-- <version>${revision}</version>-->
70 70 <!-- </dependency>-->
71 71  
72   -<!-- 商城相关模块。默认注释,保证编译速度-->
73   -<!-- <dependency>-->
74   -<!-- <groupId>cn.iocoder.boot</groupId>-->
75   -<!-- <artifactId>yudao-module-promotion-biz</artifactId>-->
76   -<!-- <version>${revision}</version>-->
77   -<!-- </dependency>-->
78   -<!-- <dependency>-->
79   -<!-- <groupId>cn.iocoder.boot</groupId>-->
80   -<!-- <artifactId>yudao-module-product-biz</artifactId>-->
81   -<!-- <version>${revision}</version>-->
82   -<!-- </dependency>-->
83   -<!-- <dependency>-->
84   -<!-- <groupId>cn.iocoder.boot</groupId>-->
85   -<!-- <artifactId>yudao-module-trade-biz</artifactId>-->
86   -<!-- <version>${revision}</version>-->
87   -<!-- </dependency>-->
88   -<!-- <dependency>-->
89   -<!-- <groupId>cn.iocoder.boot</groupId>-->
90   -<!-- <artifactId>yudao-module-statistics-biz</artifactId>-->
91   -<!-- <version>${revision}</version>-->
92   -<!-- </dependency>-->
  72 + <!-- 商城相关模块。默认注释,保证编译速度-->
  73 + <dependency>
  74 + <groupId>cn.iocoder.boot</groupId>
  75 + <artifactId>yudao-module-promotion-biz</artifactId>
  76 + <version>${revision}</version>
  77 + </dependency>
  78 + <dependency>
  79 + <groupId>cn.iocoder.boot</groupId>
  80 + <artifactId>yudao-module-product-biz</artifactId>
  81 + <version>${revision}</version>
  82 + </dependency>
  83 + <dependency>
  84 + <groupId>cn.iocoder.boot</groupId>
  85 + <artifactId>yudao-module-trade-biz</artifactId>
  86 + <version>${revision}</version>
  87 + </dependency>
  88 + <dependency>
  89 + <groupId>cn.iocoder.boot</groupId>
  90 + <artifactId>yudao-module-statistics-biz</artifactId>
  91 + <version>${revision}</version>
  92 + </dependency>
93 93  
94 94 <!-- CRM 相关模块。默认注释,保证编译速度 -->
95 95 <!-- <dependency>-->
... ...