外观
LIMS Beta Laravel 开发手册
适用范围:
beta/下的 Laravel 应用
培训目标:理解一个请求怎样进入系统、怎样被拦截、业务代码应该写在哪一层
使用方式:本文用于理解项目架构和日常开发方式。实际开发、评审和验收以beta/DEVELOPMENT.md及对应专项规范为准。
最后核验:2026-08-10(PHP 8.4、Laravel 13、当前模块化装配链路)。
本文中的路径、命令和代码示例以核验时的实现为准。开始修改前,先查看附录中的实际文件、php artisan route:list 和任务涉及的专项文档;Schema、RBAC、客户配置、部署和受控文件流程不要只凭通用 Laravel 经验处理。
阅读导航
- 一至四:从遗留 PHP 到 Laravel 请求生命周期
- 五至九:路由、权限、参数、Controller 与 Service
- 十至十四:数据访问、依赖注入和模块组织
- 十五至十七:响应、真实请求和新增功能步骤
- 十八至二十一:常见错误、运行排查、测试和检查清单
- 二十二:练习:创建博客模块
- 附录:常用代码位置和专项规范
建议学习顺序
| 场景 | 建议内容 |
|---|---|
| 起步 | 第一、四至十、十二、十六和二十二章 |
| 开始实际开发 | 再读第十一、十三至十五和十七章,并以 beta/DEVELOPMENT.md 为开发规范 |
| 提交代码之前 | 第十八至二十一章和本次任务对应的专项规范 |
| 遇到具体问题 | 通过附录定位代码、权限、配置、Schema 或部署文档 |
第一次阅读可以先了解请求链路和分层。开始具体任务后,再按需要查阅对应章节和正式规范。
一、先看整体变化
以前常见的方式是“文件就是入口”:
- 浏览器直接访问某个 PHP 文件;
- 文件里
include配置和函数; - 读取
$_GET、$_POST和全局变量; - 判断权限、执行 SQL、输出 HTML;
- 一个文件可能包含完整流程。
Beta 使用 Laravel 后,变成“统一入口 + 路由 + 分层处理”:
| 对比项 | 以前的常见方式 | Beta Laravel 方式 |
|---|---|---|
| 类加载 | 手工 include、require | Composer 按 PSR-4 自动加载 |
| 请求入口 | 每个 PHP 文件都可能是入口 | 统一进入 beta/public/index.php |
| 功能定位 | 文件路径、act 参数 | HTTP 方法、URI、路由 |
| 登录和权限 | 各入口零散判断 | 中间件、Request、Service 分层校验 |
| 参数验证 | 手工逐项判断 | FormRequest 统一验证 |
| 业务逻辑 | 页面、SQL、HTML 混在一起 | Controller 调用 Service |
| 数据访问 | 页面直接查库 | QueryService、Repository、Model |
| 对象创建 | 手工 new、依赖全局对象 | Laravel 容器依赖注入 |
| 错误处理 | echo、die、各自返回 | 统一异常和响应格式 |
原来的做法是“找到文件并执行”;Laravel 的做法是“请求先进入框架,再由路由和各层代码处理”。
两套环境必须分清
| 范围 | 运行要求 | 开发注意 |
|---|---|---|
beta/ | Laravel 13、PHP 8.4+、Composer | 可以使用现代 PHP 语法和 Laravel 能力 |
| 遗留模块 | 按项目约定兼容 PHP 5.6 | 不要把 PHP 7+、PHP 8+ 语法带入旧文件 |
beta/ 是渐进迁移入口,不代表遗留系统已经整体升级。
二、Composer 与 PSR-4 自动加载
Composer 不只是安装第三方包,也负责加载项目自己的类。
beta/composer.json 中的主要配置:
json
{
"autoload": {
"psr-4": {
"App\\": "app/",
"App\\Modules\\Customer\\": "app/Modules/Customer/src/",
"App\\Modules\\FileManagement\\": "app/Modules/FileManagement/src/",
"App\\Modules\\System\\": "app/Modules/System/src/"
},
"files": [
"app/Helpers/functions.php"
]
}
}App\\ 对应 beta/app/ 中的全局和 Foundation 类;模块 PHP 源码额外通过 App\\Modules\\<Module>\\ 映射到各自的 app/Modules/<Module>/src/。因此模块类名和文件路径存在固定关系:
| 类名 | 文件 |
|---|---|
App\Modules\FileManagement\Services\FileCommandService | app/Modules/FileManagement/src/Services/FileCommandService.php |
App\Modules\FileManagement\Models\FileManagementFile | app/Modules/FileManagement/src/Models/FileManagementFile.php |
使用时只需要 use:
php
use App\Modules\FileManagement\Services\FileCommandService;
final class FileController extends Controller
{
public function __construct(
private readonly FileCommandService $commands,
) {}
}使用规则
namespace必须和目录一致。- 类名必须和文件名一致。
- 引用类使用
use,不要为业务类写require_once。 - 不要修改
vendor/。 - 不要在 Controller 中
new Service()。 - 新模块或模块测试目录需要自动加载时,先补充对应的 Composer PSR-4 映射;修改自动加载配置后执行:
bash
cd beta
composer dump-autoload三、统一入口与 URL 重写
生产环境把外部 /beta/* 映射到 beta/public/*。
如果请求的不是实际静态文件,Web Server 会把请求交给:
text
beta/public/index.php例如:
| 位置 | 地址 |
|---|---|
| 浏览器访问 | /beta/filemanagement |
| Laravel 内部路由 | /filemanagement |
| PHP 统一入口 | beta/public/index.php |
注意:
/beta是部署层的外部路径。Laravel 路由中不要重复写/beta。
Nginx 的核心逻辑类似:
nginx
location /beta/ {
alias /path/to/lims3.0/beta/public/;
try_files $uri $uri/ /beta/index.php?$query_string;
}public/index.php 主要做五件事:
- 检查维护模式;
- 加载
vendor/autoload.php; - 加载
bootstrap/app.php; - 捕获当前 Request;
- 交给 Laravel 处理并返回 Response。
无论独立部署还是与遗留 LIMS 共存,对外都只能暴露 beta/public 中的内容。现有主站可以通过 /beta/ 的 Web Server 映射访问该目录,不要求把整个 LIMS 主站的 Web 根目录改为 beta/public;但不能直接暴露 beta/ 根目录,否则 .env、配置和源码可能被访问。
四、Laravel 请求生命周期
一个请求的完整处理顺序:
mermaid
flowchart LR
A["浏览器 /beta/*"] --> B["Web Server 重写"]
B --> C["public/index.php"]
C --> D["Composer 自动加载"]
D --> E["bootstrap/app.php"]
E --> F["全局 / web 中间件"]
F --> G["Router 匹配路由"]
G --> H["路由中间件"]
H --> I["FormRequest"]
I --> J["Controller"]
J --> K["Service"]
K --> L["Repository / Model"]
L --> M["Response"]对应职责:
| 顺序 | 环节 | 主要工作 |
|---|---|---|
| 1 | Web Server | 把 /beta/* 转到统一入口 |
| 2 | Composer | 自动加载框架类和业务类 |
| 3 | bootstrap/app.php | 注册路由、中间件和异常处理 |
| 4 | 全局 / web 中间件 | 处理 Session、Cookie、用户态桥接 |
| 5 | Router | 按 HTTP 方法和 URI 匹配路由 |
| 6 | 路由中间件 | 检查登录、功能开关和权限 |
| 7 | FormRequest | 整理输入、授权、验证 |
| 8 | Controller | 接收已验证数据,调用 Service |
| 9 | Service | 执行业务规则、事务、资源校验和审计 |
| 10 | Repository / Model | 查询、保存、关系和 Scope |
| 11 | Response | 返回 View、JSON、文件或重定向 |
排查请求时,按上表从入口、路由和中间件开始,依次检查到响应。
五、路由
路由说明“什么请求交给谁处理”。
一个完整路由通常包含:
- HTTP 方法;
- URI;
- Controller 和方法;
- 路由名称;
- 中间件;
- 参数约束。
项目示例:
php
Route::prefix('filemanagement')
->name('filemanagement.')
->middleware('filemanagement.permission:view')
->group(function (): void {
Route::post('/files', [FileController::class, 'store'])
->middleware('filemanagement.permission:manage')
->name('files.store');
Route::put('/files/{file}', [FileController::class, 'update'])
->whereNumber('file')
->middleware('filemanagement.permission:manage')
->name('files.update');
});路由约定
| 动作 | HTTP 方法 | Controller 方法 |
|---|---|---|
| 列表 | GET | index、list |
| 详情 | GET | show |
| 新增 | POST | store |
| 整体更新 | PUT | update |
| 局部更新 | PATCH | update 或明确动作方法 |
| 删除 | DELETE | destroy |
路由检查点
- 内部路由不写
/beta。 - 路由只负责匹配,不写业务逻辑。
- 路由必须命名,后端生成链接使用
route()。 - 数字 ID 使用
whereNumber()等约束。 - 同一模块的前缀、名称和中间件使用
group()统一声明。 - 模块路由放在模块自己的
routes/web.php。
文件管理模块路由位置:
text
beta/app/Modules/FileManagement/routes/web.php六、路由中间件
中间件负责在进入 Controller 前统一拦截请求。
允许访问时调用:
php
return $next($request);不允许访问时根据入口类型重定向到登录页,或直接返回、抛出 401、403。
Beta 后台模块使用遗留登录态时,legacy.auth 未通过通常会重定向到遗留系统登录页;只有进入 Laravel 认证异常处理链的 JSON/AJAX 请求,才会得到 401 JSON。不要把所有未登录情况都预期为 401。
当前项目中的主要中间件
| 中间件 | 用途 |
|---|---|
legacy.auth | 检查遗留系统登录态 |
LegacySessionBridge | 把遗留 Session 接入 Laravel |
feature.enabled:* | 检查某项功能是否启用 |
lims.permission:* | 按 RBAC 权限代码检查能力 |
filemanagement.permission:* | 检查文件管理模块的具体能力 |
inertia | 只用于明确的 Inertia 页面 |
模块中间件示例:
php
final class EnsureFileManagementPermission
{
public function __construct(
private readonly FileManagementPermissionService $permissions,
) {}
public function handle(
Request $request,
Closure $next,
string $ability = 'view'
): Response {
$allowed = match ($ability) {
'manage' => $this->permissions->canManageOrdinaryFiles(),
'review' => $this->permissions->canReviewOrdinaryFiles(),
'approve' => $this->permissions->canApproveOrdinaryFiles(),
'download' => $this->permissions->canDownloadOrdinaryFiles(),
default => $this->permissions->canViewModule(),
};
abort_unless($allowed, 403, '无权限访问文件管理模块');
return $next($request);
}
}权限不是只查一次
| 位置 | 解决的问题 |
|---|---|
| 路由中间件 | 能不能进入这个功能、执行这类动作 |
FormRequest authorize() | 能不能提交当前请求 |
| Service | 能不能操作这一条具体数据 |
| Query / Data Scope | 能看到哪些数据 |
必须区分:
- RBAC:能做什么;
- Data Scope:能操作哪些数据。
前端按钮隐藏只改善界面,不能代替后端权限校验。
权限判断放在哪里
先看要回答的是哪个问题,不要在每一层重复查同一个权限。
| 问题 | 位置 | 例子 |
|---|---|---|
| 能否进入模块或执行某类动作 | 路由中间件 | filemanagement.permission:manage |
| 当前请求是否允许提交 | FormRequest authorize() | 只允许申请人撤回自己的草稿 |
| 能否操作指定记录 | Policy 或 Service | 只允许文件创建方分中心提交修订 |
| 查询应该返回哪些数据 | QueryService / Data Scope | 分中心只查看授权范围内的受控文件 |
| 页面是否显示按钮 | Controller 组装视图数据 | canManageOrdinaryFiles |
普通的动作权限优先放路由中间件。Controller 不再重复判断同一个 manage、review 或 download,否则两处口径容易分叉。
个别入口只有一条路由,暂时没有值得复用的中间件或 Policy 时,Controller 可以做一次简短的 HTTP 入口拦截,例如 ApprovalSettingController。这是局部取舍,不是让 Controller 承载资源归属、数据范围或审批规则。
遗留登录态从哪里来
LegacySessionBridge 从遗留 Session 读取用户信息并写入 Laravel Session。业务代码不直接解析 Redis,也不直接读 $_SESSION。
- 路由用
legacy.auth确认已登录; - Service 和 Data Scope 通过
CurrentUserService取得用户、分中心和管理员语义; - 权限由
PermissionChecker或模块权限 Service 判断; - 不信任请求传入的
user_id、fzx_id或管理员标记。
七、FormRequest
FormRequest 用于参数整理、入口授权和验证。
Laravel 会在调用 Controller 前执行 FormRequest。验证失败时,Controller 不会执行。
常用方法
| 方法 | 职责 |
|---|---|
prepareForValidation() | 兼容旧字段、整理格式、合并输入 |
authorize() | 入口级授权,失败返回 403 |
rules() | 定义类型、必填、长度、格式和文件规则 |
withValidator() | 处理字段组合关系等补充校验 |
validated() | 取得已经通过验证的数据 |
项目示例:
php
final class FileStoreRequest extends FormRequest
{
public function authorize(): bool
{
// 当前入口权限由路由中间件负责。
return true;
}
public function rules(): array
{
return [
'fid' => ['required', 'integer', 'min:1'],
'file_name' => ['nullable', 'string', 'max:255'],
'release_date' => ['nullable', 'date_format:Y-m-d'],
'show_status' => ['nullable', 'in:0,1'],
'attachments' => ['nullable', 'array'],
'attachments.*' => ['nullable', 'file'],
];
}
}Controller 中只取已验证数据:
php
$payload = $request->validated();不要这样写:
php
$payload = $request->all();FormRequest 的边界
适合放:
- 类型和格式;
- 必填和长度;
- 正整数 ID;
- 日期格式;
- 允许值范围;
- 文件大小和类型;
- 简单字段组合关系;
- 旧请求字段的整理。
不适合放:
- 数据库事务;
- 跨表写入;
- 长业务流程;
- 状态流转;
- 审计日志;
- 复杂资源归属判断。
这些内容放到 Service。
八、Controller 与权限拦截
Controller 的职责只有三件事:
- 接收路由参数和已验证数据;
- 调用 Service;
- 返回 View、JSON、文件或重定向。
项目示例:
php
final class FileController extends Controller
{
public function __construct(
private readonly FileCommandService $commands,
) {}
public function store(FileStoreRequest $request): JsonResponse
{
$file = $this->commands->create(
$request->validated(),
$request->file('attachments', []),
);
return $this->success([
'id' => (int) $file->getKey(),
], '创建成功');
}
}Controller 可以做什么
- 接收路由参数;
- 调用
$request->validated(); - 调用一个或少量明确的 Service;
- 选择返回哪个 View;
- 组织简单响应字段;
- 返回统一 JSON。
Controller 不要做什么
- 拼接 SQL;
- 开事务;
- 写状态机;
- 处理跨表流程;
- 循环查库;
- 只根据前端参数决定数据范围;
- 直接读取
$_POST、$_SESSION; - 手工创建 Service。
判断方法:
如果一段代码离开 HTTP 也应该成立,例如审批规则、编号生成、事务和审计顺序,它就不属于 Controller。
九、Service
Service 是业务逻辑的主要落点。
它负责一个用例怎样完成,可以协调:
- Repository;
- Model;
- 文件存储;
- 当前用户服务;
- 权限服务;
- 日志服务;
- 其他业务 Service。
Service 应负责
- 业务规则;
- 状态转换;
- 资源级权限和归属校验;
- 数据范围二次校验;
- 事务和行锁;
- 防重复操作和并发保护;
- 多表写入顺序;
- 文件操作与数据库操作的协调;
- 审计日志顺序;
- 可预期业务异常。
示例:
php
public function create(
array $payload,
array $uploadedFiles
): FileManagementFile {
$this->assertVisibleCategory((int) $payload['fid']);
$attachments = $this->storage->storeOrdinaryAttachments(
$uploadedFiles,
$this->currentFzxId(),
);
return DB::transaction(function () use ($payload, $attachments) {
$attributes = $this->buildAttributes($payload, $attachments);
$file = $this->files->createOrdinaryFile($attributes);
return $file;
});
}上例重点演示 Service 的业务编排和数据库事务边界,省略了文件补偿代码。数据库事务不能回滚已经写入文件系统的附件:生产代码必须记录本次写入的文件,在数据库失败时执行清理或进入明确的补偿流程;清理失败还应记录日志并提供后续核对手段。不能因为文件保存代码写在 DB::transaction() 附近,就认为文件和数据库已经处于同一个事务中。
Service 不应负责
- 返回 View;
- 返回
JsonResponse; - 读取 Blade;
- 依赖页面结构;
- 把所有查询和动作堆成一个万能 Service。
Service 可以按职责拆分:
FileQueryService:查询;FileCommandService:新增、修改、删除;FilePreviewService:预览;FileAttachmentService:附件解析、下载授权与下载所需文件信息;ControlledFileWorkflowService:受控文件流程。
不要只因为文件行数多就拆,也不要为了目录齐全创建只转发一行代码的空层。
十、Repository、QueryService 与 Model
三者职责不同:
| 层 | 适合放什么 | 不适合放什么 |
|---|---|---|
| QueryService | 列表、统计、分页、数据范围查询 | HTTP 响应、写事务 |
| Repository | 可复用的数据访问和持久化细节 | 页面拼装、完整业务流程 |
| Model | 表映射、字段、关系、Scope、简单判断 | 跨表长流程、外部副作用 |
不是每个 Model 都必须配一个 Repository。
简单查询可以直接使用 Eloquent;复杂查询、重复查询、遗留表兼容或数据范围处理,再引入 QueryService 或 Repository。
十一、Model 用法
Model 主要负责:
- 表名;
- 主键;
- 时间戳规则;
- 可批量赋值字段;
- 字段类型;
- 模型关系;
- 查询 Scope;
- 简单业务语义判断。
示例:
php
final class FileManagementFile extends Model
{
protected $table = 'filemanagement_file';
protected $primaryKey = 'id';
public $timestamps = false;
protected $fillable = [
'fid',
'file_name',
'file_no',
'show_status',
];
public function category(): BelongsTo
{
return $this->belongsTo(FileCategory::class, 'fid', 'id');
}
public function scopeOrdinary(Builder $query): void
{
$query->where(function (Builder $builder): void {
$builder->whereNull('is_control')
->orWhere('is_control', '!=', 1);
});
}
}查询示例:
php
$files = FileManagementFile::query()
->ordinary()
->notDeleted()
->with('category')
->paginate(20);Model 使用要点
- 遗留表名、主键和时间戳规则必须显式声明。
fillable只列允许批量赋值的字段。- 不要把请求数据整包传给
create()或update()。 - 关系方法要明确外键和关联键。
- 可复用条件写成有业务含义的 Scope。
- 列表预加载关系,避免 N+1。
- 不要用全局 Scope 覆盖所有 LIMS 数据范围。
- 不要在 Model 事件中隐藏跨表长流程或外部副作用。
数据库查询优先使用 Eloquent 或 Query Builder 参数绑定,不拼接用户输入。
遗留表和多数据库兼容
Beta 会读写遗留表,也需要兼容不同客户的数据库。写查询时按达梦、MySQL、TiDB、PolarDB、金仓和瀚高的共同能力处理。
- 不依赖 MySQL 宽松
GROUP BY、GROUP_CONCAT、INSERT ... SET等专属行为。 - 遗留表的表名、主键、时间戳、
NULL/0/1语义要在 Model 或专用兼容层集中处理。 - 涉及分组、每组最新一条、日期计算和字符串函数时,要实际验证目标数据库。
- Migration 不假定所有客户已执行相同的历史脚本,变更前先查表和字段契约。
- 一个事务只覆盖一个完整的原子操作;文件已写入但数据库失败等情况,必须有明确的清理或补偿办法。
涉及 Schema 变更时另读 beta/docs/数据库变更规范.md。
十二、依赖注入
依赖注入的意思是:
类只声明自己需要什么,由 Laravel 容器负责创建和传入。
具体类自动注入
php
final class FileController extends Controller
{
public function __construct(
private readonly FileQueryService $files,
private readonly FileCommandService $commands,
private readonly CurrentUserService $currentUser,
) {}
}Laravel 会自动创建这些对象,并继续解析它们的依赖。
接口绑定实现
接口不能直接实例化,需要在 ServiceProvider 中绑定:
php
public function register(): void
{
$this->app->bind(
ControlledFileDataScope::class,
DefaultControlledFileDataScope::class,
);
}常用方式:
| 方式 | 适用场景 |
|---|---|
| 自动解析 | 构造函数依赖具体类 |
bind() | 接口绑定实现,每次解析可创建新实例 |
singleton() | 同一次应用生命周期共享一个实例 |
为什么不要手工 new
- Controller 不需要知道 Service 怎样创建;
- 依赖关系在构造函数上一眼可见;
- 测试时可以替换实现;
- 可以集中管理对象生命周期;
- 避免全局变量和 Service Locator 扩散。
不要这样写:
php
public function store(Request $request)
{
$service = new FileCommandService();
}十三、ServiceProvider
ServiceProvider 是模块的装配入口。
它负责:
- 绑定接口和实现;
- 注册单例;
- 合并模块 Laravel 运行配置;
- 注册中间件别名;
- 加载模块
routes/web.php。
模块视图不由各模块 Provider 重复加载。AppServiceProvider 会根据 bootstrap/providers.php 中已注册模块的 resources/views,统一注册以模块目录名小写表示的视图命名空间,例如 filemanagement::files.index。
register() 主要做容器绑定和配置注册:
php
public function register(): void
{
parent::register();
$this->app->bind(
ControlledFileDataScope::class,
DefaultControlledFileDataScope::class,
);
}boot() 主要做路由、中间件和视图等启动工作:
php
public function boot(): void
{
$router = $this->app->make(Router::class);
$router->aliasMiddleware(
'filemanagement.permission',
EnsureFileManagementPermission::class,
);
parent::boot();
}业务逻辑不要写在 ServiceProvider 中。
功能开关和客户配置
功能开关和模块设置的 Schema 默认值跟随模块代码;客户差异放在 beta/config/sites/{active}。运行时使用“代码默认值 + 客户差异”,不在 Controller 里根据客户名称写分支。
以文件管理模块为例:
text
app/Modules/FileManagement/config/
├── features.php # 可独立启停的业务能力 Schema
├── settings.php # 站点可维护设置的 Schema
└── filemanagement.php # Laravel config() 运行配置;确有需要才创建
config/sites/{active}/
├── site.json # 站点身份
├── features.json # 可选,Feature 差异
└── modules/
└── filemanagement.json # 可选,文件管理模块设置差异features.php 和 settings.php 由配置注册中心发现;filemanagement.php 仅用于 config('filemanagement.*') 一类的 Laravel 运行配置,不是客户配置 Schema。模块必须先在 bootstrap/providers.php 注册 Provider,Feature、Settings、模块路由和视图才会进入运行时加载链路。
判断 Feature 使用 feature.enabled:*、注入 FeatureStateService(或 app('feature')->isEnabled(...))或 Blade 的 @feature;不在业务代码里直接读 env()。新增或修改配置后至少运行:
bash
php artisan lims:site-config-validate
php artisan lims:site-config-validate --strict默认值变更会影响所有没有显式覆盖的客户。具体审核步骤见 beta/docs/功能与配置管理.md。
十四、模块目录结构
推荐结构:
text
app/Modules/FileManagement/
├── src/
│ ├── Http/
│ │ ├── Controllers/
│ │ ├── Requests/
│ │ ├── Resources/
│ │ └── Middleware/
│ ├── Services/
│ ├── Repositories/
│ ├── Models/
│ ├── DTOs/
│ ├── Support/
│ ├── Extensions/
│ └── Providers/
├── tests/
│ ├── Unit/
│ └── Feature/
├── routes/
│ └── web.php
├── resources/
│ └── views/
├── config/
│ └── filemanagement.php
└── README.md模块内的 PHP 源码统一放在 src/;测试放在模块的 tests/ 中,按 Unit 和 Feature 分类。lims:make-module 会预建 Http/Controllers、Http/Requests、Services 和 Models,方便开始开发。Repositories、DTO、Resources、Middleware、Support 和 Extensions 等目录按功能需要再创建。当前 ModuleServiceProvider 只自动加载模块 routes/web.php,不要创建尚无加载链路的 routes/api.php。
| 需求复杂度 | 建议结构 |
|---|---|
| 简单页面、下拉框 | Route + Controller + 简单查询 + View |
| 普通增删改查 | FormRequest + Controller + Service / Model |
| 复杂列表 | FormRequest + Controller + QueryService / Repository |
| 审批、授权、跨表流程 | Middleware / Policy + FormRequest + Service + Repository + Model + 测试 |
新增一层必须有实际作用:承载规则、固定边界或减少重复。
Blade 和 AJAX
普通后台页面使用 Blade + Layui,再用局部 JavaScript 增强。PDF 预览、复杂工作台等页面才考虑 Inertia,并在模块 README 说明原因。
- Blade 默认用
转义输出;{!! !!}只接受已确认安全的 HTML。 - 客户名称、备注、配置文本等可编辑内容,不直接拼进 HTML 属性、脚本或内联事件。
- 写请求带 CSRF Token;AJAX 同时处理业务失败、
422字段错误、网络失败和重复提交。 - JavaScript 不复制后端权限、数据范围和状态转换规则。页面隐藏按钮后,后端仍必须拒绝越权请求。
- 修改 Blade 后运行
php artisan view:cache;修改构建资源时再运行对应的 npm/Vite 检查。
十五、统一响应、异常和日志
beta/bootstrap/app.php 统一处理:
- 参数验证失败;
- 未登录;
- 无权限;
- 资源不存在;
- 业务异常;
- 程序异常。
JSON 基本结构:
json
{
"code": "VALIDATION_ERR",
"message": "参数校验失败",
"data": {
"errors": {
"fid": [
"分类不能为空"
]
}
}
}| 情况 | 建议处理 |
|---|---|
| 参数不合法 | 由 FormRequest 返回 422 |
| 未登录 | 中间件或异常处理返回 401 |
| 无权限 | 中间件、Gate 或异常处理返回 403 |
| 可预期业务冲突 | 抛 BusinessException 或明确业务异常 |
| 程序错误 | 继续抛出并记录,生产环境返回通用提示 |
不要把 SQL、服务器路径、堆栈和敏感配置返回给前端。
审批、授权、文件、报告、客户和跨中心操作必须保留可追溯日志。
十六、跟一遍真实请求:新增普通文件
浏览器请求:
text
POST /beta/filemanagement/files处理过程:
| 步骤 | 项目落点 | 发生的事情 |
|---|---|---|
| 1 | Web Server | 把 /beta/* 转到统一入口 |
| 2 | beta/public/index.php | 加载 Composer 和 Laravel |
| 3 | beta/bootstrap/app.php | 注册中间件和异常处理 |
| 4 | FileManagement/routes/web.php | 匹配 files.store,检查登录和 manage 权限 |
| 5 | FileStoreRequest | 兼容旧字段,校验分类、日期和附件 |
| 6 | FileController::store() | 获取 validated() 数据,调用 Service |
| 7 | FileCommandService::create() | 执行业务校验、附件保存和事务 |
| 8 | FileRepository / FileManagementFile | 查询并写入数据库 |
| 9 | Controller / ApiResponse | 返回统一 JSON 和记录 ID |
| 10 | 异常处理 / 日志 | 失败时返回对应状态码并记录信息 |
对应文件:
text
beta/app/Modules/FileManagement/routes/web.php
beta/app/Modules/FileManagement/src/Http/Requests/FileStoreRequest.php
beta/app/Modules/FileManagement/src/Http/Controllers/FileController.php
beta/app/Modules/FileManagement/src/Services/FileCommandService.php
beta/app/Modules/FileManagement/src/Repositories/FileRepository.php
beta/app/Modules/FileManagement/src/Models/FileManagementFile.php十七、新增功能的推荐步骤
- 确定 URL、HTTP 方法和路由名。
- 确定登录、功能开关和动作权限。
- 明确输入字段、兼容字段和验证规则。
- 创建 FormRequest。
- 明确业务规则、资源权限、数据范围和事务边界。
- 创建或复用 Service。
- 判断是否需要 QueryService、Repository、DTO 或 Resource。
- 在 Controller 中注入 Service,只保留调用和响应。
- 需要接口绑定或中间件别名时,更新 ServiceProvider。
- 补成功、验证失败、无权限、越权和非法状态测试。
- 运行检查并确认没有破坏旧 URL、字段和多站点行为。
开发前先回答五个问题
- 谁能进入这个入口?
- 谁能操作这一条数据?
- 输入的类型、边界和兼容规则是什么?
- 一个完整事务包含哪些写入和审计?
- 成功和失败的响应契约是什么?
十八、常见错误
| 错误做法 | 正确做法 |
|---|---|
| Controller 中写事务和长 SQL | 移到 Service、QueryService 或 Repository |
把 $request->all() 直接写库 | 只使用 validated(),再组装明确字段 |
| 只隐藏前端按钮 | 后端中间件 + 资源权限 + Data Scope |
业务类直接读取 $_POST、$_SESSION | Request 处理输入,CurrentUserService 处理用户 |
手工 require 业务类 | 使用 PSR-4、namespace 和 use |
Controller 中 new Service() | 使用构造函数依赖注入 |
路由中重复写 /beta | 内部只写应用路径 |
修改 vendor/ | 通过 Composer、配置或扩展点处理 |
| Model 事件里隐藏跨表流程 | 在 Service 中显式处理 |
| 列表循环访问未预加载关系 | 使用 with()、批量查询或聚合 |
相信请求中的 fzx_id、user_id | 从当前用户上下文和 Data Scope 获取范围 |
| 为了分层创建大量空类 | 只建立有明确职责的层 |
十九、常用命令
第一次运行前的检查
开发环境、站点配置和遗留系统依赖以 beta/DEVELOPMENT.md 为准。不要复制其他客户的 .env,也不要提交 .env、密钥或本地连接信息。
进入 beta/ 后先确认运行环境和依赖:
bash
cd beta
php -v
composer --version
composer install
php artisan about
php artisan route:list --path=filemanagement
php artisan lims:site-config-validate首次验证建议完成以下闭环:
- PHP 版本满足 8.4+,Composer 依赖安装成功;
php artisan about能正常加载应用,没有配置或扩展错误;route:list中能找到准备访问的路由;- 从遗留 LIMS 正常登录后访问
/beta/*,确认请求带有有效的遗留 Session; - 运行一个与当前模块相关的最小测试集;
- 再开始修改代码,不要通过关闭中间件或伪造管理员身份绕过环境问题。
常见状态码和排查顺序
| 现象 | 含义 | 优先检查 |
|---|---|---|
404 | 路由或资源不存在 | 用 route:list 核对 HTTP 方法、内部 URI 和路由参数;确认内部路由没有重复 /beta |
302 到遗留登录页 | legacy.auth 未识别到本次请求桥接的有效身份 | 遗留系统是否已登录、Session Cookie 是否传入、Redis Session、LegacySessionBridge 和 legacy.auth 是否正常 |
401 | Laravel 认证异常的 JSON/AJAX 响应 | 检查认证 Guard、请求类型和异常处理;后台模块的遗留登录态失败通常优先表现为重定向 |
403 | 已登录但无权限 | 路由中间件、RBAC 权限码、数据库用户管理员标记、资源归属和 Data Scope |
419 | CSRF Token 或 Session 失效 | 写请求是否处于 web 中间件、页面 Token、Cookie、域名和 Session 配置 |
422 | 参数验证失败或可预期输入错误 | 查看 JSON 中的字段错误,核对 FormRequest 的整理逻辑和验证规则,不要先改成 request()->all() 绕过验证 |
500 | 未处理的程序或环境错误 | 查看应用日志和异常链,检查数据库、缓存、文件权限和配置;不要向前端返回堆栈和敏感配置 |
排查时先确认请求的 HTTP 方法、URI、状态码和响应,再定位对应环节。重定向到登录页、401、403 和 422 通常是边界按设计拒绝请求,不应一律当作程序异常处理。应用异常默认查看 beta/storage/logs/;具体日志通道以当前环境配置为准。
开发常用命令
bash
cd beta
# 重新生成自动加载
composer dump-autoload
# 查看路由
php artisan route:list
# 清理配置缓存
php artisan config:clear
# 清理路由和视图缓存
php artisan route:clear
php artisan view:clear
# 运行测试
php artisan test
# 格式化本次改动的 PHP 文件或目录
composer format -- app/Modules/FileManagement/src
# 检查本次改动的 PHP 文件或目录是否符合格式
composer format:check -- app/Modules/FileManagement/src
# 检查 Blade
php artisan view:cache
# 检查空格和冲突标记等差异问题
git diff --check二十、测试怎么写
测试要证明边界有效,不只是证明正常请求能返回 200。
| 代码 | 主要测试 |
|---|---|
| Enum、值对象、纯规则 | Unit Test |
| Controller、路由中间件、FormRequest | Feature Test |
| 权限和 Data Scope | 不同用户、角色、分中心的 Feature Test |
| 事务和状态流转 | 成功、拒绝、重复操作、非法状态和回滚 |
| 上传、预览和下载 | 无权访问、非法类型、路径穿越和文件缺失 |
权限用例至少准备一个有权用户和一个无权用户,并分别断言成功与 403。如果权限检查会从数据库 users.admin 判断超级管理员,无权用户的数据库记录必须是 admin=0;只在模拟 Session 里改成 0 不能覆盖数据库判断。
测试遗留登录态时,Session、Redis 返回值、users 表和 RBAC 数据要表达同一个身份。不要把用户同时建成“Session 普通用户”和“数据库超级管理员”,这会让越权测试失去意义。
建议按本次改动运行最小相关集:
bash
php artisan test app/Modules/FileManagement/tests/Feature/OrdinaryFileRoutesTest.php
php artisan test --filter=test_view_only_user_cannot_access_approval_settings测试失败时先看响应状态、异常和失败断言,再看大段 HTML 输出。测试使用了不同的数据库驱动时,交付说明要写清未验证的目标数据库。
二十一、提交前检查清单
路由
- [ ] HTTP 方法正确;
- [ ] 内部 URI 没有重复
/beta; - [ ] 路由名称清楚;
- [ ] 登录、功能开关和权限中间件完整;
- [ ] ID 等路由参数有约束。
参数
- [ ] 使用 FormRequest;
- [ ] 数字 ID 校验为正整数;
- [ ] 日期、数组、文件和允许值有边界;
- [ ] Controller 只使用
validated()数据; - [ ] 兼容旧字段时集中在
prepareForValidation()。
权限与数据范围
- [ ] 入口动作权限已检查;
- [ ] Service 对具体资源做二次校验;
- [ ] 查询应用了正确 Data Scope;
- [ ] 没有信任前端传入的用户、客户或分中心范围;
- [ ] 前端按钮隐藏不是唯一权限措施。
分层
- [ ] Controller 没有事务、复杂 SQL 和状态流转;
- [ ] Service 负责业务规则和事务;
- [ ] Repository / QueryService 负责复杂查询;
- [ ] Model 只保留关系、Scope 和简单语义;
- [ ] 没有无意义的转发层。
数据与安全
- [ ] 没有拼接用户输入到 SQL;
- [ ] 没有 N+1;
- [ ] 上传、下载和预览校验了权限、归属和路径;
- [ ] 用户可控内容输出时已考虑 XSS;
- [ ] 日志没有密码、Token、私钥和客户隐私。
兼容与验证
- [ ] 旧 URL、请求字段和响应字段保持兼容;
- [ ] 多站点和分中心口径已确认;
- [ ] 相关测试通过;
- [ ] 无法验证的数据库或客户环境风险已记录;
- [ ]
git diff中没有无关格式化和调试输出。
二十二、练习:创建博客模块
练习目标
每位培训人员从远程最新的 beta 分支创建个人练习分支,使用 lims:make-module 从零生成一个最小的“培训博客”模块,并完成仅限当前登录用户操作的文章增、删、改、查页面。
这个练习不合入 beta,主要用于检查是否已经理解:
- 怎样用模块脚手架创建骨架,并识别其自动完成和未自动完成的注册;
- 怎样创建 Migration、Model、路由、Blade 页面和模块测试;
- 怎样使用 FormRequest 处理写入参数;
- 怎样划分 Controller、Service 和 Model 的职责;
- 怎样限制当前用户的数据范围;
- 怎样用 Feature Test 证明 CRUD、参数和越权边界有效。
第一步:创建个人练习分支
开始前先确认工作区没有未处理的修改。不要为了切换分支而删除或覆盖已有修改。
bash
git status
git switch beta
git pull --ff-only origin beta
git switch -c training/<姓名拼音>-training-blog分支名示例:
text
training/zhangsan-training-blog如果 git status 显示有未提交修改,先确认修改归属并妥善处理,再创建练习分支。
第二步:创建模块骨架并注册自动加载
在 beta/ 目录执行:
bash
php artisan lims:make-module TrainingBlog --key=training-blog --name="培训博客"该命令会创建最小模块骨架,并把 TrainingBlogServiceProvider 注册到 bootstrap/providers.php。命令不会自动修改 composer.json,因此必须补充正式 PSR-4 映射:
json
{
"autoload": {
"psr-4": {
"App\\Modules\\TrainingBlog\\": "app/Modules/TrainingBlog/src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\Modules\\TrainingBlog\\": "app/Modules/TrainingBlog/tests/"
}
}
}不要删除现有映射,也不要把上述 JSON 片段直接覆盖整个 composer.json。添加映射后执行:
bash
composer dump-autoload脚手架自动创建的 features.php、settings.php、Provider、路由和视图目录均应保留;没有真实配置项时,不需要为了练习添加 Feature 或 Settings 定义。
第三步:实现需求
实现一个只供已登录用户管理自己文章的最小博客后台。使用 Blade 页面,不要求引入 Inertia、Vite 模块资源、附件、评论、标签、分类、公开前台或 RBAC 新权限码。
1. 数据表和 Model
在根目录 database/migrations/ 创建 training_blog_posts 表。使用项目的 Schema 变更规范,至少包含:
| 字段 | 要求 | 说明 |
|---|---|---|
id | 主键 | 文章 ID |
author_id | 正整数、必填 | 当前登录用户的 ID,不接受前端传入 |
title | 最长 200 个字符、必填 | 文章标题 |
content | 长文本、必填 | 文章正文 |
created_at、updated_at | 时间字段 | 创建和最后修改时间 |
不要为本练习关联遗留 users 表建立外键;训练环境与客户环境的遗留 Schema 未必具有一致的外键约束。author_id 的写入和查询范围必须由当前登录用户决定。
创建:
text
app/Modules/TrainingBlog/src/Models/TrainingBlogPost.phpModel 负责表名、可批量赋值字段和必要的字段转换;不得在 Model 事件中写入当前用户、做资源授权或隐藏跨表流程。
2. 路由和页面
路由文件必须保持在:
text
app/Modules/TrainingBlog/routes/web.php所有页面路由必须经过 web、legacy.auth。外部浏览器地址含 /beta,Laravel 路由 URI 不含 /beta。
| HTTP 方法 | Laravel URI | 路由名 | Controller 方法 | 用途 |
|---|---|---|---|---|
GET | /training-blog/posts | training-blog.posts.index | index | 分页显示当前用户的文章 |
GET | /training-blog/posts/create | training-blog.posts.create | create | 新建页面 |
POST | /training-blog/posts | training-blog.posts.store | store | 创建文章 |
GET | /training-blog/posts/{post} | training-blog.posts.show | show | 查看一篇自己的文章 |
GET | /training-blog/posts/{post}/edit | training-blog.posts.edit | edit | 编辑页面 |
PUT | /training-blog/posts/{post} | training-blog.posts.update | update | 更新文章 |
DELETE | /training-blog/posts/{post} | training-blog.posts.destroy | destroy | 删除文章 |
{post} 必须使用 whereNumber('post') 约束。所有页面使用模块视图命名空间,例如:
text
trainingblog::posts.index
trainingblog::posts.form
trainingblog::posts.show视图放在:
text
app/Modules/TrainingBlog/resources/views/posts/可以复用项目现有后台布局和 Blade 表单写法;写操作必须带 CSRF Token,编辑页面的更新请求使用 @method('PUT'),删除操作使用 @method('DELETE')。
3. 输入、数据范围和业务规则
创建独立的 BlogPostStoreRequest 和 BlogPostUpdateRequest。两者规则相同:
| 字段 | 校验规则 |
|---|---|
title | 必填、字符串、去除首尾空白后长度 1 至 200 |
content | 必填、字符串、去除首尾空白后长度 1 至 10000 |
Controller 只能使用 $request->validated(),不得从请求读取或写入 author_id,不得直接调用 $_POST、$_SESSION,也不得在 Controller 中拼接查询。
当前用户范围是本练习的硬性边界:
- 列表只能显示
author_id等于当前登录用户 ID 的文章; - 查看、编辑、更新和删除都只能操作当前用户自己的文章;
- 访问其他用户文章时,统一返回
404,不泄露文章是否存在; - 创建文章时由 Service 通过
CurrentUserService写入author_id; - 前端伪造
author_id、路由 ID 或隐藏字段均不得扩大可操作范围。
创建场景型 BlogPostService,由它取得当前用户、执行归属范围查询、创建、更新和删除。这个练习的查询足够简单,Service 可以直接使用 Model,不要求为了填目录再创建 Repository、DTO、Policy 或 Resource。
4. 最小文件清单
实际命名可以微调,但职责和路径必须保持一致:
text
database/migrations/<timestamp>_create_training_blog_posts_table.php
app/Modules/TrainingBlog/routes/web.php
app/Modules/TrainingBlog/src/Models/TrainingBlogPost.php
app/Modules/TrainingBlog/src/Http/Requests/BlogPostStoreRequest.php
app/Modules/TrainingBlog/src/Http/Requests/BlogPostUpdateRequest.php
app/Modules/TrainingBlog/src/Http/Controllers/BlogPostController.php
app/Modules/TrainingBlog/src/Services/BlogPostService.php
app/Modules/TrainingBlog/resources/views/posts/index.blade.php
app/Modules/TrainingBlog/resources/views/posts/form.blade.php
app/Modules/TrainingBlog/resources/views/posts/show.blade.php
app/Modules/TrainingBlog/tests/Feature/BlogPostCrudTest.php第四步:编写测试
至少覆盖以下 Feature Test:
- 已登录用户可以创建文章,数据库记录的
author_id等于当前用户,且不受请求传入author_id影响; - 用户列表只返回自己的文章,不能返回另一用户的文章;
- 用户可以查看、编辑、更新和删除自己的文章;
- 用户尝试查看、编辑、更新或删除另一用户的文章时返回
404,另一用户数据不变; - 对带有
Accept: application/json的写请求,空标题、超过 200 字符的标题、空正文和超过 10000 字符的正文返回422;普通 Blade 表单验证失败时应重定向回表单并显示字段错误; - 未建立遗留登录态的请求会被
legacy.auth重定向到遗留登录页。
测试必须建立与当前项目一致的遗留 Session 桥接身份和数据库用户,不能只伪造普通 Laravel Session。测试中至少准备两位非管理员用户和各自的一篇文章,用于证明数据范围正确。
第五步:自检和提交评审
完成后至少执行:
bash
cd beta
php artisan test app/Modules/TrainingBlog/tests/Feature/BlogPostCrudTest.php
composer format:check -- app/Modules/TrainingBlog
php artisan view:cache
git diff --check提交前逐项确认:
- [ ] 使用
lims:make-module创建了TrainingBlog,Provider 已注册; - [ ]
composer.json已补全模块源码和测试的 PSR-4 映射,并已执行composer dump-autoload; - [ ] Migration 位于根目录
database/migrations/,没有在模块内另建未加载的数据库目录; - [ ] 外部地址有
/beta,Laravel 路由内部没有重复/beta; - [ ] 所有模块路由经过
web、legacy.auth,写路由具有 CSRF 防护; - [ ] 所有写入参数都经过 FormRequest,Controller 只使用
validated(); - [ ] Controller 没有数据范围查询、事务或业务规则;
- [ ] Service 从当前用户上下文取得
author_id,并对每个资源操作应用归属范围; - [ ] 越权访问其他用户文章返回
404,不会暴露文章是否存在; - [ ] CRUD、参数失败、登录态和数据范围测试全部通过;
- [ ] 没有提交
.env、缓存、日志和调试文件。
每个人把个人分支推送到远程,并创建以 beta 为目标分支的 Draft Pull Request 供培训评审。PR 中写清:模块创建命令、实现内容、测试命令、测试结果和仍未验证的环境。除非培训负责人明确要求,否则不要把练习 PR 合入 beta。
评分参考
| 项目 | 分值 |
|---|---|
| 脚手架、Provider 和 Composer 自动加载注册正确 | 15 |
| Migration、Model 和模块目录归属正确 | 15 |
| 路由、Blade 页面和 CRUD 完整 | 20 |
| FormRequest 与 Controller 边界正确 | 15 |
| Service、当前用户归属和越权防护正确 | 20 |
| 测试覆盖并能稳定通过 | 10 |
| 分支、提交和代码整洁 | 5 |
| 合计 | 100 |
附录:快速定位
| 要看什么 | 文件 |
|---|---|
| Composer 与 PSR-4 | beta/composer.json |
| 统一入口 | beta/public/index.php |
| 应用、中间件、异常 | beta/bootstrap/app.php |
| 全局路由入口 | beta/routes/web.php、beta/routes/beta.php |
| 模块装配 | beta/app/Foundation/Module/ModuleServiceProvider.php |
| 文件模块 Provider | beta/app/Modules/FileManagement/src/Providers/FileManagementServiceProvider.php |
| 文件模块路由 | beta/app/Modules/FileManagement/routes/web.php |
| FormRequest 示例 | beta/app/Modules/FileManagement/src/Http/Requests/FileStoreRequest.php |
| Controller 示例 | beta/app/Modules/FileManagement/src/Http/Controllers/FileController.php |
| Service 示例 | beta/app/Modules/FileManagement/src/Services/FileCommandService.php |
| Model 示例 | beta/app/Modules/FileManagement/src/Models/FileManagementFile.php |
| 项目代码规范 | beta/docs/代码规范.md |
| 模块结构规范 | beta/docs/模块结构规范.md |
| 当前架构与模块边界 | beta/docs/代码规范.md、beta/docs/模块结构规范.md |
| RBAC 使用和迁移 | beta/docs/RBAC/README.md |
| 客户功能与模块配置 | beta/docs/功能与配置管理.md |
| Schema 变更 | beta/docs/数据库变更规范.md |
| 受控文件权限映射 | beta/docs/文件管理/受控文件/权限与数据范围.md |
| 受控文件状态机 | beta/docs/文件管理/受控文件/工作流与状态机.md |
| 开发环境 | beta/DEVELOPMENT.md |
| 部署与上线检查 | beta/DEPLOY.md |
最后记住:
路由找人,中间件守门,FormRequest 验参数,Controller 做转发,Service 管业务,Repository / Model 管数据,容器负责把这些对象组装起来。