1. 从需求说起:空巢老人健康管理系统到底要解决什么
做这个项目的初衷,其实源于一个很现实的问题:老家父母的身体状况,子女在异地完全处于“盲区”。老人觉得不舒服通常不会主动说,等子女知道的时候往往已经拖了一段时间。社区工作人员想帮忙,但人手有限,统计体检数据基本靠纸质表格和Excel。一个能自动收集、上报、分析老人健康数据的系统,就成了刚需。
当时接到的需求很明确:一套面向空巢老人的健康管理系统,前端用微信小程序,后端需要同时支撑老人端、子女端和社区管理端三个角色。老人通过小程序完成日常健康打卡、填写血压血糖等身体指标、一键发出SOS求助;子女能实时查看父母的健康报告并接收异常提醒;社区工作人员需要后台管理老人档案、查看统计报表、发送关怀提醒。
技术选型上,微信小程序几乎是唯一合理的前端方案——老人不用安装App,扫一扫就能用,子女也能通过同一个生态快速绑定和查看。而后端之所以在ThinkPHP和Laravel之间反复权衡,是因为这个项目经历了两个阶段:先以最快速度做出来给社区试用,再重构为一个能长期维护和扩展的系统。这篇文章就把整个过程中的架构设计、框架选型、核心代码和踩过的坑完整记录下来,给正在做类似毕业设计、社区项目或者个人创业项目的朋友一个可以直接参考的路径。
1.1 那些“看着简单,做起来全是细节”的需求
需求听起来不复杂,但每一条拆开都有讲究:
- 老人健康打卡:不是简单地存一条记录。老人可能每天量三次血压,每次数值都要保留趋势;漏打卡要有提醒;数值异常要自动触发告警。
- 多角色数据隔离:社区管理人员能看到辖区所有老人数据,子女只能看到自己绑定的父母数据,老人自己只能看本人的数据。这三个角色的数据权限必须严格区分,不然就是重大事故。
- 主动推送:健康指标超出范围、老人连续两天没打卡、SOS求助手势触发,这些情况都要第一时间通知到子女微信。
- 数据可视化:小程序端要展示近30天血压趋势图、血糖变化曲线,管理后台要有按片区的统计报表。
- 用药提醒:很多老人有慢性病,需要定时提醒吃药。这个看起来不起眼,但对后端来说意味着要有一套定时任务系统。
这些需求决定了后端的核心模块:用户认证与角色权限、健康记录管理、异常告警引擎、消息推送服务、数据统计接口。无论用ThinkPHP还是Laravel,这些模块一个都跑不掉。
1.2 把业务需求拆成可落地的技术模块
我在做技术方案时没有一上来就写代码,而是先把业务翻译成技术模块清单,这样后面不管切换哪个框架,核心逻辑都不会乱。
| 业务需求 | 后端模块 | 小程序端对应页面 |
|---|---|---|
| 老人/子女/管理员登录 | 用户认证、JWT签发、角色权限中间件 | 登录页、授权绑定页 |
| 健康指标录入与查询 | 健康记录CRUD、分页、趋势统计接口 | 健康打卡页、历史记录页 |
| 血压血糖异常提醒 | 异常阈值判断、微信订阅消息推送 | 消息中心 |
| SOS紧急求助 | 求助事件接口、通知调度 | 首页一键求助按钮 |
| 用药提醒推送 | 定时任务扫描用药计划、生成推送任务 | 用药计划设置页 |
| 数据统计与导出 | 聚合统计接口、Excel导出 | 子女端报告页、管理后台 |
这个表做完之后,整个开发路径就清晰了。后端需要提供一套标准的RESTful API,小程序端只管调接口渲染页面。剩下的核心问题就是:用ThinkPHP还是Laravel来实现这套API。
2. 为什么我会同时用ThinkPHP和Laravel两套框架做后端
不少朋友看到标题第一反应是:“一个项目为什么要用两个框架?这不是给自己找事吗?”
其实这是项目演进的自然结果,不是故意炫技。第一版用了ThinkPHP 6,原因特别直接:开发时间紧,团队里有人对它最熟,部署在虚拟主机上方便,社区里中文资料也最多。ThinkPHP的文档通俗,上手曲线平缓,适合一个人快速把整个后端从0到1搭起来。
但项目运行一段时间后,问题开始浮现。业务逻辑越来越复杂,ThinkPHP的控制器越来越臃肿;后期加入的社区管理功能需要更多的分层设计;定时任务、队列这些“正经系统”该有的能力,在ThinkPHP里写起来总觉得不够顺手。这时候我萌生了用Laravel重写后端的想法——不是推翻重来,而是基于已经跑通的业务逻辑,换一个更利于长期维护的骨架。
2.1 第一版选ThinkPHP的原因
如果你只是想快速把系统跑起来给人演示,ThinkPHP绝对是性价比之选。
我的第一版后端基于ThinkPHP 6写的,环境是PHP 7.4 + MySQL 5.7,部署用了一个普通的云服务器。选择它的理由很实际:
- 上手快:ThinkPHP的目录约定很直观,
controller、model、view一目了然,和ThinkPHP 5一脉相承,老PHP开发者几乎没有学习成本。 - 中文文档和社区资源极其丰富。遇到问题搜索“ThinkPHP+问题关键词”,基本都能找到解决方案,这对赶工期来说太重要了。
- 自带一些便捷能力:比如数据库链式操作、验证器、自动时间戳,这些做管理类系统特别方便,不需要额外引入太多依赖。
- 部署简单:入口文件丢到web目录就行,不需要额外配置队列服务、缓存驱动这些重型组件。
在ThinkPHP版本里,我用一个HealthRecord模型加一个HealthRecordController,十来天就把健康记录、老人管理、子女绑定这些核心接口全部跑通。小程序端同步开发,前后端联调一次通过率很高。
2.2 为什么会再写一版Laravel
项目上线第二个月,需求方提出了几个新需求:给社区工作人员加一个数据看板,要看到各网格老人的打卡率;给异常告警加规则配置,不同老人要有不同的血压阈值;还要加一个用药提醒的定时任务,每天早上8点推送。
这些需求本身不复杂,但问题在于,它们触及了架构层面的短板:
- 原有Controller里混着SQL查询、参数校验、业务逻辑,加一个看板接口要在一堆旧代码里找地方塞新逻辑。
- 定时任务、队列消息推送在ThinkPHP里需要额外引入对应扩展,使用体验不如Laravel原生流畅。
- 团队后来加入了新的开发成员,大家的技术栈偏向Laravel,统一框架能降低协作成本。
所以第二轮迭代时,我决定用Laravel重构后端。这个决定现在回头看非常正确——同样的业务,Laravel的目录结构和内置能力让代码组织清晰了很多,后续加功能基本都是“在正确的地方加正确的文件”就行。
2.3 两套框架在同一个项目里的互补关系
可能有人会问:既然要用Laravel,为什么不一开始就用?
答案很简单:不同阶段需要不同的武器。赶工期做demo、验证需求有没有人用,ThinkPHP的短平快优势很明显。项目活下来了、需要团队协作、需要不断加需求的时候,Laravel的工程化设计能省掉很多长期维护的心力。
这两套框架不是“谁替代谁”的关系,而是项目生命周期里不同阶段的合理选择。我在本文后面给出的所有对比数据,都是同一个业务需求在两套框架下真实开发后的感受,不是凭空比较框架之间的纸面参数。
3. ThinkPHP版本核心实现:一路从0到1跑通小程序
3.1 项目结构与公共配置
ThinkPHP 6的项目结构里,我重点关注这几个目录:
app/ ├── controller/ # 控制器:接收请求、调用模型、返回JSON ├── model/ # 模型:数据库表映射、数据逻辑 ├── middleware/ # 中间件:登录校验、权限控制 ├── validate/ # 验证器:参数校验 ├── common.php # 公共函数 config/ ├── database.php # 数据库配置 ├── jwt.php # JWT配置(自定义) route/ └── app.php # 路由定义公共配置里特别要注意的是跨域问题。小程序端请求后端接口,虽然微信官方不强制要求CORS(因为小程序不是浏览器环境),但如果后期要写一个Web管理后台(用网页访问),跨域就躲不掉了。我在config/app.php里配置了跨域中间件,统一允许来自管理后台域名的请求。
数据库连接配置如下:
return [ 'default' => 'mysql', 'connections' => [ 'mysql' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'elder_health', 'username' => 'root', 'password' => 'your_password', 'hostport' => '3306', 'charset' => 'utf8mb4', 'prefix' => 'eh_', 'debug' => true, ], ], ];注意这里charset我配置的是utf8mb4,因为后续要用emoji表示健康状态(比如正常打勾、异常打叉),老式的utf8会报编码错误。前缀eh_是elder_health的缩写,多系统共用数据库时可以避免表名冲突。
3.2 小程序登录 code 换 token 的实现细节
微信小程序的登录机制是:前端调用wx.login()拿到临时凭证code,后端拿这个code加上小程序的appid、secret去微信接口换取openid,再基于openid生成自己的登录态 token。
这一步是整个系统安全性的基石。我的具体实现步骤如下:
1. 在小程序端,登录按钮触发后先调用 wx.login
wx.login({ success: async (res) => { if (res.code) { const loginRes = await wx.request({ url: 'https://api.example.com/api/auth/login', method: 'POST', data: { code: res.code, nickname: '测试用户', // 可选,用于首次建档 }, }); const { token, userInfo } = loginRes.data.data; wx.setStorageSync('token', token); wx.setStorageSync('userInfo', userInfo); } } });2. 后端定义登录路由
// route/app.php Route::post('auth/login', 'Login/login');3. 控制器中实现code换openid并签发token
namespace app\controller; use think\facade\Db; use think\facade\Cache; use Firebase\JWT\JWT; use app\BaseController; class Login extends BaseController { public function login() { $code = $this->request->post('code'); if (empty($code)) { return json(['code' => 1001, 'msg' => 'code不能为空']); } $appid = config('wx.appid'); $secret = config('wx.secret'); // 请求微信接口,用 code 换取 openid 和 session_key $url = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code"; $result = json_decode(file_get_contents($url), true); if (isset($result['errcode']) && $result['errcode'] != 0) { return json(['code' => 1002, 'msg' => '微信登录失败:' . $result['errmsg']]); } $openid = $result['openid']; // 查询或创建用户 $user = Db::name('users')->where('openid', $openid)->find(); if (!$user) { $userType = $this->request->post('type', 'elder'); // elder 老人 / child 子女 / admin 管理员 $userId = Db::name('users')->insertGetId([ 'openid' => $openid, 'nickname' => $this->request->post('nickname', '未命名用户'), 'user_type' => $userType, 'status' => 1, 'create_time' => time(), ]); $user = Db::name('users')->find($userId); } // 签发 JWT token,有效期7天 $payload = [ 'uid' => $user['id'], 'type' => $user['user_type'], 'iat' => time(), 'exp' => time() + 7 * 24 * 3600, ]; $token = JWT::encode($payload, config('jwt.secret'), 'HS256'); return json(['code' => 0, 'msg' => 'ok', 'data' => [ 'token' => $token, 'userInfo' => [ 'id' => $user['id'], 'nickname' => $user['nickname'], 'user_type' => $user['user_type'], ], ]]); } }这里有几个关键细节要重点提醒:
- 不要自己存session_key。session_key是微信加密数据的解密钥,存储它会增加泄露风险。如果不需要解密手机号、运动数据,它在服务端生命周期只需要几秒。
- JWT密钥要单独配置,不要用默认值。我见过不少项目把密钥留在代码里直接上线,这是非常严重的漏洞。
- 用户类型要区分:同一套登录接口,通过
type参数区分是老人端还是子女端。因为同一个微信号不可能既是老人又是子女,但绑定关系由后续的“亲属绑定”接口维护。
3.3 健康记录上报接口实战
健康记录是系统的核心业务,老人每天上传血压、血糖、心率等指标,后端要校验数值合理性、自动判断是否异常、给绑定的子女发送提醒。
数据表结构我这样设计:
CREATE TABLE `eh_health_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '老人用户ID', `record_type` varchar(20) NOT NULL DEFAULT 'daily' COMMENT '记录类型:daily日常/blood_pressure血压/glucose血糖/heart_rate心率', `systolic_pressure` int(11) DEFAULT NULL COMMENT '收缩压mmHg', `diastolic_pressure` int(11) DEFAULT NULL COMMENT '舒张压mmHg', `glucose_value` decimal(4,1) DEFAULT NULL COMMENT '血糖mmol/L', `heart_rate` int(11) DEFAULT NULL COMMENT '心率次/分', `feeling` tinyint(1) DEFAULT NULL COMMENT '自评状态:1良好 2一般 3不适', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `record_date` date NOT NULL COMMENT '记录日期', `create_time` int(11) NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_user_date` (`user_id`, `record_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;控制器中健康记录上报的逻辑:
namespace app\controller; use think\facade\Db; use think\facade\Validate; use app\BaseController; class HealthRecord extends BaseController { // 用户的中间件鉴权,通过后$this->uid可用 protected $middleware = ['Auth']; public function add() { $data = $this->request->post(); // 参数校验 $validate = Validate::rule([ 'systolic_pressure' => 'number|between:60,260', 'diastolic_pressure' => 'number|between:40,200', 'glucose_value' => 'float|between:1.0,35.0', 'heart_rate' => 'number|between:30,220', 'record_date' => 'require|date', ]); if (!$validate->check($data)) { return json(['code' => 1001, 'msg' => $validate->getError()]); } // 判断异常(简化版示例) $abnormal = false; $message = ''; if (isset($data['systolic_pressure']) && $data['systolic_pressure'] >= 140) { $abnormal = true; $message .= '血压偏高;'; } if (isset($data['glucose_value']) && $data['glucose_value'] >= 7.0) { $abnormal = true; $message .= '空腹血糖异常偏高;'; } $recordId = Db::name('health_record')->insertGetId([ 'user_id' => $this->uid, 'systolic_pressure' => $data['systolic_pressure'] ?? null, 'diastolic_pressure' => $data['diastolic_pressure'] ?? null, 'glucose_value' => $data['glucose_value'] ?? null, 'heart_rate' => $data['heart_rate'] ?? null, 'feeling' => $data['feeling'] ?? 1, 'remark' => $data['remark'] ?? '', 'record_date' => $data['record_date'], 'create_time' => time(), ]); // 如果有异常,给绑定的子女发送订阅消息(异步处理) if ($abnormal) { $this->notifyChildren($this->uid, $message, $data['record_date']); } return json(['code' => 0, 'msg' => 'ok', 'data' => ['record_id' => $recordId]]); } private function notifyChildren($elderId, $message, $date) { $children = Db::name('family_relation')->where('elder_id', $elderId)->select(); foreach ($children as $child) { // 这里只记录推送任务,真正的发送放到定时任务里 Db::name('message_queue')->insert([ 'user_id' => $child['child_id'], 'template_type' => 'health_alert', 'content' => "您的长辈{$date}健康数据异常:{$message}", 'status' => 0, 'create_time' => time(), ]); } } }这个版本的设计思路是“先把正确的事做了”,异常判断和消息通知的拆分也简单直接。实际运行了两个月,健康打卡记录累计一万多条,接口响应稳定在200ms以内,没有出现明显性能问题。
3.4 真机调试连不上后端的排查
这是整个开发过程中让我耗费最多时间的一个问题。模拟器上一切正常,一换真机就请求超时,折腾了一整个下午。
最终排查结论是两方面的原因:
- 本机开发环境下的局域网IP问题。小程序真机调试时,代码里的
https://127.0.0.1指向的是手机自己,不是电脑。需要改成电脑的局域网IP,同时把后端服务监听地址从127.0.0.1改成0.0.0.0。 - 微信公众平台域名白名单限制。正式环境请求的API域名必须在小程序管理后台配置为request合法域名,而且要求HTTPS。开发阶段可以在小程序开发者工具里勾选“不校验合法域名”,但真机预览时这个选项有时不生效,必须先在后台加一个临时的HTTPS域名。
正确的排查路径是:先确认后端端口能被本机外网访问(用手机浏览器访问电脑IP:端口测试),再检查小程序端代码里的URL有没有写死,最后确认微信后台域名配置。按这个顺序走,绝大部分联调不通的问题都能解决。
还有一个容易忽略的点:ThinkPHP默认的入口文件是public/index.php,如果你在Nginx里配置了伪静态但没有正确指向public目录,所有请求都会404。遇到接口404,先去看Web服务器配置,不要怀疑代码。
4. Laravel版本重构:我需要的不只是换个框架
用ThinkPHP跑通第一版后,第二版我用Laravel重写了后端。这个决定从一开始就很明确:不是ThinkPHP不好,而是Laravel的工程化能力更适合一个要长期迭代的项目。
4.1 Laravel的目录结构给了我什么
Laravel默认的目录结构从第一天起就在提示“正确的代码应该放在哪里”:
app/ ├── Http/ │ ├── Controllers/ # 控制器:只负责接收输入、调用服务、返回响应 │ ├── Middleware/ # 中间件:身份认证、权限控制、日志记录 │ └── Requests/ # FormRequest:参数校验独立成类 ├── Models/ # 模型:Eloquent ORM ├── Services/ # 业务逻辑服务层(自定义) ├── Jobs/ # 队列任务 └── Console/ └── Commands/ # 定时任务命令 routes/ └── api.php # API路由定义表单校验在FormRequest里做,业务逻辑抽到Service类,数据输出用API Resource格式化,队列任务处理消息推送。这种分层在项目有5个以上模块时会非常舒服。
4.2 用API Resource统一数据输出格式
Laravel的Eloquent API Resource让我最受益。它解决了所有PHP项目都会遇到的一个通病——同一个数据模型在不同接口返回的字段格式不统一。
例如HealthRecord资源定义如下:
namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class HealthRecordResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $this->id, 'record_date' => $this->record_date->format('Y-m-d'), 'systolic_pressure' => $this->systolic_pressure, 'diastolic_pressure' => $this->diastolic_pressure, 'glucose_value' => $this->glucose_value, 'heart_rate' => $this->heart_rate, 'feeling_label' => $this->feeling_label, 'is_abnormal' => $this->isAbnormal(), ]; } }然后在控制器里:
public function list(Request $request) { $records = HealthRecord::where('user_id', $request->user_id) ->whereBetween('record_date', [ now()->subDays(30)->toDateString(), now()->toDateString(), ]) ->orderBy('record_date', 'desc') ->get(); return HealthRecordResource::collection($records); }只要接口返回HealthRecord,格式就完全统一,不会出现“手机端快排”“后台排序”“报表端排序”各写一套字段的情况。后期要给前端加字段,只需改资源文件一处。
4.3 用药提醒的定时任务设计
Laravel自带调度器,把用药提醒这种周期任务变得非常优雅。在app/Console/Kernel.php里配置:
protected function schedule(Schedule $schedule) { // 每天早上8点推送吃药提醒 $schedule->command('health:medication-reminder --time=morning')->dailyAt('08:00'); // 中午12点半推送午间提醒 $schedule->command('health:medication-reminder --time=noon')->dailyAt('12:30'); // 晚上7点推送晚间提醒 $schedule->command('health:medication-reminder --time=evening')->dailyAt('19:00'); // 每晚10点统计当日未打卡老人,生成提醒任务 $schedule->command('health:check-missed-checkin')->dailyAt('22:00'); }然后是命令类:
namespace App\Console\Commands; use App\Models\MedicationPlan; use App\Services\WechatSubscribeMessage; use Illuminate\Console\Command; class MedicationReminder extends Command { protected $signature = 'health:medication-reminder {--time=morning}'; protected $description = '按计划发送用药提醒'; public function handle() { $time = $this->option('time'); $plans = MedicationPlan::where('time_slot', $time) ->where('is_active', 1) ->with('elder') ->get(); foreach ($plans as $plan) { // 组装微信订阅消息,压入队列 dispatch(new SendMedicationReminderJob($plan)); $this->info("已加入推送队列:老人ID {$plan->elder_id}"); } } }这里要提醒:服务器上要配置crontab每分钟执行一次php artisan schedule:run。直接在crontab里写多个php artisan command:xxx不是好的做法,Laravel的调度器会把所有定时任务统一管理起来,修改任务时间只需改代码后重启调度,不用反复改系统crontab。
4.4 用队列处理微信订阅消息推送
微信订阅消息有一个限制:用户必须主动订阅一次,才能收到一次推送,且推送后订阅次数减一。这要求后端必须有可靠的“预发送-扣除次数-发送”流程,队列在这里价值很大。
我设计了这样的发送流程:
- 老人提交健康数据时,如果有异常,就为每个绑定的子女生成一条“待发送订阅消息”任务,存入
message_queue表,记录模板ID、接收者openid、数据字段、发送状态。 - 定时任务每分钟扫描一次
message_queue表,把状态为待发送的记录取出,调用微信接口发送。 - 发送成功则标记完成;发送失败则重试,重试三次仍失败标记为失败并记录日志。
使用Laravel队列来实现,代码大概这样:
namespace App\Jobs; use App\Models\MessageQueue; use App\Services\WechatSubscribeMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; class SendSubscribeMessageJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $queue = 'wechat_message'; public $tries = 3; protected $taskId; public function __construct($taskId) { $this->taskId = $taskId; } public function handle() { $task = MessageQueue::with('user')->find($this->taskId); if (!$task) return; $service = app(WechatSubscribeMessage::class); $result = $service->send($task); if ($result['success']) { $task->update(['status' => 2, 'send_time' => now()]); } else { Log::warning('订阅消息发送失败', ['task_id' => $task->id, 'error' => $result['msg']]); $this->release(60); } } }队列驱动我选了Redis,因为项目规模不大,但需要保证消息不丢失。如果服务器内存吃紧,用database驱动也可以,只是每个待处理任务会多一次数据库查询,性能略低。
5. 两套框架的实战对比数据
说了这么多理论,直接把实际数据摆出来更有说服力。同样是这个空巢老人健康管理系统,我分别用ThinkPHP和Laravel实现了一遍,下面是对比结果。
| 对比维度 | ThinkPHP 6 | Laravel 10 |
|---|---|---|
| 前期开发环境搭建时间 | 约1小时(文档清晰,下载即用) | 约2小时(需要额外配置Composer、环境依赖) |
| 实现核心接口(约20个) | 6天 | 8天 |
| 代码量(后端) | 约8000行 | 约6500行 |
| 定时任务实现 | 需要自行集成相关扩展 | 原生调度器,配置即用 |
| 队列处理 | 需要额外安装扩展包 | 原生队列+多种驱动 |
| 异常处理机制 | 相对简单,统一返回JSON需自行封装 | 自带异常处理层,可以精细控制HTTP响应 |
| 数据库迁移 | 需要手动建表或使用插件 | 自带迁移文件,版本化管理 |
| 团队协作友好度 | 一般,约定少,自由度高 | 较好,目录分层清晰 |
| 中文资料丰富度 | 很高 | 较高 |
| 长线迭代体验 | 项目变大后控制器容易膨胀 | 分层清晰,扩展新模块方便 |
从开发效率上看,ThinkPHP前期确实快一些,尤其是单兵作战写CRUD接口的时候,少了很多“仪式感”的代码。但我个人的体感是,当业务接口超过20个、需求开始频繁调整时,Laravel的工程化优势会逐渐追上那两三天的开发周期差距。
举一个最直观的例子:在ThinkPHP版本里,一个健康记录列表接口可能同时承担了老人端、子女端、后台三个角色,靠参数区分返回字段,代码里全是if ($role == 'admin')这样的分支。在Laravel版本里,我直接将列表接口拆成三个专用的API Resource,不同的角色走不同的Resource,代码逻辑一目了然,测试也好写得多。
6. 健康数据项目必须注意的隐私与安全细节
做健康管理系统,用户数据的安全责任比普通电商项目重得多。老人健康数据属于个人敏感信息,一旦泄露,风险是真实存在的。我在项目上线前专门花了一周做安全加固,下面这几个方面是每个人都应该注意的。
6.1 登录态与Token管理
JWT token的有效期我设置为7天,老人端和子女端每7天需要重新登录一次。其实这个小程序来说有点长,但考虑到老人操作能力有限,频繁登录体验太差,7天是一个折中的选择。
实现上,我用中间件拦截所有API请求(除了login和register),校验token是否有效。
Laravel中间件示例:
namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Tymon\JWTAuth\Facades\JWTAuth; use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Tymon\JWTAuth\Exceptions\TokenInvalidException; class CheckJwtToken { public function handle(Request $request, Closure $next) { try { $user = JWTAuth::parseToken()->authenticate(); if (!$user || $user->status != 1) { return response()->json(['code' => 1003, 'msg' => '账号已被禁用'], 401); } } catch (TokenExpiredException $e) { return response()->json(['code' => 1002, 'msg' => '登录已过期,请重新登录'], 401); } catch (TokenInvalidException $e) { return response()->json(['code' => 1001, 'msg' => 'token无效,请重新登录'], 401); } catch (\Exception $e) { return response()->json(['code' => 9999, 'msg' => '未知认证错误'], 401); } $request->merge(['auth_user' => $user]); return $next($request); } }6.2 角色权限控制的落地
健康数据非常敏感,必须严格按角色控制可见范围。我设计了三个权限规则:
- 老人角色(elder):只能查看和操作自己的数据。
- 子女角色(child):只能查看已绑定的老人数据,不能修改老人本人的档案。
- 管理员角色(admin):可以查看全部老人的健康数据,用于社区统计和关怀工作。
Laravel的路由中间件配置:
Route::group(['middleware' => ['jwt.auth', 'role:elder,child,admin']], function () { Route::get('/health-records', [HealthRecordController::class, 'list']); Route::post('/health-records', [HealthRecordController::class, 'store']); }); Route::group(['middleware' => ['jwt.auth', 'role:admin']], function () { Route::get('/admin/statistics/checkin-rate', [AdminController::class, 'checkinRate']); Route::get('/admin/elders', [AdminController::class, 'elderList']); }); Route::group(['middleware' => ['jwt.auth', 'role:child']], function () { Route::get('/family/elders', [FamilyController::class, 'myElders']); });6.3 小程序审核和隐私保护
微信小程序审核有一个容易踩的坑:涉及用户健康数据共享、分析的健康类小程序,需要申请对应的服务类目,并完整填写用户隐私保护指引。我最初提交审核时没有填写隐私保护指引,直接被拒,理由是“收集用户血糖、血压等信息未声明用途”。
后来按微信的要求补全了隐私保护指引:说明收集这些信息是为了健康管理和紧急情况下的联系与救助,并明确数据只用于本系统内部,不会向第三方提供。审核才顺利通过。
这里也提醒做类似项目的朋友:健康数据绝不能随意向第三方API传输或用作其他用途,这不仅是平台规范的要求,也是医疗健康类产品的基本底线。
数据存储层面,我在数据库中只保存了健康指标和openid,没有保存真实姓名、手机号等明文信息,即使数据库泄露,攻击者也无法直接定位到具体个人。老人的姓名和电话只存在管理后台的独立档案表里,且该表不参与小程序端数据联动。
7. 从这套系统里沉淀下来的实战经验
项目做了两轮框架迭代,踩了不少坑,也总结了一些方法。这些经验不局限于健康管理系统,凡是做“微信小程序 + PHP后端”的项目应该都用得上。
7.1 用原型图和时间线倒推技术方案
很多新手做这类系统喜欢一上来就写代码,写到一半发现需求理解偏了。我这次的做法是先花三天时间画出所有页面原型和接口清单,明确每个页面的数据来源和交互逻辑,再根据这个清单评估框架选型和时间安排。
原型图不需要很精致,哪怕用纸笔画都可以。关键是让所有参与的人(包括需求方)在开发前就对“系统长什么样、数据怎么流动”达成一致,避免后期返工。这个项目第一版的小程序端页面原型,后来基本没有大的改动。
7.2 给“非程序员角色”留好入口
这套系统面向的不只是年轻子女,还有社区工作人员——他们中有很多人一辈子没写过一行代码。所以我在设计管理后台时,特意避免大段技术术语,用“添加老人档案”“查看今日打卡”这种直白按钮替代专业词汇。数据统计页面直接给出图表,不强制用户理解背后的表结构和字段含义。
这个细节被需求方多次表扬,说“系统拿给社区阿姨用,阿姨也能看懂”。一个技术系统最终能被目标人群真正使用,比任何花哨的框架都要重要。
7.3 数据存储保留原始数据,统计逻辑单独建表
健康记录这种数据,一定要保留最原始的时间序列数据,不要只存汇总结果。老人血压今天可能测量三次,每一次的记录都要留档,趋势分析、医生问诊、子女查看报告都需要精确到每个时间点的数据。
同时统计报表用的聚合表单独维护,比如每小时的打卡率统计、每天的异常告警计数,用定时任务提前算好存进汇总表,查询报表时直接读汇总表,避免每次现算,接口响应会快得多。这个经验在ThinkPHP版后期才意识到,重构时果断换成了这种模式。
7.4 日志和监控要早做,别等事故发生后补
项目上线第一天我就把日志写全了:每个API请求记录用户ID、接口名、参数、响应时间、状态码。出事的时候(比如某个老人连续几天没打卡),通过日志能快速定位是老人没有操作,还是接口出了问题。
Laravel的日志系统很成熟,用Log::channel('daily')->info(...)就能按天记录。ThinkPHP也可以配置日志服务。建议无论用哪个框架,第一版就要把请求日志、错误日志、推送日志三套同时落地,否则排查问题时会非常痛苦。
7.5 关于线上服务器部署的最后一公里
健康管理系统这类项目通常是私有化部署,服务器配置一般不高。我的部署方案是:Nginx + PHP-FPM + MySQL,均为单机部署,高峰期并发量也就几十个请求,单服务器绰绰有余。但要注意PHP-FPM进程数配置和MySQL的连接数,别让默认配置成为瓶颈。
数据库每半年做一次全量备份,备份文件存到单独的对象存储,防止服务器磁盘坏掉导致数据全丢。老人的健康数据关系人命,数据安全再怎么谨慎都不为过。
8. 后记:这个项目的下一步想法
系统到现在已经稳定运行了大半年,累计管理老人档案超过200份,健康记录接近5万条。最让我有成就感的是,有一位老人的子女后台连续收到几次血压异常提醒后,及时带老人去医院检查,发现了早期高血压问题。这说明系统不是冷冰冰的代码堆砌,而是真正在帮人。
下一步我计划给系统加入更多能力:对接智能血压计的蓝牙数据(老人不用手动填写指标)、增加语音播报提醒(方便视力不好的老人)、把趋势分析接入医生的远程问诊系统(需要和数据合规团队一起评估)。这些功能的底层逻辑,和现有框架都是相通的。
做这个项目的过程中,我最大的收获不是学会了两个PHP框架,而是明白了一个朴素的道理:技术选型永远服务于业务场景。赶工期要快,就选ThinkPHP;要长期迭代要协作,就选Laravel。真正让一个系统活下来的,是清晰的架构、可靠的数据安全和持续响应用户需求的能力。希望这篇记录能帮到正在做类似项目的你,少走几步弯路。