拿到“大学生就业平台微信小程序”这个项目需求时,我第一反应倒不是技术难度,而是用户场景太杂。大学生求职这件事,平时看着是投简历、等面试,实际上牵扯到就业办、辅导员、企业HR、应届生、实习生甚至家长,信息链路特别长。如果只做一个岗位列表,那和普通招聘网站没有区别。这个项目的价值点在于:把学校就业信息、企业招聘入口、学生简历投递和面试进度管理,压缩进一个微信小程序里,学生扫码即用、企业端统一管理,就业办只做审核和看数据。项目代号叫 weixin116,我按这个思路从需求拆解做到上线,整个周期大概六周。如果你手里也接了类似的校园招聘需求,或者想做一个轻量级垂直招聘工具,这篇笔记应该能让你少走不少弯路。
1. 项目整体定位与需求拆解
1.1 这不是一个公开招聘网站
做需求拆解时最容易犯的错,是直接套用市面上的招聘App功能:职位搜索、简历上传、聊天沟通、职位推荐。问题是,大学生就业场景和社招场景有本质区别。社招看的是“匹配”,学生求职看的是“机会”和“流程”:哪个企业来宣讲、哪些岗位招应届生、简历投出去之后有没有被查看、什么时候面试、结果怎么样。
所以我在需求文档里把核心目标写得很简单:让学生用最少步骤找到岗位并完成投递,让企业用最短路径筛选简历,让学校能看到整体的就业进度。一个“社交聊天式招聘”在学生场景里并不实用,大家更需要的是一条清晰可追踪的求职链路。
也因为这个定位,第一版砍掉了不少看上去很酷的功能。在线聊天先不做,智能简历解析先不做,算法推荐先不做。先用一个足够稳的信息流把用户循环跑通,再谈增强功能。小程序的开发和审核成本虽然不算高,但是用户心智成本很高:如果你的页面让一个没接触过的学生感到困惑,他下次大概率不会再打开。
1.2 用户角色与核心流程
这个项目涉及三类角色,每类角色的核心诉求差异很大。
| 角色 | 核心诉求 | 在系统中的操作 |
|---|---|---|
| 学生 | 快速找到实习/校招岗位,知道投递结果 | 浏览岗位、搜索筛选、在线填简历、一键投递、查看进度 |
| 企业HR | 发布岗位,高效筛选候选人 | 提交企业入驻资料、发布职位、查看简历、标记面试状态 |
| 学校就业办 | 审核企业资质,掌握就业数据 | 审核企业入驻、审核职位内容、查看投递统计数据 |
学生端的核心流程是“看岗位 -> 投简历 -> 等消息”。企业端的核心流程是“入驻 -> 发岗位 -> 收简历 -> 邀面试”。两个流程中间靠一张投递记录表连接,学校端则通过这张表的统计结果做数据展示。
这里我特别想强调一个容易忽略的细节:“简历”在学生场景里不能一上来就让用户去传PDF文件。实操中你会发现,很多学生手头根本没有一份像样的简历,或者有也是针对考研、出国准备的,和招聘岗位完全不匹配。所以系统要做一份结构化的在线简历:基本信息、教育经历、技能标签、自我评价。这样学生第一次使用可以在十分钟内填完,投递时企业也能看到统一格式的信息,后端做筛选排序也方便。
2. 技术选型与项目架构
2.1 应用端框架:原生小程序和跨端方案怎么选
当时在原生小程序和跨端框架之间犹豫过。跨端方案的优势是以后可以顺带输出一个App,一套代码多端运行,听起来很划算。但我的结论是:如果需求只明确做微信小程序,直接选原生开发。
原因很简单:原生小程序调用微信登录、订阅消息、云开发这些能力时,路径是最短的。跨端框架虽然能打包到微信,但遇到底层API的差异时,最后还是得写条件编译或者自定义插件,排错成本比省下的那点工作量高得多。开发周期只有六周,我不想把时间花在“适配差量”上。
原生小程序的结构也很清晰:
WXML负责页面结构;WXSS负责样式;JavaScript负责业务逻辑和API调用;JSON负责页面配置。
对于职业生涯中的第一个完整项目,用小程序原生开发还能把每个页面拆得很干净:一个页面一个目录,页面逻辑、样式、配置互不干扰。等到后期如果真需要App,再单独做接口对接也不迟。
2.2 后端、数据库与存储方案
后端我选了微信云开发。说实话,对于这种校园就业平台,自己做Node.js后端完全可以,但会多出服务器部署、域名备案、HTTPS证书、接口维护这些一系列工作。对一个小团队来说,这些不是核心竞争力,反而会拖慢节奏。微信云开发把“云函数 + 云数据库 + 云存储”三件事直接接到小程序里,学生端和企业端的数据读写能统一走云函数,省掉了一整套中间层。
数据库用文档型,天然适合这种数据结构不固定的业务。比如一个岗位可能有“薪资范围”、“工作地点”、“岗位标签”,企业入驻时还可能有“公司Logo”、“营业执照照片”,字段类型不强求统一,用文档型比关系型更舒服。更关键的是,小程序端可以直接通过云数据库查询,调试阶段效率非常高。
云存储则用来放三类文件:企业Logo、企业的资质照片、学生上传的附件简历。文件读写权限可以通过云存储的安全规则控制,不允许其他人随便读取业务文件。
2.3 项目目录与数据模型设计
实际项目里,我按页面和业务把目录拆成了下面几块:
miniprogram/ pages/ jobs/ // 岗位列表、岗位详情 resume/ // 在线简历编辑 application/ // 我的投递记录 company/ // 企业入驻、职位发布 user/ // 个人中心 components/ job-card/ // 岗位卡片组件 empty-state/ // 空状态组件 cloudfunctions/ login/ // 登录并获取用户角色 job/ // 职位查询和状态变更 application/ // 简历投递与进度更新 company/ // 企业入驻与审核数据库里最关键的四张表:
users:用户基础信息,包含openid、用户角色、学校、专业、毕业年份;jobs:岗位信息,包含企业ID、职位名称、岗位类别、城市、薪资、状态;applications:投递记录,包含岗位ID、学生openid、投递状态、创建时间;companies:企业信息,包含企业名称、资质照片地址、联系人、审核状态。
这个数据模型的主线就是:“学生投递后,在 applications 表里生成一条记录,企业端根据这条记录去读取学生信息,学校端根据这条记录做统计。” 其他所有功能都围绕这张表展开。
3. 核心功能模块与实操实现
3.1 微信登录与身份绑定
小程序登录不推荐前端直接拿code去换用户身份,而是直接走云函数。云函数里通过cloud.getWXContext()拿到用户的OPENID,这个标识是整个平台识别唯一用户的核心依据。
登录云函数的实现可以精简成:
const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { const { OPENID } = cloud.getWXContext() const db = cloud.database() const userRes = await db.collection('users') .where({ openid: OPENID }) .get() if (userRes.data.length > 0) { return { code: 0, data: userRes.data[0] } } await db.collection('users').add({ data: { openid: OPENID, role: 'student', createTime: db.serverDate() } }) return { code: 0, data: { role: 'student' } } }这里有个实操细节:第一次登录时不要急着弹一堆授权框。微信从隐私保护政策升级之后,像手机号、位置、相册这些能力都需要用户单独授权,如果一次性全弹,用户大概率会直接关掉小程序。正确做法是先静默登录,建立账号,然后引导用户进入“完善个人资料”页面,需要手机号时再点击“获取手机号”按钮,需要上传头像时再触发照片选择。每一步授权都要让用户知道为什么需要,界面上一句话讲清楚。
学生身份绑定是另一个容易踩坑的地方。很多人做完登录以后,发现数据库里有几十个openid,但不知道这些用户对应的是谁。所以我在登录后的第一个表单里强制收集四项信息:姓名、学校、专业、毕业年份。这一步必须在用户还没产生“这是不是骗子应用”疑虑前完成引导,尽量让表单不超过一屏。
3.2 岗位信息流与筛选服务
岗位列表页是这个项目里流量最集中的页面。我没有把“招聘职位”和“宣讲会信息”混在一起,而是分成两个Tabs:一个是岗位信息流,一个是活动资讯。这样学生进来之后马上就知道去哪里找什么。
岗位信息流的设计思路是:默认展示最近发布且审核通过的职位,支持按“实习/校招”、“城市”、“岗位类别”筛选,按“最新”和“热门”排序。后台查询时,把状态不是已发布的记录挡在列表之外,避免学生看到还没审核或者已经下架的岗位。
查询逻辑大致是这样:
const db = cloud.database() const _ = db.command exports.main = async (event) => { const { page = 1, pageSize = 20, city, category, jobType } = event const whereOption = { status: 'published' } if (city) whereOption.city = city if (category) whereOption.category = category if (jobType) whereOption.jobType = jobType const res = await db.collection('jobs') .where(whereOption) .orderBy('publishedAt', 'desc') .skip((page - 1) * pageSize) .limit(pageSize) .get() return { code: 0, data: res.data } }这里我要提醒一个容易被忽略的性能问题:云数据库单次limit最大只能到 20,超过这个数会被截断。所以千万别直接limit(100),正确做法是前端做触底分页,每页加载 20 条,用skip跳过前面的数据。用户行为上,学生滑到底部的次数不会太多,20条数据也够撑过两屏。
岗位详情页除了展示职位描述和公司信息外,底部操作区放两个按钮:一个是“立即投递”,一个是“收藏职位”。收藏功能看似简单,但很影响回访率。学生可能一时拿不定主意,先收藏,等再打开小程序的时候从“我的收藏”进入,转化路径短很多。
3.3 在线简历与投递闭环
在线简历是整个平台里“看起来简单但其实最需要设计”的模块。我把它拆成三个区块:基本信息、教育经历、技能和自我评价。每个区块在界面上用分步表单引导,而不是让用户一次看到十几个输入框。
基本信息字段包括:姓名、性别、出生年份、手机号、邮箱、所在城市。教育经历字段包括:学校、学历、专业、入学年份、毕业年份。技能和自我评价部分,我用标签式输入:预先提供常见技能标签,也允许用户自定义添加。这样企业端看简历时,能一眼扫到关键词,而不是从一大段文字里自己找信息。
投递动作的核心逻辑是一个云函数,主要负责两件事:查重和写入记录。查重是防止同一个学生对同一个岗位重复投递。
exports.main = async (event) => { const { jobId } = event const { OPENID } = cloud.getWXContext() const db = cloud.database() const existed = await db.collection('applications') .where({ jobId, studentOpenid: OPENID }) .count() if (existed.total > 0) { return { code: 1, msg: '你已经投递过这个岗位了' } } await db.collection('applications').add({ data: { jobId, studentOpenid: OPENID, status: 'submitted', createTime: db.serverDate(), updateTime: db.serverDate() } }) return { code: 0, msg: '投递成功' } }这里比较关键的字段是status,我用一组固定值来标记整个求职阶段:submitted已投递、viewed已被查看、interview面试邀请、accepted录用、rejected不合适。学生端“我的投递”页面,按这个状态展示不同颜色的进度标签,让学生清楚知道自己的简历走到哪一步。
投递成功之后,是一个容易被忽略的体验点:好多学生投完之后就断了消息。所以投递成功后,要引导用户确认开启“面试通知”的订阅消息授权。注意,小程序的订阅消息是一次性授权,用户同意了也只能发一次;如果后续状态变化还要再发,就必须在用户每次点击按钮时再申请一次。实操中我的做法是在投递按钮的open-type="subscribe"上绑定模板ID,学生点击投递的一瞬间顺便完成授权,一举两得。
3.4 企业管理端与审核流
企业端我放在了同一个小程序里,用角色去区分入口。学生登录默认看到学生页面,企业用户登录后能看到“发布职位”和“候选人管理”两个扩展入口。这样做的好处是避免单独做一个企业端小程序,减少一个审核和发布成本。
企业入驻流程必须卡得严一点,因为平台的公信力全靠企业资质审核撑着。企业提交的资料包括:企业名称、统一社会信用代码、营业执照照片、联系人姓名、联系电话。企业填写完提交后,状态是pending,学校就业办在管理端看到待审核列表,确认资质无误后把状态改成approved。只有状态是approved的企业,才能发布职位。
职位发布表单里,我加了几个必填项:职位名称、岗位类别、招聘类型、城市、薪资范围、职位描述。岗位类别做成固定选项,因为后面要做筛选。职位描述这里用户会复制一大段招聘文案,前端要把textarea的高度放宽,同时后端做好长度限制,避免有人传几万字的“小作文”进来。
候选人管理列表对应企业发布的每个职位,显示投递人数、简历状态、操作按钮。企业HR可以查看学生简历,然后把状态从“已投递”改为“邀面试”。如果学生联系方式不可用,就直接标记“不合适”。这样数据回流到投递表,学生端就能实时看到进度更新。
4. 部署上线与边界情况处理
4.1 隐私协议、用户授权与小程序审核
小程序审核现在最关注的不是功能,而是隐私合规。在开发阶段就要把“用户隐私保护指引”配置好:要声明收集哪些信息、为什么收集、如何使用。如果不填写完整,提审时很容易因为“收集用户信息未声明”被驳回。
实操中我整理了一份隐私相关清单:
- 首次登录需要用户阅读并同意《用户隐私协议》;
- 获取手机号前必须说明用途;
- 上传简历附件前要写明文件用途;
- 使用位置筛选岗位时,要允许用户取消定位权限。
另外,招聘、求职类小程序有一个坑:审核人员会对照开放类目检查资质。校园项目如果挂在某个学校就业办主体下,通常需要提供学校相关的证明材料;如果是商业运营,大概率会让你补充人力资源服务资质。这个合规问题最好在开发前就和主体方确认清楚,不要等提交审核被拒再补材料。
4.2 云数据库权限与分页
云开发默认提供的数据库权限有“仅创建者可读写”、“所有用户可读”等选项。我的建议是:所有业务表都不应该直接开“所有用户可读写”权限。学生如果直接通过客户端写数据库,理论上可以篡改别人的简历、伪造投递记录。正确的做法是把数据库权限全部收紧,数据操作通过云函数完成,因为云函数在服务端执行时不受客户端读写权限限制,能通过代码自己校验用户身份。
权限规则可以这样理解:客户端能查到的,只有“公开信息”,比如已发布职位、已通过审核的企业信息;其余所有与身份绑定的读写,全部走云函数。这样既保证学生体验流畅,又避免明显的安全漏洞。
分页问题前面说过了,云数据库单次查询有上限。此外,查询记录量大了之后,skip方式会有性能问题,真正的生产环境可以考虑用“游标分页”,也就是根据上一次返回的最后一条记录的ID继续往后查。这个优化可以放在二期中做,第一版先把用户主流程跑通更重要。
5. 踩坑记录与调试心得
5.1 常见问题排查速查表
开发周期里遇到的问题远比预期多,很多问题看起来奇怪,其实原因很常规。我把几个典型问题整理成了下面的速查表,方便你排查时对照。
| 问题现象 | 真正原因 | 解决方法 |
|---|---|---|
| 用户登录后用户表里查不到记录 | 云函数还没调用成功,或者OPENID字段名不一致 | 在云函数里把返回参数打日志,检查getWXContext()返回值 |
| 前端查不到数据,但云数据库控制台里有内容 | 数据库权限设置限制了客户端读取 | 把该查询改成云函数,或者调整安全规则 |
| 投递成功但收藏列表不显示 | 收藏入参和查参的字段名不一致 | 统一定义字段名,前端和云函数都用jobId |
| 用户授权了订阅消息但收不到通知 | 一次性订阅消息只能发送一次,或者用户未点击授权按钮 | 确保每次需要通知时都重新拉起订阅授权 |
| 上传图片一直失败 | 图片太大或后端存储路径权限不对 | 前端设置compressed: true,控制上传前大小 |
| 真机上点击按钮没反应,开发工具却正常 | 某些 API 在真机需要用户在手势事件内调用 | 日志检查报错,确认调用的API是否需要非异步回调中触发 |
5.2 我反复提醒自己的几个习惯
这个项目做下来,我最大的体会是:校园工具的可用性,比功能数量重要得多。学生用户没有耐心去研究你的页面,他只想三步之内看到结果。所以每次加功能前,我会先问自己一个问题:这个功能能不能让用户更大概率把简历投出去?如果答案是否定的,就先不做。
代码层面的习惯也很重要。所有时间字段我都用数据库的db.serverDate(),而不是用本地时间字符串。因为本地时间可能因为设备时区不同出现错乱,当学校端要看“今日投递量”的时候,数据就会对不上。这个问题在开发工具里不明显,一旦有学生用了不同时区或不同系统版本,立刻暴露。
还有一个很实用的习惯:后端云函数尽量保持“单一职责”。比如处理投递的云函数不要顺手把“统计投递数量”也做了。云函数冷启动时间虽然不慢,但一个函数里逻辑越复杂,出错时越难排查。我在二期中准备把通知发送、数据统计、简历附件解析拆成独立函数,这样后续维护和扩容都方便。
对企业端的体验,我也有一个观察:企业HR不会像学生那样高频使用小程序,所以企业端一定要减少“一次性操作”的成本。比如发布职位时,把常用的“岗位类别”缓存起来,下一次自动带上;职位描述里自动补充基本信息模板,让HR只需要改中间几段。这些细节做不做,直接影响企业愿不愿意持续发布岗位。
这个项目从需求确认到交付,前后改了不下五轮。最耗时间的不是写代码,而是搞清楚学校就业办到底要什么统计数据、企业HR想看简历里的哪些字段。但我现在回头看,这些反复其实都值。一个大学生就业平台,技术上不难,难的是让每一端的人都觉得好用。后面如果再让我做一个类似的小程序,我一定会先把这几个核心流程设计得更细:从登录到投递不超过一分钟,从企业入驻到发职位不超过一天,从学校看到数据到导出报表不超过一次点击。这比任何花哨的算法推荐都更能决定平台的活跃度。