☰
微信小程序+Spring Boot+UniApp的大学生兼职推荐系统全栈开发
2026/10/13 11:44:18 网站建设 项目流程

微信小程序 + springboot + uniapp 的大学生兼职推荐系统,这个题目我盯了很久。说实话,大学生兼职类小程序市面上已经有不少,但真正把"推荐"两个字做扎实的并不多。多数产品就是做了一个职位列表加搜索,用户来了自己翻,翻不到合适的就走了。而这篇博文要拆解的这个系统,核心不只是把兼职信息搬到微信里,而是围绕大学生的专业、空闲时间、技能标签、通勤距离等信息,用一套可落地的推荐逻辑把合适的岗位推给合适的人。后端用 Spring Boot 提供接口服务,前端用 UniApp 一套代码同时覆盖微信小程序和 App 端,整个项目从前端页面到后端接口再到推荐算法,是一条完整的全栈链路。

这篇内容适合正在做毕设、准备接外包项目,或者想从零搭建一个小程序兼职平台的开发者参考。下文会按我实际做这个项目时的思路走:先讲清楚为什么要这么设计业务,再分别拆前端和后端的核心实现,然后重点讲推荐系统和工程化落地,最后聊聊上线阶段踩到的几个坑。

1. 为什么选择"兼职推荐系统"作为小程序切入点

1.1 大学生兼职市场的真实痛点

先聊一个现象。大学里找兼职,最常见的路径是加各种兼职群、关注学校勤工助学公众号,或者在 58 同城、BOSS 直聘上搜"大学生兼职"。这些渠道都存在明显的问题:兼职群里信息杂乱,广告和诈骗信息混在一起;综合招聘平台上的岗位面向社会人群,很多要求全职或者有工作经验,学生筛选成本极高。

我在项目启动前做过一轮调研,找了 30 多个在校学生聊他们的找兼职经历。反馈最集中的问题有三个:一是不知道哪些岗位是真的适合学生做的,很多岗位要求"每周五天到岗",跟课表直接冲突;二是信息匹配效率太低,翻了几十页列表才能找到一两个能投的;三是缺少一个可信的信用机制,学生不敢投,企业也不敢招,双方都有顾虑。

这正是"推荐系统"切入的价值点。如果把学生的空闲时间、所在校区、技能标签、期望薪资这些信息结构化,再给每个兼职岗位打上对应的标签,系统就可以主动计算"哪些岗位最适合这个学生",而不是让学生自己去大海捞针。

1.2 系统核心角色与业务闭环

这个系统的用户角色分三类:学生、企业管理员、平台管理员。

学生端是微信小程序,做四件事:完善个人简历(技能、空闲时间、校区信息)、浏览和搜索兼职岗位、投递简历并跟踪状态、查看录用结果和评价。企业端本来也应该有小程序,但为了控制开发量,第一版做成了 Spring Boot 管理的后台网页,企业管理员登录后发布岗位、查看收到的简历、标记录用或拒绝。平台管理员负责审核岗位信息,同时对整个系统的运行状态做管理。

业务闭环是这样的:企业发布岗位 -> 管理员审核通过 -> 推荐系统把岗位推送给匹配的学生 -> 学生投递简历 -> 企业查看并操作状态 -> 学生收到通知 -> 兼职结束互相评价。推荐系统不是孤立存在的,它依赖整个业务链路的数据积累。学生投递行为、收藏行为、企业录用行为,这些数据都会被记录并反馈到推荐逻辑里,形成逐步优化的闭环。

2. 技术选型:UniApp 与 Spring Boot 的组合逻辑

2.1 前端选型:为什么用 UniApp 而不是原生小程序

技术选型是这类项目最开始要定的事,也是最容易反复摇摆的地方。一开始我有两个备选方案:原生微信小程序 + WeUI,或者 UniApp + uni-ui。最终选了 UniApp,核心原因是"一套代码多端复用"这个收益在真实项目里确实能吃到。

具体说,这个项目的用户端不只是微信小程序,很多学校的学生会习惯用 App 访问,企业管理员也可能在手机上查看简历。如果用原生小程序开发,微信端是没问题了,但 App 端还得再写一套,开发量直接翻倍。UniApp 底层把 Vue 语法编译成各端代码,页面的点击事件、请求逻辑、组件渲染逻辑只要写一遍。工程上的代价是调试时偶尔会遇到编译差异的问题,但整体收益远大于成本。

UniApp 对微信小程序的兼容性在这个项目里也用到了不少细节。比如uni.login对接微信登录、uni.request封装网络请求、uni.setStorage做本地缓存、uni.customShare自定义转发按钮,这些都是跨端 API,不需要额外引 SDK。页面结构上使用<template>+<script>+<style>的 Vue 单文件组件结构,写过 Vue 的人几乎零成本上手。

2.2 后端选型:Spring Boot 项目结构怎么搭

后端没有太多悬念,直接用了 Spring Boot 2.7.x + MyBatis-Plus + MySQL 8.0。选 MyBatis-Plus 而不是 JPA,是因为这个项目的查询场景比较复杂,推荐算法需要很多自定义 SQL,MyBatis-Plus 的灵活度更合适。

项目按模块分包,不是传统的按三层架构堆文件,而是按业务域划分:controller/service/mapper只作为技术层存在,业务上分成 user、job、application、recommend、message、admin 六个模块。每个模块有自己的 controller、service、entity、mapper,这样后期加功能时定位代码非常快。

一个值得说的设计是统一返回值结构。所有接口统一返回Result<T>,包含 code、message、data 三个字段,前端用同一套逻辑处理成功和失败。这个看似简单的约定,在前后端联调阶段省了大量时间。前端再也不用从各种不同的返回结构里去猜当前是成功还是失败。

2.3 接口约定与推荐服务的衔接设计

前后端接口约定上,考虑到推荐模块本身是从职位列表上叠加的一层逻辑,我没有把推荐做得太重。做法是:列表接口GET /api/job/list支持传入recommend=true参数,后端在推荐模块开启时走推荐排序逻辑,关闭时走普通的分页时间排序。这样一是方便对照验证推荐效果,二是即便推荐引擎出问题,列表功能依然可用。

接口设计上还有一个关键点:所有接口统一走/api/前缀,登录后返回 token,后续请求在 header 里带Authorization: Bearer token。Spring Boot 写一个拦截器统一校验,微信小程序端在uni.request的 header 中自动附加 token。这套机制简洁可靠,容易出现的问题都在 Token 的过期和失效处理上,后面会细说。

3. UniApp 前端设计与实现的关键节点

3.1 页面结构划分与导航设计

微信小程序的页面导航和传统 Web 不一样,底部 tabBar 是固定的几个入口,页面跳转是栈的方式。这个项目底部分五个 tab:首页、职位、消息、我的。

首页是推荐位,默认展示推荐算法算出来的 10 个岗位卡片,这页是为"推荐"这个核心功能服务的。职位页是完整列表,按分类筛选、按关键词搜索、按距离排序,这部分是给有明确目标的学生用的。消息页集成系统通知和投递状态变更通知,学生能在这里看到"简历被查看"、"已被录用"等状态变化。我的页面是个人中心,管理简历、设置空闲时间、查看收藏等。

tabBar 在pages.json里配置,需要注意图标文件只能使用本地路径,不能使用网络图片,否则小程序编译会报错。这个细节很容易忽略,我第一次配置时直接填了 CDN 链接,运行时 tabBar 的图标一直显示不出来,排查了很久才发现是微信小程序对 tabBar 图标的限制。

3.2 登录鉴权与用户角色切换

微信小程序登录流程比 Web 登录多了一层。前端调uni.login拿到临时code,把 code 传给后端,后端拿 code 去微信接口换取openid,然后生成自己的 token 返回。因为微信官方限制,code 只能使用一次,而且有效期很短,所以这一步必须由后端完成,前端不能直接拿 openid 来用。

用户角色切换是这类系统的特殊需求。同一个微信账号,既可能是学生,也可能是企业管理员。我不会把角色写死成一个字段,而是设计了user表和student_profile/employer_profile两张扩展表,用户在登录后选择身份,然后跳转到对应的信息完善页面。后续接口根据当前 token 对应用户的身份,加载对应角色的数据。

这里有个容易被忽略的问题:微信小程序的 code2session 接口要求服务端具备小程序的 appid 和 secret,这些信息放到 Spring Boot 的application.yml里,不同环境用不同配置。为防止泄露,我用了@ConfigurationProperties读取,避免硬编码在代码里。

3.3 职位列表、搜索与详情页实现细节

职位列表页是这个项目前端代码量最大的页面。列表用scroll-view实现,配合上拉加载更多。这里的核心是分页逻辑:每页 10 条,往下滚动时触发加载下一页。踩过一个坑,scroll-view的滚动事件在小程序端触发频率很高,如果不加防抖,一个快速滑动可能触发十几次请求,直接把后端打懵。解决方案是加锁,用一个loading标志位判断当前是否已在请求中,未完成前不再发请求。

搜索功能支持两个维度:关键词搜索和分类筛选。关键词匹配的是岗位名称和岗位描述,分类则是一级分类(家教、餐饮、促销、实习等)。这两个条件在后端拼成动态 SQL,前端只需要把参数拼到请求里。排序上除了默认推荐排序,还支持按发布时间、薪资待遇排序。

详情页就比较简单了,展示岗位描述、薪资、工作地点、时间要求、联系人信息,底部是"立即投递"按钮。投递后按钮变灰,后端做幂等校验防止同一个学生重复投递同一个岗位。

4. Spring Boot 后端核心模块拆解

4.1 数据库表设计与关键字段说明

数据库是这个系统的根基,我设计了十张核心表,按业务域分组:

表名用途关键字段
tb_user用户账号openid, role, phone, status
tb_student学生扩展信息user_id, school, major, grade, district_id
tb_employer企业扩展信息user_id, company_name, credit_code
tb_job兼职岗位title, category, salary, workplace, job_time, head_count, status
tb_resume学生简历user_id, skills, self_intro, free_time
tb_application投递记录job_id, student_id, status, apply_time
tb_collection收藏记录job_id, student_id, create_time
tb_feedback评价反馈application_id, rating, comment
tb_notification通知消息user_id, content, type, read_flag
tb_category职位分类parent_id, name, icon

其中tb_job的status字段控制岗位状态:0待审核、1招聘中、2已招满、3已下架。企业发布的岗位默认是待审核状态,管理员在后台审核通过后才会在前端露出。这个状态机设计让平台对信息质量有控制力,避免学生看到一堆未经验证的兼职信息。

head_count字段值得一提。它表示岗位总招聘人数,每次有人被录用就扣减。这个字段不是简单的int而是设置了version乐观锁字段,防止并发投递时超录。具体实现后面详述。

4.2 用户画像与岗位标签化处理

推荐系统能跑起来的前提是数据要结构化。我给每个岗位打标签,标签分为两类:一类是"硬属性",如工作地点、工作时段、薪资范围,另一类是"软技能",如沟通能力、文字功底、编程能力、设计能力。硬属性可以直接通过字段匹配,软技能则在发布岗位时由企业勾选。

学生的数据同样做标签化。注册时让学闹勾选自己的技能标签(最大选 5 个),完善简历时填写空闲时间段(比如每周二下午、周六全天),同时后台通过学生账号的学校信息自动计算所在校区。这部分数据是推荐系统的输入,如果学生不填,推荐效果会很差,所以前端在注册流程中把它设计为必填步骤,没有完整简历的学生进入首页时会被引导去完善资料。

这个设计在实操中有一个改进点:不要让学生填太多选项,每类问题控制在 5 个以内,否则用户会失去耐心。我第一版问卷里技能标签列了 15 个,测试时有学生反馈"选不过来",后来精简到两类各 5 个选项,完成率明显上来了。

4.3 推荐算法落地:基于标签匹配与行为反馈的加权排序

推荐模块是技术含量最高的部分,也最容易写过头。我没有选择一开始就上协同过滤或者深度学习模型,而是先用一个可解释、可调试的标签加权算法跑通全流程,后续有数据积累再迭代。

推荐得分由三部分组成:

推荐得分 = 标签匹配分(60%) + 行为反馈分(30%) + 新鲜度加分(10%)

标签匹配分:学生标签与职位标签的交集数量,每个命中标签得 20 分,满分 100 再乘 0.6 权重。行为反馈分:如果学生收藏过同分类的职位,加 30 分;投递过同类的职位,加 50 分;被企业录用过同类职位,加 70 分。这部分模拟"用户对这类岗位的真实偏好"。新鲜度加分:岗位发布时间在 3 天内加 10 分,7 天内加 5 分,超过 15 天不加分。

计算逻辑放在 service 层,先用 SQL 查出当前用户的标签和空闲时间,再查出所有招聘中的岗位,在内存中计算得分并按分数降序排列。首次跑通后我做了评测:随机选了 50 个学生,让系统各推荐 10 个岗位,然后人工核对合理性。刚开始发现一个严重问题——位置偏远但标签全中的岗位排序太靠前,学生根本不会去。于是我又加入一个距离惩罚因子:如果岗位地点与学生校区距离超过 5 公里,总分乘以 0.7。加上这个约束后,推荐结果的合理性提升了一大截。

4.4 投递流程与通知触达的实现

投递记录表tb_application的状态流转是:已投递 -> 已查看 -> 已录用 / 已拒绝。企业在后台点击"录用"时,后端除了更新投递状态,还要做两件事:扣减岗位head_count,生成一条通知写入tb_notification表。

学生端消息页的消息列表从哪里来?就是查tb_notification表。这里没有用 WebSocket 实时推送,因为第一版需求没有强实时性要求,学生在消息页下拉刷新或者进入页面时拉取最新通知即可。微信小程序有一个订阅消息能力,可以实现审核结果、录用结果的通知触达,但需要用户主动订阅且每次订阅只能推送一次。我在投递按钮上加了一个订阅请求,用户点击投递时弹窗让用户勾选"允许发送录用通知",这样可以做到录用结果的实时触达,是一个体验提升点。

5. 微信小程序特有的工程化与兼容性处理

5.1 微信登录与 Spring Boot 令牌机制的对接

这一节容易踩坑,单独拿出来说。微信小程序的登录流程:

  1. 前端uni.login获取临时 code
  2. 前端把 code 通过uni.request发送到后端POST /api/auth/wx-login
  3. 后端调用微信接口https://api.weixin.qq.com/sns/jscode2session,传 appid、secret、code
  4. 微信返回openid和session_key
  5. 后端用openid查用户,没有则创建,有则更新最后登录时间
  6. 后端生成一个 UUID token,存入 Redis 并设置过期时间,返回给前端
  7. 前端把 token 存入uni.setStorageSync('token', token)

后续所有请求在uni.request的 header 中带上 token。Spring Boot 端用一个拦截器JwtInterceptor,检查 header 中的 token 是否在 Redis 中存在且未过期,不存在返回 401,前端收到 401 后清理本地 token 并跳转登录页。

这个链路里最容易出问题的点:一是code只能用一次,如果前端在请求超时后重试,同一个 code 传给后端两次,第二次一定失败;二是session_key不能暴露给前端,它涉及微信敏感数据的解密,必须留在后端。

5.2 网络请求封装与多环境域名切换

UniApp 开发环境和生产环境的接口地址不同,这是多环境域名问题的根源。我把request封装为一个公共模块,用环境变量控制 baseURL。

// utils/request.js const BASE_URL = { development: 'http://127.0.0.1:8080/api', production: 'https://api.example.com/api' } function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL[process.env.NODE_ENV] + path, method, data, header: { 'Authorization': 'Bearer ' + uni.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data) } else { uni.showToast({ title: res.data.message, icon: 'none' }) reject(res.data) } }, fail: (err) => reject(err) }) }) }

开发环境可以在微信开发者工具里勾选 "不校验合法域名",但上线前必须在微信公众平台配置 request 合法域名,且必须是 HTTPS 且完成备案。这个流程不复杂,但经常被忽略,导致真机预览时所有请求都报fail url not in domain list。另外在 H5 端做多域名切换时,也可以在 manifest.json 的 h5 配置里修改 devServer.proxy 来代理接口,减少跨域问题。

5.3 页面性能优化与缓存策略

小程序包体积限制是 2MB(主包),超过就需要分包。我第一版把所有页面都放主包,打包后发现体积 3.2MB,提示超限。后来把消息模块和反馈模块拆成独立分包,主包减到 1.6MB。分包加载确实会有一点切换延迟,但能接受。

图片资源是体积大头,项目里做了三件事控制:所有图片压缩后上传到对象存储,不打包进小程序;列表页的图片懒加载,使用微信小程序的lazy-load属性;icon 统一用 iconfont 字体图标,而不是切图。

列表页的数据缓存也做了处理:首页推荐列表存入本地缓存,缓存时间 5 分钟,学生打开小程序时先渲染缓存,再请求新数据。这样既保证了首屏速度,又不会让数据太旧。uni.setStorageSync存的是 JSON 字符串,读出来时要JSON.parse,这个 API 的同步阻塞在小数据量时没什么感觉,但注意不要存大数据。

6. 测试结果与上线阶段发现的几个问题

6.1 并发场景下的名额超录问题

测试阶段最让我头疼的是名额超录。模拟 200 个学生同时投递一个仅剩 5 个名额的岗位,居然有 23 个人投递成功,远超预期。排查后发现是扣减head_count的 SQL 写法有问题。

最初的代码是:

// 先查出当前名额 Job job = jobMapper.selectById(jobId); if (job.getHeadCount() > 0) { // 扣减 job.setHeadCount(job.getHeadCount() - 1); jobMapper.updateById(job); // 创建投递记录 }

这段代码在单线程下没问题,但高并发下多个请求同时读到head_count > 0,同时执行扣减,就会超录。解决方案是两层保护:投递表加唯一索引(student_id + job_id),保证同一个学生不能重复投递;名额扣减改用乐观锁更新:

UPDATE tb_job SET head_count = head_count - 1, version = version + 1 WHERE id = #{jobId} AND head_count > 0 AND version = #{version}

如果更新影响行数为 0,说明名额已被抢完或者版本冲突,直接返回"岗位已满"。改造后我用 JMeter 再次测试 200 并发,投递成功数正确落在 5 以内。

6.2 推荐冷启动问题的临时方案

系统刚上线时,每个学生都是新用户,没有任何行为数据,行为反馈分全部为 0,推荐结果完全由标签匹配和新鲜度决定。结果就是所有学生看到的推荐列表几乎一样,没有个性化差异。

这个问题的本质是冷启动。我没有从头去研究复杂的冷启动方案,而是用了两个临时手段过渡:一是上线前从学校就业办导入了过往几届学生的兼职偏好数据,给每个新学生的标签相似度做个初始画像;二是推荐列表的每个分类下保留一个固定位给"精选岗位",由平台管理员手动推荐,保证即使算法还没跑起来,首页也不会是空的。

过渡手段虽然土,但在冷启动阶段是有效的,之后随着用户行为数据积累,行为反馈分的权重才逐渐发挥作用。

6.3 慢查询定位与索引优化

系统运行两周后,职位列表页的响应时间出现了明显劣化,高峰期超过 4 秒。查看 MySQL 慢查询日志,发现一个高频慢 SQL 是tb_application表按student_id查投递记录,这个表数据量只有几千行,但全表扫描耗时居然超过 1 秒。原因是student_id字段没有建索引。

这类问题的排查思路值得分享:先开启 MySQL 慢查询日志,找到long_query_time超过阈值的 SQL,然后EXPLAIN分析 SQL 的执行计划,看 key 是否为 NULL(代表全表扫描)。两个高频操作都加了索引:

  • tb_application表的student_id加普通索引
  • tb_job表的status + category加联合索引,因为列表页筛选最常走这两个字段

优化后同样接口的响应时间从 4 秒降到 300 毫秒以内。这提醒我,索引不是可有可无的,尤其在数据量增长之后,没有索引的查询会变成系统的定时炸弹。

6.4 测试过程中发现的移动端兼容性问题

最后说说兼容性。我用 UniApp 编译到微信小程序、H5、Android App 三端分别测试,发现了一些差异:

  • uni.showToast在 H5 端默认显示时间比小程序短,需要显式设置 duration
  • 部分 CSS 的position: fixed在 iOS 小程序端会出现遮挡底部导航栏的问题,需要用bottom: calc(env(safe-area-inset-bottom))适配
  • Android 端使用的是 X5 内核,border-radius在个别低版本机型上会失效,用overflow: hidden包裹可以规避
  • 使用canvas绘制分享图片时,iOS Safari 上有时导出白图,需要把 canvas 的宽高设置成两倍物理像素

移动端兼容性的核心经验是:不要只看微信开发者工具里的效果,一定要在至少一台 Android 真机和一台 iOS 真机上测试,尤其是涉及底部安全区、键盘弹出、图片上传这些交互时,开发者工具的模拟器和真机的行为差异非常大。

对我来说,这个项目给我最重要的收获不是某个具体技术,而是一个完整系统的工程化思维:前端不是单纯的页面堆叠,后端不是单纯的增删改查,推荐算法也不是孤立的模型。它们被一个核心业务目标串联起来:让大学生更快找到合适的兼职,让企业更快找到靠谱的学生。每一次技术选型、每一处代码优化,都是在为这个目标服务。如果你也要做一个类似的全栈项目,建议先把这个业务闭环理清楚,再去动代码,你会发现后面的路顺畅得多。

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

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

立即咨询