Springboot考研信息平台

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、项目截图

登录注册

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

管理员端

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

学生端

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

学校负责人端

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

4、核心代码
4.1、前端核心代码

首页

vue 复制代码
<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>

专业

vue 复制代码
<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>

考研政策

vue 复制代码
<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>

数据

java 复制代码
<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

java 复制代码
@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);
    }
}
java 复制代码
@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();
    }
}
java 复制代码
@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();
    }
}
java 复制代码
@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

java 复制代码
@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;
    }
}
java 复制代码
@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);
    }
}
java 复制代码
@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

java 复制代码
@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);
}
java 复制代码
@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);
}
java 复制代码
@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);
}
相关推荐
全栈派森34 分钟前
云存储最佳实践
后端·python·程序人生·flask
CircleMouse39 分钟前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
獨枭2 小时前
使用 163 邮箱实现 Spring Boot 邮箱验证码登录
java·spring boot·后端
维基框架2 小时前
Spring Boot 封装 MinIO 工具
java·spring boot·后端
秋野酱2 小时前
基于javaweb的SpringBoot酒店管理系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
☞无能盖世♛逞何英雄☜2 小时前
Flask框架搭建
后端·python·flask
Q_Q19632884752 小时前
python的家教课程管理系统
开发语言·spring boot·python·django·flask·node.js·php
进击的雷神2 小时前
Perl语言深度考查:从文本处理到正则表达式的全面掌握
开发语言·后端·scala
进击的雷神3 小时前
Perl测试起步:从零到精通的完整指南
开发语言·后端·scala
豌豆花下猫3 小时前
Python 潮流周刊#102:微软裁员 Faster CPython 团队(摘要)
后端·python·ai