ThinkPHP和Laravel两个版本的学生信息管理系统,算是PHP后端开发里一个比较典型的练手项目。这个标题其实暴露了不少信息:学生信息管理是Web开发中最常见的CRUD业务之一,覆盖了增删改查、分页搜索、关联查询、权限控制这些基本功;而同时用ThinkPHP和Laravel跑一套业务,本质上就是在做框架对比和迁移复刻,对技术选型和框架差异的理解非常有帮助。
这篇内容我会按照实际开发流程去讲:从需求拆解、数据库设计、两个框架各自的核心实现,到具体的路由、控制器、模型、表单验证、分页搜索这些细节,再到部署时容易踩的坑。全程用我自己实际做过这个项目的经验来写,代码直接给你能用的版本。
1. 需求复盘与技术方案选型逻辑
1.1 学生信息管理系统到底在管什么
学生信息管理系统这个题目,看着平平无奇,但其实是无数计算机专业学生和初级后端开发者的第一课。它的业务本质就是围绕"学生"这个核心实体做一套完整的数据管理闭环。
打开系统需要的第一个页面,肯定是登录。登录背后是用户表、密码加密、会话管理、权限校验。登录之后是学生列表,列表背后是分页、搜索、排序、筛选。点进详情,是学生基础信息、所在班级、成绩记录。管理员操作层面,是新增学生、编辑学生、删除学生、批量导入导出。这些看似琐碎的功能,拆开来看就是:用户认证与授权、单表CRUD、一对一关联、一对多关联、多对多关联、文件上传与解析、数据导出。
值得留意的是,这个项目标题把"ThinkPHP"和"Laravel"并列放在一起,说明需求方希望同一套业务逻辑分别在两个框架里跑通。这种项目的价值不在于功能本身有多复杂,而在于同一套业务规则在不同框架下的实现差异,你在写第二遍的时候,对框架的体会会远远超过写第一遍的时候。
1.2 为什么同时用两个框架做同一套系统
我在实际做这类项目时,通常建议先理清两个框架的定位差异。
ThinkPHP在国内的普及度非常高,特别是在很多传统企业和外包项目里,它的中文文档和相对简单的上手曲线让团队能快速交付。它的数据库操作封装得比较直白,模板引擎也是自有的,整体风格偏向"怎么方便怎么来"。Laravel则是国际主流的现代PHP框架代表,遵循更多的设计模式,Eloquent ORM、中间件、服务容器、Blade模板这些概念组合在一起,工程结构更清晰,但学习曲线也更陡一些。
如果是一个实际交付的商业项目,我大概率只会选其中一个。但如果目标是做技术对比学习、技术栈迁移验证、或者教学演示,双版本就是很好的组合。比如你让一个团队从ThinkPHP往Laravel迁移,代码结构变化很大,但业务逻辑可以完全复用。做这个项目的过程中,你会把路由定义、模型关联、表单验证、中间件、模板渲染等概念在两种语境下各学一遍,后面再碰到其他框架,很快就能上手。
1.3 核心模块划分
根据需求拆解,我把系统分成四个模块:
- 登录认证模块:支持用户登录、退出登录、登录状态校验,区分管理员和普通用户。
- 学生管理模块:学生信息的增删改查,支持按姓名、学号、班级、状态筛选,支持分页。
- 班级管理模块:维护班级列表,每个班级关联所属专业和年级。
- 成绩管理模块(可选扩展):记录学生各科成绩,支持按学生查看成绩列表。
实际开发中,我建议先做前三个模块,跑通之后再往里面加成绩模块。不要把第一版就设计成大而全的系统,先把核心链路打通,后续扩展的成本会低很多。
2. 数据库设计与环境准备
2.1 数据库表结构设计
学生信息管理系统的基础是几张核心表。我实际用的结构如下:
学生表 students
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int 主键自增 | 学生ID |
| student_no | varchar(20) unique | 学号 |
| name | varchar(50) | 姓名 |
| gender | tinyint | 0男 1女 |
| class_id | int 外键 | 所属班级 |
| phone | varchar(20) | 手机号 |
| varchar(100) | 邮箱 | |
| status | tinyint | 1正常 0停用 |
| created_at | datetime | 创建时间 |
| updated_at | datetime | 更新时间 |
班级表 classes
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int 主键自增 | 班级ID |
| class_name | varchar(50) | 班级名称 |
| grade | varchar(20) | 年级 |
| major | varchar(50) | 专业 |
用户表 users
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int 主键自增 | 用户ID |
| username | varchar(50) unique | 账号 |
| password | varchar(255) | 密码哈希 |
| role | tinyint | 1管理员 2普通用户 |
成绩表 scores
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int 主键自增 | 成绩ID |
| student_id | int 外键 | 学生ID |
| course_name | varchar(50) | 课程名 |
| score | decimal(5,2) | 分数 |
| exam_date | date | 考试日期 |
外键关系就是:学生属于班级(多对一),学生拥有成绩(一对多)。post
2.2 ThinkPHP侧环境准备
ThinkPHP 6.0以上版本要求PHP >= 7.2.5,我建议直接用PHP 8.1,更稳定。用Composer创建项目:
composer create-project topthink/think tp-student创建完成后进入项目目录,确认目录结构:
- app/controller:控制器文件
- app/model:模型文件
- config/database.php:数据库配置
- route/app.php:路由定义文件
数据库配置在 config/database.php,填好数据库名、用户名、密码,然后把建表SQL执行一下即可。我在实操中遇到的第一个坑是:TP6默认走的是app\model命名空间,而不是ThinkPHP5的model(全小写目录),很多新手在这里迷茫很久。
2.3 Laravel侧环境准备
Laravel 10/11版本要求PHP >= 8.1,创建项目同样走Composer:
composer create-project laravel/laravel laravel-studentLaravel的项目结构比ThinkPHP重不少,核心关注点如下:
- routes/web.php:所有web路由都写在这里
- app/Models:Eloquent模型文件
- app/Http/Controllers:控制器
- database/migrations:数据库迁移文件
- resources/views:Blade模板
Laravel的数据库配置在 .env 文件里,改一下 DB_DATABASE、DB_USERNAME、DB_PASSWORD 即可。创建表和字段建议用迁移而不是手动建表,这是Laravel工程化的体现之一,后面细说。
3. 核心功能实现方案:同一套业务,两种框架的写法
3.1 路由定义与访问入口
路由是框架的入口,两个框架的路由写法差别非常明显。
ThinkPHP 6的路由定义在 route/app.php,必须先在 config/route.php 里开启强制路由(也可以依赖默认的控制器路由)。我建议直接显式定义路由,因为TP的隐式路由在部署到Nginx后经常出现URL重写问题,尤其是带pathinfo的场景。
// 学生管理 Route::get('student/list', 'Student/list'); Route::get('student/create', 'Student/create'); Route::post('student/save', 'Student/save'); Route::get('student/edit/:id', 'Student/edit'); Route::post('student/update', 'Student/update'); Route::post('student/delete', 'Student/delete');Laravel 11的路由定义在 routes/web.php,使用的是闭包或者控制器方法引用:
Route::get('/students', [StudentController::class, 'index'])->name('students.index'); Route::get('/students/create', [StudentController::class, 'create'])->name('students.create'); Route::post('/students', [StudentController::class, 'store'])->name('students.store'); Route::get('/students/{student}/edit', [StudentController::class, 'edit'])->name('students.edit'); Route::put('/students/{student}', [StudentController::class, 'update'])->name('students.update'); Route::delete('/students/{student}', [StudentController::class, 'destroy'])->name('students.destroy');Laravel的路由会自动绑定模型,{student} 参数可以直接在控制器方法里注入Student模型实例,这个体验非常好,省去手动查一遍数据的繁琐代码。而ThinkPHP需要自己在控制器里用find查一次。
3.2 学生列表的查询逻辑
学生列表是核心页面,要做好三件事:关联班级、分页显示、条件筛选。
ThinkPHP的模型查询,主要是依靠链式操作:
$list = Student::alias('s') ->join('class c', 's.class_id = c.id') ->field('s.*, c.class_name, c.grade') ->where('s.status', 1) ->whereLike('s.name', '%' . $keyword . '%') ->paginate(10);paginate(10)是内置的分页方法,返回的分页对象自带 render() 方法,模板里直接渲染页码,非常方便。注意whereLike拼接搜索关键词时,要处理好特殊字符,防止SQL注入风险。
Laravel的Eloquent写法更贴近面向对象的感觉:
$students = Student::query() ->with('studentClass') ->when($keyword, function ($query) use ($keyword) { $query->where('name', 'like', '%' . $keyword . '%') ->orWhere('student_no', 'like', '%' . $keyword . '%'); }) ->paginate(10);with('studentClass')是预加载关联模型,减少N+1查询问题。when方法则是条件查询的优雅写法,只有条件为真时才执行闭包里的逻辑。分页数据在前端Blade模板里通过$students->links()渲染,底层也是调用paginate。
两边的排序逻辑都差不多:
// TP ->order('s.id', 'desc') // Laravel ->orderByDesc('id')3.3 新增与编辑数据的表单验证
表单验证是学生信息管理里最容易忽略但又最重要的环节。如果你不对手机号格式、学号唯一性做校验,脏数据很快就会把数据库搞乱。
ThinkPHP的内置验证器写法:
$validate = Validator::make($data, [ 'student_no' => 'require|unique:student', 'name' => 'require|max:50', 'phone' => 'require|regex:/^1[3-9]\d{9}$/', 'class_id' => 'require|number' ]); if ($validate->fails()) { return json(['code' => 0, 'msg' => $validate->getError()]); }Laravel的表单验证可以用FormRequest或者在控制器里用validate方法:
$validated = $request->validate([ 'student_no' => 'required|unique:students,student_no,' . $student->id, 'name' => 'required|max:50', 'phone' => 'required|regex:/^1[3-9]\d{9}$/', 'class_id' => 'required|exists:classes,id', ]);Laravel的unique验证有个细节:更新时要排除当前记录自身,所以在规则里加了,后面带上学生ID。如果你忘掉这个细节,编辑一个学生不改学号也会提示学号已存在,这种低级Bug我踩过,印象很深。
3.4 删除功能的实现
删除功能不止是前端按钮加个confirm弹窗,后端还要考虑依赖数据。如果一个学生已经有了成绩记录,直接删掉学生会让成绩变成孤儿数据。
ThinkPHP删除:
$student = Student::find($id); if (!$student) return json(['code' => 0, 'msg' => '学生不存在']); $student->delete();Laravel删除(支持软删除场景):
$student = Student::findOrFail($id); $student->delete();如果表里有外键约束,直接删除会报SQL错误。所以我在设计数据库时有两种方案:一是物理外键加ON DELETE CASCADE,二是代码层先删除关联成绩再删除学生。实际项目我更倾向于代码层处理,因为物理外键在高并发下会带来额外的数据库开销,对于这样的学生管理系统用代码层维护一致性就够了。
4. 框架核心差异深度拆解
4.1 ORM对比:think-orm vs Eloquent
TP6使用的think-orm和Laravel的Eloquent看着很像,实际使用体验差异很大。
think-orm定义模型:
namespace app\model; use think\Model; class Student extends Model { protected $name = 'student'; protected $autoWriteTimestamp = true; public function studentClass() { return $this->belongsTo(ClassModel::class, 'class_id'); } public function scores() { return $this->hasMany(Score::class, 'student_id'); } }Eloquent定义模型:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; class Student extends Model { protected $fillable = ['student_no', 'name', 'gender', 'class_id', 'phone', 'email', 'status']; public function studentClass(): BelongsTo { return $this->belongsTo(StudentClass::class, 'class_id'); } public function scores(): HasMany { return $this->hasMany(Score::class, 'student_id'); } }Eloquent的$fillable是白名单机制,防止批量赋值带来的安全隐患,这是Laravel很强调的一点。而TP的模型默认可以批量赋值,如果你在框架层面做了限制,需要单独配置。从安全角度来说,Eloquent的白名单设计更稳健。
另外Eloquent的修改器、访问器、事件机制(creating/created/updating/updated)在复杂业务里非常有用。比如自动生成学号,可以在模型的creating事件里处理;自动格式化日期,可以在serializeDate里处理。TP也有类似的模型事件,但整体生态的丰富度和文档完善度还是略逊一筹。
4.2 模板引擎对比:think-template vs Blade
ThinkPHP默认的模板引擎语法:
<table class="table"> <thead> <tr> <th>ID</th> <th>学号</th> <th>姓名</th> <th>班级</th> <th>操作</th> </tr> </thead> <tbody> {volist name="list" id="item"} <tr> <td>{$item.id}</td> <td>{$item.student_no}</td> <td>{$item.name}</td> <td>{$item.class_name}</td> <td> <a href="/student/edit/id/{$item.id}">编辑</a> <a href="javascript:;" class="del-btn"><table class="table"> <thead> <tr> <th>ID</th> <th>学号</th> <th>姓名</th> <th>班级</th> <th>操作</th> </tr> </thead> <tbody> @foreach($students as $student) <tr> <td>{{ $student->id }}</td> <td>{{ $student->student_no }}</td> <td>{{ $student->name }}</td> <td>{{ $student->studentClass->class_name ?? '-' }}</td> <td> <a href="{{ route('students.edit', $student->id) }}">编辑</a> <a href="javascript:;" class="del-btn">namespace app\middleware; use think\facade\Session; use think\facade\Redirect; class AuthCheck { public function handle($request, \Closure $next) { if (!Session::has('user_id')) { return redirect('/login'); } return $next($request); } }在路由里应用中间件:
Route::group('student', function(){ Route::get('list', 'Student/list'); })->middleware(\app\middleware\AuthCheck::class);Laravel中间件(app/Http/Middleware/CheckRole.php):
namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class CheckRole { public function handle(Request $request, Closure $next, string $role) { if (!Auth::check()) { return redirect()->route('login'); } if ($role === 'admin' && Auth::user()->role !== 1) { abort(403, '无权限访问'); } return $next($request); } }在路由里应用中间件并带参数:
Route::middleware('auth')->group(function () { Route::get('/students', [StudentController::class, 'index'])->name('students.index'); Route::post('/students/delete', [StudentController::class, 'destroy'])->middleware('CheckRole:admin'); });Laravel的中间件支持参数传递,一个中间件可以实现不同角色的不同校验逻辑,功能设计上更有层次感。TP6从5.x版本开始引入中间件,但整体生态的中间件可复用程度不如Laravel高。
5. 实操过程:双版本开发的完整流程记录
5.1 ThinkPHP版本开发顺序
实际开发时,我习惯按这个顺序推进:用户登录 → 学生列表 → 新增学生 → 编辑学生 → 删除学生 → 班级管理 → 分页搜索优化。
第一步,创建数据库并执行建表SQL。直接在MySQL里执行即可,注意表名不加前缀,TP和Laravel的默认习惯略有不同,TP建议加前缀(默认tp_),Laravel则不需要。
第二步,配置TP数据库连接。在 config/database.php 中修改:
'default' => 'mysql', 'connections' => [ 'mysql' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'student_db', 'username' => 'root', 'password' => '你的密码', 'hostport' => '3306', 'charset' => 'utf8mb4', 'prefix' => '', 'fields_strict' => false, ], ],第三步,写控制器。Student控制器的核心结构如下,这个类主要在app/controller/Student.php:
namespace app\controller; use app\model\Student; use app\model\StudentClass; use think\facade\View; use think\facade\Request; use think\facade\Validate; use think\response\Json; class Student { // 列表页 public function list() { $keyword = trim(Request::param('keyword', '')); $classId = Request::param('class_id', ''); $query = Student::alias('s') ->join('class c', 's.class_id = c.id') ->field('s.*, c.class_name, c.grade') ->where('s.status', '=', 1); if ($keyword !== '') { $query->whereLike('s.name|s.student_no', '%' . $keyword . '%'); } if ($classId !== '') { $query->where('s.class_id', (int)$classId); } $list = $query->order('s.id', 'desc')->paginate(10); $classList = StudentClass::field('id, class_name')->select(); View::assign('list', $list); View::assign('classList', $classList); View::assign('keyword', $keyword); View::assign('classId', $classId); return View::fetch('student/list'); } // 新增保存 public function save() { if (!Request::isPost()) { return json(['code' => 0, 'msg' => '非法请求']); } $data = [ 'student_no' => trim(Request::post('student_no')), 'name' => trim(Request::post('name')), 'gender' => Request::post('gender', 0), 'class_id' => Request::post('class_id', 0), 'phone' => trim(Request::post('phone')), 'email' => trim(Request::post('email')), ]; $validate = Validate::rule([ 'student_no' => 'require|unique:student', 'name' => 'require|max:50', 'phone' => 'require|regex:/^1[3-9]\d{9}$/', 'class_id' => 'require|number', ])->message([ 'student_no.require' => '学号不能为空', 'student_no.unique' => '学号已存在', 'name.require' => '姓名不能为空', 'phone.regex' => '手机号格式不正确', ]); if (!$validate->check($data)) { return json(['code' => 0, 'msg' => $validate->getError()]); } $student = Student::create($data); if ($student) { return json(['code' => 1, 'msg' => '保存成功']); } return json(['code' => 0, 'msg' => '保存失败']); } }这是学生模块的列表和新增保存,其他编辑、删除基本类似。TP的ORM方法链路清晰,文档能覆盖大多数场景,遇到getter、setter相关的需求再按文档来。
5.2 Laravel版本开发顺序
Laravel版适合用Artisan命令行工具加快开发效率。创建模型和迁移文件:
php artisan make:model Student -m php artisan make:model StudentClass -m php artisan make:controller StudentController --resource php artisan make:middleware CheckRole-m参数会自动创建迁移文件。在 database/migrations 里编写表结构:
public function up(): void { Schema::create('students', function (Blueprint $table) { $table->id(); $table->string('student_no', 20)->unique(); $table->string('name', 50); $table->tinyInteger('gender')->default(0); $table->foreignId('class_id')->constrained('classes'); $table->string('phone', 20)->nullable(); $table->string('email', 100)->nullable(); $table->tinyInteger('status')->default(1); $table->timestamps(); }); }然后执行迁移:
php artisan migrate这种迁移方式的好处是所有表结构变更都在代码里,团队协作时不会出现"我改了数据库你那边还是旧结构"的问题。
StudentController的index方法:
public function index(Request $request) { $keyword = trim($request->get('keyword', '')); $classId = $request->get('class_id', ''); $students = Student::query() ->with('studentClass') ->when($keyword, function ($query) use ($keyword) { return $query->where(function ($q) use ($keyword) { $q->where('name', 'like', "%{$keyword}%") ->orWhere('student_no', 'like', "%{$keyword}%"); }); }) ->when($classId, function ($query) use ($classId) { return $query->where('class_id', $classId); }) ->orderByDesc('id') ->paginate(10); $classes = StudentClass::all(); return view('students.index', compact('students', 'classes', 'keyword', 'classId')); }这里有个细节:搜索时如果同时有"姓名"和"学号"两个条件,要用嵌套 where 包起来,否则 orWhere 会把班级筛选条件也搞乱,查出来的数据不符合预期。这个在ThinkPHP里也有同样问题,whereLike('name|student_no') 是or逻辑,但后续叠加的and条件要放在正确的括号里,这一点新手很容易踩坑。
表单验证写法前面已经列过,这里补充一下,Laravel的表单验证失败时默认会重定向回上一页并把错误信息闪存到session,前端通过@error('name')显示错误提示,体验非常友好。
5.3 分页展示的样式与处理
ThinkPHP的分页渲染默认样式比较朴素,如果要主题化,可以引入模板继承或直接把 paginate 对象转成数组,自己渲染。我实际项目里是自定义了分页模板,跟后台UI框架统一样式。
Laravel的分页在Blade里渲染特别方便,不管是默认的Tailwind还是Bootstrap,Laravel都内置了支持。它通过links()方法渲染,分页按钮、上一页下一页、页码信息都处理好了,几乎零成本接入。
如果业务上需要更复杂的分页,比如"跳转指定页"、"每页条数切换",两个框架都能通过参数配置实现,但Laravel的组件化语法写起来更顺手。
6. 从开发到部署:常见的坑与排查思路
6.1 ThinkPHP的经典坑位
第一个坑是URL路由在Nginx下失效。TP6的路由地址依赖pathinfo模式,Nginx默认配置没加try_files的话,访问/student/list会直接404。解决办法是配置Nginx规则,让所有请求都转发给index.php:
location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; break; } }或者更简单的方案,把所有框架入口都改成php artisan serve走内置服务器先开发调试,部署时再调整Nginx。
第二个坑是TP的验证器unique规则默认是查当前连接库的表名,如果你的模型表名映射了别名,unique验证可能查错表。解决办法是指定完整的验证规则:unique:student,student_no,不要简写。
第三个坑是whereLike('s.name|s.student_no', ...)这种写法,TP6的字段或查询在5.x和6.0的语法不完全兼容,一定要确认框架版本对应正确的写法。
6.2 Laravel的经典坑位
Laravel最常见的坑是Composer依赖版本冲突。特别是已经跑起来的Laravel项目,如果你手动改了composer.json把版本号改高了,再composer update极有可能直接崩掉。我的建议是永远用composer create-project laravel/laravel重新建项目,不要拿旧项目升级大版本,Laravel大版本之间的差异不是简单改版本号就能平滑的。
第二个坑是目录权限。Laravel的storage/和bootstrap/cache/目录需要写入权限,部署到Linux服务器后忘记chmod -R 775 storage bootstrap/cache,打开页面就会报错,而且错误信息很不直观,尤其是PHP-FPM作为www用户运行时。
第三个坑是.env文件。Laravel如果开了调试模式下忘记配置正确的APP_KEY,加密相关功能会异常。我第一次跑Laravel时就是忘了php artisan key:generate,导致密码哈希和session都报错,排查了好一会儿才反应过来。
6.3 安全与上线检查清单
学生管理系统涉及学生隐私数据,上线前必须做安全检查。我从实际项目中整理了一份自检清单:
- 密码必须用框架内置的哈希算法,TP用
password_hash(),Laravel用bcrypt()或者Hash::make()。 - 所有涉及用户输入的位置都要转义,模板输出默认开启过滤。
- 登录接口必须有防暴力破解策略,最简单的方案是限制单IP连续失败次数。
- 文件上传功能如果做成Excel导入,必须校验文件MIME类型和扩展名,防止上传可执行脚本。
- 删除操作建议做二次确认,接口侧通过POST或者DELETE请求,禁用GET直接删除。
- 生产环境必须关闭调试模式:TP设置
app_debug为false,Laravel设置APP_DEBUG=false。
这几点你要是都能落实,系统基本能扛住绝大多数常见的Web攻击了。
7. 数据导入导出与成绩模块的扩展实现(选修)
前面只讲了基础CRUD,实际交付的学生管理系统一般还要求Excel导入导出,以及成绩管理。这块我挑一个重点讲,就是成绩模块的扩展思路。
成绩表和学生的关联是典型的一对多。判断学生是否有成绩只需要查 scores 表里 student_id 是否存在记录即可。后端接口按学生ID查询成绩:
// Laravel版本 public function scores(Student $student) { $scores = $student->scores()->orderByDesc('exam_date')->get(); return view('students.scores', compact('student', 'scores')); }在这个页面上,老师可以为该学生录入各科成绩。表单里要校验的是课程名不能为空、分数在0到100之间、考试日期不能选未来日期。
Excel导入导出我用的是开源库,PHP侧可选phpoffice/phpspreadsheet,Laravel侧也可用Maatwebsite/Excel,实际使用起来差别不大。导入流程很简单:
- 上传Excel文件
- 解析每一行数据
- 校验学号是否存在、字段是否合格
- 批量插入数据库
- 返回导入成功与失败条数
这块是实际项目里用户反馈使用频率最高的功能,因为手工录入上百个学生的信息效率太低。建议就算是练手项目,也把导入导出做上,对面试和实际工作都有帮助。
8. 关于性能优化的几点实操经验
学生管理系统的数据量一般来说不会太大,但不代表完全不考虑性能。我总结几个实际的优化点:
- 列表页的SQL必须加索引。student_no唯一索引,class_id普通索引,score表里student_id和course_name联合索引。索引建好了,即使数据到几十万条,查询也很快。
- 关联查询尽量用预加载,避免N+1。Laravel的with、TP的with都能解决这个问题。如果你发现列表页的SQL执行了成千上万次,基本就是N+1问题。
- 分页查询不要用
offset跳过太多数据,深分页是个性能隐患。如果数据量真的很大,用游标分页或者id范围分页替代。 - 模板里尽量减少重复访问数据库的操作,比如在循环里调用模型关联。把数据提前算好,模板只负责渲染。
- 静态资源(JS、CSS、图片)放到CDN或者做负载均衡,这个是部署层面的常规操作。
这些优化点,对这样的系统已经足够。不要一开始就上Redis、ElasticSearch,先搞清楚瓶颈在哪再动手。
9. 最后想说点实在的
这个双版本学生系统做完之后,我最大的感受是:ThinkPHP适合快速理解Web开发的核心链路,因为它把很多事情简化了;Laravel适合建立工程化思维,因为它的分层、中间件、服务容器都在潜移默化地教你"代码应该怎么组织才更清晰"。
如果你正在学习PHP后端,我建议你按这个顺序来:先用ThinkPHP把学生信息的增删改查做明白,理解MVC、ORM、Session、分页这些基础概念;再用Laravel从头写一遍同一套系统,你会发现你开始考虑数据库迁移怎么管理、表单验证怎么复用、权限中间件怎么设计。这个对比的过程,比任何框架文档都更有教育意义。
最后一个小技巧:做这类双版本项目时,维护一份两个框架核心代码对照表。比如"TP的路由对应Laravel的routes","TP的模型对应Laravel的Eloquent Model","TP的中间件对应Laravel的Middleware"。遇到问题先查这张表,基本能解决80%的"这个功能在两边的用法是什么"的疑问。我自己的这张表在之后的多个项目里都派上了用场。