Springboot考研信息平台

发布于:2025-05-15 ⋅ 阅读:(17) ⋅ 点赞:(0)

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、项目说明
  1. 项目采用前后端分离的架构模式,这种架构模式具有诸多显著的优势。
    • 开发效率提升 :前端开发人员和后端开发人员可以相对独立地进行开发工作,无需相互等待。前端人员可以专注于使用 Vue 等前端技术构建用户界面和交互逻辑,后端人员则专注于利用 Springboot 等后端技术实现业务逻辑和数据处理功能,从而提高了整体开发效率。
    • 技术选型灵活 :前后端分离使得前端和后端可以各自选择最适合的技术栈。前端可以根据项目的 UI 设计和用户体验需求选择 Vue 框架配合 Element Plus UI 等组件库,后端则可以根据业务复杂度、性能要求等因素选择 Springboot 框架结合 Mybatis 等技术,这种灵活性有利于充分发挥各技术的优势,满足项目不同的需求。
    • 可维护性和可扩展性增强 :前后端代码分离,各自形成了独立的模块体系,便于代码的管理和维护。当需要对前端界面进行优化或者对后端业务逻辑进行调整时,可以只针对相应的部分进行修改,而不会对另一部分产生过多的影响。同时,这种分离架构也为项目的后续扩展提供了便利,例如,未来可以方便地添加新的前端功能模块或者后端服务模块,以满足考研信息平台不断发展的业务需求。
  2. 在项目运行时,前端和后端通过定义好的 API 接口进行通信。前端通过 Axios 向端后发送 HTTP 请求,包括获取考研信息、用户注册登录等操作的请求;后端接收到请求后,利用 Springboot 进行业务逻辑处理,通过 Mybatis 操作 MySQL 数据库获取或存储数据,再将处理结果以 JSON 等格式返回给前端,前端再根据返回的数据更新页面显示,实现了前后端的紧密协作,为用户提供高质量的考研信息服务。
3、项目截图

登录注册

​ 平台提供了简洁、便捷的登录注册界面。登录界面包含用户名和密码输入框,以及登录按钮。用户可以输入自己的账号信息进行登录,新用户则可以通过注册界面进行账号注册。注册界面要求填写用户名、密码、邮箱等基本信息,并设置有验证码功能,以确保注册账号的安全性。同时,登录注册界面还设计有忘记密码的找回功能,方便用户在忘记密码时能够快速找回账号登录。

image-20250515202629060

image-20250515202650116

管理员端

​ 管理员登录后进入的管理端界面功能丰富。主要包含考研信息管理模块,可以添加、编辑、删除考研院校和专业的信息,如更新院校的招生简章、专业目录等;还有用户管理模块,能够查看和管理用户的基本信息,处理用户反馈和投诉;此外,还包括数据分析模块,通过图表展示考研相关的数据统计,如用户浏览量、注册用户数量变化趋势等,帮助管理员了解平台的运营情况,以便更好地进行决策和管理。

image-20250515202854735

image-20250515202908378

image-20250515202919816

image-20250515202929621

image-20250515202938507

image-20250515202948464

image-20250515203001176

学生端

​ 学生端界面以提供考研信息查询和学习服务为主。界面包含考研院校和专业查询功能,学生可以通过输入关键词或者筛选条件(如地区、学科类别等)快速查询到自己感兴趣的院校和专业信息。同时,学生端还提供考研资讯推送功能,展示最新的考研政策动态、考试安排、备考技巧等文章,方便学生及时获取考研信息。

image-20250515202739917

image-20250515202754628

image-20250515202803376

image-20250515202812817

学校负责人端

​ 学校负责人登录后可查看本校的考研信息统计情况,如报考本校的人数、各专业的报考热度等。同时,可以对本校的考研信息进行审核和发布,确保信息的准确性和及时性。还可以与其他学校进行考研合作交流的信息对接和管理,促进学校间考研资源的共享和协同。

image-20250515203034286

image-20250515203052343

image-20250515203058974

image-20250515203105139

image-20250515203111818

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);
}

网站公告

今日签到

点亮在社区的每一天
去签到