Springboot考研信息平台
1、技术栈
前端
- Vue 是一套用于构建用户界面的渐进式 JavaScript 框架。 Vue 作为前端核心框架,提供了响应式的数据绑定和高效的组件化开发模式。通过 Vue,开发人员可以轻松地创建动态的、交互式的用户界面,使平台能够实时响应用户的操作,如信息查询、表单提交等,为用户提供健康、流畅的操作体验。
- Axios 是一个基于 Promise 的 HTTP 库,可以用于浏览器和 node.js。在该平台中,Axios 主要负责与后端进行数据交互。它能够以简洁的代码实现对后端接口的请求,无论是获取考研资讯、用户信息,还是用户的提交注册、登录等操作,都能高效地完成数据的传输。同时,Axios 还支持请求和响应的拦截,方便进行请求的全局配置和响应的统一处理,如添加请求头信息、处理请求错误等,提高了前端与后端通信的可靠性和灵活性。
- Element Plus 是一套基于 Vue 3 的桌面端组件库。在考研信息平台中,Element Plus UI 为平台提供了丰富多样的 UI 组件,如布局组件(Grid 布局)、表格组件、表单组件(输入框、下拉菜单等)、按钮组件、弹窗组件等。这些组件具有统一的视觉风格和交互规范,能够快速搭建出美观、专业的用户界面。例如,利用 Element Plus 的表格组件可以方便地展示考研院校信息、专业信息等列表数据;借助弹窗组件可以实现用户注册、登录等信息输入的交互功能,大大提高了前端开发的效率和界面的一致性。
- Apache ECharts 是一个商业级的数据可视化工具,以简洁直观的图表展示数据。在考研信息平台里,Apache ECharts 用于将复杂的考研数据以图表形式呈现给用户。比如,可以展示历年考研报名人数的趋势图、不同专业考研竞争比例的饼图、各院校录取分数线的柱状图等。通过这些可视化图表,用户能够更快速、清晰地了解考研相关的数据情况,辅助他们做出更合理的考研决策。
后端
- Springboot 是一个用于快速开发基于 Spring 的 Java 应用程序的框架,它简化了 Spring 应用的初始搭建以及开发过程。在考研信息平台的后端,Springboot 作为核心框架,提供了强大的依赖管理和自动配置功能。通过 Springboot,可以快速地搭建起后端的项目架构,整合各种技术组件,如 Mybatis、MySQL 等。它使得后端开发更加高效、简洁,开发人员可以专注于业务逻辑的实现,如用户管理、考研信息管理、数据统计等功能的开发,而无需过多关注复杂的配置细节。
- MySQL 是一个关系型数据库管理系统,在考研信息平台中用于存储各类数据。这些数据包括用户信息(如用户名、密码、浏览历史等)、考研院校和专业信息(院校名称、专业代码、招生简章等)、考研资讯(政策动态、考试安排等)、以及用户与平台的交互记录(如查询记录、收藏信息等)。MySQL 提供了可靠的数据存储和查询功能,通过 SQL 语言可以高效地对数据进行增删改查操作,确保平台数据的完整性和安全性,为平台的稳定运行提供数据支撑。
- Mybatis 是一个优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。在该平台后端,Mybatis 用于实现 Java 应用程序与 MySQL 数据库之间的交互。它通过映射器(mapper)将 Java 接口和 XML 中的 SQL 映射,使得开发人员可以使用面向对象的方式进行数据库操作。例如,通过定义相应的 mapper 接口和 XML 映射文件,可以方便地实现对用户表、考研信息表等的 CRUD(增删改查)操作。Mybatis 提供了足够的灵活性,能够处理复杂的业务场景,同时避免了繁琐的 JDBC 编程,提高了后端开发的效率。
- 阿里云 OSS(Object Storage Service)是一种海量、安全、低成本、高可靠的云存储服务。在考研信息平台中,阿里 OSS云 用于存储一些文件类型的资源,如考研院校的招生简章文件、考研辅导资料、用户上传的个人资料等。通过使用阿里云 OSS,平台可以方便地管理和分发这些文件资源,用户可以快速地访问和下载所需的文件。同时,阿里云 提 OSS供了强大的存储容量和高可用性,确保文件的安全存储和稳定访问,为平台的文件存储需求提供了可靠的解决方案。
2、项目说明
- 项目采用前后端分离的架构模式,这种架构模式具有诸多显著的优势。
- 开发效率提升 :前端开发人员和后端开发人员可以相对独立地进行开发工作,无需相互等待。前端人员可以专注于使用 Vue 等前端技术构建用户界面和交互逻辑,后端人员则专注于利用 Springboot 等后端技术实现业务逻辑和数据处理功能,从而提高了整体开发效率。
- 技术选型灵活 :前后端分离使得前端和后端可以各自选择最适合的技术栈。前端可以根据项目的 UI 设计和用户体验需求选择 Vue 框架配合 Element Plus UI 等组件库,后端则可以根据业务复杂度、性能要求等因素选择 Springboot 框架结合 Mybatis 等技术,这种灵活性有利于充分发挥各技术的优势,满足项目不同的需求。
- 可维护性和可扩展性增强 :前后端代码分离,各自形成了独立的模块体系,便于代码的管理和维护。当需要对前端界面进行优化或者对后端业务逻辑进行调整时,可以只针对相应的部分进行修改,而不会对另一部分产生过多的影响。同时,这种分离架构也为项目的后续扩展提供了便利,例如,未来可以方便地添加新的前端功能模块或者后端服务模块,以满足考研信息平台不断发展的业务需求。
- 在项目运行时,前端和后端通过定义好的 API 接口进行通信。前端通过 Axios 向端后发送 HTTP 请求,包括获取考研信息、用户注册登录等操作的请求;后端接收到请求后,利用 Springboot 进行业务逻辑处理,通过 Mybatis 操作 MySQL 数据库获取或存储数据,再将处理结果以 JSON 等格式返回给前端,前端再根据返回的数据更新页面显示,实现了前后端的紧密协作,为用户提供高质量的考研信息服务。
3、项目截图
登录注册
平台提供了简洁、便捷的登录注册界面。登录界面包含用户名和密码输入框,以及登录按钮。用户可以输入自己的账号信息进行登录,新用户则可以通过注册界面进行账号注册。注册界面要求填写用户名、密码、邮箱等基本信息,并设置有验证码功能,以确保注册账号的安全性。同时,登录注册界面还设计有忘记密码的找回功能,方便用户在忘记密码时能够快速找回账号登录。
管理员端
管理员登录后进入的管理端界面功能丰富。主要包含考研信息管理模块,可以添加、编辑、删除考研院校和专业的信息,如更新院校的招生简章、专业目录等;还有用户管理模块,能够查看和管理用户的基本信息,处理用户反馈和投诉;此外,还包括数据分析模块,通过图表展示考研相关的数据统计,如用户浏览量、注册用户数量变化趋势等,帮助管理员了解平台的运营情况,以便更好地进行决策和管理。
学生端
学生端界面以提供考研信息查询和学习服务为主。界面包含考研院校和专业查询功能,学生可以通过输入关键词或者筛选条件(如地区、学科类别等)快速查询到自己感兴趣的院校和专业信息。同时,学生端还提供考研资讯推送功能,展示最新的考研政策动态、考试安排、备考技巧等文章,方便学生及时获取考研信息。
学校负责人端
学校负责人登录后可查看本校的考研信息统计情况,如报考本校的人数、各专业的报考热度等。同时,可以对本校的考研信息进行审核和发布,确保信息的准确性和及时性。还可以与其他学校进行考研合作交流的信息对接和管理,促进学校间考研资源的共享和协同。
4、核心代码
4.1、前端核心代码
首页
<template>
<div class="home-view">
<!-- 顶部图片轮播 -->
<el-carousel height="400px" class="top-carousel">
<el-carousel-item v-for="index in 3" :key="index">
<img
:src="getImageUrl(index)"
alt="Banner"
class="carousel-image"
/>
</el-carousel-item>
</el-carousel>
<!-- 三列内容区 -->
<div class="triple-layout">
<!-- 修改后的政策部分 -->
<div class="policy-section">
<h2 class="section-title">最新政策</h2>
<div class="policy-list">
<div
v-for="policy in featuredPolicies"
:key="policy.id"
class="policy-item"
>
<div class="policy-header">
<el-tag
effect="plain"
size="small"
:type="policyTypeMap[policy.policyType]"
>
{{ policy.policyType }}
</el-tag>
<span class="publish-date">
<i class="el-icon-date"></i>
{{ formatDate(policy.publishDate) }}
</span>
</div>
<h3 class="policy-title">{{ policy.title }}</h3>
<div class="policy-content">
<p class="content-excerpt">
{{ truncateText(policy.content, 80) }}
</p>
<div class="publish-info">
<el-icon><office-building /></el-icon>
<span class="department">{{ policy.publishDepartment }}</span>
</div>
</div>
<el-button
type="primary"
size="small"
link
@click="showPolicyDetail(policy)"
>
查看详情
</el-button>
</div>
</div>
<!-- 政策详情对话框 -->
<el-dialog
v-model="dialogVisible"
:title="currentPolicy.title"
width="60%"
>
<el-descriptions border :column="1" size="medium">
<el-descriptions-item label="发布部门">
<el-tag size="small">{{ currentPolicy.publishDepartment }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="政策类型">
<el-tag
size="small"
:type="policyTypeMap[currentPolicy.policyType]"
>
{{ currentPolicy.policyType }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="发布日期">
{{ formatDate(currentPolicy.publishDate) }}
</el-descriptions-item>
<el-descriptions-item label="生效日期">
{{ formatDate(currentPolicy.effectiveDate) }}
</el-descriptions-item>
<el-descriptions-item label="政策内容">
<div class="policy-full-content">
{{ currentPolicy.content }}
</div>
<el-button
v-if="currentPolicy.attachmentUrl"
type="primary"
size="small"
class="mt-2"
@click="downloadAttachment(currentPolicy.attachmentUrl)"
>
下载附件
</el-button>
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
<!-- 招生简章 -->
<div class="admission-section">
<h2 class="section-title">招生简章</h2>
<div class="admission-list">
<div
v-for="admission in latestAdmissions"
:key="admission.id"
class="admission-item"
>
<div class="admission-content">
<h4 class="admission-title">{{ admission.title }}</h4>
<div class="admission-meta">
<span class="university">{{
getUniversityName(admission.university_id)
}}</span>
<span class="date">{{ formatDate(admission.publish_date) }}</span>
</div>
</div>
<el-button
type="primary"
size="small"
link
@click="showAdmissionDetail(admission)"
>
查看详情
</el-button>
</div>
</div>
</div>
<!-- 招生简章详情对话框 -->
<el-dialog
v-model="admissionDialogVisible"
:title="currentAdmission.title"
width="60%"
>
<el-descriptions border :column="1" size="medium">
<el-descriptions-item label="发布院校">
<el-tag size="small">{{
getUniversityName(currentAdmission.university_id)
}}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="招生年份">
{{ currentAdmission.admission_year }}
</el-descriptions-item>
<el-descriptions-item label="发布日期">
{{ formatAdmissionDate(currentAdmission.publish_date) }}
</el-descriptions-item>
<el-descriptions-item label="内容详情">
<div class="admission-content">
{{ currentAdmission.content }}
<el-button
v-if="currentAdmission.attachment_url"
type="primary"
size="small"
class="mt-2"
@click="downloadAttachment(currentAdmission.attachment_url)"
>
下载招生简章全文
</el-button>
</div>
</el-descriptions-item>
</el-descriptions>
</el-dialog>
<!-- 优化后的论坛讨论部分 -->
<div class="forum-section">
<h2 class="section-title">热门讨论</h2>
<div class="forum-list">
<div
v-for="post in filteredHotPosts"
:key="post.id"
class="forum-item"
:class="{ 'deleted-post': post.status === 'deleted' }"
@click="goToForumView()"
style="cursor: pointer;"
>
<div class="post-header">
<el-avatar
:src="post.author?.image || defaultAvatar"
size="small"
/>
<div class="post-meta">
<div class="meta-line">
<span class="author">用户{{ post.author_id }}</span>
<el-tag
size="mini"
:type="postStatusMap[post.status].type"
effect="plain"
>
{{ postStatusMap[post.status].label }}
</el-tag>
</div>
<span class="post-time">
<i class="el-icon-time"></i>
{{ formatPostTime(post.created_at) }}
</span>
</div>
</div>
<div class="post-content">
<h5 class="post-title">
<span
class="topic-tag"
:style="topicStyle(post.target_type)"
>
{{ targetTypeMap[post.target_type] }}
</span>
{{ post.title }}
</h5>
<div class="post-stats">
<div class="stat-item">
<i class="el-icon-chat-line-round"></i>
<span class="count">{{ post.reply_count }}</span>
</div>
<div class="stat-item">
<i class="el-icon-view"></i>
<span class="count">{{ post.view_count }}</span>
</div>
<el-tag
v-if="post.is_top"
size="mini"
type="warning"
effect="dark"
class="top-tag"
>
置顶
</el-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue' // 添加 computed 导入
import { getPolicyList } from '@/api/policy'
import { getAdmissionList } from '@/api/admission'
import { getPostList } from '@/api/forum'
import { getUniversityList } from '@/api/university'
import { OfficeBuilding } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import { useRouter } from 'vue-router' // 引入 useRouter
const router = useRouter()
// import { OfficeBuilding } from '@element-plus/icons-vue'
// 数据获取
const featuredPolicies = ref([])
const latestAdmissions = ref([])
const hotPosts = ref([])
const universities = ref([])
// 新增论坛相关数据
const defaultAvatar = ref(
'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
)
const postStatusMap = {
published: { label: '已发布', type: 'success' },
pending: { label: '审核中', type: 'warning' },
deleted: { label: '已删除', type: 'danger' },
}
const targetTypeMap = {
policy: '政策相关',
admission: '招生讨论',
university: '院校交流',
major: '专业探讨',
general: '综合讨论',
}
const topicColors = {
policy: '#f56c6c',
admission: '#409eff',
university: '#67c23a',
major: '#e6a23c',
general: '#909399',
}
const dialogVisible = ref(false)
const currentPolicy = ref({})
const currentSlideIndex = ref(0)
const policyTypeMap = {
'国家线': 'danger',
'地方政策': 'warning',
'院校政策': 'success',
}
// 计算属性
const filteredHotPosts = computed(() => {
return hotPosts.value
.filter((post) => post.status !== 'deleted')
.sort((a, b) => b.reply_count - a.reply_count)
.slice(0, 5)
})
// 新增方法
const topicStyle = (type) => ({
backgroundColor: topicColors[type] + '15',
color: topicColors[type],
borderColor: topicColors[type],
})
const formatPostTime = (timeStr) => {
return dayjs(timeStr).format('MM-DD HH:mm')
}
const showPolicyDetail = (policy) => {
currentPolicy.value = policy
dialogVisible.value = true
}
const handleCarouselChange = (index) => {
currentSlideIndex.value = index
}
const admissionDialogVisible = ref(false)
const currentAdmission = ref({
university_id: null,
admission_year: '',
publish_date: '',
content: '',
attachment_url: null,
})
const showAdmissionDetail = (admission) => {
// 使用 router.push 进行页面跳转
router.push({
name: 'AdmissionsDetail',
params: { id: admission.id } // 传递招生简章的 ID 作为参数
})
}
// 修改日期格式化方法
const formatAdmissionDate = (dateStr) => {
return dayjs(dateStr).format('YYYY-MM-DD')
}
const downloadAttachment = (url) => {
// 实现附件下载逻辑
window.open(url, '_blank')
}
// 新增图片路径处理方法
const getImageUrl = (index) => {
return new URL(`../assets/lun/${index}.png`, import.meta.url).href
}
// 原有方法保持不变...
const loadData = async () => {
const [policyRes, admissionRes, postRes, uniRes] = await Promise.all([
getPolicyList(null, 'published'),
getAdmissionList({ status: 'published' }),
getPostList({ order: 'hot' }),
getUniversityList(),
])
featuredPolicies.value = policyRes.data.data
latestAdmissions.value = admissionRes.data.data
hotPosts.value = postRes.data.data
universities.value = uniRes.data.data
}
// 工具函数
const getUniversityName = (id) => {
return universities.value.find((u) => u.id === id)?.name || '未知院校'
}
const formatDate = (dateArray) => {
if (!dateArray || dateArray.length !== 3) return ''
return dayjs(
new Date(dateArray[0], dateArray[1] - 1, dateArray[2])
).format('YYYY-MM-DD')
}
const truncateText = (text, length) => {
return text.length > length ? text.slice(0, length) + '...' : text
}
// 新增跳转方法
const goToForumView = () => {
router.push({ name: 'forumView' });
}
loadData()
</script>
<style scoped>
.home-view {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
}
/* 顶部轮播 */
.top-carousel {
border-radius: 12px;
overflow: hidden;
margin-bottom: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.carousel-image {
width: 100%;
height: 400px;
object-fit: cover;
}
/* 三列布局 */
.triple-layout {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
margin-bottom: 40px;
}
.section-title {
font-size: 18px;
color: #303133;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid #409eff;
}
/* 政策部分样式 */
.policy-section {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.policy-list {
max-height: 320px; /* 根据需要调整 */
overflow-y: auto;
}
.policy-item {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.policy-item:last-child {
border-bottom: none;
}
.policy-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.policy-title {
font-size: 16px;
color: #1a1a1a;
margin-bottom: 12px;
line-height: 1.4;
}
.policy-content {
font-size: 14px;
color: #444;
line-height: 1.6;
margin-bottom: 12px;
}
.publish-info {
font-size: 12px;
color: #666;
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
}
/* 招生简章 */
.admission-section {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.admission-list {
max-height: 320px;
overflow-y: auto;
}
.admission-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.admission-item:last-child {
border-bottom: none;
}
.admission-title {
font-size: 14px;
margin-bottom: 6px;
}
.admission-meta {
font-size: 12px;
color: #666;
display: flex;
gap: 8px;
}
/* 新增论坛样式 */
.forum-section {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.forum-list {
max-height: 400px;
overflow-y: auto;
padding-right: 8px;
}
.forum-item {
padding: 16px;
margin-bottom: 12px;
background: #f8fafc;
border-radius: 8px;
transition: all 0.3s ease;
border: 1px solid transparent;
&:hover {
transform: translateX(4px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
border-color: #409eff30;
}
}
.post-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.post-meta {
margin-left: 12px;
.meta-line {
display: flex;
align-items: center;
gap: 8px;
}
.author {
font-size: 13px;
color: #606266;
font-weight: 500;
}
.post-time {
font-size: 12px;
color: #909399;
display: flex;
align-items: center;
gap: 4px;
}
}
.post-title {
font-size: 14px;
color: #303133;
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 8px;
.topic-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid;
flex-shrink: 0;
}
}
.post-stats {
display: flex;
align-items: center;
gap: 16px;
.stat-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #909399;
i {
font-size: 14px;
}
.count {
font-weight: 500;
}
}
.top-tag {
margin-left: auto;
}
}
.deleted-post {
opacity: 0.6;
background: #fef0f0;
.post-title {
color: #f56c6c;
text-decoration: line-through;
}
}
/* 滚动条样式 */
.forum-list::-webkit-scrollbar {
width: 6px;
}
.forum-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.forum-list::-webkit-scrollbar-thumb {
background: #c0c4cc;
border-radius: 4px;
}
.forum-list::-webkit-scrollbar-thumb:hover {
background: #909399;
}
/* 响应式 */
@media (max-width: 992px) {
.triple-layout {
grid-template-columns: 1fr;
gap: 20px;
}
.policy-section {
height: auto; /* 移除高度限制 */
}
.top-carousel {
height: 300px;
}
.carousel-image {
height: 300px;
}
}
.policy-full-content {
white-space: pre-wrap;
line-height: 1.8;
max-height: 400px;
overflow-y: auto;
padding: 8px;
background: #f8f9fa;
border-radius: 4px;
}
:deep(.el-descriptions__body) {
background: #f8f9fa;
}
.admission-content {
white-space: pre-wrap;
line-height: 1.8;
padding: 12px;
background: #f8f9fa;
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
}
.admission-content p {
margin-bottom: 1em;
}
</style>
专业
<template>
<div style="margin: 0 10px;">
<!-- 搜索栏 -->
<div style="margin-bottom: 20px;">
<el-input
v-model="searchName"
placeholder="请输入专业名称"
prefix-icon="el-icon-search"
clearable
style="width: 250px; margin-right: 10px;"
/>
<el-select
v-model="selectedUniversity"
filterable
clearable
placeholder="选择所属院校"
style="width: 200px; margin-right: 10px;"
>
<el-option
v-for="uni in universityList"
:key="uni.id"
:label="uni.name"
:value="uni.id"
/>
</el-select>
<el-button type="primary" @click="loadMajors">搜索</el-button>
<el-button type="success" @click="openAddDialog">新增专业</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="paginatedData" style="width: 100%">
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="name" label="专业名称" width="150" />
<el-table-column label="所属院校" width="180">
<template #default="{ row }">
{{ getUniversityName(row.university_id) }}
</template>
</el-table-column>
<el-table-column prop="duration" label="学制" width="100" />
<el-table-column label="学费" width="120">
<template #default="{ row }">
{{ row.tuition_fee ? `¥${row.tuition_fee.toFixed(2)}` : '-' }}
</template>
</el-table-column>
<!-- 新增课程列 -->
<el-table-column label="课程" width="200">
<template #default="{ row }">
<div style="display: flex; flex-wrap: wrap; gap: 5px;">
<el-tag
v-for="(course, index) in row.courses"
:key="index"
type="info"
size="small"
>
{{ course }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
<el-button link type="primary" @click="openEditDialog(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div style="margin-top: 20px; text-align: center;">
<el-pagination
background
layout="prev, pager, next"
:total="tableData.length"
:page-size="pageSize"
:current-page="currentPage"
@current-change="handlePageChange"
/>
</div>
<!-- 专业详情对话框 -->
<el-dialog v-model="detailVisible" title="专业详情" width="800px">
<el-descriptions :column="2" border>
<el-descriptions-item label="专业名称">{{ currentMajor.name }}</el-descriptions-item>
<el-descriptions-item label="所属院校">{{ getUniversityName(currentMajor.university_id) }}</el-descriptions-item>
<el-descriptions-item label="学制">{{ currentMajor.duration }}</el-descriptions-item>
<el-descriptions-item label="学费">¥{{ currentMajor.tuition_fee?.toFixed(2) || '-' }}</el-descriptions-item>
<el-descriptions-item label="课程设置" :span="2">
<el-tag
v-for="(course, index) in currentMajor.courses"
:key="index"
style="margin-right: 5px; margin-bottom: 5px;"
>
{{ course }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="就业方向" :span="2">
{{ currentMajor.employment_direction || '暂无信息' }}
</el-descriptions-item>
<el-descriptions-item label="专业简介" :span="2">
{{ currentMajor.description || '暂无简介' }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
<el-form :model="formData" label-width="100px">
<el-form-item label="专业名称" required>
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item label="所属院校" required>
<el-select
v-model="formData.university_id"
filterable
placeholder="请选择院校"
style="width: 100%;"
>
<el-option
v-for="uni in universityList"
:key="uni.id"
:label="uni.name"
:value="uni.id"
/>
</el-select>
</el-form-item>
<el-form-item label="学制">
<el-input v-model="formData.duration" placeholder="如:4年" />
</el-form-item>
<el-form-item label="学费">
<el-input v-model="formData.tuition_fee" type="number">
<template #append>元/年</template>
</el-input>
</el-form-item>
<!-- 修改对话框中的课程输入绑定 -->
<el-form-item label="课程设置">
<el-input
v-model="coursesInput"
type="textarea"
:rows="3"
placeholder="请输入课程名称,用逗号分隔(如:高等数学, 大学英语)"
/>
</el-form-item>
<el-form-item label="就业方向">
<el-input
v-model="formData.employment_direction"
type="textarea"
:rows="2"
placeholder="请输入主要就业方向"
/>
</el-form-item>
<el-form-item label="专业简介">
<el-input
v-model="formData.description"
type="textarea"
:rows="4"
placeholder="请输入专业详细介绍"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getMajorList,
deleteMajor,
createMajor,
updateMajor
} from '@/api/major'
import { getUniversityList } from '@/api/university'
// 移除原来的coursesText计算属性
const coursesInput = ref('')
// 数据相关
const tableData = ref([])
const universityList = ref([])
const searchName = ref('')
const selectedUniversity = ref('')
const currentPage = ref(1)
const pageSize = 8
// 分页计算
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize
return tableData.value.slice(start, start + pageSize)
})
// 对话框相关
const dialogVisible = ref(false)
const detailVisible = ref(false)
const dialogTitle = ref('新增专业')
const currentMajor = ref({})
const formData = ref({
name: '',
university_id: null,
duration: '',
tuition_fee: null,
courses: [],
employment_direction: '',
description: ''
})
// // 课程设置文本处理
// const coursesText = computed({
// get: () => formData.value.courses?.join(', ') || '',
// set: (val) => {
// formData.value.courses = val.split(/[,,]/).map(s => s.trim()).filter(Boolean)
// }
// })
// 加载院校列表
const loadUniversities = async () => {
try {
const res = await getUniversityList('', '')
universityList.value = res.data.data
} catch (error) {
ElMessage.error('加载院校列表失败')
}
}
// 加载专业列表
const loadMajors = async () => {
try {
const res = await getMajorList({
name: searchName.value,
university_id: selectedUniversity.value
})
tableData.value = res.data.data
} catch (error) {
ElMessage.error('加载失败')
}
}
// 获取院校名称
const getUniversityName = (id) => {
const uni = universityList.value.find(u => u.id === id)
return uni?.name || '未知院校'
}
// 打开详情对话框
const openDetail = (row) => {
currentMajor.value = row
detailVisible.value = true
}
// 打开新增对话框时重置输入
const openAddDialog = () => {
formData.value = {
name: '',
university_id: null,
duration: '',
tuition_fee: null,
courses: [],
employment_direction: '',
description: ''
}
coursesInput.value = '' // 新增时清空课程输入
dialogTitle.value = '新增专业'
dialogVisible.value = true
}
// 打开编辑对话框时填充数据
const openEditDialog = (row) => {
formData.value = { ...row }
coursesInput.value = row.courses.join(', ') // 将数组转为字符串
dialogTitle.value = '编辑专业'
dialogVisible.value = true
}
// 修改后的提交方法
const submitForm = async () => {
try {
// 在提交前处理课程数据
const payload = {
...formData.value,
courses: coursesInput.value.split(/[,,]/) // 处理中英文逗号
.map(s => s.trim())
.filter(Boolean),
tuition_fee: Number(formData.value.tuition_fee) || null
}
if (formData.value.id) {
await updateMajor(formData.value.id, payload)
} else {
await createMajor(payload)
}
ElMessage.success('操作成功')
dialogVisible.value = false
loadMajors()
} catch (error) {
ElMessage.error('操作失败')
}
}
// 删除处理
const handleDelete = async (id) => {
try {
await ElMessageBox.confirm('确定删除该专业吗?', '警告', { type: 'warning' })
await deleteMajor(id)
ElMessage.success('删除成功')
loadMajors()
} catch (error) {
console.log('取消删除')
}
}
// 分页切换
const handlePageChange = (page) => {
currentPage.value = page
}
// 初始化加载
onMounted(() => {
loadUniversities()
loadMajors()
})
</script>
考研政策
<template>
<div style="margin: 0 10px;">
<!-- 搜索栏 -->
<div style="margin-bottom: 20px;">
<el-input
v-model="searchTitle"
placeholder="请输入政策标题"
prefix-icon="el-icon-search"
clearable
style="width: 250px; margin-right: 10px;"
/>
<el-select
v-model="searchStatus"
clearable
placeholder="选择状态"
style="width: 120px; margin-right: 10px;"
>
<el-option label="已发布" value="published" />
<el-option label="草稿" value="draft" />
</el-select>
<el-button type="primary" @click="loadPolicies">搜索</el-button>
<el-button type="success" @click="openAddDialog">新增政策</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="paginatedData" style="width: 100%">
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="title" label="政策标题" width="200" />
<el-table-column prop="publishDepartment" label="发布部门" width="150" />
<el-table-column prop="policyType" label="政策类型" width="120">
<template #default="{ row }">
<el-tag>{{ row.policyType || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="publishDate" label="发布时间" width="120" />
<el-table-column prop="effectiveDate" label="生效时间" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'published' ? 'success' : 'info'">
{{ row.status === 'published' ? '已发布' : '草稿' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="viewCount" label="浏览次数" width="100" />
<el-table-column fixed="right" label="操作" width="150">
<template #default="{ row }">
<el-button link type="primary" @click="openEditDialog(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div style="margin-top: 20px; text-align: center;">
<el-pagination
background
layout="prev, pager, next"
:total="tableData.length"
:page-size="pageSize"
:current-page="currentPage"
@current-change="handlePageChange"
/>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
<el-form :model="formData" label-width="100px">
<el-form-item label="政策标题" required>
<el-input v-model="formData.title" />
</el-form-item>
<el-form-item label="政策类型">
<el-select v-model="formData.policyType" placeholder="请选择">
<el-option label="国家线" value="国家线" />
<el-option label="院校政策" value="院校政策" />
</el-select>
</el-form-item>
<el-form-item label="发布部门">
<el-input v-model="formData.publishDepartment" />
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="formData.publishDate"
type="date"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="生效时间">
<el-date-picker
v-model="formData.effectiveDate"
type="date"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="政策内容">
<el-input v-model="formData.content" type="textarea" :rows="4" />
</el-form-item>
<el-form-item label="附件上传">
<el-upload
action="http://localhost:8080/upload"
:headers="{ 'Authorization': 'Bearer ' + token }"
:data="{ type: 'policy' }"
name="file"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:show-file-list="false"
>
<el-button type="primary">点击上传PDF</el-button>
<template #tip>
<div class="el-upload__tip">
只能上传PDF文件,且不超过10MB
</div>
</template>
</el-upload>
<div v-if="formData.attachmentUrl" style="margin-top: 10px;">
当前附件:
<a :href="formData.attachmentUrl" target="_blank" class="link">{{ formData.attachmentUrl }}</a>
<el-button
type="danger"
size="small"
@click="formData.attachmentUrl = ''"
style="margin-left: 10px;"
>
移除
</el-button>
</div>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="formData.status">
<el-radio label="published">发布</el-radio>
<el-radio label="draft">草稿</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getPolicyList,
deletePolicy,
createPolicy,
updatePolicy
} from '@/api/policy'
// 数据相关
const tableData = ref([])
const searchTitle = ref('')
const searchStatus = ref('')
const currentPage = ref(1)
const pageSize = 10
const token = ref(localStorage.getItem('token')) // 根据实际存储方式调整
// 分页计算
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize
return tableData.value.slice(start, start + pageSize)
})
// 对话框相关
const dialogVisible = ref(false)
const dialogTitle = ref('新增政策')
const formData = ref({
title: '',
content: '',
publishDepartment: '',
policyType: '',
publishDate: '',
effectiveDate: '',
attachmentUrl: '',
status: 'draft'
})
// 文件上传处理
const handleUploadSuccess = (response) => {
if (response.code === 1) {
formData.value.attachmentUrl = response.data
ElMessage.success('上传成功')
} else {
ElMessage.error(response.message || '上传失败')
}
}
const handleUploadError = (error) => {
ElMessage.error('文件上传失败: ' + (error.message || '未知错误'))
}
// 加载数据(含日期转换)
const loadPolicies = async () => {
try {
const res = await getPolicyList(searchTitle.value, searchStatus.value)
tableData.value = res.data.data.map(item => ({
...item,
publishDate: formatDateArray(item.publishDate),
effectiveDate: formatDateArray(item.effectiveDate),
createdAt: formatDateArray(item.createdAt)
}))
} catch (error) {
ElMessage.error('加载失败')
}
}
// 日期数组转字符串([2024,2,28] → "2024-02-28")
const formatDateArray = (dateArray) => {
if (!dateArray || dateArray.length !== 3) return ''
const [year, month, day] = dateArray
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
}
// 打开新增对话框
const openAddDialog = () => {
formData.value = {
title: '',
content: '',
publishDepartment: '',
policyType: '',
publishDate: '',
effectiveDate: '',
attachmentUrl: '',
status: 'draft'
}
dialogTitle.value = '新增政策'
dialogVisible.value = true
}
// 打开编辑对话框
const openEditDialog = (row) => {
formData.value = { ...row }
dialogTitle.value = '编辑政策'
dialogVisible.value = true
}
// 提交表单(字段名转换)
const submitForm = async () => {
try {
const payload = {
title: formData.value.title,
content: formData.value.content,
publishDepartment: formData.value.publishDepartment,
policyType: formData.value.policyType,
publishDate: formData.value.publishDate,
effectiveDate: formData.value.effectiveDate,
attachmentUrl: formData.value.attachmentUrl,
status: formData.value.status
}
if (formData.value.id) {
await updatePolicy(formData.value.id, payload)
} else {
await createPolicy(payload)
}
ElMessage.success('操作成功')
dialogVisible.value = false
loadPolicies()
} catch (error) {
ElMessage.error('操作失败')
}
}
// 删除处理
const handleDelete = async (id) => {
try {
await ElMessageBox.confirm('确定删除该政策吗?', '警告', { type: 'warning' })
await deletePolicy(id)
ElMessage.success('删除成功')
loadPolicies()
} catch (error) {
console.log('取消删除')
}
}
// 分页切换
const handlePageChange = (page) => {
currentPage.value = page
}
// 初始化加载
onMounted(() => {
loadPolicies()
})
</script>
<style scoped>
.link {
color: #409eff;
text-decoration: underline;
word-break: break-all;
}
</style>
数据
<template>
<div class="dashboard">
<el-row :gutter="20">
<!-- 用户统计 -->
<el-col :span="12">
<el-card>
<template #header>
<span>用户统计</span>
</template>
<BaseChart :options="userChartOptions" />
</el-card>
</el-col>
<!-- 发帖统计 -->
<el-col :span="12">
<el-card>
<template #header>
<span>论坛发帖统计</span>
</template>
<BaseChart :options="postChartOptions" />
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mt-4">
<!-- 招生简章统计 -->
<el-col :span="12">
<el-card>
<template #header>
<span>招生简章状态分布</span>
</template>
<BaseChart :options="admissionChartOptions" />
</el-card>
</el-col>
<!-- 政策统计 -->
<el-col :span="12">
<el-card>
<template #header>
<span>政策类型分布</span>
</template>
<BaseChart :options="policyChartOptions" />
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import BaseChart from '@/components/BaseChart.vue';
import { getUserList } from '@/api/user';
import { getPostList } from '@/api/forum';
import { getAdmissionList } from '@/api/admission';
import { getPolicyList } from '@/api/policy';
import { ElMessage } from 'element-plus';
// 用户统计图表数据
const userChartOptions = ref({});
// 发帖统计图表数据
const postChartOptions = ref({});
// 招生简章统计图表数据
const admissionChartOptions = ref({});
// 政策统计图表数据
const policyChartOptions = ref({});
// 处理用户数据
const processUserData = (users) => {
const identityCount = users.reduce((acc, user) => {
acc[user.identity] = (acc[user.identity] || 0) + 1;
return acc;
}, {});
// 添加 identity 映射
const identityMapping = {
'ADMIN': '管理员',
'USER': '用户'
};
return {
tooltip: { trigger: 'item' },
series: [{
type: 'pie',
data: Object.entries(identityCount).map(([name, value]) => ({
name: identityMapping[name] || name, // 使用映射后的名称
value
}))
}]
};
};
// 处理发帖数据
const processPostData = (posts) => {
const postCountByMonth = posts.reduce((acc, post) => {
const month = new Date(post.created_at).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit' });
acc[month] = (acc[month] || 0) + 1;
return acc;
}, {});
return {
xAxis: {
type: 'category',
data: Object.keys(postCountByMonth)
},
yAxis: { type: 'value' },
series: [{
data: Object.values(postCountByMonth),
type: 'bar',
barWidth: '60%'
}]
};
};
// 处理招生简章数据
const processAdmissionData = (admissions) => {
const statusCount = admissions.reduce((acc, item) => {
acc[item.status] = (acc[item.status] || 0) + 1;
return acc;
}, {});
// 添加状态映射
const statusMapping = {
'published': '已发布',
'draft': '草稿'
};
return {
tooltip: { trigger: 'item' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: Object.entries(statusCount).map(([name, value]) => ({
name: statusMapping[name] || name, // 使用映射后的名称
value
}))
}]
};
};
// 处理政策数据
const processPolicyData = (policies) => {
// 使用中文类型映射(根据实际业务需求补充)
const typeMapping = {
'national': '国家线',
'local': '地方政策',
'institution': '院校政策'
};
const typeCount = policies.reduce((acc, policy) => {
const rawType = policy.policyType;
// 使用映射后的类型,如果没有映射则显示原值
const type = typeMapping[rawType] || rawType || '未分类';
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {});
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'middle'
},
series: [{
name: '政策类型',
type: 'pie',
radius: [50, 100],
center: ['40%', '50%'],
roseType: 'radius',
itemStyle: {
borderRadius: 5
},
label: {
show: true,
formatter: '{b}: {c}'
},
data: Object.entries(typeCount)
.sort((a, b) => b[1] - a[1])
.map(([name, value]) => ({
name,
value,
itemStyle: {
color: getColorByType(name) // 自定义颜色方法
}
}))
}]
};
};
// 示例颜色分配(可根据需要扩展)
function getColorByType(type) {
const colors = {
'国家线': '#5470c6',
'地方政策': '#91cc75',
'院校政策': '#fac858',
'未分类': '#ee6666'
};
return colors[type] || '#73c0de';
}
onMounted(async () => {
try {
// 用户数据
const userRes = await getUserList('', '');
const rawUserData = userRes.data?.data || userRes.data;
if (!Array.isArray(rawUserData)) throw new Error('用户数据格式异常');
userChartOptions.value = processUserData(rawUserData);
// 发帖数据
const postRes = await getPostList(null);
const rawPostData = postRes.data.data
if (!Array.isArray(rawPostData)) throw new Error('发帖数据格式异常');
postChartOptions.value = processPostData(rawPostData);
// 招生简章数据
const admissionRes = await getAdmissionList({
title: '',
status: '',
university_id: '',
})
const rawAdmissionData = admissionRes.data.data;
if (!Array.isArray(rawAdmissionData)) throw new Error('招生简章数据格式异常');
admissionChartOptions.value = processAdmissionData(rawAdmissionData);
// 政策数据
const policyRes = await getPolicyList({ status: undefined });
const rawPolicyData = policyRes.data.data;
if (!Array.isArray(rawPolicyData)) throw new Error('政策数据格式异常');
policyChartOptions.value = processPolicyData(rawPolicyData);
} catch (error) {
console.error('完整错误信息:', error);
ElMessage.error(`数据加载失败: ${error.response?.data?.message || error.message}`);
}
});
</script>
<style scoped>
.dashboard {
padding: 20px;
}
.mt-4 {
margin-top: 20px;
}
</style>
4.2、后端核心代码
controller
@Slf4j
@RestController
@RequestMapping("/admission")
@RequiredArgsConstructor
public class AdmissionController {
private final AdmissionService admissionService;
@GetMapping("/list")
public Result getAdmissionList(@RequestParam(required = false) String title,
@RequestParam(required = false) String status,
@RequestParam(required = false) Integer universityId) {
List<Admission> admissionList = admissionService.getAdmissionList(title, status, universityId);
return Result.success(admissionList);
}
@DeleteMapping("/delete")
public Result deleteAdmission(@RequestParam Integer id) {
admissionService.deleteAdmission(id);
return Result.success();
}
@PostMapping("/add")
public Result createAdmission(@RequestBody Admission admission) {
log.info("新增:{},", admission);
admissionService.createAdmission(admission);
return Result.success();
}
@PutMapping("/update/{id}")
public Result updateAdmission(@PathVariable Integer id, @RequestBody Admission admission) {
admission.setId(id);
log.info("更新:{}",admission);
admissionService.updateAdmission(admission);
return Result.success();
}
@GetMapping("/detail/{id}")
public Result<UniversityVO> getDetail(@PathVariable Integer id){
log.info("详情:{}",id);
UniversityVO vo = admissionService.getDetail(id);
return Result.success(vo);
}
}
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class ApiController {
private final UserService userService;
private final JwtProperties jwtProperties;
/**
* 登录
* @param userDTO
* @return
*/
@PostMapping("/login")
public Result Login(@RequestBody UserDTO userDTO){
log.info("用户登录信息:{}",userDTO);
User user = userService.login(userDTO);
if (user == null){
return Result.error("用户名或密码错误");
}
//创建载荷,将用户id写进载荷
Map<String, Object> map = new HashMap<>();
map.put(JwtClaimsConstant.USER_ID, user.getId());
//生成令牌
String token = JwtUtil.createJWT(
jwtProperties.getUserSecretKey(),
jwtProperties.getUserTtl(),
map);
return Result.success(token);
}
/**
* 注册
* @param user
* @return
*/
@PostMapping("/register")
public Result register(@RequestBody User user){
log.info("注册用户:{}",user);
userService.register(user);
return Result.success();
}
}
@RestController
@RequestMapping("/major")
@Slf4j
@RequiredArgsConstructor
public class MajorController {
private final MajorService majorService;
/**
* 查询专业列表
* @param name
* @param universityId
* @return
*/
@GetMapping("list")
public Result getMajorList(
@RequestParam(required = false) String name,
@RequestParam(required = false,name = "university_id") String universityId
){
log.info("查询专业列表:{},{}", name, universityId);
List<MajorVO> list = majorService.getList(name, universityId);
return Result.success(list);
}
@DeleteMapping("delete")
public Result deleteMajor(Integer id){
log.info("删除专业:{}", id);
majorService.deleteMajor(id);
return Result.success();
}
@PostMapping("add")
public Result addMajor(@RequestBody MajorVO majorVO){
log.info("添加专业:{}", majorVO);
majorService.addMajor(majorVO);
return Result.success();
}
@PutMapping("update/{id}")
public Result updateMajor(@PathVariable Integer id, @RequestBody MajorVO majorVO){
log.info("修改专业:{},{}", id, majorVO);
majorService.updateMajor(id, majorVO);
return Result.success();
}
}
@RestController
@RequestMapping("/policy")
@Slf4j
public class PolicyController {
@Autowired
private PolicyService policyService;
// 分页查询政策列表
@GetMapping("/list")
public Result<List<Policy>> list(
@RequestParam(required = false) String title,
@RequestParam(required = false) String status) {
log.info("分页查询政策列表:{},{}", title, status);
List<Policy> policies = policyService.getPolicyList(title, status);
return Result.success(policies);
}
// 新增政策
@PostMapping("/add")
public Result<String> add(@RequestBody Policy policy) {
log.info("新增政策:{}", policy);
policyService.addPolicy(policy);
return Result.success("添加成功");
}
// 修改政策
@PutMapping("/update/{id}")
public Result<String> update(@PathVariable Integer id,@RequestBody Policy policy) {
policy.setId(id);
policyService.updatePolicy(policy);
return Result.success("更新成功");
}
// 删除政策
@DeleteMapping("/delete")
public Result<String> delete(@RequestParam Integer id) {
policyService.deletePolicy(id);
return Result.success("删除成功");
}
}
service
@Service
@RequiredArgsConstructor
public class AdmissionServiceImpl implements AdmissionService {
private final AdmissionMapper admissionMapper;
private final UniversityMapper universityMapper;
/**
* 获取列表
*
* @param title
* @param status
* @param universityId
* @return
*/
@Override
public List<Admission> getAdmissionList(String title, String status, Integer universityId) {
List<Admission> list = admissionMapper.selectAll(title,status,universityId);
return list;
}
/**
* 根据id删除
*
* @param id
* @return
*/
@Override
public void deleteAdmission(Integer id) {
admissionMapper.deleteById(id);
}
/**
* 新增
*
* @param admission
*/
@Override
public void createAdmission(Admission admission) {
admission.setCreatedBy(1L);
admission.setCreatedAt(LocalDateTime.now());
admissionMapper.insert(admission);
}
/**
* 修改
*
* @param admission
*/
@Override
public void updateAdmission(Admission admission) {
admissionMapper.update(admission);
}
/**
* 详情
*
* @param id
* @return
*/
@Override
public UniversityVO getDetail(Integer id) {
Admission admission = admissionMapper.getById(id);
if (admission == null) {
return null;
}
Integer universityId = admission.getUniversityId();
University university = universityMapper.getById(universityId);
if (university == null) {
return null;
}
UniversityVO vo = new UniversityVO();
BeanUtils.copyProperties(university, vo);
List<Admission> list = admissionMapper.selectByUniversityId(universityId);
vo.setAdmissions(list);
return vo;
}
}
@Service
@RequiredArgsConstructor
public class MajorServiceImpl implements MajorService {
private final MajorMapper majorMapper;
/**
* 查询专业列表
*
* @param name
* @param universityId
* @return
*/
@Override
public List<MajorVO> getList(String name, String universityId) {
List<Major> list = majorMapper.selectByCondition(name, universityId);
List<MajorVO> vos = new ArrayList<>();
for (Major major : list) {
String coursesStr = major.getCourses();
MajorVO majorVO = new MajorVO();
BeanUtils.copyProperties(major, majorVO);
if (coursesStr != null && !coursesStr.isEmpty()) {
List<String> coursesList = Arrays.asList(coursesStr.split(",\\s*"));
majorVO.setCourses(coursesList); // 设置到 MajorVO
} else {
majorVO.setCourses(new ArrayList<>()); // 确保 VO 中的课程列表不为空
}
vos.add(majorVO);
}
return vos;
}
/**
* 删除专业
*
* @param id
*/
@Override
public void deleteMajor(Integer id) {
majorMapper.delete(id);
}
/**
* 新增专业
*
* @param majorVO
*/
@Override
public void addMajor(MajorVO majorVO) {
Major major = new Major();
BeanUtils.copyProperties(majorVO, major);
major.setCourses(String.join(",", majorVO.getCourses()));
majorMapper.insert(major);
}
/**
* 修改
*
* @param id
* @param majorVO
*/
@Override
public void updateMajor(Integer id, MajorVO majorVO) {
Major major = new Major();
BeanUtils.copyProperties(majorVO, major);
major.setCourses(String.join(",", majorVO.getCourses()));
major.setId(id);
System.out.println(major);
majorMapper.update(major);
}
}
@Service
public class PolicyServiceImpl implements PolicyService {
@Autowired
private PolicyMapper policyMapper;
@Override
public List<Policy> getPolicyList(String title, String status) {
return policyMapper.selectByCondition(title, status);
}
@Override
public void addPolicy(Policy policy) {
// 设置默认值
if (policy.getStatus() == null) policy.setStatus("draft");
if (policy.getViewCount() == null) policy.setViewCount(0);
policyMapper.insert(policy);
}
@Override
public void updatePolicy(Policy policy) {
policyMapper.update(policy);
}
@Override
public void deletePolicy(Integer id) {
policyMapper.deleteById(id);
}
}
mapper
@Mapper
public interface AdmissionMapper {
List<Admission> selectAll(@Param("title") String title, @Param("status") String status, @Param("universityId") Integer universityId);
@Delete("delete from admissions where id = #{id}")
void deleteById(Integer id);
@Insert({
"INSERT INTO admissions (title, university_id, admission_year, content, publish_date, attachment_url, status, created_by, created_at)",
"VALUES (#{title}, #{universityId}, #{admissionYear}, #{content}, #{publishDate}, #{attachmentUrl}, #{status}, #{createdBy}, #{createdAt})"
})
@Options(useGeneratedKeys = true, keyProperty = "id") // 如果 `id` 是自增主键
void insert(Admission admission);
void update(Admission admission);
@Select("select * from admissions where id = #{id}")
Admission getById(Integer id);
@Select("select * from admissions where university_id = #{universityId}")
List<Admission> selectByUniversityId(Integer universityId);
}
@Mapper
public interface PostMapper {
List<ForumPost> selectList(
@Param("title")String title,
@Param("status")String status,
@Param("targetType") String targetType
);
@Update("update forum_posts set status = #{status} where id = #{id}")
void updateStatus(@Param("id") Integer id,@Param("status") String status);
@Insert("INSERT INTO forum_posts (title, content, author_id, target_type, target_id, status, is_top, view_count, reply_count, created_at, updated_at) " +
"VALUES (#{title}, #{content}, #{authorId}, #{targetType}, #{targetId}, #{status}, #{isTop}, #{viewCount}, #{replyCount}, #{createdAt}, #{updatedAt})")
void insert(ForumPost post);
@Update({
"<script>",
"UPDATE forum_posts",
"<set>",
"<if test='title != null'>title = #{title},</if>",
"<if test='content != null'>content = #{content},</if>",
"<if test='authorId != null'>author_id = #{authorId},</if>",
"<if test='targetType != null'>target_type = #{targetType},</if>",
"<if test='targetId != null'>target_id = #{targetId},</if>",
"<if test='status != null'>status = #{status},</if>",
"<if test='isTop != null'>is_top = #{isTop},</if>",
"<if test='viewCount != null'>view_count = #{viewCount},</if>",
"<if test='replyCount != null'>reply_count = #{replyCount},</if>",
"<if test='createdAt != null'>created_at = #{createdAt},</if>",
"<if test='updatedAt != null'>updated_at = #{updatedAt},</if>",
"</set>",
"WHERE id = #{id}",
"</script>"
})
void update(ForumPost post);
}
@Mapper
public interface UniversityMapper {
// 分页查询院校列表(动态SQL)
List<University> selectByCondition(
@Param("name") String name,
@Param("region") String region
);
// 新增院校
@Insert("INSERT INTO universities (name, region, ranking, official_website, description, logo_url, created_by, created_at) " +
"VALUES (#{name}, #{region}, #{ranking}, #{officialWebsite}, #{description}, #{logoUrl}, #{createdBy}, NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insert(University university);
// 更新院校
@Update("UPDATE universities SET name=#{name}, region=#{region}, ranking=#{ranking}, " +
"official_website=#{officialWebsite}, description=#{description}, logo_url=#{logoUrl} " +
"WHERE id=#{id}")
void update(University university);
// 删除院校
@Delete("DELETE FROM universities WHERE id=#{id}")
void deleteById(Integer id);
@Select("select * from universities where id = #{id}")
University getById(Integer universityId);
}