本文目录导读:

我将为您提供一个完整的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优化模块等。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播