From 1fc4d671be7b8f66bcc4836d0483bc5ad571380b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 29 Jul 2026 17:09:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(cms):=20=E6=96=B0=E5=A2=9E=E5=8D=95?= =?UTF-8?q?=E9=A1=B5=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增CmsPage实体,定义单页属性及状态说明 - 添加CmsPageParam用于前端查询参数绑定和筛选 - 实现CmsPageMapper及对应XML,实现单页分页和列表查询 - 开发CmsPageService接口及实现类CmsPageServiceImpl,封装业务逻辑 - 增加CmsPageController,实现单页的增删改查及状态变更接口 - 支持通过path查询已发布的公开单页,用于前端访问 - 实现path唯一性校验和租户隔离,确保数据安全与完整 - 使用swagger注解补充接口文档说明,方便前后端协作 - 采用逻辑删除保护数据安全,完善分页排序及默认值处理 --- .../cms/controller/CmsPageController.java | 127 ++++++++++++++++++ .../com/gxwebsoft/cms/entity/CmsPage.java | 98 ++++++++++++++ .../gxwebsoft/cms/mapper/CmsPageMapper.java | 29 ++++ .../cms/mapper/xml/CmsPageMapper.xml | 49 +++++++ .../com/gxwebsoft/cms/param/CmsPageParam.java | 52 +++++++ .../gxwebsoft/cms/service/CmsPageService.java | 52 +++++++ .../cms/service/impl/CmsPageServiceImpl.java | 107 +++++++++++++++ 7 files changed, 514 insertions(+) create mode 100644 src/main/java/com/gxwebsoft/cms/controller/CmsPageController.java create mode 100644 src/main/java/com/gxwebsoft/cms/entity/CmsPage.java create mode 100644 src/main/java/com/gxwebsoft/cms/mapper/CmsPageMapper.java create mode 100644 src/main/java/com/gxwebsoft/cms/mapper/xml/CmsPageMapper.xml create mode 100644 src/main/java/com/gxwebsoft/cms/param/CmsPageParam.java create mode 100644 src/main/java/com/gxwebsoft/cms/service/CmsPageService.java create mode 100644 src/main/java/com/gxwebsoft/cms/service/impl/CmsPageServiceImpl.java diff --git a/src/main/java/com/gxwebsoft/cms/controller/CmsPageController.java b/src/main/java/com/gxwebsoft/cms/controller/CmsPageController.java new file mode 100644 index 0000000..59ae631 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/controller/CmsPageController.java @@ -0,0 +1,127 @@ +package com.gxwebsoft.cms.controller; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.gxwebsoft.cms.entity.CmsPage; +import com.gxwebsoft.cms.param.CmsPageParam; +import com.gxwebsoft.cms.service.CmsPageService; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.common.core.web.ApiResult; +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; +import java.util.Map; + +/** + * 单页管理控制器 + * + * @author WorkBuddy + * @since 2026-07-29 + */ +@Tag(name = "单页管理") +@RestController +@RequestMapping("/api/cms/cms-page") +public class CmsPageController extends BaseController { + + @Resource + private CmsPageService cmsPageService; + + @Operation(summary = "分页查询单页") + @GetMapping("/page") + public ApiResult> page(CmsPageParam param) { + return success(cmsPageService.pageRel(param)); + } + + @Operation(summary = "查询全部单页") + @GetMapping() + public ApiResult> list(CmsPageParam param) { + return success(cmsPageService.listRel(param)); + } + + @Operation(summary = "根据id查询单页") + @GetMapping("/{id}") + public ApiResult get(@PathVariable("id") Integer id) { + final CmsPage page = cmsPageService.getByIdRel(id); + if (ObjectUtil.isNotEmpty(page)) { + return success(page); + } + return fail("单页ID不存在", null); + } + + @Operation(summary = "根据path查询已发布单页(供公开站)") + @GetMapping("/getByPath/{path}") + public ApiResult getByPath(@PathVariable("path") String path) { + final CmsPage page = cmsPageService.getPublishedByPath(path); + // 公开站约定:data 为空表示页面不存在或未发布 + return success(page); + } + + @Operation(summary = "添加单页") + @PostMapping() + public ApiResult save(@RequestBody CmsPage page) { + User loginUser = getLoginUser(); + if (loginUser != null) { + page.setTenantId(loginUser.getTenantId()); + } + page.setTitle(StrUtil.trim(page.getTitle())); + if (StrUtil.isBlank(page.getPath())) { + return fail("访问路径(path)不能为空"); + } + if (cmsPageService.saveRel(page)) { + return success("添加成功"); + } + // 区分是 path 重复还是其他失败 + if (cmsPageService.pathExists(page.getPath(), null)) { + return fail("该访问路径(path)已被占用,请更换"); + } + return fail("添加失败"); + } + + @Operation(summary = "修改单页") + @PutMapping() + public ApiResult update(@RequestBody CmsPage page) { + if (page.getPageId() == null) { + return fail("缺少 pageId"); + } + if (StrUtil.isNotBlank(page.getPath()) && cmsPageService.pathExists(page.getPath(), page.getPageId())) { + return fail("该访问路径(path)已被占用,请更换"); + } + if (cmsPageService.updateByIdRel(page)) { + return success("修改成功"); + } + return fail("修改失败"); + } + + @Operation(summary = "删除单页") + @DeleteMapping("/{id}") + public ApiResult remove(@PathVariable("id") Integer id) { + if (cmsPageService.removeById(id)) { + return success("删除成功"); + } + return fail("删除失败"); + } + + @Operation(summary = "修改单页状态(草稿/发布/下线)") + @PutMapping("/status") + public ApiResult updateStatus(@RequestBody Map body) { + Object idObj = body.get("pageId"); + Object statusObj = body.get("status"); + if (idObj == null) { + return fail("缺少 pageId"); + } + CmsPage entity = new CmsPage(); + entity.setPageId(((Number) idObj).intValue()); + if (statusObj != null) { + entity.setStatus(((Number) statusObj).intValue()); + } + if (cmsPageService.updateById(entity)) { + return success("修改成功"); + } + return fail("修改失败"); + } +} diff --git a/src/main/java/com/gxwebsoft/cms/entity/CmsPage.java b/src/main/java/com/gxwebsoft/cms/entity/CmsPage.java new file mode 100644 index 0000000..2c6f1b5 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/entity/CmsPage.java @@ -0,0 +1,98 @@ +package com.gxwebsoft.cms.entity; + +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 io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 单页(独立页面,如关于我们/联系方式/隐私政策/服务条款等) + * 与文章不同:单页以固定 path(slug)访问,内容单独成篇,无栏目/分类概念。 + * + * 状态约定(统一为后台前端习惯,避免与文章状态语义混淆): + * 0 草稿 / 1 已发布 / 2 已下线 + * + * @author WorkBuddy + * @since 2026-07-29 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("cms_page") +@Schema(name = "CmsPage对象", description = "单页") +public class CmsPage implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "单页ID") + @TableId(value = "page_id", type = IdType.AUTO) + private Integer pageId; + + @Schema(description = "页面标题") + private String title; + + @Schema(description = "访问路径(slug),租户内唯一,如 about / contact / privacy") + private String path; + + @Schema(description = "正文(富文本 HTML)") + private String content; + + @Schema(description = "SEO 关键词") + private String keywords; + + @Schema(description = "SEO 描述") + private String description; + + @Schema(description = "封面/头图") + private String image; + + @Schema(description = "模板标识(可选,留空则用站点当前模板)") + private String template; + + @Schema(description = "排序(数字越小越靠前)") + private Integer sortNumber; + + @Schema(description = "状态 0草稿 1已发布 2已下线") + private Integer status; + + @Schema(description = "备注") + private String comments; + + @Schema(description = "阅读量") + private Integer views; + + @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 String statusText; + + public String getStatusText() { + if (this.status == null) return ""; + switch (this.status) { + case 0: return "草稿"; + case 1: return "已发布"; + case 2: return "已下线"; + default: return ""; + } + } +} diff --git a/src/main/java/com/gxwebsoft/cms/mapper/CmsPageMapper.java b/src/main/java/com/gxwebsoft/cms/mapper/CmsPageMapper.java new file mode 100644 index 0000000..0735171 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/mapper/CmsPageMapper.java @@ -0,0 +1,29 @@ +package com.gxwebsoft.cms.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.cms.entity.CmsPage; +import com.gxwebsoft.cms.param.CmsPageParam; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 单页Mapper + * + * @author WorkBuddy + * @since 2026-07-29 + */ +public interface CmsPageMapper extends BaseMapper { + + /** + * 分页关联查询 + */ + List selectPageRel(@Param("page") IPage page, + @Param("param") CmsPageParam param); + + /** + * 查询全部 + */ + List selectListRel(@Param("param") CmsPageParam param); +} diff --git a/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsPageMapper.xml b/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsPageMapper.xml new file mode 100644 index 0000000..9fa5349 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/mapper/xml/CmsPageMapper.xml @@ -0,0 +1,49 @@ + + + + + + + SELECT a.* + FROM cms_page a + + + AND a.page_id = #{param.pageId} + + + AND a.title LIKE CONCAT('%', #{param.title}, '%') + + + AND a.path = #{param.path} + + + AND a.template = #{param.template} + + + AND a.status = #{param.status} + + + AND a.deleted = #{param.deleted} + + + AND a.deleted = 0 + + + AND (a.path LIKE CONCAT('%', #{param.keywords}, '%') + OR a.title LIKE CONCAT('%', #{param.keywords}, '%') + ) + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/cms/param/CmsPageParam.java b/src/main/java/com/gxwebsoft/cms/param/CmsPageParam.java new file mode 100644 index 0000000..930e88d --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/param/CmsPageParam.java @@ -0,0 +1,52 @@ +package com.gxwebsoft.cms.param; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.gxwebsoft.common.core.annotation.QueryField; +import com.gxwebsoft.common.core.annotation.QueryType; +import com.gxwebsoft.common.core.web.BaseParam; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 单页查询参数 + * + * @author WorkBuddy + * @since 2026-07-29 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(name = "CmsPageParam对象", description = "单页查询参数") +public class CmsPageParam extends BaseParam { + private static final long serialVersionUID = 1L; + + @Schema(description = "单页ID") + @QueryField(type = QueryType.EQ) + private Integer pageId; + + @Schema(description = "页面标题") + @QueryField(type = QueryType.LIKE) + private String title; + + @Schema(description = "访问路径(slug)") + @QueryField(type = QueryType.EQ) + private String path; + + @Schema(description = "模板标识") + @QueryField(type = QueryType.EQ) + private String template; + + @Schema(description = "状态 0草稿 1已发布 2已下线") + @QueryField(type = QueryType.EQ) + private Integer status; + + @Schema(description = "是否删除, 0否, 1是") + @QueryField(type = QueryType.EQ) + private Integer deleted; + + @Schema(description = "单页ID集查询") + @TableField(exist = false) + private java.util.Set pageIds; +} diff --git a/src/main/java/com/gxwebsoft/cms/service/CmsPageService.java b/src/main/java/com/gxwebsoft/cms/service/CmsPageService.java new file mode 100644 index 0000000..c24c020 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/service/CmsPageService.java @@ -0,0 +1,52 @@ +package com.gxwebsoft.cms.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.cms.entity.CmsPage; +import com.gxwebsoft.cms.param.CmsPageParam; + +import java.util.List; + +/** + * 单页Service + * + * @author WorkBuddy + * @since 2026-07-29 + */ +public interface CmsPageService extends IService { + + /** + * 分页关联查询 + */ + PageResult pageRel(CmsPageParam param); + + /** + * 关联查询全部 + */ + List listRel(CmsPageParam param); + + /** + * 根据id查询 + */ + CmsPage getByIdRel(Integer pageId); + + /** + * 根据 path 查询已发布单页(供公开站使用,仅返回 status=1) + */ + CmsPage getPublishedByPath(String path); + + /** + * 新增单页(写入租户、校验 path 唯一) + */ + boolean saveRel(CmsPage page); + + /** + * 修改单页(校验 path 唯一,排除自身) + */ + boolean updateByIdRel(CmsPage page); + + /** + * path 是否已被当前租户占用(排除指定 pageId) + */ + boolean pathExists(String path, Integer excludePageId); +} diff --git a/src/main/java/com/gxwebsoft/cms/service/impl/CmsPageServiceImpl.java b/src/main/java/com/gxwebsoft/cms/service/impl/CmsPageServiceImpl.java new file mode 100644 index 0000000..17772ac --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/service/impl/CmsPageServiceImpl.java @@ -0,0 +1,107 @@ +package com.gxwebsoft.cms.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.gxwebsoft.cms.entity.CmsPage; +import com.gxwebsoft.cms.mapper.CmsPageMapper; +import com.gxwebsoft.cms.param.CmsPageParam; +import com.gxwebsoft.cms.service.CmsPageService; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 单页Service实现 + * + * @author WorkBuddy + * @since 2026-07-29 + */ +@Service +public class CmsPageServiceImpl extends com.baomidou.mybatisplus.extension.service.impl.ServiceImpl implements CmsPageService { + + @Override + public PageResult pageRel(CmsPageParam param) { + PageParam page = new PageParam<>(param); + List list = baseMapper.selectPageRel(page, param); + return new PageResult<>(list, page.getTotal()); + } + + @Override + public List listRel(CmsPageParam param) { + List list = baseMapper.selectListRel(param); + PageParam page = new PageParam<>(); + page.setDefaultOrder("sort_number asc,create_time desc"); + return page.sortRecords(list); + } + + @Override + public CmsPage getByIdRel(Integer pageId) { + CmsPageParam param = new CmsPageParam(); + param.setPageId(pageId); + return param.getOne(baseMapper.selectListRel(param)); + } + + @Override + public CmsPage getPublishedByPath(String path) { + if (StrUtil.isBlank(path)) { + return null; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(CmsPage.class) + .eq(CmsPage::getPath, path) + .eq(CmsPage::getStatus, 1) + .last("limit 1"); + return getOne(wrapper, false); + } + + @Override + public boolean pathExists(String path, Integer excludePageId) { + if (StrUtil.isBlank(path)) { + return false; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(CmsPage.class) + .eq(CmsPage::getPath, path); + if (excludePageId != null) { + wrapper.ne(CmsPage::getPageId, excludePageId); + } + return count(wrapper) > 0; + } + + @Override + public boolean saveRel(CmsPage page) { + if (StrUtil.isBlank(page.getPath())) { + return false; + } + if (pathExists(page.getPath(), null)) { + return false; + } + if (page.getSortNumber() == null) { + page.setSortNumber(0); + } + if (page.getStatus() == null) { + page.setStatus(0); + } + if (page.getViews() == null) { + page.setViews(0); + } + return save(page); + } + + @Override + public boolean updateByIdRel(CmsPage page) { + if (page.getPageId() == null) { + return false; + } + if (StrUtil.isNotBlank(page.getPath()) && pathExists(page.getPath(), page.getPageId())) { + return false; + } + if (page.getSortNumber() == null) { + page.setSortNumber(0); + } + return updateById(page); + } +}