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

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

View File

@ -290,7 +290,15 @@ public class CollectionUtils {
return valueFunc.apply(t);
}
public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc,
public static <T, V extends Comparable<? super V>> T getMinObject(List<T> from, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) {
return null;
}
assert from.size() > 0; // 断言避免告警
return from.stream().min(Comparator.comparing(valueFunc)).get();
}
public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
BinaryOperator<V> accumulator) {
return getSumValue(from, valueFunc, accumulator, null);
}

View File

@ -4,6 +4,9 @@ import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
* 商品 SPU API 接口
@ -21,6 +24,16 @@ public interface ProductSpuApi {
*/
List<ProductSpuRespDTO> getSpuList(Collection<Long> ids);
/**
* 批量查询 SPU MAP
*
* @param ids SPU 编号列表
* @return SPU MAP
*/
default Map<Long, ProductSpuRespDTO> getSpusMap(Collection<Long> ids) {
return convertMap(getSpuList(ids), ProductSpuRespDTO::getId);
}
/**
* 批量查询 SPU 数组并且校验是否 SPU 是否有效
*

View File

@ -4,10 +4,6 @@ import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
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.product.controller.app.spu.vo.AppProductSpuDetailRespVO;
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuRespVO;
@ -51,11 +47,6 @@ public class AppProductSpuController {
@Resource
private ProductBrowseHistoryService productBrowseHistoryService;
@Resource
private MemberLevelApi memberLevelApi;
@Resource
private MemberUserApi memberUserApi;
@GetMapping("/list-by-ids")
@Operation(summary = "获得商品 SPU 列表")
@Parameter(name = "ids", description = "编号列表", required = true)
@ -68,9 +59,6 @@ public class AppProductSpuController {
// 拼接返回
list.forEach(spu -> spu.setSalesCount(spu.getSalesCount() + spu.getVirtualSalesCount()));
List<AppProductSpuRespVO> voList = BeanUtils.toBean(list, AppProductSpuRespVO.class);
// 处理 vip 价格
MemberLevelRespDTO memberLevel = getMemberLevel();
voList.forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
return success(voList);
}
@ -85,9 +73,6 @@ public class AppProductSpuController {
// 拼接返回
pageResult.getList().forEach(spu -> spu.setSalesCount(spu.getSalesCount() + spu.getVirtualSalesCount()));
PageResult<AppProductSpuRespVO> voPageResult = BeanUtils.toBean(pageResult, AppProductSpuRespVO.class);
// 处理 vip 价格
MemberLevelRespDTO memberLevel = getMemberLevel();
voPageResult.getList().forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
return success(voPageResult);
}
@ -115,37 +100,7 @@ public class AppProductSpuController {
spu.setSalesCount(spu.getSalesCount() + spu.getVirtualSalesCount());
AppProductSpuDetailRespVO spuVO = BeanUtils.toBean(spu, AppProductSpuDetailRespVO.class)
.setSkus(BeanUtils.toBean(skus, AppProductSpuDetailRespVO.Sku.class));
// 处理 vip 价格
MemberLevelRespDTO memberLevel = getMemberLevel();
spuVO.setVipPrice(calculateVipPrice(spuVO.getPrice(), memberLevel));
return success(spuVO);
}
private MemberLevelRespDTO getMemberLevel() {
Long userId = getLoginUserId();
if (userId == null) {
return null;
}
MemberUserRespDTO user = memberUserApi.getUser(userId);
if (user.getLevelId() == null || user.getLevelId() <= 0) {
return null;
}
return memberLevelApi.getMemberLevel(user.getLevelId());
}
/**
* 计算会员 VIP 优惠价格
*
* @param price 原价
* @param memberLevel 会员等级
* @return 优惠价格
*/
public Integer calculateVipPrice(Integer price, MemberLevelRespDTO memberLevel) {
if (memberLevel == null || memberLevel.getDiscountPercent() == null) {
return 0;
}
Integer newPrice = price * memberLevel.getDiscountPercent() / 100;
return price - newPrice;
}
}

View File

@ -46,9 +46,6 @@ public class AppProductSpuDetailRespVO {
@Schema(description = "市场价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Integer marketPrice;
@Schema(description = "VIP 价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "968") // 通过会员等级计算出折扣后价格
private Integer vipPrice;
@Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
private Integer stock;

View File

@ -38,9 +38,6 @@ public class AppProductSpuRespVO {
@Schema(description = "市场价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Integer marketPrice;
@Schema(description = "VIP 价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "968") // 通过会员等级计算出折扣后价格
private Integer vipPrice;
@Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
private Integer stock;

View File

@ -87,6 +87,7 @@ public interface ProductCategoryService {
* 校验商品分类是否有效如下情况视为无效
* 1. 商品分类编号不存在
* 2. 商品分类被禁用
* 3. 商品分类层级校验必须使用第二级的商品分类及以下
*
* @param ids 商品分类编号数组
*/

View File

@ -20,6 +20,7 @@ import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.product.dal.dataobject.category.ProductCategoryDO.CATEGORY_LEVEL;
import static cn.iocoder.yudao.module.product.dal.dataobject.category.ProductCategoryDO.PARENT_ID_NULL;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.*;
@ -112,13 +113,19 @@ public class ProductCategoryServiceImpl implements ProductCategoryService {
Map<Long, ProductCategoryDO> categoryMap = CollectionUtils.convertMap(list, ProductCategoryDO::getId);
// 校验
ids.forEach(id -> {
// 校验分类是否存在
ProductCategoryDO category = categoryMap.get(id);
if (category == null) {
throw exception(CATEGORY_NOT_EXISTS);
}
// 校验分类是否启用
if (!CommonStatusEnum.ENABLE.getStatus().equals(category.getStatus())) {
throw exception(CATEGORY_DISABLED, category.getName());
}
// 商品分类层级校验必须使用第二级的商品分类
if (getCategoryLevel(id) < CATEGORY_LEVEL) {
throw exception(SPU_SAVE_FAIL_CATEGORY_LEVEL_ERROR);
}
});
}

View File

@ -13,11 +13,11 @@ import java.util.List;
public interface DiscountActivityApi {
/**
* 获得商品匹配的的限时折扣信息
* 获得 skuId 商品匹配的的限时折扣信息
*
* @param skuIds 商品 SKU 编号数组
* @return 限时折扣信息
*/
List<DiscountProductRespDTO> getMatchDiscountProductList(Collection<Long> skuIds);
List<DiscountProductRespDTO> getMatchDiscountProductListBySkuIds(Collection<Long> skuIds);
}

View File

@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.promotion.api.discount.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 限时折扣活动商品 Response DTO
*
@ -44,5 +46,14 @@ public class DiscountProductRespDTO {
* 活动标题
*/
private String activityName;
/**
* 活动开始时间点
*/
private LocalDateTime activityStartTime;
/**
* 活动结束时间点
*/
private LocalDateTime activityEndTime;
}

View File

@ -12,13 +12,12 @@ import java.util.List;
*/
public interface RewardActivityApi {
/**
* 基于指定的 SPU 编号数组获得它们匹配的满减送活动
* 获得 spuId 商品匹配的的满减送活动列表
*
* @param spuIds SPU 编号数组
* @param spuIds SPU 编号
* @return 满减送活动列表
*/
List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds);
List<RewardActivityMatchRespDTO> getMatchRewardActivityListBySpuIds(Collection<Long> spuIds);
}

View File

@ -18,6 +18,11 @@ import java.util.Map;
@Data
public class RewardActivityMatchRespDTO {
/**
* 匹配的 SPU 数组
*/
private List<Long> spuIds;
/**
* 活动编号主键自增
*/
@ -100,6 +105,13 @@ public class RewardActivityMatchRespDTO {
*/
private Map<Long, Integer> giveCouponTemplateCounts;
/**
* 规则描述
*
* 通过 {@link #limit}{@link #discountPrice} 等字段进行拼接
*/
private String description;
}
}

View File

@ -11,7 +11,7 @@ public interface ErrorCodeConstants {
// ========== 促销活动相关 1-013-001-000 ============
ErrorCode DISCOUNT_ACTIVITY_NOT_EXISTS = new ErrorCode(1_013_001_000, "限时折扣活动不存在");
ErrorCode DISCOUNT_ACTIVITY_SPU_CONFLICTS = new ErrorCode(1_013_001_001, "存在商品参加了其它限时折扣活动");
ErrorCode DISCOUNT_ACTIVITY_SPU_CONFLICTS = new ErrorCode(1_013_001_001, "存在商品参加了其它限时折扣活动【{}】");
ErrorCode DISCOUNT_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_001_002, "限时折扣活动已关闭,不能修改");
ErrorCode DISCOUNT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_001_003, "限时折扣活动未关闭,不能删除");
ErrorCode DISCOUNT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_001_004, "限时折扣活动已关闭,不能重复关闭");
@ -38,14 +38,18 @@ public interface ErrorCodeConstants {
// ========== 满减送活动 1-013-006-000 ==========
ErrorCode REWARD_ACTIVITY_NOT_EXISTS = new ErrorCode(1_013_006_000, "满减送活动不存在");
ErrorCode REWARD_ACTIVITY_SPU_CONFLICTS = new ErrorCode(1_013_006_001, "存在商品参加了其它满减送活动");
ErrorCode REWARD_ACTIVITY_SPU_CONFLICTS = new ErrorCode(1_013_006_001, "该时间段存在商品参加了其它满减送活动");
ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_002, "满减送活动已关闭,不能修改");
ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_006_003, "满减送活动未关闭,不能删除");
ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_006_004, "满减送活动已关闭,不能重复关闭");
ErrorCode REWARD_ACTIVITY_SCOPE_ALL_EXISTS = new ErrorCode(1_013_006_005, "已存在商品范围为全场的满减送活动");
ErrorCode REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS = new ErrorCode(1_013_006_006, "存在商品类型参加了其它满减送活动");
ErrorCode REWARD_ACTIVITY_SCOPE_EXISTS = new ErrorCode(1_013_006_005, "与该时间段满减送活动【{}】商品范围冲突,原因:{}");
// ========== TODO 空着 1-013-007-000 ============
// ========== 积分商城活动 1-013-007-000 ==========
ErrorCode POINT_ACTIVITY_NOT_EXISTS = new ErrorCode(1_013_007_000, "积分商城活动不存在");
ErrorCode POINT_ACTIVITY_SPU_CONFLICTS = new ErrorCode(1_013_007_001, "存在商品参加了其它积分商城活动");
ErrorCode POINT_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_007_002, "积分商城活动已关闭,不能修改");
ErrorCode POINT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END = new ErrorCode(1_013_007_003, "积分商城活动未关闭或未结束,不能删除");
ErrorCode POINT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_007_004, "积分商城活动已关闭,不能重复关闭");
// ========== 秒杀活动 1-013-008-000 ==========
ErrorCode SECKILL_ACTIVITY_NOT_EXISTS = new ErrorCode(1_013_008_000, "秒杀活动不存在");

View File

@ -5,6 +5,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* 优惠劵领取方式
@ -20,12 +21,12 @@ public enum CouponTakeTypeEnum implements IntArrayValuable {
REGISTER(3, "新人券"), // 注册时自动领取
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponTakeTypeEnum::getValue).toArray();
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponTakeTypeEnum::getType).toArray();
/**
*
*/
private final Integer value;
private final Integer type;
/**
* 名字
*/
@ -35,4 +36,9 @@ public enum CouponTakeTypeEnum implements IntArrayValuable {
public int[] array() {
return ARRAYS;
}
public static boolean isUser(Integer type) {
return Objects.equals(USER.getType(), type);
}
}

View File

@ -1,12 +1,13 @@
package cn.iocoder.yudao.module.promotion.api.discount;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
import cn.iocoder.yudao.module.promotion.convert.discount.DiscountActivityConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import cn.iocoder.yudao.module.promotion.service.discount.DiscountActivityService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.util.Collection;
import java.util.List;
@ -23,8 +24,9 @@ public class DiscountActivityApiImpl implements DiscountActivityApi {
private DiscountActivityService discountActivityService;
@Override
public List<DiscountProductRespDTO> getMatchDiscountProductList(Collection<Long> skuIds) {
return DiscountActivityConvert.INSTANCE.convertList02(discountActivityService.getMatchDiscountProductList(skuIds));
public List<DiscountProductRespDTO> getMatchDiscountProductListBySkuIds(Collection<Long> skuIds) {
List<DiscountProductDO> list = discountActivityService.getMatchDiscountProductListBySkuIds(skuIds);
return BeanUtils.toBean(list, DiscountProductRespDTO.class);
}
}

View File

@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.promotion.api.reward;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.util.Collection;
import java.util.List;
@ -22,8 +22,8 @@ public class RewardActivityApiImpl implements RewardActivityApi {
private RewardActivityService rewardActivityService;
@Override
public List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds) {
return rewardActivityService.getMatchRewardActivityList(spuIds);
public List<RewardActivityMatchRespDTO> getMatchRewardActivityListBySpuIds(Collection<Long> spuIds) {
return rewardActivityService.getMatchRewardActivityListBySpuIds(spuIds);
}
}

View File

@ -9,12 +9,12 @@ import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityType
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;

View File

@ -3,9 +3,10 @@ package cn.iocoder.yudao.module.promotion.controller.admin.discount;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.*;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.convert.discount.DiscountActivityConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
@ -13,12 +14,12 @@ import cn.iocoder.yudao.module.promotion.service.discount.DiscountActivityServic
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -33,9 +34,6 @@ public class DiscountActivityController {
@Resource
private DiscountActivityService discountActivityService;
@Resource
private ProductSpuApi productSpuApi;
@PostMapping("/create")
@Operation(summary = "创建限时折扣活动")
@PreAuthorize("@ss.hasPermission('promotion:discount-activity:create')")
@ -73,7 +71,7 @@ public class DiscountActivityController {
@Operation(summary = "获得限时折扣活动")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('promotion:discount-activity:query')")
public CommonResult<DiscountActivityDetailRespVO> getDiscountActivity(@RequestParam("id") Long id) {
public CommonResult<DiscountActivityRespVO> getDiscountActivity(@RequestParam("id") Long id) {
DiscountActivityDO discountActivity = discountActivityService.getDiscountActivity(id);
if (discountActivity == null) {
return success(null);
@ -88,18 +86,14 @@ public class DiscountActivityController {
@PreAuthorize("@ss.hasPermission('promotion:discount-activity:query')")
public CommonResult<PageResult<DiscountActivityRespVO>> getDiscountActivityPage(@Valid DiscountActivityPageReqVO pageVO) {
PageResult<DiscountActivityDO> pageResult = discountActivityService.getDiscountActivityPage(pageVO);
if (CollUtil.isEmpty(pageResult.getList())) { // TODO @zhangshuai方法里的空行目的是让代码分块可以更清晰所以上面这个空格可以不要而下面判断之后的空格其实加下比较好类似的还有 spuList以及后面的 convert
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 拼接数据
List<DiscountProductDO> products = discountActivityService.getDiscountProductsByActivityId(
convertSet(pageResult.getList(), DiscountActivityDO::getId));
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(
convertSet(products, DiscountProductDO::getSpuId));
return success(DiscountActivityConvert.INSTANCE.convertPage(pageResult, products, spuList));
return success(DiscountActivityConvert.INSTANCE.convertPage(pageResult, products));
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.promotion.controller.admin.discount.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.List;
@Schema(description = "管理后台 - 限时折扣活动的详细 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class DiscountActivityDetailRespVO extends DiscountActivityRespVO {
/**
* 商品列表
*/
private List<Product> products;
}

View File

@ -1,11 +1,11 @@
package cn.iocoder.yudao.module.promotion.controller.admin.discount.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.List;
@ -25,25 +25,7 @@ public class DiscountActivityRespVO extends DiscountActivityBaseVO {
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") // TODO @zhangshuai属性和属性之间最多空一行噢
private Long spuId;
@Schema(description = "限时折扣商品", requiredMode = Schema.RequiredMode.REQUIRED)
private List<DiscountActivityBaseVO.Product> products;
// ========== 商品字段 ==========
// TODO @zhangshuai一个优惠活动会关联多个商品所以它不用返回 spuName
// TODO 最终界面展示字段就编号活动名称参与商品数活动状态开始时间结束时间操作
@Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, // SPU name 读取
example = "618大促")
private String spuName;
@Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, // SPU picUrl 读取
example = "https://www.iocoder.cn/xx.png")
private String picUrl;
@Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, // SPU marketPrice 读取
example = "50")
private Integer marketPrice;
private List<Product> products;
}

View File

@ -1,13 +1,13 @@
package cn.iocoder.yudao.module.promotion.controller.admin.discount.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
@Schema(description = "管理后台 - 限时折扣活动更新 Request VO")
@ -25,6 +25,6 @@ public class DiscountActivityUpdateReqVO extends DiscountActivityBaseVO {
*/
@NotEmpty(message = "商品列表不能为空")
@Valid
private List<DiscountActivityCreateReqVO.Product> products;
private List<Product> products;
}

View File

@ -0,0 +1,141 @@
package cn.iocoder.yudao.module.promotion.controller.admin.point;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivitySaveReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductRespVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointProductDO;
import cn.iocoder.yudao.module.promotion.service.point.PointActivityService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
@Tag(name = "管理后台 - 积分商城活动")
@RestController
@RequestMapping("/promotion/point-activity")
@Validated
public class PointActivityController {
@Resource
private PointActivityService pointActivityService;
@Resource
private ProductSpuApi productSpuApi;
@PostMapping("/create")
@Operation(summary = "创建积分商城活动")
@PreAuthorize("@ss.hasPermission('promotion:point-activity:create')")
public CommonResult<Long> createPointActivity(@Valid @RequestBody PointActivitySaveReqVO createReqVO) {
return success(pointActivityService.createPointActivity(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新积分商城活动")
@PreAuthorize("@ss.hasPermission('promotion:point-activity:update')")
public CommonResult<Boolean> updatePointActivity(@Valid @RequestBody PointActivitySaveReqVO updateReqVO) {
pointActivityService.updatePointActivity(updateReqVO);
return success(true);
}
@PutMapping("/close")
@Operation(summary = "关闭积分商城活动")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('promotion:point-activity:close')")
public CommonResult<Boolean> closeSeckillActivity(@RequestParam("id") Long id) {
pointActivityService.closePointActivity(id);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除积分商城活动")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('promotion:point-activity:delete')")
public CommonResult<Boolean> deletePointActivity(@RequestParam("id") Long id) {
pointActivityService.deletePointActivity(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得积分商城活动")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('promotion:point-activity:query')")
public CommonResult<PointActivityRespVO> getPointActivity(@RequestParam("id") Long id) {
PointActivityDO pointActivity = pointActivityService.getPointActivity(id);
if (pointActivity == null) {
return success(null);
}
List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(Collections.singletonList(id));
PointActivityRespVO respVO = BeanUtils.toBean(pointActivity, PointActivityRespVO.class);
respVO.setProducts(BeanUtils.toBean(products, PointProductRespVO.class));
return success(respVO);
}
@GetMapping("/page")
@Operation(summary = "获得积分商城活动分页")
@PreAuthorize("@ss.hasPermission('promotion:point-activity:query')")
public CommonResult<PageResult<PointActivityRespVO>> getPointActivityPage(@Valid PointActivityPageReqVO pageReqVO) {
PageResult<PointActivityDO> pageResult = pointActivityService.getPointActivityPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 拼接数据
List<PointActivityRespVO> resultList = buildPointActivityRespVOList(pageResult.getList());
return success(new PageResult<>(resultList, pageResult.getTotal()));
}
@GetMapping("/list-by-ids")
@Operation(summary = "获得积分商城活动列表,基于活动编号数组")
@Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
public CommonResult<List<PointActivityRespVO>> getPointActivityListByIds(@RequestParam("ids") List<Long> ids) {
// 1. 获得开启的活动列表
List<PointActivityDO> activityList = pointActivityService.getPointActivityListByIds(ids);
activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));
if (CollUtil.isEmpty(activityList)) {
return success(Collections.emptyList());
}
// 2. 拼接返回
List<PointActivityRespVO> result = buildPointActivityRespVOList(activityList);
return success(result);
}
private List<PointActivityRespVO> buildPointActivityRespVOList(List<PointActivityDO> activityList) {
List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(
convertSet(activityList, PointActivityDO::getId));
Map<Long, List<PointProductDO>> productsMap = convertMultiMap(products, PointProductDO::getActivityId);
Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpusMap(
convertSet(activityList, PointActivityDO::getSpuId));
List<PointActivityRespVO> result = BeanUtils.toBean(activityList, PointActivityRespVO.class);
result.forEach(activity -> {
// 设置 product 信息
PointProductDO minProduct = getMinObject(productsMap.get(activity.getId()), PointProductDO::getPoint);
assert minProduct != null;
activity.setPoint(minProduct.getPoint()).setPrice(minProduct.getPrice());
findAndThen(spuMap, activity.getSpuId(),
spu -> activity.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
});
return result;
}
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 积分商城活动分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PointActivityPageReqVO extends PageParam {
@Schema(description = "积分商城活动商品", example = "19509")
private Long spuId;
@Schema(description = "活动状态", example = "2")
private Integer status;
@Schema(description = "备注", example = "你说的对")
private String remark;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductRespVO;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 积分商城活动 Response VO")
@Data
@ExcelIgnoreUnannotated
public class PointActivityRespVO {
@Schema(description = "积分商城活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11373")
@ExcelProperty("积分商城活动编号")
private Long id;
@Schema(description = "积分商城活动商品", requiredMode = Schema.RequiredMode.REQUIRED, example = "19509")
@ExcelProperty("积分商城活动商品")
private Long spuId;
@Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@ExcelProperty("活动状态")
private Integer status;
@Schema(description = "积分商城活动库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@ExcelProperty("积分商城活动库存")
private Integer stock; // 剩余库存积分兑换时扣减
@Schema(description = "积分商城活动总库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@ExcelProperty("积分商城活动总库存")
private Integer totalStock;
@Schema(description = "备注", example = "你说的对")
@ExcelProperty("备注")
private String remark;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("排序")
private Integer sort;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@Schema(description = "积分商城商品", requiredMode = Schema.RequiredMode.REQUIRED)
private List<PointProductRespVO> products;
// ========== 商品字段 ==========
@Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, // SPU name 读取
example = "618大促")
private String spuName;
@Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, // SPU picUrl 读取
example = "https://www.iocoder.cn/xx.png")
private String picUrl;
@Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, // SPU marketPrice 读取
example = "50")
private Integer marketPrice;
//======================= 显示所需兑换积分最少的 sku 信息 =======================
@Schema(description = "兑换积分", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer point;
@Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860")
private Integer price;
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductSaveReqVO;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 积分商城活动新增/修改 Request VO")
@Data
public class PointActivitySaveReqVO {
@Schema(description = "积分商城活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11373")
private Long id;
@Schema(description = "积分商城活动商品", requiredMode = Schema.RequiredMode.REQUIRED, example = "19509")
@NotNull(message = "积分商城活动商品不能为空")
private Long spuId;
@Schema(description = "备注", example = "你说的对")
private String remark;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "排序不能为空")
private Integer sort;
@Schema(description = "积分商城商品", requiredMode = Schema.RequiredMode.REQUIRED)
private List<PointProductSaveReqVO> products;
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 积分商城商品 Response VO")
@Data
@ExcelIgnoreUnannotated
public class PointProductRespVO {
@Schema(description = "积分商城商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31718")
private Long id;
@Schema(description = "积分商城活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29388")
private Long activityId;
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8112")
private Long spuId;
@Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2736")
private Long skuId;
@Schema(description = "可兑换数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3926")
private Integer count;
@Schema(description = "兑换积分", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer point;
@Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860")
private Integer price;
@Schema(description = "积分商城商品库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer stock;
@Schema(description = "积分商城商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer activityStatus;
}

View File

@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 积分商城商品新增/修改 Request VO")
@Data
public class PointProductSaveReqVO {
@Schema(description = "积分商城商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31718")
private Long id;
@Schema(description = "积分商城活动 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "29388")
@NotNull(message = "积分商城活动 id不能为空")
private Long activityId;
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8112")
@NotNull(message = "商品 SPU 编号不能为空")
private Long spuId;
@Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2736")
@NotNull(message = "商品 SKU 编号不能为空")
private Long skuId;
@Schema(description = "可兑换数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3926")
@NotNull(message = "可兑换数量不能为空")
private Integer count;
@Schema(description = "兑换积分", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "兑换积分不能为空")
private Integer point;
@Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860")
@NotNull(message = "兑换金额,单位:分不能为空")
private Integer price;
@Schema(description = "积分商城商品库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "积分商城商品不能为空")
private Integer stock;
@Schema(description = "积分商城商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotNull(message = "积分商城商品状态不能为空")
private Integer activityStatus;
}

View File

@ -88,6 +88,7 @@ public class RewardActivityBaseVO {
return point == null || point >= 0;
}
}
@AssertTrue(message = "商品范围编号的数组不能为空")

View File

@ -102,7 +102,7 @@ public class SeckillActivityController {
@GetMapping("/list-by-ids")
@Operation(summary = "获得秒杀活动列表,基于活动编号数组")
@Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
public CommonResult<List<SeckillActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
public CommonResult<List<SeckillActivityRespVO>> getSeckillActivityListByIds(@RequestParam("ids") List<Long> ids) {
// 1. 获得开启的活动列表
List<SeckillActivityDO> activityList = seckillActivityService.getSeckillActivityListByIds(ids);
activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));

View File

@ -54,7 +54,7 @@ public class SeckillActivityRespVO extends SeckillActivityBaseVO {
example = "50")
private Integer marketPrice;
@Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@Schema(description = "秒杀金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer seckillPrice; // products 获取最小 price 读取
}

View File

@ -1,25 +1,13 @@
package cn.iocoder.yudao.module.promotion.controller.app.activity;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.controller.app.activity.vo.AppActivityRespVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivityDO;
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.dal.dataobject.seckill.SeckillActivityDO;
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.bargain.BargainActivityService;
import cn.iocoder.yudao.module.promotion.service.combination.CombinationActivityService;
import cn.iocoder.yudao.module.promotion.service.discount.DiscountActivityService;
import cn.iocoder.yudao.module.promotion.service.reward.RewardActivityService;
import cn.iocoder.yudao.module.promotion.service.seckill.SeckillActivityService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -31,11 +19,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@Tag(name = "用户 APP - 营销活动") // 用于提供跨多个活动的 HTTP 接口
@RestController
@ -49,152 +36,31 @@ public class AppActivityController {
private SeckillActivityService seckillActivityService;
@Resource
private BargainActivityService bargainActivityService;
@Resource
private DiscountActivityService discountActivityService;
@Resource
private RewardActivityService rewardActivityService;
@Resource
private ProductSpuApi productSpuApi;
@GetMapping("/list-by-spu-id")
@Operation(summary = "获得单个商品,近期参与的每个活动")
@Operation(summary = "获得单个商品,进行中的拼团、秒杀、砍价活动信息", description = "每种活动,只返回一个")
@Parameter(name = "spuId", description = "商品编号", required = true)
public CommonResult<List<AppActivityRespVO>> getActivityListBySpuId(@RequestParam("spuId") Long spuId) {
// 每种活动只返回一个
return success(getAppActivityList(Collections.singletonList(spuId)));
}
@GetMapping("/list-by-spu-ids")
@Operation(summary = "获得多个商品,近期参与的每个活动")
@Parameter(name = "spuIds", description = "商品编号数组", required = true)
public CommonResult<Map<Long, List<AppActivityRespVO>>> getActivityListBySpuIds(@RequestParam("spuIds") List<Long> spuIds) {
if (CollUtil.isEmpty(spuIds)) {
return success(MapUtil.empty());
}
// 每种活动只返回一个key SPU 编号
return success(convertMultiMap(getAppActivityList(spuIds), AppActivityRespVO::getSpuId));
}
private List<AppActivityRespVO> getAppActivityList(Collection<Long> spuIds) {
if (CollUtil.isEmpty(spuIds)) {
return new ArrayList<>();
}
// 获取开启的且开始的且没有结束的活动
List<AppActivityRespVO> activityList = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
List<AppActivityRespVO> activityVOList = new ArrayList<>();
// 1. 拼团活动
getCombinationActivities(spuIds, now, activityList);
CombinationActivityDO combinationActivity = combinationActivityService.getMatchCombinationActivityBySpuId(spuId);
if (combinationActivity != null) {
activityVOList.add(new AppActivityRespVO(combinationActivity.getId(), PromotionTypeEnum.COMBINATION_ACTIVITY.getType(),
combinationActivity.getName(), combinationActivity.getSpuId(), combinationActivity.getStartTime(), combinationActivity.getEndTime()));
}
// 2. 秒杀活动
getSeckillActivities(spuIds, now, activityList);
SeckillActivityDO seckillActivity = seckillActivityService.getMatchSeckillActivityBySpuId(spuId);
if (seckillActivity != null) {
activityVOList.add(new AppActivityRespVO(seckillActivity.getId(), PromotionTypeEnum.SECKILL_ACTIVITY.getType(),
seckillActivity.getName(), seckillActivity.getSpuId(), seckillActivity.getStartTime(), seckillActivity.getEndTime()));
}
// 3. 砍价活动
getBargainActivities(spuIds, now, activityList);
// 4. 限时折扣活动
getDiscountActivities(spuIds, now, activityList);
// 5. 满减送活动
getRewardActivityList(spuIds, now, activityList);
return activityList;
}
private void getCombinationActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
List<CombinationActivityDO> combinationActivities = combinationActivityService.getCombinationActivityBySpuIdsAndStatusAndDateTimeLt(
spuIds, CommonStatusEnum.ENABLE.getStatus(), now);
if (CollUtil.isEmpty(combinationActivities)) {
return;
}
combinationActivities.forEach(item -> {
activityList.add(new AppActivityRespVO(item.getId(), PromotionTypeEnum.COMBINATION_ACTIVITY.getType(),
item.getName(), item.getSpuId(), item.getStartTime(), item.getEndTime()));
});
}
private void getSeckillActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
List<SeckillActivityDO> seckillActivities = seckillActivityService.getSeckillActivityBySpuIdsAndStatusAndDateTimeLt(
spuIds, CommonStatusEnum.ENABLE.getStatus(), now);
if (CollUtil.isEmpty(seckillActivities)) {
return;
}
seckillActivities.forEach(item -> {
activityList.add(new AppActivityRespVO(item.getId(), PromotionTypeEnum.SECKILL_ACTIVITY.getType(),
item.getName(), item.getSpuId(), item.getStartTime(), item.getEndTime()));
});
}
private void getBargainActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
List<BargainActivityDO> bargainActivities = bargainActivityService.getBargainActivityBySpuIdsAndStatusAndDateTimeLt(
spuIds, CommonStatusEnum.ENABLE.getStatus(), now);
if (CollUtil.isNotEmpty(bargainActivities)) {
return;
}
bargainActivities.forEach(item -> {
activityList.add(new AppActivityRespVO(item.getId(), PromotionTypeEnum.BARGAIN_ACTIVITY.getType(),
item.getName(), item.getSpuId(), item.getStartTime(), item.getEndTime()));
});
}
private void getDiscountActivities(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
List<DiscountActivityDO> discountActivities = discountActivityService.getDiscountActivityBySpuIdsAndStatusAndDateTimeLt(
spuIds, CommonStatusEnum.ENABLE.getStatus(), now);
if (CollUtil.isEmpty(discountActivities)) {
return;
}
List<DiscountProductDO> products = discountActivityService.getDiscountProductsByActivityId(
convertSet(discountActivities, DiscountActivityDO::getId));
Map<Long, Long> productMap = convertMap(products, DiscountProductDO::getActivityId, DiscountProductDO::getSpuId);
discountActivities.forEach(item -> activityList.add(new AppActivityRespVO(item.getId(), PromotionTypeEnum.DISCOUNT_ACTIVITY.getType(),
item.getName(), productMap.get(item.getId()), item.getStartTime(), item.getEndTime())));
}
private void getRewardActivityList(Collection<Long> spuIds, LocalDateTime now, List<AppActivityRespVO> activityList) {
// 1.1 获得所有的活动
List<RewardActivityDO> rewardActivityList = rewardActivityService.getRewardActivityListByStatusAndDateTimeLt(
CommonStatusEnum.ENABLE.getStatus(), now);
if (CollUtil.isEmpty(rewardActivityList)) {
return;
}
// 1.2 获得所有的商品信息
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(spuIds);
if (CollUtil.isEmpty(spuList)) {
return;
}
// 2. 构建活动
for (RewardActivityDO rewardActivity : rewardActivityList) {
// 情况一所有商品都能参加
if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
buildAppActivityRespVO(rewardActivity, spuIds, activityList);
}
// 情况二指定商品参加
if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
List<Long> fSpuIds = spuList.stream().map(ProductSpuRespDTO::getId).filter(id ->
rewardActivity.getProductScopeValues().contains(id)).toList();
buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
}
// 情况三指定商品类型参加
if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
List<Long> fSpuIds = spuList.stream().filter(spuItem -> rewardActivity.getProductScopeValues()
.contains(spuItem.getCategoryId())).map(ProductSpuRespDTO::getId).toList();
buildAppActivityRespVO(rewardActivity, fSpuIds, activityList);
}
}
}
private static void buildAppActivityRespVO(RewardActivityDO rewardActivity, Collection<Long> spuIds,
List<AppActivityRespVO> activityList) {
for (Long spuId : spuIds) {
// 校验商品是否已经加入过活动
if (anyMatch(activityList, appActivity -> ObjUtil.equal(appActivity.getId(), rewardActivity.getId()) &&
ObjUtil.equal(appActivity.getSpuId(), spuId))) {
continue;
}
activityList.add(new AppActivityRespVO(rewardActivity.getId(),
PromotionTypeEnum.REWARD_ACTIVITY.getType(), rewardActivity.getName(), spuId,
rewardActivity.getStartTime(), rewardActivity.getEndTime()));
BargainActivityDO bargainActivity = bargainActivityService.getMatchBargainActivityBySpuId(spuId);
if (bargainActivity != null) {
activityVOList.add(new AppActivityRespVO(bargainActivity.getId(), PromotionTypeEnum.BARGAIN_ACTIVITY.getType(),
bargainActivity.getName(), bargainActivity.getSpuId(), bargainActivity.getStartTime(), bargainActivity.getEndTime()));
}
return success(activityVOList);
}
}

View File

@ -73,7 +73,7 @@ public class AppCouponTemplateController {
// 1.1 处理查询条件商品范围编号
Long productScopeValue = getProductScopeValue(productScope, spuId);
// 1.2 处理查询条件领取方式 = 直接领取
List<Integer> canTakeTypes = singletonList(CouponTakeTypeEnum.USER.getValue());
List<Integer> canTakeTypes = singletonList(CouponTakeTypeEnum.USER.getType());
// 2. 查询
List<CouponTemplateDO> list = couponTemplateService.getCouponTemplateList(canTakeTypes, productScope,
@ -105,7 +105,7 @@ public class AppCouponTemplateController {
// 1.1 处理查询条件商品范围编号
Long productScopeValue = getProductScopeValue(pageReqVO.getProductScope(), pageReqVO.getSpuId());
// 1.2 处理查询条件领取方式 = 直接领取
List<Integer> canTakeTypes = singletonList(CouponTakeTypeEnum.USER.getValue());
List<Integer> canTakeTypes = singletonList(CouponTakeTypeEnum.USER.getType());
// 2. 分页查询
PageResult<CouponTemplateDO> pageResult = couponTemplateService.getCouponTemplatePage(

View File

@ -0,0 +1,114 @@
package cn.iocoder.yudao.module.promotion.controller.app.point;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.point.vo.AppPointActivityDetailRespVO;
import cn.iocoder.yudao.module.promotion.controller.app.point.vo.AppPointActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.point.vo.AppPointActivityRespVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointProductDO;
import cn.iocoder.yudao.module.promotion.service.point.PointActivityService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
@Tag(name = "用户 App - 积分商城活动")
@RestController
@RequestMapping("/promotion/point-activity")
@Validated
public class AppPointActivityController {
@Resource
private PointActivityService pointActivityService;
@Resource
private ProductSpuApi productSpuApi;
@GetMapping("/page")
@Operation(summary = "获得积分商城活动分页")
public CommonResult<PageResult<AppPointActivityRespVO>> getPointActivityPage(AppPointActivityPageReqVO pageReqVO) {
// 1. 查询满足当前阶段的活动
PageResult<PointActivityDO> pageResult = pointActivityService.getPointActivityPage(
BeanUtils.toBean(pageReqVO, PointActivityPageReqVO.class));
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2. 拼接数据
List<AppPointActivityRespVO> resultList = buildAppPointActivityRespVOList(pageResult.getList());
return success(new PageResult<>(resultList, pageResult.getTotal()));
}
@GetMapping("/get-detail")
@Operation(summary = "获得积分商城活动明细")
@Parameter(name = "id", description = "活动编号", required = true, example = "1024")
public CommonResult<AppPointActivityDetailRespVO> getPointActivity(@RequestParam("id") Long id) {
// 1. 获取活动
PointActivityDO activity = pointActivityService.getPointActivity(id);
if (activity == null
|| ObjUtil.equal(activity.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
return success(null);
}
// 2. 拼接数据
List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(Collections.singletonList(id));
AppPointActivityDetailRespVO respVO = BeanUtils.toBean(activity, AppPointActivityDetailRespVO.class);
respVO.setProducts(BeanUtils.toBean(products, AppPointActivityDetailRespVO.Product.class));
return success(respVO);
}
@GetMapping("/list-by-ids")
@Operation(summary = "获得积分商城活动列表,基于活动编号数组")
@Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
public CommonResult<List<AppPointActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
// 1. 获得开启的活动列表
List<PointActivityDO> activityList = pointActivityService.getPointActivityListByIds(ids);
activityList.removeIf(activity -> CommonStatusEnum.isDisable(activity.getStatus()));
if (CollUtil.isEmpty(activityList)) {
return success(Collections.emptyList());
}
// 2. 拼接返回
List<AppPointActivityRespVO> result = buildAppPointActivityRespVOList(activityList);
return success(result);
}
private List<AppPointActivityRespVO> buildAppPointActivityRespVOList(List<PointActivityDO> activityList) {
List<PointProductDO> products = pointActivityService.getPointProductListByActivityIds(
convertSet(activityList, PointActivityDO::getId));
Map<Long, List<PointProductDO>> productsMap = convertMultiMap(products, PointProductDO::getActivityId);
Map<Long, ProductSpuRespDTO> spuMap = productSpuApi.getSpusMap(
convertSet(activityList, PointActivityDO::getSpuId));
List<AppPointActivityRespVO> result = BeanUtils.toBean(activityList, AppPointActivityRespVO.class);
result.forEach(activity -> {
// 设置 product 信息
PointProductDO minProduct = getMinObject(productsMap.get(activity.getId()), PointProductDO::getPoint);
assert minProduct != null;
activity.setPoint(minProduct.getPoint()).setPrice(minProduct.getPrice());
findAndThen(spuMap, activity.getSpuId(),
spu -> activity.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
});
return result;
}
}

View File

@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.promotion.controller.app.point.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "用户 App - 积分商城活动的详细 Response VO")
@Data
public class AppPointActivityDetailRespVO {
@Schema(description = "积分商城活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11373")
private Long id;
@Schema(description = "积分商城活动商品", requiredMode = Schema.RequiredMode.REQUIRED, example = "19509")
private Long spuId;
@Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer status;
@Schema(description = "积分商城活动库存(剩余库存积分兑换时扣减)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer stock;
@Schema(description = "积分商城活动总库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer totalStock;
@Schema(description = "备注", example = "你说的对")
private String remark;
@Schema(description = "商品信息数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<Product> products;
@Schema(description = "商品信息")
@Data
public static class Product {
@Schema(description = "积分商城商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31718")
private Long id;
@Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2736")
private Long skuId;
@Schema(description = "可兑换数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3926")
private Integer count;
@Schema(description = "兑换积分", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer point;
@Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860")
private Integer price;
@Schema(description = "积分商城商品库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer stock;
}
}

View File

@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.promotion.controller.app.point.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "用户 App - 积分商城活动分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AppPointActivityPageReqVO extends PageParam {
}

View File

@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.promotion.controller.app.point.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "用户 App - 积分商城活动 Response VO")
@Data
public class AppPointActivityRespVO {
@Schema(description = "积分商城活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11373")
@ExcelProperty("积分商城活动编号")
private Long id;
@Schema(description = "积分商城活动商品", requiredMode = Schema.RequiredMode.REQUIRED, example = "19509")
@ExcelProperty("积分商城活动商品")
private Long spuId;
@Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@ExcelProperty("活动状态")
private Integer status;
@Schema(description = "积分商城活动库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@ExcelProperty("积分商城活动库存")
private Integer stock; // 剩余库存积分兑换时扣减
@Schema(description = "积分商城活动总库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@ExcelProperty("积分商城活动总库存")
private Integer totalStock;
// TODO @puhui999只返回必要的字段例如说 remarksortcreateTime 应该是不需要的呢也可以看看别的也不需要哈
@Schema(description = "备注", example = "你说的对")
@ExcelProperty("备注")
private String remark;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("排序")
private Integer sort;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")
private LocalDateTime createTime;
// ========== 商品字段 ==========
@Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, // SPU name 读取
example = "618大促")
private String spuName;
@Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, // SPU picUrl 读取
example = "https://www.iocoder.cn/xx.png")
private String picUrl;
@Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, // SPU marketPrice 读取
example = "50")
private Integer marketPrice;
//======================= 显示所需兑换积分最少的 sku 信息 =======================
@Schema(description = "兑换积分", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer point;
@Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860")
private Integer price;
}

View File

@ -30,8 +30,18 @@ public class AppRewardActivityController {
@Operation(summary = "获得满减送活动")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<AppRewardActivityRespVO> getRewardActivity(@RequestParam("id") Long id) {
RewardActivityDO rewardActivity = rewardActivityService.getRewardActivity(id);
return success(BeanUtils.toBean(rewardActivity, AppRewardActivityRespVO.class));
RewardActivityDO activity = rewardActivityService.getRewardActivity(id);
if (activity == null) {
return success(null);
}
// 拼接 Rule 描述
AppRewardActivityRespVO activityVO = BeanUtils.toBean(activity, AppRewardActivityRespVO.class);
for (int i = 0; i < activityVO.getRules().size(); i++) {
AppRewardActivityRespVO.Rule ruleVO = activityVO.getRules().get(i);
RewardActivityDO.Rule rule = activity.getRules().get(i);
ruleVO.setDescription(rewardActivityService.getRewardActivityRuleDescription(activity.getConditionType(), rule));
}
return success(activityVO);
}
}

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivi
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "用户 App - 满减送活动 Response VO")
@ -19,6 +20,12 @@ public class AppRewardActivityRespVO {
@Schema(description = "活动标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "满啦满啦")
private String name;
@Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime startTime;
@Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime endTime;
@Schema(description = "条件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer conditionType;
@ -26,9 +33,18 @@ public class AppRewardActivityRespVO {
private Integer productScope;
@Schema(description = "商品 SPU 编号的数组", example = "1,2,3")
private List<Long> productSpuIds;
private List<Long> productScopeValues;
@Schema(description = "优惠规则的数组")
private List<RewardActivityBaseVO.Rule> rules;
private List<Rule> rules;
@Schema(description = "优惠规则")
@Data
public static class Rule extends RewardActivityBaseVO.Rule {
@Schema(description = "规则描述")
private String description; // 通过 {@link #limit}{@link #discountPrice} 等字段进行拼接
}
}

View File

@ -151,7 +151,7 @@ public class AppSeckillActivityController {
}
@GetMapping("/list-by-ids")
@Operation(summary = "获得拼团活动列表,基于活动编号数组")
@Operation(summary = "获得秒杀活动列表,基于活动编号数组")
@Parameter(name = "ids", description = "活动编号数组", required = true, example = "[1024, 1025]")
public CommonResult<List<AppSeckillActivityRespVO>> getCombinationActivityListByIds(@RequestParam("ids") List<Long> ids) {
// 1. 获得开启的活动列表

View File

@ -1,20 +1,17 @@
package cn.iocoder.yudao.module.promotion.convert.discount;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.*;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityBaseVO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.Map;
/**
* 限时折扣活动 Convert
@ -33,105 +30,22 @@ public interface DiscountActivityConvert {
DiscountActivityRespVO convert(DiscountActivityDO bean);
List<DiscountActivityRespVO> convertList(List<DiscountActivityDO> list);
List<DiscountActivityBaseVO.Product> convertList2(List<DiscountProductDO> list);
List<DiscountProductRespDTO> convertList02(List<DiscountProductDO> list);
List<DiscountActivityBaseVO.Product> convertList2(List<DiscountProductDO> list);
PageResult<DiscountActivityRespVO> convertPage(PageResult<DiscountActivityDO> page);
default PageResult<DiscountActivityRespVO> convertPage(PageResult<DiscountActivityDO> page,
List<DiscountProductDO> discountProductDOList,
List<ProductSpuRespDTO> spuList) {
List<DiscountProductDO> discountProductDOList) {
PageResult<DiscountActivityRespVO> pageResult = convertPage(page);
// 拼接商品 TODO @zhangshuai类似空行的问题也可以看看
Map<Long, DiscountProductDO> discountActivityMap = CollectionUtils.convertMap(discountProductDOList, DiscountProductDO::getActivityId);
Map<Long, ProductSpuRespDTO> spuMap = CollectionUtils.convertMap(spuList, ProductSpuRespDTO::getId);
pageResult.getList().forEach(item -> {
item.setProducts(convertList2(discountProductDOList));
item.setSpuId(discountActivityMap.get(item.getId())==null?null: discountActivityMap.get(item.getId()).getSpuId());
if (item.getSpuId() != null) {
MapUtils.findAndThen(spuMap, item.getSpuId(),
spu -> item.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl()).setMarketPrice(spu.getMarketPrice()));
}
});
pageResult.getList().forEach(item -> item.setProducts(convertList2(discountProductDOList)));
return pageResult;
}
DiscountProductDO convert(DiscountActivityBaseVO.Product bean);
default DiscountActivityDetailRespVO convert(DiscountActivityDO activity, List<DiscountProductDO> products){
if ( activity == null && products == null ) {
return null;
}
DiscountActivityDetailRespVO discountActivityDetailRespVO = new DiscountActivityDetailRespVO();
if ( activity != null ) {
discountActivityDetailRespVO.setName( activity.getName() );
discountActivityDetailRespVO.setStartTime( activity.getStartTime() );
discountActivityDetailRespVO.setEndTime( activity.getEndTime() );
discountActivityDetailRespVO.setRemark( activity.getRemark() );
discountActivityDetailRespVO.setId( activity.getId() );
discountActivityDetailRespVO.setStatus( activity.getStatus() );
discountActivityDetailRespVO.setCreateTime( activity.getCreateTime() );
}
if (!products.isEmpty()) {
discountActivityDetailRespVO.setSpuId(products.get(0).getSpuId());
}
discountActivityDetailRespVO.setProducts( convertList2( products ) );
return discountActivityDetailRespVO;
default DiscountActivityRespVO convert(DiscountActivityDO activity, List<DiscountProductDO> products) {
return BeanUtils.toBean(activity, DiscountActivityRespVO.class).setProducts(convertList2(products));
}
// =========== 比较是否相等 ==========
/**
* 比较两个限时折扣商品是否相等
*
* @param productDO 数据库中的商品
* @param productVO 前端传入的商品
* @return 是否匹配
*/
@SuppressWarnings("DuplicatedCode")
default boolean isEquals(DiscountProductDO productDO, DiscountActivityBaseVO.Product productVO) {
if (ObjectUtil.notEqual(productDO.getSpuId(), productVO.getSpuId())
|| ObjectUtil.notEqual(productDO.getSkuId(), productVO.getSkuId())
|| ObjectUtil.notEqual(productDO.getDiscountType(), productVO.getDiscountType())) {
return false;
}
if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PRICE.getType())) {
return ObjectUtil.equal(productDO.getDiscountPrice(), productVO.getDiscountPrice());
}
if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PERCENT.getType())) {
return ObjectUtil.equal(productDO.getDiscountPercent(), productVO.getDiscountPercent());
}
return true;
}
/**
* 比较两个限时折扣商品是否相等
* 注意比较时忽略 id 编号
*
* @param productDO 商品 1
* @param productVO 商品 2
* @return 是否匹配
*/
@SuppressWarnings("DuplicatedCode")
default boolean isEquals(DiscountProductDO productDO, DiscountProductDO productVO) {
if (ObjectUtil.notEqual(productDO.getSpuId(), productVO.getSpuId())
|| ObjectUtil.notEqual(productDO.getSkuId(), productVO.getSkuId())
|| ObjectUtil.notEqual(productDO.getDiscountType(), productVO.getDiscountType())) {
return false;
}
if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PRICE.getType())) {
return ObjectUtil.equal(productDO.getDiscountPrice(), productVO.getDiscountPrice());
}
if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PERCENT.getType())) {
return ObjectUtil.equal(productDO.getDiscountPercent(), productVO.getDiscountPercent());
}
return true;
}
}
}

View File

@ -66,10 +66,16 @@ public class DiscountProductDO extends BaseDO {
*/
private Integer discountPrice;
/**
* 活动标题
*
* 冗余 {@link DiscountActivityDO#getName()}
*/
private String activityName;
/**
* 活动状态
*
* 关联 {@link DiscountActivityDO#getStatus()}
* 冗余 {@link DiscountActivityDO#getStatus()}
*/
private Integer activityStatus;
/**

View File

@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.point;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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 lombok.ToString;
/**
* 积分商城活动 DO
*
* @author HUIHUI
*/
@TableName(value = "promotion_point_activity", autoResultMap = true)
@KeySequence("promotion_point_activity_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PointActivityDO extends BaseDO {
/**
* 积分商城活动编号
*/
@TableId
private Long id;
/**
* 积分商城活动商品
*/
private Long spuId;
/**
* 活动状态
*
* 枚举 {@link CommonStatusEnum 对应的类}
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 排序
*/
private Integer sort;
/**
* 积分商城活动库存(剩余库存积分兑换时扣减)
*/
private Integer stock;
/**
* 积分商城活动总库存
*/
private Integer totalStock;
}

View File

@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.point;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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.*;
/**
* 积分商城商品 DO
*
* @author HUIHUI
*/
@TableName("promotion_point_product")
@KeySequence("promotion_point_product_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PointProductDO extends BaseDO {
/**
* 积分商城商品编号
*/
@TableId
private Long id;
/**
* 积分商城活动 id
*
* 关联 {@link PointActivityDO#getId()}
*/
private Long activityId;
/**
* 商品 SPU 编号
*/
private Long spuId;
/**
* 商品 SKU 编号
*/
private Long skuId;
/**
* 可兑换次数
*/
private Integer count;
/**
* 所需兑换积分
*/
private Integer point;
/**
* 所需兑换金额单位
*/
private Integer price;
/**
* 积分商城商品库存
*/
private Integer stock;
/**
* 积分商城商品状态
*
* 枚举 {@link CommonStatusEnum 对应的类}
*/
private Integer activityStatus;
}

View File

@ -6,14 +6,11 @@ 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.bargain.vo.activity.BargainActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 砍价活动 Mapper
@ -86,35 +83,13 @@ public interface BargainActivityMapper extends BaseMapperX<BargainActivityDO> {
.last("LIMIT " + count));
}
/**
* 查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
*
* @param spuIds spu 编号
* @param status 状态
* @return 包含 spuId activityId map 对象列表
*/
default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
return selectMaps(new QueryWrapper<BargainActivityDO>()
.select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
.in("spu_id", spuIds)
.eq("status", status)
.groupBy("spu_id"));
}
/**
* 获取指定活动编号的活动列表且
* 开始时间和结束时间小于给定时间 dateTime 的活动列表
*
* @param ids 活动编号
* @param dateTime 指定日期
* @return 活动列表
*/
default List<BargainActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) {
return selectList(new LambdaQueryWrapperX<BargainActivityDO>()
.in(BargainActivityDO::getId, ids)
.lt(BargainActivityDO::getStartTime, dateTime)
.gt(BargainActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间也就是说获取指定时间段的活动
.orderByDesc(BargainActivityDO::getCreateTime));
default BargainActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) {
LocalDateTime now = LocalDateTime.now();
return selectOne(new LambdaQueryWrapperX<BargainActivityDO>()
.eq(BargainActivityDO::getSpuId, spuId)
.eq(BargainActivityDO::getStatus, status)
.lt(BargainActivityDO::getStartTime, now)
.gt(BargainActivityDO::getEndTime, now)); // 开始时间 < now < 结束时间也就是说获取指定时间段的活动
}
}

View File

@ -6,14 +6,10 @@ 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.combination.vo.activity.CombinationActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 拼团活动 Mapper
@ -39,40 +35,13 @@ public interface CombinationActivityMapper extends BaseMapperX<CombinationActivi
.eq(CombinationActivityDO::getStatus, status));
}
default List<CombinationActivityDO> selectListByStatus(Integer status, Integer count) {
return selectList(new LambdaQueryWrapperX<CombinationActivityDO>()
default CombinationActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) {
LocalDateTime now = LocalDateTime.now();
return selectOne(new LambdaQueryWrapperX<CombinationActivityDO>()
.eq(CombinationActivityDO::getSpuId, spuId)
.eq(CombinationActivityDO::getStatus, status)
.last("LIMIT " + count));
.lt(CombinationActivityDO::getStartTime, now)
.gt(CombinationActivityDO::getEndTime, now)); // 开始时间 < now < 结束时间也就是说获取指定时间段的活动
}
/**
* 查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
* @param spuIds spu 编号
* @param status 状态
* @return 包含 spuId activityId map 对象列表
*/
default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
return selectMaps(new QueryWrapper<CombinationActivityDO>()
.select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
.in("spu_id", spuIds)
.eq("status", status)
.groupBy("spu_id"));
}
/**
* 获取指定活动编号的活动列表且
* 开始时间和结束时间小于给定时间 dateTime 的活动列表
*
* @param ids 活动编号
* @param dateTime 指定日期
* @return 活动列表
*/
default List<CombinationActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) {
return selectList(new LambdaQueryWrapperX<CombinationActivityDO>()
.in(CombinationActivityDO::getId, ids)
.lt(CombinationActivityDO::getStartTime, dateTime)
.gt(CombinationActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间也就是说获取指定时间段的活动
.orderByDesc(CombinationActivityDO::getCreateTime));
}
}
}

View File

@ -70,7 +70,7 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
.in(CouponTemplateDO::getTakeType, canTakeTypes) // 2. 领取方式一致
.and(ww -> ww.gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()) // 3.1 未过期
.or().eq(CouponTemplateDO::getValidityType, CouponTemplateValidityTypeEnum.TERM.getType())) // 3.2 领取之后
.apply(" (take_count < total_count OR total_count = -1 )"); // 4. 剩余数量大于 0或者无限领取
.apply(" (take_count < total_count OR total_count = -1)"); // 4. 剩余数量大于 0或者无限领取
}
return canTakeConsumer;
}

View File

@ -1,14 +1,14 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.discount;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 限时折扣商城 Mapper
@ -18,10 +18,6 @@ import java.util.Map;
@Mapper
public interface DiscountProductMapper extends BaseMapperX<DiscountProductDO> {
default List<DiscountProductDO> selectListBySkuId(Collection<Long> skuIds) {
return selectList(DiscountProductDO::getSkuId, skuIds);
}
default List<DiscountProductDO> selectListByActivityId(Long activityId) {
return selectList(DiscountProductDO::getActivityId, activityId);
}
@ -30,22 +26,28 @@ public interface DiscountProductMapper extends BaseMapperX<DiscountProductDO> {
return selectList(DiscountProductDO::getActivityId, activityIds);
}
// TODO @zhangshuai逻辑里尽量避免写 join 语句哈你可以看看这个查询有什么办法优化目前的一个思路是分 2 次查询性能也是 ok
List<DiscountProductDO> getMatchDiscountProductList(@Param("skuIds") Collection<Long> skuIds);
default List<DiscountProductDO> selectListBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
return selectList(new LambdaQueryWrapperX<DiscountProductDO>()
.in(DiscountProductDO::getSpuId, spuIds)
.eq(DiscountProductDO::getActivityStatus, status));
}
/**
* 查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
*
* @param spuIds spu 编号
* @param status 状态
* @return 包含 spuId activityId map 对象列表
*/
default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
return selectMaps(new QueryWrapper<DiscountProductDO>()
.select("spu_id AS spuId, MAX(DISTINCT(activity_id)) AS activityId")
.in("spu_id", spuIds)
.eq("activity_status", status)
.groupBy("spu_id"));
default void updateByActivityId(DiscountProductDO discountProductDO) {
update(discountProductDO, new LambdaUpdateWrapper<DiscountProductDO>()
.eq(DiscountProductDO::getActivityId, discountProductDO.getActivityId()));
}
default void deleteByActivityId(Long activityId) {
delete(DiscountProductDO::getActivityId, activityId);
}
default List<DiscountProductDO> selectListBySkuIdsAndStatusAndNow(Collection<Long> skuIds, Integer status) {
LocalDateTime now = LocalDateTime.now();
return selectList(new LambdaQueryWrapperX<DiscountProductDO>()
.in(DiscountProductDO::getSkuId, skuIds)
.eq(DiscountProductDO::getActivityStatus,status)
.lt(DiscountProductDO::getActivityStartTime, now)
.gt(DiscountProductDO::getActivityEndTime, now));
}
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.point;
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.point.vo.activity.PointActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 积分商城活动 Mapper
*
* @author HUIHUI
*/
@Mapper
public interface PointActivityMapper extends BaseMapperX<PointActivityDO> {
default PageResult<PointActivityDO> selectPage(PointActivityPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PointActivityDO>()
.eqIfPresent(PointActivityDO::getSpuId, reqVO.getSpuId())
.eqIfPresent(PointActivityDO::getStatus, reqVO.getStatus())
.eqIfPresent(PointActivityDO::getRemark, reqVO.getRemark())
.eqIfPresent(PointActivityDO::getSort, reqVO.getSort())
.betweenIfPresent(PointActivityDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PointActivityDO::getId));
}
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.point;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointProductDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
* 积分商城商品 Mapper
*
* @author HUIHUI
*/
@Mapper
public interface PointProductMapper extends BaseMapperX<PointProductDO> {
default List<PointProductDO> selectListByActivityId(Collection<Long> activityIds) {
return selectList(PointProductDO::getActivityId, activityIds);
}
default List<PointProductDO> selectListByActivityId(Long activityId) {
return selectList(PointProductDO::getActivityId, activityId);
}
default void updateByActivityId(PointProductDO pointProductDO) {
update(pointProductDO, new LambdaUpdateWrapper<PointProductDO>()
.eq(PointProductDO::getActivityId, pointProductDO.getActivityId()));
}
}

View File

@ -6,7 +6,7 @@ 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.reward.vo.RewardActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
@ -30,29 +30,23 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> {
.orderByDesc(RewardActivityDO::getId));
}
default List<RewardActivityDO> selectListBySpuIdsAndStatus(Collection<Long> spuIds, Integer status) {
default List<RewardActivityDO> selectListBySpuIdAndStatusAndNow(Collection<Long> spuIds,
Collection<Long> categoryIds,
Integer status) {
LocalDateTime now = LocalDateTime.now();
Function<Collection<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
.map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
.collect(Collectors.joining(" OR "));
return selectList(new QueryWrapper<RewardActivityDO>()
.eq("status", status)
.apply(productScopeValuesFindInSetFunc.apply(spuIds)));
}
/**
* 获取指定活动编号的活动列表且
* 开始时间和结束时间小于给定时间 dateTime 的活动列表
*
* @param status 状态
* @param dateTime 指定日期
* @return 活动列表
*/
default List<RewardActivityDO> selectListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
.eq(RewardActivityDO::getStatus, status)
.lt(RewardActivityDO::getStartTime, dateTime)
.gt(RewardActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间也就是说获取指定时间段的活动
.orderByAsc(RewardActivityDO::getStartTime)
.lt(RewardActivityDO::getStartTime, now)
.gt(RewardActivityDO::getEndTime, now)
.and(i -> i.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.SPU.getScope())
.and(i1 -> i1.apply(productScopeValuesFindInSetFunc.apply(spuIds)))
.or(i1 -> i1.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.ALL.getScope()))
.or(i1 -> i1.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.CATEGORY.getScope())
.and(i2 -> i2.apply(productScopeValuesFindInSetFunc.apply(categoryIds)))))
.orderByDesc(RewardActivityDO::getId)
);
}

View File

@ -8,15 +8,11 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 秒杀活动 Mapper
@ -51,7 +47,7 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
Assert.isTrue(count > 0);
return update(null, new LambdaUpdateWrapper<SeckillActivityDO>()
.eq(SeckillActivityDO::getId, id)
.gt(SeckillActivityDO::getStock, count)
.ge(SeckillActivityDO::getStock, count)
.setSql("stock = stock - " + count));
}
@ -69,41 +65,21 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
.setSql("stock = stock + " + count));
}
default PageResult<SeckillActivityDO> selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status) {
default PageResult<SeckillActivityDO> selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status, LocalDateTime dateTime) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<SeckillActivityDO>()
.eqIfPresent(SeckillActivityDO::getStatus, status)
.lt(SeckillActivityDO::getStartTime, dateTime)
.gt(SeckillActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间也就是说获取指定时间段的活动
.apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
}
/**
* 查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
*
* @param spuIds spu 编号
* @param status 状态
* @return 包含 spuId activityId map 对象列表
*/
default List<Map<String, Object>> selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(@Param("spuIds") Collection<Long> spuIds, @Param("status") Integer status) {
return selectMaps(new QueryWrapper<SeckillActivityDO>()
.select("spu_id AS spuId, MAX(DISTINCT(id)) AS activityId") // 时间越大 id 也越大 直接用 id
.in("spu_id", spuIds)
.eq("status", status)
.groupBy("spu_id"));
}
/**
* 获取指定活动编号的活动列表且
* 开始时间和结束时间小于给定时间 dateTime 的活动列表
*
* @param ids 活动编号
* @param dateTime 指定日期
* @return 活动列表
*/
default List<SeckillActivityDO> selectListByIdsAndDateTimeLt(Collection<Long> ids, LocalDateTime dateTime) {
return selectList(new LambdaQueryWrapperX<SeckillActivityDO>()
.in(SeckillActivityDO::getId, ids)
.lt(SeckillActivityDO::getStartTime, dateTime)
.gt(SeckillActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间也就是说获取指定时间段的活动
.orderByDesc(SeckillActivityDO::getCreateTime));
default SeckillActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) {
LocalDateTime now = LocalDateTime.now();
return selectOne(new LambdaQueryWrapperX<SeckillActivityDO>()
.eq(SeckillActivityDO::getSpuId, spuId)
.eq(SeckillActivityDO::getStatus, status)
.lt(SeckillActivityDO::getStartTime, now)
.gt(SeckillActivityDO::getEndTime, now)); // 开始时间 < now < 结束时间也就是说获取指定时间段的活动
}
}

View File

@ -6,10 +6,8 @@ import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.Ba
import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.BargainActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.BargainActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
import jakarta.validation.Valid;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Set;
@ -108,13 +106,11 @@ public interface BargainActivityService {
List<BargainActivityDO> getBargainActivityListByCount(Integer count);
/**
* 取指定 spu 编号最近参加的活动每个 spuId 只返回一条记录
* SPU 进行中的砍价活动
*
* @param spuIds spu 编号
* @param status 状态
* @param dateTime 日期时间
* @return 砍价活动列表
* @param spuId SPU 编号数组
* @return 砍价活动
*/
List<BargainActivityDO> getBargainActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
BargainActivityDO getMatchBargainActivityBySpuId(Long spuId);
}

View File

@ -1,7 +1,5 @@
package cn.iocoder.yudao.module.promotion.service.bargain;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
@ -15,17 +13,17 @@ import cn.iocoder.yudao.module.promotion.controller.admin.bargain.vo.activity.Ba
import cn.iocoder.yudao.module.promotion.convert.bargain.BargainActivityConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.bargain.BargainActivityDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.bargain.BargainActivityMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
@ -194,15 +192,8 @@ public class BargainActivityServiceImpl implements BargainActivityService {
}
@Override
public List<BargainActivityDO> getBargainActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
// 1. 查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
List<Map<String, Object>> spuIdAndActivityIdMaps = bargainActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
return Collections.emptyList();
}
// 2. 查询活动详情
return bargainActivityMapper.selectListByIdsAndDateTimeLt(
convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), dateTime);
public BargainActivityDO getMatchBargainActivityBySpuId(Long spuId) {
return bargainActivityMapper.selectBySpuIdAndStatusAndNow(spuId, CommonStatusEnum.ENABLE.getStatus());
}
}

View File

@ -7,9 +7,8 @@ import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activit
import cn.iocoder.yudao.module.promotion.controller.admin.combination.vo.activity.CombinationActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO;
import jakarta.validation.Valid;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@ -109,22 +108,20 @@ public interface CombinationActivityService {
PageResult<CombinationActivityDO> getCombinationActivityPage(PageParam pageParam);
/**
* 获取指定活动指定 sku 编号的商品
* 获取指定活动指定 SKU 编号的商品
*
* @param activityId 活动编号
* @param skuId sku 编号
* @param skuId SKU 编号
* @return 活动商品信息
*/
CombinationProductDO selectByActivityIdAndSkuId(Long activityId, Long skuId);
/**
* 取指定 spu 编号最近参加的活动每个 spuId 只返回一条记录
* SPU 进行中的拼团活动
*
* @param spuIds spu 编号
* @param status 状态
* @param dateTime 日期时间
* @return 拼团活动列表
* @param spuId SPU 编号数组
* @return 拼团活动
*/
List<CombinationActivityDO> getCombinationActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
CombinationActivityDO getMatchCombinationActivityBySpuId(Long spuId);
}

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.promotion.service.combination;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
@ -20,19 +19,18 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationA
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationProductDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationActivityMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationProductMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
@ -178,7 +176,7 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
combinationProductMapper.updateBatch(diffList.get(1));
}
if (CollUtil.isNotEmpty(diffList.get(2))) {
combinationProductMapper.deleteBatchIds(CollectionUtils.convertList(diffList.get(2), CombinationProductDO::getId));
combinationProductMapper.deleteByIds(CollectionUtils.convertList(diffList.get(2), CombinationProductDO::getId));
}
}
@ -238,15 +236,8 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
}
@Override
public List<CombinationActivityDO> getCombinationActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
// 1.查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
List<Map<String, Object>> spuIdAndActivityIdMaps = combinationActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
return Collections.emptyList();
}
// 2.查询活动详情
return combinationActivityMapper.selectListByIdsAndDateTimeLt(
convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), dateTime);
public CombinationActivityDO getMatchCombinationActivityBySpuId(Long spuId) {
return combinationActivityMapper.selectBySpuIdAndStatusAndNow(spuId, CommonStatusEnum.ENABLE.getStatus());
}
}

View File

@ -279,7 +279,7 @@ public class CouponServiceImpl implements CouponService {
}
}
// 校验领取方式
if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getValue())) {
if (ObjectUtil.notEqual(couponTemplate.getTakeType(), takeType.getType())) {
throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
}
}

View File

@ -12,10 +12,10 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponTemplateMapper;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@ -57,8 +57,10 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
public void updateCouponTemplate(CouponTemplateUpdateReqVO updateReqVO) {
// 校验存在
CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId());
// 校验发放数量不能过小
if (updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) {
// 校验发放数量不能过小仅在 CouponTakeTypeEnum.USER 用户领取时
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
&& updateReqVO.getTotalCount() > 0 // 大于 0 的原因是因为 -1 不限制
&& updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) {
throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount());
}
// 校验商品范围
@ -118,7 +120,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
@Override
public List<CouponTemplateDO> getCouponTemplateListByTakeType(CouponTakeTypeEnum takeType) {
return couponTemplateMapper.selectListByTakeType(takeType.getValue());
return couponTemplateMapper.selectListByTakeType(takeType.getType());
}
@Override

View File

@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivit
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import jakarta.validation.Valid;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
@ -27,7 +26,7 @@ public interface DiscountActivityService {
* @param skuIds SKU 编号数组
* @return 匹配的限时折扣商品
*/
List<DiscountProductDO> getMatchDiscountProductList(Collection<Long> skuIds);
List<DiscountProductDO> getMatchDiscountProductListBySkuIds(Collection<Long> skuIds);
/**
* 创建限时折扣活动
@ -90,15 +89,4 @@ public interface DiscountActivityService {
*/
List<DiscountProductDO> getDiscountProductsByActivityId(Collection<Long> activityIds);
/**
* 获取指定 spu 编号最近参加的活动每个 spuId 只返回一条记录
*
* @param spuIds spu 编号
* @param status 状态
* @param dateTime 当前日期时间
* @return 折扣活动列表
*/
List<DiscountActivityDO> getDiscountActivityBySpuIdsAndStatusAndDateTimeLt(
Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
}

View File

@ -1,11 +1,13 @@
package cn.iocoder.yudao.module.promotion.service.discount;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityBaseVO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityPageReqVO;
@ -15,23 +17,20 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivit
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.discount.DiscountActivityMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.discount.DiscountProductMapper;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnum;
import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.hutool.core.collection.CollUtil.intersectionDistinct;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
/**
@ -48,16 +47,16 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
@Resource
private DiscountProductMapper discountProductMapper;
@Override
public List<DiscountProductDO> getMatchDiscountProductList(Collection<Long> skuIds) {
return discountProductMapper.getMatchDiscountProductList(skuIds);
}
@Resource
private ProductSkuApi productSkuApi;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createDiscountActivity(DiscountActivityCreateReqVO createReqVO) {
// 校验商品是否冲突
validateDiscountActivityProductConflicts(null, createReqVO.getProducts());
// 校验商品是否存在
validateProductExists(createReqVO.getProducts());
// 插入活动
DiscountActivityDO discountActivity = DiscountActivityConvert.INSTANCE.convert(createReqVO)
@ -65,7 +64,8 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
discountActivityMapper.insert(discountActivity);
// 插入商品
List<DiscountProductDO> discountProducts = BeanUtils.toBean(createReqVO.getProducts(), DiscountProductDO.class,
product -> product.setActivityId(discountActivity.getId()).setActivityStatus(discountActivity.getStatus())
product -> product.setActivityId(discountActivity.getId())
.setActivityName(discountActivity.getName()).setActivityStatus(discountActivity.getStatus())
.setActivityStartTime(createReqVO.getStartTime()).setActivityEndTime(createReqVO.getEndTime()));
discountProductMapper.insertBatch(discountProducts);
// 返回
@ -82,36 +82,40 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
}
// 校验商品是否冲突
validateDiscountActivityProductConflicts(updateReqVO.getId(), updateReqVO.getProducts());
// 校验商品是否存在
validateProductExists(updateReqVO.getProducts());
// 更新活动
DiscountActivityDO updateObj = DiscountActivityConvert.INSTANCE.convert(updateReqVO)
.setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime()));
DiscountActivityDO updateObj = DiscountActivityConvert.INSTANCE.convert(updateReqVO);
discountActivityMapper.updateById(updateObj);
// 更新商品
updateDiscountProduct(updateReqVO);
updateDiscountProduct(updateObj, updateReqVO.getProducts());
}
private void updateDiscountProduct(DiscountActivityUpdateReqVO updateReqVO) {
// TODO @zhangshuai这里的逻辑可以优化下哈参考 CombinationActivityServiceImpl updateCombinationProduct主要是 CollectionUtils.diffList 的使用哈
// 然后原先是使用 DiscountActivityConvert.INSTANCE.isEquals 对比现在看看是不是简化就基于 skuId 对比就完事了之前写的太精细意义不大
List<DiscountProductDO> dbDiscountProducts = discountProductMapper.selectListByActivityId(updateReqVO.getId());
// 计算要删除的记录
List<Long> deleteIds = convertList(dbDiscountProducts, DiscountProductDO::getId,
discountProductDO -> updateReqVO.getProducts().stream()
.noneMatch(product -> DiscountActivityConvert.INSTANCE.isEquals(discountProductDO, product)));
if (CollUtil.isNotEmpty(deleteIds)) {
discountProductMapper.deleteBatchIds(deleteIds);
private void updateDiscountProduct(DiscountActivityDO activity, List<DiscountActivityCreateReqVO.Product> products) {
// 第一步对比新老数据获得添加修改删除的列表
List<DiscountProductDO> newList = BeanUtils.toBean(products, DiscountProductDO.class,
product -> product.setActivityId(activity.getId())
.setActivityName(activity.getName()).setActivityStatus(activity.getStatus())
.setActivityStartTime(activity.getStartTime()).setActivityEndTime(activity.getEndTime()));
List<DiscountProductDO> oldList = discountProductMapper.selectListByActivityId(activity.getId());
List<List<DiscountProductDO>> diffList = CollectionUtils.diffList(oldList, newList, (oldVal, newVal) -> {
boolean same = ObjectUtil.equal(oldVal.getSkuId(), newVal.getSkuId());
if (same) {
newVal.setId(oldVal.getId());
}
return same;
});
// 第二步批量添加修改删除
if (CollUtil.isNotEmpty(diffList.get(0))) {
discountProductMapper.insertBatch(diffList.get(0));
}
// 计算新增的记录
List<DiscountProductDO> newDiscountProducts = convertList(updateReqVO.getProducts(),
product -> DiscountActivityConvert.INSTANCE.convert(product)
.setActivityId(updateReqVO.getId())
.setActivityStartTime(updateReqVO.getStartTime())
.setActivityEndTime(updateReqVO.getEndTime()));
newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch(
dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到说明是更新的
if (CollectionUtil.isNotEmpty(newDiscountProducts)) {
discountProductMapper.insertBatch(newDiscountProducts);
if (CollUtil.isNotEmpty(diffList.get(1))) {
discountProductMapper.updateBatch(diffList.get(1));
}
if (CollUtil.isNotEmpty(diffList.get(2))) {
discountProductMapper.deleteByIds(convertList(diffList.get(2), DiscountProductDO::getId));
}
}
@ -122,22 +126,44 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
* @param products 商品列表
*/
private void validateDiscountActivityProductConflicts(Long id, List<DiscountActivityBaseVO.Product> products) {
if (CollUtil.isEmpty(products)) {
return;
}
// 查询商品参加的活动
// TODO @zhangshuai下面 121 这个查询是不是不用做呀直接 convert skuId 集合就 ok
List<DiscountProductDO> list = discountProductMapper.selectListByActivityId(id);
// TODO @zhangshuai一般简单的 stream 方法建议是使用 CollectionUtils例如说这里是 convertList 对把
List<Long> skuIds = list.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
List<DiscountProductDO> matchDiscountProductList = getMatchDiscountProductList(skuIds);
if (id != null) { // 排除自己这个活动
matchDiscountProductList.removeIf(product -> id.equals(product.getActivityId()));
}
// 如果非空则说明冲突
if (CollUtil.isNotEmpty(matchDiscountProductList)) {
throw exception(DISCOUNT_ACTIVITY_SPU_CONFLICTS);
// 1.1 查询所有开启的折扣活动
List<DiscountActivityDO> activityList = discountActivityMapper.selectList(DiscountActivityDO::getStatus,
CommonStatusEnum.ENABLE.getStatus());
if (id != null) { // 时排除自己
activityList.removeIf(item -> ObjectUtil.equal(item.getId(), id));
}
// 1.2 查询活动下的所有商品
List<DiscountProductDO> productList = discountProductMapper.selectListByActivityId(
convertList(activityList, DiscountActivityDO::getId));
Map<Long, List<DiscountProductDO>> productListMap = convertMultiMap(productList, DiscountProductDO::getActivityId);
// 2. 校验商品是否冲突
activityList.forEach(item -> {
findAndThen(productListMap, item.getId(), discountProducts -> {
if (!intersectionDistinct(convertList(discountProducts, DiscountProductDO::getSpuId),
convertList(products, DiscountActivityBaseVO.Product::getSpuId)).isEmpty()) {
throw exception(DISCOUNT_ACTIVITY_SPU_CONFLICTS, item.getName());
}
});
});
}
/**
* 校验活动商品是否都存在
*
* @param products 活动商品
*/
private void validateProductExists(List<DiscountActivityBaseVO.Product> products) {
// 1.获得商品所有的 sku
List<ProductSkuRespDTO> skus = productSkuApi.getSkuListBySpuId(
convertList(products, DiscountActivityBaseVO.Product::getSpuId));
Map<Long, ProductSkuRespDTO> skuMap = convertMap(skus, ProductSkuRespDTO::getId);
// 2. 校验商品 sku 都存在
products.forEach(product -> {
if (!skuMap.containsKey(product.getSkuId())) {
throw exception(SKU_NOT_EXISTS);
}
});
}
@Override
@ -148,9 +174,11 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
throw exception(DISCOUNT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED);
}
// 更新
DiscountActivityDO updateObj = new DiscountActivityDO().setId(id).setStatus(PromotionActivityStatusEnum.CLOSE.getStatus());
discountActivityMapper.updateById(updateObj);
// 更新活动状态
discountActivityMapper.updateById(new DiscountActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus()));
// 更新活动商品状态
discountProductMapper.updateByActivityId(new DiscountProductDO().setActivityId(id).setActivityStatus(
CommonStatusEnum.DISABLE.getStatus()));
}
@Override
@ -161,8 +189,10 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
throw exception(DISCOUNT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED);
}
// 删除
// 删除活动
discountActivityMapper.deleteById(id);
// 删除活动商品
discountProductMapper.deleteByActivityId(id);
}
private DiscountActivityDO validateDiscountActivityExists(Long id) {
@ -190,20 +220,12 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
@Override
public List<DiscountProductDO> getDiscountProductsByActivityId(Collection<Long> activityIds) {
return discountProductMapper.selectList("activity_id", activityIds);
return discountProductMapper.selectList(DiscountProductDO::getActivityId, activityIds);
}
@Override
public List<DiscountActivityDO> getDiscountActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
// 1. 查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
List<Map<String, Object>> spuIdAndActivityIdMaps = discountProductMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
return Collections.emptyList();
}
// 2. 查询活动详情
return discountActivityMapper.selectListByIdsAndDateTimeLt(
convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), dateTime);
public List<DiscountProductDO> getMatchDiscountProductListBySkuIds(Collection<Long> skuIds) {
return discountProductMapper.selectListBySkuIdsAndStatusAndNow(skuIds, CommonStatusEnum.ENABLE.getStatus());
}
}

View File

@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.promotion.service.point;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivitySaveReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointProductDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* 积分商城活动 Service 接口
*
* @author HUIHUI
*/
public interface PointActivityService {
/**
* 创建积分商城活动
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createPointActivity(@Valid PointActivitySaveReqVO createReqVO);
/**
* 更新积分商城活动
*
* @param updateReqVO 更新信息
*/
void updatePointActivity(@Valid PointActivitySaveReqVO updateReqVO);
/**
* 关闭积分商城活动
*
* @param id 编号
*/
void closePointActivity(Long id);
/**
* 删除积分商城活动
*
* @param id 编号
*/
void deletePointActivity(Long id);
/**
* 获得积分商城活动
*
* @param id 编号
* @return 积分商城活动
*/
PointActivityDO getPointActivity(Long id);
/**
* 获得积分商城活动分页
*
* @param pageReqVO 分页查询
* @return 积分商城活动分页
*/
PageResult<PointActivityDO> getPointActivityPage(PointActivityPageReqVO pageReqVO);
/**
* 获得积分商城活动列表
*
* @param ids 活动编号
* @return 积分商城活动列表
*/
List<PointActivityDO> getPointActivityListByIds(Collection<Long> ids);
/**
* 获得活动商品
*
* @param activityIds 活动编号
* @return 获得活动商品
*/
List<PointProductDO> getPointProductListByActivityIds(Collection<Long> activityIds);
}

View File

@ -0,0 +1,247 @@
package cn.iocoder.yudao.module.promotion.service.point;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity.PointActivitySaveReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductSaveReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.point.PointProductDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.point.PointActivityMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.point.PointProductMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.hutool.core.collection.CollUtil.intersectionDistinct;
import static cn.hutool.core.collection.CollUtil.isNotEmpty;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
import static java.util.Collections.singletonList;
/**
* 积分商城活动 Service 实现类
*
* @author HUIHUI
*/
@Service
@Validated
public class PointActivityServiceImpl implements PointActivityService {
@Resource
private PointActivityMapper pointActivityMapper;
@Resource
private PointProductMapper pointProductMapper;
@Resource
private ProductSpuApi productSpuApi;
@Resource
private ProductSkuApi productSkuApi;
private static List<PointProductDO> buildPointProductDO(PointActivityDO pointActivity, List<PointProductSaveReqVO> products) {
return BeanUtils.toBean(products, PointProductDO.class, product ->
product.setSpuId(pointActivity.getSpuId()).setActivityId(pointActivity.getId())
.setActivityStatus(pointActivity.getStatus()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createPointActivity(PointActivitySaveReqVO createReqVO) {
// 1.1 校验商品是否存在
validateProductExists(createReqVO.getSpuId(), createReqVO.getProducts());
// 1.2 校验商品是否已经参加别的活动
validatePointActivityProductConflicts(null, createReqVO.getProducts());
// 2.1 插入积分商城活动
PointActivityDO pointActivity = BeanUtils.toBean(createReqVO, PointActivityDO.class)
.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setStock(getSumValue(createReqVO.getProducts(), PointProductSaveReqVO::getStock, Integer::sum));
pointActivity.setTotalStock(pointActivity.getStock());
pointActivityMapper.insert(pointActivity);
// 2.2 插入积分商城活动商品
pointProductMapper.insertBatch(buildPointProductDO(pointActivity, createReqVO.getProducts()));
return pointActivity.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updatePointActivity(PointActivitySaveReqVO updateReqVO) {
// 1.1 校验存在
PointActivityDO activity = validatePointActivityExists(updateReqVO.getId());
if (CommonStatusEnum.DISABLE.getStatus().equals(activity.getStatus())) {
throw exception(POINT_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED);
}
// 1.2 校验商品是否存在
validateProductExists(updateReqVO.getSpuId(), updateReqVO.getProducts());
// 1.3 校验商品是否已经参加别的活动
validatePointActivityProductConflicts(updateReqVO.getId(), updateReqVO.getProducts());
// 2.1 更新积分商城活动
PointActivityDO updateObj = BeanUtils.toBean(updateReqVO, PointActivityDO.class)
.setStock(getSumValue(updateReqVO.getProducts(), PointProductSaveReqVO::getStock, Integer::sum));
if (updateObj.getStock() > activity.getTotalStock()) { // 如果更新的库存大于原来的库存则更新总库存
updateObj.setTotalStock(updateObj.getStock());
}
pointActivityMapper.updateById(updateObj);
// 2.2 更新商品
updateSeckillProduct(updateObj, updateReqVO.getProducts());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void closePointActivity(Long id) {
// 校验存在
PointActivityDO pointActivity = validatePointActivityExists(id);
if (CommonStatusEnum.DISABLE.getStatus().equals(pointActivity.getStatus())) {
throw exception(POINT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED);
}
// 更新
pointActivityMapper.updateById(new PointActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus()));
// 更新活动商品状态
pointProductMapper.updateByActivityId(new PointProductDO().setActivityId(id).setActivityStatus(
CommonStatusEnum.DISABLE.getStatus()));
}
/**
* 更新秒杀商品
*
* @param activity 秒杀活动
* @param products 该活动的最新商品配置
*/
private void updateSeckillProduct(PointActivityDO activity, List<PointProductSaveReqVO> products) {
// 第一步对比新老数据获得添加修改删除的列表
List<PointProductDO> newList = buildPointProductDO(activity, products);
List<PointProductDO> oldList = pointProductMapper.selectListByActivityId(activity.getId());
List<List<PointProductDO>> diffList = diffList(oldList, newList, (oldVal, newVal) -> {
boolean same = ObjectUtil.equal(oldVal.getSkuId(), newVal.getSkuId());
if (same) {
newVal.setId(oldVal.getId());
}
return same;
});
// 第二步批量添加修改删除
if (isNotEmpty(diffList.get(0))) {
pointProductMapper.insertBatch(diffList.get(0));
}
if (isNotEmpty(diffList.get(1))) {
pointProductMapper.updateBatch(diffList.get(1));
}
if (isNotEmpty(diffList.get(2))) {
pointProductMapper.deleteByIds(convertList(diffList.get(2), PointProductDO::getId));
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deletePointActivity(Long id) {
// 校验存在
PointActivityDO pointActivity = validatePointActivityExists(id);
if (CommonStatusEnum.ENABLE.getStatus().equals(pointActivity.getStatus())) {
throw exception(POINT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END);
}
// 删除商城活动
pointActivityMapper.deleteById(id);
// 删除活动商品
List<PointProductDO> products = pointProductMapper.selectListByActivityId(id);
pointProductMapper.deleteByIds(convertSet(products, PointProductDO::getId));
}
private PointActivityDO validatePointActivityExists(Long id) {
PointActivityDO pointActivityDO = pointActivityMapper.selectById(id);
if (pointActivityDO == null) {
throw exception(POINT_ACTIVITY_NOT_EXISTS);
}
return pointActivityDO;
}
/**
* 校验秒杀商品是否都存在
*
* @param spuId 商品 SPU 编号
* @param products 秒杀商品
*/
private void validateProductExists(Long spuId, List<PointProductSaveReqVO> products) {
// 1. 校验商品 spu 是否存在
ProductSpuRespDTO spu = productSpuApi.getSpu(spuId);
if (spu == null) {
throw exception(SPU_NOT_EXISTS);
}
// 2. 校验商品 sku 都存在
List<ProductSkuRespDTO> skus = productSkuApi.getSkuListBySpuId(singletonList(spuId));
Map<Long, ProductSkuRespDTO> skuMap = convertMap(skus, ProductSkuRespDTO::getId);
products.forEach(product -> {
if (!skuMap.containsKey(product.getSkuId())) {
throw exception(SKU_NOT_EXISTS);
}
});
}
/**
* 校验商品是否冲突
*
* @param id 编号
* @param products 商品列表
*/
private void validatePointActivityProductConflicts(Long id, List<PointProductSaveReqVO> products) {
// 1.1 查询所有开启的积分商城活动
List<PointActivityDO> activityList = pointActivityMapper.selectList(PointActivityDO::getStatus,
CommonStatusEnum.ENABLE.getStatus());
if (id != null) { // 更新时排除自己
activityList.removeIf(item -> ObjectUtil.equal(item.getId(), id));
}
// 1.2 查询活动下的所有商品
List<PointProductDO> productList = pointProductMapper.selectListByActivityId(
convertList(activityList, PointActivityDO::getId));
Map<Long, List<PointProductDO>> productListMap = convertMultiMap(productList, PointProductDO::getActivityId);
// 2. 校验商品是否冲突
activityList.forEach(item -> {
findAndThen(productListMap, item.getId(), discountProducts -> {
if (!intersectionDistinct(convertList(discountProducts, PointProductDO::getSpuId),
convertList(products, PointProductSaveReqVO::getSpuId)).isEmpty()) {
throw exception(POINT_ACTIVITY_SPU_CONFLICTS);
}
});
});
}
@Override
public PointActivityDO getPointActivity(Long id) {
return pointActivityMapper.selectById(id);
}
@Override
public PageResult<PointActivityDO> getPointActivityPage(PointActivityPageReqVO pageReqVO) {
return pointActivityMapper.selectPage(pageReqVO);
}
@Override
public List<PointActivityDO> getPointActivityListByIds(Collection<Long> ids) {
return pointActivityMapper.selectList(PointActivityDO::getId, ids);
}
@Override
public List<PointProductDO> getPointProductListByActivityIds(Collection<Long> activityIds) {
return pointProductMapper.selectListByActivityId(activityIds);
}
}

View File

@ -1,17 +1,23 @@
package cn.iocoder.yudao.module.promotion.service.reward;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import jakarta.validation.Valid;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue;
/**
* 满减送活动 Service 接口
*
@ -65,20 +71,35 @@ public interface RewardActivityService {
PageResult<RewardActivityDO> getRewardActivityPage(RewardActivityPageReqVO pageReqVO);
/**
* 基于指定的 SPU 编号数组获得它们匹配的满减送活动
* 获得 spuId 商品匹配的的满减送活动列表
*
* @param spuIds SPU 编号数组
* @param spuIds SPU 编号数组
* @return 满减送活动列表
*/
List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds);
List<RewardActivityMatchRespDTO> getMatchRewardActivityListBySpuIds(Collection<Long> spuIds);
/**
* 获取指定 spu 编号最近参加的活动每个 spuId 只返回一条记录
*
* @param status 状态
* @param dateTime 当前日期时间
* @return 满减送活动列表
*/
List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime);
default String getRewardActivityRuleDescription(Integer conditionType, RewardActivityDO.Rule rule) {
String description = "";
if (PromotionConditionTypeEnum.PRICE.getType().equals(conditionType)) {
description += StrUtil.format("满 {} 元", MoneyUtils.fenToYuanStr(rule.getLimit()));
} else {
description += StrUtil.format("满 {} 件", rule.getLimit());
}
List<String> tips = new ArrayList<>(10);
if (rule.getDiscountPrice() != null) {
tips.add(StrUtil.format("减 {}", MoneyUtils.fenToYuanStr(rule.getDiscountPrice())));
}
if (Boolean.TRUE.equals(rule.getFreeDelivery())) {
tips.add("包邮");
}
if (rule.getPoint() != null && rule.getPoint() > 0) {
tips.add(StrUtil.format("送 {} 积分", rule.getPoint()));
}
if (CollUtil.isNotEmpty(rule.getGiveCouponTemplateCounts())) {
tips.add(StrUtil.format("送 {} 张优惠券",
getSumValue(rule.getGiveCouponTemplateCounts().values(), count -> count, Integer::sum)));
}
return description + StrUtil.join("", tips);
}
}

View File

@ -1,10 +1,13 @@
package cn.iocoder.yudao.module.promotion.service.reward;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityBaseVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
@ -13,19 +16,15 @@ import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivi
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.*;
import static cn.hutool.core.collection.CollUtil.intersectionDistinct;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
/**
@ -52,9 +51,9 @@ public class RewardActivityServiceImpl implements RewardActivityService {
// 1.2 校验商品是否冲突
validateRewardActivitySpuConflicts(null, createReqVO);
// 2. 插入
// 插入
RewardActivityDO rewardActivity = BeanUtils.toBean(createReqVO, RewardActivityDO.class)
.setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime()));
.setStatus(CommonStatusEnum.ENABLE.getStatus());
rewardActivityMapper.insert(rewardActivity);
// 返回
return rewardActivity.getId();
@ -73,8 +72,7 @@ public class RewardActivityServiceImpl implements RewardActivityService {
validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO);
// 2. 更新
RewardActivityDO updateObj = BeanUtils.toBean(updateReqVO, RewardActivityDO.class)
.setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime()));
RewardActivityDO updateObj = BeanUtils.toBean(updateReqVO, RewardActivityDO.class);
rewardActivityMapper.updateById(updateObj);
}
@ -87,8 +85,7 @@ public class RewardActivityServiceImpl implements RewardActivityService {
}
// 更新
RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus());
rewardActivityMapper.updateById(updateObj);
rewardActivityMapper.updateById(new RewardActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus()));
}
@Override
@ -118,22 +115,61 @@ public class RewardActivityServiceImpl implements RewardActivityService {
* @param rewardActivity 请求
*/
private void validateRewardActivitySpuConflicts(Long id, RewardActivityBaseVO rewardActivity) {
List<RewardActivityDO> list = rewardActivityMapper.selectList(RewardActivityDO::getProductScope,
rewardActivity.getProductScope(), RewardActivityDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
// 1. 获得开启的所有的活动
List<RewardActivityDO> list = rewardActivityMapper.selectList(RewardActivityDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
if (id != null) { // 排除自己这个活动
list.removeIf(activity -> id.equals(activity.getId()));
}
// 情况一全部商品参加
if (PromotionProductScopeEnum.isAll(rewardActivity.getProductScope()) && !list.isEmpty()) {
throw exception(REWARD_ACTIVITY_SCOPE_ALL_EXISTS);
}
if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) || // 情况二指定商品参加
PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) { // 情况三指定商品类型参加
if (anyMatch(list, item -> !intersectionDistinct(item.getProductScopeValues(),
rewardActivity.getProductScopeValues()).isEmpty())) {
throw exception(PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope()) ?
REWARD_ACTIVITY_SPU_CONFLICTS : REWARD_ACTIVITY_SCOPE_CATEGORY_EXISTS);
// 2. 完全不允许重叠
for (RewardActivityDO item : list) {
// 2.1 校验满减送活动时间是否冲突如果时段不冲突那么不同的时间段内则可以存在相同的商品范围
if (!LocalDateTimeUtil.isOverlap(item.getStartTime(), item.getEndTime(),
rewardActivity.getStartTime(), rewardActivity.getEndTime())) {
continue;
}
// 2.2 校验商品范围是否重叠
// 情况一如果与该时间段内商品范围为全部的活动冲突 rewardActivity 商品范围为全部那么则直接校验不通过
// 例如说rewardActivity 是全部活动结果有个 db 里的 activity 是某个分类它也是冲突的也就是说当前时间段内有且仅有只能有一个活动
if (PromotionProductScopeEnum.isAll(item.getProductScope()) ||
PromotionProductScopeEnum.isAll(rewardActivity.getProductScope())) {
throw exception(REWARD_ACTIVITY_SCOPE_EXISTS, item.getName(),
PromotionProductScopeEnum.isAll(item.getProductScope()) ?
"该活动商品范围为全部已覆盖包含本活动范围" : "本活动商品范围为全部已覆盖包含了该活动商品范围");
}
// 情况二如果与该时间段内商品范围为类别的活动冲突
if (PromotionProductScopeEnum.isCategory(item.getProductScope())) {
// 校验分类是否冲突
if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
if (!intersectionDistinct(item.getProductScopeValues(), rewardActivity.getProductScopeValues()).isEmpty()) {
throw exception(REWARD_ACTIVITY_SCOPE_EXISTS, item.getName(), "商品分类范围重叠");
}
}
// 校验商品分类是否冲突
if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(rewardActivity.getProductScopeValues());
if (!intersectionDistinct(item.getProductScopeValues(),
convertSet(spuList, ProductSpuRespDTO::getCategoryId)).isEmpty()) {
throw exception(REWARD_ACTIVITY_SCOPE_EXISTS, item.getName(), "该活动商品分类范围已包含本活动所选商品");
}
}
}
// 情况三如果与该时间段内商品范围为商品的活动冲突
if (PromotionProductScopeEnum.isSpu(item.getProductScope())) {
// 校验商品是否冲突
if (PromotionProductScopeEnum.isSpu(rewardActivity.getProductScope())) {
if (!intersectionDistinct(item.getProductScopeValues(), rewardActivity.getProductScopeValues()).isEmpty()) {
throw exception(REWARD_ACTIVITY_SCOPE_EXISTS, item.getName(), "活动商品范围所选商品重叠");
}
}
// 校验商品分类是否冲突
if (PromotionProductScopeEnum.isCategory(rewardActivity.getProductScope())) {
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(item.getProductScopeValues());
if (!intersectionDistinct(rewardActivity.getProductScopeValues(),
convertSet(spuList, ProductSpuRespDTO::getCategoryId)).isEmpty()) {
throw exception(REWARD_ACTIVITY_SCOPE_EXISTS, item.getName(), "本活动商品分类范围包含了该活动所选商品");
}
}
}
}
}
@ -157,14 +193,47 @@ public class RewardActivityServiceImpl implements RewardActivityService {
}
@Override
public List<RewardActivityMatchRespDTO> getMatchRewardActivityList(Collection<Long> spuIds) {
List<RewardActivityDO> list = rewardActivityMapper.selectListBySpuIdsAndStatus(spuIds, CommonStatusEnum.ENABLE.getStatus());
return BeanUtils.toBean(list, RewardActivityMatchRespDTO.class);
}
public List<RewardActivityMatchRespDTO> getMatchRewardActivityListBySpuIds(Collection<Long> spuIds) {
// 1. 查询商品分类
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(spuIds);
if (CollUtil.isEmpty(spuList)) {
return Collections.emptyList();
}
Map<Long, ProductSpuRespDTO> spuMap = convertMap(spuList, ProductSpuRespDTO::getId);
@Override
public List<RewardActivityDO> getRewardActivityListByStatusAndDateTimeLt(Integer status, LocalDateTime dateTime) {
return rewardActivityMapper.selectListByStatusAndDateTimeLt(status, dateTime);
// 2. 查询出指定 spuId spu 参加的活动
List<RewardActivityDO> activityList = rewardActivityMapper.selectListBySpuIdAndStatusAndNow(
spuIds, convertSet(spuList, ProductSpuRespDTO::getCategoryId), CommonStatusEnum.ENABLE.getStatus());
if (CollUtil.isEmpty(activityList)) {
return Collections.emptyList();
}
// 3. 转换成 Response DTO
return convertList(activityList, activity -> {
RewardActivityMatchRespDTO activityDTO = BeanUtils.toBean(activity, RewardActivityMatchRespDTO.class);
// 3.1 设置对应匹配的 spuIds
activityDTO.setSpuIds(new ArrayList<>());
for (Long spuId : spuIds) {
if (PromotionProductScopeEnum.isAll(activityDTO.getProductScope())) {
activityDTO.getSpuIds().add(spuId);
} else if (PromotionProductScopeEnum.isSpu(activityDTO.getProductScope())) {
if (CollUtil.contains(activityDTO.getProductScopeValues(), spuId)) {
activityDTO.getSpuIds().add(spuId);
}
} else if (PromotionProductScopeEnum.isCategory(activityDTO.getProductScope())) {
ProductSpuRespDTO spu = spuMap.get(spuId);
if (spu != null && CollUtil.contains(activityDTO.getProductScopeValues(), spu.getCategoryId())) {
activityDTO.getSpuIds().add(spuId);
}
}
}
// 3.2 设置每个 Rule 的描述
activityDTO.setRules(convertList(activity.getRules(), rule ->
BeanUtils.toBean(rule, RewardActivityMatchRespDTO.Rule.class)
.setDescription(getRewardActivityRuleDescription(activityDTO.getConditionType(), rule))));
return activityDTO;
});
}
}

View File

@ -10,7 +10,6 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityD
import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillProductDO;
import jakarta.validation.Valid;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
@ -110,7 +109,7 @@ public interface SeckillActivityService {
List<SeckillActivityDO> getSeckillActivityListByConfigIdAndStatus(Long configId, Integer status);
/**
* 通过活动时段获取秒杀活动
* 通过活动时段获取开始的秒杀活动
*
* @param pageReqVO 请求
* @return 秒杀活动列表
@ -130,14 +129,12 @@ public interface SeckillActivityService {
SeckillValidateJoinRespDTO validateJoinSeckill(Long activityId, Long skuId, Integer count);
/**
* 取指定 spu 编号最近参加的活动每个 spuId 只返回一条记录
* SPU 进行中的秒杀活动
*
* @param spuIds spu 编号
* @param status 状态
* @param dateTime 日期时间
* @return 秒杀活动列表
* @param spuId SPU 编号数组
* @return 秒杀活动
*/
List<SeckillActivityDO> getSeckillActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime);
SeckillActivityDO getMatchSeckillActivityBySpuId(Long spuId);
/**
* 获得拼团活动列表

View File

@ -1,8 +1,6 @@
package cn.iocoder.yudao.module.promotion.service.seckill;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -30,7 +28,6 @@ import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -56,8 +53,10 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
private SeckillActivityMapper seckillActivityMapper;
@Resource
private SeckillProductMapper seckillProductMapper;
@Resource
private SeckillConfigService seckillConfigService;
@Resource
private ProductSpuApi productSpuApi;
@Resource
@ -219,7 +218,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
seckillProductMapper.updateBatch(diffList.get(1));
}
if (isNotEmpty(diffList.get(2))) {
seckillProductMapper.deleteBatchIds(convertList(diffList.get(2), SeckillProductDO::getId));
seckillProductMapper.deleteByIds(convertList(diffList.get(2), SeckillProductDO::getId));
}
}
@ -249,7 +248,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
seckillActivityMapper.deleteById(id);
// 删除活动商品
List<SeckillProductDO> products = seckillProductMapper.selectListByActivityId(id);
seckillProductMapper.deleteBatchIds(convertSet(products, SeckillProductDO::getId));
seckillProductMapper.deleteByIds(convertSet(products, SeckillProductDO::getId));
}
private SeckillActivityDO validateSeckillActivityExists(Long id) {
@ -289,7 +288,7 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
@Override
public PageResult<SeckillActivityDO> getSeckillActivityAppPageByConfigId(AppSeckillActivityPageReqVO pageReqVO) {
return seckillActivityMapper.selectPage(pageReqVO, CommonStatusEnum.ENABLE.getStatus());
return seckillActivityMapper.selectPage(pageReqVO, CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now());
}
@Override
@ -325,15 +324,8 @@ public class SeckillActivityServiceImpl implements SeckillActivityService {
}
@Override
public List<SeckillActivityDO> getSeckillActivityBySpuIdsAndStatusAndDateTimeLt(Collection<Long> spuIds, Integer status, LocalDateTime dateTime) {
// 1.查询出指定 spuId spu 参加的活动最接近现在的一条记录多个的话一个 spuId 对应一个最近的活动编号
List<Map<String, Object>> spuIdAndActivityIdMaps = seckillActivityMapper.selectSpuIdAndActivityIdMapsBySpuIdsAndStatus(spuIds, status);
if (CollUtil.isEmpty(spuIdAndActivityIdMaps)) {
return Collections.emptyList();
}
// 2.查询活动详情
return seckillActivityMapper.selectListByIdsAndDateTimeLt(
convertSet(spuIdAndActivityIdMaps, map -> MapUtil.getLong(map, "activityId")), dateTime);
public SeckillActivityDO getMatchSeckillActivityBySpuId(Long spuId) {
return seckillActivityMapper.selectBySpuIdAndStatusAndNow(spuId, CommonStatusEnum.ENABLE.getStatus());
}
@Override

View File

@ -1,25 +0,0 @@
package cn.iocoder.yudao.module.promotion.util;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import java.time.LocalDateTime;
/**
* 活动工具类
*
* @author 芋道源码
*/
public class PromotionUtils {
/**
* 根据时间计算活动状态
*
* @param endTime 结束时间
* @return 活动状态
*/
public static Integer calculateActivityStatus(LocalDateTime endTime) {
return LocalDateTimeUtils.beforeNow(endTime) ? CommonStatusEnum.DISABLE.getStatus() : CommonStatusEnum.ENABLE.getStatus();
}
}

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.promotion.dal.mysql.discount.DiscountProductMapper">
<select id="getMatchDiscountProductList" resultType="cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO">
SELECT pdp.*
FROM promotion_discount_product pdp
LEFT JOIN promotion_discount_activity pda
ON pdp.activity_id = pda.id
<where>
<if test="skuIds != null and skuIds.size > 0">
AND pdp.sku_id in
<foreach collection="skuIds" item="skuId" index="index" open="(" close=")" separator=",">
#{skuId}
</foreach>
</if>
AND pda.start_time &lt;= CURRENT_TIME AND pda.end_time &gt;= CURRENT_TIME
AND pda.`status` = 20
AND pda.deleted != 1
</where>
</select>
</mapper>

View File

@ -2,8 +2,9 @@ package cn.iocoder.yudao.module.promotion.service.reward;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.product.api.category.ProductCategoryApi;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO;
@ -11,17 +12,14 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.reward.RewardActivityMapper;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import static cn.hutool.core.util.RandomUtil.randomEle;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
@ -29,8 +27,6 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.REWARD_ACTIVITY_NOT_EXISTS;
import static com.google.common.primitives.Longs.asList;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*;
/**
@ -39,14 +35,17 @@ import static org.junit.jupiter.api.Assertions.*;
* @author 芋道源码
*/
@Disabled // TODO 芋艿后续 fix 补充的单测
@Import(RewardActivityServiceImpl.class)
public class RewardActivityServiceImplTest extends BaseDbUnitTest {
public class RewardActivityServiceImplTest extends BaseMockitoUnitTest {
@Resource
private RewardActivityServiceImpl rewardActivityService;
@InjectMocks
private RewardActivityServiceImpl rewardActivityServiceImpl;
@Resource
@Mock
private RewardActivityMapper rewardActivityMapper;
@Mock
private ProductCategoryApi productCategoryApi;
@Mock
private ProductSpuApi productSpuApi;
@Test
public void testCreateRewardActivity_success() {
@ -59,7 +58,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
});
// 调用
Long rewardActivityId = rewardActivityService.createRewardActivity(reqVO);
Long rewardActivityId = rewardActivityServiceImpl.createRewardActivity(reqVO);
// 断言
assertNotNull(rewardActivityId);
// 校验记录的属性是否正确
@ -86,7 +85,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
});
// 调用
rewardActivityService.updateRewardActivity(reqVO);
rewardActivityServiceImpl.updateRewardActivity(reqVO);
// 校验是否更新正确
RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的
assertPojoEquals(reqVO, rewardActivity, "rules");
@ -105,7 +104,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
Long id = dbRewardActivity.getId();
// 调用
rewardActivityService.closeRewardActivity(id);
rewardActivityServiceImpl.closeRewardActivity(id);
// 校验状态
RewardActivityDO rewardActivity = rewardActivityMapper.selectById(id);
assertEquals(rewardActivity.getStatus(), CommonStatusEnum.DISABLE.getStatus());
@ -117,7 +116,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class);
// 调用, 并断言异常
assertServiceException(() -> rewardActivityService.updateRewardActivity(reqVO), REWARD_ACTIVITY_NOT_EXISTS);
assertServiceException(() -> rewardActivityServiceImpl.updateRewardActivity(reqVO), REWARD_ACTIVITY_NOT_EXISTS);
}
@Test
@ -129,7 +128,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
Long id = dbRewardActivity.getId();
// 调用
rewardActivityService.deleteRewardActivity(id);
rewardActivityServiceImpl.deleteRewardActivity(id);
// 校验数据不存在了
assertNull(rewardActivityMapper.selectById(id));
}
@ -140,7 +139,7 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
Long id = randomLongId();
// 调用, 并断言异常
assertServiceException(() -> rewardActivityService.deleteRewardActivity(id), REWARD_ACTIVITY_NOT_EXISTS);
assertServiceException(() -> rewardActivityServiceImpl.deleteRewardActivity(id), REWARD_ACTIVITY_NOT_EXISTS);
}
@Test
@ -161,66 +160,98 @@ public class RewardActivityServiceImplTest extends BaseDbUnitTest {
reqVO.setStatus(CommonStatusEnum.DISABLE.getStatus());
// 调用
PageResult<RewardActivityDO> pageResult = rewardActivityService.getRewardActivityPage(reqVO);
PageResult<RewardActivityDO> pageResult = rewardActivityServiceImpl.getRewardActivityPage(reqVO);
// 断言
assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size());
assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules");
}
@Test
public void testGetRewardActivities_all() {
// mock 数据
RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setProductScope(PromotionProductScopeEnum.ALL.getScope()));
rewardActivityMapper.insert(allActivity);
RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
rewardActivityMapper.insert(productActivity);
// 准备参数
Set<Long> spuIds = asSet(1L, 2L);
// 调用 TODO getMatchRewardActivities 没有这个方法但是找到了 getMatchRewardActivityList
List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
// 断言
assertEquals(matchRewardActivityList.size(), 1);
matchRewardActivityList.forEach((activity) -> {
if (activity.getId().equals(productActivity.getId())) {
assertPojoEquals(activity, productActivity);
assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
} else {
fail();
}
});
}
@Test
public void testGetRewardActivities_product() {
// mock 数据
RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L)));
rewardActivityMapper.insert(productActivity01);
RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L)));
rewardActivityMapper.insert(productActivity02);
// 准备参数
Set<Long> spuIds = asSet(1L, 2L, 3L);
// 调用 TODO getMatchRewardActivities 没有这个方法但是找到了 getMatchRewardActivityList
List<RewardActivityMatchRespDTO> matchRewardActivityList = rewardActivityService.getMatchRewardActivityList(spuIds);
// 断言
assertEquals(matchRewardActivityList.size(), 2);
matchRewardActivityList.forEach((activity) -> {
if (activity.getId().equals(productActivity01.getId())) {
assertPojoEquals(activity, productActivity01);
assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
} else if (activity.getId().equals(productActivity02.getId())) {
assertPojoEquals(activity, productActivity02);
assertEquals(activity.getProductScopeValues(), singletonList(3L));
} else {
fail();
}
});
}
// TODO 芋艿后续完善单测
// @Test
// public void testGetRewardActivities_all() {
// LocalDateTime now = LocalDateTime.now();
// // mock 数据
// RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
// .setProductScope(PromotionProductScopeEnum.ALL.getScope()).setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
// rewardActivityMapper.insert(allActivity);
// RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
// .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
// .setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
// rewardActivityMapper.insert(productActivity);
// // 准备参数
// Set<Long> spuIds = asSet(1L, 2L);
//
// // 调用
// List<RewardActivityDO> activityList = rewardActivityServiceImpl.getRewardActivityListByStatusAndDateTimeLt(
// CommonStatusEnum.ENABLE.getStatus(), now);
// List<RewardActivityDO> matchRewardActivityList = filterMatchActivity(spuIds, activityList);
// // 断言
// assertEquals(matchRewardActivityList.size(), 1);
// matchRewardActivityList.forEach((activity) -> {
// if (activity.getId().equals(productActivity.getId())) {
// assertPojoEquals(activity, productActivity);
// assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
// } else {
// fail();
// }
// });
// }
//
// @Test
// public void testGetRewardActivities_product() {
// LocalDateTime now = LocalDateTime.now();
// // mock 数据
// RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
// .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
// .setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
// rewardActivityMapper.insert(productActivity01);
// RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())
// .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))
// .setStartTime(now.minusDays(1)).setEndTime(now.plusDays(1)));
// rewardActivityMapper.insert(productActivity02);
// // 准备参数
// Set<Long> spuIds = asSet(1L, 2L, 3L);
//
// List<RewardActivityDO> activityList = rewardActivityServiceImpl.getRewardActivityListByStatusAndDateTimeLt(
// CommonStatusEnum.ENABLE.getStatus(), now);
// List<RewardActivityDO> matchRewardActivityList = filterMatchActivity(spuIds, activityList);
// // 断言
// assertEquals(matchRewardActivityList.size(), 2);
// matchRewardActivityList.forEach((activity) -> {
// if (activity.getId().equals(productActivity01.getId())) {
// assertPojoEquals(activity, productActivity01);
// assertEquals(activity.getProductScopeValues(), asList(1L, 2L));
// } else if (activity.getId().equals(productActivity02.getId())) {
// assertPojoEquals(activity, productActivity02);
// assertEquals(activity.getProductScopeValues(), singletonList(3L));
// } else {
// fail();
// }
// });
// }
//
// /**
// * 获得满减送的订单项商品列表
// *
// * @param spuIds 商品编号
// * @param activityList 活动列表
// * @return 订单项商品列表
// */
// private List<RewardActivityDO> filterMatchActivity(Collection<Long> spuIds, List<RewardActivityDO> activityList) {
// List<RewardActivityDO> resultActivityList = new ArrayList<>();
// for (RewardActivityDO activity : activityList) {
// // 情况一全部商品都可以参与
// if (PromotionProductScopeEnum.isAll(activity.getProductScope())) {
// resultActivityList.add(activity);
// }
// // 情况二指定商品参与
// if (PromotionProductScopeEnum.isSpu(activity.getProductScope()) &&
// !intersectionDistinct(activity.getProductScopeValues(), spuIds).isEmpty()) {
// resultActivityList.add(activity);
// }
// }
// return resultActivityList;
// }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,7 +72,7 @@ public class TradePriceServiceImplTest extends BaseMockitoUnitTest {
.setStatus(ProductSpuStatusEnum.ENABLE.getStatus())));
// 调用
TradePriceCalculateRespBO calculateRespBO = tradePriceService.calculatePrice(calculateReqBO);
TradePriceCalculateRespBO calculateRespBO = tradePriceService.calculateOrderPrice(calculateReqBO);
// 断言
assertEquals(TradeOrderTypeEnum.NORMAL.getType(), calculateRespBO.getType());
assertEquals(0, calculateRespBO.getPromotions().size());

View File

@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
@ -31,6 +32,7 @@ import static org.mockito.Mockito.when;
*
* @author 芋道源码
*/
@Disabled // TODO 芋艿后续修复
public class TradeCouponPriceCalculatorTest extends BaseMockitoUnitTest {
@InjectMocks

View File

@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.trade.service.delivery.bo.DeliveryExpressTemplate
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.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
@ -32,6 +33,7 @@ import static org.mockito.Mockito.when;
*
* @author jason
*/
@Disabled // TODO 芋艿后续修复
public class TradeDeliveryPriceCalculatorTest extends BaseMockitoUnitTest {
@InjectMocks

View File

@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
@ -26,6 +27,7 @@ import static org.mockito.Mockito.when;
*
* @author 芋道源码
*/
@Disabled // TODO 芋艿后续修复
public class TradeDiscountActivityPriceCalculatorTest extends BaseMockitoUnitTest {
@InjectMocks
@ -57,7 +59,7 @@ public class TradeDiscountActivityPriceCalculatorTest extends BaseMockitoUnitTes
TradePriceCalculatorHelper.recountAllPrice(result);
// mock 方法限时折扣活动
when(discountActivityApi.getMatchDiscountProductList(eq(asSet(10L, 20L)))).thenReturn(asList(
when(discountActivityApi.getMatchDiscountProductListBySkuIds(eq(asSet(10L, 20L)))).thenReturn(asList(
randomPojo(DiscountProductRespDTO.class, o -> o.setActivityId(1000L)
.setActivityName("活动 1000 号").setSkuId(10L)
.setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()).setDiscountPrice(40)),

View File

@ -1,118 +0,0 @@
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.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import org.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()
.setType(TradeOrderTypeEnum.NORMAL.getType())
.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

@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
@ -27,6 +28,7 @@ import static org.mockito.Mockito.when;
*
* @author owen
*/
@Disabled // TODO 芋艿后续修复
public class TradePointUsePriceCalculatorTest extends BaseMockitoUnitTest {
@InjectMocks

View File

@ -2,28 +2,10 @@ package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
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 java.util.LinkedHashMap;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
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;
// TODO 芋艿后续在修复
/**
* {@link TradeRewardActivityPriceCalculator} 的单元测试类
*
@ -37,210 +19,212 @@ public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest
@Mock
private RewardActivityApi rewardActivityApi;
@Test
public void testCalculate_match() {
// 准备参数
TradePriceCalculateReqBO param = new TradePriceCalculateReqBO()
.setItems(asList(
new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true), // 匹配活动 1
new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(true), // 匹配活动 1
new TradePriceCalculateReqBO.Item().setSkuId(30L).setCount(4).setSelected(true) // 匹配活动 2
));
TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
.setType(TradeOrderTypeEnum.NORMAL.getType())
.setPrice(new TradePriceCalculateRespBO.Price())
.setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>())
.setItems(asList(
new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
.setPrice(100).setSpuId(1L),
new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(true)
.setPrice(50).setSpuId(2L),
new TradePriceCalculateRespBO.OrderItem().setSkuId(30L).setCount(4).setSelected(true)
.setPrice(30).setSpuId(3L)
));
// 保证价格被初始化上
TradePriceCalculatorHelper.recountPayPrice(result.getItems());
TradePriceCalculatorHelper.recountAllPrice(result);
// mock 方法满减送 RewardActivity 信息
when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList(
randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
.setConditionType(PromotionConditionTypeEnum.PRICE.getType())
.setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
.setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(20).setDiscountPrice(70)
.setFreeDelivery(false)))),
randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号")
.setConditionType(PromotionConditionTypeEnum.COUNT.getType())
.setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))
.setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10)
.setPoint(50).setFreeDelivery(false),
new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60)
.setPoint(100).setFreeDelivery(false), // 最大可满足因为是 4
new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)
.setFreeDelivery(false))))
));
// 调用
tradeRewardActivityPriceCalculator.calculate(param, result);
// 断言 Order 部分
TradePriceCalculateRespBO.Price price = result.getPrice();
assertEquals(price.getTotalPrice(), 470);
assertEquals(price.getDiscountPrice(), 130);
assertEquals(price.getPointPrice(), 0);
assertEquals(price.getDeliveryPrice(), 0);
assertEquals(price.getCouponPrice(), 0);
assertEquals(price.getPayPrice(), 340);
assertNull(result.getCouponId());
// 断言SKU 1
assertEquals(result.getItems().size(), 3);
TradePriceCalculateRespBO.OrderItem orderItem01 = result.getItems().get(0);
assertEquals(orderItem01.getSkuId(), 10L);
assertEquals(orderItem01.getCount(), 2);
assertEquals(orderItem01.getPrice(), 100);
assertEquals(orderItem01.getDiscountPrice(), 40);
assertEquals(orderItem01.getDeliveryPrice(), 0);
assertEquals(orderItem01.getCouponPrice(), 0);
assertEquals(orderItem01.getPointPrice(), 0);
assertEquals(orderItem01.getPayPrice(), 160);
assertEquals(orderItem01.getGivePoint(), 0);
// 断言SKU 2
TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1);
assertEquals(orderItem02.getSkuId(), 20L);
assertEquals(orderItem02.getCount(), 3);
assertEquals(orderItem02.getPrice(), 50);
assertEquals(orderItem02.getDiscountPrice(), 30);
assertEquals(orderItem02.getDeliveryPrice(), 0);
assertEquals(orderItem02.getCouponPrice(), 0);
assertEquals(orderItem02.getPointPrice(), 0);
assertEquals(orderItem02.getPayPrice(), 120);
assertEquals(orderItem02.getGivePoint(), 0);
// 断言SKU 3
TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2);
assertEquals(orderItem03.getSkuId(), 30L);
assertEquals(orderItem03.getCount(), 4);
assertEquals(orderItem03.getPrice(), 30);
assertEquals(orderItem03.getDiscountPrice(), 60);
assertEquals(orderItem03.getDeliveryPrice(), 0);
assertEquals(orderItem03.getCouponPrice(), 0);
assertEquals(orderItem03.getPointPrice(), 0);
assertEquals(orderItem03.getPayPrice(), 60);
assertEquals(orderItem03.getGivePoint(), 100);
// 断言Promotion 部分第一个
assertEquals(result.getPromotions().size(), 2);
TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
assertEquals(promotion01.getId(), 1000L);
assertEquals(promotion01.getName(), "活动 1000 号");
assertEquals(promotion01.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
assertEquals(promotion01.getTotalPrice(), 350);
assertEquals(promotion01.getDiscountPrice(), 70);
assertTrue(promotion01.getMatch());
assertEquals(promotion01.getDescription(), "满减送:省 0.70 元");
assertEquals(promotion01.getItems().size(), 2);
TradePriceCalculateRespBO.PromotionItem promotionItem011 = promotion01.getItems().get(0);
assertEquals(promotionItem011.getSkuId(), 10L);
assertEquals(promotionItem011.getTotalPrice(), 200);
assertEquals(promotionItem011.getDiscountPrice(), 40);
TradePriceCalculateRespBO.PromotionItem promotionItem012 = promotion01.getItems().get(1);
assertEquals(promotionItem012.getSkuId(), 20L);
assertEquals(promotionItem012.getTotalPrice(), 150);
assertEquals(promotionItem012.getDiscountPrice(), 30);
// 断言Promotion 部分第二个
TradePriceCalculateRespBO.Promotion promotion02 = result.getPromotions().get(1);
assertEquals(promotion02.getId(), 2000L);
assertEquals(promotion02.getName(), "活动 2000 号");
assertEquals(promotion02.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
assertEquals(promotion02.getTotalPrice(), 120);
assertEquals(promotion02.getDiscountPrice(), 60);
assertTrue(promotion02.getMatch());
assertEquals(promotion02.getDescription(), "满减送:省 0.60 元");
TradePriceCalculateRespBO.PromotionItem promotionItem02 = promotion02.getItems().get(0);
assertEquals(promotion02.getItems().size(), 1);
assertEquals(promotionItem02.getSkuId(), 30L);
assertEquals(promotionItem02.getTotalPrice(), 120);
assertEquals(promotionItem02.getDiscountPrice(), 60);
}
@Test
public void testCalculate_notMatch() {
// 准备参数
TradePriceCalculateReqBO param = new TradePriceCalculateReqBO()
.setItems(asList(
new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true),
new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(true),
new TradePriceCalculateReqBO.Item().setSkuId(30L).setCount(4).setSelected(true)
));
TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
.setType(TradeOrderTypeEnum.NORMAL.getType())
.setPrice(new TradePriceCalculateRespBO.Price())
.setPromotions(new ArrayList<>())
.setItems(asList(
new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
.setPrice(100).setSpuId(1L),
new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(true)
.setPrice(50).setSpuId(2L)
));
// 保证价格被初始化上
TradePriceCalculatorHelper.recountPayPrice(result.getItems());
TradePriceCalculatorHelper.recountAllPrice(result);
// mock 方法限时折扣 DiscountActivity 信息
when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList(
randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
.setProductScopeValues(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
.setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70))))
));
// 调用
tradeRewardActivityPriceCalculator.calculate(param, result);
// 断言 Order 部分
TradePriceCalculateRespBO.Price price = result.getPrice();
assertEquals(price.getTotalPrice(), 350);
assertEquals(price.getDiscountPrice(), 0);
assertEquals(price.getPointPrice(), 0);
assertEquals(price.getDeliveryPrice(), 0);
assertEquals(price.getCouponPrice(), 0);
assertEquals(price.getPayPrice(), 350);
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.getPayPrice(), 200);
// 断言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.getPayPrice(), 150);
// 断言 Promotion 部分
assertEquals(result.getPromotions().size(), 1);
TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
assertEquals(promotion01.getId(), 1000L);
assertEquals(promotion01.getName(), "活动 1000 号");
assertEquals(promotion01.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
assertEquals(promotion01.getTotalPrice(), 350);
assertEquals(promotion01.getDiscountPrice(), 0);
assertFalse(promotion01.getMatch());
assertEquals(promotion01.getDescription(), "TODO"); // TODO 芋艿后面再想想
assertEquals(promotion01.getItems().size(), 2);
TradePriceCalculateRespBO.PromotionItem promotionItem011 = promotion01.getItems().get(0);
assertEquals(promotionItem011.getSkuId(), 10L);
assertEquals(promotionItem011.getTotalPrice(), 200);
assertEquals(promotionItem011.getDiscountPrice(), 0);
TradePriceCalculateRespBO.PromotionItem promotionItem012 = promotion01.getItems().get(1);
assertEquals(promotionItem012.getSkuId(), 20L);
assertEquals(promotionItem012.getTotalPrice(), 150);
assertEquals(promotionItem012.getDiscountPrice(), 0);
}
// @Test
// public void testCalculate_match() {
// // 准备参数
// TradePriceCalculateReqBO param = new TradePriceCalculateReqBO()
// .setItems(asList(
// new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true), // 匹配活动 1
// new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(true), // 匹配活动 1
// new TradePriceCalculateReqBO.Item().setSkuId(30L).setCount(4).setSelected(true) // 匹配活动 2
// ));
// TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
// .setType(TradeOrderTypeEnum.NORMAL.getType())
// .setPrice(new TradePriceCalculateRespBO.Price())
// .setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>())
// .setItems(asList(
// new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
// .setPrice(100).setSpuId(1L),
// new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(true)
// .setPrice(50).setSpuId(2L),
// new TradePriceCalculateRespBO.OrderItem().setSkuId(30L).setCount(4).setSelected(true)
// .setPrice(30).setSpuId(3L)
// ));
// // 保证价格被初始化上
// TradePriceCalculatorHelper.recountPayPrice(result.getItems());
// TradePriceCalculatorHelper.recountAllPrice(result);
//
// // mock 方法满减送 RewardActivity 信息
// when(rewardActivityApi.getRewardActivityListByStatusAndNow(CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now()))
// .thenReturn(asList(
// randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
// .setConditionType(PromotionConditionTypeEnum.PRICE.getType())
// .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(asList(1L, 2L))
// .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(20).setDiscountPrice(70)
// .setFreeDelivery(false)))),
// randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号")
// .setConditionType(PromotionConditionTypeEnum.COUNT.getType())
// .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductScopeValues(singletonList(3L))
// .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10)
// .setPoint(50).setFreeDelivery(false),
// new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60)
// .setPoint(100).setFreeDelivery(false), // 最大可满足因为是 4
// new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)
// .setFreeDelivery(false))))
// ));
//
// // 调用
// tradeRewardActivityPriceCalculator.calculate(param, result);
// // 断言 Order 部分
// TradePriceCalculateRespBO.Price price = result.getPrice();
// assertEquals(price.getTotalPrice(), 470);
// assertEquals(price.getDiscountPrice(), 130);
// assertEquals(price.getPointPrice(), 0);
// assertEquals(price.getDeliveryPrice(), 0);
// assertEquals(price.getCouponPrice(), 0);
// assertEquals(price.getPayPrice(), 340);
// assertNull(result.getCouponId());
// // 断言SKU 1
// assertEquals(result.getItems().size(), 3);
// TradePriceCalculateRespBO.OrderItem orderItem01 = result.getItems().get(0);
// assertEquals(orderItem01.getSkuId(), 10L);
// assertEquals(orderItem01.getCount(), 2);
// assertEquals(orderItem01.getPrice(), 100);
// assertEquals(orderItem01.getDiscountPrice(), 40);
// assertEquals(orderItem01.getDeliveryPrice(), 0);
// assertEquals(orderItem01.getCouponPrice(), 0);
// assertEquals(orderItem01.getPointPrice(), 0);
// assertEquals(orderItem01.getPayPrice(), 160);
// assertEquals(orderItem01.getGivePoint(), 0);
// // 断言SKU 2
// TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1);
// assertEquals(orderItem02.getSkuId(), 20L);
// assertEquals(orderItem02.getCount(), 3);
// assertEquals(orderItem02.getPrice(), 50);
// assertEquals(orderItem02.getDiscountPrice(), 30);
// assertEquals(orderItem02.getDeliveryPrice(), 0);
// assertEquals(orderItem02.getCouponPrice(), 0);
// assertEquals(orderItem02.getPointPrice(), 0);
// assertEquals(orderItem02.getPayPrice(), 120);
// assertEquals(orderItem02.getGivePoint(), 0);
// // 断言SKU 3
// TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2);
// assertEquals(orderItem03.getSkuId(), 30L);
// assertEquals(orderItem03.getCount(), 4);
// assertEquals(orderItem03.getPrice(), 30);
// assertEquals(orderItem03.getDiscountPrice(), 60);
// assertEquals(orderItem03.getDeliveryPrice(), 0);
// assertEquals(orderItem03.getCouponPrice(), 0);
// assertEquals(orderItem03.getPointPrice(), 0);
// assertEquals(orderItem03.getPayPrice(), 60);
// assertEquals(orderItem03.getGivePoint(), 100);
// // 断言Promotion 部分第一个
// assertEquals(result.getPromotions().size(), 2);
// TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
// assertEquals(promotion01.getId(), 1000L);
// assertEquals(promotion01.getName(), "活动 1000 号");
// assertEquals(promotion01.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
// assertEquals(promotion01.getTotalPrice(), 350);
// assertEquals(promotion01.getDiscountPrice(), 70);
// assertTrue(promotion01.getMatch());
// assertEquals(promotion01.getDescription(), "满减送:省 0.70 元");
// assertEquals(promotion01.getItems().size(), 2);
// TradePriceCalculateRespBO.PromotionItem promotionItem011 = promotion01.getItems().get(0);
// assertEquals(promotionItem011.getSkuId(), 10L);
// assertEquals(promotionItem011.getTotalPrice(), 200);
// assertEquals(promotionItem011.getDiscountPrice(), 40);
// TradePriceCalculateRespBO.PromotionItem promotionItem012 = promotion01.getItems().get(1);
// assertEquals(promotionItem012.getSkuId(), 20L);
// assertEquals(promotionItem012.getTotalPrice(), 150);
// assertEquals(promotionItem012.getDiscountPrice(), 30);
// // 断言Promotion 部分第二个
// TradePriceCalculateRespBO.Promotion promotion02 = result.getPromotions().get(1);
// assertEquals(promotion02.getId(), 2000L);
// assertEquals(promotion02.getName(), "活动 2000 号");
// assertEquals(promotion02.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
// assertEquals(promotion02.getTotalPrice(), 120);
// assertEquals(promotion02.getDiscountPrice(), 60);
// assertTrue(promotion02.getMatch());
// assertEquals(promotion02.getDescription(), "满减送:省 0.60 元");
// TradePriceCalculateRespBO.PromotionItem promotionItem02 = promotion02.getItems().get(0);
// assertEquals(promotion02.getItems().size(), 1);
// assertEquals(promotionItem02.getSkuId(), 30L);
// assertEquals(promotionItem02.getTotalPrice(), 120);
// assertEquals(promotionItem02.getDiscountPrice(), 60);
// }
//
// @Test
// public void testCalculate_notMatch() {
// // 准备参数
// TradePriceCalculateReqBO param = new TradePriceCalculateReqBO()
// .setItems(asList(
// new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true),
// new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(true),
// new TradePriceCalculateReqBO.Item().setSkuId(30L).setCount(4).setSelected(true)
// ));
// TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
// .setType(TradeOrderTypeEnum.NORMAL.getType())
// .setPrice(new TradePriceCalculateRespBO.Price())
// .setPromotions(new ArrayList<>())
// .setItems(asList(
// new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
// .setPrice(100).setSpuId(1L),
// new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(true)
// .setPrice(50).setSpuId(2L)
// ));
// // 保证价格被初始化上
// TradePriceCalculatorHelper.recountPayPrice(result.getItems());
// TradePriceCalculatorHelper.recountAllPrice(result);
//
// // mock 方法限时折扣 DiscountActivity 信息
// when(rewardActivityApi.getRewardActivityListByStatusAndNow(CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now()))
// .thenReturn(singletonList(
// randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号")
// .setProductScopeValues(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType())
// .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70))))
// ));
//
// // 调用
// tradeRewardActivityPriceCalculator.calculate(param, result);
// // 断言 Order 部分
// TradePriceCalculateRespBO.Price price = result.getPrice();
// assertEquals(price.getTotalPrice(), 350);
// assertEquals(price.getDiscountPrice(), 0);
// assertEquals(price.getPointPrice(), 0);
// assertEquals(price.getDeliveryPrice(), 0);
// assertEquals(price.getCouponPrice(), 0);
// assertEquals(price.getPayPrice(), 350);
// 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.getPayPrice(), 200);
// // 断言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.getPayPrice(), 150);
// // 断言 Promotion 部分
// assertEquals(result.getPromotions().size(), 1);
// TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
// assertEquals(promotion01.getId(), 1000L);
// assertEquals(promotion01.getName(), "活动 1000 号");
// assertEquals(promotion01.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType());
// assertEquals(promotion01.getTotalPrice(), 350);
// assertEquals(promotion01.getDiscountPrice(), 0);
// assertFalse(promotion01.getMatch());
// assertEquals(promotion01.getDescription(), "TODO"); // TODO 芋艿后面再想想
// assertEquals(promotion01.getItems().size(), 2);
// TradePriceCalculateRespBO.PromotionItem promotionItem011 = promotion01.getItems().get(0);
// assertEquals(promotionItem011.getSkuId(), 10L);
// assertEquals(promotionItem011.getTotalPrice(), 200);
// assertEquals(promotionItem011.getDiscountPrice(), 0);
// TradePriceCalculateRespBO.PromotionItem promotionItem012 = promotion01.getItems().get(1);
// assertEquals(promotionItem012.getSkuId(), 20L);
// assertEquals(promotionItem012.getTotalPrice(), 150);
// assertEquals(promotionItem012.getDiscountPrice(), 0);
// }
}

View File

@ -30,6 +30,16 @@ public enum PayOrderStatusEnum implements IntArrayValuable {
return new int[0];
}
/**
* 判断是否等待支付
*
* @param status 状态
* @return 是否等待支付
*/
public static boolean isWaiting(Integer status) {
return Objects.equals(status, WAITING.getStatus());
}
/**
* 判断是否支付成功
*

View File

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollectionUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*;
@ -11,12 +12,14 @@ import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.WalletPayClient;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@ -51,10 +54,21 @@ public class PayOrderController {
@GetMapping("/get")
@Operation(summary = "获得支付订单")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@Parameters({
@Parameter(name = "id", description = "编号", required = true, example = "1024"),
@Parameter(name = "sync", description = "是否同步", example = "true")
})
@PreAuthorize("@ss.hasPermission('pay:order:query')")
public CommonResult<PayOrderRespVO> getOrder(@RequestParam("id") Long id) {
return success(PayOrderConvert.INSTANCE.convert(orderService.getOrder(id)));
public CommonResult<PayOrderRespVO> getOrder(@RequestParam("id") Long id,
@RequestParam(value = "sync", required = false) Boolean sync) {
PayOrderDO order = orderService.getOrder(id);
// sync 仅在等待支付
if (Boolean.TRUE.equals(sync) && PayOrderStatusEnum.isWaiting(order.getStatus())) {
orderService.syncOrderQuietly(order.getId());
// 重新查询因为同步后可能会有变化
order = orderService.getOrder(id);
}
return success(BeanUtils.toBean(order, PayOrderRespVO.class));
}
@GetMapping("/get-detail")

View File

@ -1,17 +1,21 @@
package cn.iocoder.yudao.module.pay.controller.app.order;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO;
import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO;
import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.WalletPayClient;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
@ -37,12 +41,22 @@ public class AppPayOrderController {
@Resource
private PayOrderService payOrderService;
// TODO 芋艿临时 demo技术打样
@GetMapping("/get")
@Operation(summary = "获得支付订单")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<PayOrderRespVO> getOrder(@RequestParam("id") Long id) {
return success(PayOrderConvert.INSTANCE.convert(payOrderService.getOrder(id)));
@Parameters({
@Parameter(name = "id", description = "编号", required = true, example = "1024"),
@Parameter(name = "sync", description = "是否同步", example = "true")
})
public CommonResult<PayOrderRespVO> getOrder(@RequestParam("id") Long id,
@RequestParam(value = "sync", required = false) Boolean sync) {
PayOrderDO order = payOrderService.getOrder(id);
// sync 仅在等待支付
if (Boolean.TRUE.equals(sync) && PayOrderStatusEnum.isWaiting(order.getStatus())) {
payOrderService.syncOrderQuietly(order.getId());
// 重新查询因为同步后可能会有变化
order = payOrderService.getOrder(id);
}
return success(BeanUtils.toBean(order, PayOrderRespVO.class));
}
@PostMapping("/submit")

View File

@ -24,6 +24,11 @@ public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO
return selectList(PayOrderExtensionDO::getOrderId, orderId);
}
default List<PayOrderExtensionDO> selectListByOrderIdAndStatus(Long orderId, Integer status) {
return selectList(PayOrderExtensionDO::getOrderId, orderId,
PayOrderExtensionDO::getStatus, status);
}
default List<PayOrderExtensionDO> selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime) {
return selectList(new LambdaQueryWrapper<PayOrderExtensionDO>()
.eq(PayOrderExtensionDO::getStatus, status)

View File

@ -16,6 +16,15 @@ public interface RedisKeyConstants {
*/
String PAY_NOTIFY_LOCK = "pay_notify:lock:%d";
/**
* 支付钱包的分布式锁
*
* KEY 格式pay_wallet:lock:%d
* VALUE 数据格式HASH // RLock.classRedisson Lock 使用 Hash 数据结构
* 过期时间不固定
*/
String PAY_WALLET_LOCK = "pay_wallet:lock:%d";
/**
* 支付序号的缓存
*

View File

@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.pay.dal.redis.wallet;
import jakarta.annotation.Resource;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Repository;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.module.pay.dal.redis.RedisKeyConstants.PAY_WALLET_LOCK;
/**
* 支付钱包的锁 Redis DAO
*
* @author 芋道源码
*/
@Repository
public class PayWalletLockRedisDAO {
@Resource
private RedissonClient redissonClient;
public <V> V lock(Long id, Long timeoutMillis, Callable<V> callable) throws Exception {
String lockKey = formatKey(id);
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
// 执行逻辑
return callable.call();
} catch (Exception e) {
throw e;
} finally {
lock.unlock();
}
}
private static String formatKey(Long id) {
return String.format(PAY_WALLET_LOCK, id);
}
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.pay.service.demo;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
@ -14,11 +15,11 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoOrderMapper;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
@ -111,10 +112,29 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
@Override
public void updateDemoOrderPaid(Long id, Long payOrderId) {
// 校验并获得支付订单可支付
PayOrderRespDTO payOrder = validateDemoOrderCanPaid(id, payOrderId);
// 1.1 校验订单是否存在
PayDemoOrderDO order = payDemoOrderMapper.selectById(id);
if (order == null) {
log.error("[updateDemoOrderPaid][order({}) payOrder({}) 不存在订单,请进行处理!]", id, payOrderId);
throw exception(DEMO_ORDER_NOT_FOUND);
}
// 1.2 校验订单已支付
if (order.getPayStatus()) {
// 特殊如果订单已支付且支付单号相同直接返回说明重复回调
if (ObjectUtil.equals(order.getPayOrderId(), payOrderId)) {
log.warn("[updateDemoOrderPaid][order({}) 已支付,且支付单号相同({}),直接返回]", order, payOrderId);
return;
}
// 异常支付单号不同说明支付单号错误
log.error("[updateDemoOrderPaid][order({}) 支付单不匹配({})请进行处理order 数据是:{}]",
order, payOrderId, toJsonString(order));
throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR);
}
// 更新 PayDemoOrderDO 状态为已支付
// 2. 校验支付订单的合法性
PayOrderRespDTO payOrder = validatePayOrderPaid(order, payOrderId);
// 3. 更新 PayDemoOrderDO 状态为已支付
int updateCount = payDemoOrderMapper.updateByIdAndPayed(id, false,
new PayDemoOrderDO().setPayStatus(true).setPayTime(LocalDateTime.now())
.setPayChannelCode(payOrder.getChannelCode()));
@ -124,56 +144,35 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
}
/**
* 校验交易订单满足被支付的条件
* 校验支付订单的合法性
*
* 1. 交易订单未支付
* 2. 支付单已支付
*
* @param id 交易订单编号
* @param order 交易订单
* @param payOrderId 支付订单编号
* @return 交易订单
* @return 支付订单
*/
private PayOrderRespDTO validateDemoOrderCanPaid(Long id, Long payOrderId) {
// 1.1 校验订单是否存在
PayDemoOrderDO order = payDemoOrderMapper.selectById(id);
if (order == null) {
throw exception(DEMO_ORDER_NOT_FOUND);
}
// 1.2 校验订单未支付
if (order.getPayStatus()) {
log.error("[validateDemoOrderCanPaid][order({}) 不处于待支付状态请进行处理order 数据是:{}]",
id, toJsonString(order));
throw exception(DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID);
}
// 1.3 校验支付订单匹配
if (notEqual(order.getPayOrderId(), payOrderId)) { // 支付单号
log.error("[validateDemoOrderCanPaid][order({}) 支付单不匹配({})请进行处理order 数据是:{}]",
id, payOrderId, toJsonString(order));
throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR);
}
// 2.1 校验支付单是否存在
private PayOrderRespDTO validatePayOrderPaid(PayDemoOrderDO order, Long payOrderId) {
// 1. 校验支付单是否存在
PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId);
if (payOrder == null) {
log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 不存在,请进行处理!]", id, payOrderId);
log.error("[validatePayOrderPaid][order({}) payOrder({}) 不存在,请进行处理!]", order.getId(), payOrderId);
throw exception(PAY_ORDER_NOT_FOUND);
}
// 2.2 校验支付单已支付
// 2.1 校验支付单已支付
if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 未支付请进行处理payOrder 数据是:{}]",
id, payOrderId, toJsonString(payOrder));
log.error("[validatePayOrderPaid][order({}) payOrder({}) 未支付请进行处理payOrder 数据是:{}]",
order.getId(), payOrderId, toJsonString(payOrder));
throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS);
}
// 2.3 校验支付金额一致
// 2.1 校验支付金额一致
if (notEqual(payOrder.getPrice(), order.getPrice())) {
log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 支付金额不匹配请进行处理order 数据是:{}payOrder 数据是:{}]",
id, payOrderId, toJsonString(order), toJsonString(payOrder));
log.error("[validatePayOrderPaid][order({}) payOrder({}) 支付金额不匹配请进行处理order 数据是:{}payOrder 数据是:{}]",
order.getId(), payOrderId, toJsonString(order), toJsonString(payOrder));
throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH);
}
// 2.4 校验支付订单匹配二次
if (notEqual(payOrder.getMerchantOrderId(), id.toString())) {
log.error("[validateDemoOrderCanPaid][order({}) 支付单不匹配({})请进行处理payOrder 数据是:{}]",
id, payOrderId, toJsonString(payOrder));
// 2.2 校验支付订单匹配二次
if (notEqual(payOrder.getMerchantOrderId(), order.getId().toString())) {
log.error("[validatePayOrderPaid][order({}) 支付单不匹配({})请进行处理payOrder 数据是:{}]",
order.getId(), payOrderId, toJsonString(payOrder));
throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR);
}
return payOrder;

View File

@ -139,6 +139,16 @@ public interface PayOrderService {
*/
int syncOrder(LocalDateTime minCreateTime);
/**
* 同步订单的支付状态
*
* 1. Quietly 表示即使同步失败也不会抛出异常
* 2. 什么时候回出现异常因为是主动同步可能和支付渠道的异步回调存在并发冲突导致抛出异常
*
* @param id 订单编号
*/
void syncOrderQuietly(Long id);
/**
* 将已过期的订单状态修改为已关闭
*

View File

@ -163,7 +163,14 @@ public class PayOrderServiceImpl implements PayOrderService {
// 4. 如果调用直接支付成功则直接更新支付单状态为成功例如说付款码支付免密支付时就直接验证支付成功
if (unifiedOrderResp != null) {
getSelf().notifyOrder(channel, unifiedOrderResp);
try {
getSelf().notifyOrder(channel, unifiedOrderResp);
} catch (Exception e) {
// 兼容 https://gitee.com/zhijiantianya/yudao-cloud/issues/I8SM9H 场景
// 支付宝或微信扫码之后时由于 PayClient 是直接返回支付成功而支付也会有回调导致存在并发更新问题此时一般是可以 try catch 直接忽略
log.warn("[submitOrder][order({}) channel({}) 支付结果({}) 通知时发生异常,可能是并发问题]",
order, channel, unifiedOrderResp, e);
}
// 如有渠道错误码则抛出业务异常提示用户
if (StrUtil.isNotEmpty(unifiedOrderResp.getChannelErrorCode())) {
throw exception(PAY_ORDER_SUBMIT_CHANNEL_ERROR, unifiedOrderResp.getChannelErrorCode(),
@ -460,6 +467,18 @@ public class PayOrderServiceImpl implements PayOrderService {
return count;
}
@Override
public void syncOrderQuietly(Long id) {
// 1. 查询待支付订单
List<PayOrderExtensionDO> orderExtensions = orderExtensionMapper.selectListByOrderIdAndStatus(id,
PayOrderStatusEnum.WAITING.getStatus());
// 2. 遍历执行
for (PayOrderExtensionDO orderExtension : orderExtensions) {
syncOrder(orderExtension);
}
}
/**
* 同步单个支付拓展单
*

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pay.service.wallet;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -113,16 +114,28 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
@Override
@Transactional(rollbackFor = Exception.class)
public void updateWalletRechargerPaid(Long id, Long payOrderId) {
// 1.1 获取钱包充值记录
PayWalletRechargeDO walletRecharge = walletRechargeMapper.selectById(id);
if (walletRecharge == null) {
log.error("[updateWalletRechargerPaid][钱包充值记录不存在,钱包充值记录 id({})]", id);
// 1.1 校验钱包充值是否存在
PayWalletRechargeDO recharge = walletRechargeMapper.selectById(id);
if (recharge == null) {
log.error("[updateWalletRechargerPaid][recharge({}) payOrder({}) 不存在充值订单,请进行处理!]", id, payOrderId);
throw exception(WALLET_RECHARGE_NOT_FOUND);
}
// 1.2 校验钱包充值是否可以支付
PayOrderDO payOrderDO = validateWalletRechargerCanPaid(walletRecharge, payOrderId);
if (recharge.getPayStatus()) {
// 特殊如果订单已支付且支付单号相同直接返回说明重复回调
if (ObjectUtil.equals(recharge.getPayOrderId(), payOrderId)) {
log.warn("[updateWalletRechargerPaid][recharge({}) 已支付,且支付单号相同({}),直接返回]", recharge, payOrderId);
return;
}
// 异常支付单号不同说明支付单号错误
log.error("[updateWalletRechargerPaid][recharge({}) 已支付,但是支付单号不同({}),请进行处理!]", recharge, payOrderId);
throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_ID_ERROR);
}
// 2. 更新钱包充值的支付状态
// 2. 校验支付订单的合法性
PayOrderDO payOrderDO = validatePayOrderPaid(recharge, payOrderId);
// 3. 更新钱包充值的支付状态
int updateCount = walletRechargeMapper.updateByIdAndPaid(id, false,
new PayWalletRechargeDO().setId(id).setPayStatus(true).setPayTime(LocalDateTime.now())
.setPayChannelCode(payOrderDO.getChannelCode()));
@ -130,14 +143,14 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
throw exception(WALLET_RECHARGE_UPDATE_PAID_STATUS_NOT_UNPAID);
}
// 3. 更新钱包余额
// 4. 更新钱包余额
// TODO @jason这样的话未来提现会不会把充值的也提现走哈类似先充 100 110然后提现 110
// TODO 需要钱包中加个可提现余额
payWalletService.addWalletBalance(walletRecharge.getWalletId(), String.valueOf(id),
PayWalletBizTypeEnum.RECHARGE, walletRecharge.getTotalPrice());
payWalletService.addWalletBalance(recharge.getWalletId(), String.valueOf(id),
PayWalletBizTypeEnum.RECHARGE, recharge.getTotalPrice());
// 4. 发送订阅消息
getSelf().sendWalletRechargerPaidMessage(payOrderId, walletRecharge);
// 5. 发送订阅消息
getSelf().sendWalletRechargerPaidMessage(payOrderId, recharge);
}
@Async
@ -266,43 +279,38 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
return wallet;
}
private PayOrderDO validateWalletRechargerCanPaid(PayWalletRechargeDO walletRecharge, Long payOrderId) {
// 1.1 校验充值记录的支付状态
if (walletRecharge.getPayStatus()) {
log.error("[validateWalletRechargerCanPaid][钱包({}) 不处于未支付状态! 钱包数据是:{}]",
walletRecharge.getId(), toJsonString(walletRecharge));
throw exception(WALLET_RECHARGE_UPDATE_PAID_STATUS_NOT_UNPAID);
}
// 1.2 校验支付订单匹配
if (notEqual(walletRecharge.getPayOrderId(), payOrderId)) { // 支付单号
log.error("[validateWalletRechargerCanPaid][钱包({}) 支付单不匹配({}),请进行处理! 钱包数据是:{}]",
walletRecharge.getId(), payOrderId, toJsonString(walletRecharge));
throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_ID_ERROR);
}
// 2.1 校验支付单是否存在
/**
* 校验支付订单的合法性
*
* @param recharge 充值订单
* @param payOrderId 支付订单编号
* @return 支付订单
*/
private PayOrderDO validatePayOrderPaid(PayWalletRechargeDO recharge, Long payOrderId) {
// 1. 校验支付单是否存在
PayOrderDO payOrder = payOrderService.getOrder(payOrderId);
if (payOrder == null) {
log.error("[validateWalletRechargerCanPaid][钱包({}) payOrder({}) 不存在,请进行处理!]",
walletRecharge.getId(), payOrderId);
log.error("[validatePayOrderPaid][充值订单({}) payOrder({}) 不存在,请进行处理!]",
recharge.getId(), payOrderId);
throw exception(PAY_ORDER_NOT_FOUND);
}
// 2.2 校验支付单已支付
// 2.1 校验支付单已支付
if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
log.error("[validateWalletRechargerCanPaid][钱包({}) payOrder({}) 未支付请进行处理payOrder 数据是:{}]",
walletRecharge.getId(), payOrderId, toJsonString(payOrder));
log.error("[validatePayOrderPaid][充值订单({}) payOrder({}) 未支付请进行处理payOrder 数据是:{}]",
recharge.getId(), payOrderId, toJsonString(payOrder));
throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_STATUS_NOT_SUCCESS);
}
// 2.3 校验支付金额一致
if (notEqual(payOrder.getPrice(), walletRecharge.getPayPrice())) {
log.error("[validateDemoOrderCanPaid][钱包({}) payOrder({}) 支付金额不匹配,请进行处理!钱包 数据是:{}payOrder 数据是:{}]",
walletRecharge.getId(), payOrderId, toJsonString(walletRecharge), toJsonString(payOrder));
// 2.2 校验支付金额一致
if (notEqual(payOrder.getPrice(), recharge.getPayPrice())) {
log.error("[validatePayOrderPaid][充值订单({}) payOrder({}) 支付金额不匹配,请进行处理!钱包 数据是:{}payOrder 数据是:{}]",
recharge.getId(), payOrderId, toJsonString(recharge), toJsonString(payOrder));
throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_PRICE_NOT_MATCH);
}
// 2.4 校验支付订单的商户订单匹配
if (notEqual(payOrder.getMerchantOrderId(), walletRecharge.getId().toString())) {
log.error("[validateDemoOrderCanPaid][钱包({}) 支付单不匹配({})请进行处理payOrder 数据是:{}]",
walletRecharge.getId(), payOrderId, toJsonString(payOrder));
// 2.3 校验支付订单的商户订单匹配
if (notEqual(payOrder.getMerchantOrderId(), recharge.getId().toString())) {
log.error("[validatePayOrderPaid][充值订单({}) 支付单不匹配({})请进行处理payOrder 数据是:{}]",
recharge.getId(), payOrderId, toJsonString(payOrder));
throw exception(WALLET_RECHARGE_UPDATE_PAID_PAY_ORDER_ID_ERROR);
}
return payOrder;

View File

@ -2,17 +2,20 @@ package cn.iocoder.yudao.module.pay.service.wallet;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletMapper;
import cn.iocoder.yudao.module.pay.dal.redis.wallet.PayWalletLockRedisDAO;
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -32,10 +35,17 @@ import static cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYM
*/
@Service
@Slf4j
public class PayWalletServiceImpl implements PayWalletService {
public class PayWalletServiceImpl implements PayWalletService {
/**
* 通知超时时间单位毫秒
*/
public static final long UPDATE_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS;
@Resource
private PayWalletMapper walletMapper;
@Resource
private PayWalletLockRedisDAO lockRedisDAO;
@Resource
@Lazy // 延迟加载避免循环依赖
@ -122,76 +132,86 @@ public class PayWalletServiceImpl implements PayWalletService {
@Override
@Transactional(rollbackFor = Exception.class)
@SneakyThrows
public PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId,
PayWalletBizTypeEnum bizType, Integer price) {
// 1. 获取钱包
PayWalletDO payWallet = getWallet(walletId);
if (payWallet == null) {
log.error("[reduceWalletBalance],用户钱包({})不存在.", walletId);
log.error("[reduceWalletBalance][用户钱包({})不存在]", walletId);
throw exception(WALLET_NOT_FOUND);
}
// 2.1 扣除余额
int updateCounts;
switch (bizType) {
case PAYMENT: {
updateCounts = walletMapper.updateWhenConsumption(payWallet.getId(), price);
break;
// 2. 加锁更新钱包余额目的避免钱包流水的并发更新时余额变化不连贯
return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> {
// 2. 扣除余额
int updateCounts;
switch (bizType) {
case PAYMENT: {
updateCounts = walletMapper.updateWhenConsumption(payWallet.getId(), price);
break;
}
case RECHARGE_REFUND: {
updateCounts = walletMapper.updateWhenRechargeRefund(payWallet.getId(), price);
break;
}
default: {
// TODO 其它类型待实现
throw new UnsupportedOperationException("待实现");
}
}
case RECHARGE_REFUND: {
updateCounts = walletMapper.updateWhenRechargeRefund(payWallet.getId(), price);
break;
if (updateCounts == 0) {
throw exception(WALLET_BALANCE_NOT_ENOUGH);
}
default: {
// TODO 其它类型待实现
throw new UnsupportedOperationException("待实现");
}
}
if (updateCounts == 0) {
throw exception(WALLET_BALANCE_NOT_ENOUGH);
}
// 2.2 生成钱包流水
Integer afterBalance = payWallet.getBalance() - price;
WalletTransactionCreateReqBO bo = new WalletTransactionCreateReqBO().setWalletId(payWallet.getId())
.setPrice(-price).setBalance(afterBalance).setBizId(String.valueOf(bizId))
.setBizType(bizType.getType()).setTitle(bizType.getDescription());
return walletTransactionService.createWalletTransaction(bo);
// 3. 生成钱包流水
Integer afterBalance = payWallet.getBalance() - price;
WalletTransactionCreateReqBO bo = new WalletTransactionCreateReqBO().setWalletId(payWallet.getId())
.setPrice(-price).setBalance(afterBalance).setBizId(String.valueOf(bizId))
.setBizType(bizType.getType()).setTitle(bizType.getDescription());
return walletTransactionService.createWalletTransaction(bo);
});
}
@Override
@Transactional(rollbackFor = Exception.class)
@SneakyThrows
public PayWalletTransactionDO addWalletBalance(Long walletId, String bizId,
PayWalletBizTypeEnum bizType, Integer price) {
// 1.1 获取钱包
// 1. 获取钱包
PayWalletDO payWallet = getWallet(walletId);
if (payWallet == null) {
log.error("[addWalletBalance],用户钱包({})不存在.", walletId);
log.error("[addWalletBalance][用户钱包({})不存在]", walletId);
throw exception(WALLET_NOT_FOUND);
}
// 1.2 更新钱包金额
switch (bizType) {
case PAYMENT_REFUND: { // 退款更新
walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price);
break;
}
case RECHARGE: { // 充值更新
walletMapper.updateWhenRecharge(payWallet.getId(), price);
break;
}
case UPDATE_BALANCE: // 更新余额
walletMapper.updateWhenRecharge(payWallet.getId(), price);
break;
default: {
// TODO 其它类型待实现
throw new UnsupportedOperationException("待实现");
}
}
// 2. 生成钱包流水
WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO()
.setWalletId(payWallet.getId()).setPrice(price).setBalance(payWallet.getBalance() + price)
.setBizId(bizId).setBizType(bizType.getType()).setTitle(bizType.getDescription());
return walletTransactionService.createWalletTransaction(transactionCreateReqBO);
// 2. 加锁更新钱包余额目的避免钱包流水的并发更新时余额变化不连贯
return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> {
// 2. 更新钱包金额
switch (bizType) {
case PAYMENT_REFUND: { // 退款更新
walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price);
break;
}
case RECHARGE: { // 充值更新
walletMapper.updateWhenRecharge(payWallet.getId(), price);
break;
}
case UPDATE_BALANCE: // 更新余额
walletMapper.updateWhenRecharge(payWallet.getId(), price);
break;
default: {
// TODO 其它类型待实现
throw new UnsupportedOperationException("待实现");
}
}
// 3. 生成钱包流水
WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO()
.setWalletId(payWallet.getId()).setPrice(price).setBalance(payWallet.getBalance() + price)
.setBizId(bizId).setBizType(bizType.getType()).setTitle(bizType.getDescription());
return walletTransactionService.createWalletTransaction(transactionCreateReqBO);
});
}
@Override

Some files were not shown because too many files have changed in this diff Show More