feat(cms): 新增多图轮播组及轮播项管理功能

- 新增轮播组及轮播项实体定义,支持组内多图轮播配置
- 实现轮播组包含轮播项的分页、查询、详情接口,支持一并返回组内items
- 支持轮播组的新增、修改(含全量替换items)、删除操作,确保组及items数据一致
- 提供轮播组状态启用/停用修改接口
- 提供轮播项的单独新增、修改、删除接口,支持组内单项维护
- 实现轮播组和轮播项Mapper及Service层逻辑,含批量查询与保存items的功能
- 新增对应数据库表结构及索引,支持多图轮播组及轮播项数据存储
- 接口编写包含Swagger注解,完善API文档描述
This commit is contained in:
2026-07-19 19:13:50 +08:00
parent bff5083b02
commit 8256817ef7
9 changed files with 661 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
package com.gxwebsoft.cms.controller;
import com.gxwebsoft.cms.entity.CmsBannerGroup;
import com.gxwebsoft.cms.entity.CmsBannerItem;
import com.gxwebsoft.cms.mapper.CmsBannerItemMapper;
import com.gxwebsoft.cms.param.CmsBannerGroupParam;
import com.gxwebsoft.cms.service.CmsBannerGroupService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 轮播组管理控制器
*
* @author WorkBuddy
* @since 2026-07-19
*/
@Tag(name = "轮播组管理")
@RestController
@RequestMapping("/api/cms/cms-banner-group")
public class CmsBannerGroupController extends BaseController {
@Resource
private CmsBannerGroupService cmsBannerGroupService;
@Resource
private CmsBannerItemMapper cmsBannerItemMapper;
@Operation(summary = "分页查询轮播组(带组内items)")
@GetMapping("/page")
public ApiResult<PageResult<CmsBannerGroup>> page(CmsBannerGroupParam param) {
return success(cmsBannerGroupService.pageRel(param));
}
@Operation(summary = "查询全部轮播组(带组内items)")
@GetMapping()
public ApiResult<List<CmsBannerGroup>> list(CmsBannerGroupParam param) {
return success(cmsBannerGroupService.listRel(param));
}
@Operation(summary = "根据id查询轮播组(含组内items)")
@GetMapping("/{groupId}")
public ApiResult<CmsBannerGroup> get(@PathVariable("groupId") Integer groupId) {
CmsBannerGroup group = cmsBannerGroupService.getByIdRel(groupId);
if (group == null) {
return fail("轮播组不存在", group);
}
return success(group);
}
@Operation(summary = "添加轮播组(可携带items一次性创建)")
@PostMapping()
public ApiResult<?> save(@RequestBody CmsBannerGroup group) {
if (cmsBannerGroupService.saveGroup(group)) {
return success("添加成功");
}
return fail("添加失败");
}
@Operation(summary = "修改轮播组(可携带items一次性全量替换)")
@PutMapping()
public ApiResult<?> update(@RequestBody CmsBannerGroup group) {
if (group.getGroupId() == null) {
return fail("轮播组ID不能为空");
}
if (cmsBannerGroupService.updateGroup(group)) {
return success("修改成功");
}
return fail("修改失败");
}
@Operation(summary = "删除轮播组(连带删除组内items)")
@DeleteMapping("/{groupId}")
public ApiResult<?> remove(@PathVariable("groupId") Integer groupId) {
if (cmsBannerGroupService.removeGroup(groupId)) {
return success("删除成功");
}
return fail("删除失败");
}
@Operation(summary = "修改轮播组状态(启用/停用)")
@PutMapping("/status")
public ApiResult<?> updateStatus(@RequestBody CmsBannerGroup group) {
CmsBannerGroup entity = new CmsBannerGroup();
entity.setGroupId(group.getGroupId());
entity.setStatus(group.getStatus());
if (cmsBannerGroupService.updateById(entity)) {
return success("修改成功");
}
return fail("修改失败");
}
// ---------------- 单项 CRUD可选保存组时已全量提交items以下接口用于单独维护 ----------------
@Operation(summary = "单独新增轮播项")
@PostMapping("/{groupId}/item")
public ApiResult<?> addItem(@PathVariable("groupId") Integer groupId,
@RequestBody CmsBannerItem item) {
item.setItemId(null);
item.setGroupId(groupId);
if (item.getLinkType() == null) {
item.setLinkType(0);
}
if (cmsBannerGroupService.getById(groupId) == null) {
return fail("轮播组不存在");
}
if (bannerItemInsert(item)) {
return success("添加成功");
}
return fail("添加失败");
}
@Operation(summary = "单独修改轮播项")
@PutMapping("/item")
public ApiResult<?> updateItem(@RequestBody CmsBannerItem item) {
if (item.getItemId() == null) {
return fail("轮播项ID不能为空");
}
if (bannerItemUpdate(item)) {
return success("修改成功");
}
return fail("修改失败");
}
@Operation(summary = "单独删除轮播项")
@DeleteMapping("/item/{itemId}")
public ApiResult<?> removeItem(@PathVariable("itemId") Integer itemId) {
if (bannerItemDelete(itemId)) {
return success("删除成功");
}
return fail("删除失败");
}
// ---------------- 单项CRUD依赖的mapper ----------------
private boolean bannerItemInsert(CmsBannerItem item) {
return cmsBannerItemMapper.insert(item) > 0;
}
private boolean bannerItemUpdate(CmsBannerItem item) {
return cmsBannerItemMapper.updateById(item) > 0;
}
private boolean bannerItemDelete(Integer itemId) {
return cmsBannerItemMapper.deleteById(itemId) > 0;
}
}

View File

@@ -0,0 +1,83 @@
package com.gxwebsoft.cms.entity;
import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 轮播组(多图轮播)
*
* @author WorkBuddy
* @since 2026-07-19
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("cms_banner_group")
@Schema(name = "CmsBannerGroup对象", description = "轮播组(多图轮播)")
public class CmsBannerGroup implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "轮播组ID")
@TableId(value = "group_id", type = IdType.AUTO)
private Integer groupId;
@Schema(description = "组标题(如:首页幻灯片)")
private String title;
@Schema(description = "展示位置标识(home_slider/home_promo/inner_page等)")
private String position;
@Schema(description = "状态: 1启用 0停用")
private Integer status;
@Schema(description = "排序权重(越小越靠前)")
@JsonProperty("sortNum")
@JSONField(name = "sortNum")
private Integer sortNumber;
@Schema(description = "生效开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime;
@Schema(description = "生效结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime;
@Schema(description = "备注")
private String remark;
@Schema(description = "是否删除: 0否 1是")
@TableLogic
private Integer deleted;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
/**
* 组内轮播项(非数据库字段)
* 列表/详情查询时返回
*/
@Schema(description = "组内轮播项")
@TableField(exist = false)
private List<CmsBannerItem> items;
}

View File

@@ -0,0 +1,71 @@
package com.gxwebsoft.cms.entity;
import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 轮播项(组内单张图)
*
* @author WorkBuddy
* @since 2026-07-19
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("cms_banner_item")
@Schema(name = "CmsBannerItem对象", description = "轮播项(组内单张图)")
public class CmsBannerItem implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "轮播项ID")
@TableId(value = "item_id", type = IdType.AUTO)
private Integer itemId;
@Schema(description = "所属轮播组ID")
private Integer groupId;
@Schema(description = "图片地址")
private String image;
@Schema(description = "单图标题(alt/无障碍)")
private String title;
@Schema(description = "跳转类型: 0无 1外链 2文章 3产品")
private Integer linkType;
@Schema(description = "外链地址")
private String linkUrl;
@Schema(description = "跳转目标ID(文章/产品)")
private Integer linkTargetId;
@Schema(description = "组内排序(越小越靠前)")
@JsonProperty("sortNum")
@JSONField(name = "sortNum")
private Integer sortNumber;
@Schema(description = "是否删除: 0否 1是")
@TableLogic
private Integer deleted;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,16 @@
package com.gxwebsoft.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.cms.entity.CmsBannerGroup;
import org.apache.ibatis.annotations.Mapper;
/**
* 轮播组Mapper
*
* @author WorkBuddy
* @since 2026-07-19
*/
@Mapper
public interface CmsBannerGroupMapper extends BaseMapper<CmsBannerGroup> {
}

View File

@@ -0,0 +1,16 @@
package com.gxwebsoft.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.cms.entity.CmsBannerItem;
import org.apache.ibatis.annotations.Mapper;
/**
* 轮播项Mapper
*
* @author WorkBuddy
* @since 2026-07-19
*/
@Mapper
public interface CmsBannerItemMapper extends BaseMapper<CmsBannerItem> {
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.cms.param;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 轮播组查询参数
*
* @author WorkBuddy
* @since 2026-07-19
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "CmsBannerGroupParam对象", description = "轮播组查询参数")
public class CmsBannerGroupParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "轮播组ID")
@QueryField(type = QueryType.EQ)
private Integer groupId;
@Schema(description = "组标题")
@QueryField(type = QueryType.LIKE)
private String title;
@Schema(description = "展示位置标识")
@QueryField(type = QueryType.EQ)
private String position;
@Schema(description = "状态: 1启用 0停用")
@QueryField(type = QueryType.EQ)
private Integer status;
}

View File

@@ -0,0 +1,65 @@
package com.gxwebsoft.cms.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.cms.entity.CmsBannerGroup;
import com.gxwebsoft.cms.param.CmsBannerGroupParam;
import com.gxwebsoft.common.core.web.PageResult;
import java.util.List;
/**
* 轮播组Service接口
*
* @author WorkBuddy
* @since 2026-07-19
*/
public interface CmsBannerGroupService extends IService<CmsBannerGroup> {
/**
* 分页查询(带组内items)
*
* @param param 查询参数
* @return 分页结果
*/
PageResult<CmsBannerGroup> pageRel(CmsBannerGroupParam param);
/**
* 查询全部(带组内items)
*
* @param param 查询参数
* @return 列表
*/
List<CmsBannerGroup> listRel(CmsBannerGroupParam param);
/**
* 根据id查询(带组内items)
*
* @param groupId 轮播组ID
* @return 轮播组
*/
CmsBannerGroup getByIdRel(Integer groupId);
/**
* 保存轮播组(可携带items一次性创建)
*
* @param group 轮播组(含items)
* @return 是否成功
*/
boolean saveGroup(CmsBannerGroup group);
/**
* 更新轮播组(可携带items一次性全量替换)
*
* @param group 轮播组(含items)
* @return 是否成功
*/
boolean updateGroup(CmsBannerGroup group);
/**
* 删除轮播组(连带删除组内items)
*
* @param groupId 轮播组ID
* @return 是否成功
*/
boolean removeGroup(Integer groupId);
}

View File

@@ -0,0 +1,175 @@
package com.gxwebsoft.cms.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.cms.entity.CmsBannerGroup;
import com.gxwebsoft.cms.entity.CmsBannerItem;
import com.gxwebsoft.cms.mapper.CmsBannerGroupMapper;
import com.gxwebsoft.cms.mapper.CmsBannerItemMapper;
import com.gxwebsoft.cms.param.CmsBannerGroupParam;
import com.gxwebsoft.cms.service.CmsBannerGroupService;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 轮播组Service实现
*
* @author WorkBuddy
* @since 2026-07-19
*/
@Service
public class CmsBannerGroupServiceImpl extends ServiceImpl<CmsBannerGroupMapper, CmsBannerGroup> implements CmsBannerGroupService {
@Resource
private CmsBannerItemMapper bannerItemMapper;
@Override
public PageResult<CmsBannerGroup> pageRel(CmsBannerGroupParam param) {
PageParam<CmsBannerGroup, CmsBannerGroupParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
IPage<CmsBannerGroup> result = page(page, page.getOrderWrapper());
List<CmsBannerGroup> records = result.getRecords();
// 批量填充 items
fillItems(records);
return new PageResult<>(records, result.getTotal());
}
@Override
public List<CmsBannerGroup> listRel(CmsBannerGroupParam param) {
PageParam<CmsBannerGroup, CmsBannerGroupParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<CmsBannerGroup> records = list(page.getOrderWrapper());
fillItems(records);
return records;
}
@Override
public CmsBannerGroup getByIdRel(Integer groupId) {
CmsBannerGroup group = getById(groupId);
if (group == null) {
return null;
}
List<CmsBannerItem> items = listItemsByGroupId(groupId);
group.setItems(items);
return group;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveGroup(CmsBannerGroup group) {
// 先保存组
boolean ok = save(group);
if (!ok) {
return false;
}
// 再保存组内items
saveItems(group.getGroupId(), group.getItems());
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateGroup(CmsBannerGroup group) {
// 更新组主信息
boolean ok = updateById(group);
if (!ok) {
return false;
}
Integer groupId = group.getGroupId();
// 全量替换items: 先删除旧items, 再插入新items
if (group.getItems() != null) {
bannerItemMapper.delete(new LambdaQueryWrapper<CmsBannerItem>()
.eq(CmsBannerItem::getGroupId, groupId));
saveItems(groupId, group.getItems());
}
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeGroup(Integer groupId) {
// 逻辑删除组
boolean ok = removeById(groupId);
if (!ok) {
return false;
}
// 逻辑删除组内items
bannerItemMapper.delete(new LambdaQueryWrapper<CmsBannerItem>()
.eq(CmsBannerItem::getGroupId, groupId));
return true;
}
// ----------------- private -----------------
/**
* 查询某个组下的所有轮播项(按sort_number升序)
*/
private List<CmsBannerItem> listItemsByGroupId(Integer groupId) {
if (groupId == null) {
return Collections.emptyList();
}
return bannerItemMapper.selectList(new LambdaQueryWrapper<CmsBannerItem>()
.eq(CmsBannerItem::getGroupId, groupId)
.orderByAsc(CmsBannerItem::getSortNumber));
}
/**
* 批量查询多个组的items并按group分组
*/
private Map<Integer, List<CmsBannerItem>> listItemsByGroupIds(List<Integer> groupIds) {
if (groupIds == null || groupIds.isEmpty()) {
return Collections.emptyMap();
}
List<CmsBannerItem> allItems = bannerItemMapper.selectList(new LambdaQueryWrapper<CmsBannerItem>()
.in(CmsBannerItem::getGroupId, groupIds)
.orderByAsc(CmsBannerItem::getSortNumber));
return allItems.stream().collect(Collectors.groupingBy(CmsBannerItem::getGroupId));
}
/**
* 批量给group列表填充items
*/
private void fillItems(List<CmsBannerGroup> records) {
if (records == null || records.isEmpty()) {
return;
}
List<Integer> groupIds = records.stream()
.map(CmsBannerGroup::getGroupId)
.collect(Collectors.toList());
Map<Integer, List<CmsBannerItem>> itemsMap = listItemsByGroupIds(groupIds);
for (CmsBannerGroup g : records) {
g.setItems(itemsMap.getOrDefault(g.getGroupId(), new ArrayList<>()));
}
}
/**
* 批量保存组内items
*/
private void saveItems(Integer groupId, List<CmsBannerItem> items) {
if (groupId == null || items == null || items.isEmpty()) {
return;
}
for (int i = 0; i < items.size(); i++) {
CmsBannerItem item = items.get(i);
item.setItemId(null); // 新增时清除前端可能传入的ID
item.setGroupId(groupId);
if (item.getSortNumber() == null) {
item.setSortNumber(i);
}
if (item.getLinkType() == null) {
item.setLinkType(0);
}
bannerItemMapper.insert(item);
}
}
}

View File

@@ -0,0 +1,45 @@
-- ----------------------------
-- 轮播组表
-- 一个轮播组对应一组多图轮播(如首页幻灯片)
-- 与 cms_banner(旧版单图) 并存,互不影响
-- ----------------------------
CREATE TABLE IF NOT EXISTS `cms_banner_group` (
`group_id` INT NOT NULL AUTO_INCREMENT COMMENT '轮播组ID',
`title` VARCHAR(120) NOT NULL COMMENT '组标题(如:首页幻灯片)',
`position` VARCHAR(60) NOT NULL DEFAULT 'home_slider' COMMENT '展示位置标识(home_slider/home_promo/inner_page等)',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1启用 0停用',
`sort_number` INT NOT NULL DEFAULT 0 COMMENT '排序权重(越小越靠前)',
`start_time` DATETIME DEFAULT NULL COMMENT '生效开始时间',
`end_time` DATETIME DEFAULT NULL COMMENT '生效结束时间',
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是',
`tenant_id` INT DEFAULT NULL COMMENT '租户ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`group_id`),
KEY `idx_position` (`position`),
KEY `idx_status` (`status`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='轮播组(多图轮播)';
-- ----------------------------
-- 轮播项表
-- 组内单张图片及其跳转配置
-- ----------------------------
CREATE TABLE IF NOT EXISTS `cms_banner_item` (
`item_id` INT NOT NULL AUTO_INCREMENT COMMENT '轮播项ID',
`group_id` INT NOT NULL COMMENT '所属轮播组ID',
`image` VARCHAR(500) NOT NULL COMMENT '图片地址',
`title` VARCHAR(120) DEFAULT NULL COMMENT '单图标题(alt/无障碍)',
`link_type` TINYINT NOT NULL DEFAULT 0 COMMENT '跳转类型: 0无 1外链 2文章 3产品',
`link_url` VARCHAR(500) DEFAULT NULL COMMENT '外链地址',
`link_target_id` INT DEFAULT NULL COMMENT '跳转目标ID(文章/产品)',
`sort_number` INT NOT NULL DEFAULT 0 COMMENT '组内排序(越小越靠前)',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是',
`tenant_id` INT DEFAULT NULL COMMENT '租户ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`item_id`),
KEY `idx_group_id` (`group_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='轮播项(组内单张图)';