本文目录导读:

我将为您提供一个完整的Java内容管理系统(CMS)案例,包含核心功能代码和架构设计。
系统架构设计
技术栈
- 后端: Spring Boot 2.7.x + MyBatis-Plus + Spring Security
- 前端: Vue 3 + Element Plus
- 数据库: MySQL 8.0
- 缓存: Redis
- 构建工具: Maven
项目结构
cms-system/
├── src/main/java/com/example/cms/
│ ├── config/ # 配置类
│ ├── controller/ # 控制器
│ ├── service/ # 业务逻辑
│ ├── mapper/ # 数据访问
│ ├── entity/ # 实体类
│ ├── dto/ # 数据传输对象
│ ├── common/ # 通用工具类
│ └── security/ # 安全配置
├── src/main/resources/
│ ├── mapper/ # MyBatis映射文件
│ ├── static/ # 静态资源
│ └── application.yml # 配置文件
└── pom.xml
核心代码实现
实体类设计
// Article.java - 文章实体
package com.example.cms.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
@TableName("cms_article")
public class Article {
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private String summary;
@TableField("content")
private String content;
private Long categoryId;
private Long authorId;
private String coverImage;
@TableField("status")
private Integer status; // 0-草稿 1-已发布 2-已下线
@TableField("view_count")
private Integer viewCount;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField("deleted")
private Integer deleted;
// 非数据库字段,用于关联查询
@TableField(exist = false)
private Category category;
@TableField(exist = false)
private List<Tag> tags;
}
// Category.java - 栏目实体
package com.example.cms.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
@TableName("cms_category")
public class Category {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableField("slug")
private String slug;
private Long parentId;
private Integer sort;
@TableField("status")
private Integer status;
private LocalDateTime createTime;
@TableField(exist = false)
private List<Category> children;
}
文章服务层
// ArticleService.java
package com.example.cms.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.cms.dto.ArticleDTO;
import com.example.cms.entity.Article;
import com.example.cms.mapper.ArticleMapper;
import com.example.cms.vo.ArticleVO;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ArticleService extends ServiceImpl<ArticleMapper, Article> {
// 分页查询文章列表
public IPage<ArticleVO> getArticlePage(Integer pageNum, Integer pageSize, Integer status, Long categoryId) {
Page<Article> page = new Page<>(pageNum, pageSize);
Page<ArticleVO> result = new Page<>(pageNum, pageSize);
// 使用自定义SQL查询,关联分类表
List<ArticleVO> articleVOs = baseMapper.selectArticleListWithCategory(
page, status, categoryId
);
BeanUtils.copyProperties(articleVOs, result.getRecords());
result.setTotal(page.getTotal());
result.setPages(page.getPages());
return result;
}
// 发布文章
@Transactional
public Long publishArticle(ArticleDTO dto) {
Article article = new Article();
BeanUtils.copyProperties(dto, article);
article.setStatus(1);
article.setViewCount(0);
// 保存文章
save(article);
// 保存文章标签关联
if (dto.getTagIds() != null && !dto.getTagIds().isEmpty()) {
baseMapper.insertArticleTags(article.getId(), dto.getTagIds());
}
// 更新文章数量统计
updateCategoryArticleCount(dto.getCategoryId(), 1);
return article.getId();
}
// 更新文章
@Transactional
public boolean updateArticle(Long id, ArticleDTO dto) {
Article article = getById(id);
if (article == null) {
return false;
}
// 如果分类改变,更新分类文章数量
if (!article.getCategoryId().equals(dto.getCategoryId())) {
updateCategoryArticleCount(article.getCategoryId(), -1);
updateCategoryArticleCount(dto.getCategoryId(), 1);
}
BeanUtils.copyProperties(dto, article);
return updateById(article);
}
// 删除文章(逻辑删除)
@Transactional
public boolean deleteArticle(Long id) {
Article article = getById(id);
if (article == null) {
return false;
}
// 逻辑删除
removeById(id);
// 更新分类文章数量
updateCategoryArticleCount(article.getCategoryId(), -1);
return true;
}
// 增加浏览次数
@Cacheable(value = "articleViewCount", key = "#articleId")
public void incrementViewCount(Long articleId) {
baseMapper.incrementViewCount(articleId);
}
// 获取热门文章
public List<ArticleVO> getHotArticles(Integer limit) {
return baseMapper.selectHotArticles(limit);
}
private void updateCategoryArticleCount(Long categoryId, int count) {
baseMapper.updateCategoryArticleCount(categoryId, count);
}
}
安全的API接口控制
// ArticleController.java
package com.example.cms.controller;
import com.example.cms.common.Result;
import com.example.cms.dto.ArticleDTO;
import com.example.cms.service.ArticleService;
import com.example.cms.vo.ArticleVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/articles")
public class ArticleController {
@Autowired
private ArticleService articleService;
// 文章列表(公开接口)
@GetMapping
public Result list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long categoryId) {
return Result.success(articleService.getArticlePage(page, size, status, categoryId));
}
// 文章详情(公开接口)
@GetMapping("/{id}")
public Result detail(@PathVariable Long id) {
// 增加浏览量
articleService.incrementViewCount(id);
return Result.success(articleService.getById(id));
}
// 发布文章(需要管理员权限)
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public Result create(@Valid @RequestBody ArticleDTO dto) {
Long articleId = articleService.publishArticle(dto);
return Result.success("文章发布成功", articleId);
}
// 编辑文章(需要管理员权限)
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public Result update(@PathVariable Long id, @Valid @RequestBody ArticleDTO dto) {
boolean success = articleService.updateArticle(id, dto);
return success ? Result.success("文章更新成功") : Result.error("文章不存在");
}
// 删除文章(需要管理员权限)
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public Result delete(@PathVariable Long id) {
boolean success = articleService.deleteArticle(id);
return success ? Result.success("文章删除成功") : Result.error("文章不存在");
}
// 获取热门文章(公开接口)
@GetMapping("/hot")
public Result hotArticles() {
return Result.success(articleService.getHotArticles(10));
}
}
数据库设计
-- 文章表 CREATE TABLE `cms_article` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '文章ID', varchar(200) NOT NULL COMMENT '文章标题', `summary` varchar(500) DEFAULT NULL COMMENT '#39;, `content` longtext COMMENT '文章内容', `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `author_id` bigint(20) DEFAULT NULL COMMENT '作者ID', `cover_image` varchar(500) DEFAULT NULL COMMENT '封面图', `status` tinyint(4) DEFAULT '0' COMMENT '状态:0-草稿 1-已发布 2-已下线', `view_count` int(11) DEFAULT '0' COMMENT '浏览次数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除:0-正常 1-已删除', PRIMARY KEY (`id`), KEY `idx_category_id` (`category_id`), KEY `idx_status_create_time` (`status`, `create_time`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='文章表'; -- 分类表 CREATE TABLE `cms_category` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID', `name` varchar(100) NOT NULL COMMENT '分类名称', `slug` varchar(100) DEFAULT NULL COMMENT '简称', `parent_id` bigint(20) DEFAULT '0' COMMENT '父分类ID', `sort` int(11) DEFAULT '0' COMMENT '排序', `status` tinyint(4) DEFAULT '1' COMMENT '状态:0-禁用 1-启用', `article_count` int(11) DEFAULT '0' COMMENT '文章数量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_slug` (`slug`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表'; -- 标签表 CREATE TABLE `cms_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '标签ID', `name` varchar(100) NOT NULL COMMENT '标签名称', `article_count` int(11) DEFAULT '0' COMMENT '文章数量', PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表'; -- 文章标签关联表 CREATE TABLE `cms_article_tag` ( `article_id` bigint(20) NOT NULL, `tag_id` bigint(20) NOT NULL, PRIMARY KEY (`article_id`, `tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联表';
配置文件
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/cms_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
password:
database: 0
cache:
type: redis
redis:
time-to-live: 60000
cache-null-values: false
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.example.cms.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
server:
port: 8080
安全配置
// SecurityConfig.java
package com.example.cms.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors().and()
.authorizeRequests()
.antMatchers("/api/auth/**", "/public/**").permitAll()
.antMatchers("/api/articles").permitAll() // 公开接口
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated().and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
前端Vue组件示例
<!-- AdminArticleList.vue -->
<template>
<div class="article-management">
<el-card>
<div class="toolbar">
<el-input
v-model="searchParams.title"
placeholder="搜索文章标题"
style="width: 200px; margin-right: 10px;"
clearable
/>
<el-select v-model="searchParams.status" placeholder="状态筛选" clearable>
<el-option label="草稿" :value="0" />
<el-option label="已发布" :value="1" />
<el-option label="已下线" :value="2" />
</el-select>
<el-button type="primary" @click="loadData">搜索</el-button>
<el-button type="success" @click="createArticle">新建文章</el-button>
</div>
<el-table :data="articleList" stripe border>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="标题" min-width="200" />
<el-table-column prop="category.name" label="分类" width="100" />
<el-table-column prop="viewCount" label="浏览量" width="100" />
<el-table-column label="状态" width="80">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button size="small" @click="editArticle(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteArticle(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.size"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@change="loadData"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
const articleList = ref([])
const total = ref(0)
const searchParams = reactive({ '',
status: null,
page: 1,
size: 10
})
const getStatusType = (status) => {
return status === 1 ? 'success' : status === 0 ? 'info' : 'warning'
}
const getStatusLabel = (status) => {
return status === 1 ? '已发布' : status === 0 ? '草稿' : '已下线'
}
const loadData = async () => {
try {
const response = await axios.get('/api/articles', { params: searchParams })
articleList.value = response.data.data.records
total.value = response.data.data.total
} catch (error) {
ElMessage.error('加载文章列表失败')
}
}
const createArticle = () => {
// 跳转到编辑页面
router.push('/admin/articles/create')
}
const editArticle = (row) => {
router.push(`/admin/articles/edit/${row.id}`)
}
const deleteArticle = async (row) => {
try {
await ElMessageBox.confirm(`确定删除文章"${row.title}"?`, '提示')
await axios.delete(`/api/articles/${row.id}`)
ElMessage.success('文章删除成功')
loadData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.article-management {
padding: 20px;
}
.toolbar {
margin-bottom: 20px;
display: flex;
align-items: center;
}
</style>
核心功能亮点
多级分类管理
- 支持无限级分类
- 分类排序和权限控制
- 分类文章数量自动统计
完整的工作流
- 草稿、审核、发布、下线流程
- 定时发布功能
- 版本管理
性能优化
- Redis缓存热门文章
- 数据库索引优化
- 分页查询优化
安全机制
- Spring Security + JWT认证
- 角色权限控制(管理员、编辑、作者)
- 防SQL注入和XSS攻击
统计分析
- 文章浏览量统计
- 分类点击排行
- 用户行为分析
运行与部署
本地运行
# 克隆项目 git clone https://github.com/your-project/cms-system.git # 进入项目目录 cd cms-system # 安装依赖 mvn clean install # 运行项目 mvn spring-boot:run
Docker部署
# Dockerfile FROM openjdk:8-jre-alpine ADD target/cms-system.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app.jar"]
# 构建镜像 docker build -t cms-system . # 运行容器 docker run -d -p 8080:8080 \ -e DB_HOST=database \ -e REDIS_HOST=redis \ --link mysql:database \ --link redis:redis \ cms-system
初始化数据
-- 初始化管理员账号
INSERT INTO sys_user (username, password, role)
VALUES ('admin', '$2a$10$...加密后的密码', 'ADMIN');
-- 初始化示例分类
INSERT INTO cms_category (name, slug, parent_id, sort, status) VALUES
('技术', 'technology', 0, 1, 1),
('产品', 'product', 0, 2, 1),
('设计', 'design', 0, 3, 1);
这个案例包含了CMS系统的主要功能,您可以根据实际需求进行扩展,比如增加评论系统、用户系统、SEO优化模块等。