县区级的就业创业服务平台,听起来像是个大工程,但真正从零做完回头看,核心其实就三件事:求职者能找到工作、创业者能申报政策、运营人员能高效审核。我负责的这个项目,技术栈最后落在微信小程序 + Python + Android,交付仓库代号是_926q2b4。选这三样不是跟风,而是县区场景下最现实、最稳的组合。这篇复盘我会把从选型、建模、接口设计到小程序端和Android端实现、再到线上排障的完整过程摊开讲,涉及大量可以直接抄走的表结构、接口定义和避坑经验。如果你正在做政务类小程序、地方人才平台、校企对接系统,或者类似的需要“C端小程序 + 管理端App”的项目,这篇应该能帮你少走不少弯路。
1. 项目整体设计与技术选型
1.1 三层技术栈的分工逻辑
这个项目最容易被误读的地方,是一看到标题里有“Android”就以为要做安卓原生App给求职者用。实际完全不是这样。微信小程序承担的是C端用户入口,就业创业服务里的求职者、创业者,拿起手机扫一扫就能用,不需要安装任何东西。后端接口服务用Python来写,负责用户认证、业务逻辑、数据存储和审核流程。而Android端,其实是我留给就业服务中心运营人员的移动审核台。
为什么用户端不用原生App?县域场景下,用户的手机配置参差不齐,很多人没有装额外App的习惯,让他们为了找工作专门下载一个应用,转化率非常难看。微信小程序用完即走,还能通过公众号、社群、扫码等多种方式扩散,对中老年用户更友好。为什么后端选Python而不是Java或Node?团队熟悉度是一方面,另一个重要原因是FastAPI这套东西开发效率确实高,自带OpenAPI文档,小程序端和Android端都靠它做接口联调,文档自动生成省掉了大量写接口说明的时间。
Android端是这个项目里容易被低估的一环。县区就业服务很多工作在线下发生:工作人员要去招聘会现场做企业签到、到乡镇街道走访、帮年龄大的求职者代传纸质材料、现场核验企业资质。这些场景抱着笔记本电脑根本跑不动,但拿一部Android手机加一个轻量客户端,拍照、定位、填表、提交审核,一步到位。所以三层技术栈的分工,本质是“用户轻、后端快、管理端便携”。
1.2 县区场景下的需求池梳理
县域就业创业平台和市级以上平台有个明显区别:用户群体更复杂,业务链条更短,但线下服务权重更高。我一开始照着招聘网站的模板设计,简历投递、职位搜索做得花里胡哨,结果被用户反馈打醒——县里的人用平台就关心三件事:有没有离家近的活、补贴怎么申请、培训在哪里报名。
最后整理出来的需求池大概是这样的结构:
| 业务域 | 核心功能 | 优先级 | 说明 |
|---|---|---|---|
| 就业服务 | 岗位发布与搜索 | P0 | 企业发布岗位,用户按区域、薪资、工种筛选 |
| 就业服务 | 简历投递 | P0 | 用户维护简历,一键投递,支持附件上传 |
| 就业服务 | 招聘会报名 | P1 | 线下招聘会活动列表、在线报名、签到码 |
| 就业服务 | 技能培训 | P1 | 培训机构入驻、课程发布、报名记录 |
| 创业服务 | 创业政策展示 | P0 | 政策原文展示、适用条件拆解 |
| 创业服务 | 补贴申报 | P0 | 创业补贴、场地补贴等在线申报与审批 |
| 创业服务 | 孵化基地导航 | P2 | 本地创业园、孵化器信息与地图位置 |
| 管理后台 | 企业与岗位审核 | P0 | 运营人员审核企业资质、岗位合规性 |
| 管理后台 | 申报材料审批 | P0 | 逐级审批申报材料、驳回并填写理由 |
| 管理后台 | 数据统计 | P1 | 岗位数、投递数、申报数、审核时效 |
这里有个隐形需求容易被忽略:代填帮扶。县里很多求职者年龄偏大,手机操作不熟练,运营人员需要能“替用户提交”简历或申报材料。这个需求我在Android端做了专门入口,后面细讲。
1.3 开发路线的先后顺序
项目推进顺序是按照“数据先行、接口次之、前端最后”来排的。第一步先把数据库表结构定死,第二步用Python把核心接口写完,第三步小程序端和Android端并行开发。为什么要这个顺序?因为小程序和Android都是接口的消费者,接口语义稳定了,两头才不会返工。
我见过太多项目先做页面再做接口,页面做完了发现接口对不上,又回头改后端。县区项目周期紧、人手少,经不起这种反复。实际操作中,我用FastAPI把接口定义好之后,直接把自动生成的OpenAPI文档地址发给小程序端开发,让他们对着文档自己mock数据调页面,这时候后端再去实现细节,两边互不阻塞。
2. Python后端的骨架搭建
2.1 框架与工程结构
后端我选了FastAPI + SQLAlchemy + MySQL + Redis这套组合。FastAPI负责路由和参数校验,SQLAlchemy做ORM,MySQL存业务数据,Redis缓存登录态和短信验证码。为什么不用Django?县区项目业务体量没那么大,Django自带Admin和ORM虽然方便,但框架太重,前后端分离的模式下很多内置能力用不上。FastAPI的异步支持在接口并发上更从容,而且Pydantic的参数校验能省掉大量手工判断。
工程结构我是这样的:
app/ ├── main.py # 应用入口 ├── config.py # 配置读取 ├── database.py # 数据库连接与会话 ├── models/ # SQLAlchemy模型 │ ├── user.py │ ├── enterprise.py │ ├── job.py │ ├── resume.py │ ├── policy.py │ └── application.py ├── schemas/ # Pydantic请求响应模型 ├── api/ │ ├── v1/ │ │ ├── auth.py # 登录接口 │ │ ├── jobs.py # 岗位接口 │ │ └── applications.py # 申报接口 │ └── admin/ │ └── reviews.py # 审核接口 └── core/ ├── security.py # JWT工具 ├── oss.py # 文件存储 └── deps.py # 依赖注入这个结构把普通用户接口和管理员接口从一开始就分开了,避免后面权限越界。管理接口统一挂在/admin/api前缀下,中间件里单独做管理员身份校验,和用户侧JWT是两套逻辑。
2.2 核心数据表的字段设计
这一块是整个项目的地基,设计不好后面全是坑。我挑几个核心表说说。
用户表(user):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| openid | VARCHAR(64) | 微信openid,唯一索引 |
| unionid | VARCHAR(64) | 公众号unionid,可空 |
| phone | VARCHAR(20) | 手机号,可空,用户未授权时为空 |
| real_name | VARCHAR(50) | 真实姓名 |
| id_card | VARCHAR(18) | 身份证号,脱敏存储 |
| avatar | VARCHAR(255) | 头像URL |
| role | TINYINT | 0普通用户,1企业用户,2运营人员 |
| status | TINYINT | 0禁用,1正常 |
岗位表(job):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| enterprise_id | BIGINT | 所属企业ID |
| title | VARCHAR(100) | 岗位名称 |
| salary_min | INT | 最低薪资 |
| salary_max | INT | 最高薪资 |
| education | VARCHAR(20) | 学历要求 |
| address | VARCHAR(255) | 工作地址 |
| lng / lat | DECIMAL(10,7) | 经纬度 |
| status | TINYINT | 0下架,1上架 |
| audit_status | TINYINT | 0待审核,1通过,2驳回 |
| view_count | INT | 浏览次数 |
企业表(enterprise)里有个字段得多说一句:credit_code是统一社会信用代码,这在县区平台里是高频查询字段,很多企业用户不知道自己企业在平台注册过没有,运营人员经常用信用代码反查。所以这个字段我一律加索引。申报表(application)我用了JSON字段存表单数据,因为不同政策类型的申报表单长得不一样,用关系表去建模会把简单问题复杂化。JSON存一份、状态机管审核流程,既灵活又够用。
2.3 接口权限与登录态设计
登录态用的是JWT。流程是:小程序端wx.login拿到code,传给后端的/auth/login接口,后端拿code向微信接口换openid,签发JWT返回给小程序。用户后续请求带上Token,后端解出来就知道是谁了。
这里有个细节:用户没有授权手机号之前,JWT照常签发,openid也能标识用户身份,但很多业务是不允许未绑定手机号用户操作的。所以我设计了两个token状态:一个是临时登录态,只允许浏览;一个是完整登录态,绑定手机号后才能投递简历和申报。这样既不会因为用户拒绝授权就把人挡在门外,又能保证关键业务的可追溯性。
核心接口列个清单:
| 接口 | 方法 | 说明 |
|---|---|---|
| /api/v1/auth/login | POST | 微信code换JWT |
| /api/v1/auth/phone | POST | 绑定手机号 |
| /api/v1/jobs/list | GET | 分页岗位列表 |
| /api/v1/jobs/{id} | GET | 岗位详情 |
| /api/v1/resume | POST | 保存简历 |
| /api/v1/applications | POST | 提交申报 |
| /admin/api/v1/reviews/{id} | POST | 审核通过/驳回 |
3. 微信小程序端的关键实现
3.1 登录与手机号获取的完整链路
微信小程序获取手机号,是这次开发中踩坑最多的地方。先讲链路。小程序端先调用wx.login拿code,然后请求后端登录接口换token。拿到登录态之后,页面里放一个button,设置open-type="getPhoneNumber",用户点这个按钮会弹窗确认授权,allowed之后微信返回一个code,把这个code传给后端,后端用code换手机号并绑定到用户账号。
这里有几个硬性条件,提前不说清楚很容易白干。第一,个人主体的小程序无法调用getPhoneNumber接口,只有企业、政府、媒体等非个人主体才行。第二,小程序类目要匹配,比如“求职招聘”类目需要对应资质。第三,每次按钮点击返回的code只能用一次,后端处理时需要做幂等处理,防止重复请求导致绑定失败。
实操中我还建议做一个“动态手机号验证组件”的降级方案:如果某个用户手机型号或微信版本过低导致授权失败,可以让用户手动输入手机号,再通过短信验证码验证,两条路都能完成绑定。政务场景里用户耐心有限,不能让他们卡在一个页面里反复失败。
3.2 首页信息流、分页加载与顶部导航适配
首页是用户第一眼看到的东西,信息架构不能太复杂。我的做法是顶部轮播Banner,下面是“找工作”“创业政策”“技能培训”“招聘会”四个金刚区入口,再往下是推荐岗位列表。岗位列表使用onReachBottom触底加载更多,接口传page和pageSize参数,后端返回总条数和当前页数据,前端把列表拼接起来。这个分页逻辑看着简单,但有几个细节必须处理。
第一个是加载锁。onReachBottom可能连续触发,如果不加isLoading判断,会重复请求同一页数据。第二个是数据去重。如果后端排序字段不稳定,翻页时可能出现重复数据,所以我要求后端排序加一个唯一字段,比如order by id desc,而不是只按发布时间排。第三个是空态设计。县区岗位总量有限,加载完要显示“没有更多了”,不能一直转圈。
顶部导航栏的适配也值得单独说。小程序默认导航栏在部分安卓机型上会和微信胶囊按钮重叠,标题被挡住。我用wx.getMenuButtonBoundingClientRect拿胶囊位置,动态计算自定义导航栏高度,在iPhone和安卓上分别适配。动态设置标题用的是wx.setNavigationBarTitle,比如用户从“岗位列表”进入“岗位详情”后,把标题改成岗位名称,体验会好很多。
3.3 业务表单的校验与附件上传
简历投递和创业补贴申报,是小程序端表单逻辑最重的两块。简历表单包含姓名、身份证、学历、期望岗位、手机号。身份证是我特别强调校验的字段,前端用正则加校验位算法双重检查,后端用Pydantic的validator再校验一次,双层校验确保脏数据进不了库。移动端键盘类型也必须指定,身份证输入框用type="idcard",数字输入用type="number",否则用户弹出来的是全键盘,输入体验很差。
附件上传走的是wx.uploadFile。图片在上传前先用wx.compressImage压缩,县区用户用4G网络居多,一张原图两三兆传半天还容易失败,压缩到200KB以内体验会直线上升。后端接文件后统一存到OSS,返回URL存数据库。这里注意一个问题:wx.uploadFile的name字段和后端接口接收的字段名必须一致,否则后端收不到文件,这种问题排查起来特别隐蔽。
政策申报我设计成多步骤表单,第一步选申报类型,第二步填主体信息,第三步传证明材料,第四步预览提交。分成四步的好处是每步校验范围小,用户的注意力不会被一个长表单吓退。提交以后进入审核状态机,页面上能看状态流转:待审核、已通过、已驳回,被驳回时展示驳回理由和可修改的入口,不要用户一看到“驳回”就无从下手。
4. Android端在项目里做了什么
4.1 为什么县区平台需要一个原生Android端
前面说了,Android端不是给求职者用的,是给就业服务中心运营人员用的移动审核台。县区就业服务有一个特点:大量工作需要线下完成。招聘会现场要扫码签到、企业入驻需要实地核验营业执照、孤寡老人和不会用手机的群体需要工作人员代填材料。这些场景下,工作人员拿着一部安卓手机就能把事办了。
Android端在原生的壳里主要做了三件事:拍照上传、表单录入、审核操作。很多单位里已经在用企业微信或者内部办公系统,所以我这个Android端做了单点登录的对接,运营人员用自己的工号登录,不用额外记一套密码。拍照上传这里有一个所有Android开发者都会遇到的坑:Android 7.0以后,如果直接用file:// Uri打开相机拍照,会抛FileUriExposedException;Android 10以后分区存储,访问/storage/emulated/0/Android/data目录受限。适配方案是用FileProvider生成content:// Uri,并用系统文件选择器或者MediaStore去拿文件。
4.2 Android端的技术实现重点
Android端的网络层用的是Retrofit + OkHttp,接口定义和后端OpenAPI文档一一对应。因为涉及到多个图片文件上传,比如企业资质要传营业执照、法人身份证、场地照片三张图,我在OkHttp里封装了一个带进度回调的MultipartBody,每张图片上传时能实时刷新ProgressBar。进度条这个功能看起来小,但运营人员现场传图时如果看不到进度,会以为App卡死了,反复点提交造成重复数据。
考虑到县区4G网络有时不稳定,我做了上传失败自动重试,重试间隔用指数退避,最多重试三次。超过三次以后,把数据暂存在本地SQLite里,等网络恢复后统一上报。这个“离线暂存、联网同步”的机制,在招聘会现场特别管用,现场人多信号差,照片一张张传很容易断,先存本地再批量同步,成功率大幅提升。
WebView在小程序开发里是用来加载协议说明和政策原文的,我把政策库做成了一个H5页面,Android端用WebView直接加载,不需要原生重写一遍展示逻辑。但WebView的坑也不少,最典型的是文件下载和打开本地文件,需要在WebChromeClient里处理onShowFileChooser回调。Android 9以后默认禁止明文HTTP流量,如果H5页面有非HTTPS资源,需要在manifest里配置usesCleartextTraffic或者networkSecurityConfig按域名放行。
4.3 小程序与Android联调中的抓包实战
联调阶段最有用的工具是Charles。开发时我对比过好几款,最终日常主力是Charles,因为它的断点调试和Map Local功能在开发调试中效率最高。抓小程序的包,步骤是:手机和电脑连同一个局域网,手机WiFi代理指向电脑IP的8888端口,再安装Charles的CA证书,就能看到HTTPS请求的具体内容。这个操作是纯开发自测,用于检查数据格式字段对不对、请求Header带没带Token,属于接口联调的基本功。
Android 7.0以上有个证书信任问题:如果targetSdkVersion >= 24,应用默认不信任用户安装的CA证书,会导致Charles解不了密。解决方法是调试包在networkSecurityConfig里加一个debug-overrides,只信任用户证书,正式包不加。千万别把调试配置带到生产环境,否则HTTPS形同虚设。
Charles里我最常用的两个功能,一个是断点拦截:在列表接口上打断点,手动修改返回值,测试小程序端对异常数据的兼容性;另一个是Map Local:把某个接口的响应指向本地JSON文件,这样前后端联调时后端还没实现,前端也能先跑起来。有一说一,这个技巧在并行开发时帮了大忙,等后端接口真实上线后再把Map Local关掉就行。
5. 上线后遇到的常见问题与排查技巧
5.1 手机号获取失败的排查思路
线上反馈最多的问题,就是用户的手机号获取不下来。排查时我一般按这个顺序走:先看小程序的AppID对应的主体是不是非个人主体,再看是否已经开通了“手机号快速验证”组件能力,然后看用户点击时后端有没有收到code回调。如果后端收到了code但解密失败,检查AES密钥是否和小程序后台配置一致,前后台密钥不一致是特别容易发生的配置错误。
另外一个隐蔽原因:同一用户在短时间内多次触发手机号按钮,微信侧会做频率限制,提示“调用频率过快”。我在前端代码里做了按钮防抖,用户点击后3秒内禁止二次点击,同时在按钮上方给一段小小的提示文案“获取中请稍候”,这样既减少无效请求,也降低用户因等待焦虑反复乱点的概率。手机号绑定成功后,我会把用户资料单独推送到一个消息队列做异步处理,不让用户等页面卡顿。
5.2 列表触底加载重复请求与卡顿
后台工单里有一条是用户反馈“刷着刷着页面就卡住了,也不加载新的”,还有人说“下滑时突然跳回顶部”。前者一般是分页参数被重复赋值导致的,比如page在onReachBottom里被重置成1,每次都在请求第一页。后者多半是数据列表没有绑定唯一的key,小程序端diff算法混乱导致渲染错乱。岗位列表我用jobId做key,这两个问题就一起消失了。
还有一个性能和体验问题:岗位列表数据量大时,一次性把每条岗位的全部字段都返回,小程序端渲染就会卡。优化思路是列表接口只返回ID、标题、薪资区间、公司名、缩略图几个关键字段,详情内容点进去再请求详情接口。这个“列表轻、详情重”的模式,对于低端安卓手机上的小程序性能提升非常明显。县区用户里有很多用千元机,不能拿开发机性能去评估线上体验。
5.3 Android端文件上传失败的目录与权限问题
Android端的文件上传问题主要出在文件路径获取上。很多教程写的是拼接一个/storage/emulated/0/路径,这在旧版本安卓上没问题,但Android 10以后分区存储直接把这条路堵了,除非应用声明了MANAGE_EXTERNAL_STORAGE权限,否则拿不到完整访问权。我在适配时把方案改成了用系统文档选择器ACTION_OPEN_DOCUMENT获取Uri,这样既不用申请敏感权限,也能通过ContentResolver拿到输入流。
另一个坑是拍照后再上传时文件丢失。用系统相机拍照返回后,Activity可能因为内存不足被系统回收,onActivityResult里拿到的Uri变成空。稳妥做法是在onSaveInstanceState里保存Uri字符串,在Activity重建后恢复。这套东西不加的话,线上会随机出现“拍完照没反应”的反馈,很难复现,但确实真实存在。
5.4 小程序正式版的域名与HTTPS配置
开发小程序时,可以在开发者工具里勾选“不校验合法域名”,这时候请求随便发。但一到真机预览或发布体验版,wx.request就会被域名白名单卡住。所以上线前最重要的一件事,就是把后端接口域名配到小程序后台的request合法域名列表里。这里有一个坑:域名必须是HTTPS,而且证书链必须完整,缺中间证书会导致部分安卓手机请求失败。
我遇到过最诡异的情况是:开发工具里一切正常,真机上就是请求失败。最后查出来是后端接口的SSL证书只有叶子证书,缺少中间证书链,微信在真机上校验严格就把它拒了。解决办法是用证书链检查工具看一眼,补全中间证书后重新部署,问题立刻消失。这个坑如果没人提醒,可能要在线上折腾好几天。
写在最后
整个项目从需求梳理到上线,周期大约是四个月,其中光联调和排障就占了一个多月。我个人最大的感受是:县区就业创业平台最难的不是技术,而是把流程理顺——谁能审、怎么审、审完怎么通知用户,这些业务规则必须在写代码前定清楚,否则后端接口写一半就要推翻重来。
最后分享一个小技巧:我从一开始就在所有后端接口的响应里加了一个全局字段trace_id,每次请求生成唯一ID,日志里打印,小程序端出问题反馈时,用户只要截个图或者发个错误码,我就能靠trace_id把整条请求链路捞出来查。别看这只是一个小字段,排查远程问题时会帮你省掉大量沟通成本。后面如果想继续扩展这个平台,方向也很明确:用Python写定时任务把市里公共招聘数据同步下来,再做一套简单的可视化统计报表,就能让运营人员每周少花半天时间手动整理数据了。