项目使用到的技术与库
1.前端 Vue2 elementUi Cookie WangEditer
2.后端 SpringBoot Mybatis-Plus
3.数据库 MySql
一、效果展示
1.1主页效果:
1.2 文章编辑页面:
1.3 成功发布文章
1.4 文章关键字搜索提示
1.5 文章查询结果展示
1.6 文章内容及交互展示
二、表单设计的sql
用户:
create table paitool.user
(id int auto_incrementprimary key,account varchar(255) not null,password varchar(255) not null,phone varchar(20) null,address varchar(255) null,isVip tinyint(1) default 0 null,email varchar(255) null,registration_date datetime default CURRENT_TIMESTAMP null,last_login datetime null,status enum ('active', 'inactive') default 'active' null,constraint account_UNIQUEunique (account),constraint email_UNIQUEunique (email),constraint phone_UNIQUEunique (phone)
);
文章:
create table paitool.forum_posts
(id int auto_incrementprimary key,title varchar(255) not null,content text not null,author_id int not null,created_at timestamp default CURRENT_TIMESTAMP null,updated_at timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,heat_value int default 0 null,rating decimal(3, 2) default 0.00 null,tag varchar(10) default '其它' null,constraint forum_posts_ibfk_1foreign key (author_id) references paitool.user (id)
);
文章交互表-点赞:
create table paitool.forum_post_likes
(user_id int not null,post_id int not null,primary key (user_id, post_id),constraint forum_post_likes_ibfk_1foreign key (user_id) references paitool.user (id),constraint forum_post_likes_ibfk_2foreign key (post_id) references paitool.forum_posts (id)
);
文章交互表-收藏:
create table paitool.forum_post_favorites
(user_id int not null,post_id int not null,primary key (user_id, post_id),constraint forum_post_favorites_ibfk_1foreign key (user_id) references paitool.user (id),constraint forum_post_favorites_ibfk_2foreign key (post_id) references paitool.forum_posts (id)
);
文章交互表-评论
create table paitool.forum_comments
(id int auto_incrementprimary key,post_id int not null,user_id int not null,comment_text text not null,created_at timestamp default CURRENT_TIMESTAMP null,constraint forum_comments_ibfk_1foreign key (user_id) references paitool.user (id),constraint forum_comments_ibfk_2foreign key (post_id) references paitool.forum_posts (id)
);
三、前端代码
3.1 论坛主页
Html:
<template><div id="forumLayOut"><div id="Top" style="background-color: rgb(250, 250, 250); padding-top: 20px"><div id="serchBorder" style="padding-bottom: 13px;"><!-- 搜索框 --><el-autocomplete v-model="searchKeyWord" :fetch-suggestions="querySearchAsync" :trigger-on-focus="false"placeholder="请输入关键字" style="width: 300px;" @select="handleSelect"></el-autocomplete><el-button type="primary" @click="onSubmit">查询</el-button></div><!-- 分类查询 --><div><div style="margin-bottom: 15px;"><el-checkbox-group v-model="checkboxGroup1" :max="1"><el-checkbox-button v-for="city in cities" :label="city" :key="city">{{ city}}</el-checkbox-button></el-checkbox-group></div></div><div style="height: 380px; width: 100%;"><!-- 轮播图 --><div class="block" style="width: 30%; float: left; margin-left: 5%; height: 400px;"><el-carousel height="350px" style="width: 100%; "><el-carousel-item v-for="item in 4" :key="item"><img src="https://img95.699pic.com/photo/50035/3211.jpg_wh860.jpg" alt="风景测试"><h3 class="small">{{ item }}</h3></el-carousel-item></el-carousel></div><div style=" height: 350px; background-color: rgb(250, 250, 250); width: 25%; float: left;border: 1px solid rgb(240, 240, 242); margin-left: 3%;"><div style="height: 50px; width: 100%; background-color: rgb(245, 245, 245); "><i class="el-icon-share"></i><div><b>热门</b></div><hr></div><div class="link-container"><a href="#" class="link" id="TurnLink">杀死谷歌,成为AI时代的搜索皇帝!</a><p style="color: gray;">Perplexity CEO 最新四万字访谈</p></div><div class="link-container"><a href="#" class="link" id="TurnLink">重写系统后痛批:这门语言烂透了!</a><p style="color: gray;">耗时18个月,开发者弃TypeScript投Rust</p></div><div class="link-container"><a href="#" class="link" id="TurnLink">Shire 编码智能体语言</a><p style="color: gray;">打造你的专属 AI 编程助手</p></div></div><div style="float: left; margin-left: 3%; height: 350px; background-color: rgb(250, 250, 250); width: 25%; float: left;border: 1px solid rgb(240, 240, 242); "><div style="height: 50px; width: 100%; background-color: rgb(245, 245, 245); "><i class="el-icon-message-solid"></i><div><b>头条</b></div><hr></div><div class="link-container"><a href="#" class="link" id="TurnLink">史上开发最久的游戏!</a><p style="color: gray;">耗时 22 年,5 名打工人凑了几百欧就开工,只剩 1 人坚守到发布...</p></div><div class="link-container"><a href="#" class="link" id="TurnLink">实习期间创下 Transformer</a><p style="color: gray;">他说:当年整个 AI 圈都无法预见我们今天的高度</p></div><div class="link-container"><a href="#" class="link" id="TurnLink">杀死谷歌,成为AI时代的搜索皇帝!</a><p style="color: gray;">Perplexity CEO 最新四万字访谈</p></div></div></div></div><el-divider></el-divider><div id="bottom"><!-- Tabs 标签页 --><el-tabs v-model="activeName" @tab-click="handleClick" style="padding-left: 2em; "><el-tab-pane label="我的文章" name="first"><div class="parent-div" style="min-height: 500px"><div v-if="posts.length === 0"><el-empty :image-size="200"></el-empty></div><div class="custom-card" v-for="(post, index) in posts" :key="index"@click="getForumPostDetail(post.id)"><div class="card-content"><h1 class="card-title">标题: {{ post.title }}</h1><div class="card-meta"><span>作者: {{ post.account }}</span><span>标签: {{ post.tag }}</span><span><i class="el-icon-view">{{ post.heatValue }}</i></span></div><div class="card-rating"><span>文章评分:</span><el-rate v-model="post.rating" disabled show-score text-color="#ff9900"score-template="{value}" style="display: inline-block;"></el-rate></div></div></div></div></el-tab-pane><el-tab-pane label="推荐文章" name="second"><div v-if="posts.length === 0"><el-empty :image-size="200"></el-empty></div><div class="custom-card" v-for="(post, index) in posts" :key="index"@click="getForumPostDetail(post.postId)"><div class="card-content"><h1 class="card-title">标题: {{ post.title }}</h1><div class="card-meta"><span>作者: {{ post.account }}</span><span>标签: {{ post.tag }}</span><span><i class="el-icon-view">{{ post.heat_value }}</i></span></div><div class="card-rating"><span>文章评分:</span><el-rate v-model="post.rating" disabled show-score text-color="#ff9900"score-template="{value}" style="display: inline-block;"></el-rate></div></div></div></el-tab-pane><el-tab-pane label="热门文章" name="third"><div class="parent-div" style="min-height: 500px"><div v-if="posts.length === 0"><el-empty :image-size="200"></el-empty></div><div class="custom-card" v-for="(post, index) in posts" :key="index"@click="getForumPostDetail(post.id)"><div class="card-content"><h1 class="card-title">标题: {{ post.title }}</h1><div class="card-meta"><span>作者: {{ post.account }}</span><span>标签: {{ post.tag }}</span><span><i class="el-icon-view">{{ post.heatValue }}</i></span></div><div class="card-rating"><span>文章评分:</span><el-rate v-model="post.rating" disabled show-score text-color="#ff9900"score-template="{value}" style="display: inline-block;"></el-rate></div></div></div></div></el-tab-pane><el-tab-pane label="优质文章" name="fourth"><div class="parent-div" style="min-height: 500px"><div v-if="posts.length === 0"><el-empty :image-size="200"></el-empty></div><div class="custom-card" v-for="(post, index) in posts" :key="index"@click="getForumPostDetail(post.id)"><div class="card-content"><h1 class="card-title">标题: {{ post.title }}</h1><div class="card-meta"><span>作者: {{ post.account }}</span><span>标签: {{ post.tag }}</span><span><i class="el-icon-view">{{ post.heatValue }}</i></span></div><div class="card-rating"><span>文章评分:</span><el-rate v-model="post.rating" disabled show-score text-color="#ff9900"score-template="{value}" style="display: inline-block;"></el-rate></div></div></div></div></el-tab-pane><el-tab-pane label="我的收藏" name="fifth"><div v-if="posts.length === 0"><el-empty :image-size="200"></el-empty></div><div class="parent-div" style="min-height: 500px"><div v-if="posts.length === 0"><el-empty :image-size="200"></el-empty></div><div class="custom-card" v-for="(post, index) in posts" :key="index"@click="getForumPostDetail(post.id)"><div class="card-content"><h1 class="card-title">标题: {{ post.title }}</h1><div class="card-meta"><span>作者: {{ post.account }}</span><span>标签: {{ post.tag }}</span><span><i class="el-icon-view">{{ post.heatValue }}</i></span></div><div class="card-rating"><span>文章评分:</span><el-rate v-model="post.rating" disabled show-score text-color="#ff9900"score-template="{value}" style="display: inline-block;"></el-rate></div></div></div></div></el-tab-pane></el-tabs><el-button type="warning" round id="iWantPost" @click="navigateToPostEdit">我要发布文章</el-button></div></div>
</template>
js:
<script>
import axios from 'axios';
import Cookies from 'js-cookie';const cityOptions = ['新闻报道', '科技动态', '生活时尚', '教育学习', '健康养生'];
export default {components: {},data() {return {searchKeyWord: '',suggestions: [], // 添加这个属性checkboxGroup1: [],cities: cityOptions,activeName: 'first',currentPage1: 5,currentPage2: 5,currentPage3: 5,currentPage4: 4,posts: [],}},methods: {onSubmit() {this.$router.push({name: 'ArticalSearchView',params: {searchKeyWord: this.searchKeyWord}})},handleClick(tab) {// 我的文章if (tab.name === 'first') {this.posts = []this.getMyArticle();}// 推荐文章if (tab.name === 'second') {this.posts = [];axios.get('/api/forum/getAllForumPost', {params: {pageSize: 1,pageNumber: 10}}).then((response) => {console.log(response.data.data);this.posts = response.data.data;});}// 热门文章if (tab.name === 'third') {this.posts = [];axios.get('/api/forum/getHotPosts').then((response) => {console.log(response.data.data);this.posts = response.data.data;});}// 优质文章if (tab.name === 'fourth') {this.posts = [];axios.get('/api/forum/getOutStandPosts').then((response) => {console.log(response.data.data);this.posts = response.data.data;});}// 我的收藏if (tab.name === 'fifth') {this.posts = [];const id = Cookies.get("userId");if (id === null) {this.$message({message: '请先登录',type: 'warning'});return;}axios.get('/api/forum/getMyFavorite', {params: {id: id}}).then((response) => {console.log(response.data.data);this.posts = response.data.data;});}},// 处理分页功能handleSizeChange(val) {console.log(`每页 ${val} 条`);},handleCurrentChange(val) {console.log(`当前页: ${val}`);},navigateToPostEdit() {this.$router.push({ name: 'ForumPostEditView' });},// 跳转到文章详情getForumPostDetail(postId) {console.log("getForumPostDetail");console.log(postId);this.$router.push(`/post/${postId}`);},getMyArticle() {this.posts = [];const id = Cookies.get("userId");if (id === null) {this.$message({message: '请先登录',type: 'warning'});return;} else {axios.get('/api/forum/MyArticle', {params: {id: id}}).then((response) => {console.log(response.data.data);this.posts = response.data.data;})}},// 异步获取建议列表querySearchAsync(queryString, cb) {if (queryString.length === 0) {return cb([]); // 当查询字符串为空时,直接返回空数组}axios.get('/api/forum/getLikeSearch', { params: { keyword: queryString } }).then(response => {// 确保从后端返回的数据中提取出正确的数组const results = response.data.data || [];// 调用callback函数,传入搜索结果cb(results);}).catch(error => {console.error('Error fetching search suggestions:', error);cb([]);});},// 处理选择事件handleSelect(item) {this.searchKeyWord = item.value; this.onSubmit();}},mounted() {this.getMyArticle();}}</script>
css:
<style scoped>
#forumLayOut {background-color: white;height: auto;width: 100%;line-height: normal;
}#serchBorder {line-height: normal;
}.el-carousel__item h3 {color: #475669;font-size: 14px;opacity: 0.75;line-height: 150px;margin: 0;
}.el-carousel__item:nth-child(2n) {background-color: #99a9bf;
}.el-carousel__item:nth-child(2n+1) {background-color: #d3dce6;
}#Pagination {align-self: center;/* 居中对齐 */margin-bottom: 1rem;/* 可选,增加底部边距 */margin-top: 10%;
}#iWantPost {position: fixed;/* 设置为固定定位 */bottom: 60px;/* 距离底部的距离,可根据需要调整 */right: 40px;/* 距离右侧的距离,可根据需要调整 */
}.el-tabs__content {overflow: hidden;position: relative;height: auto;
}.custom-card {background-color: #ffffff;border-radius: 4px;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);margin-bottom: 16px;transition: box-shadow 0.3s ease-in-out;
}.custom-card:hover {box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);cursor: pointer;background-color: rgb(245, 245, 245);
}.card-content {padding: 16px;
}.card-title {font-size: 1.2em;margin-bottom: 8px;color: #333;
}.card-meta {display: flex;align-items: center;justify-content: space-between;margin-bottom: 12px;color: #666;
}.card-rating {color: #666;
}.link-container {line-height: normal;float: left;width: 100%;text-align: left;padding-left: 40px;padding-top: 10px;
}.link {text-decoration: none;font-size: large;color: black;
}.link:hover {text-decoration: underline;
}
</style>
3.2 发布文章页面
<template><div style="border: 1px solid #ccc; line-height: normal; height: 100%;"><div><el-form :inline="true" :model="formInline" class="demo-form-inline"><el-form-item label="文章标题"><el-input v-model="formInline.title" placeholder="请输入文章标题" maxlength="20"></el-input></el-form-item><el-form-item label="类别"><el-select v-model="formInline.category" placeholder="请选择文章类别"><el-option label="新闻报道" value="news"></el-option><el-option label="科技动态" value="technology"></el-option><el-option label="生活时尚" value="lifestyle"></el-option><el-option label="教育学习" value="education"></el-option><el-option label="健康养生" value="health"></el-option></el-select></el-form-item><el-form-item><el-button type="primary" @click="onSubmit"v-loading.fullscreen.lock="fullscreenLoading">提交</el-button></el-form-item></el-form></div><Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" :mode="mode" /><Editor style="height: 500px; overflow-y: hidden; height: 100%;" v-model="html" :defaultConfig="editorConfig":mode="mode" @onCreated="onCreated" /></div></template><script>
import Vue from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import axios from 'axios'
import Cookies from 'js-cookie'export default Vue.extend({components: { Editor, Toolbar },data() {return {editor: null,html: ' ',toolbarConfig: {},editorConfig: { placeholder: '请输入内容...' },mode: 'default', // or 'simple'formInline: {title: '',category: ''},fullscreenLoading: false}},methods: {onCreated(editor) {this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错},onSubmit() {this.fullscreenLoading = true;const userId = Cookies.get('userId'); // 获取并转换userIdaxios.post('/api/forum/add', {"title": this.formInline.title,"content": this.editor.getHtml(), "authorId": userId,"tag": this.formInline.category,}).then((response) => {console.log(response.data);this.fullscreenLoading = false;this.$router.push({ name: 'ForumSucessPostView' });}).catch(error => {console.error(error);this.fullscreenLoading = false;});},},mounted() {},beforeDestroy() {const editor = this.editorif (editor == null) returneditor.destroy() // 组件销毁时,及时销毁编辑器},})
</script><style src="@wangeditor/editor/dist/css/style.css"></style>
3.3 文章发布成功页面
3.4 查看文章页面
html:
<template><div style="line-height: normal; background-color: rgb(246, 247, 249); height: auto; min-height: 80%;"><!-- 文章信息 --><div style="padding-top: 10px; width: auto; min-width: 40%;"><!-- 实现文字垂直居中 --><div id="Infor" style="background-color: white;"><h1 style="font-size: 28px; text-align: center">文章标题:{{title}}</h1><span>创作者:{{author}}</span><span style="margin-left: 20px;">创作日期:{{createAt}}</span><span style="margin-left: 20px;"><i class="el-icon-view">{{heatValue}}</i></span></div><el-divider><i class="el-icon-mobile-phone"></i></el-divider><!-- 文章内容展示区 --><div id="contentDisplay"><div v-html="content"style="padding-left: 2em; padding-top: 15px; padding-right: 2em; padding-bottom: 30px;"></div></div></div><el-divider><i class="el-icon-edit"></i></el-divider><!-- 交互按键 --><div id="buttom"><el-button type="warning" round @click="getBackToForum">返回到论坛</el-button><el-button type="warning" icon="el-icon-star-off" circle @click="PostFavorite"></el-button><el-button type="danger" icon="el-icon-thumb" circle @click="PostLike"></el-button></div><!-- 评论区 --><div id="commentListShow"><el-card class="box-card"><div slot="header" class="clearfix"><span>评论详情</span></div><div id="commentInputArea"><el-input type="textarea" placeholder="请您输入友善的评论吧" v-model="textarea" maxlength="300"show-word-limit id="inputFrame" :clearable="clearAble" resize="none"></el-input><div style="margin-top: 10px; padding-bottom: 50px;"><el-button type="primary" @click="SubmitComment">发表评论</el-button><el-button type="primary" @click="CancelComment">取消评论</el-button></div></div><div id="commentList"><div class="comment-card" v-for="comment in comments" :key="comment.id"><div class="comment-head"><h1 class="username">{{ comment.account }}</h1><p class="created-at">发表于:{{ comment.createdAt }}</p></div><el-divider></el-divider><p class="comment-text">{{ comment.commentText }}</p></div></div></el-card></div></div>
</template>
script:
<script>
import axios from 'axios';
import Cookies from 'js-cookie';export default{data() {return {postId: '',title: '',content: '',value1: null,textarea: '',userId:'',clearAble: true,comments:{},author:'',createAt:'',heatValue:'',}},created() {this.postId = this.$route.params.postId;this.fetchPostDetail(this.$route.params.postId);this.userId = Cookies.get('userId');},mounted() {this.readComment();},methods: {// 前端实现路径传参async fetchPostDetail(postId) {try {this.fullscreenLoading = true;const url = `/api/forum/post/${postId}`;// 发起GET请求const response = await axios.get(url);if (response.status === 200) {// 请求成功,处理响应数据const postData = response.data;console.log('文章详情:', postData);// 更新组件状态或执行其他操作this.title = response.data.data.title;this.content = response.data.data.content;this.author = response.data.data.account;this.createAt = response.data.data.createdAt;this.heatValue = response.data.data.heatValue;this.fullscreenLoading = false;} else {console.error('请求失败,状态码:', response.status);}} catch (error) {console.error('请求错误:', error);}},getBackToForum() {this.$router.push({ name: 'forum' });},// 取消评论CancelComment(){this.textarea = '';},// 执行点赞按钮PostLike(){this.isLogin();axios.get('/api/forum/like',{params:{postId : this.postId,userId : this.userId}}).then((response)=>{this.MessageNotify(response);})},// 执行收藏按钮PostFavorite(){this.isLogin();axios.get('/api/forum/favorite',{params:{postId : this.postId,userId : this.userId}}).then((response)=>{this.MessageNotify(response);})},SubmitComment(){this.isLogin();axios.post('/api/forum/writeComment',{postId : this.postId,userId : this.userId,commentText : this.textarea}).then((response)=>{this.MessageNotify(response);this.textarea = '';this.readComment();})console.log("submit");},isLogin(){if(Cookies.get('userId') == null){this.$message.error('请先登录');return;}},// 消息提醒MessageNotify(response){if(response.data.code == 200){this.$message.success(response.data.data);}else{console.log(response.data);this.$message.error(response.data.message);}},readComment(){axios.get('/api/forum/getComment',{params:{postId : this.postId}}).then((response)=>{this.comments = response.data.data;})},}}</script>
css:
3.5 文章搜索页面
html:
<template><div id="layout"><div id="searchFrame"><div id="InputFrame"><el-input type="textarea" placeholder="请输入内容" v-model="textarea" rows="1" resize="none"style="font-size: larger; width: 80%;"></el-input><el-button type="warning" @click="SearchSubmit" icon="el-icon-search">查询</el-button></div></div><div id="excess"><div id="Interate"><i class="el-icon-search"> 搜索结果</i></div></div><div id="SearchContent"><div v-if="posts.length === 0"><el-empty :image-size="200"></el-empty></div><div class="custom-card" v-for="(post, index) in posts" :key="index" @click="getForumPostDetail(post.id)"><div class="card-content"><h1 class="card-title">标题: {{ post.title }}</h1><div class="card-meta"><span>作者: {{ post.account }}</span><span>标签: {{ post.tag }}</span><span><i class="el-icon-view">{{ post.heatValue }}</i></span></div><div class="card-rating"><span>文章评分:</span><el-rate v-model="post.rating" disabled show-score text-color="#ff9900" score-template="{value}"style="display: inline-block;"></el-rate></div></div></div></div></div>
</template>
script:
<script>
import axios from 'axios';export default {data() {return {textarea: '',searchKeyWord: '',posts: [],}},methods: {SearchSubmit() {console.log(this.textarea);axios.get('/api/forum/search', {params: {searchKeyWord: this.textarea}}).then((response) => {if (response.data.code !== 200) {this.$notify({title: '警告',message: '搜索失败',type: 'warning'});}console.log(response.data.data);this.posts = response.data.data;});},Search() {axios.get('/api/forum/search', {params: {searchKeyWord: this.searchKeyWord}}).then((response) => {if (response.data.code !== 200) {this.$notify({title: '警告',message: '搜索失败',type: 'warning'});}console.log(response.data.data);this.posts = response.data.data;});},// 跳转到文章详情getForumPostDetail(postId) {console.log("getForumPostDetail");console.log(postId);this.$router.push(`/post/${postId}`);},},mounted() {this.searchKeyWord = this.$route.params.searchKeyWord;this.textarea = this.searchKeyWord;this.Search();}}
</script>
css:
<style scoped>
#layout {width: 100%;min-height: 90%;background-color: rgb(245, 246, 247);line-height: normal;
}#SearchContent{min-height: 800px;}#searchFrame {height: 70px;width: 100%;background-color: white;box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);position: -webkit-sticky;/* Safari */position: sticky;top: 0;z-index: 1000;line-height: normal;
}#InputFrame {width: 40%;margin: 0 auto;height: 60%;padding-top: 15px;
}#Interate {float: left;margin-top: 20px;margin-left: 20px;
}#excess {height: 61px;width: 80%;background-color: white;margin: 0 auto;margin-top: 25px;border: 1px solid rgb(245, 245, 245);border-radius: 4px;
}.custom-card {background-color: #ffffff;border-radius: 4px;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);margin-bottom: 16px;transition: box-shadow 0.3s ease-in-out;width: 80%;margin: 0 auto;border: 1px solid rgb(245, 245, 245);
}.custom-card:hover {box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);cursor: pointer;background-color: rgb(245, 245, 245);
}.card-content {padding: 16px;
}.card-title {font-size: 1.2em;margin-bottom: 8px;color: #333;
}.card-meta {display: flex;align-items: center;justify-content: space-between;margin-bottom: 12px;color: #666;
}.card-rating {color: #666;
}.card-rating {color: #666;
}
</style>
四、后端代码
4.1项目后端依赖库
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><scope>compile</scope></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.1</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.11</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency><!--swagger--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.2</version></dependency><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.3.15</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-spring-web</artifactId><version>3.0.0</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.76</version></dependency><!-- https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java --><dependency><groupId>com.alibaba</groupId><artifactId>dashscope-sdk-java</artifactId><version>2.8.2</version></dependency><!--okhttp3 依赖--><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>4.9.3</version></dependency><!-- Lombok dependency --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- 验证码模块--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.12.0</version></dependency>
4.2工具类Result类与实体类
public class Result<T> {// 状态码常量public static final int SUCCESS = 200;public static final int ERROR = 500;private int code; // 状态码private String message; // 消息private T data; // 数据// 构造函数,用于创建成功的结果对象private Result(int code, String message, T data) {this.code = code;this.message = message;this.data = data;}// 成功结果的静态方法public static <T> Result<T> success(T data) {return new Result<>(SUCCESS, "Success", data);}// 错误结果的静态方法public static <T> Result<T> error(String message) {return new Result<>(ERROR, message, null);}// 错误结果的静态方法,可以传入自定义的状态码public static <T> Result<T> error(int code, String message) {return new Result<>(code, message, null);}// 获取状态码public int getCode() {return code;}// 设置状态码public void setCode(int code) {this.code = code;}// 获取消息public String getMessage() {return message;}// 设置消息public void setMessage(String message) {this.message = message;}// 获取数据public T getData() {return data;}// 设置数据public void setData(T data) {this.data = data;}// 用于转换为Map类型的方法,方便序列化为JSONpublic Map<String, Object> toMap() {Map<String, Object> map = new HashMap<>();map.put("code", code);map.put("message", message);map.put("data", data);return map;}
}
Entity:
Forumpost:
@TableName(value ="forum_posts")
@Data
public class ForumPosts implements Serializable {/*** */@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** */@TableField(value = "title")private String title;/*** */@TableField(value = "content")private String content;/*** */@TableField(value = "author_id")private Integer authorId;/*** */@TableField(value = "created_at")private LocalDateTime createdAt;/*** */@TableField(value = "updated_at")private LocalDateTime updatedAt;/*** */@TableField(value = "heat_value")private Integer heatValue;/*** */@TableField(value = "rating")private BigDecimal rating;/*** */@TableField(value = "tag")private String tag;@TableField(exist = false)private static final long serialVersionUID = 1L;@Overridepublic boolean equals(Object that) {if (this == that) {return true;}if (that == null) {return false;}if (getClass() != that.getClass()) {return false;}ForumPosts other = (ForumPosts) that;return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))&& (this.getTitle() == null ? other.getTitle() == null : this.getTitle().equals(other.getTitle()))&& (this.getContent() == null ? other.getContent() == null : this.getContent().equals(other.getContent()))&& (this.getAuthorId() == null ? other.getAuthorId() == null : this.getAuthorId().equals(other.getAuthorId()))&& (this.getCreatedAt() == null ? other.getCreatedAt() == null : this.getCreatedAt().equals(other.getCreatedAt()))&& (this.getUpdatedAt() == null ? other.getUpdatedAt() == null : this.getUpdatedAt().equals(other.getUpdatedAt()))&& (this.getHeatValue() == null ? other.getHeatValue() == null : this.getHeatValue().equals(other.getHeatValue()))&& (this.getRating() == null ? other.getRating() == null : this.getRating().equals(other.getRating()))&& (this.getTag() == null ? other.getTag() == null : this.getTag().equals(other.getTag()));}@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + ((getId() == null) ? 0 : getId().hashCode());result = prime * result + ((getTitle() == null) ? 0 : getTitle().hashCode());result = prime * result + ((getContent() == null) ? 0 : getContent().hashCode());result = prime * result + ((getAuthorId() == null) ? 0 : getAuthorId().hashCode());result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode());result = prime * result + ((getUpdatedAt() == null) ? 0 : getUpdatedAt().hashCode());result = prime * result + ((getHeatValue() == null) ? 0 : getHeatValue().hashCode());result = prime * result + ((getRating() == null) ? 0 : getRating().hashCode());result = prime * result + ((getTag() == null) ? 0 : getTag().hashCode());return result;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append(getClass().getSimpleName());sb.append(" [");sb.append("Hash = ").append(hashCode());sb.append(", id=").append(id);sb.append(", title=").append(title);sb.append(", content=").append(content);sb.append(", authorId=").append(authorId);sb.append(", createdAt=").append(createdAt);sb.append(", updatedAt=").append(updatedAt);sb.append(", heatValue=").append(heatValue);sb.append(", rating=").append(rating);sb.append(", tag=").append(tag);sb.append(", serialVersionUID=").append(serialVersionUID);sb.append("]");return sb.toString();}
}
ForumPostLike:
@Data@TableName(value ="forum_post_likes")public class ForumPostLike {private int userId;private int postId;}
ForumPostFavorites:
@Data
@TableName(value ="forum_post_favorites")
public class ForumPostFavorites {private int userId;private int postId;}
ForumComments
@TableName(value ="forum_comments")
@Data
public class ForumComments implements Serializable {/*** */@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** */@TableField(value = "post_id")private Integer postId;/*** */@TableField(value = "user_id")private Integer userId;/*** */@TableField(value = "comment_text")private String commentText;/*** */@TableField(value = "created_at")private LocalDateTime createdAt;@TableField(exist = false)private static final long serialVersionUID = 1L;@Overridepublic boolean equals(Object that) {if (this == that) {return true;}if (that == null) {return false;}if (getClass() != that.getClass()) {return false;}ForumComments other = (ForumComments) that;return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))&& (this.getPostId() == null ? other.getPostId() == null : this.getPostId().equals(other.getPostId()))&& (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId()))&& (this.getCommentText() == null ? other.getCommentText() == null : this.getCommentText().equals(other.getCommentText()))&& (this.getCreatedAt() == null ? other.getCreatedAt() == null : this.getCreatedAt().equals(other.getCreatedAt()));}@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + ((getId() == null) ? 0 : getId().hashCode());result = prime * result + ((getPostId() == null) ? 0 : getPostId().hashCode());result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode());result = prime * result + ((getCommentText() == null) ? 0 : getCommentText().hashCode());result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode());return result;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append(getClass().getSimpleName());sb.append(" [");sb.append("Hash = ").append(hashCode());sb.append(", id=").append(id);sb.append(", postId=").append(postId);sb.append(", userId=").append(userId);sb.append(", commentText=").append(commentText);sb.append(", createdAt=").append(createdAt);sb.append(", serialVersionUID=").append(serialVersionUID);sb.append("]");return sb.toString();}
}
DTO:
@Data
public class CommentDTO {private int userId;private int postId;private String commentText;}
@Data
public class ForumAddPostDTO {@JsonProperty("title")private String title;@JsonProperty("content")private String content;@JsonProperty("authorId")private Integer authorId;@JsonProperty("tag")private String tag;
}
VO:
@Data
public class ArticleVO {private String title;private String content;private String account;private Integer heatValue;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime createdAt;}
@Data
public class CommentVo {private int id;private String commentText;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime createdAt;private String account;
}
@Data
public class LikeSearchVo {private String value;
}
4.3 自定义异常与全局异常
public class BaseException extends RuntimeException{public BaseException(){}public BaseException(String msg){super(msg);}}
public class NotFoundArticleException extends BaseException{public NotFoundArticleException(String msg){super(msg);}}
public class AlreadyLikeException extends BaseException{public AlreadyLikeException(String msg){super(msg);}}
全局异常处理类:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandlerpublic Result exceptionHandler(BaseException ex){log.error("异常信息:{}", ex.getMessage());return Result.error(ex.getMessage());}}
4.4Controller层
1.ForumPostController
@RequestMapping("/forum")
@RestController
@Api(tags = "文章管理")
@Slf4j
public class ForumPostController {@AutowiredForumPostsService forumPostsService;@AutowiredUserService userService;@AutowiredForumCommentsService forumCommentsService;@ApiOperation("新增文章")@PostMapping("/add")public Result addForumPost(@RequestBody ForumAddPostDTO forumAddPostDTO) throws ParseException {ForumPosts forumPosts = new ForumPosts();BeanUtils.copyProperties(forumAddPostDTO, forumPosts);forumPostsService.save(forumPosts);return Result.success("新增成功");}@GetMapping("/getAllForumPost")@ApiOperation("推荐文章查询")public Result getAllForumPost(@RequestParam(value="pageSize", defaultValue = "10") int pageSize,@RequestParam(value="pageNumber", defaultValue = "1") int pageNumber){Page<ForumPosts> page = new Page<>(pageNumber, pageSize);// 创建查询包装器并指定排序规则QueryWrapper<ForumPosts> queryWrapper = new QueryWrapper<>();// 假设你想按照创建时间降序排序queryWrapper.orderByDesc("created_at");Page<ForumPosts> paged = forumPostsService.page(page,queryWrapper);List<ForumPosts> postsList = paged.getRecords();List<ForumPageVO> posts = new ArrayList<>();for (ForumPosts post : postsList) {ForumPageVO vo = new ForumPageVO();// 获取账户信息User user = userService.getById(post.getAuthorId());vo.setAccount(user.getAccount());// 直接从ForumPosts对象复制其他字段vo.setTag(post.getTag());vo.setRating(post.getRating()); // 如果需要字符串形式vo.setTitle(post.getTitle());vo.setPostId(post.getId());vo.setHeat_value(post.getHeatValue());// 添加到列表posts.add(vo);}return Result.success(posts);}// 文章阅读@ApiOperation("读取文章")@GetMapping("post/{id}")public Result<ArticleVO> readArtical(@PathVariable int id){ArticleVO articleVO = forumPostsService.readArticle(id);return Result.success(articleVO);}// 我的文章功能@ApiOperation("我的文章")@GetMapping("/MyArticle")public Result getMyArticle(@Param("id") int id){List<ForumPosts> forumPostsList = forumPostsService.getByAuthorId(id);return Result.success(forumPostsList);}// 热门文章功能@ApiOperation("热门文章")@GetMapping("/getHotPosts")public Result getHotPosts(){QueryWrapper<ForumPosts> queryWrapper = new QueryWrapper();queryWrapper.orderByDesc("heat_value");List<ForumPosts> forumPosts = forumPostsService.list(queryWrapper);return Result.success(forumPosts);}// 热门文章功能@ApiOperation("优质文章")@GetMapping("/getOutStandPosts")public Result getOutStandPosts(){QueryWrapper<ForumPosts> queryWrapper = new QueryWrapper();queryWrapper.orderByDesc("rating");List<ForumPosts> forumPosts = forumPostsService.list(queryWrapper);return Result.success(forumPosts);}// 我的收藏@ApiOperation("我的收藏")@GetMapping("/getMyFavorite")public Result getMyFavorite(@Param("id") int id){List<ForumPosts> forumPostsList = forumPostsService.getMyFavorite(id);return Result.success(forumPostsList);}// 文章查询@ApiOperation("文章查询")@GetMapping("/search")public Result postSearch(@RequestParam("searchKeyWord") String searchText){if (searchText == null){return Result.error("不能输入为空噢");}LambdaQueryWrapper<ForumPosts> lambdaQueryWrapper = new LambdaQueryWrapper<>();// TODO:通过LamdaQueryWrapper 模糊查询// 模糊查询title字段,%searchText%会被自动添加lambdaQueryWrapper.like(ForumPosts::getTitle, searchText);// 假设这里有一个service接口用于操作ForumPosts表List<ForumPosts> postsList = forumPostsService.list(lambdaQueryWrapper);// 根据你的Result类的具体实现,返回查询结果return Result.success(postsList);}/*** 标题模糊查询* @param keyword 关键词* @return 匹配的标题列表*/@GetMapping("/getLikeSearch")@ApiOperation("标题模糊查询")public Result getLikeSearch(@RequestParam String keyword){List<LikeSearchVo> titles = forumPostsService.findTitlesByKeyword(keyword);return Result.success(titles);}// 用户交互// 点赞@ApiOperation("点赞")@GetMapping("/like")public Result PostLike(@RequestParam("postId") int postId,@RequestParam("userId") int userId){String result = forumPostsService.PostLike(postId,userId);return Result.success(result);}// 收藏@ApiOperation("收藏")@GetMapping("/favorite")public Result PostFavorite(@RequestParam("postId") int postId,@RequestParam("userId") int userId){String result = forumPostsService.PostFavorite(postId,userId);return Result.success(result);}// 写评论@ApiOperation("写评论")@PostMapping("/writeComment")public Result WriteComment(@RequestBody CommentDTO commentdto){ForumComments forumComments = new ForumComments();BeanUtils.copyProperties(commentdto,forumComments);forumCommentsService.save(forumComments);return Result.success("评论成功");}// 读评论@ApiOperation("读取评论")@GetMapping("/getComment")public Result<List<CommentVo>> GetComment(@RequestParam("postId") int postId){List<CommentVo> comment = forumCommentsService.getComment(postId);return Result.success(comment);}}
4.5 Service层
public interface ForumPostsService extends IService<ForumPosts> {List<ForumPosts> getByAuthorId(Integer id);String PostLike(int postId, int userId);String PostFavorite(int postId, int userId);ArticleVO readArticle(int id);List<ForumPosts> getMyFavorite(int id);List<LikeSearchVo> findTitlesByKeyword(String keyword);
}
public interface ForumCommentsService extends IService<ForumComments> {List<CommentVo> getComment(int postId);
}
4.6 Mapper层
public interface ForumCommentsMapper extends BaseMapper<ForumComments> {}
@Mapper
public interface ForumPostFavoritesMapper extends BaseMapper<ForumPostFavorites> {
}
@Mapper
public interface ForumPostLikeMapper extends BaseMapper<ForumPostLike> {
}
@Mapper
public interface ForumPostsMapper extends BaseMapper<ForumPosts> {@Select("select * from paitool.user as a,paitool.forum_posts as b where a.id=b.author_id and a.id = #{id}")List<ForumPosts> getByAuthorId(Integer id);}
五、进阶思路
1.通过ElasticSearch优化搜索引擎
2.使用Redis存储热门文章,以减少数据库压力
3.通过若依框架+AI 完善管理系统