Merge branch 'develop' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17

# Conflicts:
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
This commit is contained in:
YunaiV
2024-09-30 09:04:03 +08:00
108 changed files with 2761 additions and 1611 deletions

View File

@@ -62,3 +62,8 @@ tenant-id: {{appTenentId}}
GET {{appApi}}/trade/order/get-express-track-list?id=70
Authorization: Bearer {{appToken}}
tenant-id: {{appTenentId}}
### /trade-order/settlement-product 获得商品结算信息
GET {{appApi}}/trade/order/settlement-product?spuIds=633
Authorization: Bearer {{appToken}}
tenant-id: {{appTenentId}}

View File

@@ -17,9 +17,11 @@ import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
import cn.iocoder.yudao.module.trade.service.price.TradePriceService;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
@@ -47,9 +49,10 @@ public class AppTradeOrderController {
private TradeOrderQueryService tradeOrderQueryService;
@Resource
private DeliveryExpressService deliveryExpressService;
@Resource
private AfterSaleService afterSaleService;
@Resource
private TradePriceService priceService;
@Resource
private TradeOrderProperties tradeOrderProperties;
@@ -61,6 +64,13 @@ public class AppTradeOrderController {
return success(tradeOrderUpdateService.settlementOrder(getLoginUserId(), settlementReqVO));
}
@GetMapping("/settlement-product")
@Operation(summary = "获得商品结算信息", description = "用于商品列表、商品详情,获得参与活动后的价格信息")
@Parameter(name = "spuIds", description = "商品 SPU 编号数组")
public CommonResult<List<AppTradeProductSettlementRespVO>> settlementProduct(@RequestParam("spuIds") List<Long> spuIds) {
return success(priceService.calculateProductPrice(getLoginUserId(), spuIds));
}
@PostMapping("/create")
@Operation(summary = "创建订单")
@PreAuthenticated
@@ -79,21 +89,32 @@ public class AppTradeOrderController {
@GetMapping("/get-detail")
@Operation(summary = "获得交易订单")
@Parameter(name = "id", description = "交易订单编号")
@Parameters({
@Parameter(name = "id", description = "交易订单编号"),
@Parameter(name = "sync", description = "是否同步支付状态", example = "true")
})
@PreAuthenticated
public CommonResult<AppTradeOrderDetailRespVO> getOrder(@RequestParam("id") Long id) {
// 查询订单
public CommonResult<AppTradeOrderDetailRespVO> getOrderDetail(@RequestParam("id") Long id,
@RequestParam(value = "sync", required = false) Boolean sync) {
// 1.1 查询订单
TradeOrderDO order = tradeOrderQueryService.getOrder(getLoginUserId(), id);
if (order == null) {
return success(null);
}
// 1.2 sync 仅在等待支付
if (Boolean.TRUE.equals(sync)
&& TradeOrderStatusEnum.isUnpaid(order.getStatus()) && !order.getPayStatus()) {
tradeOrderUpdateService.syncOrderPayStatusQuietly(order.getId(), order.getPayOrderId());
// 重新查询,因为同步后,可能会有变化
order = tradeOrderQueryService.getOrder(id);
}
// 查询订单项
// 2.1 查询订单项
List<TradeOrderItemDO> orderItems = tradeOrderQueryService.getOrderItemListByOrderId(order.getId());
// 查询物流公司
// 2.2 查询物流公司
DeliveryExpressDO express = order.getLogisticsId() != null && order.getLogisticsId() > 0 ?
deliveryExpressService.getDeliveryExpress(order.getLogisticsId()) : null;
// 最终组合
// 2.3 最终组合
return success(TradeOrderConvert.INSTANCE.convert02(order, orderItems, tradeOrderProperties, express));
}

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.trade.controller.app.order.vo;
import cn.iocoder.yudao.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -34,6 +35,13 @@ public class AppTradeOrderSettlementRespVO {
@Schema(description = "总积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer totalPoint;
/**
* 营销活动数组
*
* 只对应 {@link TradePriceCalculateRespBO.Price#items} 商品匹配的活动
*/
private List<TradePriceCalculateRespBO.Promotion> promotions;
@Schema(description = "购物项")
@Data
public static class Item {

View File

@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.trade.controller.app.order.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Schema(description = "用户 App - 商品结算信息 Response VO")
@Data
public class AppTradeProductSettlementRespVO {
@Schema(description = "SPU 商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long spuId;
@Schema(description = "SKU 价格信息数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private List<Sku> skus;
@Schema(description = "满减送活动信息", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private RewardActivity rewardActivity;
@Schema(description = "SKU 价格信息")
@Data
public static class Sku implements Serializable {
@Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "优惠后价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer promotionPrice;
@Schema(description = "营销类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
private Integer promotionType; // 对应 PromotionTypeEnum 枚举,目前只有 4 和 6 两种
@Schema(description = "营销编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long promotionId; // 目前只有限时折扣活动的编号
@Schema(description = "活动结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime promotionEndTime;
}
@Schema(description = "满减送活动信息")
@Data
public static class RewardActivity {
@Schema(description = "满减活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "条件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer conditionType;
@Schema(description = "优惠规则的数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<RewardActivityRule> rules;
}
@Schema(description = "优惠规则")
@Data
public static class RewardActivityRule {
@Schema(description = "优惠门槛", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 1. 满 N 元,单位:分; 2. 满 N 件
private Integer limit;
@Schema(description = "优惠价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer discountPrice;
@Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean freeDelivery;
@Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer point;
@Schema(description = "赠送的优惠劵编号的数组")
private Map<Long, Integer> giveCouponTemplateCounts;
}
}

View File

@@ -46,8 +46,7 @@ public interface AfterSaleConvert {
@Mapping(source = "afterSale.refundPrice", target = "price"),
@Mapping(source = "orderProperties.payAppKey", target = "appKey")
})
PayRefundCreateReqDTO convert(String userIp, AfterSaleDO afterSale,
TradeOrderProperties orderProperties);
PayRefundCreateReqDTO convert(String userIp, AfterSaleDO afterSale, TradeOrderProperties orderProperties);
MemberUserRespVO convert(MemberUserRespDTO bean);

View File

@@ -386,7 +386,7 @@ public class AfterSaleServiceImpl implements AfterSaleService {
public void afterCommit() {
// 创建退款单
PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties)
.setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));
.setReason(StrUtil.format("退款【{}】", afterSale.getSpuName()));;
Long payRefundId = payRefundApi.createRefund(createReqDTO);
// 更新售后单的退款单号
tradeAfterSaleMapper.updateById(new AfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));

View File

@@ -49,6 +49,17 @@ public interface TradeOrderUpdateService {
*/
void updateOrderPaid(Long id, Long payOrderId);
/**
* 同步订单的支付状态
*
* 1. Quietly 表示,即使同步失败,也不会抛出异常
* 2. 什么时候回出现异常?因为是主动同步,可能和支付模块的回调通知 {@link #updateOrderPaid(Long, Long)} 存在并发冲突,导致抛出异常
*
* @param id 订单编号
* @param payOrderId 支付订单编号
*/
void syncOrderPayStatusQuietly(Long id, Long payOrderId);
/**
* 【管理员】发货交易订单
*

View File

@@ -9,7 +9,6 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
@@ -166,7 +165,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
TradePriceCalculateReqBO calculateReqBO = TradeOrderConvert.INSTANCE.convert(userId, settlementReqVO, cartList);
calculateReqBO.getItems().forEach(item -> Assert.isTrue(item.getSelected(), // 防御性编程,保证都是选中的
"商品({}) 未设置为选中", item.getSkuId()));
return tradePriceService.calculatePrice(calculateReqBO);
return tradePriceService.calculateOrderPrice(calculateReqBO);
}
@Override
@@ -269,12 +268,24 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
@Transactional(rollbackFor = Exception.class)
@TradeOrderLog(operateType = TradeOrderOperateTypeEnum.MEMBER_PAY)
public void updateOrderPaid(Long id, Long payOrderId) {
// 1. 校验并获得交易订单(可支付)
KeyValue<TradeOrderDO, PayOrderRespDTO> orderResult = validateOrderPayable(id, payOrderId);
TradeOrderDO order = orderResult.getKey();
PayOrderRespDTO payOrder = orderResult.getValue();
// 1.1 校验订单是否存在
TradeOrderDO order = validateOrderExists(id);
// 1.2 校验订单已支付
if (!TradeOrderStatusEnum.isUnpaid(order.getStatus()) || order.getPayStatus()) {
// 特殊:如果订单已支付,且支付单号相同,直接返回,说明重复回调
if (ObjectUtil.equals(order.getPayOrderId(), payOrderId)) {
log.warn("[updateOrderPaid][order({}) 已支付,且支付单号相同({}),直接返回]", order, payOrderId);
return;
}
log.error("[updateOrderPaid][order({}) 支付单不匹配({})请进行处理order 数据是:{}]",
id, payOrderId, JsonUtils.toJsonString(order));
throw exception(ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR);
}
// 2. 更新 TradeOrderDO 状态为已支付,等待发货
// 2. 校验支付订单的合法性
PayOrderRespDTO payOrder = validatePayOrderPaid(order, payOrderId);
// 3. 更新 TradeOrderDO 状态为已支付,等待发货
int updateCount = tradeOrderMapper.updateByIdAndStatus(id, order.getStatus(),
new TradeOrderDO().setStatus(TradeOrderStatusEnum.UNDELIVERED.getStatus()).setPayStatus(true)
.setPayTime(LocalDateTime.now()).setPayChannelCode(payOrder.getChannelCode()));
@@ -282,66 +293,65 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
throw exception(ORDER_UPDATE_PAID_STATUS_NOT_UNPAID);
}
// 3. 执行 TradeOrderHandler 的后置处理
// 4. 执行 TradeOrderHandler 的后置处理
List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(id);
tradeOrderHandlers.forEach(handler -> handler.afterPayOrder(order, orderItems));
// 4. 记录订单日志
// 5. 记录订单日志
TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.UNDELIVERED.getStatus());
TradeOrderLogUtils.setUserInfo(order.getUserId(), UserTypeEnum.MEMBER.getValue());
}
/**
* 校验交易订单满足被支付的条件
* <p>
* 1. 交易订单未支付
* 2. 支付单已支付
*
* @param id 交易订单编号
* @param payOrderId 支付订单编号
* @return 交易订单
*/
private KeyValue<TradeOrderDO, PayOrderRespDTO> validateOrderPayable(Long id, Long payOrderId) {
// 校验订单是否存在
TradeOrderDO order = validateOrderExists(id);
// 校验订单未支付
if (!TradeOrderStatusEnum.isUnpaid(order.getStatus()) || order.getPayStatus()) {
log.error("[validateOrderPaid][order({}) 不处于待支付状态请进行处理order 数据是:{}]",
id, JsonUtils.toJsonString(order));
throw exception(ORDER_UPDATE_PAID_STATUS_NOT_UNPAID);
}
// 校验支付订单匹配
if (ObjectUtil.notEqual(order.getPayOrderId(), payOrderId)) { // 支付单号
log.error("[validateOrderPaid][order({}) 支付单不匹配({})请进行处理order 数据是:{}]",
id, payOrderId, JsonUtils.toJsonString(order));
throw exception(ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR);
}
// 校验支付单是否存在
@Override
public void syncOrderPayStatusQuietly(Long id, Long payOrderId) {
PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId);
if (payOrder == null) {
log.error("[validateOrderPaid][order({}) payOrder({}) 不存在,请进行处理!]", id, payOrderId);
return;
}
if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
return;
}
try {
getSelf().updateOrderPaid(id, payOrderId);
} catch (Throwable e) {
log.warn("[syncOrderPayStatusQuietly][id({}) payOrderId({}) 同步支付状态失败]", id, payOrderId, e);
}
}
/**
* 校验支付订单的合法性
*
* @param order 交易订单
* @param payOrderId 支付订单编号
* @return 支付订单
*/
private PayOrderRespDTO validatePayOrderPaid(TradeOrderDO order, Long payOrderId) {
// 1. 校验支付单是否存在
PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId);
if (payOrder == null) {
log.error("[validatePayOrderPaid][order({}) payOrder({}) 不存在,请进行处理!]", order.getId(), payOrderId);
throw exception(ORDER_NOT_FOUND);
}
// 校验支付单已支付
// 2.1 校验支付单已支付
if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
log.error("[validateOrderPaid][order({}) payOrder({}) 未支付请进行处理payOrder 数据是:{}]",
id, payOrderId, JsonUtils.toJsonString(payOrder));
log.error("[validatePayOrderPaid][order({}) payOrder({}) 未支付请进行处理payOrder 数据是:{}]",
order.getId(), payOrderId, JsonUtils.toJsonString(payOrder));
throw exception(ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS);
}
// 校验支付金额一致
// 2.2 校验支付金额一致
if (ObjectUtil.notEqual(payOrder.getPrice(), order.getPayPrice())) {
log.error("[validateOrderPaid][order({}) payOrder({}) 支付金额不匹配请进行处理order 数据是:{}payOrder 数据是:{}]",
id, payOrderId, JsonUtils.toJsonString(order), JsonUtils.toJsonString(payOrder));
log.error("[validatePayOrderPaid][order({}) payOrder({}) 支付金额不匹配请进行处理order 数据是:{}payOrder 数据是:{}]",
order.getId(), payOrderId, JsonUtils.toJsonString(order), JsonUtils.toJsonString(payOrder));
throw exception(ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH);
}
// 校验支付订单匹配(二次)
if (ObjectUtil.notEqual(payOrder.getMerchantOrderId(), id.toString())) {
log.error("[validateOrderPaid][order({}) 支付单不匹配({})请进行处理payOrder 数据是:{}]",
id, payOrderId, JsonUtils.toJsonString(payOrder));
// 2.2 校验支付订单匹配(二次)
if (ObjectUtil.notEqual(payOrder.getMerchantOrderId(), order.getId().toString())) {
log.error("[validatePayOrderPaid][order({}) 支付单不匹配({})请进行处理payOrder 数据是:{}]",
order.getId(), payOrderId, JsonUtils.toJsonString(payOrder));
throw exception(ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR);
}
return new KeyValue<>(order, payOrder);
return payOrder;
}
@Override

View File

@@ -1,10 +1,12 @@
package cn.iocoder.yudao.module.trade.service.price;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeProductSettlementRespVO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import jakarta.validation.Valid;
import java.util.List;
/**
* 价格计算 Service 接口
*
@@ -13,11 +15,20 @@ import jakarta.validation.Valid;
public interface TradePriceService {
/**
* 价格计算
* 【订单】价格计算
*
* @param calculateReqDTO 计算信息
* @return 计算结果
*/
TradePriceCalculateRespBO calculatePrice(@Valid TradePriceCalculateReqBO calculateReqDTO);
TradePriceCalculateRespBO calculateOrderPrice(@Valid TradePriceCalculateReqBO calculateReqDTO);
/**
* 【商品】价格计算,用于商品列表、商品详情
*
* @param userId 用户编号,允许为空
* @param spuIds 商品 SPU 编号数组
* @return 计算结果
*/
List<AppTradeProductSettlementRespVO> calculateProductPrice(Long userId, List<Long> spuIds);
}

View File

@@ -1,24 +1,33 @@
package cn.iocoder.yudao.module.trade.service.price;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.api.discount.DiscountActivityApi;
import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeProductSettlementRespVO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import cn.iocoder.yudao.module.trade.service.price.calculator.TradeDiscountActivityPriceCalculator;
import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculator;
import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_PAY_PRICE_ILLEGAL;
@@ -37,12 +46,19 @@ public class TradePriceServiceImpl implements TradePriceService {
private ProductSkuApi productSkuApi;
@Resource
private ProductSpuApi productSpuApi;
@Resource
private DiscountActivityApi discountActivityApi;
@Resource
private RewardActivityApi rewardActivityApi;
@Resource
private List<TradePriceCalculator> priceCalculators;
@Resource
private TradeDiscountActivityPriceCalculator discountActivityPriceCalculator;
@Override
public TradePriceCalculateRespBO calculatePrice(TradePriceCalculateReqBO calculateReqBO) {
public TradePriceCalculateRespBO calculateOrderPrice(TradePriceCalculateReqBO calculateReqBO) {
// 1.1 获得商品 SKU 数组
List<ProductSkuRespDTO> skuList = checkSkuList(calculateReqBO);
// 1.2 获得商品 SPU 数组
@@ -85,4 +101,55 @@ public class TradePriceServiceImpl implements TradePriceService {
return productSpuApi.validateSpuList(convertSet(skuList, ProductSkuRespDTO::getSpuId));
}
@Override
public List<AppTradeProductSettlementRespVO> calculateProductPrice(Long userId, List<Long> spuIds) {
// 1.1 获得 SPU 与 SKU 的映射
List<ProductSkuRespDTO> allSkuList = productSkuApi.getSkuListBySpuId(spuIds);
Map<Long, List<ProductSkuRespDTO>> spuIdAndSkuListMap = convertMultiMap(allSkuList, ProductSkuRespDTO::getSpuId);
// 1.2 获得会员等级
MemberLevelRespDTO level = discountActivityPriceCalculator.getMemberLevel(userId);
// 1.3 获得限时折扣活动
Map<Long, DiscountProductRespDTO> skuIdAndDiscountMap = convertMap(
discountActivityApi.getMatchDiscountProductListBySkuIds(convertSet(allSkuList, ProductSkuRespDTO::getId)),
DiscountProductRespDTO::getSkuId);
// 1.4 获得满减送活动
List<RewardActivityMatchRespDTO> rewardActivityMap = rewardActivityApi.getMatchRewardActivityListBySpuIds(spuIds);
// 2. 价格计算
return convertList(spuIds, spuId -> {
AppTradeProductSettlementRespVO spuVO = new AppTradeProductSettlementRespVO().setSpuId(spuId);
// 2.1 优惠价格
List<ProductSkuRespDTO> skuList = spuIdAndSkuListMap.get(spuId);
List<AppTradeProductSettlementRespVO.Sku> skuVOList = convertList(skuList, sku -> {
AppTradeProductSettlementRespVO.Sku skuVO = new AppTradeProductSettlementRespVO.Sku()
.setId(sku.getId()).setPromotionPrice(sku.getPrice());
TradePriceCalculateRespBO.OrderItem orderItem = new TradePriceCalculateRespBO.OrderItem()
.setPayPrice(sku.getPrice()).setCount(1);
// 计算限时折扣的优惠价格
DiscountProductRespDTO discountProduct = skuIdAndDiscountMap.get(sku.getId());
Integer discountPrice = discountActivityPriceCalculator.calculateActivityPrice(discountProduct, orderItem);
// 计算 VIP 优惠金额
Integer vipPrice = discountActivityPriceCalculator.calculateVipPrice(level, orderItem);
if (discountPrice <= 0 && vipPrice <= 0) {
return skuVO;
}
// 选择一个大的优惠
if (discountPrice > vipPrice) {
return skuVO.setPromotionPrice(sku.getPrice() - discountPrice)
.setPromotionType(PromotionTypeEnum.DISCOUNT_ACTIVITY.getType())
.setPromotionId(discountProduct.getId()).setPromotionEndTime(discountProduct.getActivityEndTime());
} else {
return skuVO.setPromotionPrice(sku.getPrice() - vipPrice)
.setPromotionType(PromotionTypeEnum.MEMBER_LEVEL.getType());
}
});
spuVO.setSkus(skuVOList);
// 2.2 满减送活动
RewardActivityMatchRespDTO rewardActivity = CollUtil.findOne(rewardActivityMap,
activity -> CollUtil.contains(activity.getSpuIds(), spuId));
spuVO.setRewardActivity(BeanUtils.toBean(rewardActivity, AppTradeProductSettlementRespVO.RewardActivity.class));
return spuVO;
});
}
}

View File

@@ -122,9 +122,9 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
*/
private boolean isGlobalExpressFree(TradePriceCalculateRespBO result) {
TradeConfigDO config = tradeConfigService.getTradeConfig();
return config != null
&& Boolean.TRUE.equals(config.getDeliveryExpressFreeEnabled()) // 开启包邮
&& result.getPrice().getPayPrice() >= config.getDeliveryExpressFreePrice(); // 满足包邮的价格
return config == null
|| Boolean.TRUE.equals(config.getDeliveryExpressFreeEnabled()) // 开启包邮
|| result.getPrice().getPayPrice() >= config.getDeliveryExpressFreePrice(); // 满足包邮的价格
}
private void calculateDeliveryPrice(List<OrderItem> selectedSkus,

View File

@@ -1,8 +1,11 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.api.discount.DiscountActivityApi;
import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
@@ -10,20 +13,23 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import jakarta.annotation.Resource;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.number.MoneyUtils.calculateRatePrice;
import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice;
/**
* 限时折扣的 {@link TradePriceCalculator} 实现类
*
* 由于“会员折扣”和“限时折扣”是冲突,需要选择优惠金额多的,所以也放在这里计算
*
* @author 芋道源码
*/
@Component
@@ -32,6 +38,10 @@ public class TradeDiscountActivityPriceCalculator implements TradePriceCalculato
@Resource
private DiscountActivityApi discountActivityApi;
@Resource
private MemberLevelApi memberLevelApi;
@Resource
private MemberUserApi memberUserApi;
@Override
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
@@ -39,51 +49,103 @@ public class TradeDiscountActivityPriceCalculator implements TradePriceCalculato
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
return;
}
// 获得 SKU 对应的限时折扣活动
List<DiscountProductRespDTO> discountProducts = discountActivityApi.getMatchDiscountProductList(
convertSet(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSkuId));
if (CollUtil.isEmpty(discountProducts)) {
return;
}
Map<Long, DiscountProductRespDTO> discountProductMap = convertMap(discountProducts, DiscountProductRespDTO::getSkuId);
// 处理每个 SKU 的限时折扣
// 1.1 获得 SKU 对应的限时折扣活动
List<DiscountProductRespDTO> discountProducts = discountActivityApi.getMatchDiscountProductListBySkuIds(
convertSet(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSkuId));
Map<Long, DiscountProductRespDTO> discountProductMap = convertMap(discountProducts, DiscountProductRespDTO::getSkuId);
// 1.2 获得会员等级
MemberLevelRespDTO level = getMemberLevel(param.getUserId());
// 2. 计算每个 SKU 的优惠金额
result.getItems().forEach(orderItem -> {
// 1. 获取该 SKU 的优惠信息
DiscountProductRespDTO discountProduct = discountProductMap.get(orderItem.getSkuId());
if (discountProduct == null) {
if (!orderItem.getSelected()) {
return;
}
// 2.1 计算限时折扣的优惠金额
DiscountProductRespDTO discountProduct = discountProductMap.get(orderItem.getSkuId());
Integer discountPrice = calculateActivityPrice(discountProduct, orderItem);
// 2.2 计算 VIP 优惠金额
Integer vipPrice = calculateVipPrice(level, orderItem);
if (discountPrice <= 0 && vipPrice <= 0) {
return;
}
// 2. 计算优惠金额
Integer newPayPrice = calculatePayPrice(discountProduct, orderItem);
Integer newDiscountPrice = orderItem.getPayPrice() - newPayPrice;
// 3.1 记录优惠明细
if (orderItem.getSelected()) {
// 注意,只有在选中的情况下,才会记录到优惠明细。否则仅仅是更新 SKU 优惠金额,用于展示
// 3. 选择优惠金额多的
if (discountPrice > vipPrice) {
TradePriceCalculatorHelper.addPromotion(result, orderItem,
discountProduct.getActivityId(), discountProduct.getActivityName(), PromotionTypeEnum.DISCOUNT_ACTIVITY.getType(),
StrUtil.format("限时折扣:省 {} 元", formatPrice(newDiscountPrice)),
newDiscountPrice);
StrUtil.format("限时折扣:省 {} 元", formatPrice(discountPrice)),
discountPrice);
// 更新 SKU 优惠金额
orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice);
} else {
assert level != null;
TradePriceCalculatorHelper.addPromotion(result, orderItem,
level.getId(), level.getName(), PromotionTypeEnum.MEMBER_LEVEL.getType(),
String.format("会员等级折扣:省 %s 元", formatPrice(vipPrice)),
vipPrice);
// 更新 SKU 的优惠金额
orderItem.setVipPrice(vipPrice);
}
// 3.2 更新 SKU 优惠金额
orderItem.setDiscountPrice(orderItem.getDiscountPrice() + newDiscountPrice);
// 4. 分摊优惠
TradePriceCalculatorHelper.recountPayPrice(orderItem);
TradePriceCalculatorHelper.recountAllPrice(result);
});
TradePriceCalculatorHelper.recountAllPrice(result);
}
private Integer calculatePayPrice(DiscountProductRespDTO discountProduct,
TradePriceCalculateRespBO.OrderItem orderItem) {
Integer price = orderItem.getPayPrice();
if (PromotionDiscountTypeEnum.PRICE.getType().equals(discountProduct.getDiscountType())) { // 减价
price -= discountProduct.getDiscountPrice() * orderItem.getCount();
} else if (PromotionDiscountTypeEnum.PERCENT.getType().equals(discountProduct.getDiscountType())) { // 打折
price = price * discountProduct.getDiscountPercent() / 100;
} else {
throw new IllegalArgumentException(String.format("优惠活动的商品(%s) 的优惠类型不正确", discountProduct));
/**
* 获得用户的等级
*
* @param userId 用户编号
* @return 用户等级
*/
public MemberLevelRespDTO getMemberLevel(Long userId) {
MemberUserRespDTO user = memberUserApi.getUser(userId);
if (user == null || user.getLevelId() == null || user.getLevelId() <= 0) {
return null;
}
return price;
return memberLevelApi.getMemberLevel(user.getLevelId());
}
/**
* 计算优惠活动的价格
*
* @param discount 优惠活动
* @param orderItem 交易项
* @return 优惠价格
*/
public Integer calculateActivityPrice(DiscountProductRespDTO discount,
TradePriceCalculateRespBO.OrderItem orderItem) {
if (discount == null) {
return 0;
}
Integer newPrice = orderItem.getPayPrice();
if (PromotionDiscountTypeEnum.PRICE.getType().equals(discount.getDiscountType())) { // 减价
newPrice -= discount.getDiscountPrice() * orderItem.getCount();
} else if (PromotionDiscountTypeEnum.PERCENT.getType().equals(discount.getDiscountType())) { // 打折
newPrice = calculateRatePrice(orderItem.getPayPrice(), discount.getDiscountPercent() / 100.0);
} else {
throw new IllegalArgumentException(String.format("优惠活动的商品(%s) 的优惠类型不正确", discount));
}
return orderItem.getPayPrice() - newPrice;
}
/**
* 计算会员 VIP 的优惠价格
*
* @param level 会员等级
* @param orderItem 交易项
* @return 优惠价格
*/
public Integer calculateVipPrice(MemberLevelRespDTO level,
TradePriceCalculateRespBO.OrderItem orderItem) {
if (level == null || level.getDiscountPercent() == null) {
return 0;
}
Integer newPrice = calculateRatePrice(orderItem.getPayPrice(), level.getDiscountPercent().doubleValue());
return orderItem.getPayPrice() - newPrice;
}
}

View File

@@ -1,88 +0,0 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice;
/**
* 会员 VIP 折扣的 {@link TradePriceCalculator} 实现类
*
* @author 芋道源码
*/
@Component
@Order(TradePriceCalculator.ORDER_MEMBER_LEVEL)
public class TradeMemberLevelPriceCalculator implements TradePriceCalculator {
@Resource
private MemberLevelApi memberLevelApi;
@Resource
private MemberUserApi memberUserApi;
@Override
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
// 0. 只有【普通】订单,才计算该优惠
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
return;
}
// 1. 获得用户的会员等级
MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
if (user.getLevelId() == null || user.getLevelId() <= 0) {
return;
}
MemberLevelRespDTO level = memberLevelApi.getMemberLevel(user.getLevelId());
if (level == null || level.getDiscountPercent() == null) {
return;
}
// 2. 计算每个 SKU 的优惠金额
result.getItems().forEach(orderItem -> {
// 2.1 计算优惠金额
Integer vipPrice = calculateVipPrice(orderItem.getPayPrice(), level.getDiscountPercent());
if (vipPrice <= 0) {
return;
}
// 2.2 记录优惠明细
if (orderItem.getSelected()) {
// 注意,只有在选中的情况下,才会记录到优惠明细。否则仅仅是更新 SKU 优惠金额,用于展示
TradePriceCalculatorHelper.addPromotion(result, orderItem,
level.getId(), level.getName(), PromotionTypeEnum.MEMBER_LEVEL.getType(),
String.format("会员等级折扣:省 %s 元", formatPrice(vipPrice)),
vipPrice);
}
// 2.3 更新 SKU 的优惠金额
orderItem.setVipPrice(vipPrice);
TradePriceCalculatorHelper.recountPayPrice(orderItem);
});
TradePriceCalculatorHelper.recountAllPrice(result);
}
/**
* 计算会员 VIP 优惠价格
*
* @param price 原价
* @param discountPercent 折扣
* @return 优惠价格
*/
public Integer calculateVipPrice(Integer price, Integer discountPercent) {
if (discountPercent == null) {
return 0;
}
Integer newPrice = price * discountPercent / 100;
return price - newPrice;
}
}

View File

@@ -13,8 +13,6 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
*/
public interface TradePriceCalculator {
int ORDER_MEMBER_LEVEL = 5;
int ORDER_SECKILL_ACTIVITY = 8;
int ORDER_BARGAIN_ACTIVITY = 8;
int ORDER_COMBINATION_ACTIVITY = 8;

View File

@@ -1,14 +1,11 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
@@ -17,8 +14,6 @@ import jakarta.annotation.Resource;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -47,14 +42,15 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
return;
}
// 获得 SKU 对应的满减送活动
List<RewardActivityMatchRespDTO> rewardActivities = rewardActivityApi.getMatchRewardActivityList(
List<RewardActivityMatchRespDTO> rewardActivities = rewardActivityApi.getMatchRewardActivityListBySpuIds(
convertSet(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSpuId));
if (CollUtil.isEmpty(rewardActivities)) {
return;
}
// 处理每个满减送活动
rewardActivities.forEach(rewardActivity -> calculate(param, result, rewardActivity));
// 处理最新的满减送活动
if (!rewardActivities.isEmpty()) {
calculate(param, result, rewardActivities.get(0));
}
}
private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result,
@@ -69,7 +65,7 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
if (rule == null) {
TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems,
rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(),
getRewardActivityNotMeetTip(rewardActivity, orderItems));
"满减送:" + rewardActivity.getRules().get(0).getDescription());
return;
}
@@ -77,6 +73,10 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
Integer newDiscountPrice = rule.getDiscountPrice();
// 2.2 计算分摊的优惠金额
List<Integer> divideDiscountPrices = TradePriceCalculatorHelper.dividePrice(orderItems, newDiscountPrice);
// 2.3 计算是否包邮
if (Boolean.TRUE.equals(rule.getFreeDelivery())) {
result.setFreeDelivery(true);
}
// 3.1 记录使用的优惠劵
result.setCouponId(param.getCouponId());
@@ -110,16 +110,8 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
// 4.3 记录赠送的优惠券
if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) {
for (Map.Entry<Long, Integer> entry : rule.getGiveCouponTemplateCounts().entrySet()) {
Map<Long, Integer> giveCouponTemplateCounts = result.getGiveCouponTemplateCounts();
// TODO @puhui999是不是有一种可能性这个 key 没有,别的 key 有哈。
// TODO 这里还有一种简化的写法。就是下面,大概两行就可以啦
// result.getGiveCouponTemplateCounts().put(entry.getKey(),
// result.getGiveCouponTemplateCounts().getOrDefault(entry.getKey(), 0) + entry.getValue());
if (giveCouponTemplateCounts.get(entry.getKey()) == null) { // 情况一:还没有赠送的优惠券
result.setGiveCouponTemplateCounts(rule.getGiveCouponTemplateCounts());
} else { // 情况二:别的满减活动送过同类优惠券,则直接增加数量
giveCouponTemplateCounts.put(entry.getKey(), giveCouponTemplateCounts.get(entry.getKey()) + entry.getValue());
}
result.getGiveCouponTemplateCounts().put(entry.getKey(),
result.getGiveCouponTemplateCounts().getOrDefault(entry.getKey(), 0) + entry.getValue());
}
}
}
@@ -133,28 +125,14 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
*/
private List<TradePriceCalculateRespBO.OrderItem> filterMatchActivityOrderItems(TradePriceCalculateRespBO result,
RewardActivityMatchRespDTO rewardActivity) {
// 情况一:全部商品都可以参与
if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
return result.getItems();
}
// 情况二:指定商品参与
if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
return filterList(result.getItems(),
orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getSpuId()));
}
// 情况三:指定商品类型参与
if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
return filterList(result.getItems(),
orderItem -> CollUtil.contains(rewardActivity.getProductScopeValues(), orderItem.getCategoryId()));
}
return ListUtil.of();
return filterList(result.getItems(), orderItem -> CollUtil.contains(rewardActivity.getSpuIds(), orderItem.getSpuId()));
}
/**
* 获得最大匹配的满减送活动的规则
*
* @param rewardActivity 满减送活动
* @param orderItems 商品项
* @param orderItems 商品项
* @return 匹配的活动规则
*/
private RewardActivityMatchRespDTO.Rule getMaxMatchRewardActivityRule(RewardActivityMatchRespDTO rewardActivity,
@@ -179,31 +157,4 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
return null;
}
/**
* 获得满减送活动不匹配时的提示
*
* @param rewardActivity 满减送活动
* @return 提示
*/
private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity,
List<TradePriceCalculateRespBO.OrderItem> orderItems) {
// 1. 计算数量和价格
Integer count = TradePriceCalculatorHelper.calculateTotalCount(orderItems);
Integer price = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
assert count != null && price != null;
// 2. 构建不满足时的提示信息:按最低档规则算
String meetTip = "满减送:购满 {} {},可以减 {} 元";
List<RewardActivityMatchRespDTO.Rule> rules = new ArrayList<>(rewardActivity.getRules());
rules.sort(Comparator.comparing(RewardActivityMatchRespDTO.Rule::getLimit)); // 按优惠门槛升序
RewardActivityMatchRespDTO.Rule rule = rules.get(0);
if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType())) {
return StrUtil.format(meetTip, rule.getLimit(), "", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
}
if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType())) {
return StrUtil.format(meetTip, rule.getLimit(), "", MoneyUtils.fenToYuanStr(rule.getDiscountPrice()));
}
return StrUtil.EMPTY;
}
}