小如首页

This commit is contained in:
zjf committed 2026-04-22 15:08:53 +08:00
1 parent fbc2cf433a
commit d1539a8b5b
39 files changed
+1118 -18

No files matched your search

@@ -120,6 +120,11 @@ public class ProductSpuRespDTO {
*/
private Boolean subCommissionType;
/**
* 是否热门推荐
*/
private Boolean hotRecommend;
// ========== 限购相关字段 =========
/**
@@ -7,7 +7,6 @@ import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@@ -52,7 +51,7 @@ public class ProductSpuPageReqVO extends PageParam {
@Schema(description = "商品分类编号", example = "1")
private Long categoryId;
@Schema(description = "商品类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@Schema(description = "商品类型:0 商城商品,1 提货柜商品;不传时按 0(商城)", example = "0")
private Integer types;
@Schema(description = "设备IMEI", example = "371")
@@ -65,4 +64,7 @@ public class ProductSpuPageReqVO extends PageParam {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
@Schema(description = "是否热门推荐;不传表示不按该条件筛选", example = "true")
private Boolean hotRecommend;
}
@@ -118,6 +118,10 @@ public class ProductSpuRespVO {
@ExcelProperty("分销类型")
private Boolean subCommissionType;
@Schema(description = "是否热门推荐", example = "false")
@ExcelProperty("热门推荐")
private Boolean hotRecommend;
// ========== 统计相关字段 =========
@Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000")
@@ -85,6 +85,9 @@ public class ProductSpuSaveReqVO {
@NotNull(message = "商品分销类型不能为空")
private Boolean subCommissionType;
@Schema(description = "是否热门推荐", example = "false")
private Boolean hotRecommend;
// ========== 统计相关字段 =========
@Schema(description = "虚拟销量", example = "66")
@@ -106,6 +106,14 @@ public class AppProductSpuController {
return success(voPageResult);
}
@GetMapping("/hot-recommend-page")
@Operation(summary = "获得热门推荐商品 SPU 分页(首页模块)")
@PermitAll
public CommonResult<PageResult<AppProductSpuRespVO>> getHotRecommendSpuPage(@Valid AppProductSpuPageReqVO pageVO) {
pageVO.setHotRecommend(Boolean.TRUE);
return getSpuPage(pageVO);
}
@GetMapping("/get-detail")
@Operation(summary = "获得商品 SPU 明细")
@Parameter(name = "id", description = "编号", required = true)
@@ -45,6 +45,9 @@ public class AppProductSpuPageReqVO extends PageParam {
@Schema(description = "关键字", example = "好看")
private String keyword;
@Schema(description = "是否热门推荐;不传表示不按该条件筛选", example = "true")
private Boolean hotRecommend;
@Schema(description = "排序字段", example = "price") // 参见 AppProductSpuPageReqVO.SORT_FIELD_XXX 常量
private String sortField;
@@ -167,6 +167,11 @@ public class ProductSpuDO extends BaseDO {
*/
private Boolean subCommissionType;
/**
* 是否热门推荐
*/
private Boolean hotRecommend;
// ========== 统计相关字段 =========
/**
@@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuCreatorDO;
import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
import cn.iocoder.yudao.module.product.enums.ProductConstants;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuTypesEnum;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
@@ -37,6 +38,7 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
.likeIfPresent(ProductSpuDO::getName, reqVO.getName())
.eqIfPresent(ProductSpuDO::getCategoryId, reqVO.getCategoryId())
.eqIfPresent(ProductSpuDO::getTypes, reqVO.getTypes())
.eq(reqVO.getHotRecommend() != null, ProductSpuDO::getHotRecommend, reqVO.getHotRecommend())
.betweenIfPresent(ProductSpuDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(ProductSpuDO::getSort)
.orderByDesc(ProductSpuDO::getId);
@@ -58,6 +60,7 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
.eqIfPresent(ProductSpuDO::getDeviceId, reqVO.getDeviceId())
.eqIfPresent(ProductSpuDO::getCategoryId, reqVO.getCategoryId())
.eqIfPresent(ProductSpuDO::getTypes, reqVO.getTypes())
.eq(reqVO.getHotRecommend() != null, ProductSpuDO::getHotRecommend, reqVO.getHotRecommend())
.betweenIfPresent(ProductSpuDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(ProductSpuDO::getCreateTime)
.orderByDesc(ProductSpuDO::getId);
@@ -91,6 +94,8 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
.eqIfPresent(ProductSpuDO::getDeviceId, pageReqVO.getDeviceId())
//根据类型查询商品
.eqIfPresent(ProductSpuDO::getTypes, pageReqVO.getTypes())
// 热门推荐
.eq(pageReqVO.getHotRecommend() != null, ProductSpuDO::getHotRecommend, pageReqVO.getHotRecommend())
// 关键字匹配,目前只匹配商品名
.likeIfPresent(ProductSpuDO::getName, pageReqVO.getKeyword())
// 分类
@@ -191,13 +196,16 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
default PageResult<ProductSpuCreatorDO> selectPageWithCreator(ProductSpuPageReqVO pageReqVO){
Integer tabType = pageReqVO.getTabType();
// types:0 商城商品、1 提货柜商品;未传与库表默认一致,按商城筛
int types = ObjectUtil.defaultIfNull(pageReqVO.getTypes(), ProductSpuTypesEnum.DISABLE.getTypes());
MPJLambdaWrapperX<ProductSpuDO> query = new MPJLambdaWrapperX<ProductSpuDO>()
.selectAll(ProductSpuDO.class)
.selectAs(AdminUserDO::getUsername, ProductSpuCreatorDO::getUsername)
.likeIfPresent(ProductSpuDO::getName, pageReqVO.getName())
.eqIfPresent(ProductSpuDO::getDeviceId, pageReqVO.getDeviceId())
.eq(ProductSpuDO::getTypes, types)
.eqIfPresent(ProductSpuDO::getCategoryId, pageReqVO.getCategoryId())
.eqIfPresent(ProductSpuDO::getTypes, pageReqVO.getTypes())
.eq(pageReqVO.getHotRecommend() != null, ProductSpuDO::getHotRecommend, pageReqVO.getHotRecommend())
.eqIfPresent(ProductSpuDO::getCreator, pageReqVO.getCreator())
.betweenIfPresent(ProductSpuDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(ProductSpuDO::getSort)
@@ -219,6 +227,7 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
.eqIfPresent(ProductSpuDO::getDeviceId, pageReqVO.getDeviceId())
.eqIfPresent(ProductSpuDO::getCategoryId, pageReqVO.getCategoryId())
.eqIfPresent(ProductSpuDO::getTypes, pageReqVO.getTypes())
.eq(pageReqVO.getHotRecommend() != null, ProductSpuDO::getHotRecommend, pageReqVO.getHotRecommend())
.eqIfPresent(ProductSpuDO::getCreator, pageReqVO.getCreator())
.betweenIfPresent(ProductSpuDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(ProductSpuDO::getCreateTime)
@@ -88,6 +88,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
productSkuService.validateSkuList(skuSaveReqList, createReqVO.getSpecType());
ProductSpuDO spu = BeanUtils.toBean(createReqVO, ProductSpuDO.class);
if (spu.getHotRecommend() == null) {
spu.setHotRecommend(Boolean.FALSE);
}
// 初始化 SPU 中 SKU 相关属性
initSpuFromSkus(spu, skuSaveReqList);
// 传入部门
@@ -114,6 +117,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
// 更新 SPU
ProductSpuDO updateObj = BeanUtils.toBean(updateReqVO, ProductSpuDO.class).setStatus(spu.getStatus());
if (updateObj.getHotRecommend() == null) {
updateObj.setHotRecommend(Boolean.TRUE.equals(spu.getHotRecommend()));
}
initSpuFromSkus(updateObj, skuSaveReqList);
productSpuMapper.updateById(updateObj);
// 批量更新 SKU
@@ -144,6 +150,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
// 6. 创建 SPU(复用现有逻辑)
ProductSpuDO spu = BeanUtils.toBean(spuSaveReqVO, ProductSpuDO.class);
if (spu.getHotRecommend() == null) {
spu.setHotRecommend(Boolean.FALSE);
}
// 初始化 SPU 中 SKU 相关属性
initSpuFromSkus(spu, skuSaveReqList);
// 传入部门
@@ -215,6 +224,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
ProductSpuDO updateObj = BeanUtils.toBean(spuSaveReqVO, ProductSpuDO.class)
.setId(spu.getId()) // 关键:更新场景必须带上原 SPU 主键,避免误新增/误更新
.setStatus(spu.getStatus());
if (updateObj.getHotRecommend() == null) {
updateObj.setHotRecommend(Boolean.TRUE.equals(spu.getHotRecommend()));
}
initSpuFromSkus(updateObj, skuSaveReqList);
productSpuMapper.updateById(updateObj);
// 批量更新 SKU
@@ -149,4 +149,8 @@ public interface ErrorCodeConstants {
ErrorCode GIFT_VOUCHER_TEMPLATE_EXPIRED = new ErrorCode(1_013_022_002, "提货券模板已过期");
ErrorCode GIFT_VOUCHER_TEMPLATE_NOT_ENOUGH = new ErrorCode(1_013_022_003, "当前剩余数量不够领取");
// ========== 营销榜单 1-013-023-000 ==========
ErrorCode RANKING_NOT_EXISTS = new ErrorCode(1_013_023_000, "营销榜单不存在");
ErrorCode RANKING_SPU_DUPLICATE = new ErrorCode(1_013_023_001, "榜单内存在重复的 SPU");
}
@@ -20,8 +20,7 @@ public class BannerBaseVO {
@NotNull(message = "标题不能为空")
private String title;
@Schema(description = "跳转链接", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "跳转链接不能为空")
@Schema(description = "跳转链接;不填或空字符串表示仅展示图片,点击不跳转", example = "https://example.com/path")
private String url;
@Schema(description = "图片地址", requiredMode = Schema.RequiredMode.REQUIRED)
@@ -26,6 +26,9 @@ public class DiscountActivityBaseVO {
@NotNull(message = "活动标题不能为空")
private String name;
@Schema(description = "活动图片", example = "https://www.iocoder.cn/xx.png")
private String picUrl;
@Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "开始时间不能为空")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.promotion.controller.admin.ranking;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingUpdateReqVO;
import cn.iocoder.yudao.module.promotion.service.ranking.RankingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 营销榜单")
@RestController
@RequestMapping("/promotion/ranking")
@Validated
public class RankingController {
@Resource
private RankingService rankingService;
@PostMapping("/create")
@Operation(summary = "创建营销榜单")
@PreAuthorize("@ss.hasPermission('promotion:ranking:create')")
public CommonResult<Long> createRanking(@Valid @RequestBody RankingCreateReqVO createReqVO) {
return success(rankingService.createRanking(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新营销榜单")
@PreAuthorize("@ss.hasPermission('promotion:ranking:update')")
public CommonResult<Boolean> updateRanking(@Valid @RequestBody RankingUpdateReqVO updateReqVO) {
rankingService.updateRanking(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除营销榜单")
@Parameter(name = "id", description = "榜单编号", required = true)
@PreAuthorize("@ss.hasPermission('promotion:ranking:delete')")
public CommonResult<Boolean> deleteRanking(@RequestParam("id") Long id) {
rankingService.deleteRanking(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得营销榜单详情")
@Parameter(name = "id", description = "榜单编号", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('promotion:ranking:query')")
public CommonResult<RankingRespVO> getRanking(@RequestParam("id") Long id) {
return success(rankingService.getRanking(id));
}
@GetMapping("/page")
@Operation(summary = "获得营销榜单分页")
@PreAuthorize("@ss.hasPermission('promotion:ranking:query')")
public CommonResult<PageResult<RankingRespVO>> getRankingPage(@Valid RankingPageReqVO pageVO) {
return success(rankingService.getRankingPage(pageVO));
}
}
@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
@Schema(description = "管理后台 - 营销榜单 Base VO")
@Data
public class RankingBaseVO {
@Schema(description = "榜单标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "热销榜")
@NotBlank(message = "榜单标题不能为空")
private String title;
@Schema(description = "副标题", example = "本周最受欢迎")
private String subtitle;
@Schema(description = "榜单图标 URL", example = "https://example.com/icon.png")
private String iconUrl;
@Schema(description = "排序,数值越大越靠前", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "排序不能为空")
private Integer sort;
@Schema(description = "状态:0 开启 1 关闭", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
@InEnum(CommonStatusEnum.class)
private Integer status;
@Schema(description = "备注")
private String remark;
@Schema(description = "榜单内商品(SPU)", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "榜单商品不能为空")
@Valid
private List<SpuItem> spus;
@Schema(description = "榜单商品项")
@Data
public static class SpuItem {
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "SPU 编号不能为空")
private Long spuId;
@Schema(description = "排序,数值越大越靠前", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "商品排序不能为空")
private Integer sort;
}
}
@@ -0,0 +1,13 @@
package cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - 营销榜单创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class RankingCreateReqVO extends RankingBaseVO {
}
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.promotion.controller.admin.ranking.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;
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 RankingPageReqVO extends PageParam {
@Schema(description = "榜单标题", example = "热销")
private String title;
@Schema(description = "状态:0 开启 1 关闭", example = "0")
private Integer status;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 营销榜单 Response VO")
@Data
public class RankingRespVO {
@Schema(description = "榜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "榜单标题", requiredMode = Schema.RequiredMode.REQUIRED)
private String title;
@Schema(description = "副标题")
private String subtitle;
@Schema(description = "榜单图标 URL")
private String iconUrl;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "状态:0 开启 1 关闭")
private Integer status;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "榜单内商品")
private List<SpuResp> spus;
@Schema(description = "榜单商品")
@Data
public static class SpuResp {
@Schema(description = "关联记录编号", example = "1")
private Long id;
@Schema(description = "SPU 编号", example = "10")
private Long spuId;
@Schema(description = "排序")
private Integer sort;
}
}
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 营销榜单更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class RankingUpdateReqVO extends RankingBaseVO {
@Schema(description = "榜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "榜单编号不能为空")
private Long id;
}
@@ -19,7 +19,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@RestController
@RequestMapping("/promotion/banner")
@Tag(name = "用户 APP - 首页 Banner")
@Tag(name = "用户 APP / 小程序 - 首页 Banner")
@Validated
public class AppBannerController {
@@ -27,8 +27,8 @@ public class AppBannerController {
private BannerService bannerService;
@GetMapping("/list")
@Operation(summary = "获得 banner 列表")
@Parameter(name = "position", description = "Banner position", example = "1")
@Operation(summary = "获得 banner 列表(仅启用;按排序字段降序,数值越大越靠前)")
@Parameter(name = "position", description = "位置:1=首页", example = "1")
@PermitAll
public CommonResult<List<AppBannerRespVO>> getBannerList(@RequestParam("position") Integer position) {
List<BannerDO> bannerList = bannerService.getBannerListByPosition(position);
@@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.promotion.controller.app.banner.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Schema(description = "用户 App - Banner Response VO")
@Schema(description = "用户 App / 小程序 - Banner Response VO")
@Data
public class AppBannerRespVO {
@@ -13,15 +11,12 @@ public class AppBannerRespVO {
private Long id;
@Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "标题不能为空")
private String title;
@Schema(description = "跳转链接", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "跳转链接不能为空")
@Schema(description = "跳转链接;为空表示不跳转", example = "/pages/goods/detail?id=1")
private String url;
@Schema(description = "图片地址", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "图片地址不能为空")
private String picUrl;
}
@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.promotion.controller.app.discount;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.promotion.controller.app.discount.vo.AppDiscountActivityRespVO;
import cn.iocoder.yudao.module.promotion.service.discount.DiscountActivityService;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
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 javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import java.time.Duration;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
@Tag(name = "用户 App - 限时折扣活动")
@RestController
@RequestMapping("/promotion/discount-activity")
@Validated
public class AppDiscountActivityController {
private static final int ACTIVITY_COUNT_MIN = 1;
private static final int ACTIVITY_COUNT_MAX = 20;
private static final int PRODUCT_COUNT_MIN = 1;
private static final int PRODUCT_COUNT_MAX = 50;
private final LoadingCache<String, List<AppDiscountActivityRespVO>> homeListCache =
buildAsyncReloadingCache(Duration.ofSeconds(10L), new CacheLoader<String, List<AppDiscountActivityRespVO>>() {
@Override
public List<AppDiscountActivityRespVO> load(String key) {
String[] parts = key.split("_", 2);
int activityCount = Integer.parseInt(parts[0]);
int productCount = Integer.parseInt(parts[1]);
return discountActivityService.getAppHomeDiscountActivityList(activityCount, productCount);
}
});
@Resource
private DiscountActivityService discountActivityService;
@GetMapping("/home-list")
@Operation(summary = "获得进行中的限时折扣活动列表", description = "用于 App/小程序首页模块;含活动基本信息及每个活动下若干上架商品与活动价")
@Parameter(name = "activityCount", description = "活动条数上限", example = "3")
@Parameter(name = "productCount", description = "每个活动展示的商品数上限", example = "6")
@PermitAll
public CommonResult<List<AppDiscountActivityRespVO>> getHomeList(
@RequestParam(name = "activityCount", defaultValue = "3") Integer activityCount,
@RequestParam(name = "productCount", defaultValue = "6") Integer productCount) {
int ac = Math.min(Math.max(activityCount, ACTIVITY_COUNT_MIN), ACTIVITY_COUNT_MAX);
int pc = Math.min(Math.max(productCount, PRODUCT_COUNT_MIN), PRODUCT_COUNT_MAX);
return success(homeListCache.getUnchecked(ac + "_" + pc));
}
}
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.promotion.controller.app.discount.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户 App - 限时折扣活动商品")
@Data
public class AppDiscountActivityProductRespVO {
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long spuId;
@Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long skuId;
@Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "示例商品")
private String spuName;
@Schema(description = "展示图(SKU 图优先,否则 SPU 封面)", example = "https://...")
private String picUrl;
@Schema(description = "原价(SKU 销售价),单位:分", example = "1000")
private Integer originalPrice;
@Schema(description = "市场价,单位:分", example = "2000")
private Integer marketPrice;
@Schema(description = "优惠类型:1 满减(减价) 2 折扣(百分比)", example = "2")
private Integer discountType;
@Schema(description = "折扣百分比,如 80 表示付原价的 80%", example = "80")
private Integer discountPercent;
@Schema(description = "优惠金额(减价类型),单位:分", example = "100")
private Integer discountPrice;
@Schema(description = "活动价(单件),单位:分", example = "800")
private Integer activityPrice;
}
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.promotion.controller.app.discount.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "用户 App - 限时折扣活动(首页模块)")
@Data
public class AppDiscountActivityRespVO {
@Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "活动名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "春季限时折")
private String name;
@Schema(description = "活动图片", example = "https://www.iocoder.cn/xx.png")
private String picUrl;
@Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime startTime;
@Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime endTime;
@Schema(description = "备注")
private String remark;
@Schema(description = "活动内展示的商品(仅上架 SPU)")
private List<AppDiscountActivityProductRespVO> products;
}
@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.promotion.controller.app.ranking;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.promotion.controller.app.ranking.vo.AppRankingRespVO;
import cn.iocoder.yudao.module.promotion.service.ranking.RankingService;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
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 javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import java.time.Duration;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
@Tag(name = "用户 App - 营销榜单")
@RestController
@RequestMapping("/promotion/ranking")
@Validated
public class AppRankingController {
private static final int COUNT_MIN = 1;
private static final int COUNT_MAX = 50;
private final LoadingCache<Integer, List<AppRankingRespVO>> listCache =
buildAsyncReloadingCache(Duration.ofSeconds(10L), new CacheLoader<Integer, List<AppRankingRespVO>>() {
@Override
public List<AppRankingRespVO> load(Integer count) {
return rankingService.getAppRankingList(count);
}
});
@Resource
private RankingService rankingService;
@GetMapping("/list")
@Operation(summary = "获得营销榜单列表", description = "仅返回开启状态的榜单;商品仅包含上架 SPU,用于首页/榜单频道")
@Parameter(name = "count", description = "最多返回几条榜单", example = "10")
@PermitAll
public CommonResult<List<AppRankingRespVO>> getRankingList(
@RequestParam(name = "count", defaultValue = "10") Integer count) {
int c = Math.min(Math.max(count, COUNT_MIN), COUNT_MAX);
return success(listCache.getUnchecked(c));
}
@GetMapping("/get")
@Operation(summary = "获得营销榜单详情", description = "榜单须为开启状态,否则返回 null;商品仅上架 SPU")
@Parameter(name = "id", description = "榜单编号", required = true, example = "1")
@PermitAll
public CommonResult<AppRankingRespVO> getRanking(@RequestParam("id") Long id) {
return success(rankingService.getAppRanking(id));
}
}
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.promotion.controller.app.ranking.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "用户 App - 营销榜单")
@Data
public class AppRankingRespVO {
@Schema(description = "榜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "榜单标题", requiredMode = Schema.RequiredMode.REQUIRED)
private String title;
@Schema(description = "副标题")
private String subtitle;
@Schema(description = "榜单图标 URL")
private String iconUrl;
@Schema(description = "排序,越大越靠前")
private Integer sort;
@Schema(description = "上架商品列表(仅展示上架 SPU;下架商品会被过滤)")
private List<AppRankingSpuRespVO> spus;
}
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.promotion.controller.app.ranking.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户 App - 榜单商品")
@Data
public class AppRankingSpuRespVO {
@Schema(description = "榜单-SPU 关联 id", example = "1")
private Long id;
@Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Long spuId;
@Schema(description = "排序,越大越靠前", example = "10")
private Integer sort;
@Schema(description = "商品名称", example = "示例商品")
private String name;
@Schema(description = "封面图", example = "https://...")
private String picUrl;
@Schema(description = "销售价(SPU 汇总展示价),单位:分", example = "1990")
private Integer price;
@Schema(description = "市场价,单位:分", example = "2990")
private Integer marketPrice;
}
@@ -33,6 +33,10 @@ public class DiscountActivityDO extends BaseDO {
* 活动标题
*/
private String name;
/**
* 活动图片
*/
private String picUrl;
/**
* 状态
*
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.ranking;
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
*/
@TableName("promotion_ranking")
@KeySequence("promotion_ranking_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RankingDO extends BaseDO {
@TableId
private Long id;
/** 榜单标题 */
private String title;
/** 副标题 */
private String subtitle;
/** 榜单图标 URL */
private String iconUrl;
/** 排序,越大越靠前 */
private Integer sort;
/** 状态 {@link CommonStatusEnum} */
private Integer status;
/** 备注 */
private String remark;
}
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.ranking;
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
*/
@TableName("promotion_ranking_spu")
@KeySequence("promotion_ranking_spu_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RankingSpuDO extends BaseDO {
@TableId
private Long id;
/** 榜单编号 */
private Long rankingId;
/** 商品 SPU 编号 */
private Long spuId;
/** 排序,越大越靠前 */
private Integer sort;
}
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.banner;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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;
@@ -33,7 +34,11 @@ public interface BannerMapper extends BaseMapperX<BannerDO> {
}
default List<BannerDO> selectBannerListByPosition(Integer position) {
return selectList(new LambdaQueryWrapperX<BannerDO>().eq(BannerDO::getPosition, position));
return selectList(new LambdaQueryWrapperX<BannerDO>()
.eq(BannerDO::getPosition, position)
.eq(BannerDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
.orderByDesc(BannerDO::getSort)
.orderByDesc(BannerDO::getId));
}
}
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.discount;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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;
@@ -43,4 +44,17 @@ public interface DiscountActivityMapper extends BaseMapperX<DiscountActivityDO>
.orderByDesc(DiscountActivityDO::getCreateTime));
}
/**
* App 首页:进行中的限时折扣活动(开启且当前时间在活动期内),与砍价首页时间条件一致
*/
default List<DiscountActivityDO> selectAppHomeList(int limit) {
LocalDateTime now = LocalDateTime.now();
return selectList(new LambdaQueryWrapperX<DiscountActivityDO>()
.eq(DiscountActivityDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
.le(DiscountActivityDO::getStartTime, now)
.ge(DiscountActivityDO::getEndTime, now)
.orderByDesc(DiscountActivityDO::getId)
.last("LIMIT " + limit));
}
}
@@ -44,4 +44,14 @@ public interface DiscountProductMapper extends BaseMapperX<DiscountProductDO> {
.gt(DiscountProductDO::getActivityEndTime, now));
}
/**
* App 首页:单个活动下展示用商品(按 id 顺序取前 limit 条)
*/
default List<DiscountProductDO> selectListByActivityIdWithLimit(Long activityId, int limit) {
return selectList(new LambdaQueryWrapperX<DiscountProductDO>()
.eq(DiscountProductDO::getActivityId, activityId)
.orderByAsc(DiscountProductDO::getId)
.last("LIMIT " + limit));
}
}
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.ranking;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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.ranking.vo.RankingPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.ranking.RankingDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface RankingMapper extends BaseMapperX<RankingDO> {
default PageResult<RankingDO> selectPage(RankingPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<RankingDO>()
.likeIfPresent(RankingDO::getTitle, reqVO.getTitle())
.eqIfPresent(RankingDO::getStatus, reqVO.getStatus())
.betweenIfPresent(RankingDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(RankingDO::getSort)
.orderByDesc(RankingDO::getId));
}
/**
* App:仅开启的榜单,按 sort、id 降序
*/
default List<RankingDO> selectAppEnabledList(int limit) {
return selectList(new LambdaQueryWrapperX<RankingDO>()
.eq(RankingDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
.orderByDesc(RankingDO::getSort)
.orderByDesc(RankingDO::getId)
.last("LIMIT " + limit));
}
}
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.ranking;
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.ranking.RankingSpuDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface RankingSpuMapper extends BaseMapperX<RankingSpuDO> {
default List<RankingSpuDO> selectListByRankingId(Long rankingId) {
return selectList(new LambdaQueryWrapperX<RankingSpuDO>()
.eq(RankingSpuDO::getRankingId, rankingId)
.orderByDesc(RankingSpuDO::getSort)
.orderByDesc(RankingSpuDO::getId));
}
default List<RankingSpuDO> selectListByRankingIds(Collection<Long> rankingIds) {
return selectList(new LambdaQueryWrapperX<RankingSpuDO>()
.in(RankingSpuDO::getRankingId, rankingIds)
.orderByDesc(RankingSpuDO::getSort)
.orderByDesc(RankingSpuDO::getId));
}
default void deleteByRankingId(Long rankingId) {
delete(RankingSpuDO::getRankingId, rankingId);
}
}
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.promotion.service.banner;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.banner.vo.BannerPageReqVO;
@@ -32,6 +33,7 @@ public class BannerServiceImpl implements BannerService {
public Long createBanner(BannerCreateReqVO createReqVO) {
// 插入
BannerDO banner = BannerConvert.INSTANCE.convert(createReqVO);
banner.setUrl(blankUrlToEmpty(banner.getUrl()));
bannerMapper.insert(banner);
// 返回
return banner.getId();
@@ -43,6 +45,7 @@ public class BannerServiceImpl implements BannerService {
this.validateBannerExists(updateReqVO.getId());
// 更新
BannerDO updateObj = BannerConvert.INSTANCE.convert(updateReqVO);
updateObj.setUrl(blankUrlToEmpty(updateObj.getUrl()));
bannerMapper.updateById(updateObj);
}
@@ -83,4 +86,9 @@ public class BannerServiceImpl implements BannerService {
return bannerMapper.selectBannerListByPosition(position);
}
/** DB 字段 url 多为 NOT NULL,未配置跳转时存空串 */
private static String blankUrlToEmpty(String url) {
return StrUtil.nullToDefault(StrUtil.trim(url), "");
}
}
@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
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.DiscountActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.discount.vo.AppDiscountActivityRespVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountActivityDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
@@ -89,4 +90,12 @@ public interface DiscountActivityService {
*/
List<DiscountProductDO> getDiscountProductsByActivityId(Collection<Long> activityIds);
/**
* App 首页:进行中的限时折扣活动及每个活动下若干商品(仅上架 SPU)
*
* @param activityCount 活动数量上限
* @param productCount 每个活动内商品数量上限
*/
List<AppDiscountActivityRespVO> getAppHomeDiscountActivityList(Integer activityCount, Integer productCount);
}
@@ -2,19 +2,27 @@ package cn.iocoder.yudao.module.promotion.service.discount;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
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.number.MoneyUtils;
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.product.enums.spu.ProductSpuStatusEnum;
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;
import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityUpdateReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.discount.vo.AppDiscountActivityProductRespVO;
import cn.iocoder.yudao.module.promotion.controller.app.discount.vo.AppDiscountActivityRespVO;
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;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
import cn.iocoder.yudao.module.promotion.dal.mysql.discount.DiscountActivityMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.discount.DiscountProductMapper;
import org.springframework.stereotype.Service;
@@ -22,7 +30,9 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -49,6 +59,8 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
@Resource
private ProductSkuApi productSkuApi;
@Resource
private ProductSpuApi productSpuApi;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -59,7 +71,7 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
validateProductExists(createReqVO.getProducts());
// 插入活动
DiscountActivityDO discountActivity = DiscountActivityConvert.INSTANCE.convert(createReqVO)
DiscountActivityDO discountActivity = BeanUtils.toBean(createReqVO, DiscountActivityDO.class)
.setStatus(CommonStatusEnum.ENABLE.getStatus());
discountActivityMapper.insert(discountActivity);
// 插入商品
@@ -86,7 +98,7 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
validateProductExists(updateReqVO.getProducts());
// 更新活动
DiscountActivityDO updateObj = DiscountActivityConvert.INSTANCE.convert(updateReqVO);
DiscountActivityDO updateObj = BeanUtils.toBean(updateReqVO, DiscountActivityDO.class);
discountActivityMapper.updateById(updateObj);
// 更新商品
updateDiscountProduct(updateObj, updateReqVO.getProducts());
@@ -234,4 +246,86 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
return discountProductMapper.selectListBySkuIdsAndStatusAndNow(skuIds, CommonStatusEnum.ENABLE.getStatus());
}
@Override
public List<AppDiscountActivityRespVO> getAppHomeDiscountActivityList(Integer activityCount, Integer productCount) {
int ac = activityCount == null || activityCount < 1 ? 3 : activityCount;
int pc = productCount == null || productCount < 1 ? 6 : productCount;
List<DiscountActivityDO> activities = discountActivityMapper.selectAppHomeList(ac);
if (CollUtil.isEmpty(activities)) {
return Collections.emptyList();
}
List<AppDiscountActivityRespVO> result = new ArrayList<>(activities.size());
for (DiscountActivityDO activity : activities) {
AppDiscountActivityRespVO vo = new AppDiscountActivityRespVO();
vo.setId(activity.getId());
vo.setName(activity.getName());
vo.setPicUrl(activity.getPicUrl());
vo.setStartTime(activity.getStartTime());
vo.setEndTime(activity.getEndTime());
vo.setRemark(activity.getRemark());
vo.setProducts(buildAppHomeProducts(activity.getId(), pc));
result.add(vo);
}
return result;
}
/**
* 多取一些活动商品再过滤上架,避免下架 SPU 把坑位占满
*/
private List<AppDiscountActivityProductRespVO> buildAppHomeProducts(Long activityId, int productCount) {
int fetchLimit = Math.min(200, Math.max(productCount * 15, productCount));
List<DiscountProductDO> rawList = discountProductMapper.selectListByActivityIdWithLimit(activityId, fetchLimit);
if (CollUtil.isEmpty(rawList)) {
return Collections.emptyList();
}
List<ProductSkuRespDTO> skus = productSkuApi.getSkuList(convertList(rawList, DiscountProductDO::getSkuId));
Map<Long, ProductSkuRespDTO> skuMap = convertMap(skus, ProductSkuRespDTO::getId);
List<ProductSpuRespDTO> spus = productSpuApi.getSpuList(convertList(rawList, DiscountProductDO::getSpuId));
Map<Long, ProductSpuRespDTO> spuMap = convertMap(spus, ProductSpuRespDTO::getId);
List<AppDiscountActivityProductRespVO> items = new ArrayList<>();
for (DiscountProductDO dp : rawList) {
if (items.size() >= productCount) {
break;
}
ProductSpuRespDTO spu = spuMap.get(dp.getSpuId());
ProductSkuRespDTO sku = skuMap.get(dp.getSkuId());
if (spu == null || sku == null || !ProductSpuStatusEnum.isEnable(spu.getStatus())) {
continue;
}
Integer skuPrice = sku.getPrice();
if (skuPrice == null) {
continue;
}
AppDiscountActivityProductRespVO pvo = new AppDiscountActivityProductRespVO();
pvo.setSpuId(dp.getSpuId());
pvo.setSkuId(dp.getSkuId());
pvo.setSpuName(spu.getName());
pvo.setPicUrl(StrUtil.emptyToDefault(sku.getPicUrl(), spu.getPicUrl()));
pvo.setOriginalPrice(skuPrice);
pvo.setMarketPrice(ObjectUtil.defaultIfNull(sku.getMarketPrice(), spu.getMarketPrice()));
pvo.setDiscountType(dp.getDiscountType());
pvo.setDiscountPercent(dp.getDiscountPercent());
pvo.setDiscountPrice(dp.getDiscountPrice());
pvo.setActivityPrice(calculateDiscountSkuActivityPrice(skuPrice, dp));
items.add(pvo);
}
return items;
}
private static Integer calculateDiscountSkuActivityPrice(int skuPrice, DiscountProductDO p) {
if (PromotionDiscountTypeEnum.PRICE.getType().equals(p.getDiscountType())) {
int off = ObjectUtil.defaultIfNull(p.getDiscountPrice(), 0);
return Math.max(skuPrice - off, 0);
}
if (PromotionDiscountTypeEnum.PERCENT.getType().equals(p.getDiscountType())) {
Integer percent = p.getDiscountPercent();
if (percent == null) {
return skuPrice;
}
return MoneyUtils.calculateRatePrice(skuPrice, percent.doubleValue());
}
return skuPrice;
}
}
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.promotion.service.ranking;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingUpdateReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.ranking.vo.AppRankingRespVO;
import javax.validation.Valid;
import java.util.List;
public interface RankingService {
Long createRanking(@Valid RankingCreateReqVO createReqVO);
void updateRanking(@Valid RankingUpdateReqVO updateReqVO);
void deleteRanking(Long id);
RankingRespVO getRanking(Long id);
PageResult<RankingRespVO> getRankingPage(RankingPageReqVO pageReqVO);
/**
* App:开启榜单列表(含上架 SPU 摘要),条数由调用方限制
*/
List<AppRankingRespVO> getAppRankingList(int count);
/**
* App:榜单详情;非开启或未找到返回 null
*/
AppRankingRespVO getAppRanking(Long id);
}
@@ -0,0 +1,222 @@
package cn.iocoder.yudao.module.promotion.service.ranking;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
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.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingBaseVO;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingCreateReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.ranking.vo.RankingUpdateReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.ranking.vo.AppRankingRespVO;
import cn.iocoder.yudao.module.promotion.controller.app.ranking.vo.AppRankingSpuRespVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.ranking.RankingDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.ranking.RankingSpuDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.ranking.RankingMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.ranking.RankingSpuMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.ArrayList;
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.module.promotion.enums.ErrorCodeConstants.RANKING_NOT_EXISTS;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.RANKING_SPU_DUPLICATE;
@Service
@Validated
public class RankingServiceImpl implements RankingService {
@Resource
private RankingMapper rankingMapper;
@Resource
private RankingSpuMapper rankingSpuMapper;
@Resource
private ProductSpuApi productSpuApi;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createRanking(RankingCreateReqVO createReqVO) {
validateSpuItems(createReqVO.getSpus());
productSpuApi.validateSpuList(convertList(createReqVO.getSpus(), RankingBaseVO.SpuItem::getSpuId));
RankingDO ranking = BeanUtils.toBean(createReqVO, RankingDO.class);
ranking.setSubtitle(StrUtil.nullToDefault(createReqVO.getSubtitle(), ""));
ranking.setIconUrl(StrUtil.nullToDefault(createReqVO.getIconUrl(), ""));
ranking.setStatus(ObjectUtil.defaultIfNull(createReqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus()));
rankingMapper.insert(ranking);
List<RankingSpuDO> spus = buildRankingSpus(ranking.getId(), createReqVO.getSpus());
rankingSpuMapper.insertBatch(spus);
return ranking.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateRanking(RankingUpdateReqVO updateReqVO) {
validateRankingExists(updateReqVO.getId());
validateSpuItems(updateReqVO.getSpus());
productSpuApi.validateSpuList(convertList(updateReqVO.getSpus(), RankingBaseVO.SpuItem::getSpuId));
RankingDO updateObj = BeanUtils.toBean(updateReqVO, RankingDO.class);
updateObj.setSubtitle(StrUtil.nullToDefault(updateReqVO.getSubtitle(), ""));
updateObj.setIconUrl(StrUtil.nullToDefault(updateReqVO.getIconUrl(), ""));
rankingMapper.updateById(updateObj);
updateRankingSpus(updateReqVO.getId(), updateReqVO.getSpus());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteRanking(Long id) {
validateRankingExists(id);
rankingSpuMapper.delete(RankingSpuDO::getRankingId, id);
rankingMapper.deleteById(id);
}
@Override
public RankingRespVO getRanking(Long id) {
RankingDO ranking = rankingMapper.selectById(id);
if (ranking == null) {
return null;
}
List<RankingSpuDO> spuRows = rankingSpuMapper.selectListByRankingId(id);
return buildRespVO(ranking, spuRows);
}
@Override
public PageResult<RankingRespVO> getRankingPage(RankingPageReqVO pageReqVO) {
PageResult<RankingDO> page = rankingMapper.selectPage(pageReqVO);
if (CollUtil.isEmpty(page.getList())) {
return PageResult.empty(page.getTotal());
}
List<RankingSpuDO> allSpus = rankingSpuMapper.selectListByRankingIds(convertList(page.getList(), RankingDO::getId));
Map<Long, List<RankingSpuDO>> spuMap = CollectionUtils.convertMultiMap(allSpus, RankingSpuDO::getRankingId);
List<RankingRespVO> list = new ArrayList<>(page.getList().size());
for (RankingDO ranking : page.getList()) {
list.add(buildRespVO(ranking, spuMap.getOrDefault(ranking.getId(), CollUtil.newArrayList())));
}
return new PageResult<>(list, page.getTotal());
}
private void validateRankingExists(Long id) {
if (rankingMapper.selectById(id) == null) {
throw exception(RANKING_NOT_EXISTS);
}
}
private static void validateSpuItems(List<RankingBaseVO.SpuItem> spus) {
long distinct = spus.stream().map(RankingBaseVO.SpuItem::getSpuId).distinct().count();
if (distinct != spus.size()) {
throw exception(RANKING_SPU_DUPLICATE);
}
}
private static List<RankingSpuDO> buildRankingSpus(Long rankingId, List<RankingBaseVO.SpuItem> items) {
return BeanUtils.toBean(items, RankingSpuDO.class, row -> row.setRankingId(rankingId));
}
private void updateRankingSpus(Long rankingId, List<RankingBaseVO.SpuItem> items) {
List<RankingSpuDO> newList = buildRankingSpus(rankingId, items);
List<RankingSpuDO> oldList = rankingSpuMapper.selectListByRankingId(rankingId);
List<List<RankingSpuDO>> diffList = CollectionUtils.diffList(oldList, newList, (oldVal, newVal) -> {
boolean same = ObjectUtil.equal(oldVal.getSpuId(), newVal.getSpuId());
if (same) {
newVal.setId(oldVal.getId());
}
return same;
});
if (CollUtil.isNotEmpty(diffList.get(0))) {
rankingSpuMapper.insertBatch(diffList.get(0));
}
if (CollUtil.isNotEmpty(diffList.get(1))) {
rankingSpuMapper.updateBatch(diffList.get(1));
}
if (CollUtil.isNotEmpty(diffList.get(2))) {
rankingSpuMapper.deleteByIds(convertList(diffList.get(2), RankingSpuDO::getId));
}
}
private static RankingRespVO buildRespVO(RankingDO ranking, List<RankingSpuDO> spuRows) {
RankingRespVO vo = BeanUtils.toBean(ranking, RankingRespVO.class);
List<RankingRespVO.SpuResp> spus = convertList(spuRows, row -> {
RankingRespVO.SpuResp r = new RankingRespVO.SpuResp();
r.setId(row.getId());
r.setSpuId(row.getSpuId());
r.setSort(row.getSort());
return r;
});
vo.setSpus(spus);
return vo;
}
@Override
public List<AppRankingRespVO> getAppRankingList(int count) {
List<RankingDO> rankings = rankingMapper.selectAppEnabledList(count);
if (CollUtil.isEmpty(rankings)) {
return Collections.emptyList();
}
List<AppRankingRespVO> result = new ArrayList<>(rankings.size());
for (RankingDO ranking : rankings) {
result.add(buildAppRankingRespVO(ranking));
}
return result;
}
@Override
public AppRankingRespVO getAppRanking(Long id) {
RankingDO ranking = rankingMapper.selectById(id);
if (ranking == null || !CommonStatusEnum.ENABLE.getStatus().equals(ranking.getStatus())) {
return null;
}
return buildAppRankingRespVO(ranking);
}
private AppRankingRespVO buildAppRankingRespVO(RankingDO ranking) {
List<RankingSpuDO> spuRows = rankingSpuMapper.selectListByRankingId(ranking.getId());
Map<Long, ProductSpuRespDTO> spuMap = Collections.emptyMap();
if (CollUtil.isNotEmpty(spuRows)) {
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(convertList(spuRows, RankingSpuDO::getSpuId));
spuMap = CollectionUtils.convertMap(spuList, ProductSpuRespDTO::getId);
}
List<AppRankingSpuRespVO> items = new ArrayList<>();
for (RankingSpuDO row : spuRows) {
ProductSpuRespDTO spu = spuMap.get(row.getSpuId());
if (spu == null || !ProductSpuStatusEnum.isEnable(spu.getStatus())) {
continue;
}
AppRankingSpuRespVO item = new AppRankingSpuRespVO();
item.setId(row.getId());
item.setSpuId(row.getSpuId());
item.setSort(row.getSort());
item.setName(spu.getName());
item.setPicUrl(spu.getPicUrl());
item.setPrice(spu.getPrice());
item.setMarketPrice(spu.getMarketPrice());
items.add(item);
}
AppRankingRespVO vo = new AppRankingRespVO();
vo.setId(ranking.getId());
vo.setTitle(ranking.getTitle());
vo.setSubtitle(ranking.getSubtitle());
vo.setIconUrl(ranking.getIconUrl());
vo.setSort(ranking.getSort());
vo.setSpus(items);
return vo;
}
}