去年接了个中小企业的内部需求:员工电子档案借阅管理系统。后端用 PHP,前端用 uniapp 打包成微信小程序。接活之前我以为就是普通的增删改查,真正动手才发现,档案借阅这件事的复杂度不在 CRUD,而在“借阅”这个动作背后的状态流转、权限边界、超期归还、审计追踪。这套系统从需求梳理到上线,前后花了六周左右,中间踩了不少坑,也沉淀了一些通用性比较强的设计思路,今天完整拆开聊一聊,给准备做同类系统的朋友一个参考。
我在这篇文章里会把这套系统的业务模型、技术选型、数据库设计、后端 API、小程序端实现,以及上线前后遇到的典型问题都过一遍。无论你是自己公司的内部系统,还是接外包单子,只要涉及“档案借阅”“文件管理”“资料外借”这类流程型业务,这套思路都能复用。
1. 项目梳理:一个档案借阅系统到底要管哪些事
1.1 业务的本质:不是档案管理,是“借阅流程管理”
很多人在接到这个需求时会下意识往“档案管理系统”方向想,然后做出一堆档案录入、分类、检索的功能,但真正去企业调研一圈就会发现,痛点完全不在“存”而在“借”。
中小企业的员工档案包括简历、身份证复印件、学历证书、劳动合同、体检报告、保密协议等,纸质时代是放人事柜子里,谁要看就找 HR 要钥匙。电子化之后,文件是存到服务器了,但“谁能看”“看多久”“谁审批”“什么时候还”这些问题如果没有流程约束,系统的上线反而会让敏感信息失控。
所以我在梳理需求时把核心定为“借阅流程管理”,档案本身是静态资源,借阅记录才是动态核心。系统要回答的问题很简单:
- 员工能不能借、能借什么档案、借多久
- 审批人是谁、审批怎么流转、超时怎么办
- 借出去的档案是否按时归还、谁还没还
- 每次借阅留没留痕、能不能审计
1.2 角色权限:三类角色,一套矩阵
员工电子档案和普通资料的定位不一样,它涉及个人信息和公司机密,所以权限边界必须清楚。我在这个系统里设计了三类角色:
- 普通员工:只能提交借阅申请,查看自己的借阅记录,档案详情页不能直接看文件内容,只有审批通过后才能在规定时间内在线预览。
- 审批人(HR 或部门主管):能收到借阅申请,审批通过或驳回,能查看名下审批的所有记录。
- 系统管理员(档案管理员):负责档案上传、分类、下架,拥有最高权限,能看到全部借阅记录和审计日志。
角色的权限矩阵在开发前就要定好,不然后面加字段、改接口都是牵一发动全身。我这里用最简单的方式:用户表加 role 字段,中间件里做权限判断,不引入复杂权限框架。中小型系统这个量级,够用且维护成本低。
1.3 功能清单:从申请到归档的全链路
最终系统功能拆成六大模块:
- 档案管理:管理员上传档案(PDF/图片)、维护档案分类、设置借阅期限、上下架
- 借阅申请:员工检索档案、提交申请、填写借阅事由和期望借阅天数
- 审批中心:审批人查看待办、通过/驳回,可填审批意见
- 借阅管理:已批准借阅的记录,超期自动标记,支持续借申请
- 审计日志:所有关键操作记录操作人、时间、IP、操作类型
- 消息通知:申请提交后通知审批人,审批结果通知申请人,超期提醒借阅人
每一块都不难,难点在于状态怎么串。我在设计时把借阅单的状态机放在了最核心的位置,后面细讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:ThinkPHP/Laravel 怎么选,uniapp 为什么合适
2.1 后端框架:ThinkPHP 还是 Laravel
这是开发前第一个绕不开的问题。标题里把 ThinkPHP 和 Laravel 都点了,实际项目二选一即可,不用混用。我个人在这个项目里最终选了 Laravel,但 ThinkPHP 在某些场景下反而更合适,这里把判断逻辑说一下。
ThinkPHP 的优势是国内文档全、上手门槛低、模板开发快,适合“快速交付、工期短、后期改动少”的项目。它的数据库操作非常直白,Model 一层掌握后就能干活,对新手友好的程度是 Laravel 比不了的。
Laravel 的优势是工程规范、生态完善,Eloquent ORM、中间件、队列、事件、通知这些组件非常顺手。拿这个项目来说:
- 审批通过后要发通知,Laravel 自带 Notifications,不用自己写消息表和服务类
- 超期提醒要定时扫表,Laravel 的 Task Scheduling + Command 一行 cron 搞定
- 借阅状态变化后要写审计日志,Eloquent 的 Observer 比 TP 的钩子更直观
所以我的建议是:如果是外包交付、客户后续改动少、团队 PHP 水平一般,选 ThinkPHP,出活快;如果这个系统要长期演进、你后续还要加功能、团队能接受学习成本,选 Laravel,后期省心。
2.2 前端选型:uniapp 做微信小程序,值不值得
员工借阅的入口放在微信小程序,是客户明确提的,理由是员工不用安装 App、微信里搜一下就能用、消息触达方便。既然要做微信小程序,前端技术栈就要选型。
原生微信小程序开发我做过,开发体验还算能用,但有两个痛点:一是代码只能在微信生态跑,以后如果客户要支付宝小程序或 H5,等于重写;二是组件化开发效率不如 Vue 顺手。
uniapp 正好补上这两个痛点。它基于 Vue 语法,写一套代码可以编译到微信小程序、H5、App,我这次重点只需要微信小程序,但将来如果要扩展管理端 H5 或内部 App,代码大部分能复用。实测下来,uniapp 项目在 HBuilderX 里建好之后,运行到微信开发者工具非常顺滑,条件编译也能处理平台差异。
另一个点是团队技术栈。如果团队熟悉 Vue,那 uniapp 的学习成本几乎为零;如果团队只熟悉原生小程序,也可以先用原生,毕竟没有绝对优劣,只有适合不适合。
2.3 整体架构:前后端分离 + 简单分层
这个系统规模不大,但前后端分离是必须的。后端提供纯 JSON API,小程序端通过 HTTP 请求取数,不用服务端渲染模板。
后端分层我按 Laravel 的习惯走:
- Controller 层:参数校验、调用 Service、统一返回
- Service 层:业务逻辑,比如借阅申请的创建、状态流转、审批校验
- Model 层:数据操作、关联关系、观察者
- 中间件层:登录鉴权、管理员权限校验
不搞 Repository 这种过度设计,中小型项目引入太多抽象层反而拖慢进度。
API 统一返回格式约定为:
json复制{
"code": 0,
"message": "success",
"data": {}
}
code 为 0 表示成功,非 0 为业务错误码。小程序端拦截器统一处理,不用每个接口单独判断。
3. 数据库设计与借阅状态机
3.1 核心表结构:不会超纲的五张表
档案借阅系统的数据表不多,最核心的是用户表、档案表、借阅记录表、审批记录表、操作日志表。下面把每张表的关键字段列出来,可作为直接参考。
用户表(users)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(50) | 员工姓名 |
| employee_no | varchar(20) | 工号 |
| openid | varchar(64) | 微信 openid,可空 |
| role | tinyint | 1员工 2审批人 3管理员 |
| status | tinyint | 1在职 0离职 |
| created_at | timestamp | 创建时间 |
员工离职后账号应禁用,不能删除,否则历史借阅记录关联会断裂。
档案表(archives)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| title | varchar(100) | 档案名称 |
| category | varchar(50) | 分类,如劳动合同、身份证 |
| file_url | varchar(255) | 文件路径 |
| borrow_days | int | 最大借阅天数 |
| status | tinyint | 1可借 0下架 |
| borrow_count | int | 被借次数,冗余统计 |
| created_at | timestamp | 创建时间 |
这里我特意加了 borrow_days 字段,而不是在代码里写死。不同档案的借阅期限不一样,劳动合同可能允许借一天,体检报告可能允许借一周,管理员上传时灵活配置。
借阅记录表(borrow_records)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| archive_id | bigint | 档案ID |
| user_id | bigint | 借阅人ID |
| borrow_start | date | 实际借出日期 |
| borrow_end | date | 应归还日期 |
| actual_return | date | 实际归还日期,可空 |
| status | varchar(20) | 见状态机 |
| reason | varchar(255) | 借阅事由 |
| created_at | timestamp | 申请时间 |
审批记录表(approval_records)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| borrow_id | bigint | 借阅记录ID |
| approver_id | bigint | 审批人ID |
| action | varchar(10) | approve/reject |
| remark | varchar(255) | 审批意见 |
| created_at | timestamp | 审批时间 |
审批记录单独建表,不要和借阅记录混在一起,因为一次借阅可能经历多次操作(比如驳回后重新提交、续借审批)。
操作日志表(operation_logs)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 操作人ID |
| action | varchar(50) | 操作类型 |
| detail | text | 操作详情 |
| ip | varchar(45) | 操作IP |
| created_at | timestamp | 操作时间 |
日志表是审计的关键,尤其是员工档案这类涉及敏感信息的场景,客户一定会问“谁在什么时间看过谁的档案”,没有日志表这个功能就无从谈起。
3.2 借阅状态机:整个系统的核心脉络
这个系统我最满意的设计之一就是状态机。如果状态设计得乱,后面每个接口都要加一堆 if 判断,越写越崩溃。我最终定义了 7 个状态:
- pending:待审批
- approved:审批通过,待借出
- rejected:已驳回
- borrowed:借阅中
- returned:已归还
- overdue:已超期
- cancelled:已取消
状态流转的规则如下:
text复制pending → approved → borrowed → returned
pending → rejected(驳回后用户可以重新提交)
pending → cancelled(申请人主动取消)
borrowed → overdue(超过应归还日期,定时任务标记)
overdue → returned(超期后归还)
每个状态下允许的操作要提前想清楚:
- 只有 pending 状态能取消
- 只有 pending 状态能审批
- 只有 approved 状态能“确认借出”
- 只有 borrowed 和 overdue 状态能“确认归还”
- rejected 状态只能走“重新申请”
代码层面对状态流转做了统一校验,每个更新操作都先查当前状态是否合法,不合法直接抛业务异常。这样做的好处是,业务逻辑再多也不会出现“状态被覆盖”的情况。
3.3 审批流设计:单级审批够不够
中小企业的审批流通常不需要复杂的多级会签,我这次用的是单级审批:员工提交申请后,默认推送给所在部门的主管(或 HR 专员),审批人通过或驳回即可。
但设计时我留了扩展点:审批人字段指向用户表的 role=2 的用户。如果客户后续要把流程改为多级审批,只需要加一张审批配置表,记录每个步骤的角色和顺序,然后把审批记录表里的 action 字段扩展为 step_no 就行。
审批超时提醒是一个容易被遗漏的需求。我做了个定时任务:每 10 分钟扫描一次待审批记录,如果超过 24 小时未处理,给审批人发一条微信订阅消息提醒。实现上很简单,但客户感知很强。
3.4 “取最新一条且去重”的 SQL 问题,一次讲透
搜索热词里有一条是“laravel 怎样利用 orderby 和 groupby 取最新一条且去重的数据”,这个问题在做借阅记录列表时非常典型:比如查看某档案被哪些员工借过,同一员工可能借了多次,列表里只需要显示最新一条。
新手最常见的写法是:
php复制BorrowRecord::query()
->groupBy('user_id')
->orderByDesc('created_at')
->get();
这种写法在 MySQL 里查出来的“最新一条”其实是不可靠的。原因是 SQL 的执行顺序中,WHERE 先执行,GROUP BY 后执行,ORDER BY 排在最后。也就是说,分组之后每条记录选哪一行,完全由 MySQL 自己决定,不一定是当前组里最新的那一行。你看着好像取到了最新数据,实际上可能取到的是任意一条,数据不一致非常隐蔽。
正确的解法有几种,我推荐用子查询先分组最大时间,再关联原表:
php复制$subQuery = BorrowRecord::query()
->selectRaw('user_id, MAX(created_at) as max_created')
->groupBy('user_id');
$records = BorrowRecord::query()
->joinSub($subQuery, 'latest', function ($join) {
$join->on('borrow_records.user_id', '=', 'latest.user_id')
->on('borrow_records.created_at', '=', 'latest.max_created');
})
->get();
生成的 SQL 类似于:
sql复制SELECT *
FROM borrow_records
INNER JOIN (
SELECT user_id, MAX(created_at) AS max_created
FROM borrow_records
GROUP BY user_id
) AS latest
ON borrow_records.user_id = latest.user_id
AND borrow_records.created_at = latest.max_created
这种写法利用 MAX(created_at) 先锁定每个用户的最新时间,再通过 join 取整条记录,结果一定正确。需要注意的是,如果同一用户在同一秒提交了两条记录,可能会出现同一组返回多行的情况,实际业务里不太可能,但严谨起见可以在 select 里加 DISTINCT 或者用 id 做二次条件。
还有一个常见问题是 groupBy 搭配 select 时,MySQL 的 ONLY_FULL_GROUP_BY 模式会直接报错。解决办法是不要 select 非聚合字段,改用上面说的 join 方案,既兼容 ONLY_FULL_GROUP_BY,也不会误用乱序数据。
4. 后端接口与核心业务逻辑实现
4.1 接口规划:尽量少,但每一条都清晰
后端接口划分如下:
| 模块 | 接口 | 说明 |
|---|---|---|
| 认证 | POST /api/auth/login | 微信登录,返回 token |
| 档案 | GET /api/archives | 档案列表,支持搜索分页 |
| 档案 | GET /api/archives/ | 档案详情 |
| 借阅 | POST /api/borrows | 提交借阅申请 |
| 借阅 | GET /api/borrows/my | 我的借阅列表 |
| 借阅 | POST /api/borrows/{id}/cancel | 取消申请 |
| 审批 | GET /api/approvals/pending | 待审批列表 |
| 审批 | POST /api/approvals/{id}/handle | 通过/驳回 |
| 归还 | POST /api/borrows/{id}/return | 确认归还 |
| 统计 | GET /api/dashboard/stats | 管理员首页统计 |
控制器里只做参数校验和返回,业务逻辑抽到 Service。比如提交借阅申请时,Service 里至少有这几步:校验档案状态、校验用户是否有未归还的同类档案、创建借阅记录、创建待办通知、写操作日志。如果都堆在 Controller 里,后来加一个“续借”功能,Controller 会变得没法看。
4.2 微信登录:openid 与员工身份绑定
小程序端登录流程用的是微信官方 code2Session 接口:小程序端 uni.login 拿 code,传给后端,后端拿着 code 请求微信接口换 openid 和 session_key。
后端拿到 openid 后,先查用户表有没有记录的 openid 等于这个值,如果有,直接签发 token;如果没有,说明是新用户,需要走“绑定”流程。员工首次进入小程序时,需要输入工号和姓名进行绑定,绑定成功后把 openid 写入用户表。
这个绑定流程非常重要。如果小程序纯粹采用“微信授权即登录”,任何能打开小程序的人都会自动创建一个账号,无法和企业内部的员工数据对上。绑定逻辑虽然简单,却是保证“员工实名”的关键一步。我做的时候还加了一步:绑定操作限制只能绑定一次,如果已绑定,再次绑定会提示联系管理员解绑,防止员工 A 误绑了员工 B 的账号。
token 签发我用的是 Laravel Sanctum,轻量、无需额外配置,个人项目用 JWT 也行,但 Sanctum 对 session 和 token 都支持,更适合长期维护。
小程序端请求时把 token 放在请求头:
text复制Authorization: Bearer {token}
后端中间件统一解析 token,解析失败返回 401,小程序端拦截器收到 401 自动跳转登录页。
4.3 借阅申请与审批:事务性操作一个都不能少
借阅申请接口的核心逻辑,我用一个流程图来描述思路:
- 前端传入 archive_id、reason、期望借阅天数
- 后端校验档案存在且状态为“可借”
- 校验用户是否有未归还的借阅记录(防止重复借同类档案)
- 创建 borrow_records 记录,状态为 pending,应归还日期 = 实际借出日期 + 档案 borrow_days
- 写入 operation_logs
- 发送通知给审批人
这里的“应归还日期”有个细节:是在申请通过时就算好,还是在确认借出时算?我最后选择在审批通过、管理员确认借出时计算。因为申请到审批之间可能隔几天,如果申请时就算好,审批通过后系统自动延期,反而容易乱。
审批接口的核心是事务。审批人点击通过,会同时执行:更新借阅记录状态为 approved、写入审批记录、通知申请人。这三步必须在一个数据库事务里完成,任何一步失败都要回滚,否则会出现“状态改了但通知没发”的尴尬。
Laravel 里用 DB::transaction 包裹即可:
php复制DB::transaction(function () use ($borrowId, $approverId, $remark) {
$borrow = BorrowRecord::lockForUpdate()->findOrFail($borrowId);
$this->assertCanApprove($borrow);
$borrow->update(['status' => 'approved']);
ApprovalRecord::create([...]);
// 通知、日志
});
注意这里用了 lockForUpdate,加的是行锁。原因是审批这个操作的并发场景虽然不高,但万一审批人和管理员同时操作同一条记录,行锁能防止状态被重复更新。
我还做了一个细节:审批人只能操作自己是审批人的记录,接口里必须校验当前登录用户的 id 是否等于记录的 approver_id。这个问题很容易被忽略,开发时测试数据少,不校验也看不出问题,上线后就会出现 A 主管把 B 主管的记录给审批了。
4.4 归还与超期提醒:定时任务的设计思路
借出后,系统就要盯着“还”这件事。归还动作比较简单:确认借出后,状态变成 borrowed,管理员或借阅人点归还,后端把 actual_return 置为当前日期,状态改为 returned。归还时还有一个校验:如果档案有实体文件(比如纸质劳动合同),线上归还和线下归还可能不同步,所以我们规定小程序端的“确认归还”由档案管理员操作,普通员工只能申请归还,避免出现线上显示已还、线下还没还的纠纷。
超期提醒用 Laravel 的任务调度实现。我在 app/Console/Kernel.php 里加了一个每日任务,每天早上 9 点扫描所有状态为 borrowed 且 borrow_end 小于今天的记录,把状态更新为 overdue,并向借阅人发送一条微信订阅消息。
命令示例如下:
bash复制php artisan schedule:run
服务器 crontab 加一行:
bash复制* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1
Laravel 的调度器每分钟触发一次,内部自动判断哪些任务到点执行,非常稳定。ThinkPHP 的话也有类似方案,但实现成本稍高,这也是我选 Laravel 的原因之一。
5. 小程序端实现与交互细节
5.1 uni-app 项目的初始化与目录结构
前端我用 HBuilderX 创建 uniapp 项目,选择 Vue3 版本,模板用默认的空模板。创建完成后,项目结构里几个关键目录:
- pages:页面文件,每个页面一个 vue 文件
- static:静态资源
- utils:封装的请求、工具函数
- store:全局状态管理,用的 Pinia 或 Vuex
- manifest.json:应用配置,包括微信小程序的 appid
- pages.json:页面路由和底部 tabbar 配置
小程序端页面我设计了四个 Tab:首页(档案列表)、借阅记录、审批中心、我的。审批中心 Tab 只对审批人显示,这里用条件判断控制,在 pages.json 的 tabBar 里不好做动态显隐,我是在首页放一个入口,根据角色判断是否展示。
5.2 微信登录与全局用户状态
uniapp 里获取微信登录 code 的方式:
javascript复制uni.login({
provider: 'weixin',
success: (loginRes) => {
const code = loginRes.code
// 携带 code 请求后端登录接口
}
})
code 有效期只有 5 分钟,且只能用一次,所以一定要及时传到后端。我之前犯过一个错误:在 onLaunch 里调 uni.login,然后又在上一个页面里调了一次,导致后端拿到的 code 已经无效,报错和微信文档里的“code been used”一致。
登录成功后,后端返回 token 和用户信息,我把它们存到 uni.setStorageSync,后续请求统一从 storage 里取 token,请求拦截器挂在 header 上。
全局用户状态我放在 store 里,应用启动时先读 storage,如果 token 存在就拉取一次用户信息刷新状态,如果拉取接口返回 401,就清理本地缓存并跳转登录页。
这里有一个经验:无论后端怎么设计“自动登录”,小程序端一定要做“登录过期”的兜底。员工借阅档案是低频操作,可能半个月才打开一次,token 过期是常态。如果没有自动跳登录的逻辑,用户会一直停留在白屏或报错页面,体验非常差。
5.3 档案列表与借阅申请:前后端联调的关键细节
档案列表页是用户接触最多的页面,我在实际开发中把几个细节处理好了:
- 列表分页:后端返回当前页、每页条数、总条数,小程序端触底加载下一页,用 v-if 控制“没有更多了”
- 搜索防抖:搜索框输入时做 300ms 防抖,避免每敲一个字就发一次请求
- 状态标签:借阅状态用不同颜色区分,待审批橙色、已通过绿色、已超期红色,用户扫一眼就明白
- 加载体验:首次加载显示 loading,切 Tab 时保留页面状态,不用重新请求
借阅申请页用的是表单,字段有借阅事由和期望借阅天数。期望天数如果超过档案的最大借阅天数,前端直接拦截提示。选择日期我用的是 uni-datetime-picker 组件,开始日期默认今天,结束日期联动计算。
提交成功后,页面跳转到“我的借阅”列表,同时弹 toast 提示“申请已提交,请等待审批”。我实测过,如果提交后不跳转、不提示,用户会以为是按钮坏了,反复点提交,导致后端出现多条相同的申请记录。
5.4 支付类功能没有,但订阅消息必须有
这个系统没有支付功能,但有一个和支付同等重要的功能:微信订阅消息通知。用户审批通过、借阅超期、归还确认这些状态变化,都需要及时触达用户。
微信小程序的消息通知,技术原理是一次性订阅消息:用户在小程序里主动订阅后,后端才能在下一次触发时发送一条模板消息。所以我在两个位置设置了“订阅授权”的引导:
- 提交借阅申请后,弹窗引导用户订阅“审批结果通知”
- 借阅状态变为“借阅中”后,引导订阅“超期提醒”
后端发送消息用的是微信的 subscribeMessage.send 接口,需要提前在小程序后台申请模板,拿到模板 ID 配置到后端。这里踩过一个坑:同一个模板,一次性订阅授权只能发一条消息。也就是说,用户授权一次,后端只能发一条,不能一条授权多次使用。所以要在用户可能收到多条消息的场景里,连续弹两次订阅授权。
5.5 打包上线:从 HBuilderX 到微信开发者工具
uniapp 项目开发完成后,在 HBuilderX 里点“运行到小程序模拟器”,会自动唤起微信开发者工具,前提是微信开发者工具配置了服务端口。首次运行要修改 manifest.json 里的微信小程序 appid,改成客户在微信公众平台申请的真实 appid,否则登录、订阅消息全都调不通。
打包上线流程:
- 确认 manifest.json 里的 appid 正确
- 点击“发行 -> 小程序-微信”,生成微信小程序代码包
- 在微信开发者工具中导入项目,确认编译无错
- 点击“上传”,填入版本号和备注
- 到微信公众平台“版本管理”中提交审核
- 审核通过后点击“发布”
这里有一点容易被忽略:小程序后台的服务器域名必须配置为 HTTPS,并且 ICP 备案过的域名。开发阶段可以在开发者工具里勾选“不校验合法域名”,但体验版和线上版必须走正式域名,否则所有请求都会失败,报错一般是 “url not in domain list”。
6. 上线前后踩过的坑与排查记录
6.1 ThinkPHP 安装提示 ext-json 缺失
虽然这个项目最终选了 Laravel,但我在环境准备时也装过 ThinkPHP,搜索热词里的 “thinkphp安装ext-json” 非常典型。某些 PHP 版本(尤其是编译安装的 PHP 7.x)默认没有启用 json 扩展,安装 ThinkPHP 或运行 composer require 时会直接报 ext-json 缺失。
解决办法:
bash复制# Ubuntu/Debian
sudo apt-get install php-json
# 或者编译安装时挂载
./configure --enable-json
装完后重启 PHP-FPM:
bash复制sudo systemctl restart php-fpm
这个坑的重点不是安装本身,而是很多新手把扩展安装到 CLI 的 PHP 里,但 Web 服务用的 FPM PHP,两者配置不一定是同一套。检查时用 php -m | grep json 看 CLI,再用 phpinfo() 看 FPM,两边都要确认。
6.2 微信登录报错 wx1cb4398e1413dce7 的排查思路
开发微信登录时遇到一个错误码,形如 wx1cb4398e1413dce7,这类问题我排查后发现主要集中在几个环节:
- 小程序 appid 配错:manifest.json 里的 appid 和微信公众平台不一致。开发时可能用了测试号,但后端 code2Session 用的是正式号的 appid/secret,code 就会校验失败。
- code 重复使用:同一个 code 只能使用一次,如果前端发起了两次登录请求,第二次必然失败。
- 后端请求微信接口时网络问题或参数错误:appid、secret、js_code 必须严格匹配,grant_type 固定为 authorization_code。
- 域名未配置或请求被拦截:开发阶段可以在开发者工具里勾选不校验域名,线上必须确保 request 合法域名已配置。
排查这类错误,我习惯在后端登录接口里打日志,把微信接口的原始返回记录下来,比如:
php复制Log::info('wechat_login_response', $response);
有了原始返回,问题定位就清晰了。微信返回的 errcode 和 errmsg 是权威依据,前端报的 wx 开头错误码只是一个包装,关键要看原始信息。
6.3 真机预览出现 net::ERR_CONNECTION_RESET
小程序真机预览时,如果页面请求后端接口报 net::ERR_CONNECTION_RESET,基本可以判断不是代码逻辑问题,而是网络链路问题。常见原因有三个:
- 后端服务地址写的是 localhost 或 127.0.0.1,手机访问不到开发机
- 开发机防火墙没放行端口
- 后端服务没有监听 0.0.0.0,只监听了 127.0.0.1
解决办法:后端服务启动时绑定 0.0.0.0,小程序端的请求地址改成开发机的局域网 IP,手机和电脑连同一个 Wi-Fi。调试完再改成正式域名。
我在本地调试时习惯用 php artisan serve --host=0.0.0.0,这样手机可以直接访问开发机的 8000 端口。但要注意局域网 IP 通常不是固定的,如果每次都改代码里的 baseURL,很麻烦,建议把 API 地址提取到配置文件里,测试环境和生产环境分离。
6.4 uniapp 运行到微信开发者工具一直报 page not found
uniapp 项目跑起来,微信开发者工具提示 Page not found 的情况,我遇到两种:
- pages.json 里注册了页面路径,但 pages 目录里没有对应文件,或者文件名大小写不一致
- 使用了分包加载,分包的根路径写错了
第一种最常见,加了新页面后忘了在 pages.json 里注册。uniapp 不像原生小程序那样能通过目录结构自动注册,必须手动维护 pages 数组。我建议每次新建页面后,先确认 pages.json 里的路径能和文件系统对应,再运行调试。
6.5 uniapp 打开 webview 页面有过渡白屏
这个系统里有一块“制度文档预览”,我用 webview 加载 PDF 链接,结果打开时白屏非常明显,体验很糟糕。排查后总结出两个优化:
- webview 加载前先展示 loading 状态,等 onLoad 事件触发后再隐藏,避免白屏裸奔
- PDF 文件如果较大,先压缩或转成图片预览,不要直接用 webview 加载几十 MB 的文件
小程序 webview 对 PDF 的支持在不同机型上表现不一致,有的 iOS 设备直接打不开。稳妥的方案是后端把 PDF 转成图片,小程序端用 image 组件预览,虽然实现成本多一点,但兼容性最好。
7. 从项目角度再看这套系统
做完这个系统,我回过头来总结了几点体会。
电子档案借阅管理系统不是技术难,而是流程建模难。如果一开始就把状态机设计清楚、把角色权限边界划清楚、把“谁在什么时间做了什么”的审计逻辑落地,后面的代码就是填空题。反过来,如果一上来就急着写增删改查,等客户提出“这个档案为什么能被他看到”的时候,改动成本会成倍上涨。
另外,中小型企业的系统,功能不必追求大而全,单点体验比功能数量重要得多。比如这个系统的借阅流程,从申请到审批到归还,一个闭环走通,客户就会觉得系统好用;如果做了二十个模块但每个流程都有断点,客户只会觉得你做了一个半成品。
如果你也正在做类似的项目,建议从这三件事开始:把状态机画在纸上、把角色权限矩阵定下来、把每个状态变化对应的通知梳理出来。这三件事做完,代码实现只是时间问题。
最后分享一个小技巧:像这类管理系统,开发阶段就把日志系统做好,每个关键操作都记录操作人、操作时间、操作内容。系统上线后遇到任何“数据不对”的反馈,日志就是最直接的破案线索。很多项目上线后最耗时的不是改 bug,而是查“谁动了这条数据”,这套档案借阅系统的日志设计,让我省了非常多排查时间。
