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
This commit is contained in:
YunaiV 2023-11-19 18:39:55 +08:00
commit e40c0e3057
50 changed files with 720 additions and 160 deletions

View File

@ -246,3 +246,11 @@ VALUES (
'转账订单', '', 2, 3, 1117,
'transfer', 'ep:credit-card', 'pay/transfer/index', 0, 'PayTransfer'
);
-- 转账通知脚本
ALTER TABLE `pay_app`
ADD COLUMN `transfer_notify_url` varchar(1024) NOT NULL COMMENT '转账结果的回调地址' AFTER `refund_notify_url`;
ALTER TABLE `pay_notify_task`
MODIFY COLUMN `merchant_order_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '商户订单编号' AFTER `status`,
ADD COLUMN `merchant_transfer_id` varchar(64) COMMENT '商户转账单编号' AFTER `merchant_order_id`;

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import java.util.Map;
@ -86,4 +87,12 @@ public interface PayClient {
*/
PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
/**
* 获得转账订单信息
*
* @param outTradeNo 外部订单号
* @param type 转账类型
* @return 转账信息
*/
PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
}

View File

@ -53,11 +53,24 @@ public class PayTransferRespDTO {
/**
* 创建WAITING状态的转账返回
*/
public static PayTransferRespDTO waitingOf(String channelOrderNo,
public static PayTransferRespDTO waitingOf(String channelTransferNo,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
respDTO.channelTransferNo = channelOrderNo;
respDTO.channelTransferNo = channelTransferNo;
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建IN_PROGRESS状态的转账返回
*/
public static PayTransferRespDTO dealingOf(String channelTransferNo,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
respDTO.channelTransferNo = channelTransferNo;
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;

View File

@ -188,11 +188,11 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
@Override
public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
validatePayTransferReqDTO(reqDTO);
PayTransferRespDTO resp;
try{
validatePayTransferReqDTO(reqDTO);
try {
resp = doUnifiedTransfer(reqDTO);
}catch (ServiceException ex) { // 业务异常都是实现类已经翻译所以直接抛出即可
} catch (ServiceException ex) { // 业务异常都是实现类已经翻译所以直接抛出即可
throw ex;
} catch (Throwable ex) {
// 系统异常则包装成 PayException 异常抛出
@ -219,9 +219,25 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
}
}
@Override
public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
try {
return doGetTransfer(outTradeNo, type);
} catch (ServiceException ex) { // 业务异常都是实现类已经翻译所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
getId(), outTradeNo, type, ex);
throw buildPayException(ex);
}
}
protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
throws Throwable;
protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
throws Throwable;
// ========== 各种工具方法 ==========
private PayException buildPayException(Throwable ex) {

View File

@ -23,14 +23,8 @@ import com.alipay.api.AlipayResponse;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.*;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayFundTransUniTransferRequest;
import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.response.AlipayFundTransUniTransferResponse;
import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.alipay.api.request.*;
import com.alipay.api.response.*;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@ -126,7 +120,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
}
// 2.2 解析订单的状态
Integer status = parseStatus(response.getTradeStatus());
Assert.notNull(status, () -> {
Assert.notNull(status, () -> {
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
});
return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
@ -228,7 +222,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 校验公钥类型 必须使用公钥证书模式
if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
throw exception0(ERROR_CONFIGURATION.getCode(),"支付宝单笔转账必须使用公钥证书模式");
throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式");
}
// 1.2 构建 AlipayFundTransUniTransferModel
AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
@ -238,45 +232,97 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
model.setOutBizNo(reqDTO.getOutTransferNo());
model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
if (reqDTO.getChannelExtras() != null) {
model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
}
// 个性化的参数
Participant payeeInfo = new Participant();
PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
switch (transferType) {
// TODO @jason是不是不用传递 transferType 参数哈因为应该已经明确是支付宝啦
// @芋艿 是不是还要考虑转账到银行卡所以传 transferType 但是转账到银行卡不知道要如何测试??
case ALIPAY_BALANCE: {
// 个性化的参数
Participant payeeInfo = new Participant();
payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
model.setPayeeInfo(payeeInfo);
// 1.3 构建 AlipayFundTransUniTransferRequest
AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
request.setBizModel(model);
// 执行请求
AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
// 处理结果
if (!response.isSuccess()) {
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败 返回 WAIT 状态. 后续 job 会轮询
if (ObjectUtils.equalsAny(response.getSubCode(), "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
}
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
reqDTO.getOutTransferNo(), response);
}
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
response.getOutBizNo(), response);
break;
}
case BANK_CARD: {
Participant payeeInfo = new Participant();
payeeInfo.setIdentityType("BANKCARD_ACCOUNT");
// TODO 待实现
throw exception(NOT_IMPLEMENTED);
}
default: {
throw exception0(BAD_REQUEST.getCode(),"不正确的转账类型: {}",transferType);
throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType);
}
}
// 1.3 构建 AlipayFundTransUniTransferRequest
AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
request.setBizModel(model);
// 执行请求
AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
// 处理结果
if (!response.isSuccess()) {
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败 返回 WAIT 状态. 后续 job 会轮询或相同 outBizNo 重新发起转账
// 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
}
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
reqDTO.getOutTransferNo(), response);
} else {
if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
reqDTO.getOutTransferNo(), response);
}
if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
}
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
response.getOutBizNo(), response);
}
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable {
// 1.1 构建 AlipayFundTransCommonQueryModel
AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD");
model.setBizScene("DIRECT_TRANSFER"); //业务场景
model.setOutBizNo(outTradeNo);
// 1.2 构建 AlipayFundTransCommonQueryRequest
AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
request.setBizModel(model);
// 2.1 执行请求
AlipayFundTransCommonQueryResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
// 2.2 处理返回结果
if (response.isSuccess()) {
if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
outTradeNo, response);
}
if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response);
}
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
response.getOutBizNo(), response);
} else {
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
// 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
return PayTransferRespDTO.waitingOf(null, outTradeNo, response);
}
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
outTradeNo, response);
}
}
// ========== 各种工具方法 ==========

View File

@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifie
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import java.time.LocalDateTime;
import java.util.Map;
@ -71,4 +72,9 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
throw new UnsupportedOperationException("待实现");
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
throw new UnsupportedOperationException("待实现");
}
}

View File

@ -16,6 +16,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDT
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
@ -431,6 +432,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
throw new UnsupportedOperationException("待实现");
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
throw new UnsupportedOperationException("待实现");
}
// ========== 各种工具方法 ==========
static String formatDateV2(LocalDateTime time) {

View File

@ -38,4 +38,8 @@ public enum PayTransferStatusRespEnum {
public static boolean isClosed(Integer status) {
return Objects.equals(status, CLOSED.getStatus());
}
public static boolean isInProgress(Integer status) {
return Objects.equals(status, IN_PROGRESS.getStatus());
}
}

View File

@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.github.yulichang.base.MPJBaseMapper;
import com.github.yulichang.interfaces.MPJBaseJoin;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
@ -39,6 +40,13 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
// 转换返回
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
default T selectOne(String field, Object value) {
return selectOne(new QueryWrapper<T>().eq(field, value));
}

View File

@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.product.controller.admin.favorite;
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.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
import cn.iocoder.yudao.module.product.service.favorite.ProductFavoriteService;
import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - 商品收藏")
@RestController
@RequestMapping("/product/favorite")
@Validated
public class ProductFavoriteController {
@Resource
private ProductFavoriteService productFavoriteService;
@Resource
private ProductSpuService productSpuService;
@GetMapping("/page")
@Operation(summary = "获得商品收藏分页")
@PreAuthorize("@ss.hasPermission('product:favorite:query')")
public CommonResult<PageResult<ProductFavoriteRespVO>> getFavoritePage(@Valid ProductFavoritePageReqVO pageVO) {
PageResult<ProductFavoriteDO> pageResult = productFavoriteService.getFavoritePage(pageVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty());
}
// 拼接数据
List<ProductSpuDO> spuList = productSpuService.getSpuList(convertSet(pageResult.getList(), ProductFavoriteDO::getSpuId));
return success(ProductFavoriteConvert.INSTANCE.convertPage(pageResult, spuList));
}
}

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 商品收藏 Base VO提供给添加修改详细的子 VO 使用
* 如果子 VO 存在差异的字段请不要添加到这里影响 Swagger 文档生成
*/
@Data
public class ProductFavoriteBaseVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5036")
@NotNull(message = "用户编号不能为空")
private Long userId;
}

View File

@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - 商品收藏分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ProductFavoritePageReqVO extends PageParam {
@Schema(description = "用户编号", example = "5036")
private Long userId;
}

View File

@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.ToString;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 商品收藏的单个 Response VO")
@Data
@ToString(callSuper = true)
public class ProductFavoriteReqVO extends ProductFavoriteBaseVO {
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32734")
@NotNull(message = "商品 SPU 编号不能为空")
private Long spuId;
}

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuRespVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.ToString;
@Schema(description = "管理后台 - 商品收藏 Response VO")
@Data
@ToString(callSuper = true)
public class ProductFavoriteRespVO extends ProductSpuRespVO {
@Schema(description = "userId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
private Long userId;
@Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
private Long spuId;
}

View File

@ -12,19 +12,7 @@ import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ProductSpuDetailRespVO extends ProductSpuBaseVO {
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1212")
private Long id;
@Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
private Integer salesCount;
@Schema(description = "浏览量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20000")
private Integer browseCount;
@Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
public class ProductSpuDetailRespVO extends ProductSpuRespVO {
// ========== SKU 相关字段 =========

View File

@ -13,7 +13,7 @@ import java.time.LocalDateTime;
@ToString(callSuper = true)
public class ProductSpuRespVO extends ProductSpuBaseVO {
@Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
private Long id;
@Schema(description = "商品价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "1999")

View File

@ -30,6 +30,7 @@ import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -75,6 +76,25 @@ public class AppProductSpuController {
return success(voList);
}
@GetMapping("/list-by-ids")
@Operation(summary = "获得商品 SPU 列表")
@Parameters({
@Parameter(name = "ids", description = "编号列表", required = true)
})
public CommonResult<List<AppProductSpuPageRespVO>> getSpuList(@RequestParam("ids") Set<Long> ids) {
List<ProductSpuDO> list = productSpuService.getSpuList(ids);
if (CollUtil.isEmpty(list)) {
return success(Collections.emptyList());
}
// 拼接返回
List<AppProductSpuPageRespVO> voList = ProductSpuConvert.INSTANCE.convertListForGetSpuList(list);
// 处理 vip 价格
MemberLevelRespDTO memberLevel = getMemberLevel();
voList.forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
return success(voList);
}
@GetMapping("/page")
@Operation(summary = "获得商品 SPU 分页")
public CommonResult<PageResult<AppProductSpuPageRespVO>> getSpuPage(@Valid AppProductSpuPageReqVO pageVO) {

View File

@ -15,6 +15,9 @@ public class AppProductSpuPageRespVO {
@Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name;
@Schema(description = "商品简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉小短袖简介")
private String introduction;
@Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long categoryId;

View File

@ -1,5 +1,8 @@
package cn.iocoder.yudao.module.product.convert.favorite;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoriteRespVO;
import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
@ -34,4 +37,18 @@ public interface ProductFavoriteConvert {
return resultList;
}
default PageResult<ProductFavoriteRespVO> convertPage(PageResult<ProductFavoriteDO> pageResult, List<ProductSpuDO> spuList) {
Map<Long, ProductSpuDO> spuMap = convertMap(spuList, ProductSpuDO::getId);
List<ProductFavoriteRespVO> voList = CollectionUtils.convertList(pageResult.getList(), favorite -> {
ProductSpuDO spu = spuMap.get(favorite.getSpuId());
return convert02(spu, favorite);
});
return new PageResult<>(voList, pageResult.getTotal());
}
@Mapping(target = "id", source = "favorite.id")
@Mapping(target = "userId", source = "favorite.userId")
@Mapping(target = "spuId", source = "favorite.spuId")
@Mapping(target = "createTime", source = "favorite.createTime")
ProductFavoriteRespVO convert02(ProductSpuDO spu, ProductFavoriteDO favorite);
}

View File

@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.product.dal.mysql.favorite;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@ -21,6 +23,12 @@ public interface ProductFavoriteMapper extends BaseMapperX<ProductFavoriteDO> {
.orderByDesc(ProductFavoriteDO::getId));
}
default PageResult<ProductFavoriteDO> selectPageByUserId(ProductFavoritePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<ProductFavoriteDO>()
.eqIfPresent(ProductFavoriteDO::getUserId, reqVO.getUserId())
.orderByDesc(ProductFavoriteDO::getId));
}
default Long selectCountByUserId(Long userId) {
return selectCount(ProductFavoriteDO::getUserId, userId);
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.product.service.favorite;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
@ -37,6 +38,13 @@ public interface ProductFavoriteService {
*/
PageResult<ProductFavoriteDO> getFavoritePage(Long userId, @Valid AppFavoritePageReqVO reqVO);
/**
* 分页查询用户收藏列表
*
* @param reqVO 请求 vo
*/
PageResult<ProductFavoriteDO> getFavoritePage(@Valid ProductFavoritePageReqVO reqVO);
/**
* 获取收藏过商品
*

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.product.service.favorite;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
@ -54,6 +55,11 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
return productFavoriteMapper.selectPageByUserAndType(userId, reqVO);
}
@Override
public PageResult<ProductFavoriteDO> getFavoritePage(@Valid ProductFavoritePageReqVO reqVO) {
return productFavoriteMapper.selectPageByUserId(reqVO);
}
@Override
public ProductFavoriteDO getFavorite(Long userId, Long spuId) {
return productFavoriteMapper.selectByUserIdAndSpuId(userId, spuId);

View File

@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.promotion.controller.admin.diy.vo.template;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotEmpty;
import java.util.List;
/**
@ -14,7 +14,7 @@ import java.util.List;
public class DiyTemplateBaseVO {
@Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "默认主题")
@NotNull(message = "模板名称不能为空")
@NotEmpty(message = "模板名称不能为空")
private String name;
@Schema(description = "备注", example = "默认主题")

View File

@ -32,6 +32,8 @@ public class DiyPageDO extends BaseDO {
private Long id;
/**
* 装修模板编号
*
* 关联 {@link DiyTemplateDO#getId()}
*/
private Long templateId;
/**

View File

@ -14,6 +14,9 @@ import java.util.List;
/**
* 装修模板 DO
*
* 1. 新建一个模版下面可以包含多个 {@link DiyPageDO} 页面例如说首页我的
* 2. 如果需要使用某个模版则将 {@link #used} 设置为 true表示已使用有且仅有一个
*
* @author owen
*/
@TableName(value = "promotion_diy_template", autoResultMap = true)

View File

@ -42,7 +42,6 @@ public class DiyPageServiceImpl implements DiyPageService {
DiyPageDO diyPage = DiyPageConvert.INSTANCE.convert(createReqVO);
diyPage.setProperty("{}");
diyPageMapper.insert(diyPage);
// 返回
return diyPage.getId();
}
@ -57,6 +56,13 @@ public class DiyPageServiceImpl implements DiyPageService {
diyPageMapper.updateById(updateObj);
}
/**
* 校验 Page 页面在一个 template 模版下的名字是唯一的
*
* @param id Page 编号
* @param templateId 模版编号
* @param name Page 名字
*/
void validateNameUnique(Long id, Long templateId, String name) {
if (templateId != null || StrUtil.isBlank(name)) {
return;

View File

@ -32,9 +32,11 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
@Resource
private DiyTemplateMapper diyTemplateMapper;
@Resource
private DiyPageService diyPageService;
// TODO @疯狂事务
@Override
public Long createDiyTemplate(DiyTemplateCreateReqVO createReqVO) {
// 校验名称唯一
@ -120,9 +122,11 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
}
@Override
// TODO @疯狂事务
public void useDiyTemplate(Long id) {
// 校验存在
validateDiyTemplateExists(id);
// TODO @疯狂要不已使用的情况抛个业务异常
// 已使用的更新为未使用
DiyTemplateDO used = diyTemplateMapper.selectByUsed(true);
if (used != null) {
@ -136,8 +140,8 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
this.updateUsed(id, true, LocalDateTime.now());
}
@Transactional(rollbackFor = Exception.class)
@Override
@Transactional(rollbackFor = Exception.class)
public void updateDiyTemplateProperty(DiyTemplatePropertyUpdateRequestVO updateReqVO) {
// 校验存在
validateDiyTemplateExists(updateReqVO.getId());
@ -151,6 +155,7 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
return diyTemplateMapper.selectByUsed(true);
}
// TODO @疯狂挪到 useDiyTemplate 下面改名 updateTemplateUsed 会不会好点哈
/**
* 更新模板是否使用
*

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.pay.api.notify.dto;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 转账单的通知 Request DTO
*
* @author jason
*/
@Data
public class PayTransferNotifyReqDTO {
/**
* 商户转账单号
*/
@NotEmpty(message = "商户转账单号不能为空")
private String merchantTransferId;
/**
* 转账订单编号
*/
@NotNull(message = "转账订单编号不能为空")
private Long payTransferId;
}

View File

@ -65,12 +65,13 @@ public interface ErrorCodeConstants {
// ========== 转账模块 1-007-009-000 ==========
ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_000, "发起转账报错,错误码:{},错误提示:{}");
ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账交易单不存在");
ErrorCode PAY_TRANSFER_STATUS_IS_SUCCESS = new ErrorCode(1_007_009_002, "转账单已成功转账");
ErrorCode PAY_TRANSFER_EXISTS = new ErrorCode(1_007_009_003, "已经存在转账单");
ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经存在,请查询转账订单相关状态");
ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在");
ErrorCode PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH = new ErrorCode(1_007_009_002, "两次相同转账请求的类型不匹配");
ErrorCode PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "两次相同转账请求的金额不匹配");
ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经发起,请查询转账订单相关状态");
ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_005, "转账单不处于待转账");
ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中");
// ========== 示例订单 1-007-900-000 ==========
ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1_007_900_000, "示例订单不存在");
ErrorCode DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1_007_900_001, "示例订单更新支付状态失败,订单不是【未支付】状态");
@ -84,4 +85,8 @@ public interface ErrorCodeConstants {
ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1_007_900_009, "发起退款失败,退款单编号不匹配");
ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1_007_900_010, "发起退款失败,退款单金额不匹配");
// ========== 示例转账订单 1-007-901-001 ==========
ErrorCode DEMO_TRANSFER_NOT_FOUND = new ErrorCode(1_007_901_001, "示例转账单不存在");
ErrorCode DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR = new ErrorCode(1_007_901_002, "转账失败,转账单编号不匹配");
ErrorCode DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH = new ErrorCode(1_007_901_003, "转账失败,转账单金额不匹配");
}

View File

@ -14,6 +14,7 @@ public enum PayNotifyTypeEnum {
ORDER(1, "支付单"),
REFUND(2, "退款单"),
TRANSFER(3, "转账单")
;
/**

View File

@ -40,9 +40,13 @@ public enum PayTransferStatusEnum {
public static boolean isClosed(Integer status) {
return Objects.equals(status, CLOSED.getStatus());
}
public static boolean isWaiting(Integer status) {
return Objects.equals(status, WAITING.getStatus());
}
public static boolean isInProgress(Integer status) {
return Objects.equals(status, IN_PROGRESS.getStatus());
}
/**
* 是否处于待转账或者转账中的状态

View File

@ -6,8 +6,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
import cn.iocoder.yudao.module.pay.convert.demo.PayDemoOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import cn.iocoder.yudao.module.pay.service.demo.PayDemoOrderService;

View File

@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.demo;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferRespVO;
import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
@ -14,6 +16,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -33,9 +36,19 @@ public class PayDemoTransferController {
}
@GetMapping("/page")
@Operation(summary = "获得示例订单分页")
@Operation(summary = "获得示例转账订单分页")
public CommonResult<PageResult<PayDemoTransferRespVO>> getDemoTransferPage(@Valid PageParam pageVO) {
PageResult<PayDemoTransferDO> pageResult = demoTransferService.getDemoTransferPage(pageVO);
return success(PayDemoTransferConvert.INSTANCE.convertPage(pageResult));
}
@PostMapping("/update-status")
@Operation(summary = "更新示例转账订单的转账状态") // pay-module 转账服务进行回调
@PermitAll // 无需登录安全由 PayDemoTransferService 内部校验实现
@OperateLog(enable = false) // 禁用操作日志因为没有操作人
public CommonResult<Boolean> updateDemoTransferStatus(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
demoTransferService.updateDemoTransferStatus(Long.valueOf(notifyReqDTO.getMerchantTransferId()),
notifyReqDTO.getPayTransferId());
return success(true);
}
}

View File

@ -1,9 +1,8 @@
package cn.iocoder.yudao.module.pay.controller.admin.demo.vo;
package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 示例订单创建 Request VO")

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.pay.controller.admin.demo.vo;
package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.pay.convert.demo;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

View File

@ -18,7 +18,7 @@ public interface PayTransferConvert {
PayTransferDO convert(PayTransferCreateReqDTO dto);
PayTransferUnifiedReqDTO convert2(PayTransferCreateReqDTO dto);
PayTransferUnifiedReqDTO convert2(PayTransferDO dto);
PayTransferCreateReqDTO convert(PayTransferCreateReqVO vo);

View File

@ -54,4 +54,9 @@ public class PayAppDO extends BaseDO {
*/
private String refundNotifyUrl;
/**
* 转账结果的回调地址
*/
private String transferNotifyUrl;
}

View File

@ -66,6 +66,10 @@ public class PayNotifyTaskDO extends TenantBaseDO {
* 商户订单编号
*/
private String merchantOrderId;
/**
* 商户转账单编号
*/
private String merchantTransferId;
/**
* 通知状态
*

View File

@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.pay.dal.mysql.transfer;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
@ -40,6 +40,10 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
.betweenIfPresent(PayTransferDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayTransferDO::getId));
}
default List<PayTransferDO> selectListByStatus(Integer status){
return selectList(PayTransferDO::getStatus, status);
}
}

View File

@ -14,6 +14,7 @@ import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
@ -181,4 +182,9 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
throw new UnsupportedOperationException("待实现");
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
throw new UnsupportedOperationException("待实现");
}
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.pay.job.transfer;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 转账订单的同步 Job
*
* 由于转账订单的转账结果有些渠道是异步通知进行同步的考虑到异步通知可能会失败小概率所以需要定时进行同步
*
* @author jason
*/
@Component
public class PayTransferSyncJob implements JobHandler {
@Resource
private PayTransferService transferService;
@Override
@TenantJob
public String execute(String param) {
int count = transferService.syncTransfer();
return StrUtil.format("同步转账订单 {} 个", count);
}
}

View File

@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.pay.service.demo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import javax.validation.Valid;

View File

@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoOrderMapper;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;

View File

@ -28,4 +28,12 @@ public interface PayDemoTransferService {
* @param pageVO 分页查询参数
*/
PageResult<PayDemoTransferDO> getDemoTransferPage(PageParam pageVO);
/**
* 更新转账业务示例订单的转账状态
*
* @param id 编号
* @param payTransferId 转账单编号
*/
void updateDemoTransferStatus(Long id, Long payTransferId);
}

View File

@ -1,18 +1,25 @@
package cn.iocoder.yudao.module.pay.service.demo;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoTransferDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoTransferMapper;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Valid;
import javax.validation.Validator;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.WAITING;
/**
@ -33,6 +40,8 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
@Resource
private PayDemoTransferMapper demoTransferMapper;
@Resource
private PayTransferService payTransferService;
@Resource
private Validator validator;
@Override
@ -50,4 +59,41 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
public PageResult<PayDemoTransferDO> getDemoTransferPage(PageParam pageVO) {
return demoTransferMapper.selectPage(pageVO);
}
@Override
public void updateDemoTransferStatus(Long id, Long payTransferId) {
PayTransferDO payTransfer = validateDemoTransferStatusCanUpdate(id, payTransferId);
// 更新示例订单状态
if (payTransfer != null) {
demoTransferMapper.updateById(new PayDemoTransferDO().setId(id)
.setPayTransferId(payTransferId)
.setPayChannelCode(payTransfer.getChannelCode())
.setTransferStatus(payTransfer.getStatus())
.setTransferTime(payTransfer.getSuccessTime()));
}
}
private PayTransferDO validateDemoTransferStatusCanUpdate(Long id, Long payTransferId) {
PayDemoTransferDO demoTransfer = demoTransferMapper.selectById(id);
if (demoTransfer == null) {
throw exception(DEMO_TRANSFER_NOT_FOUND);
}
if (PayTransferStatusEnum.isSuccess(demoTransfer.getTransferStatus())
|| PayTransferStatusEnum.isClosed(demoTransfer.getTransferStatus())) {
// 无需更新返回 null
return null;
}
PayTransferDO transfer = payTransferService.getTransfer(payTransferId);
if (transfer == null) {
throw exception(PAY_TRANSFER_NOT_FOUND);
}
if (!Objects.equals(demoTransfer.getPrice(), transfer.getPrice())) {
throw exception(DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH);
}
if (ObjectUtil.notEqual(transfer.getMerchantTransferId(), id.toString())) {
throw exception(DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR);
}
// TODO 校验账号
return transfer;
}
}

View File

@ -13,11 +13,13 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
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.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyLogMapper;
import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper;
import cn.iocoder.yudao.module.pay.dal.redis.notify.PayNotifyLockRedisDAO;
@ -25,6 +27,7 @@ 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.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
@ -73,6 +76,9 @@ public class PayNotifyServiceImpl implements PayNotifyService {
@Resource
@Lazy // 循环依赖避免报错
private PayRefundService refundService;
@Resource
@Lazy // 循环依赖避免报错
private PayTransferService transferService;
@Resource
private PayNotifyTaskMapper notifyTaskMapper;
@ -100,6 +106,10 @@ public class PayNotifyServiceImpl implements PayNotifyService {
PayRefundDO refundDO = refundService.getRefund(task.getDataId());
task.setAppId(refundDO.getAppId())
.setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl());
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
PayTransferDO transfer = transferService.getTransfer(task.getDataId());
task.setAppId(transfer.getAppId()).setMerchantTransferId(transfer.getMerchantTransferId())
.setNotifyUrl(transfer.getNotifyUrl());
}
// 执行插入
@ -214,6 +224,9 @@ public class PayNotifyServiceImpl implements PayNotifyService {
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
.payRefundId(task.getDataId()).build();
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
request = new PayTransferNotifyReqDTO().setMerchantTransferId(task.getMerchantTransferId())
.setPayTransferId(task.getDataId());
} else {
throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task));
}

View File

@ -48,4 +48,10 @@ public interface PayTransferService {
*/
PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO);
/**
* 同步渠道转账单状态
*
* @return 同步到状态的转账数量包括转账成功转账失败转账中的
*/
int syncTransfer();
}

View File

@ -1,30 +1,36 @@
package cn.iocoder.yudao.module.pay.service.transfer;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.dal.mysql.transfer.PayTransferMapper;
import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.Validator;
import java.util.Objects;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert.INSTANCE;
@ -51,6 +57,8 @@ public class PayTransferServiceImpl implements PayTransferService {
@Resource
private PayChannelService channelService;
@Resource
private PayNotifyService notifyService;
@Resource
private PayNoRedisDAO noRedisDAO;
@Resource
private Validator validator;
@ -70,56 +78,60 @@ public class PayTransferServiceImpl implements PayTransferService {
@Override
public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
// 1.1 校验转账单是否可以提交
validateTransferCanCreate(reqDTO.getAppId(), reqDTO.getMerchantTransferId());
// 1.2 校验 App
appService.validPayApp(reqDTO.getAppId());
// 1.3 校验支付渠道是否有效
// 1.1 校验 App
PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
// 1.2 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
PayClient client = channelService.getPayClient(channel.getId());
if (client == null) {
log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(CHANNEL_NOT_FOUND);
}
// 2.创建转账单
String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
PayTransferDO transfer = INSTANCE.convert(reqDTO)
.setChannelId(channel.getId())
.setNo(no).setStatus(WAITING.getStatus())
.setNotifyUrl("http://127.0.0.1:48080/admin-api/pay/todo"); // TODO 需要加个transfer Notify url
transferMapper.insert(transfer);
PayTransferRespDTO unifiedTransferResp = null;
// 1.3 校验转账单已经发起过转账
PayTransferDO transfer = validateTransferCanCreate(reqDTO);
if (transfer == null) {
// 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账
String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
transfer = INSTANCE.convert(reqDTO)
.setChannelId(channel.getId())
.setNo(no).setStatus(WAITING.getStatus())
.setNotifyUrl(payApp.getTransferNotifyUrl());
transferMapper.insert(transfer);
}
try {
// 3. 调用三方渠道发起转账
PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(reqDTO)
.setOutTransferNo(no);
unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
} catch (ServiceException ex) {
// 业务异常.直接返回转账失败的结果
log.error("[createTransfer][转账 id({}) requestDTO({}) 发生业务异常]", transfer.getId(), reqDTO, ex);
unifiedTransferResp = PayTransferRespDTO.closedOf("", "", no, ex);
} catch (Throwable e) {
// 注意这里仅打印异常不进行抛出
// 原因是虽然调用支付渠道进行转账发生异常网络请求超时实际转账成功这个结果后续通过转账回调或者转账轮询可以拿到
// TODO 需要加转账回调业务接口 转账轮询未实现
// 最终在异常的情况下支付中心会异步回调业务的转账回调接口提供转账结果
log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
}
if (Objects.nonNull(unifiedTransferResp)) {
PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer)
.setOutTransferNo(transfer.getNo());
PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
// 4. 通知转账结果
getSelf().notifyTransfer(channel, unifiedTransferResp);
} catch (Throwable e) {
// 注意这里仅打印异常不进行抛出
// 原因是虽然调用支付渠道进行转账发生异常网络请求超时实际转账成功这个结果后续转账轮询可以拿到
// 或者使用相同 no 再次发起转账请求
log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
}
return transfer.getId();
}
@Override
public PayTransferDO getTransfer(Long id) {
return transferMapper.selectById(id);
}
@Override
public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
return transferMapper.selectPage(pageReqVO);
private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto) {
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(dto.getAppId(), dto.getMerchantTransferId());
if (transfer != null) {
// 已经存在,并且状态不为等待状态说明已经调用渠道转账并返回结果.
if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
}
if (ObjectUtil.notEqual(dto.getPrice(), transfer.getPrice())) {
throw exception(PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH);
}
if (ObjectUtil.notEqual(dto.getType(), transfer.getType())) {
throw exception(PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH);
}
}
// 如果状态为等待状态不知道渠道转账是否发起成功 允许使用相同的 no 再次发起转账渠道会保证幂等
return transfer;
}
@Transactional(rollbackFor = Exception.class)
@ -133,34 +145,48 @@ public class PayTransferServiceImpl implements PayTransferService {
if (PayTransferStatusRespEnum.isClosed(notify.getStatus())) {
notifyTransferClosed(channel, notify);
}
// WAITING 状态无需处理
// TODO IN_PROGRESS 待处理
}
private void validateTransferCanCreate(Long appId, String merchantTransferId) {
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, merchantTransferId);
if (transfer != null) { // 是否存在
throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
// 转账处理中的回调
if (PayTransferStatusRespEnum.isInProgress(notify.getStatus())) {
notifyTransferInProgress(channel, notify);
}
// WAITING 状态无需处理
}
private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
// 1. 更新 PayTransferDO 转账成功
Boolean transferred = updateTransferSuccess(channel, notify);
if (transferred) {
private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
if (transfer == null) {
throw exception(PAY_TRANSFER_NOT_FOUND);
}
if (isInProgress(transfer.getStatus())) { // 如果已经是转账中直接返回不用重复更新
return;
}
// 2. TODO 插入转账通知记录
if (!isWaiting(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
}
// 2.更新
int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(),
CollUtil.newArrayList(WAITING.getStatus()),
new PayTransferDO().setStatus(IN_PROGRESS.getStatus()));
if (updateCounts == 0) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
}
log.info("[notifyTransferInProgress][transfer({}) 更新为转账进行中状态]", transfer.getId());
// 3. 插入转账通知记录
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
transfer.getId());
}
private Boolean updateTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
if (transfer == null) {
throw exception(PAY_TRANSFER_NOT_FOUND);
}
if (isSuccess(transfer.getStatus())) { // 如果已成功直接返回不用重复更新
return Boolean.TRUE;
return;
}
if (!isPendingStatus(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
@ -176,10 +202,13 @@ public class PayTransferServiceImpl implements PayTransferService {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
}
log.info("[updateTransferSuccess][transfer({}) 更新为已转账]", transfer.getId());
return Boolean.FALSE;
// 3. 插入转账通知记录
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
transfer.getId());
}
private void updateTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
if (transfer == null) {
@ -192,6 +221,7 @@ public class PayTransferServiceImpl implements PayTransferService {
if (!isPendingStatus(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
}
// 2.更新
int updateCount = transferMapper.updateByIdAndStatus(transfer.getId(),
CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()),
@ -203,11 +233,61 @@ public class PayTransferServiceImpl implements PayTransferService {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
}
log.info("[updateTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId());
// 3. 插入转账通知记录
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
transfer.getId());
}
private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
// 更新 PayTransferDO 转账关闭
updateTransferClosed(channel, notify);
@Override
public PayTransferDO getTransfer(Long id) {
return transferMapper.selectById(id);
}
@Override
public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
return transferMapper.selectPage(pageReqVO);
}
@Override
public int syncTransfer() {
List<PayTransferDO> list = transferMapper.selectListByStatus(WAITING.getStatus());
if (CollUtil.isEmpty(list)) {
return 0;
}
int count = 0;
for (PayTransferDO transfer : list) {
count += syncTransfer(transfer) ? 1 : 0;
}
return count;
}
private boolean syncTransfer(PayTransferDO transfer) {
try {
// 1. 查询转账订单信息
PayClient payClient = channelService.getPayClient(transfer.getChannelId());
if (payClient == null) {
log.error("[syncTransfer][渠道编号({}) 找不到对应的支付客户端]", transfer.getChannelId());
return false;
}
PayTransferRespDTO resp = payClient.getTransfer(transfer.getNo(),
PayTransferTypeEnum.typeOf(transfer.getType()));
// 2. 回调转账结果
notifyTransfer(transfer.getChannelId(), resp);
return true;
} catch (Throwable ex) {
log.error("[syncTransfer][transfer({}) 同步转账单状态异常]", transfer.getId(), ex);
return false;
}
}
private void notifyTransfer(Long channelId, PayTransferRespDTO notify) {
// 校验渠道是否有效
PayChannelDO channel = channelService.validPayChannel(channelId);
// 通知转账结果给对应的业务
TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyTransfer(channel, notify));
}
/**

View File

@ -37,11 +37,11 @@
</dependency>
<!-- 会员中心。默认注释,保证编译速度 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-member-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-member-biz</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据报表。默认注释,保证编译速度 -->
<!-- <dependency>-->
@ -56,11 +56,11 @@
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- 支付服务。默认注释,保证编译速度 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-pay-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-pay-biz</artifactId>
<version>${revision}</version>
</dependency>
<!-- 微信公众号模块。默认注释,保证编译速度 -->
<!-- <dependency>-->
@ -69,27 +69,27 @@
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- 商城相关模块。默认注释,保证编译速度-->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-promotion-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-product-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-trade-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-statistics-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- 商城相关模块。默认注释,保证编译速度-->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-promotion-biz</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-product-biz</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-trade-biz</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-statistics-biz</artifactId>
<version>${revision}</version>
</dependency>
<!-- CRM 相关模块。默认注释,保证编译速度 -->
<!-- <dependency>-->