☰
SpringBoot+Vue3+MyBatis+MySQL实现前后端分离校园求职招聘系统全栈实战
2026/10/6 4:42:22 网站建设 项目流程

把 SpringBoot、Vue3、MyBatis、MySQL 这一整套组合做成一个前后端分离的校园求职招聘系统,是我去年完整跑通的一个实战项目。这个系统解决的是高校求职场景里最常见的痛点:学生找实习和全职岗位,招聘方发岗位收简历,管理员做内容审核,三个角色在同一套业务链路里各司其职。如果你正在准备 Java 后端面试,或者想找一个能快速落地的全栈练手项目,这套源码的拆解过程值得看一看。我尽量不按课程目录念,只讲实际开发里真正影响进度和代码质量的部分。先说清楚它能给你什么:一是完整的角色权限与业务闭环,二是 SpringBoot 和 MyBatis 的后端写法和联调思路,三是 Vue3 组合式 API 的前端组织方式,后面每个环节我都会穿插自己踩过的坑。

1. 项目全貌与设计思路

1.1 核心业务与角色划分

校园求职招聘系统,本质上是个“双边市场”的简化版。原本我以为核心功能就是发布职位、投简历两件事,真正动手梳理才发现没那么简单。我把它拆成三个角色、四条主链路:

  • 学生端:浏览岗位、搜索筛选岗位、收藏岗位、维护个人简历、投递简历、查看投递状态。
  • 招聘方端:发布岗位、编辑岗位、下线岗位、查看收到的简历、更新投递状态(待查看、已查看、已邀约、已淘汰)。
  • 管理员端:用户管理、岗位审核、举报处理、数据统计看板。

角色多了,权限逻辑就必须提前设计。学生看到的是岗位大厅,招聘方看到的是“我的公司主页”,管理员看到的是审核队列,三套界面共用同一套登录入口,但路由、菜单、接口权限完全分开。如果一上来就堆代码,后期加权限判断一定改得焦头烂额。我建议第一步先画清楚角色和状态流转图,哪怕只画在草稿纸上,也比直接建表靠谱——这也是我这次踩了不少坑之后最大的体会。

四条主链路里最核心的是岗位发布审核链和简历投递链:招聘方发布岗位,状态是待审核,管理员审核通过后学生才能看到;学生投递简历后,招聘方在后台推进状态,学生端实时看到“已查看”“已邀约面试”等反馈。这两条链路贯穿了几乎所有数据库表设计、接口设计和状态机设计。

1.2 为什么选这套技术栈组合

选型这件事,我见过太多人为了“显得高级”硬堆中间件,最后项目根本跑不完。校园求职招聘系统这种业务规模,用 SpringBoot + Vue3 + MyBatis + MySQL 恰好是性价比最高的组合,也是目前 Java 全栈岗位面试里最常被问起的标配。

SpringBoot 解决了传统 Spring 配置繁琐的问题,内嵌 Tomcat,一个 fat jar 就能跑起来。Vue3 这边,组合式 API 比 Options API 更适合多人协作和逻辑复用,配合 Vite 开发时的热更新很快,招聘大厅这种实时筛选页面,响应式体验明显更好。MyBatis 作为持久层框架,轻量、SQL 可控,面试中常问的动态 SQL、缓存、XmlConfigBuilder 初始化原理都有很好的切入点;对于岗位筛选这类需要灵活拼接条件的业务,MyBatis 的<where>、<if>标签简直是天生的匹配方案。MySQL 则是因为高校和中小公司都喜欢用,部署成本低,练手、毕设、简历项目都能自圆其说。

整体采用前后端分离架构,而不是传统的模板引擎渲染页面,一个很现实的原因是:学生端和招聘方端的界面差异太大,分开开发可以各自独立出版本;后端只提供 JSON 接口,前端拿到数据自己渲染。这样联调也方便,部署时前端打静态包丢到 Nginx,后端打 jar 包独立跑,互不阻塞。

2. 数据库设计:一切业务的地基

2.1 核心表结构与角色设计

校园求职招聘系统的数据库设计,优先级最高的不是“字段越多越好”,而是把角色和状态理清楚。我最终保留了这几张核心表:用户表、岗位表、简历表、投递记录表、收藏表。管理员没有单独建表,而是在用户表里用 role 字段区分,学生、招聘方、管理员共用账号体系,登录后用同一套逻辑鉴权。

用户表最关键的字段是:id、username、password(BCrypt 加密存储)、role(0 学生 / 1 招聘方 / 2 管理员)、real_name、phone、email、school、major、education、create_time。password 绝对不能明文存,这是基本功;school、major 这些字段我建议放用户表而不是简历表,原因很简单:学生可以维护多份简历,但学校、专业基本不变,拆开会导致冗余和同步问题。

岗位表是核心业务表,字段包括:id、publisher_id(关联用户表招聘方)、company_name、company_logo、job_title、job_type(0 全职 / 1 实习 / 2 兼职)、salary_min、salary_max、address、description、requirement、status(0 待审核 / 1 已上架 / 2 已拒绝 / 3 已下线)、del_flag、create_time、update_time。这里我把岗位状态单独拎出来,就是给管理员审核流程留出口子。如果不做审核,招聘方随发随上,平台很快就全是广告帖。

简历表建议这样设计:id、student_id、title、real_name、education_level、work_years、skills、project_experience、file_url、is_default、create_time、update_time。注意简历可以有多份,所以再加一个 is_default 字段表示默认简历,投递时如果没有指定具体简历,就用默认的那一份。投递记录表是整个系统里最容易出问题的一张表,必须包含:id、student_id、position_id、resume_id、recruiter_id、status(0 已投递 / 1 已查看 / 2 已邀约 / 3 已淘汰)、feedback、create_time,并建立 student_id + position_id 的联合唯一索引,防止学生重复投递同一岗位。

2.2 那些只有踩坑后才明白的设计细节

第一,尽量用逻辑删除而不是物理删除。我第一版直接物理删除岗位,结果发现学生的收藏记录和专业数据全部断链,页面报错的场景特别尴尬。后来所有核心表都加上 del_flag 字段,查询时统一带del_flag = 0条件,数据才稳定下来。

第二,不要过度使用外键。MyBatis 体系下,外键约束很容易成为性能瓶颈,而且一旦表多了,删除顺序错了直接卡死。我在设计阶段没有加任何数据库外键,只在 Java 业务层做逻辑校验。这样联表查询更灵活,测试数据也好清理。

第三,索引设计要有针对性。岗位表的 job_type、status 需要走筛选,所以要建普通索引;投递表经常按 student_id 查历史记录,按 recruiter_id 查收到的简历,这两个字段也索引。但不要无脑给所有字段加索引,写入性能会明显下降。

第四,时间字段别用 varchar 存。我见过有人把时间存成字符串的,排序和跨天统计痛苦到怀疑人生。统一用 datetime,并且给 create_time 设置默认值CURRENT_TIMESTAMP,这样插入时不用手动赋值。

第五,大字段要舍得拆。简历内容如果包含项目经历、技能标签、自我评价,全部塞进一个字段会导致查询变慢,而且后期做关键词搜索时很难优化。我的做法是技能标签单独存 varchar 用逗号分隔,经历类内容存 text,列表页查询时不查 text 字段,等用户点开详情再单独按 id 查询。

3. 后端实现:SpringBoot + MyBatis 的落地细节

3.1 登录鉴权与用户权限设计

登录这块我用的是 JWT + 拦截器方案,而不是传统 Session,原因很简单:前后端分离后,后端不管理会话状态,前端拿着 Token 就能请求接口。认证流程是这样的:用户提交用户名和密码,后端用 BCrypt 校验密码哈希,校验通过后生成 JWT 返回给前端,前端存入本地存储,之后每次请求都在 Authorization 请求头里带上 Token。

后端做一个 JwtInterceptor 拦截器,继承 HandlerInterceptor,在 preHandle 里解析 Token 并取出用户 id 和角色,放行或者返回 401。拦截器注册时要注意:登录接口、注册接口、验证码接口、静态资源路径必须排除,否则用户还没登录就进不来了。代码大概长这样:

@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { throw new BusinessException(401, "未登录或登录已过期"); } LoginUser loginUser = JwtUtil.parseToken(token); UserContext.set(loginUser); return true; }

角色控制我建议再做一层注解权限,比如自定义@RequireRole(role = RoleEnum.STUDENT),在拦截器里校验当前用户角色和注解要求是否匹配。这样做的好处是接口级别的权限一目了然,面试官问“怎么实现权限控制”的时候,你能说出拦截器处理认证、注解处理授权的完整链路。

实际开发中还要注意 Token 过期时间的设计。我一开始把过期时间设成 24 小时,学生第二天打开系统发现页面空了一片,还以为是后端挂了,查了半天才发现是 Token 过期。现在统一设置成 2 小时,前端在 axios 响应拦截器里对 401 做全局处理,跳转登录页并清空本地存储,体验就好多了。

3.2 核心业务接口与状态流转

后端接口我按业务模块划分,核心接口大概有十几个。岗位模块:发布岗位、编辑岗位、岗位列表分页查询、岗位详情、上下线操作、管理员审核岗位。简历模块:创建简历、编辑简历、设置默认简历、上传附件简历。投递模块:学生投递、投递记录列表、招聘方更新投递状态。

岗位列表查询是压力最大的接口,学生端一进来就要看岗位,还要带关键词、岗位类型、薪资范围、排序条件。用 MyBatis 动态 SQL 拼条件非常顺手:

<select id="listPositions" resultType="PositionVO"> SELECT p.*, u.company_name, u.company_logo FROM position p JOIN users u ON p.publisher_id = u.id <where> p.del_flag = 0 AND p.status = 1 <if test="keyword != null and keyword != ''"> AND (p.job_title LIKE CONCAT('%', #{keyword}, '%') OR p.company_name LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="jobType != null"> AND p.job_type = #{jobType} </if> <if test="minSalary != null"> AND p.salary_max &gt;= #{minSalary} </if> </where> ORDER BY p.create_time DESC LIMIT #{offset}, #{pageSize} </select>

这里有个容易踩的坑:动态 SQL 里的<if test="...">判断的是 Java 对象字段,不是数据库字段,所以jobType传字符串和传数字时要特别小心,类型对不上就会查不出数据。

投递接口需要注意幂等性。学生点了“立即投递”,前端如果没做防重复,或者网络慢导致连续请求,后端可能插入两条投递记录。我用了两层保障:第一层业务逻辑,投递前先查是否已存在这条投递记录;第二层数据库兜底,在投递表建立 student_id + position_id 唯一索引,插入时如果冲突就捕获异常,通过对外提示“你已投递过该岗位”。这样即便前端出了什么幺蛾子,数据也乱不了。

招聘方更新投递状态的接口要带上状态变更校验。比如只有处于“已投递”的简历才能被改成“已查看”,如果已经“已淘汰”了还强行改成“已邀约”,业务上说不通。这种状态机的控制尽量放在后端,前端只负责展示和提交,不然一个懂接口的人随便传个数字就能绕过业务逻辑。

3.3 MyBatis 实战细节:动态 SQL、关联查询与缓存

MyBatis 这块是面试的重灾区,也是实际写代码时最容易翻车的地方。先说配置。MySQL 字段一般是下划线风格,Java 属性是驼峰风格,所以 mybatis-config 里必须开启驼峰映射:

mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

log-impl设为 StdOutImpl 会在控制台打印 SQL,开发阶段一定要开,排查问题能省一半时间;上线后去掉即可,否则日志量太大。

关联查询有两种方式,一对一的岗位发布者信息用JOIN查出来映射到一个 VO 里就行;一对多的“岗位被哪些简历投递”这种场景,可以用 MyBatis 的 resultMap 做嵌套查询,也可以在业务层分两次查询再组装。我建议小项目优先分两次查询,逻辑直白,不容易出现 N+1 问题失控的情况;等确认确实有性能瓶颈再改成嵌套 resultMap。

再讲 MyBatis 缓存。一级缓存是 SqlSession 级别,同一个会话里重复执行相同 SQL 会直接命中缓存;二级缓存是 Mapper 级别,跨 Session 可共享。听起来很美好,但在我的实战经验里,二级缓存开起来反而容易出问题:假如岗位表和用户表做了联表查询,缓存的是联表结果,只要用户表更新了公司信息,岗位表缓存的关联数据就是脏的。所以我这个项目的方案是:默认不开二级缓存,只在极少量、几乎不更新的字典查询上开了局部缓存。面试的时候如果有人问“MyBatis 缓存”,你能把一级缓存的生命周期、二级缓存脏数据风险讲清楚,比背一堆配置有效得多。

还有个细节是#{}和${}的区别。写动态 SQL 时,所有传参都用#{},它会预编译并加引号,能防 SQL 注入。${}是直接拼接字符串,只在排序列名这类动态字段时用,并且要严格校验白名单。如果整个项目里$满天飞,那基本是埋雷。

关于 MyBatis 的初始化原理,其实面试官最爱问的就是 XmlConfigBuilder 的流程:解析 mybatis-config.xml,读取 properties、settings、typeAliases、environments、mappers,然后构建 Configuration 对象,最后通过 SqlSessionFactoryBuilder 生成 SqlSessionFactory。你就把这个流程讲成“读取配置 → 构建全局配置对象 → 加载 Mapper 映射 → 返回可用的 SqlSession 工厂”,逻辑就顺了。

4. 前端实现:Vue3 组合式 API 与组件化开发

4.1 项目搭建与目录划分

前端我用 Vite 创建 Vue3 项目,UI 组件库选的 Element Plus,状态管理用的 Pinia,路由用的 Vue Router 4。很多教程会在脚手架里自动生成一堆示例文件,我一律删掉重来,目录结构保持清爽:

src/ api/ # 接口请求模块,按业务拆分成 auth.js / position.js / resume.js assets/ # 静态资源 components/ # 通用组件,比如岗位卡片、分页器、空状态 router/ # 路由配置和守卫 stores/ # Pinia 状态,比如 userStore views/ student/ # 学生端页面:岗位大厅、岗位详情、我的投递 recruiter/ # 招聘方页面:岗位管理、收到的简历 admin/ # 管理员页面:审核列表、用户管理 utils/ # axios 封装、日期格式化等工具函数

目录划分这一点千万别省。我第一版直接把所有 .vue 文件平铺在 views 下面,页面多了之后找文件全靠滚动,后来拆成角色目录,组件复用和业务隔离都清晰很多。

Vue3 里我全部用了<script setup>组合式 API。很多人纠结 Composition API 和 Options API 哪个好,我的实际感受是:业务逻辑稍微复杂一点,组合式 API 能把“某个功能相关的状态、方法、生命周期”集中在一起,而不是散落在 data、methods、computed 里。尤其是简历编辑这种需要多个表单联动校验的场景,用组合式 API 写一个useResumeForm()组合函数,在创建页和编辑页里直接复用,代码量几乎减半。

Element Plus 组件库上手快,但要注意按需引入。一开始我图省事全量引入,打包出来 3 秒才加载完,后来改成自动按需导入,首屏明显快了。如果你做的是毕设或简历项目,这种优化哪怕只是提一句,也能看出你有性能意识。

4.2 登录态、路由守卫与 axios 封装

前端登录态的维护,核心是 Pinia 加上路由守卫。用户登录成功后,JWT 存到 localStorage,同时在 Pinia 的 userStore 里记录用户信息和角色。路由守卫每次跳转前判断有没有 Token:没有就强制跳转登录页,有但页面需要指定角色,再校验角色是否匹配。

axios 封装这里我吃过不少亏。我在 utils 里创建一个 axios 实例,基础路径设为/api,然后在请求拦截器里加上 Authorization 头,在响应拦截器里统一处理后端返回的 code。核心逻辑大概是:

service.interceptors.request.use((config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = token; } return config; }); service.interceptors.response.use( (response) => { const res = response.data; if (res.code === 200) { return res.data; } if (res.code === 401) { localStorage.removeItem('token'); router.push('/login'); } ElMessage.error(res.message); return Promise.reject(new Error(res.message)); }, (error) => { ElMessage.error('网络异常,请稍后重试'); return Promise.reject(error); } );

这里有个坑:如果后端返回的 code 不是 200,但 HTTP 状态码是 200(很多项目中业务异常就藏在 body 里),响应拦截器照样会走成功回调,所以一定要在拦截器里判断业务 code,而不是依赖 HTTP 状态码。我早期没注意这一点,后端报错时前端表格一直空白不弹提示,排查了很久才发现是拦截器漏了一层判断。

路由守卫的写法也要注意“递归校验”。学生端路由、招聘方端路由、管理员端路由会有嵌套子路由,meta 里标角色之后,要用to.matched.some(record => record.meta.roles.includes(role))去检查整个匹配链,而不是只看当前路由的 meta。这个细节在面试里讲出来,会显得你确实处理过复杂路由场景。

4.3 招聘大厅与简历管理页怎么做

招聘大厅是学生端最核心的页面,也是整个系统的门面。我的实现是:顶部搜索栏,岗位类型下拉,薪资范围下拉,下面是岗位卡片列表。页面进来后调用岗位列表接口,把关键词、类型、薪资、页码传给后端,后端返回分页数据,前端用 Element Plus 的分页组件展示。

这里要特别提一下搜索防抖。如果关键词输入框每次变化都立即请求,学生打一个字就发一次请求,后端压力很大。我用 Vue3 自带的 watch 加上定时器做了 300ms 防抖,接口调用频率明显降下来。这是个小优化,但面试问“前端性能优化”时可以直接拿出来说。

岗位详情页我用了“抽屉 + 路由参数”的方式:从列表卡片点击时跳转/student/position/:id,详情页根据 id 拉详情并展示岗位要求、公司介绍、薪资范围,底部是“立即投递”按钮。投递前如果没简历,直接弹窗引导去创建简历,避免投递失败后学生一脸懵。

简历管理页是学生端的第二核心。我做成列表形式,每条简历显示标题、学历、技能标签、默认标识,右侧是编辑、设为默认、删除按钮。编辑页用 Element Plus 的 el-form 做表单校验:真实姓名必填、学历必填、技能标签至少填一个。简历的“项目经验”我用 textarea 多行输入,前端先按换行拆分解析成数组,提交时再转成字符串存到后端。

5. 联调、环境与部署那点事

5.1 本地环境准备:MySQL、Node 与数据库初始化

本地开发环境要装的东西不多:JDK 11 或 17、Maven、Node.js 16+、MySQL 5.7 或 8.0。MySQL 安装这块经常有人卡住,我见过最多的报错是安装了 8.0 之后客户端连接直接报 “SSL connection error”。解决办法很简单,jdbc url 里加上参数:

jdbc:mysql://localhost:3306/job_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

serverTimezone=Asia/Shanghai也很重要。MySQL 8.0 默认时区和 Java 的时区如果对不上,时间字段会偏差 8 小时,日志里全是诡异的时间。数据库创建时字符集一定要用 utf8mb4,不是 utf8,因为 utf8 在 MySQL 里存不下 emoji 和一些生僻字,学生简历里出现特殊字符就直接报错。

数据库初始化脚本建议按顺序执行:先建库,再建用户表,再建岗位表、简历表、投递表、收藏表。别偷懒用工具直接导出一大坨 SQL,面试官要是问你表结构,你能按依赖顺序讲出来,就说明你确实理解数据模型。我在脚本里还用了一个演示数据文件,插入了十条岗位和五份简历,前端联调时不用每次手动造数据。

5.2 跨域与接口代理

前后端分离后,跨域是个绕不开的话题。我推荐用开发环境的代理方案,而不是后端加@CrossOrigin。原因很简单:代理只作用于开发环境,上线后由 Nginx 统一转发,代码不用改;如果后端写死跨域配置,上线后又多了额外的来源校验逻辑。

Vite 的配置在 vite.config.js 里:

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }

这样前端请求/api/position/list,开发服务器会把它转发到后端的/position/list,同时浏览器角度看到的是同源请求,跨域问题从根上消失。后端接口路径里就不要带/api前缀,保持干净。如果你非要后端处理跨域,记住allowCredentials为 true 时,allowedOrigins不能写通配符,这是很多人的翻车点。

5.3 打包部署到服务器(Nginx 方案)

部署我建议用 Nginx 托管前端静态资源,后端 jar 包独立运行。前端执行npm run build后会生成 dist 目录,把 dist 整个文件夹传到服务器,放到/data/job-front/dist,然后在 Nginx 配置里指定路由转发。核心配置如下:

server { listen 80; server_name your_domain; root /data/job-front/dist; index 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; } location / { try_files $uri $uri/ /index.html; } }

这里有个必须注意的地方:try_files $uri $uri/ /index.html是解决前端路由刷新 404 的关键。Vue Router 默认是 history 模式,直接访问/student/position/1时 Nginx 找不到这个文件路径,如果不加 try_files,刷新页面就是 404。我第一版部署上去,点开详情页再刷新,白屏加 404,就是漏了这一行。

后端打包用mvn clean package -DskipTests,生成 jar 包后在服务器上执行java -jar job-system.jar。如果是 SpringBoot 3.x,要小心javax到jakarta的迁移问题,老项目直接跑在 3.x 上会报一堆 ClassNotFoundException。服务器内存小的,记得加-Xms256m -Xmx512m限制堆内存,别让 Java 把机器内存吃爆。

还有个可选方案:把前端 dist 目录直接复制到后端项目的src/main/resources/static下,重新打包成一个 fat jar,这样只用启动一个进程。适合学校课程设计演示,但不适合正式上线,因为前端更新需要重新编译整个后端。我建议按选型分开部署,更贴近企业实践。

6. 常见问题与面试复盘

6.1 开发阶段最容易踩的坑(速查表)

我把实际开发和测试中遇到的高频问题整理成了一个速查表,按症状、原因、解决办法排列。这张表基本能覆盖大部分新人会踩的坑:

症状常见原因解决办法
接口返回 404 但代码没问题后端接口路径和前端请求路径没对齐,或 Nginx 转发路径缺/重新核对接口路径,检查 proxy_pass 末尾的斜杠
启动报 “Access denied for user”MySQL 用户名密码错误或权限不足检查数据库账号授权,GRANT ALL PRIVILEGES ON job_db.* TO 'root'@'localhost'
时间比真实时间早 8 小时serverTimezone 没设置或设置错误JDBC URL 增加serverTimezone=Asia/Shanghai
列表页数据不刷新前端分页参数没传到后端,或者动态 SQL 条件拼接错误控制台看 SQL 日志,检查<where>标签和参数类型
刷新页面 404前端路由 history 模式未配置 Nginx 回退Nginx 加try_files $uri $uri/ /index.html
打包后体积巨大,首屏慢Element Plus 全量引入改成按需自动导入
上传简历失败后端默认文件大小限制 1MB 太严格在 application.yml 设置spring.servlet.multipart.max-file-size和max-request-size
学生重复投递同一岗位前端按钮未防抖,后端无幂等保护联合唯一索引 + 投递前查询校验
SQL 日志打印不出来mybatis 配置log-impl没设置设置log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

开发时还有一个思路上的建议:每写完一个接口,先用 Postman 或 Apifox 跑通,再对接前端页面。如果页面调不通,你能很快判断问题出在前端还是后端,而不是两边同时抢修,最后发现是字段名少了一个字母。

6.2 从面试角度复盘这个项目的亮点

做完这个系统后,我把它写进了简历里。面试时被问得最多的问题,反而不是“用了什么框架”,而是“设计这个系统时你怎么考虑业务和数据”。我复盘了一下最值得讲的几个点:

  • 角色权限。能说清楚 JWT 认证和拦截器授权是两层东西,而不是笼统说“用了 JWT 实现登录”。
  • 状态机。岗位从发布到上架要经过管理员审核,投递记录从投递到邀约有明确状态流转,每一步都能说出“为什么不能跳状态”。
  • 幂等性。投递接口怎么防止重复提交,既讲了业务拦截,也讲了数据库唯一索引兜底,这是一个非常好的细节亮点。
  • 数据库设计。为什么要逻辑删除、为什么不用外键、哪些字段建索引,这些比“我建了十张表”有说服力得多。
  • 跨域与部署。开发代理和 Nginx 转发的区别,try_files 解决 history 路由刷新 404,这些属于实战经验,很多简历上写着“前后端分离”的人实际根本讲不透。

我在实际开发中的体会是,这个系统最打动人的地方从来不是一个花哨功能,而是那些“看起来简单但值得推敲”的细节。如果你正在把这个项目拿来做毕设或者面试项目,建议不要急着加 Redis、加消息队列,先把角色权限、核心状态流转、防重复投递这些基础但关键的问题讲清楚,然后再说性能优化和扩展。技术栈可以聊得天花乱坠,但数据库设计和业务逻辑一旦被深挖,漏洞很快会暴露出来。把一层一层的细节落到实处,这个项目才真正变成你自己的项目。

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

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

立即咨询