ThinkPHP与Laravel双框架实战:空巢老人健康管理系统后端开发全记录
2026/9/19 18:31:04 网站建设 项目流程

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的目录约定很直观,controllermodelview一目了然,和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加上小程序的appidsecret去微信接口换取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 真机调试连不上后端的排查

这是整个开发过程中让我耗费最多时间的一个问题。模拟器上一切正常,一换真机就请求超时,折腾了一整个下午。

最终排查结论是两方面的原因:

  1. 本机开发环境下的局域网IP问题。小程序真机调试时,代码里的https://127.0.0.1指向的是手机自己,不是电脑。需要改成电脑的局域网IP,同时把后端服务监听地址从127.0.0.1改成0.0.0.0
  2. 微信公众平台域名白名单限制。正式环境请求的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 用队列处理微信订阅消息推送

微信订阅消息有一个限制:用户必须主动订阅一次,才能收到一次推送,且推送后订阅次数减一。这要求后端必须有可靠的“预发送-扣除次数-发送”流程,队列在这里价值很大。

我设计了这样的发送流程:

  1. 老人提交健康数据时,如果有异常,就为每个绑定的子女生成一条“待发送订阅消息”任务,存入message_queue表,记录模板ID、接收者openid、数据字段、发送状态。
  2. 定时任务每分钟扫描一次message_queue表,把状态为待发送的记录取出,调用微信接口发送。
  3. 发送成功则标记完成;发送失败则重试,重试三次仍失败标记为失败并记录日志。

使用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 6Laravel 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。真正让一个系统活下来的,是清晰的架构、可靠的数据安全和持续响应用户需求的能力。希望这篇记录能帮到正在做类似项目的你,少走几步弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询