将 market 模块,调整成 promotion 模块

This commit is contained in:
YunaiV
2022-10-30 17:37:07 +08:00
parent 829bbf58b5
commit e6acdf7098
64 changed files with 152 additions and 155 deletions

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-mall</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>yudao-module-promotion-biz</artifactId>
<name>${project.artifactId}</name>
<description>
market模块主要实现营销相关功能
例如营销活动、banner广告、优惠券、优惠码等功能。
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-promotion-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-product-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-excel</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1 @@
package cn.iocoder.yudao.module.promotion.api.discount;

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.promotion.api.price;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
import cn.iocoder.yudao.module.promotion.service.price.PriceService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 价格 API 实现类
*
* @author 芋道源码
*/
@Service
public class PriceApiImpl implements PriceApi {
@Resource
private PriceService priceService;
@Override
public PriceCalculateRespDTO calculatePrice(PriceCalculateReqDTO calculateReqDTO) {
return priceService.calculatePrice(calculateReqDTO);
}
}

View File

@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.promotion.controller.admin.banner;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.*;
import cn.iocoder.yudao.module.promotion.convert.banner.BannerConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.banner.BannerDO;
import cn.iocoder.yudao.module.promotion.service.banner.BannerService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Api(tags = "管理后台 - Banner 管理")
@RestController
@RequestMapping("/market/banner")
@Validated
public class BannerController {
@Resource
private BannerService bannerService;
@PostMapping("/create")
@ApiOperation("创建 Banner")
@PreAuthorize("@ss.hasPermission('market:banner:create')")
public CommonResult<Long> createBanner(@Valid @RequestBody BannerCreateReqVO createReqVO) {
return success(bannerService.createBanner(createReqVO));
}
@PutMapping("/update")
@ApiOperation("更新 Banner")
@PreAuthorize("@ss.hasPermission('market:banner:update')")
public CommonResult<Boolean> updateBanner(@Valid @RequestBody BannerUpdateReqVO updateReqVO) {
bannerService.updateBanner(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除 Banner")
@ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('market:banner:delete')")
public CommonResult<Boolean> deleteBanner(@RequestParam("id") Long id) {
bannerService.deleteBanner(id);
return success(true);
}
@GetMapping("/get")
@ApiOperation("获得 Banner")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('market:banner:query')")
public CommonResult<BannerRespVO> getBanner(@RequestParam("id") Long id) {
BannerDO banner = bannerService.getBanner(id);
return success(BannerConvert.INSTANCE.convert(banner));
}
@GetMapping("/page")
@ApiOperation("获得 Banner 分页")
@PreAuthorize("@ss.hasPermission('market:banner:query')")
public CommonResult<PageResult<BannerRespVO>> getBannerPage(@Valid BannerPageReqVO pageVO) {
PageResult<BannerDO> pageResult = bannerService.getBannerPage(pageVO);
return success(BannerConvert.INSTANCE.convertPage(pageResult));
}
}

View File

@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.promotion.controller.admin.banner.vo;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* Banner Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
* @author xia
*/
@Data
public class BannerBaseVO {
@ApiModelProperty(value = "标题", required = true)
@NotNull(message = "标题不能为空")
private String title;
@ApiModelProperty(value = "跳转链接", required = true)
@NotNull(message = "跳转链接不能为空")
private String url;
@ApiModelProperty(value = "图片地址", required = true)
@NotNull(message = "图片地址不能为空")
private String picUrl;
@ApiModelProperty(value = "排序", required = true)
@NotNull(message = "排序不能为空")
private Integer sort;
@ApiModelProperty(value = "状态", required = true)
@NotNull(message = "状态不能为空")
@InEnum(CommonStatusEnum.class)
private Integer status;
@ApiModelProperty(value = "备注")
private String memo;
}

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.promotion.controller.admin.banner.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* @author xia
*/
@ApiModel("管理后台 - Banner 创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class BannerCreateReqVO extends BannerBaseVO {
}

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.promotion.controller.admin.banner.vo;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
/**
* @author xia
*/
@ApiModel("管理后台 - Banner 分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class BannerPageReqVO extends PageParam {
@ApiModelProperty(value = "标题")
private String title;
@ApiModelProperty(value = "状态")
@InEnum(CommonStatusEnum.class)
private Integer status;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "创建时间")
private Date[] createTime;
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.promotion.controller.admin.banner.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.ToString;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
* @author xia
*/
@ApiModel("管理后台 - Banner Response VO")
@Data
@ToString(callSuper = true)
public class BannerRespVO extends BannerBaseVO {
@ApiModelProperty(value = "banner编号", required = true)
@NotNull(message = "banner编号不能为空")
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.promotion.controller.admin.banner.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotNull;
/**
* @author xia
*/
@ApiModel("管理后台 - Banner更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class BannerUpdateReqVO extends BannerBaseVO {
@ApiModelProperty(value = "banner 编号", required = true)
@NotNull(message = "banner 编号不能为空")
private Long id;
}

View File

@@ -0,0 +1,4 @@
/**
* TODO 占位
*/
package cn.iocoder.yudao.module.promotion.controller.admin.discount;

View File

@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.promotion.controller.app;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Api(tags = "用户 App - 营销")
@RestController
@RequestMapping("/market/test")
@Validated
public class AppMarketTestController {
@GetMapping("/get")
@ApiOperation("获取 market 信息")
public CommonResult<String> get() {
return success("true");
}
}

View File

@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.promotion.controller.app.banner;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerRespVO;
import cn.iocoder.yudao.module.promotion.convert.banner.BannerConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.banner.BannerDO;
import cn.iocoder.yudao.module.promotion.service.banner.BannerService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* @author: XIA
*/
@RestController
@RequestMapping("/market/banner")
@Api(tags = "用户APP- 首页Banner")
@Validated
public class AppBannerController {
@Resource
private BannerService bannerService;
// TODO @xia新建一个 AppBannerRespVO只返回必要的字段。status 要过滤下。然后 sort 下结果
@GetMapping("/list")
@ApiOperation("获得banner列表")
@PreAuthorize("@ss.hasPermission('market:banner:query')")
public CommonResult<List<BannerRespVO>> getBannerList() {
List<BannerDO> list = bannerService.getBannerList();
return success(BannerConvert.INSTANCE.convertList(list));
}
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.promotion.convert.banner;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerUpdateReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.banner.BannerDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* Banner Convert
*
* @author xia
*/
@Mapper
public interface BannerConvert {
BannerConvert INSTANCE = Mappers.getMapper(BannerConvert.class);
List<BannerRespVO> convertList(List<BannerDO> list);
PageResult<BannerRespVO> convertPage(PageResult<BannerDO> pageResult);
BannerRespVO convert(BannerDO banner);
BannerDO convert(BannerCreateReqVO createReqVO);
BannerDO convert(BannerUpdateReqVO updateReqVO);
}

View File

@@ -0,0 +1,4 @@
/**
* TODO 占位
*/
package cn.iocoder.yudao.module.promotion.convert.discount;

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.promotion.convert.price;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Mapper
public interface PriceConvert {
PriceConvert INSTANCE = Mappers.getMapper(PriceConvert.class);
default PriceCalculateRespDTO convert(PriceCalculateReqDTO calculateReqDTO, List<ProductSkuRespDTO> skuList) {
// 创建 PriceCalculateRespDTO 对象
PriceCalculateRespDTO priceCalculate = new PriceCalculateRespDTO();
// 创建它的 Order 属性
PriceCalculateRespDTO.Order order = new PriceCalculateRespDTO.Order().setOriginalPrice(0).setDiscountPrice(0)
.setCouponPrice(0).setPointPrice(0).setDeliveryPrice(0).setPayPrice(0)
.setItems(new ArrayList<>()).setCouponId(calculateReqDTO.getCouponId());
priceCalculate.setOrder(order).setPromotions(new ArrayList<>());
// 创建它的 OrderItem 属性
Map<Long, Integer> skuIdCountMap = CollectionUtils.convertMap(calculateReqDTO.getItems(),
PriceCalculateReqDTO.Item::getSkuId, PriceCalculateReqDTO.Item::getCount);
skuList.forEach(sku -> {
Integer count = skuIdCountMap.get(sku.getId());
PriceCalculateRespDTO.OrderItem orderItem = new PriceCalculateRespDTO.OrderItem()
.setSpuId(sku.getSpuId()).setSkuId(sku.getId()).setCount(count)
.setOriginalUnitPrice(sku.getPrice()).setOriginalPrice(sku.getPrice() * count)
.setDiscountPrice(0).setOrderPartPrice(0);
orderItem.setPayPrice(orderItem.getOriginalPrice()).setOrderDividePrice(orderItem.getOriginalPrice());
priceCalculate.getOrder().getItems().add(orderItem);
// 补充价格信息到 Order 中
order.setOriginalPrice(order.getOriginalPrice() + orderItem.getOriginalPrice()).setPayPrice(order.getOriginalPrice());
});
return priceCalculate;
}
}

View File

@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.banner;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* banner DO
*
* @author xia
*/
@TableName("market_banner")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BannerDO extends BaseDO {
/**
* 编号
*/
private Long id;
/**
* 标题
*/
private String title;
/**
* 跳转链接
*/
private String url;
/**
* 图片链接
*/
private String picUrl;
/**
* 排序
*/
private Integer sort;
/**
* 状态 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
*/
private Integer status;
/**
* 备注
*/
private String memo;
// TODO 芋艿 点击次数。&& 其他数据相关
}

View File

@@ -0,0 +1,139 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.coupon;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
import java.util.List;
// TODO 芋艿:待完善该实体
/**
* 优惠劵 DO
*/
@TableName("coupon")
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class CouponDO extends BaseDO {
// ========== 基本信息 BEGIN ==========
/**
* 优惠劵编号
*/
private Long id;
/**
* 优惠劵(码)分组编号,{@link CouponTemplateDO} 的 id
*/
private Integer templateId;
/**
* 优惠劵名
*
* 冗余自 {@link CouponTemplateDO} 的 title
*
* TODO 芋艿,暂时不考虑冗余的更新
*/
private String title;
// /**
// * 核销码
// */
// private String verifyCode;
/**
* 优惠码状态
*
* 1-未使用
* 2-已使用
* 3-已失效
*/
private Integer status;
// ========== 基本信息 END ==========
// ========== 领取情况 BEGIN ==========
/**
* 用户编号
*/
private Integer userId;
/**
* 领取类型
*
* 1 - 用户主动领取
* 2 - 后台自动发放
*/
private Integer takeType;
// ========== 领取情况 END ==========
// ========== 使用规则 BEGIN ==========
/**
* 是否设置满多少金额可用,单位:分
*/
private Integer priceAvailable;
/**
* 生效开始时间
*/
private Date validStartTime;
/**
* 生效结束时间
*/
private Date validEndTime;
/**
* 商品范围
*
* 枚举 {@link PromotionProductScopeEnum}
*/
private Integer productScope;
/**
* 商品 SPU 编号的数组
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<Long> spuIds;
// ========== 使用规则 END ==========
// ========== 使用效果 BEGIN ==========
/**
* 优惠类型
*
* 1-代金卷
* 2-折扣卷
*/
private Integer preferentialType;
/**
* 折扣
*/
private Integer percentOff;
/**
* 优惠金额,单位:分。
*/
private Integer priceOff;
/**
* 折扣上限,仅在 {@link #preferentialType} 等于 2 时生效。
*
* 例如,折扣上限为 20 元,当使用 8 折优惠券,订单金额为 1000 元时,最高只可折扣 20 元,而非 80 元。
*/
private Integer discountPriceLimit;
// ========== 使用效果 END ==========
// ========== 使用情况 BEGIN ==========
// /**
// * 使用订单号
// */
// private Integer usedOrderId; // TODO 芋艿,暂时不考虑这个字段
// /**
// * 订单中优惠面值,单位:分
// */
// private Integer usedPrice; // TODO 芋艿,暂时不考虑这个字段
/**
* 使用时间
*/
private Date usedTime;
// TODO 芋艿,后续要加优惠劵的使用日志,因为下单后,可能会取消。
// ========== 使用情况 END ==========
}

View File

@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.discount;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 限时折扣活动 DO
*
* 一个活动下,可以有 {@link DiscountProductDO} 商品;
* 一个商品,在指定时间段内,只能属于一个活动;
*
* @author 芋道源码
*/
@TableName(value = "promotion_discount_activity", autoResultMap = true)
@KeySequence("promotion_discount_activity_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
public class DiscountActivityDO extends BaseDO {
/**
* 活动编号,主键自增
*/
@TableId
private Long id;
/**
* 活动标题
*/
private String name;
/**
* 状态
*
* 枚举 {@link PromotionActivityStatusEnum}
*/
private Integer status;
/**
* 开始时间
*/
private Date startTime;
/**
* 结束时间
*/
private Date endTime;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.discount;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 限时折扣商品 DO
*
* @author 芋道源码
*/
@TableName(value = "promotion_discount_product", autoResultMap = true)
@KeySequence("promotion_discount_product_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
public class DiscountProductDO extends BaseDO {
/**
* 编号,主键自增
*/
@TableId
private Long id;
/**
* 限时折扣活动的编号
*
* 关联 {@link DiscountActivityDO#getId()}
*/
private Long activityId;
/**
* 限时折扣活动的名字
*
* 冗余 {@link DiscountActivityDO#getName()}
*/
private String activityName;
/**
* 商品 SPU 编号
*
* 关联 ProductSpuDO 的 id 编号
*/
private Long spuId;
/**
* 商品 SKU 编号
*
* 关联 ProductSkuDO 的 id 编号
*/
private Long skuId;
/**
* 开始时间
*/
private Date startTime;
/**
* 结束时间
*/
private Date endTime;
/**
* 销售价格,单位:分
*
* 冗余 ProductSkuDO 的 price 字段
*/
private Integer originalPrice;
/**
* 优惠价格,单位:分
*/
private Integer promotionPrice;
}

View File

@@ -0,0 +1,116 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.reward;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.List;
/**
* 满减送活动 DO
*
* @author 芋道源码
*/
@TableName(value = "promotion_reward_activity", autoResultMap = true)
@KeySequence("promotion_reward_activity_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
public class RewardActivityDO extends BaseDO {
/**
* 活动编号,主键自增
*/
@TableId
private Long id;
/**
* 活动标题
*/
private String name;
/**
* 状态
*
* 枚举 {@link PromotionActivityStatusEnum}
*/
private Integer status;
/**
* 开始时间
*/
private Date startTime;
/**
* 结束时间
*/
private Date endTime;
/**
* 备注
*/
private String remark;
/**
* 条件类型
*
* 枚举 {@link PromotionConditionTypeEnum}
*/
private Integer conditionType;
/**
* 商品范围
*
* 枚举 {@link PromotionProductScopeEnum}
*/
private Integer productScope;
/**
* 商品 SPU 编号的数组
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<Long> spuIds;
/**
* 优惠规则的数组
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<Rule> rules;
/**
* 优惠规则
*/
@Data
public static class Rule {
/**
* 优惠门槛
*
* 1. 满 N 元,单位:分
* 2. 满 N 件
*/
private Integer limit;
/**
* 优惠价格,单位:分
*/
private Integer discountPrice;
/**
* 是否包邮
*/
private Boolean freeDelivery;
/**
* 赠送的积分
*/
private Integer point;
/**
* 赠送的优惠劵编号的数组
*/
private List<Long> couponIds;
/**
* 赠送的优惠卷数量的数组
*/
private List<Integer> couponCounts;
}
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.banner;
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.promotion.controller.admin.banner.vo.BannerPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.banner.BannerDO;
import org.apache.ibatis.annotations.Mapper;
/**
* Banner Mapper
*
* @author xia
*/
@Mapper
public interface BannerMapper extends BaseMapperX<BannerDO> {
default PageResult<BannerDO> selectPage(BannerPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<BannerDO>()
.likeIfPresent(BannerDO::getTitle, reqVO.getTitle())
.eqIfPresent(BannerDO::getStatus, reqVO.getStatus())
.betweenIfPresent(BannerDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(BannerDO::getSort));
}
}

View File

@@ -0,0 +1,4 @@
/**
* TODO 占位
*/
package cn.iocoder.yudao.module.promotion.dal.mysql.discount;

View File

@@ -0,0 +1,8 @@
/**
* promotion 模块,我们放营销业务。
* 例如说营销活动、banner、优惠券等等
*
* 1. Controller URL以 /promotion/ 开头,避免和其它 Module 冲突
* 2. DataObject 表名:以 promotion_ 开头,方便在数据库中区分
*/
package cn.iocoder.yudao.module.promotion;

View File

@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.promotion.service.banner;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerUpdateReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.banner.BannerDO;
import javax.validation.Valid;
import java.util.List;
/**
* 首页 Banner Service 接口
*
* @author xia
*/
public interface BannerService {
/**
* 创建 Banner
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createBanner(@Valid BannerCreateReqVO createReqVO);
/**
* 更新 Banner
*
* @param updateReqVO 更新信息
*/
void updateBanner(@Valid BannerUpdateReqVO updateReqVO);
/**
* 删除 Banner
*
* @param id 编号
*/
void deleteBanner(Long id);
/**
* 获得 Banner
*
* @param id 编号
* @return Banner
*/
BannerDO getBanner(Long id);
/**
* 获得所有 Banner列表
* @return Banner列表
*/
List<BannerDO> getBannerList();
/**
* 获得 Banner 分页
*
* @param pageReqVO 分页查询
* @return Banner分页
*/
PageResult<BannerDO> getBannerPage(BannerPageReqVO pageReqVO);
}

View File

@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.promotion.service.banner;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerUpdateReqVO;
import cn.iocoder.yudao.module.promotion.convert.banner.BannerConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.banner.BannerDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.banner.BannerMapper;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.BANNER_NOT_EXISTS;
/**
* 首页 banner 实现类
*
* @author xia
*/
@Service
@Validated
public class BannerServiceImpl implements BannerService {
@Resource
private BannerMapper bannerMapper;
@Override
public Long createBanner(BannerCreateReqVO createReqVO) {
// 插入
BannerDO banner = BannerConvert.INSTANCE.convert(createReqVO);
bannerMapper.insert(banner);
// 返回
return banner.getId();
}
@Override
public void updateBanner(BannerUpdateReqVO updateReqVO) {
// 校验存在
this.validateBannerExists(updateReqVO.getId());
// 更新
BannerDO updateObj = BannerConvert.INSTANCE.convert(updateReqVO);
bannerMapper.updateById(updateObj);
}
@Override
public void deleteBanner(Long id) {
// 校验存在
this.validateBannerExists(id);
// 删除
bannerMapper.deleteById(id);
}
private void validateBannerExists(Long id) {
if (bannerMapper.selectById(id) == null) {
throw exception(BANNER_NOT_EXISTS);
}
}
@Override
public BannerDO getBanner(Long id) {
return bannerMapper.selectById(id);
}
@Override
public List<BannerDO> getBannerList() {
return bannerMapper.selectList();
}
@Override
public PageResult<BannerDO> getBannerPage(BannerPageReqVO pageReqVO) {
return bannerMapper.selectPage(pageReqVO);
}
}

View File

@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.promotion.service.coupon;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
/**
* 优惠劵 Service 接口
*
* @author 芋道源码
*/
public interface CouponService {
/**
* 校验优惠劵,包括状态、有限期
*
* 1. 如果校验通过,则返回优惠劵信息
* 2. 如果校验不通过,则直接抛出业务异常
*
* @param id 优惠劵编号
* @param userId 用户编号
* @return 优惠劵信息
*/
CouponDO validCoupon(Long id, Long userId);
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.promotion.service.coupon;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
/**
* 优惠劵 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class CouponServiceImpl implements CouponService {
// TODO 芋艿:待实现
@Override
public CouponDO validCoupon(Long id, Long userId) {
return null;
}
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.promotion.service.discount;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import java.util.Collection;
import java.util.Map;
/**
* 限时折扣 Service 接口
*
* @author 芋道源码
*/
public interface DiscountService {
/**
* 基于指定 SKU 编号数组,获得匹配的限时折扣商品
*
* 注意,匹配的条件,仅仅是日期符合,并且处于开启状态
*
* @param skuIds SKU 编号数组
* @return 匹配的限时折扣商品
*/
Map<Long, DiscountProductDO> getMatchDiscountProducts(Collection<Long> skuIds);
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.promotion.service.discount;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* 限时折扣 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class DiscountServiceImpl implements DiscountService {
// TODO 芋艿:待实现
@Override
public Map<Long, DiscountProductDO> getMatchDiscountProducts(Collection<Long> skuIds) {
Map<Long, DiscountProductDO> products = new HashMap<>();
products.put(1L, new DiscountProductDO().setPromotionPrice(100));
products.put(2L, new DiscountProductDO().setPromotionPrice(50));
return products;
}
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.promotion.service.price;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
/**
* 价格计算 Service 接口
*
* @author 芋道源码
*/
public interface PriceService {
/**
* 计算商品的价格
*
* @param calculateReqDTO 价格请求
* @return 价格相应
*/
PriceCalculateRespDTO calculatePrice(PriceCalculateReqDTO calculateReqDTO);
}

View File

@@ -0,0 +1,487 @@
package cn.iocoder.yudao.module.promotion.service.price;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
import cn.iocoder.yudao.module.promotion.convert.price.PriceConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionLevelEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.promotion.service.coupon.CouponService;
import cn.iocoder.yudao.module.promotion.service.discount.DiscountService;
import cn.iocoder.yudao.module.promotion.service.reward.RewardService;
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import com.google.common.base.Suppliers;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
import static java.util.Collections.singletonList;
/**
* 价格计算 Service 实现类
*
* 优惠计算顺序min(限时折扣, 会员折扣) > 满减送 > 优惠券。
* 参考文档:
* 1. <a href="https://help.youzan.com/displaylist/detail_4_4-1-60384">有赞文档:限时折扣、满减送、优惠券哪个优先计算?</a>
*
* TODO 芋艿:进一步完善
* 1. 限时折扣:指定金额、减免金额、折扣
* 2. 满减送:循环、折扣
* 3. 优惠劵:待定
*
* @author 芋道源码
*/
@Service
@Validated
public class PriceServiceImpl implements PriceService {
@Resource
private DiscountService discountService;
@Resource
private RewardService rewardService;
@Resource
private CouponService couponService;
@Resource
private ProductSkuApi productSkuApi;
@Override
public PriceCalculateRespDTO calculatePrice(PriceCalculateReqDTO calculateReqDTO) {
// 获得商品 SKU 数组
List<ProductSkuRespDTO> skuList = checkSkus(calculateReqDTO);
// 初始化 PriceCalculateRespDTO 对象
PriceCalculateRespDTO priceCalculate = PriceConvert.INSTANCE.convert(calculateReqDTO, skuList);
// 计算商品级别的价格
calculatePriceForSkuLevel(calculateReqDTO.getUserId(), priceCalculate);
// 计算订单级别的价格
calculatePriceForOrderLevel(calculateReqDTO.getUserId(), priceCalculate);
// 计算优惠劵级别的价格
calculatePriceForCouponLevel(calculateReqDTO.getUserId(), calculateReqDTO.getCouponId(), priceCalculate);
// 计算【优惠劵】促销 TODO 待实现
return priceCalculate;
}
private List<ProductSkuRespDTO> checkSkus(PriceCalculateReqDTO calculateReqDTO) {
// 获得商品 SKU 数组
Map<Long, Integer> skuIdCountMap = CollectionUtils.convertMap(calculateReqDTO.getItems(),
PriceCalculateReqDTO.Item::getSkuId, PriceCalculateReqDTO.Item::getCount);
List<ProductSkuRespDTO> skus = productSkuApi.getSkuList(skuIdCountMap.keySet());
// 校验商品 SKU
skus.forEach(sku -> {
Integer count = skuIdCountMap.get(sku.getId());
if (count == null) {
throw exception(SKU_NOT_EXISTS);
}
// 不校验库存不足,避免购物车场景,商品无货的情况
});
return skus;
}
// ========== 计算商品级别的价格 ==========
/**
* 计算商品级别的价格,例如说:
* 1. 会员折扣
* 2. 限时折扣 {@link cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivityDO}
*
* 其中,会员折扣、限时折扣取最低价
*
* @param userId 用户编号
* @param priceCalculate 价格计算的结果
*/
private void calculatePriceForSkuLevel(Long userId, PriceCalculateRespDTO priceCalculate) {
// 获取 SKU 级别的所有优惠信息
Supplier<Double> memberDiscountSupplier = getMemberDiscountSupplier(userId);
Map<Long, DiscountProductDO> discountProducts = discountService.getMatchDiscountProducts(
convertSet(priceCalculate.getOrder().getItems(), PriceCalculateRespDTO.OrderItem::getSkuId));
// 处理每个 SKU 的优惠
priceCalculate.getOrder().getItems().forEach(orderItem -> {
// 获取该 SKU 的优惠信息
Double memberDiscount = memberDiscountSupplier.get();
DiscountProductDO discountProduct = discountProducts.get(orderItem.getSkuId());
if (discountProduct != null // 假设优惠价格更贵,则认为没优惠
&& discountProduct.getPromotionPrice() >= orderItem.getOriginalUnitPrice()) {
discountProduct = null;
}
if (memberDiscount == null && discountProduct == null) {
return;
}
// 计算价格,判断选择哪个折扣
Integer memberPrice = memberDiscount != null ? (int) (orderItem.getPayPrice() * memberDiscount / 100) : null;
Integer promotionPrice = discountProduct != null ? discountProduct.getPromotionPrice() * orderItem.getCount() : null;
if (memberPrice == null) {
calculatePriceByDiscountActivity(priceCalculate, orderItem, discountProduct, promotionPrice);
} else if (promotionPrice == null) {
calculatePriceByMemberDiscount(priceCalculate, orderItem, memberPrice);
} else if (memberPrice < promotionPrice) {
calculatePriceByDiscountActivity(priceCalculate, orderItem, discountProduct, promotionPrice);
} else {
calculatePriceByMemberDiscount(priceCalculate, orderItem, memberPrice);
}
});
}
private void calculatePriceByMemberDiscount(PriceCalculateRespDTO priceCalculate, PriceCalculateRespDTO.OrderItem orderItem,
Integer memberPrice) {
// 记录优惠明细
addPromotion(priceCalculate, orderItem, null, PromotionTypeEnum.MEMBER.getName(),
PromotionTypeEnum.MEMBER.getType(), PromotionLevelEnum.SKU.getLevel(), memberPrice,
true, StrUtil.format("会员折扣:省 {} 元", formatPrice(orderItem.getPayPrice() - memberPrice)));
// 修改 SKU 的优惠
modifyOrderItemPayPrice(orderItem, memberPrice, priceCalculate);
}
private void calculatePriceByDiscountActivity(PriceCalculateRespDTO priceCalculate, PriceCalculateRespDTO.OrderItem orderItem,
DiscountProductDO discountProduct, Integer promotionPrice) {
// 记录优惠明细
addPromotion(priceCalculate, orderItem, discountProduct.getActivityId(), discountProduct.getActivityName(),
PromotionTypeEnum.DISCOUNT_ACTIVITY.getType(), PromotionLevelEnum.SKU.getLevel(), promotionPrice,
true, StrUtil.format("限时折扣:省 {} 元", formatPrice(orderItem.getPayPrice() - promotionPrice)));
// 修改 SKU 的优惠
modifyOrderItemPayPrice(orderItem, promotionPrice, priceCalculate);
}
// TODO 芋艿:提前实现
private Supplier<Double> getMemberDiscountSupplier(Long userId) {
return Suppliers.memoize(() -> {
if (userId == 1) {
return 90d;
}
if (userId == 2) {
return 80d;
}
return null; // 无优惠
});
}
// ========== 计算商品级别的价格 ==========
/**
* 计算订单级别的价格,例如说:
* 1. 满减送 {@link cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO}
*
* @param userId 用户编号
* @param priceCalculate 价格计算的结果
*/
@SuppressWarnings("unused")
private void calculatePriceForOrderLevel(Long userId, PriceCalculateRespDTO priceCalculate) {
// 获取 SKU 级别的所有优惠信息
Set<Long> spuIds = convertSet(priceCalculate.getOrder().getItems(), PriceCalculateRespDTO.OrderItem::getSpuId);
Map<RewardActivityDO, Set<Long>> rewardActivities = rewardService.getMatchRewardActivities(spuIds);
// 处理满减送活动
if (CollUtil.isNotEmpty(rewardActivities)) {
rewardActivities.forEach((rewardActivity, activitySpuIds) -> {
List<PriceCalculateRespDTO.OrderItem> orderItems = CollectionUtils.filterList(priceCalculate.getOrder().getItems(),
orderItem -> CollUtil.contains(activitySpuIds, orderItem.getSpuId()));
calculatePriceByRewardActivity(priceCalculate, orderItems, rewardActivity);
});
}
}
private void calculatePriceByRewardActivity(PriceCalculateRespDTO priceCalculate, List<PriceCalculateRespDTO.OrderItem> orderItems,
RewardActivityDO rewardActivity) {
// 获得最大匹配的满减送活动的规格
RewardActivityDO.Rule rule = getLastMatchRewardActivityRule(rewardActivity, orderItems);
if (rule == null) {
// 获取不到的情况下,记录不满足的优惠明细
addNotMeetPromotion(priceCalculate, orderItems, rewardActivity.getId(), rewardActivity.getName(),
PromotionTypeEnum.REWARD_ACTIVITY.getType(), PromotionLevelEnum.ORDER.getLevel(),
getRewardActivityNotMeetTip(rewardActivity));
return;
}
// 分摊金额
// TODO 芋艿limit 不能超过最大价格
List<Integer> discountPartPrices = dividePrice(orderItems, rule.getDiscountPrice());
// 记录优惠明细
addPromotion(priceCalculate, orderItems, rewardActivity.getId(), rewardActivity.getName(),
PromotionTypeEnum.REWARD_ACTIVITY.getType(), PromotionLevelEnum.ORDER.getLevel(), discountPartPrices,
true, StrUtil.format("满减送:省 {} 元", formatPrice(rule.getDiscountPrice())));
// 修改 SKU 的分摊
for (int i = 0; i < orderItems.size(); i++) {
modifyOrderItemOrderPartPriceFromDiscountPrice(orderItems.get(i), discountPartPrices.get(i), priceCalculate);
}
}
/**
* 获得最大匹配的满减送活动的规格
*
* @param rewardActivity 满减送活动
* @param orderItems 商品项
* @return 匹配的活动规格
*/
private RewardActivityDO.Rule getLastMatchRewardActivityRule(RewardActivityDO rewardActivity,
List<PriceCalculateRespDTO.OrderItem> orderItems) {
Integer count = getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getCount, Integer::sum);
// price 的计算逻辑,使用 orderDividePrice 的原因,主要考虑分摊后,这个才是该 SKU 当前真实的支付总价
Integer price = getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum);
assert count != null && price != null;
for (int i = rewardActivity.getRules().size() - 1; i >= 0; i--) {
RewardActivityDO.Rule rule = rewardActivity.getRules().get(i);
if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType())
&& price >= rule.getLimit()) {
return rule;
}
if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType())
&& count >= rule.getLimit()) {
return rule;
}
}
return null;
}
/**
* 获得满减送活动部匹配时的提示
*
* @param rewardActivity 满减送活动
* @return 提示
*/
private String getRewardActivityNotMeetTip(RewardActivityDO rewardActivity) {
return "TODO"; // TODO 芋艿:后面再想想
}
// ========== 计算优惠劵级别的价格 ==========
private void calculatePriceForCouponLevel(Long userId, Long couponId, PriceCalculateRespDTO priceCalculate) {
// 校验优惠劵
if (couponId == null) {
return;
}
CouponDO coupon = couponService.validCoupon(couponId, userId);
// 获得匹配的商品 SKU 数组
List<PriceCalculateRespDTO.OrderItem> orderItems = getMatchCouponOrderItems(priceCalculate, coupon);
if (CollUtil.isEmpty(orderItems)) {
throw exception(COUPON_NO_MATCH_SPU);
}
// 计算是否满足优惠劵的使用金额
Integer originPrice = getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum);
assert originPrice != null;
if (originPrice < coupon.getPriceAvailable()) {
throw exception(COUPON_NO_MATCH_MIN_PRICE);
}
// 计算可以优惠的金额
priceCalculate.getOrder().setCouponId(couponId);
Integer couponPrice = getCouponPrice(coupon, originPrice);
// 分摊金额
// TODO 芋艿limit 不能超过最大价格
List<Integer> couponPartPrices = dividePrice(orderItems, couponPrice);
// 记录优惠明细
addPromotion(priceCalculate, orderItems, coupon.getId(), coupon.getTitle(),
PromotionTypeEnum.COUPON.getType(), PromotionLevelEnum.COUPON.getLevel(), couponPartPrices,
true, StrUtil.format("优惠劵:省 {} 元", formatPrice(couponPrice)));
// 修改 SKU 的分摊
for (int i = 0; i < orderItems.size(); i++) {
modifyOrderItemOrderPartPriceFromCouponPrice(orderItems.get(i), couponPartPrices.get(i), priceCalculate);
}
}
private List<PriceCalculateRespDTO.OrderItem> getMatchCouponOrderItems(PriceCalculateRespDTO priceCalculate,
CouponDO coupon) {
if (PromotionProductScopeEnum.ALL.getScope().equals(coupon.getProductScope())) {
return priceCalculate.getOrder().getItems();
}
return CollectionUtils.filterList(priceCalculate.getOrder().getItems(),
orderItem -> coupon.getSpuIds().contains(orderItem.getSpuId()));
}
private Integer getCouponPrice(CouponDO coupon, Integer originPrice) {
// TODO 芋艿 getPreferentialType 的枚举判断
if (coupon.getPreferentialType().equals(1)) { // 减价
return coupon.getPriceOff();
} else if (coupon.getPreferentialType().equals(2)) { // 打折
Integer couponPrice = originPrice * coupon.getPercentOff() / 100;
return coupon.getDiscountPriceLimit() == null ? couponPrice
: Math.min(couponPrice, coupon.getDiscountPriceLimit()); // 优惠上限
}
throw new IllegalArgumentException(String.format("优惠劵(%s) 的优惠类型不正确", coupon.toString()));
}
// ========== 其它相对通用的方法 ==========
/**
* 添加单个 OrderItem 的营销明细
*
* @param priceCalculate 价格计算结果
* @param orderItem 单个订单商品 SKU
* @param id 营销编号
* @param name 营销名字
* @param type 营销类型
* @param level 营销级别
* @param newPayPrice 新的单实付金额(总)
* @param meet 是否满足优惠条件
* @param meetTip 满足条件的提示
*/
private void addPromotion(PriceCalculateRespDTO priceCalculate, PriceCalculateRespDTO.OrderItem orderItem,
Long id, String name, Integer type, Integer level,
Integer newPayPrice, Boolean meet, String meetTip) {
// 创建营销明细 Item
// TODO 芋艿orderItem.getPayPrice() 要不要改成 orderDividePrice同时newPayPrice 要不要改成直接传递 discountPrice
PriceCalculateRespDTO.PromotionItem promotionItem = new PriceCalculateRespDTO.PromotionItem().setSkuId(orderItem.getSkuId())
.setOriginalPrice(orderItem.getPayPrice()).setDiscountPrice(orderItem.getPayPrice() - newPayPrice);
// 创建营销明细
PriceCalculateRespDTO.Promotion promotion = new PriceCalculateRespDTO.Promotion()
.setId(id).setName(name).setType(type).setLevel(level)
.setOriginalPrice(promotionItem.getOriginalPrice()).setDiscountPrice(promotionItem.getDiscountPrice())
.setItems(singletonList(promotionItem)).setMeet(meet).setMeetTip(meetTip);
priceCalculate.getPromotions().add(promotion);
}
/**
* 添加多个 OrderItem 的营销明细
*
* @param priceCalculate 价格计算结果
* @param orderItems 多个订单商品 SKU
* @param id 营销编号
* @param name 营销名字
* @param type 营销类型
* @param level 营销级别
* @param discountPrices 多个订单商品 SKU 的优惠价格(总),和 orderItems 一一对应
* @param meet 是否满足优惠条件
* @param meetTip 满足条件的提示
*/
private void addPromotion(PriceCalculateRespDTO priceCalculate, List<PriceCalculateRespDTO.OrderItem> orderItems,
Long id, String name, Integer type, Integer level,
List<Integer> discountPrices, Boolean meet, String meetTip) {
// 创建营销明细 Item
List<PriceCalculateRespDTO.PromotionItem> promotionItems = new ArrayList<>(discountPrices.size());
for (int i = 0; i < orderItems.size(); i++) {
PriceCalculateRespDTO.OrderItem orderItem = orderItems.get(i);
promotionItems.add(new PriceCalculateRespDTO.PromotionItem().setSkuId(orderItem.getSkuId())
.setOriginalPrice(orderItem.getPayPrice()).setDiscountPrice(discountPrices.get(i)));
}
// 创建营销明细
PriceCalculateRespDTO.Promotion promotion = new PriceCalculateRespDTO.Promotion()
.setId(id).setName(name).setType(type).setLevel(level)
.setOriginalPrice(getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum))
.setDiscountPrice(getSumValue(discountPrices, value -> value, Integer::sum))
.setItems(promotionItems).setMeet(meet).setMeetTip(meetTip);
priceCalculate.getPromotions().add(promotion);
}
private void addNotMeetPromotion(PriceCalculateRespDTO priceCalculate, List<PriceCalculateRespDTO.OrderItem> orderItems,
Long id, String name, Integer type, Integer level, String meetTip) {
// 创建营销明细 Item
List<PriceCalculateRespDTO.PromotionItem> promotionItems = CollectionUtils.convertList(orderItems,
orderItem -> new PriceCalculateRespDTO.PromotionItem().setSkuId(orderItem.getSkuId())
.setOriginalPrice(orderItem.getOrderDividePrice()).setDiscountPrice(0));
// 创建营销明细
Integer originalPrice = getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum);
PriceCalculateRespDTO.Promotion promotion = new PriceCalculateRespDTO.Promotion()
.setId(id).setName(name).setType(type).setLevel(level)
.setOriginalPrice(originalPrice).setDiscountPrice(0)
.setItems(promotionItems).setMeet(false).setMeetTip(meetTip);
priceCalculate.getPromotions().add(promotion);
}
/**
* 修改 OrderItem 的 payPrice 价格,同时会修改 Order 的 payPrice 价格
*
* @param orderItem 订单商品 SKU
* @param newPayPrice 新的 payPrice 价格
* @param priceCalculate 价格计算结果
*/
private void modifyOrderItemPayPrice(PriceCalculateRespDTO.OrderItem orderItem, Integer newPayPrice,
PriceCalculateRespDTO priceCalculate) {
int diffPayPrice = orderItem.getPayPrice() - newPayPrice;
// 设置 OrderItem 价格相关字段
orderItem.setDiscountPrice(orderItem.getDiscountPrice() + diffPayPrice);
orderItem.setPayPrice(newPayPrice);
orderItem.setOrderDividePrice(orderItem.getPayPrice() - orderItem.getOrderPartPrice());
// 设置 Order 相关相关字段
priceCalculate.getOrder().setPayPrice(priceCalculate.getOrder().getPayPrice() - diffPayPrice);
}
/**
* 修改 OrderItem 的 orderPartPrice 价格,同时会修改 Order 的 discountPrice 价格
*
* 本质:分摊 Order 的 discountPrice 价格,到对应的 OrderItem 的 orderPartPrice 价格中
*
* @param orderItem 订单商品 SKU
* @param addOrderPartPrice 新增的 discountPrice 价格
* @param priceCalculate 价格计算结果
*/
private void modifyOrderItemOrderPartPriceFromDiscountPrice(PriceCalculateRespDTO.OrderItem orderItem, Integer addOrderPartPrice,
PriceCalculateRespDTO priceCalculate) {
// 设置 OrderItem 价格相关字段
orderItem.setOrderPartPrice(orderItem.getOrderPartPrice() + addOrderPartPrice);
orderItem.setOrderDividePrice(orderItem.getPayPrice() - orderItem.getOrderPartPrice());
// 设置 Order 相关相关字段
PriceCalculateRespDTO.Order order = priceCalculate.getOrder();
order.setDiscountPrice(order.getDiscountPrice() + addOrderPartPrice);
order.setPayPrice(order.getPayPrice() - addOrderPartPrice);
}
/**
* 修改 OrderItem 的 orderPartPrice 价格,同时会修改 Order 的 couponPrice 价格
*
* 本质:分摊 Order 的 couponPrice 价格,到对应的 OrderItem 的 orderPartPrice 价格中
*
* @param orderItem 订单商品 SKU
* @param addOrderPartPrice 新增的 couponPrice 价格
* @param priceCalculate 价格计算结果
*/
private void modifyOrderItemOrderPartPriceFromCouponPrice(PriceCalculateRespDTO.OrderItem orderItem, Integer addOrderPartPrice,
PriceCalculateRespDTO priceCalculate) {
// 设置 OrderItem 价格相关字段
orderItem.setOrderPartPrice(orderItem.getOrderPartPrice() + addOrderPartPrice);
orderItem.setOrderDividePrice(orderItem.getPayPrice() - orderItem.getOrderPartPrice());
// 设置 Order 相关相关字段
PriceCalculateRespDTO.Order order = priceCalculate.getOrder();
order.setCouponPrice(order.getCouponPrice() + addOrderPartPrice);
order.setPayPrice(order.getPayPrice() - addOrderPartPrice);
}
private List<Integer> dividePrice(List<PriceCalculateRespDTO.OrderItem> orderItems, Integer price) {
List<Integer> prices = new ArrayList<>(orderItems.size());
Integer total = getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum);
assert total != null;
int remainPrice = price;
// 遍历每一个,进行分摊
for (int i = 0; i < orderItems.size(); i++) {
PriceCalculateRespDTO.OrderItem orderItem = orderItems.get(i);
int partPrice;
if (i < orderItems.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减
partPrice = (int) (price * (1.0D * orderItem.getOrderDividePrice() / total));
remainPrice -= partPrice;
} else {
partPrice = remainPrice;
}
Assert.isTrue(partPrice > 0, "分摊金额必须大于 0");
prices.add(partPrice);
}
return prices;
}
private String formatPrice(Integer price) {
return String.format("%.2f", price / 100d);
}
}

View File

@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.promotion.service.reward;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import java.util.Map;
import java.util.Set;
/**
* 满减送 Service 接口
*
* @author 芋道源码
*/
public interface RewardService {
/**
* 基于指定的 SPU 编号数组,获得它们匹配的满减送活动
*
* @param spuIds SPU 编号数组
* @return 满减送活动,与对应的 SPU 编号的映射。即value 就是 SPU 编号的集合
*/
Map<RewardActivityDO, Set<Long>> getMatchRewardActivities(Set<Long> spuIds);
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.promotion.service.reward;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
/**
* 满减送 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class RewardServiceImpl implements RewardService {
// TODO 芋艿:待实现
@Override
public Map<RewardActivityDO, Set<Long>> getMatchRewardActivities(Set<Long> spuIds) {
return Collections.emptyMap();
}
}

View File

@@ -0,0 +1,446 @@
package cn.iocoder.yudao.module.promotion.service.price;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateReqDTO;
import cn.iocoder.yudao.module.promotion.api.price.dto.PriceCalculateRespDTO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionLevelEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.promotion.service.coupon.CouponService;
import cn.iocoder.yudao.module.promotion.service.discount.DiscountService;
import cn.iocoder.yudao.module.promotion.service.reward.RewardService;
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
/**
* {@link PriceServiceImpl} 的单元测试
*
* @author 芋道源码
*/
public class PriceServiceTest extends BaseMockitoUnitTest {
@InjectMocks
private PriceServiceImpl priceService;
@Mock
private DiscountService discountService;
@Mock
private RewardService rewardService;
@Mock
private CouponService couponService;
@Mock
private ProductSkuApi productSkuApi;
@Test
public void testCalculatePrice_memberDiscount() {
// 准备参数
// TODO 芋艿userId = 1实现 9 折;后续改成 mock
PriceCalculateReqDTO calculateReqDTO = new PriceCalculateReqDTO().setUserId(1L)
.setItems(singletonList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2)));
// mock 方法(商品 SKU 信息)
ProductSkuRespDTO productSku = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100));
when(productSkuApi.getSkuList(eq(asSet(10L)))).thenReturn(singletonList(productSku));
// 调用
PriceCalculateRespDTO priceCalculate = priceService.calculatePrice(calculateReqDTO);
// 断言 Order 部分
PriceCalculateRespDTO.Order order = priceCalculate.getOrder();
assertEquals(order.getOriginalPrice(), 200);
assertEquals(order.getDiscountPrice(), 0);
assertEquals(order.getPointPrice(), 0);
assertEquals(order.getDeliveryPrice(), 0);
assertEquals(order.getPayPrice(), 180);
assertNull(order.getCouponId());
// 断言 OrderItem 部分
assertEquals(order.getItems().size(), 1);
PriceCalculateRespDTO.OrderItem orderItem = order.getItems().get(0);
assertEquals(orderItem.getSkuId(), 10L);
assertEquals(orderItem.getCount(), 2);
assertEquals(orderItem.getOriginalPrice(), 200);
assertEquals(orderItem.getOriginalUnitPrice(), 100);
assertEquals(orderItem.getDiscountPrice(), 20);
assertEquals(orderItem.getPayPrice(), 180);
assertEquals(orderItem.getOrderPartPrice(), 0);
assertEquals(orderItem.getOrderDividePrice(), 180);
// 断言 Promotion 部分
assertEquals(priceCalculate.getPromotions().size(), 1);
PriceCalculateRespDTO.Promotion promotion = priceCalculate.getPromotions().get(0);
assertNull(promotion.getId());
assertEquals(promotion.getName(), "会员折扣");
assertEquals(promotion.getType(), PromotionTypeEnum.MEMBER.getType());
assertEquals(promotion.getLevel(), PromotionLevelEnum.SKU.getLevel());
assertEquals(promotion.getOriginalPrice(), 200);
assertEquals(promotion.getDiscountPrice(), 20);
assertTrue(promotion.getMeet());
assertEquals(promotion.getMeetTip(), "会员折扣:省 0.20 元");
PriceCalculateRespDTO.PromotionItem promotionItem = promotion.getItems().get(0);
assertEquals(promotion.getItems().size(), 1);
assertEquals(promotionItem.getSkuId(), 10L);
assertEquals(promotionItem.getOriginalPrice(), 200);
assertEquals(promotionItem.getDiscountPrice(), 20);
}
@Test
public void testCalculatePrice_discountActivity() {
// 准备参数
PriceCalculateReqDTO calculateReqDTO = new PriceCalculateReqDTO().setUserId(randomLongId())
.setItems(asList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2),
new PriceCalculateReqDTO.Item().setSkuId(20L).setCount(3)));
// mock 方法(商品 SKU 信息)
ProductSkuRespDTO productSku01 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100));
ProductSkuRespDTO productSku02 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(20L).setPrice(50));
when(productSkuApi.getSkuList(eq(asSet(10L, 20L)))).thenReturn(asList(productSku01, productSku02));
// mock 方法(限时折扣 DiscountActivity 信息)
DiscountProductDO discountProduct01 = randomPojo(DiscountProductDO.class, o -> o.setActivityId(1000L).setActivityName("活动 1000 号")
.setSkuId(10L).setPromotionPrice(80));
DiscountProductDO discountProduct02 = randomPojo(DiscountProductDO.class, o -> o.setActivityId(2000L).setActivityName("活动 2000 号")
.setSkuId(20L).setPromotionPrice(40));
when(discountService.getMatchDiscountProducts(eq(asSet(10L, 20L)))).thenReturn(
MapUtil.builder(10L, discountProduct01).put(20L, discountProduct02).map());
// 调用
PriceCalculateRespDTO priceCalculate = priceService.calculatePrice(calculateReqDTO);
// 断言 Order 部分
PriceCalculateRespDTO.Order order = priceCalculate.getOrder();
assertEquals(order.getOriginalPrice(), 350);
assertEquals(order.getDiscountPrice(), 0);
assertEquals(order.getPointPrice(), 0);
assertEquals(order.getDeliveryPrice(), 0);
assertEquals(order.getPayPrice(), 280);
assertNull(order.getCouponId());
// 断言 OrderItem 部分
assertEquals(order.getItems().size(), 2);
PriceCalculateRespDTO.OrderItem orderItem01 = order.getItems().get(0);
assertEquals(orderItem01.getSkuId(), 10L);
assertEquals(orderItem01.getCount(), 2);
assertEquals(orderItem01.getOriginalPrice(), 200);
assertEquals(orderItem01.getOriginalUnitPrice(), 100);
assertEquals(orderItem01.getDiscountPrice(), 40);
assertEquals(orderItem01.getPayPrice(), 160);
assertEquals(orderItem01.getOrderPartPrice(), 0);
assertEquals(orderItem01.getOrderDividePrice(), 160);
PriceCalculateRespDTO.OrderItem orderItem02 = order.getItems().get(1);
assertEquals(orderItem02.getSkuId(), 20L);
assertEquals(orderItem02.getCount(), 3);
assertEquals(orderItem02.getOriginalPrice(), 150);
assertEquals(orderItem02.getOriginalUnitPrice(), 50);
assertEquals(orderItem02.getDiscountPrice(), 30);
assertEquals(orderItem02.getPayPrice(), 120);
assertEquals(orderItem02.getOrderPartPrice(), 0);
assertEquals(orderItem02.getOrderDividePrice(), 120);
// 断言 Promotion 部分
assertEquals(priceCalculate.getPromotions().size(), 2);
PriceCalculateRespDTO.Promotion promotion01 = priceCalculate.getPromotions().get(0);
assertEquals(promotion01.getId(), 1000L);
assertEquals(promotion01.getName(), "活动 1000 号");
assertEquals(promotion01.getType(), PromotionTypeEnum.DISCOUNT_ACTIVITY.getType());
assertEquals(promotion01.getLevel(), PromotionLevelEnum.SKU.getLevel());
assertEquals(promotion01.getOriginalPrice(), 200);
assertEquals(promotion01.getDiscountPrice(), 40);
assertTrue(promotion01.getMeet());
assertEquals(promotion01.getMeetTip(), "限时折扣:省 0.40 元");
PriceCalculateRespDTO.PromotionItem promotionItem01 = promotion01.getItems().get(0);
assertEquals(promotion01.getItems().size(), 1);
assertEquals(promotionItem01.getSkuId(), 10L);
assertEquals(promotionItem01.getOriginalPrice(), 200);
assertEquals(promotionItem01.getDiscountPrice(), 40);
PriceCalculateRespDTO.Promotion promotion02 = priceCalculate.getPromotions().get(1);
assertEquals(promotion02.getId(), 2000L);
assertEquals(promotion02.getName(), "活动 2000 号");
assertEquals(promotion02.getType(), PromotionTypeEnum.DISCOUNT_ACTIVITY.getType());
assertEquals(promotion02.getLevel(), PromotionLevelEnum.SKU.getLevel());
assertEquals(promotion02.getOriginalPrice(), 150);
assertEquals(promotion02.getDiscountPrice(), 30);
assertTrue(promotion02.getMeet());
assertEquals(promotion02.getMeetTip(), "限时折扣:省 0.30 元");
PriceCalculateRespDTO.PromotionItem promotionItem02 = promotion02.getItems().get(0);
assertEquals(promotion02.getItems().size(), 1);
assertEquals(promotionItem02.getSkuId(), 20L);
assertEquals(promotionItem02.getOriginalPrice(), 150);
assertEquals(promotionItem02.getDiscountPrice(), 30);
}
/**
* 测试满减送活动,匹配的情况
*/
@Test
public void testCalculatePrice_rewardActivity() {
// 准备参数
PriceCalculateReqDTO calculateReqDTO = new PriceCalculateReqDTO().setUserId(randomLongId())
.setItems(asList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2),
new PriceCalculateReqDTO.Item().setSkuId(20L).setCount(3),
new PriceCalculateReqDTO.Item().setSkuId(30L).setCount(4)));
// mock 方法(商品 SKU 信息)
ProductSkuRespDTO productSku01 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100).setSpuId(1L));
ProductSkuRespDTO productSku02 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(20L).setPrice(50).setSpuId(2L));
ProductSkuRespDTO productSku03 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(30L).setPrice(30).setSpuId(3L));
when(productSkuApi.getSkuList(eq(asSet(10L, 20L, 30L)))).thenReturn(asList(productSku01, productSku02, productSku03));
// mock 方法(限时折扣 DiscountActivity 信息)
RewardActivityDO rewardActivity01 = randomPojo(RewardActivityDO.class, o -> o.setId(1000L).setName("活动 1000 号")
.setSpuIds(asList(10L, 20L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
.setRules(singletonList(new RewardActivityDO.Rule().setLimit(200).setDiscountPrice(70))));
RewardActivityDO rewardActivity02 = randomPojo(RewardActivityDO.class, o -> o.setId(2000L).setName("活动 2000 号")
.setSpuIds(singletonList(30L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType())
.setRules(asList(new RewardActivityDO.Rule().setLimit(1).setDiscountPrice(10),
new RewardActivityDO.Rule().setLimit(2).setDiscountPrice(60), // 最大可满足,因为是 4 个
new RewardActivityDO.Rule().setLimit(10).setDiscountPrice(100))));
Map<RewardActivityDO, Set<Long>> matchRewardActivities = new LinkedHashMap<>();
matchRewardActivities.put(rewardActivity01, asSet(1L, 2L));
matchRewardActivities.put(rewardActivity02, asSet(3L));
when(rewardService.getMatchRewardActivities(eq(asSet(1L, 2L, 3L)))).thenReturn(matchRewardActivities);
// 调用
PriceCalculateRespDTO priceCalculate = priceService.calculatePrice(calculateReqDTO);
// 断言 Order 部分
PriceCalculateRespDTO.Order order = priceCalculate.getOrder();
assertEquals(order.getOriginalPrice(), 470);
assertEquals(order.getDiscountPrice(), 130);
assertEquals(order.getPointPrice(), 0);
assertEquals(order.getDeliveryPrice(), 0);
assertEquals(order.getPayPrice(), 340);
assertNull(order.getCouponId());
// 断言 OrderItem 部分
assertEquals(order.getItems().size(), 3);
PriceCalculateRespDTO.OrderItem orderItem01 = order.getItems().get(0);
assertEquals(orderItem01.getSkuId(), 10L);
assertEquals(orderItem01.getCount(), 2);
assertEquals(orderItem01.getOriginalPrice(), 200);
assertEquals(orderItem01.getOriginalUnitPrice(), 100);
assertEquals(orderItem01.getDiscountPrice(), 0);
assertEquals(orderItem01.getPayPrice(), 200);
assertEquals(orderItem01.getOrderPartPrice(), 40);
assertEquals(orderItem01.getOrderDividePrice(), 160);
PriceCalculateRespDTO.OrderItem orderItem02 = order.getItems().get(1);
assertEquals(orderItem02.getSkuId(), 20L);
assertEquals(orderItem02.getCount(), 3);
assertEquals(orderItem02.getOriginalPrice(), 150);
assertEquals(orderItem02.getOriginalUnitPrice(), 50);
assertEquals(orderItem02.getDiscountPrice(), 0);
assertEquals(orderItem02.getPayPrice(), 150);
assertEquals(orderItem02.getOrderPartPrice(), 30);
assertEquals(orderItem02.getOrderDividePrice(), 120);
PriceCalculateRespDTO.OrderItem orderItem03 = order.getItems().get(2);
assertEquals(orderItem03.getSkuId(), 30L);
assertEquals(orderItem03.getCount(), 4);
assertEquals(orderItem03.getOriginalPrice(), 120);
assertEquals(orderItem03.getOriginalUnitPrice(), 30);
assertEquals(orderItem03.getDiscountPrice(), 0);
assertEquals(orderItem03.getPayPrice(), 120);
assertEquals(orderItem03.getOrderPartPrice(), 60);
assertEquals(orderItem03.getOrderDividePrice(), 60);
// 断言 Promotion 部分(第一个)
assertEquals(priceCalculate.getPromotions().size(), 2);
PriceCalculateRespDTO.Promotion promotion01 = priceCalculate.getPromotions().get(0);
assertEquals(promotion01.getId(), 1000L);
assertEquals(promotion01.getName(), "活动 1000 号");
assertEquals(promotion01.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
assertEquals(promotion01.getLevel(), PromotionLevelEnum.ORDER.getLevel());
assertEquals(promotion01.getOriginalPrice(), 350);
assertEquals(promotion01.getDiscountPrice(), 70);
assertTrue(promotion01.getMeet());
assertEquals(promotion01.getMeetTip(), "满减送:省 0.70 元");
assertEquals(promotion01.getItems().size(), 2);
PriceCalculateRespDTO.PromotionItem promotionItem011 = promotion01.getItems().get(0);
assertEquals(promotionItem011.getSkuId(), 10L);
assertEquals(promotionItem011.getOriginalPrice(), 200);
assertEquals(promotionItem011.getDiscountPrice(), 40);
PriceCalculateRespDTO.PromotionItem promotionItem012 = promotion01.getItems().get(1);
assertEquals(promotionItem012.getSkuId(), 20L);
assertEquals(promotionItem012.getOriginalPrice(), 150);
assertEquals(promotionItem012.getDiscountPrice(), 30);
// 断言 Promotion 部分(第二个)
PriceCalculateRespDTO.Promotion promotion02 = priceCalculate.getPromotions().get(1);
assertEquals(promotion02.getId(), 2000L);
assertEquals(promotion02.getName(), "活动 2000 号");
assertEquals(promotion02.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
assertEquals(promotion02.getLevel(), PromotionLevelEnum.ORDER.getLevel());
assertEquals(promotion02.getOriginalPrice(), 120);
assertEquals(promotion02.getDiscountPrice(), 60);
assertTrue(promotion02.getMeet());
assertEquals(promotion02.getMeetTip(), "满减送:省 0.60 元");
PriceCalculateRespDTO.PromotionItem promotionItem02 = promotion02.getItems().get(0);
assertEquals(promotion02.getItems().size(), 1);
assertEquals(promotionItem02.getSkuId(), 30L);
assertEquals(promotionItem02.getOriginalPrice(), 120);
assertEquals(promotionItem02.getDiscountPrice(), 60);
}
/**
* 测试满减送活动,不匹配的情况
*/
@Test
public void testCalculatePrice_rewardActivityNotMeet() {
// 准备参数
PriceCalculateReqDTO calculateReqDTO = new PriceCalculateReqDTO().setUserId(randomLongId())
.setItems(asList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2),
new PriceCalculateReqDTO.Item().setSkuId(20L).setCount(3)));
// mock 方法(商品 SKU 信息)
ProductSkuRespDTO productSku01 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100).setSpuId(1L));
ProductSkuRespDTO productSku02 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(20L).setPrice(50).setSpuId(2L));
when(productSkuApi.getSkuList(eq(asSet(10L, 20L)))).thenReturn(asList(productSku01, productSku02));
// mock 方法(限时折扣 DiscountActivity 信息)
RewardActivityDO rewardActivity01 = randomPojo(RewardActivityDO.class, o -> o.setId(1000L).setName("活动 1000 号")
.setSpuIds(asList(10L, 20L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
.setRules(singletonList(new RewardActivityDO.Rule().setLimit(351).setDiscountPrice(70))));
Map<RewardActivityDO, Set<Long>> matchRewardActivities = new LinkedHashMap<>();
matchRewardActivities.put(rewardActivity01, asSet(1L, 2L));
when(rewardService.getMatchRewardActivities(eq(asSet(1L, 2L)))).thenReturn(matchRewardActivities);
// 调用
PriceCalculateRespDTO priceCalculate = priceService.calculatePrice(calculateReqDTO);
// 断言 Order 部分
PriceCalculateRespDTO.Order order = priceCalculate.getOrder();
assertEquals(order.getOriginalPrice(), 350);
assertEquals(order.getDiscountPrice(), 0);
assertEquals(order.getPointPrice(), 0);
assertEquals(order.getDeliveryPrice(), 0);
assertEquals(order.getPayPrice(), 350);
assertNull(order.getCouponId());
// 断言 OrderItem 部分
assertEquals(order.getItems().size(), 2);
PriceCalculateRespDTO.OrderItem orderItem01 = order.getItems().get(0);
assertEquals(orderItem01.getSkuId(), 10L);
assertEquals(orderItem01.getCount(), 2);
assertEquals(orderItem01.getOriginalPrice(), 200);
assertEquals(orderItem01.getOriginalUnitPrice(), 100);
assertEquals(orderItem01.getDiscountPrice(), 0);
assertEquals(orderItem01.getPayPrice(), 200);
assertEquals(orderItem01.getOrderPartPrice(), 0);
assertEquals(orderItem01.getOrderDividePrice(), 200);
PriceCalculateRespDTO.OrderItem orderItem02 = order.getItems().get(1);
assertEquals(orderItem02.getSkuId(), 20L);
assertEquals(orderItem02.getCount(), 3);
assertEquals(orderItem02.getOriginalPrice(), 150);
assertEquals(orderItem02.getOriginalUnitPrice(), 50);
assertEquals(orderItem02.getDiscountPrice(), 0);
assertEquals(orderItem02.getPayPrice(), 150);
assertEquals(orderItem02.getOrderPartPrice(), 0);
assertEquals(orderItem02.getOrderDividePrice(), 150);
// 断言 Promotion 部分
assertEquals(priceCalculate.getPromotions().size(), 1);
PriceCalculateRespDTO.Promotion promotion01 = priceCalculate.getPromotions().get(0);
assertEquals(promotion01.getId(), 1000L);
assertEquals(promotion01.getName(), "活动 1000 号");
assertEquals(promotion01.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
assertEquals(promotion01.getLevel(), PromotionLevelEnum.ORDER.getLevel());
assertEquals(promotion01.getOriginalPrice(), 350);
assertEquals(promotion01.getDiscountPrice(), 0);
assertFalse(promotion01.getMeet());
assertEquals(promotion01.getMeetTip(), "TODO"); // TODO 芋艿:后面再想想
assertEquals(promotion01.getItems().size(), 2);
PriceCalculateRespDTO.PromotionItem promotionItem011 = promotion01.getItems().get(0);
assertEquals(promotionItem011.getSkuId(), 10L);
assertEquals(promotionItem011.getOriginalPrice(), 200);
assertEquals(promotionItem011.getDiscountPrice(), 0);
PriceCalculateRespDTO.PromotionItem promotionItem012 = promotion01.getItems().get(1);
assertEquals(promotionItem012.getSkuId(), 20L);
assertEquals(promotionItem012.getOriginalPrice(), 150);
assertEquals(promotionItem012.getDiscountPrice(), 0);
}
@Test
public void testCalculatePrice_coupon() {
// 准备参数
PriceCalculateReqDTO calculateReqDTO = new PriceCalculateReqDTO().setUserId(randomLongId())
.setItems(asList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2),
new PriceCalculateReqDTO.Item().setSkuId(20L).setCount(3),
new PriceCalculateReqDTO.Item().setSkuId(30L).setCount(4)))
.setCouponId(1024L);
// mock 方法(商品 SKU 信息)
ProductSkuRespDTO productSku01 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100).setSpuId(1L));
ProductSkuRespDTO productSku02 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(20L).setPrice(50).setSpuId(2L));
ProductSkuRespDTO productSku03 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(30L).setPrice(30).setSpuId(3L));
when(productSkuApi.getSkuList(eq(asSet(10L, 20L, 30L)))).thenReturn(asList(productSku01, productSku02, productSku03));
// mock 方法(优惠劵 Coupon 信息)
CouponDO coupon = randomPojo(CouponDO.class, o -> o.setId(1024L).setTitle("程序员节")
.setProductScope(PromotionProductScopeEnum.SPU.getScope()).setSpuIds(asList(1L, 2L))
.setPriceAvailable(350).setPreferentialType(2).setPercentOff(50).setDiscountPriceLimit(70));
when(couponService.validCoupon(eq(1024L), eq(calculateReqDTO.getUserId()))).thenReturn(coupon);
// 200 + 150; 350
//
// 调用
PriceCalculateRespDTO priceCalculate = priceService.calculatePrice(calculateReqDTO);
// 断言 Order 部分
PriceCalculateRespDTO.Order order = priceCalculate.getOrder();
assertEquals(order.getOriginalPrice(), 470);
assertEquals(order.getDiscountPrice(), 0);
assertEquals(order.getPointPrice(), 0);
assertEquals(order.getDeliveryPrice(), 0);
assertEquals(order.getPayPrice(), 400);
assertEquals(order.getCouponId(), 1024L);
assertEquals(order.getCouponPrice(), 70);
// 断言 OrderItem 部分
assertEquals(order.getItems().size(), 3);
PriceCalculateRespDTO.OrderItem orderItem01 = order.getItems().get(0);
assertEquals(orderItem01.getSkuId(), 10L);
assertEquals(orderItem01.getCount(), 2);
assertEquals(orderItem01.getOriginalPrice(), 200);
assertEquals(orderItem01.getOriginalUnitPrice(), 100);
assertEquals(orderItem01.getDiscountPrice(), 0);
assertEquals(orderItem01.getPayPrice(), 200);
assertEquals(orderItem01.getOrderPartPrice(), 40);
assertEquals(orderItem01.getOrderDividePrice(), 160);
PriceCalculateRespDTO.OrderItem orderItem02 = order.getItems().get(1);
assertEquals(orderItem02.getSkuId(), 20L);
assertEquals(orderItem02.getCount(), 3);
assertEquals(orderItem02.getOriginalPrice(), 150);
assertEquals(orderItem02.getOriginalUnitPrice(), 50);
assertEquals(orderItem02.getDiscountPrice(), 0);
assertEquals(orderItem02.getPayPrice(), 150);
assertEquals(orderItem02.getOrderPartPrice(), 30);
assertEquals(orderItem02.getOrderDividePrice(), 120);
PriceCalculateRespDTO.OrderItem orderItem03 = order.getItems().get(2);
assertEquals(orderItem03.getSkuId(), 30L);
assertEquals(orderItem03.getCount(), 4);
assertEquals(orderItem03.getOriginalPrice(), 120);
assertEquals(orderItem03.getOriginalUnitPrice(), 30);
assertEquals(orderItem03.getDiscountPrice(), 0);
assertEquals(orderItem03.getPayPrice(), 120);
assertEquals(orderItem03.getOrderPartPrice(), 0);
assertEquals(orderItem03.getOrderDividePrice(), 120);
// 断言 Promotion 部分
assertEquals(priceCalculate.getPromotions().size(), 1);
PriceCalculateRespDTO.Promotion promotion01 = priceCalculate.getPromotions().get(0);
assertEquals(promotion01.getId(), 1024L);
assertEquals(promotion01.getName(), "程序员节");
assertEquals(promotion01.getType(), PromotionTypeEnum.COUPON.getType());
assertEquals(promotion01.getLevel(), PromotionLevelEnum.COUPON.getLevel());
assertEquals(promotion01.getOriginalPrice(), 350);
assertEquals(promotion01.getDiscountPrice(), 70);
assertTrue(promotion01.getMeet());
assertEquals(promotion01.getMeetTip(), "优惠劵:省 0.70 元");
assertEquals(promotion01.getItems().size(), 2);
PriceCalculateRespDTO.PromotionItem promotionItem011 = promotion01.getItems().get(0);
assertEquals(promotionItem011.getSkuId(), 10L);
assertEquals(promotionItem011.getOriginalPrice(), 200);
assertEquals(promotionItem011.getDiscountPrice(), 40);
PriceCalculateRespDTO.PromotionItem promotionItem012 = promotion01.getItems().get(1);
assertEquals(promotionItem012.getSkuId(), 20L);
assertEquals(promotionItem012.getOriginalPrice(), 150);
assertEquals(promotionItem012.getDiscountPrice(), 30);
}
}

View File

@@ -0,0 +1,49 @@
spring:
main:
lazy-initialization: true # 开启懒加载,加快速度
banner-mode: off # 单元测试,禁用 Banner
--- #################### 数据库相关配置 ####################
spring:
# 数据源配置项
datasource:
name: ruoyi-vue-pro
url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式DATABASE_TO_UPPER 配置表和字段使用小写
driver-class-name: org.h2.Driver
username: sa
password:
druid:
async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
initial-size: 1 # 单元测试,配置为 1提升启动速度
sql:
init:
schema-locations: classpath:/sql/create_tables.sql
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
redis:
host: 127.0.0.1 # 地址
port: 16379 # 端口(单元测试,使用 16379 端口)
database: 0 # 数据库索引
mybatis:
lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
--- #################### 定时任务相关配置 ####################
--- #################### 配置中心相关配置 ####################
--- #################### 服务保障相关配置 ####################
# Lock4j 配置项(单元测试,禁用 Lock4j
# Resilience4j 配置项
--- #################### 监控相关配置 ####################
--- #################### 芋道相关配置 ####################
# 芋道配置项,设置当前项目所有自定义的配置
yudao:
info:
base-package: cn.iocoder.yudao.module

View File

@@ -0,0 +1,4 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
</configuration>

View File

@@ -0,0 +1 @@
DELETE FROM "market_activity";

View File

@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS "market_activity" (
"id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"title" varchar(50) NOT NULL,
"activity_type" tinyint(4) NOT NULL,
"status" tinyint(4) NOT NULL,
"start_time" datetime NOT NULL,
"end_time" datetime NOT NULL,
"invalid_time" datetime,
"delete_time" datetime,
"time_limited_discount" varchar(2000),
"full_privilege" varchar(2000),
"creator" varchar(64) DEFAULT '',
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint(20) NOT NULL,
PRIMARY KEY ("id")
) COMMENT '促销活动';