trade:订单表,增加 vip 优惠字段

This commit is contained in:
YunaiV 2023-09-23 23:58:22 +08:00
parent aa5525d1f5
commit 53e789dfdc
14 changed files with 297 additions and 4 deletions

View File

@ -22,7 +22,7 @@ public enum PromotionTypeEnum implements IntArrayValuable {
DISCOUNT_ACTIVITY(4, "限时折扣"), DISCOUNT_ACTIVITY(4, "限时折扣"),
REWARD_ACTIVITY(5, "满减送"), REWARD_ACTIVITY(5, "满减送"),
MEMBER(6, "会员折扣"), MEMBER_LEVEL(6, "会员折扣"),
COUPON(7, "优惠劵") COUPON(7, "优惠劵")
; ;

View File

@ -65,6 +65,7 @@ public interface TradeOrderConvert {
@Mapping(source = "calculateRespBO.price.deliveryPrice", target = "deliveryPrice"), @Mapping(source = "calculateRespBO.price.deliveryPrice", target = "deliveryPrice"),
@Mapping(source = "calculateRespBO.price.couponPrice", target = "couponPrice"), @Mapping(source = "calculateRespBO.price.couponPrice", target = "couponPrice"),
@Mapping(source = "calculateRespBO.price.pointPrice", target = "pointPrice"), @Mapping(source = "calculateRespBO.price.pointPrice", target = "pointPrice"),
@Mapping(source = "calculateRespBO.price.vipPrice", target = "vipPrice"),
@Mapping(source = "calculateRespBO.price.payPrice", target = "payPrice") @Mapping(source = "calculateRespBO.price.payPrice", target = "payPrice")
}) })
TradeOrderDO convert(Long userId, String userIp, AppTradeOrderCreateReqVO createReqVO, TradeOrderDO convert(Long userId, String userIp, AppTradeOrderCreateReqVO createReqVO,

View File

@ -168,6 +168,7 @@ public class TradeOrderDO extends BaseDO {
* - {@link #discountPrice} * - {@link #discountPrice}
* + {@link #deliveryPrice} * + {@link #deliveryPrice}
* + {@link #adjustPrice} * + {@link #adjustPrice}
* - {@link #vipPrice}
*/ */
private Integer payPrice; private Integer payPrice;
@ -269,5 +270,9 @@ public class TradeOrderDO extends BaseDO {
// * 奖励的积分 TODO 疯狂可以使用这个字段哈 // * 奖励的积分 TODO 疯狂可以使用这个字段哈
// */ // */
// private Integer rewardPoint; // private Integer rewardPoint;
/**
* VIP 减免金额单位
*/
private Integer vipPrice;
} }

View File

@ -126,6 +126,7 @@ public class TradeOrderItemDO extends BaseDO {
* - {@link #discountPrice} * - {@link #discountPrice}
* + {@link #deliveryPrice} * + {@link #deliveryPrice}
* + {@link #adjustPrice} * + {@link #adjustPrice}
* - {@link #vipPrice}
*/ */
private Integer payPrice; private Integer payPrice;
@ -143,6 +144,10 @@ public class TradeOrderItemDO extends BaseDO {
* 对应 taobao trade.point_fee 字段 * 对应 taobao trade.point_fee 字段
*/ */
private Integer pointPrice; private Integer pointPrice;
/**
* VIP 减免金额单位
*/
private Integer vipPrice;
// TODO @芋艿如果商品 vip 折扣时到底是新增一个 vipPrice 记录优惠记录还是 vipDiscountPrice记录 vip 的优惠还是直接使用 vipPrice // TODO @芋艿如果商品 vip 折扣时到底是新增一个 vipPrice 记录优惠记录还是 vipDiscountPrice记录 vip 的优惠还是直接使用 vipPrice
// 目前 crmeb 的选择单独一个 vipPrice 记录优惠价格感觉不一定合理可以在看看有赞的 // 目前 crmeb 的选择单独一个 vipPrice 记录优惠价格感觉不一定合理可以在看看有赞的

View File

@ -89,6 +89,10 @@ public class TradePriceCalculateRespBO {
* 对应 taobao trade.point_fee 字段 * 对应 taobao trade.point_fee 字段
*/ */
private Integer pointPrice; private Integer pointPrice;
/**
* VIP 减免金额单位
*/
private Integer vipPrice;
/** /**
* 最终购买金额单位 * 最终购买金额单位
* *
@ -97,6 +101,7 @@ public class TradePriceCalculateRespBO {
* - {@link #pointPrice} * - {@link #pointPrice}
* - {@link #discountPrice} * - {@link #discountPrice}
* + {@link #deliveryPrice} * + {@link #deliveryPrice}
* - {@link #vipPrice}
*/ */
private Integer payPrice; private Integer payPrice;
@ -158,6 +163,10 @@ public class TradePriceCalculateRespBO {
* 对应 taobao trade.point_fee 字段 * 对应 taobao trade.point_fee 字段
*/ */
private Integer pointPrice; private Integer pointPrice;
/**
* VIP 减免金额单位
*/
private Integer vipPrice;
/** /**
* 应付金额单位 * 应付金额单位
* *
@ -166,6 +175,7 @@ public class TradePriceCalculateRespBO {
* - {@link #pointPrice} * - {@link #pointPrice}
* - {@link #discountPrice} * - {@link #discountPrice}
* + {@link #deliveryPrice} * + {@link #deliveryPrice}
* - {@link #vipPrice}
*/ */
private Integer payPrice; private Integer payPrice;

View File

@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
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.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 javax.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) {
// 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,6 +13,7 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
*/ */
public interface TradePriceCalculator { public interface TradePriceCalculator {
int ORDER_MEMBER_LEVEL = 5;
int ORDER_DISCOUNT_ACTIVITY = 10; int ORDER_DISCOUNT_ACTIVITY = 10;
int ORDER_REWARD_ACTIVITY = 20; int ORDER_REWARD_ACTIVITY = 20;
int ORDER_COUPON = 30; int ORDER_COUPON = 30;

View File

@ -52,7 +52,7 @@ public class TradePriceCalculatorHelper {
.setCount(item.getCount()).setCartId(item.getCartId()).setSelected(item.getSelected()); .setCount(item.getCount()).setCartId(item.getCartId()).setSelected(item.getSelected());
// sku 价格 // sku 价格
orderItem.setPrice(sku.getPrice()).setPayPrice(sku.getPrice() * item.getCount()) orderItem.setPrice(sku.getPrice()).setPayPrice(sku.getPrice() * item.getCount())
.setDiscountPrice(0).setDeliveryPrice(0).setCouponPrice(0).setPointPrice(0); .setDiscountPrice(0).setDeliveryPrice(0).setCouponPrice(0).setPointPrice(0).setVipPrice(0);
// sku 信息 // sku 信息
orderItem.setPicUrl(sku.getPicUrl()).setProperties(sku.getProperties()) orderItem.setPicUrl(sku.getPicUrl()).setProperties(sku.getProperties())
.setWeight(sku.getWeight()).setVolume(sku.getVolume()); .setWeight(sku.getWeight()).setVolume(sku.getVolume());
@ -96,7 +96,7 @@ public class TradePriceCalculatorHelper {
// 先重置 // 先重置
TradePriceCalculateRespBO.Price price = result.getPrice(); TradePriceCalculateRespBO.Price price = result.getPrice();
price.setTotalPrice(0).setDiscountPrice(0).setDeliveryPrice(0) price.setTotalPrice(0).setDiscountPrice(0).setDeliveryPrice(0)
.setCouponPrice(0).setPointPrice(0).setPayPrice(0); .setCouponPrice(0).setPointPrice(0).setVipPrice(0).setPayPrice(0);
// 再合计 item // 再合计 item
result.getItems().forEach(item -> { result.getItems().forEach(item -> {
if (!item.getSelected()) { if (!item.getSelected()) {
@ -107,6 +107,7 @@ public class TradePriceCalculatorHelper {
price.setDeliveryPrice(price.getDeliveryPrice() + item.getDeliveryPrice()); price.setDeliveryPrice(price.getDeliveryPrice() + item.getDeliveryPrice());
price.setCouponPrice(price.getCouponPrice() + item.getCouponPrice()); price.setCouponPrice(price.getCouponPrice() + item.getCouponPrice());
price.setPointPrice(price.getPointPrice() + item.getPointPrice()); price.setPointPrice(price.getPointPrice() + item.getPointPrice());
price.setVipPrice(price.getVipPrice() + item.getVipPrice());
price.setPayPrice(price.getPayPrice() + item.getPayPrice()); price.setPayPrice(price.getPayPrice() + item.getPayPrice());
}); });
} }
@ -121,7 +122,9 @@ public class TradePriceCalculatorHelper {
- orderItem.getDiscountPrice() - orderItem.getDiscountPrice()
+ orderItem.getDeliveryPrice() + orderItem.getDeliveryPrice()
- orderItem.getCouponPrice() - orderItem.getCouponPrice()
- orderItem.getPointPrice()); - orderItem.getPointPrice()
- orderItem.getVipPrice()
);
} }
/** /**
@ -145,6 +148,9 @@ public class TradePriceCalculatorHelper {
if (orderItem.getPointPrice() == null) { if (orderItem.getPointPrice() == null) {
orderItem.setPointPrice(0); orderItem.setPointPrice(0);
} }
if (orderItem.getVipPrice() == null) {
orderItem.setVipPrice(0);
}
recountPayPrice(orderItem); recountPayPrice(orderItem);
}); });
} }

View File

@ -0,0 +1,116 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.ArrayList;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
/**
* {@link TradeMemberLevelPriceCalculator} 的单元测试类
*
* @author 芋道源码
*/
public class TradeMemberLevelPriceCalculatorTest extends BaseMockitoUnitTest {
@InjectMocks
private TradeMemberLevelPriceCalculator memberLevelPriceCalculator;
@Mock
private MemberLevelApi memberLevelApi;
@Mock
private MemberUserApi memberUserApi;
@Test
public void testCalculate() {
// 准备参数
TradePriceCalculateReqBO param = new TradePriceCalculateReqBO()
.setUserId(1024L)
.setItems(asList(
new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true), // 匹配活动且已选中
new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(false) // 匹配活动但未选中
));
TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
.setPrice(new TradePriceCalculateRespBO.Price())
.setPromotions(new ArrayList<>())
.setItems(asList(
new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
.setPrice(100),
new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(false)
.setPrice(50)
));
// 保证价格被初始化上
TradePriceCalculatorHelper.recountPayPrice(result.getItems());
TradePriceCalculatorHelper.recountAllPrice(result);
// mock 方法会员等级
when(memberUserApi.getUser(eq(1024L))).thenReturn(new MemberUserRespDTO().setLevelId(2048L));
when(memberLevelApi.getMemberLevel(eq(2048L))).thenReturn(
new MemberLevelRespDTO().setId(2048L).setName("VIP 会员").setDiscountPercent(60));
// 调用
memberLevelPriceCalculator.calculate(param, result);
// 断言Price 部分
TradePriceCalculateRespBO.Price price = result.getPrice();
assertEquals(price.getTotalPrice(), 200);
assertEquals(price.getDiscountPrice(), 0);
assertEquals(price.getPointPrice(), 0);
assertEquals(price.getDeliveryPrice(), 0);
assertEquals(price.getCouponPrice(), 0);
assertEquals(price.getVipPrice(), 80);
assertEquals(price.getPayPrice(), 120);
assertNull(result.getCouponId());
// 断言SKU 1
assertEquals(result.getItems().size(), 2);
TradePriceCalculateRespBO.OrderItem orderItem01 = result.getItems().get(0);
assertEquals(orderItem01.getSkuId(), 10L);
assertEquals(orderItem01.getCount(), 2);
assertEquals(orderItem01.getPrice(), 100);
assertEquals(orderItem01.getDiscountPrice(), 0);
assertEquals(orderItem01.getDeliveryPrice(), 0);
assertEquals(orderItem01.getCouponPrice(), 0);
assertEquals(orderItem01.getPointPrice(), 0);
assertEquals(orderItem01.getVipPrice(), 80);
assertEquals(orderItem01.getPayPrice(), 120);
// 断言SKU 2
TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1);
assertEquals(orderItem02.getSkuId(), 20L);
assertEquals(orderItem02.getCount(), 3);
assertEquals(orderItem02.getPrice(), 50);
assertEquals(orderItem02.getDiscountPrice(), 0);
assertEquals(orderItem02.getDeliveryPrice(), 0);
assertEquals(orderItem02.getCouponPrice(), 0);
assertEquals(orderItem02.getPointPrice(), 0);
assertEquals(orderItem02.getVipPrice(), 60);
assertEquals(orderItem02.getPayPrice(), 90);
// 断言Promotion 部分
assertEquals(result.getPromotions().size(), 1);
TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
assertEquals(promotion01.getId(), 2048L);
assertEquals(promotion01.getName(), "VIP 会员");
assertEquals(promotion01.getType(), PromotionTypeEnum.MEMBER_LEVEL.getType());
assertEquals(promotion01.getTotalPrice(), 200);
assertEquals(promotion01.getDiscountPrice(), 80);
assertTrue(promotion01.getMatch());
assertEquals(promotion01.getDescription(), "会员等级折扣:省 0.80 元");
TradePriceCalculateRespBO.PromotionItem promotionItem01 = promotion01.getItems().get(0);
assertEquals(promotion01.getItems().size(), 1);
assertEquals(promotionItem01.getSkuId(), 10L);
assertEquals(promotionItem01.getTotalPrice(), 200);
assertEquals(promotionItem01.getDiscountPrice(), 80);
}
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.member.api.level; package cn.iocoder.yudao.module.member.api.level;
import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum; import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
/** /**
@ -9,6 +10,14 @@ import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
*/ */
public interface MemberLevelApi { public interface MemberLevelApi {
/**
* 获得会员等级
*
* @param id 会员等级编号
* @return 会员等级
*/
MemberLevelRespDTO getMemberLevel(Long id);
/** /**
* 增加会员经验 * 增加会员经验
* *

View File

@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.member.api.level.dto;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import lombok.Data;
/**
* 会员等级 Resp DTO
*
* @author 芋道源码
*/
@Data
public class MemberLevelRespDTO {
/**
* 编号
*/
private Long id;
/**
* 等级名称
*/
private String name;
/**
* 等级
*/
private Integer level;
/**
* 升级经验
*/
private Integer experience;
/**
* 享受折扣
*/
private Integer discountPercent;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
}

View File

@ -34,4 +34,11 @@ public class MemberUserRespDTO {
*/ */
private String mobile; private String mobile;
// ========== 其它信息 ==========
/**
* 会员级别编号
*/
private Long levelId;
} }

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.member.api.level; package cn.iocoder.yudao.module.member.api.level;
import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
import cn.iocoder.yudao.module.member.convert.level.MemberLevelConvert;
import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum; import cn.iocoder.yudao.module.member.enums.MemberExperienceBizTypeEnum;
import cn.iocoder.yudao.module.member.service.level.MemberLevelService; import cn.iocoder.yudao.module.member.service.level.MemberLevelService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -22,6 +24,11 @@ public class MemberLevelApiImpl implements MemberLevelApi {
@Resource @Resource
private MemberLevelService memberLevelService; private MemberLevelService memberLevelService;
@Override
public MemberLevelRespDTO getMemberLevel(Long id) {
return MemberLevelConvert.INSTANCE.convert02(memberLevelService.getLevel(id));
}
@Override @Override
public void addExperience(Long userId, Integer experience, Integer bizType, String bizId) { public void addExperience(Long userId, Integer experience, Integer bizType, String bizId) {
MemberExperienceBizTypeEnum bizTypeEnum = MemberExperienceBizTypeEnum.getByType(bizType); MemberExperienceBizTypeEnum bizTypeEnum = MemberExperienceBizTypeEnum.getByType(bizType);

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.member.convert.level; package cn.iocoder.yudao.module.member.convert.level;
import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
import cn.iocoder.yudao.module.member.controller.admin.level.vo.level.MemberLevelCreateReqVO; import cn.iocoder.yudao.module.member.controller.admin.level.vo.level.MemberLevelCreateReqVO;
import cn.iocoder.yudao.module.member.controller.admin.level.vo.level.MemberLevelRespVO; import cn.iocoder.yudao.module.member.controller.admin.level.vo.level.MemberLevelRespVO;
import cn.iocoder.yudao.module.member.controller.admin.level.vo.level.MemberLevelSimpleRespVO; import cn.iocoder.yudao.module.member.controller.admin.level.vo.level.MemberLevelSimpleRespVO;
@ -33,4 +34,6 @@ public interface MemberLevelConvert {
List<AppMemberLevelRespVO> convertList02(List<MemberLevelDO> list); List<AppMemberLevelRespVO> convertList02(List<MemberLevelDO> list);
MemberLevelRespDTO convert02(MemberLevelDO bean);
} }