diff --git a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java index 00f30d4..3ad4f22 100644 --- a/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java +++ b/yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java @@ -120,6 +120,11 @@ public class ProductSpuRespDTO { */ private Boolean subCommissionType; + /** + * 是否热门推荐 + */ + private Boolean hotRecommend; + // ========== 限购相关字段 ========= /** diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuPageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuPageReqVO.java index 4b1dd39..08e2d89 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuPageReqVO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuPageReqVO.java @@ -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; + } diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java index 513b2a7..cfc36af 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java @@ -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") diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuSaveReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuSaveReqVO.java index eae6523..9cc7554 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuSaveReqVO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuSaveReqVO.java @@ -85,6 +85,9 @@ public class ProductSpuSaveReqVO { @NotNull(message = "商品分销类型不能为空") private Boolean subCommissionType; + @Schema(description = "是否热门推荐", example = "false") + private Boolean hotRecommend; + // ========== 统计相关字段 ========= @Schema(description = "虚拟销量", example = "66") diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java index 3680339..fb68a12 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java @@ -106,6 +106,14 @@ public class AppProductSpuController { return success(voPageResult); } + @GetMapping("/hot-recommend-page") + @Operation(summary = "获得热门推荐商品 SPU 分页(首页模块)") + @PermitAll + public CommonResult> getHotRecommendSpuPage(@Valid AppProductSpuPageReqVO pageVO) { + pageVO.setHotRecommend(Boolean.TRUE); + return getSpuPage(pageVO); + } + @GetMapping("/get-detail") @Operation(summary = "获得商品 SPU 明细") @Parameter(name = "id", description = "编号", required = true) diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageReqVO.java index 12eb02f..a0a2faa 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageReqVO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageReqVO.java @@ -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; diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/spu/ProductSpuDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/spu/ProductSpuDO.java index e35f0d8..9cbb8bb 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/spu/ProductSpuDO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/spu/ProductSpuDO.java @@ -167,6 +167,11 @@ public class ProductSpuDO extends BaseDO { */ private Boolean subCommissionType; + /** + * 是否热门推荐 + */ + private Boolean hotRecommend; + // ========== 统计相关字段 ========= /** diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java index df0eeef..5593259 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java @@ -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 { .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 { .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 { .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 { default PageResult selectPageWithCreator(ProductSpuPageReqVO pageReqVO){ Integer tabType = pageReqVO.getTabType(); + // types:0 商城商品、1 提货柜商品;未传与库表默认一致,按商城筛 + int types = ObjectUtil.defaultIfNull(pageReqVO.getTypes(), ProductSpuTypesEnum.DISABLE.getTypes()); MPJLambdaWrapperX query = new MPJLambdaWrapperX() .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 { .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) diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java index 9ed1382..bd6bd83 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java @@ -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 diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java index 6e39e07..8c70a8b 100644 --- a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java +++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/ErrorCodeConstants.java @@ -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"); + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/banner/vo/BannerBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/banner/vo/BannerBaseVO.java index 0818257..6f525f0 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/banner/vo/BannerBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/banner/vo/BannerBaseVO.java @@ -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) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/discount/vo/DiscountActivityBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/discount/vo/DiscountActivityBaseVO.java index 193f7bb..c470a95 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/discount/vo/DiscountActivityBaseVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/discount/vo/DiscountActivityBaseVO.java @@ -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) diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/RankingController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/RankingController.java new file mode 100644 index 0000000..4957e13 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/RankingController.java @@ -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 createRanking(@Valid @RequestBody RankingCreateReqVO createReqVO) { + return success(rankingService.createRanking(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新营销榜单") + @PreAuthorize("@ss.hasPermission('promotion:ranking:update')") + public CommonResult 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 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 getRanking(@RequestParam("id") Long id) { + return success(rankingService.getRanking(id)); + } + + @GetMapping("/page") + @Operation(summary = "获得营销榜单分页") + @PreAuthorize("@ss.hasPermission('promotion:ranking:query')") + public CommonResult> getRankingPage(@Valid RankingPageReqVO pageVO) { + return success(rankingService.getRankingPage(pageVO)); + } + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingBaseVO.java new file mode 100644 index 0000000..9f5bd62 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingBaseVO.java @@ -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 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; + + } + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingCreateReqVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingCreateReqVO.java new file mode 100644 index 0000000..67ada69 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingCreateReqVO.java @@ -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 { +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingPageReqVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingPageReqVO.java new file mode 100644 index 0000000..b9375ec --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingPageReqVO.java @@ -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; + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingRespVO.java new file mode 100644 index 0000000..70427a3 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingRespVO.java @@ -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 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; + + } + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingUpdateReqVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingUpdateReqVO.java new file mode 100644 index 0000000..452d641 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/ranking/vo/RankingUpdateReqVO.java @@ -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; + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/banner/AppBannerController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/banner/AppBannerController.java index 6849ac9..9f223dd 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/banner/AppBannerController.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/banner/AppBannerController.java @@ -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> getBannerList(@RequestParam("position") Integer position) { List bannerList = bannerService.getBannerListByPosition(position); diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/banner/vo/AppBannerRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/banner/vo/AppBannerRespVO.java index cc36d87..2a2b042 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/banner/vo/AppBannerRespVO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/banner/vo/AppBannerRespVO.java @@ -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; } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/discount/AppDiscountActivityController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/discount/AppDiscountActivityController.java new file mode 100644 index 0000000..efbf0a3 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/discount/AppDiscountActivityController.java @@ -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> homeListCache = + buildAsyncReloadingCache(Duration.ofSeconds(10L), new CacheLoader>() { + @Override + public List 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> 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)); + } + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/discount/vo/AppDiscountActivityProductRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/discount/vo/AppDiscountActivityProductRespVO.java new file mode 100644 index 0000000..2ad4e61 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/discount/vo/AppDiscountActivityProductRespVO.java @@ -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; + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/discount/vo/AppDiscountActivityRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/discount/vo/AppDiscountActivityRespVO.java new file mode 100644 index 0000000..c44db12 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/discount/vo/AppDiscountActivityRespVO.java @@ -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 products; + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/ranking/AppRankingController.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/ranking/AppRankingController.java new file mode 100644 index 0000000..5e6f216 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/ranking/AppRankingController.java @@ -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> listCache = + buildAsyncReloadingCache(Duration.ofSeconds(10L), new CacheLoader>() { + @Override + public List 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> 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 getRanking(@RequestParam("id") Long id) { + return success(rankingService.getAppRanking(id)); + } + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/ranking/vo/AppRankingRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/ranking/vo/AppRankingRespVO.java new file mode 100644 index 0000000..38e5a84 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/ranking/vo/AppRankingRespVO.java @@ -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 spus; + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/ranking/vo/AppRankingSpuRespVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/ranking/vo/AppRankingSpuRespVO.java new file mode 100644 index 0000000..69207e3 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/ranking/vo/AppRankingSpuRespVO.java @@ -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; + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/discount/DiscountActivityDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/discount/DiscountActivityDO.java index 956a223..75e07a6 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/discount/DiscountActivityDO.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/discount/DiscountActivityDO.java @@ -33,6 +33,10 @@ public class DiscountActivityDO extends BaseDO { * 活动标题 */ private String name; + /** + * 活动图片 + */ + private String picUrl; /** * 状态 * diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/ranking/RankingDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/ranking/RankingDO.java new file mode 100644 index 0000000..8eda163 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/ranking/RankingDO.java @@ -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; + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/ranking/RankingSpuDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/ranking/RankingSpuDO.java new file mode 100644 index 0000000..8141036 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/ranking/RankingSpuDO.java @@ -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; + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/banner/BannerMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/banner/BannerMapper.java index 74bd3c7..b50eb83 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/banner/BannerMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/banner/BannerMapper.java @@ -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 { } default List selectBannerListByPosition(Integer position) { - return selectList(new LambdaQueryWrapperX().eq(BannerDO::getPosition, position)); + return selectList(new LambdaQueryWrapperX() + .eq(BannerDO::getPosition, position) + .eq(BannerDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) + .orderByDesc(BannerDO::getSort) + .orderByDesc(BannerDO::getId)); } } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountActivityMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountActivityMapper.java index efd4e4d..d53c08e 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountActivityMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountActivityMapper.java @@ -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 .orderByDesc(DiscountActivityDO::getCreateTime)); } + /** + * App 首页:进行中的限时折扣活动(开启且当前时间在活动期内),与砍价首页时间条件一致 + */ + default List selectAppHomeList(int limit) { + LocalDateTime now = LocalDateTime.now(); + return selectList(new LambdaQueryWrapperX() + .eq(DiscountActivityDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) + .le(DiscountActivityDO::getStartTime, now) + .ge(DiscountActivityDO::getEndTime, now) + .orderByDesc(DiscountActivityDO::getId) + .last("LIMIT " + limit)); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountProductMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountProductMapper.java index 54b440e..44ba800 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountProductMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/discount/DiscountProductMapper.java @@ -44,4 +44,14 @@ public interface DiscountProductMapper extends BaseMapperX { .gt(DiscountProductDO::getActivityEndTime, now)); } + /** + * App 首页:单个活动下展示用商品(按 id 顺序取前 limit 条) + */ + default List selectListByActivityIdWithLimit(Long activityId, int limit) { + return selectList(new LambdaQueryWrapperX() + .eq(DiscountProductDO::getActivityId, activityId) + .orderByAsc(DiscountProductDO::getId) + .last("LIMIT " + limit)); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/ranking/RankingMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/ranking/RankingMapper.java new file mode 100644 index 0000000..0b35e2c --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/ranking/RankingMapper.java @@ -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 { + + default PageResult selectPage(RankingPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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 selectAppEnabledList(int limit) { + return selectList(new LambdaQueryWrapperX() + .eq(RankingDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) + .orderByDesc(RankingDO::getSort) + .orderByDesc(RankingDO::getId) + .last("LIMIT " + limit)); + } + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/ranking/RankingSpuMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/ranking/RankingSpuMapper.java new file mode 100644 index 0000000..621238f --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/ranking/RankingSpuMapper.java @@ -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 { + + default List selectListByRankingId(Long rankingId) { + return selectList(new LambdaQueryWrapperX() + .eq(RankingSpuDO::getRankingId, rankingId) + .orderByDesc(RankingSpuDO::getSort) + .orderByDesc(RankingSpuDO::getId)); + } + + default List selectListByRankingIds(Collection rankingIds) { + return selectList(new LambdaQueryWrapperX() + .in(RankingSpuDO::getRankingId, rankingIds) + .orderByDesc(RankingSpuDO::getSort) + .orderByDesc(RankingSpuDO::getId)); + } + + default void deleteByRankingId(Long rankingId) { + delete(RankingSpuDO::getRankingId, rankingId); + } + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/banner/BannerServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/banner/BannerServiceImpl.java index 46c22f0..f61f4a0 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/banner/BannerServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/banner/BannerServiceImpl.java @@ -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), ""); + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityService.java index 4d6f830..9e86e71 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityService.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityService.java @@ -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 getDiscountProductsByActivityId(Collection activityIds); + /** + * App 首页:进行中的限时折扣活动及每个活动下若干商品(仅上架 SPU) + * + * @param activityCount 活动数量上限 + * @param productCount 每个活动内商品数量上限 + */ + List getAppHomeDiscountActivityList(Integer activityCount, Integer productCount); + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java index 713c030..9963e5f 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java @@ -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 getAppHomeDiscountActivityList(Integer activityCount, Integer productCount) { + int ac = activityCount == null || activityCount < 1 ? 3 : activityCount; + int pc = productCount == null || productCount < 1 ? 6 : productCount; + List activities = discountActivityMapper.selectAppHomeList(ac); + if (CollUtil.isEmpty(activities)) { + return Collections.emptyList(); + } + List 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 buildAppHomeProducts(Long activityId, int productCount) { + int fetchLimit = Math.min(200, Math.max(productCount * 15, productCount)); + List rawList = discountProductMapper.selectListByActivityIdWithLimit(activityId, fetchLimit); + if (CollUtil.isEmpty(rawList)) { + return Collections.emptyList(); + } + List skus = productSkuApi.getSkuList(convertList(rawList, DiscountProductDO::getSkuId)); + Map skuMap = convertMap(skus, ProductSkuRespDTO::getId); + List spus = productSpuApi.getSpuList(convertList(rawList, DiscountProductDO::getSpuId)); + Map spuMap = convertMap(spus, ProductSpuRespDTO::getId); + + List 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; + } + } diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/ranking/RankingService.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/ranking/RankingService.java new file mode 100644 index 0000000..ba76cb8 --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/ranking/RankingService.java @@ -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 getRankingPage(RankingPageReqVO pageReqVO); + + /** + * App:开启榜单列表(含上架 SPU 摘要),条数由调用方限制 + */ + List getAppRankingList(int count); + + /** + * App:榜单详情;非开启或未找到返回 null + */ + AppRankingRespVO getAppRanking(Long id); + +} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/ranking/RankingServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/ranking/RankingServiceImpl.java new file mode 100644 index 0000000..f3df6aa --- /dev/null +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/ranking/RankingServiceImpl.java @@ -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 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 spuRows = rankingSpuMapper.selectListByRankingId(id); + return buildRespVO(ranking, spuRows); + } + + @Override + public PageResult getRankingPage(RankingPageReqVO pageReqVO) { + PageResult page = rankingMapper.selectPage(pageReqVO); + if (CollUtil.isEmpty(page.getList())) { + return PageResult.empty(page.getTotal()); + } + List allSpus = rankingSpuMapper.selectListByRankingIds(convertList(page.getList(), RankingDO::getId)); + Map> spuMap = CollectionUtils.convertMultiMap(allSpus, RankingSpuDO::getRankingId); + + List 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 spus) { + long distinct = spus.stream().map(RankingBaseVO.SpuItem::getSpuId).distinct().count(); + if (distinct != spus.size()) { + throw exception(RANKING_SPU_DUPLICATE); + } + } + + private static List buildRankingSpus(Long rankingId, List items) { + return BeanUtils.toBean(items, RankingSpuDO.class, row -> row.setRankingId(rankingId)); + } + + private void updateRankingSpus(Long rankingId, List items) { + List newList = buildRankingSpus(rankingId, items); + List oldList = rankingSpuMapper.selectListByRankingId(rankingId); + List> 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 spuRows) { + RankingRespVO vo = BeanUtils.toBean(ranking, RankingRespVO.class); + List 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 getAppRankingList(int count) { + List rankings = rankingMapper.selectAppEnabledList(count); + if (CollUtil.isEmpty(rankings)) { + return Collections.emptyList(); + } + List 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 spuRows = rankingSpuMapper.selectListByRankingId(ranking.getId()); + Map spuMap = Collections.emptyMap(); + if (CollUtil.isNotEmpty(spuRows)) { + List spuList = productSpuApi.getSpuList(convertList(spuRows, RankingSpuDO::getSpuId)); + spuMap = CollectionUtils.convertMap(spuList, ProductSpuRespDTO::getId); + } + List 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; + } + +}