2. 数据库设计与表结构规划
人才招聘系统的表结构,直接决定了后面接口好不好写、统计好不好做。我在设计这套系统时,采用的方案是:用户中心独立两张表、职位与简历分离、投递记录用状态机驱动,审批流单独建表。下面把核心表结构展开讲,附上字段说明和设计理由。
2.1 用户、角色与简历的三层拆分
微信小程序端的用户体系比较特殊,不像PC端那样用账号密码,而是基于微信的 openid 和 unionid。所以用户表不能简单做成 username + password,而是要预留微信登录字段。我当时建的用户表核心字段如下:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信openid,小程序唯一标识', `unionid` varchar(64) DEFAULT '' COMMENT '开放平台unionid,多端登录用', `session_key` varchar(128) DEFAULT '' COMMENT '会话密钥,敏感信息加密用', `role` tinyint(1) NOT NULL DEFAULT '1' COMMENT '角色:1求职者 2企业HR 3管理员', `nickname` varchar(64) DEFAULT '', `avatar` varchar(255) DEFAULT '', `phone` varchar(20) DEFAULT '', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1正常 0禁用', `last_login_time` int(11) DEFAULT '0', `created_at` int(11) NOT NULL, `updated_at` int(11) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这个表的设计重点在于 openid 必须唯一,这是微信登录绑定的关键。角色字段用 tinyint 而不是字符串,一是节省空间,二是方便做权限判断时用整数比较,性能更好。session_key 其实不用频繁更新,但为了安全考虑,每次登录我都会重新拉取一次并覆盖,这样即使 session_key 泄露,下次登录也会自动换新的。
简历表单独拆出来,是因为求职者的简历不是单纯一个文件,而是结构化数据。我的简历表大致是这样的字段:用户ID、真实姓名、性别、出生年月、学历、工作年限、手机号、邮箱、期望城市、期望职位、期望薪资、技能标签、自我评价、附件简历地址。这里我踩过的一个坑是:一开始把技能标签设计成 varchar 直接存字符串,比如"PHP,MySQL,Redis",后来发现做筛选查询时特别麻烦,最后改成了单独一张 skill_tag 表加关联表,才真正解决。
2.2 职位、企业与投递记录的状态机设计
职位表的核心不只是存职位信息,还要维护上下架状态、审核状态、招聘紧急程度等业务字段。我设计的职位表关键字段如下:
CREATE TABLE `job` ( `id` int(11) NOT NULL AUTO_INCREMENT, `company_id` int(11) NOT NULL COMMENT '企业ID', `title` varchar(100) NOT NULL COMMENT '职位名称', `category_id` int(11) DEFAULT '0' COMMENT '职位分类ID', `salary_min` int(11) DEFAULT '0' COMMENT '薪资下限(K)', `salary_max` int(11) DEFAULT '0' COMMENT '薪资上限(K)', `education` varchar(20) DEFAULT '' COMMENT '学历要求', `experience` varchar(20) DEFAULT '' COMMENT '经验要求', `address` varchar(255) DEFAULT '' COMMENT '工作地址', `description` text COMMENT '职位描述', `requirement` text COMMENT '任职要求', `welfare` varchar(255) DEFAULT '' COMMENT '福利标签,逗号分隔', `is_hot` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否热门推荐', `audit_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '审核状态:0待审 1通过 2驳回', `publish_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '上架状态:0下架 1上架', `view_count` int(11) NOT NULL DEFAULT '0' COMMENT '浏览次数', `created_at` int(11) NOT NULL, `updated_at` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_company` (`company_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;审核状态和上架状态我故意分成两个字段,而不是合并成一个。原因很简单:一个职位可能审核通过了,但暂时不想上架,比如企业还在准备阶段。两个字段一拆,后端的逻辑就非常清晰,管理后台审核通过时只要改 audit_status,前台是否展示只看 publish_status,互不干扰。
企业表相对简单,主要包含企业名称、简介、规模、行业、融资阶段、营业执照图片、logo、企业地址等。这里要注意的是,每个企业会关联一个HR用户ID,这个关联字段直接决定了发布职位的权限归属。我当时在 job 表里没有直接存 user_id,而是通过 company_id 反查企业表再拿到 HR 的 user_id,逻辑上多了一次查询,但避免了字段冗余。如果要求性能,也可以在 job 表里冗余一个 publisher_id,根据实际并发量取舍。
投递记录表是整个系统的核心业务表,我专门为它设计了状态机:
| 状态值 | 含义 | 说明 |
|---|---|---|
| 0 | 待处理 | 求职者投递简历后,企业尚未查看 |
| 1 | 已查看 | 企业已阅读简历但未操作 |
| 2 | 已通过 | 企业邀请面试或标记通过,求职者会收到通知 |
| 3 | 已拒绝 | 企业标记不合适,流程终止 |
| 4 | 已取消 | 求职者主动撤回投递 |
| 5 | 已入职 | 完成整个招聘流程 |
这个状态机设计的核心价值在于:每次状态变更都会写入投递日志表,方便后续统计转化率、查询历史记录。我在做管理后台的数据看板时,直接根据 state 字段做 GROUP BY 就能得到各阶段的人数漏斗,不用额外写复杂的统计逻辑,非常省事。
2.3 审批流数据模型:Laravel 方案下的通用设计
热搜词里有 "laravel 审批流",这其实是很多后台系统的通用需求,包括招聘系统中的职位审核、企业入驻审核、简历公开审核等。我一开始用硬编码写判断,每个模块都写一堆 if/else,后来发现改来改去特别痛苦,干脆整理出一个轻量级的审批流模型。
核心设计思路是:一张审批配置表 + 一张审批实例表 + 一张审批记录表。审批配置表定义某个业务类型(比如"职位发布""企业入驻")的审批节点顺序;审批实例表记录某条具体业务数据的当前审批节点和状态;审批记录表保存每次审批的操作日志。
// 审批实例表结构示例 Schema::create('approval_instance', function (Blueprint $table) { $table->id(); $table->string('biz_type', 50)->comment('业务类型:job_audit/company_audit'); $table->integer('biz_id')->comment('业务数据ID'); $table->integer('current_node')->default(1)->comment('当前审批节点'); $table->tinyInteger('status')->default(0)->comment('0审批中 1通过 2驳回'); $table->timestamps(); });用 Laravel 写审批流时,我推荐把审批逻辑封装成一个 Service 类,而不是散落在 Controller 里。比如App\Services\ApprovalService,里面提供submit($bizType, $bizId)、approve($instanceId, $userId, $remark)、reject($instanceId, $userId, $remark)三个方法。Controller 里只需要调用对应方法,业务逻辑和审批流程就解耦了。这也是为什么我在标题里强调 Laravel 框架——它的 Service 容器和 Facade 机制非常适合这种模块化的业务设计,项目后期维护时特别能体会到这个优势。
3. 微信登录与权限控制的完整实现
小程序端的登录流程是整套系统的一个核心环节。热搜词里有"微信小程序用coed换车token",虽然字打错了,但指的应该就是"用 code 换取 token"。这确实是微信小程序开发里最容易搞混的点,下面把完整流程拆开讲清楚。
3.1 code2Session 换 openid 与 session_key 的流程
微信小程序端的登录流程分成几步:
- 小程序端调用
wx.login()获取一个临时凭证code,这个 code 有效期只有5分钟,且只能使用一次。 - 小程序把 code 通过 wx.request 发送到后端接口。
- 后端接收 code,调用微信的
https://api.weixin.qq.com/sns/jscode2session接口,用 appid + secret + code 换取 openid 和 session_key。 - 后端用 openid 去用户表查用户,如果不存在就自动注册一个账号。
- 后端生成自己的登录令牌 token,返回给小程序端。
这个过程中,有几个容易被坑的地方,我用 PHP 代码演示一下 ThinkPHP 环境下的实现:
public function login(Request $request) { $code = $request->post('code'); $appid = config('wechat.appid'); $secret = config('wechat.secret'); // 调用微信接口换取 openid $url = "https://api.weixin.qq.com/sns/jscode2session?" . http_build_query([ 'appid' => $appid, 'secret' => $secret, 'js_code' => $code, 'grant_type' => 'authorization_code' ]); $response = file_get_contents($url); $result = json_decode($response, true); if (isset($result['errcode']) && $result['errcode'] != 0) { return json(['code' => 400, 'msg' => '微信登录失败:' . $result['errmsg']]); } $openid = $result['openid']; $sessionKey = $result['session_key']; // 查用户,不存在则注册 $user = Db::name('user')->where('openid', $openid)->find(); if (!$user) { $userId = Db::name('user')->insertGetId([ 'openid' => $openid, 'session_key' => $sessionKey, 'role' => 1, 'created_at' => time(), 'updated_at' => time() ]); } else { $userId = $user['id']; // 每次登录更新 session_key Db::name('user')->where('id', $userId)->update([ 'session_key' => $sessionKey, 'last_login_time' => time() ]); } // 生成自己的 token $token = md5($openid . time() . uniqid()); return json(['code' => 200, 'data' => [ 'token' => $token, 'user_id' => $userId ]]); }这里要注意,file_get_contents 虽然简单,但线上环境碰到 HTTPS 有时需要配置证书,更稳妥的做法是用 cURL 或者 Guzzle。如果你用 Laravel,那我更推荐直接用Http::post()门面,代码更简洁。
3.2 Token 校验中间件与 Laravel/ThinkPHP 的通用实现
Token 拿到手之后,关键在于后续每一个需要登录的接口怎么校验。我在项目中的做法是:定义一个 Auth 中间件,所有需要登录的接口都挂上这个中间件,具体实现分框架来写。
ThinkPHP 6 里的实现方式是自定义中间件,在 app/middleware.php 注册:
public function handle($request, \Closure $next) { $token = $request->header('token'); if (!$token) { return json(['code' => 401, 'msg' => '请先登录']); } $cacheKey = 'user_token_' . $token; $userId = Cache::get($cacheKey); if (!$userId) { return json(['code' => 401, 'msg' => '登录已过期']); } // 绑定当前用户到请求对象 $request->userId = $userId; return $next($request); }Laravel 里的写法几乎一样,但中间件注册的位置不同,是在 app/Http/Kernel.php 的$routeMiddleware数组里注册。我还额外做了一个小优化:把 token 存进 Redis 而不是数据库,设置有效期7天,这样每次请求校验只需要走一次内存查询,性能好得多。如果用户30天内有活跃,就在中间件里顺带延长有效期,实现"活跃自动续期",这个细节对用户体验提升很大。
还有一个容易被忽略的点:小程序端在每次请求时需要在 header 里附带 token。我封装了一个统一的 request 方法,所有接口都走这个方法,做到自动带上 token、统一处理 401 跳转登录页、全局错误提示。项目后期加接口时,前端代码几乎没有重复的登录判断逻辑,维护成本非常低。
3.3 权限控制:角色路由守卫与页面可见性
人才招聘系统里有求职者、企业HR、管理员三类角色,小程序端的页面权限控制也是一块重要的设计内容。
小程序端没有像 Vue 那样的路由守卫,但可以在 app.js 的全局逻辑里做拦截。我的做法是:
- 用户登录后,把角色信息存到 storage。
- 在每个需要特定角色的页面的 onLoad 里调用一个 checkAuth 方法。
- 如果角色不匹配,直接 wx.redirectTo 到首页或权限提示页。
后端的权限控制用中间件做,Laravel 里可以直接用middleware('auth:hr')这样的自定义 guard。比如发布职位这个接口,只允许 HR 角色访问,我就在 Route 定义时加上:
Route::middleware(['auth:api', 'role:hr'])->post('/job/add', [JobController::class, 'store']);这样做的最大好处是,前端就算被绕过,后端的接口权限依然有保障。我做安全审计时,只需要遍历路由表,检查每个接口有没有挂中间件,就能判断是否存在越权风险,比翻代码找权限判断要高效得多。
4. 核心功能模块的接口设计与实现
招聘系统最核心的功能无非是职位检索、投递简历、收藏职位、消息通知这几个。下面挑几个有代表性的功能模块,讲讲接口设计和实现细节。
4.1 职位检索接口:关键词、分类、薪资、分页
职位列表页是求职者打开小程序后第一眼看到的东西,接口设计的好坏直接决定了首屏加载速度和用户留存。我设计的职位检索接口支持以下参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| keyword | string | 关键词,匹配职位名称和描述 |
| category_id | int | 职位分类ID |
| city | string | 城市 |
| salary_min | int | 最低薪资筛选 |
| experience | string | 经验要求 |
| page | int | 页码,默认1 |
| page_size | int | 每页条数,默认10 |
ThinkPHP 6 下的查询实现:
public function list(Request $request) { $page = (int)$request->get('page', 1); $pageSize = (int)$request->get('page_size', 10); $query = Db::name('job') ->where('audit_status', 1) ->where('publish_status', 1); $keyword = $request->get('keyword', ''); if ($keyword) { $query->where(function ($q) use ($keyword) { $q->whereLike('title', "%{$keyword}%") ->whereOr('description', 'like', "%{$keyword}%"); }); } $categoryId = (int)$request->get('category_id', 0); if ($categoryId) { $query->where('category_id', $categoryId); } $city = $request->get('city', ''); if ($city) { $query->where('address', 'like', "%{$city}%"); } $total = $query->count(); $list = $query->order('is_hot desc, id desc') ->page($page, $pageSize) ->select() ->toArray(); return json([ 'code' => 200, 'data' => [ 'list' => $list, 'total' => $total, 'page' => $page, 'has_more' => $page * $pageSize < $total ] ]); }这里有一个小细节值得注意:分页接口的返回值里,我额外加了 has_more 字段。小程序端的上拉加载更多,只需要判断 has_more 是否为 true,就能决定是否继续请求下一页,不用再拿总条数和当前页码做计算,逻辑更简单且在数据发生变化时也不会出错。
4.2 简历投递与附件上传的坑
投递简历这个功能,核心逻辑是往投递记录表插入一条数据,但有几个边界情况必须处理:同一用户不能重复投递同一职位、职位必须处于上架状态、用户简历必须完整(至少要有姓名、联系方式、教育经历)。
我在实现时,在投递记录表加了一个唯一索引uk_user_job (user_id, job_id),从数据库层面杜绝重复投递。如果用户重复点击投递按钮,第二次插入时就会触发唯一索引冲突,代码里捕获这个异常并返回"您已投递过该职位",从根源上避免并发请求导致的重复数据。
附件简历上传是一个更容易出问题的环节。微信小程序端通过 wx.chooseMessageFile 选择文件后,用 wx.uploadFile 上传到后端。这里最大的坑是:上传接口的返回格式必须是纯字符串的 JSON,不能有HTML输出。如果你用的是 ThinkPHP,记得在 upload 方法的开头关闭调试模式,否则调试页面可能会输出额外的日志信息,导致前端解析 JSON 失败。
文件上传后,我建议把文件存放在服务器本地的 public/uploads 目录,并且按日期分目录存放,例如public/uploads/resume/2025/06/。文件名用时间戳加随机串重新生成,避免中文文件名带来的URL编码问题和安全风险。同时,文件的访问权限要控制好:简历是敏感隐私信息,不能直接放在 public 目录下随意访问。我当时的处理方案是用一个专用的 download 接口,校验登录状态后通过文件流输出,用response()->download()实现,而不是直接返回静态文件URL。
4.3 微信订阅消息:面试通知与职位状态更新的推送
"微信小程序订阅信息"这个热搜词说明大家对小程序的消息推送都有需求。在人才招聘系统里,求职者最期待的通知有两个:简历被查看、企业发来面试邀请。微信小程序的订阅消息机制比较特殊,它要求用户手动点击授权按钮,而且一次性订阅只能下发一条消息。
我的实现方案是:在用户投递简历成功的回调页,展示一个"开启面试通知"的订阅按钮,用户点击后通过 wx.requestSubscribeMessage 授权给后端发送订阅消息所需参数,后端拿到参数后调用微信的 subscribeMessage.send 接口下发消息。
这里有一个项目层面的经验技巧:订阅消息的模板ID在微信公众平台申请后,不是立刻生效的,需要等审核通过。而且每个模板消息的下发,用户都需要重新授权。为了让授权体验不那么烦人,我会在用户连续投递了几个职位后,再统一弹一次授权,而不是每次投递都弹,避免用户产生反感。
4.4 职位收藏与浏览历史的冷启动处理
收藏职位和浏览历史这两个功能,虽然业务不复杂,但如果设计不好,也会影响使用体验。我的方案是各建一张表,收藏表用user_id + job_id做唯一索引,浏览历史表则每次访问职位详情时插入一条记录,并清理该用户超过30天的旧记录。
冷启动问题在于:新用户没有收藏也没有浏览历史,职位详情页的"是否已收藏"状态就不知道前端该显示什么。我的解决办法是:职位详情接口里增加两个字段is_favorited和is_applied,后端在返回详情时顺带用当前用户ID去两张表查询,直接返回布尔值。这样前端不用额外发请求判断状态,减少一次网络往返,也避免状态不同步的问题。
5. 微信小程序端开发要点与真机调试
小程序端的开发,记忆里最深的几个问题几乎都和适配、调试有关。热搜词里的"微信小程序顶部导航栏高度""微信小程序真机调试请求无法到达后端""微信小程序web-view高度更改"这几条,我全部在实际项目中踩过,下面逐个展开讲。
5.1 顶部导航栏高度与安全区适配
小程序的顶部导航栏分为两种:一种是默认的原生导航栏,高度是固定的 64px 或 88px(取决于机型);另一种是自定义导航栏,需要自己计算状态栏高度和导航栏高度。我的做法是在 app.js 里获取系统信息,动态计算导航栏高度:
const systemInfo = wx.getSystemInfoSync(); const statusBarHeight = systemInfo.statusBarHeight; // 状态栏高度 const navBarHeight = 44; // 默认导航栏内容高度 // 判断是否为胶囊按钮适配 const menuButton = wx.getMenuButtonBoundingClientRect(); const navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height; globalData.statusBarHeight = statusBarHeight; globalData.navBarHeight = navBarHeight;自定义导航栏的组件在编写时,最容易被忽略的是顶部安全区域。iPhone X 系列和部分安卓全面屏手机的底部有 Home 指示条,如果不做安全区适配,页面内容就会被系统手势区域挡住。小程序里最简单的方式是在样式中加入 env 常量和 constant 常量:
.safe-area-bottom { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }底部 tabBar 的适配原理也一样。如果首页有固定在底部的按钮组件,务必加上这个安全区 padding,否则在全面屏上,按钮会被手势区遮挡,用户点击时感觉特别别扭。
5.2 web-view 高度无法自适应的问题
如果招聘系统里嵌入了企业官网/H5页面,一定会用到 web-view 组件。这个组件最大的坑就是:高度无法靠内容自适应,默认高度是撑满整个页面。如果你想要指定高度,有两个思路:
一是直接把 web-view 作为页面唯一的组件,占满整个屏幕。这是最简单也最稳妥的方式,缺点是如果页面有别的操作按钮,只能让 H5 页面自己实现。
二是通过 postMessage 向小程序传值,在小程序端拿到 H5 页面内容高度后,动态设置 web-view 的 style 高度。H5 页面需要先引入微信的 jweixin sdk,然后在页面 onload 后发送消息:
// H5 页面 wx.miniProgram.postMessage({ data: { height: document.body.scrollHeight } });小程序端监听 message 事件,注意 web-view 的 message 事件只有在页面回退、组件销毁、分享时才触发,所以这个方案在实时性上不太靠谱。我最终的选型是:能不用 web-view 就不用,复杂的展示页面用原生小程序页面重写,只有必须引用第三方内容时才用 web-view 全屏嵌。
5.3 真机调试时请求无法到达后端的排查清单
"微信小程序真机调试请求无法到达后端"这个问题,排查顺序基本是固定的。我把它整理成一个清单:
检查开发环境是否勾选了"不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书",真机调试默认会校验合法域名,如果后端接口用的是IP地址或者没有备案的域名,请求直接就被拦了。
检查后端是否只监听了 127.0.0.1,如果后端 PHP 服务只监听 localhost,真机通过局域网IP访问时自然就连不上。用
php think run -H 0.0.0.0(ThinkPHP)或php artisan serve --host=0.0.0.0(Laravel) 才能让局域网内设备访问。检查服务器防火墙,特别是 80/443 端口。如果用的小皮面板(PhpStudy)本地开发,Windows 防火墙偶尔会弹窗拦截,直接全部允许就行。
用微信开发者工具的"真机调试"功能时,建议先在真机上打开调试模式,然后在工具里查看 Network 面板,看请求到底是在哪个环节失败的,是 DNS 解析失败、TCP 连接失败还是 HTTP 状态码异常。
如果后端接口返回的是 HTTP 400/500,大概率是代码报错了。注意小程序真机上不能正常打印 PHP 错误日志,需要去后端查看 runtime/log 日志文件。我在开发时习惯直接在接口返回里带上错误详情,方便排查,上线前再把错误详情关掉。
我之前遇到最奇怪的一个问题是:同样的代码,开发工具模拟器能通,真机上就报"errno 600002",后来发现是微信开发者工具的代理设置问题,把工具内的代理改成"不使用代理"就好了。这类问题真的得靠排查清单才能快速定位。
5.4 小程序常用组件的踩坑记录
热搜词里有"微信小程序单选框"和"微信小程序使用折线图",都是我实际用过的功能。单选框在原生小程序里有 checkbox-group 和 radio-group 两种,看名字容易混,实际区别在于:radio-group 是单选,这类场景适合做性别选择、学历选择;checkbox-group 是多选,适合技能标签筛选。
单选框的值绑定也是一个容易出错的点。radio 组件的 value 属性虽然是字符串,但实际开发中经常需要传数字ID,导致判断选中状态时类型不匹配。我的建议是:所有表单组件传值统一处理成字符串,后端接收时再转成对应类型,避免小程序端隐式类型转换带来的诡异 bug。
折线图在小程序端有两条路:一是使用 ECharts 的小程序版本 echarts-for-weixin,支持 canvas 渲染,图表交互体验较好;二是使用纯 CSS 或 SVG 手绘简单图表。ECharts 的包体积不小,如果你的项目只是展示简单的数据趋势,我建议自己封装一个轻量的 canvas 绘图工具,几十行代码就能画出一条漂亮的折线,加载速度还快得多。当然,如果需要复杂的交互,比如缩放、tooltip展示,还是直接用 ECharts 更省心。
6. 部署配置与常见问题排查实录
最后这部分,把从开发到上线的过程中,最常遇到的问题和对应的解决方案汇总一下。这个部分的内容来自我实际部署多个 PHP 项目积累的经验,信息密度比较高,建议收藏备用。
6.1 小皮面板运行目录与伪静态配置
"小皮控制面板使用thinkphp如指定运行目录""thinkphp 开启二级域名设置"这两条热搜词,都是 ThinkPHP 部署时的典型问题。
小皮面板(PhpStudy)默认站点根目录是 WWW 下的某个文件夹,如果直接访问 ThinkPHP 项目,默认会访问到 public 目录的入口文件。正确的做法是在站点设置里,把运行目录指定为项目的public目录,这样访问域名时就会自动找到入口文件 index.php。
如果用的是 Nginx 环境,还需要额外配置伪静态,否则访问 ThinkPHP 路由时会报 404。打开站点的 Nginx 配置文件,在 server 块中加入:
location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } }配置完成后重启 Nginx,ThinkPHP 的路由才能正常解析。这个配置在 Laravel 中也适用,Laravel 默认自带 public/.htaccess,但 Nginx 下还需要手动把请求转发到 index.php。
6.2 二级域名配置与多端访问
如果招聘系统需要分管理员端和用户端,或者前后端分离部署,二级域名是不可避免的。假设主域名是example.com,你希望api.example.com指向后端接口,admin.example.com指向后台管理页面,在 Nginx 里配置两个 server 块,一个监听 api 子域名指向后端 public 目录,一个监听 admin 子域名指向后台目录。
这里有个半年的教训:子域名解析生效需要时间,但真的遇到"配置了却访问不了"时,第一步应该先 ping 一下子域名,看解析是否正常,然后 curl 一下看看 web 服务器有没有收到请求,再做排查。我曾经在一个SSL证书过期的问题上折腾了整整一个晚上,到最后才发现不是代码问题,而是证书没过期,而是服务器时间不对导致证书校验失败。
6.3 ThinkPHP 路由地址跳转配置
"thinkphp route 地址跳转配置"这条热搜词,核心问题其实是两种跳转方式的选择。ThinkPHP 6 中的路由跳转有两种:一种是使用redirect()函数进行URL重定向,浏览器地址栏会变化;另一种是使用view()直接渲染模板,URL 不变。
如果是接口中的一个临时跳转,比如扫码后根据参数跳转到指定职位详情页,我推荐用 redirect 函数加路由参数:
Route::get('redirect/:id', function ($id) { return redirect('/job/detail?id=' . $id); });如果是在中间件里做登录态拦截后的跳转,比如未登录用户访问个人中心页面,在中间件里返回redirect('/pages/login/login')即可。注意这里的地址要和小程序端的页面路径保持一致,不然跳转过去就是白屏。
6.4 Laravel Session 与调用端状态保持的取舍
"laravel session"这个热搜词,说明有同学想在接口开发里用 Session。如果要开发小程序接口,我的建议是:不要用 Session。原因有两点:第一,小程序端每一次请求都是独立的,Session 依赖 Cookie 维持状态,小程序端默认不保存 Cookie;第二,接口设计的最佳实践是无状态,用 Token 鉴权,扩展性和维护性都更好。
如果项目确实需要 Session,比如后台管理端的登录状态,Laravel 默认是文件存储 Session,在高并发场景下会有磁盘IO压力,可以考虑换成 Redis 驱动。在 .env 文件里修改:
SESSION_DRIVER=redis这样 Session 存储在 Redis 中时,多个 PHP 进程能共享 Session 状态,负载均衡部署时也不用担心用户被随机登录。
6.5 常见问题排查速查表
我把自己经历过的、以及和同行交流时收集到的高频问题整理成一张速查表,建议直接收藏:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 小程序请求接口报 404 | 伪静态未配置或路由写错 | 检查 Nginx/Apache 伪静态配置;用 php think route:list 查看注册路由 |
| 接口返回 HTML 而不是 JSON | 调试模式开启输出额外信息 | 关闭 debug;或检查是否输出过 HTML 标签 |
| 真机请求失败,工具正常 | 代理设置或防火墙拦截 | 工具设置里关闭代理;检查服务器防火墙 |
| 用户上传文件失败 | 上传大小限制 | 修改 php.ini 中 upload_max_filesize 和 post_max_size |
| openid 为空 | code 已过期或重复使用 | 每次登录重新 wx.login(),确保传的是最新 code |
| 小程序 canvas 白屏 | canvas 组件层级或初始化时机不对 | 用 wx.createSelectorQuery 确保节点渲染完成后再初始化 |
| 订阅消息发送失败 | 模板ID未审核通过或用户未授权 | 检查模板ID和小程序APPID是否匹配;测试时用体验版 |
| 职位列表数据重复 | 分页参数未传递或 ORDER BY 字段不唯一 | 分页查询加 id desc 排序,确保排序稳定 |
这份速查表是每次项目交付前,我都会让测试同学重点跑一遍的用例。做小程序招聘系统这类毕设或生产项目,最大的成本不在于把功能做出来,而在于把异常场景都处理到位。很多同学项目答辩时被老师一问就问倒了,往往就是因为只做了主流程,分支流程(比如重复投递、取消投递、登录过期、权限不足)全没处理。
我个人在实际写这类系统时的体会是:先把数据表关系理清楚,再动手写接口,最后补小程序页面,这个顺序能少走一半弯路。另外一个重要的经验是,微信小程序项目的所有接口域名都要提前在微信公众平台配置,开发阶段可以把"不校验合法域名"勾上,但提交审核前必须换成正规的HTTPS域名,这一步晚了会很被动。如果你也正在做同类的招聘系统,希望这篇文章能帮你把方案选型、表结构设计、接口实现、真机调试这条链路一次走通,少踩几个我已经踩过的坑。