☰
SpringBoot+Vue3+MyBatis招生宣传管理系统实战:从表设计到部署全解析
2026/10/17 19:56:22 网站建设 项目流程

每年三四月份招生季,各大学校和培训机构的技术群里就会被同一个需求刷屏:招生宣传管理系统。说真的,这类系统的核心业务逻辑并不复杂,无非是把招生简章、专业介绍、在线咨询、意向登记这些信息从线下搬到线上。但真正动手做的时候,你会发现里面涉及的技术点远比想象中多——前后端如何分离、权限怎么控制、富文本内容怎么存储、批量导入几百条报名数据时事务怎么处理、Vue3的组合式API和Vue2的选项式API差距有多大。这篇文章我以一套完整的“Java SpringBoot+Vue3+MyBatis 招生宣传管理系统”源码为蓝本,把整个项目的设计思路、核心实现、数据库建模和实战坑点逐一拆开讲,适合正在做毕业设计的学生、刚入行想完整做一个前后端分离项目的初级开发,以及想快速搭一套学校官网后台的开发者参考。

在动手之前,先明确这个系统要覆盖的业务场景。招生宣传管理系统的用户通常分两类:前台访客(学生、家长)和后台管理员(招生办老师)。前台需要看到学校概况、招生简章、专业介绍、常见问题,能提交意向报名表或在线留言;后台需要管理这些内容的上传、编辑、审核、发布,还要能查看和导出报名数据。看清楚这一点,系统的模块边界就清晰了——前台展示是门面,后台管理才是核心。

1. 项目定位与整体设计思路

1.1 招生业务的核心痛点与系统边界

招生宣传场景下,信息发布是高频操作,而且内容形式多样:图文公告、专业介绍表格、常见问题问答、轮播图、文件附件等。如果靠手工维护静态页面,每次更新都要改代码重新部署,效率极低;如果直接塞进一个通用CMS里,又缺少招生场景的专属能力,比如意向生源的管理、报名数据的统计导出。

这个项目的业务边界可以拆成四个维度:

  • 内容管理:公告、简章、专业介绍、常见问题的增删改查,这是后台最核心的部分。
  • 前台展示:面向访客的栏目聚合页、详情页,要求加载快、结构清晰。
  • 意向管理:学生/家长提交意向登记后,管理员能查看、跟进、标记状态。
  • 系统管理:管理员账号、角色权限、操作日志。

明确了边界之后,数据库设计和后台功能开发才有的放矢。我见过不少半路弃坑的项目,问题几乎都出在第一步——没想清楚系统到底管什么,就急着建表写代码了。

1.2 技术选型:为什么是SpringBoot+Vue3+MyBatis这一套

这套技术栈现在是Java Web开发的绝对主流组合,选它不是因为追新,而是每层都有明确的优势。

SpringBoot负责后端接口和业务逻辑。它最大的价值是“约定优于配置”,内嵌Tomcat、自动配置、起步依赖管理,让开发者不用再处理一堆XML配置就能跑起一个Web项目。对于招生管理系统这种业务密集型CRUD项目,SpringBoot的成熟生态能极大压缩开发周期。

Vue3负责前端页面和交互。相比Vue2,Vue3的Composition API让逻辑复用更加灵活,配合组合式函数,可以把富文本编辑、表单校验、表格分页这些逻辑抽离成可复用的hooks。而且Vue3与Vite搭配,开发时热更新速度比Webpack快一个量级,对于前后端联调频繁的项目,体验提升非常明显。

MyBatis负责数据持久层。相比JPA,MyBatis对SQL有完全的控制力,在复杂查询、多表关联、动态条件拼接下尤其顺手。招生宣传系统里“公告列表按标题/分类/发布时间多条件筛选”这种需求,MyBatis的动态SQL写起来非常直观,不用担心JPA自动生成的SQL性能不可控。

MySQL作为存储层,开箱即用、生态成熟。这个量级的系统,表数据量在几十万以内,MySQL的性能完全够用,而且云厂商都有托管实例,部署运维省心。

1.3 前后端分离架构的价值与联调约定

前后端分离的核心价值是人效和部署解耦。前端工程和后端工程可以并行开发,只需提前约定好接口文档;部署时前端静态资源交给Nginx托管,后端打jar包或war包独立部署,互不干扰。

在联调阶段,有两点约定非常重要:

  • 接口返回值结构统一。后端统一返回{ code: 200, message: "success", data: {...} }这样的JSON结构,前端统一封装请求拦截器处理状态码,避免每个接口各写一套返回格式。
  • 异常处理统一。业务异常(比如提交的意向登记数据不完整)抛出自定义异常,由全局异常处理器统一捕获并转换为约定格式,而不是直接把500错误堆栈抛给前端。

这两条约定能省掉大量联调时互相扯皮的时间。我在项目里还额外加了一个约定:列表接口的分页参数统一用pageNum、pageSize,排序参数用orderByColumn、isAsc,这样前端封装的分页组件可以一劳永逸。

2. 数据库设计:招生业务的核心表结构

2.1 业务模块拆解与表规划

数据库设计是这类系统的地基,地基不牢,业务逻辑怎么写都别扭。根据前面梳理的业务边界,我把表规划成五个区域:

模块数据表说明
内容管理notice(公告)、category(栏目分类)、major(专业介绍)公告和栏目是父子关系,专业和公告通过关键词关联
意向管理intention(意向登记)、consult(在线留言)两个独立的业务表,状态字段不同
系统管理sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu经典RBAC权限模型
前台展示banner(轮播图)、school_info(学校概况)相对独立,配置化维护

实际开发时,我强烈建议把表设计分成“业务表”和“系统表”两组。业务表服务于招生宣传的具体业务,系统表服务于后台管理的人员权限。分组之后,代码里也能按业务包来组织,模块边界一眼可见。

2.2 核心业务表结构与字段设计

重点说两张核心业务表:notice和intention。

公告表notice的字段设计:

CREATE TABLE `notice` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `title` VARCHAR(200) NOT NULL COMMENT '公告标题', `category_id` BIGINT DEFAULT NULL COMMENT '所属栏目ID', `content` LONGTEXT COMMENT '公告内容(富文本HTML)', `cover_image` VARCHAR(500) DEFAULT NULL COMMENT '封面图URL', `status` TINYINT NOT NULL DEFAULT '0' COMMENT '状态:0草稿 1已发布', `view_count` INT NOT NULL DEFAULT '0' COMMENT '浏览量', `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志:0存在 1删除', PRIMARY KEY (`id`), KEY `idx_category_status` (`category_id`, `status`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='招生公告表';

这个设计里有三个细节值得注意:

  • content用LONGTEXT存富文本HTML。富文本编辑器的输出本身是带标签的HTML,直接存文本即可,前端展示时用v-html渲染。不要用TEXT,因为富文本内容很容易超过64KB。
  • del_flag是逻辑删除标记。招生公告删除后前端页面不能立即失效,可能还需要保留痕迹,逻辑删除方便数据恢复和审计。
  • 联合索引idx_category_status覆盖了“按栏目查询已发布公告”这个高频场景,避免回表。

意向登记表intention的字段设计:

CREATE TABLE `intention` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `student_name` VARCHAR(50) NOT NULL COMMENT '学生姓名', `phone` VARCHAR(20) NOT NULL COMMENT '联系电话', `major_id` BIGINT DEFAULT NULL COMMENT '意向专业ID', `score` DECIMAL(5,1) DEFAULT NULL COMMENT '高考分数', `province` VARCHAR(50) DEFAULT NULL COMMENT '生源地', `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', `status` TINYINT NOT NULL DEFAULT '0' COMMENT '跟进状态:0待跟进 1已联系 2已录取', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_phone` (`phone`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='意向登记表';

意向登记是招生办最看重的数据,字段设计时把姓名和电话做成NOT NULL,因为这两个字段是后续跟进的必要信息。另外单独给phone加索引,因为招生办老师经常按手机号搜索某位学生是否登记过。

2.3 索引与数据一致性的设计细节

索引不是越多越好,每多一个索引就多一份写入时的维护成本。在这个项目里,我的索引设计原则是“查询条件里最常用的字段优先,联合索引覆盖高频组合查询”。

关于数据一致性,有两个场景需要刻意处理:

场景一:删除栏目时,栏目下的公告怎么办。我不建议直接物理删除栏目后让公告表产生悬空引用。更稳妥的方式是:删除栏目时先把该栏目下的公告状态改为“下线”或转移到“未分类”,保证前台不会出现打不开的分类页。

场景二:意向专业和major表关联。专业信息可能每年调整,但历史意向数据里的专业名称必须保留。所以intention表里同时存major_id和专业的冗余快照字段(比如major_name),查询时直接用快照展示,避免关联查询也避免专业改了名称后历史数据错乱。

3. 后端核心实现:SpringBoot集成MyBatis的落地细节

3.1 工程分层与目录结构规划

SpringBoot后端代码我习惯按“controller -> service -> mapper”三层组织,每层职责单一,可替换性强。这个项目的目录结构如下:

com.example.recruit ├── controller # 接口层,接收参数,返回统一结果 │ ├── AdminNoticeController.java │ ├── AdminIntentionController.java │ └── PortalController.java ├── service # 业务逻辑层 │ ├── NoticeService.java │ └── impl │ ├── NoticeServiceImpl.java ├── mapper # MyBatis数据访问层 │ ├── NoticeMapper.java │ └── xml │ ├── NoticeMapper.xml ├── entity # 数据库实体类 │ ├── Notice.java │ └── Intention.java ├── common # 通用类 │ ├── Result.java # 统一返回结果 │ ├── PageResult.java # 分页返回结果 │ └── exception ├── config # 配置类 │ ├── MybatisPlusConfig.java │ └── WebMvcConfig.java └── security # 登录认证相关 ├── JwtUtil.java └── JwtInterceptor.java

分层的同时要注意:Controller层只做参数接收和结果封装,不写业务逻辑;Service层只做业务编排,不出现SQL;Mapper层只做数据访问。这条纪律在多人协作时尤其重要,否则代码很快就会烂成一锅粥。

3.2 登录认证与JWT拦截器设计

后台管理接口不能裸奔,需要一个简单的认证机制。对于前后端分离项目,JWT(JSON Web Token)是比Session更适合的方案:无状态、不占服务端内存、适合水平扩展。

实现逻辑分三步:

  1. 登录接口接收用户名密码,校验通过后生成JWT令牌返回前端。
  2. 前端把令牌存在localStorage,每次请求在Authorization头携带。
  3. 后端定义拦截器,拦截非白名单路径,解析并校验令牌,校验失败返回401。

JWT生成工具的核心代码:

public String generateToken(Integer userId, String username) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME); return Jwts.builder() .setSubject(username) .claim("userId", userId) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(Keys.hmacShaKeyFor(secretKeyBytes), SignatureAlgorithm.HS256) .compact(); }

注意JDK版本对Jwts.builder()API是有影响的,老版本用的是signWith(SignatureAlgorithm.HS256, secretKey),新版本改为传入SecretKey对象。版本不同API断崖式不兼容,编译报错别慌,去官方文档确认当前版本的正确写法。

拦截器里要做的关键校验是令牌过期判断。我实战中遇到过一个典型问题:JWT生成后有效期设为24小时,老师早上登录后台后挂机一天,下午提交一篇公告时提示“未登录”。解决办法有两个,要么在前端做一个“登录态即将过期”的提示并跳转登录页,要么后端维护一个刷新令牌机制。对于这个量级的项目,我更推荐简单的方案:拦截器校验通过后,判断剩余有效期小于4小时就重新生成令牌放在响应头里,前端拦截器检测到新令牌自动替换存储,用户全程无感。

3.3 MyBatis动态SQL与多条件分页查询实战

招生公告后台管理最常用的功能就是多条件筛选列表:按标题模糊搜索、按栏目筛选、按时间范围筛选、按状态筛选。这个需求用MyBatis动态SQL写起来非常自然。

先看Mapper接口:

List<Notice> selectNoticeList(@Param("query") NoticeQuery query);

对应的XML:

<select id="selectNoticeList" resultType="com.example.recruit.entity.Notice"> SELECT id, title, category_id, cover_image, status, view_count, create_time, update_time FROM notice <where> <if test="query.title != null and query.title != ''"> AND title LIKE CONCAT('%', #{query.title}, '%') </if> <if test="query.categoryId != null"> AND category_id = #{query.categoryId} </if> <if test="query.status != null"> AND status = #{query.status} </if> <if test="query.beginTime != null"> AND create_time &gt;= #{query.beginTime} </if> <if test="query.endTime != null"> AND create_time &lt;= #{query.endTime} </if> AND del_flag = '0' </where> ORDER BY create_time DESC LIMIT #{query.offset}, #{query.pageSize} </select>

<where>标签会自动处理第一个AND的多余问题,这是MyBatis最实用的小细节之一——不用手动写WHERE 1=1了。

分页我在这里用的是手写LIMIT #{offset}, #{pageSize},offset = (pageNum - 1) * pageSize。这个方案直白易懂,适合学习阶段。项目里如果用MyBatis-Plus,直接用分页插件Page对象即可,更加省事。两个方案我都用过,手写SQL的好处是SQL执行计划完全可控,分页插件的好处是少写offset计算逻辑。对于面试和毕业设计,手写LIMIT反而更能体现SQL功底。

3.4 批量插入意向登记与事务控制

招生季老师手里可能拿着一摞线下登记表,需要一次性导入系统。业务上就对应批量插入。MyBatis里批量插入有两种常见写法:

方式一:foreach拼接。

<insert id="batchInsert"> INSERT INTO intention (student_name, phone, major_id, score, province, remark, status, create_time) VALUES <foreach collection="list" item="item" separator=","> (#{item.studentName}, #{item.phone}, #{item.majorId}, #{item.score}, #{item.province}, #{item.remark}, 0, NOW()) </foreach> </insert>

方式一SQL语句紧凑,性能最高,但要注意MySQL对单条SQL长度有限制,一次插入几百条没问题,几千条就容易踩max_allowed_packet的坑。

方式二:循环单插。

@Service public class IntentionServiceImpl implements IntentionService { @Override @Transactional(rollbackFor = Exception.class) public void batchAdd(List<Intention> list) { for (Intention item : list) { intentionMapper.insert(item); } } }

方式二简单可靠,但每调一次insert都走一次网络IO和SQL解析,1000条数据会明显感觉慢。折中方案是分片批量插入:每500条为一组,用foreach批量插入。

这里必须强调@Transactional(rollbackFor = Exception.class)的必要性。默认情况下Spring的事务只对RuntimeException回滚,如果业务代码里抛的是受检异常且不设置rollbackFor,事务会静默提交——数据只插了一半,而且很难排查。设置rollbackFor = Exception.class后,任何异常都会触发整批回滚,保证数据一致性。

4. 前端核心实现:Vue3+Element Plus搭建后台与前台

4.1 Vue3工程初始化与目录结构

前端我用Vite作为构建工具初始化Vue3工程,比老旧的Vue CLI脚手架启动速度快得多,配置也简洁。

npm create vite@latest recruit-web -- --template vue cd recruit-web npm install npm install vue-router@4 pinia axios element-plus @element-plus/icons-vue

目录结构规划:

src ├── api # 接口请求封装 │ ├── notice.js │ ├── intention.js │ └── login.js ├── assets ├── components # 通用组件 │ ├── Pagination.vue │ └── RichTextEditor.vue ├── router # 路由配置 │ └── index.js ├── stores # Pinia状态管理 │ └── user.js ├── views │ ├── login/ │ │ └── Login.vue │ ├── portal/ # 前台展示页 │ │ ├── Home.vue │ │ └── NoticeDetail.vue │ └── admin/ # 后台管理页 │ ├── notice/ │ │ ├── NoticeList.vue │ │ └── NoticeEdit.vue │ └── intention/ │ └── IntentionList.vue └── utils ├── request.js # axios实例封装 └── auth.js # token存取封装

前端工程结构要按业务模块组织,而不是按类型堆叠。很多人学Vue时习惯把组件全放一个文件夹,项目一多后面找代码能找到崩溃。按模块分目录后,改某块业务时定位文件非常快。

4.2 核心后台页面拆解:公告列表与富文本编辑

后台公告列表页核心是一个表格 + 搜索表单 + 分页组件。这里Vue3的computed和ref是高频API。示例代码:

<script setup> import { ref, reactive, onMounted, computed } from 'vue' import { getNoticeList } from '@/api/notice' import Pagination from '@/components/Pagination.vue' const loading = ref(false) const tableData = ref([]) const total = ref(0) const queryParams = reactive({ pageNum: 1, pageSize: 10, title: '', categoryId: null, status: null }) const statusMap = { 0: { text: '草稿', type: 'info' }, 1: { text: '已发布', type: 'success' } } async function fetchList() { loading.value = true try { const res = await getNoticeList(queryParams) tableData.value = res.rows total.value = res.total } finally { loading.value = false } } function handleQuery() { queryParams.pageNum = 1 fetchList() } function handleReset() { queryParams.title = '' queryParams.categoryId = null queryParams.status = null handleQuery() } const currentStatusText = computed(() => statusMap[1].text) onMounted(() => { fetchList() }) </script>

富文本编辑器在公告新增/编辑页面是核心组件。Vue3生态里我用得比较多的是@vueup/vue-quill-editor,它是基于Quill 2.0的Vue3封装。注意老牌的vue-quill-editor是Vue2时代的产物,直接在Vue3项目里引入会报Cannot read property 'installed' of undefined这类错误。踩过这个坑之后我学到一个教训:npm install前先看包是否支持Vue3,否则先报错再排查很浪费时间。

4.3 路由守卫与权限控制

后台的路由守卫解决两个问题:未登录不能进后台、登录了不能重复登录页。核心代码写在src/router/index.js:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('admin_token') if (to.path.startsWith('/admin') && !token) { next('/login') } else if (to.path === '/login' && token) { next('/admin/dashboard') } else { next() } })

如果系统需要更细粒度的权限控制(比如不同角色只能访问不同菜单),可以改成动态路由方案:登录时后端返回该用户所拥有的角色和权限码,前端根据权限过滤路由表后再调用router.addRoute动态注册。这个方案灵活但是复杂度上了一个台阶,我用在真实生产项目里时建议:先想清楚你需不需要,小项目静态路由+按钮级权限判断完全够用,别一上来就整动态路由。

4.4 前台展示页与登录装饰

前台首页是访客看到的第一个页面,要兼顾美观和加载速度。核心思路是:前台页面尽量不引入大量第三方组件库,用原生CSS和少量Vue语法完成,避免首屏加载一堆无用JS。轮播图可以自己写一个轻量版,核心代码:

<script setup> import { ref, onMounted, onUnmounted } from 'vue' const props = defineProps({ banners: { type: Array, default: () => [] } }) const currentIndex = ref(0) let timer = null function next() { currentIndex.value = (currentIndex.value + 1) % props.banners.length } onMounted(() => { timer = setInterval(next, 3000) }) onUnmounted(() => { clearInterval(timer) }) </script>

注意onUnmounted里清理定时器。忘了清除定时器是Vue3新手最常见的bug之一——页面都销毁了,定时器还在后台跑,既浪费资源又可能报错“Cannot read properties of null”。

5. 环境搭建与部署实录

5.1 SpringBoot版本选择与JDK版本兼容性

这个项目里SpringBoot版本选择和JDK版本是一个“隐藏大坑”。SpringBoot 3.x要求JDK 17+,如果你用的还是JDK 8,只能选SpringBoot 2.7.x。很多教程默认JDK 8但用了SpringBoot 3.x,启动直接报UnsupportedClassVersionError,就是版本不匹配。

我的建议:

  • JDK 8:选SpringBoot 2.7.18(2.x最后版本),稳定、生态全。
  • JDK 17:可以选SpringBoot 3.2.x,性能略有提升,但要注意部分老依赖可能不兼容。

java热词里有个高频问题“java: outofmemoryerror: insufficient memory”,常见于IDEA设置的内存不足或项目启动参数过小。解决办法是在IDEA的VM options里调大堆内存,例如-Xms256m -Xmx1024m。

5.2 MySQL 8.x安装与配置要点

MySQL安装配置经常出问题,尤其是win系统。几步核心流程:

  1. 官网下载社区版安装包(注意选MySQL Community Server,不是商用版)。
  2. 安装时选择“Server only”,避免装一堆没用组件。
  3. root密码设置时选择“Use Strong Password Encryption”。
  4. 配置环境变量,把C:\Program Files\MySQL\MySQL Server 8.0\bin加入PATH,命令行才能直接敲mysql -u root -p。

连接MySQL时有个高频报错:Public Key Retrieval is not allowed。这是MySQL 8默认启用的caching_sha2_password认证插件导致的。解决方案有两种:JDBC URL上添加allowPublicKeyRetrieval=true&useSSL=false,或者把用户的认证插件改成mysql_native_password:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;

这两种方案我更推荐前者,改认证插件会在兼容性上埋坑。

5.3 前后端联调与跨域处理

本地开发时,前端跑在5173端口,后端跑在8080端口,浏览器会触发跨域限制。处理方案常用两种:

方式一:后端加全局CORS配置。

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:5173") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }

方式二:前端配置开发代理。

在vite.config.js中配置:

export default defineConfig({ server: { host: '0.0.0.0', port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } })

实际生产部署时,Nginx反向代理会同时托管前端静态资源和后端接口,天然解决跨域,所以开发期的代理配置只在联调阶段有用。

6. 常见问题与排查技巧实录

6.1 高频问题速查表

问题现象可能原因解决方案
UnsupportedClassVersionErrorJDK版本与SpringBoot版本不匹配统一JDK 8+SpringBoot 2.7.x或JDK 17+SpringBoot 3.x
Public Key Retrieval is not allowedMySQL 8认证插件问题JDBC URL加allowPublicKeyRetrieval=true&useSSL=false
前端报Cannot read property 'installed' of undefined引入了Vue2专用组件库检查组件库版本,换Vue3版本
列表接口参数为空时返回全部数据Mapper里没有加动态条件用<where><if>动态SQL
富文本内容前端不渲染接口返回HTML被v-html渲染,但内容里有非法标签后端过滤XSS脚本,前端安全渲染
MyBatis批量插入报packet too large单条SQL超MySQL限制分批插入或调大max_allowed_packet
Vue3页面加载后数据未刷新生命周期用了onMounted但没触发请求检查是否正常调用fetchList()

6.2 三个容易被忽略的性能与安全问题

第一,富文本XSS注入。公告内容是富文本HTML,前端用v-html渲染时如果不过滤,用户完全可以在内容里塞一段<script>或者恶意iframe。好在Quill编辑器默认会过滤部分危险标签,但如果有第三方接口直接写入公告内容,后端的攻击面就很大。需要做一套HTML白名单过滤,保留p, img, a, strong, h1-h3等常规标签,删除script, iframe, object, embed等危险标签。

第二,列表页深分页的性能问题。意向登记表数据多了之后,翻到第100页时LIMIT 990, 10的查询会越来越慢,因为MySQL需要扫描前990行然后丢弃。优化方案是“延迟关联”或“书签分页”:先用主键定位起始位置再取数据,比如WHERE id > ? LIMIT 10。不过这个优化属于后置项,数据量小时完全不用管。

第三,接口没有限流保护。意向登记接口是对外开放的,如果被恶意刷单,垃圾数据会直接淹没招生办的跟进列表。我用的轻量方案是:同IP在1分钟内最多提交3次,超过就拒绝。用SpringBoot拦截器 + ConcurrentHashMap做简单的计数器就能实现,不需要引入Redis,解决了80%的刷单问题。

6.3 MyBatis缓存的一些说明

热词里高频出现“mybatis缓存”,这里简单说下。MyBatis有一级缓存和二级缓存。一级缓存默认开启,是SqlSession级别的,在同一会话中执行相同SQL且数据没变化时,直接返回缓存结果。二级缓存默认关闭,是Mapper级别的。招生宣传系统里,公告的查询频繁但更新不频繁(状态从草稿变为已发布),理论上适合开二级缓存。但实际项目里我默认不开二级缓存——一旦开了,发布新公告之后要手动清理相关缓存,排查缓存一致性问题的成本高于省下的那点查询开销。先把业务做对,再做缓存优化,这是我一直坚持的原则。

7. 部署上线步骤与扩展建议

7.1 Nginx+jar包部署全流程

生产环境我用的方案是Nginx静态文件 + SpringBoot jar包双服务部署。

后端打包部署:

mvn clean package -DskipTests scp target/recruit-system.jar root@服务器IP:/opt/recruit/ nohup java -jar /opt/recruit/recruit-system.jar --spring.profiles.active=prod > /opt/recruit/log.log 2>&1 &

前端打包部署:

npm run build # dist目录拷贝到nginx html目录

Nginx配置核心:

server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /usr/share/nginx/html/recruit-web; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

try_files $uri $uri/ /index.html;这一行至关重要。Vue3是SPA单页应用,刷新页面URL变化后,如果Nginx没有这行配置,会直接404。这行配置把不存在的路径全部回退到index.html,由前端路由接管。

7.2 后续扩展方向

这套系统后续可以往这些方向扩展:

  • 数据统计模块:按省份、按专业统计意向登记数量,用ECharts做可视化报表,招生办领导很吃这一套。
  • 消息通知:意向登记提交后通过短信或邮件通知对应招生负责人,提高跟进及时率。
  • 内容版本管理:公告每次修改保留历史版本,支持回滚,适合高频更新的官方内容场景。
  • WAP适配:把Vue3前台页面改造成响应式布局,或者单独做一个H5移动端,适应用手机访问招生页的学生群体。

实际项目做完后我还发现一个高频场景:小程序端展示招生信息。如果将来要接小程序,后端接口可以复用,前端用uni-app重写,整个后端几乎不用动。这也是前后端分离架构最大的红利——业务能力沉淀在后端,前端形态可以灵活换。

就到这里。这套技术栈从设计到落地,是我实际做过的招生宣传类项目打得比较顺的一条路径。如果你的项目场景类似,直接拿这套表结构和模块划分去套,能少走不少弯路。踩过版本不兼容、跨域、分页深翻的坑之后,我最大的体会是:主流技术栈踩坑的人多,解决方案也成熟,遇到问题先确认版本匹配,再查专项资料,大多数问题都能在十分钟内解决。希望大家项目顺利,上线后服务器稳定,招生季数据跑得稳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询