微信小程序 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。页面结构上使用templatescriptstyle的 Vue 单文件组件结构写过 Vue 的人几乎零成本上手。2.2 后端选型Spring Boot 项目结构怎么搭后端没有太多悬念直接用了 Spring Boot 2.7.x MyBatis-Plus MySQL 8.0。选 MyBatis-Plus 而不是 JPA是因为这个项目的查询场景比较复杂推荐算法需要很多自定义 SQLMyBatis-Plus 的灵活度更合适。项目按模块分包不是传统的按三层架构堆文件而是按业务域划分controller/service/mapper只作为技术层存在业务上分成 user、job、application、recommend、message、admin 六个模块。每个模块有自己的 controller、service、entity、mapper这样后期加功能时定位代码非常快。一个值得说的设计是统一返回值结构。所有接口统一返回ResultT包含 code、message、data 三个字段前端用同一套逻辑处理成功和失败。这个看似简单的约定在前后端联调阶段省了大量时间。前端再也不用从各种不同的返回结构里去猜当前是成功还是失败。2.3 接口约定与推荐服务的衔接设计前后端接口约定上考虑到推荐模块本身是从职位列表上叠加的一层逻辑我没有把推荐做得太重。做法是列表接口GET /api/job/list支持传入recommendtrue参数后端在推荐模块开启时走推荐排序逻辑关闭时走普通的分页时间排序。这样一是方便对照验证推荐效果二是即便推荐引擎出问题列表功能依然可用。接口设计上还有一个关键点所有接口统一走/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, statustb_student学生扩展信息user_id, school, major, grade, district_idtb_employer企业扩展信息user_id, company_name, credit_codetb_job兼职岗位title, category, salary, workplace, job_time, head_count, statustb_resume学生简历user_id, skills, self_intro, free_timetb_application投递记录job_id, student_id, status, apply_timetb_collection收藏记录job_id, student_id, create_timetb_feedback评价反馈application_id, rating, commenttb_notification通知消息user_id, content, type, read_flagtb_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 令牌机制的对接这一节容易踩坑单独拿出来说。微信小程序的登录流程前端uni.login获取临时 code前端把 code 通过uni.request发送到后端POST /api/auth/wx-login后端调用微信接口https://api.weixin.qq.com/sns/jscode2session传 appid、secret、code微信返回openid和session_key后端用openid查用户没有则创建有则更新最后登录时间后端生成一个 UUID token存入 Redis 并设置过期时间返回给前端前端把 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 真机上测试尤其是涉及底部安全区、键盘弹出、图片上传这些交互时开发者工具的模拟器和真机的行为差异非常大。对我来说这个项目给我最重要的收获不是某个具体技术而是一个完整系统的工程化思维前端不是单纯的页面堆叠后端不是单纯的增删改查推荐算法也不是孤立的模型。它们被一个核心业务目标串联起来让大学生更快找到合适的兼职让企业更快找到靠谱的学生。每一次技术选型、每一处代码优化都是在为这个目标服务。如果你也要做一个类似的全栈项目建议先把这个业务闭环理清楚再去动代码你会发现后面的路顺畅得多。