Skip to content

LIMS Beta Laravel 开发手册

适用范围:beta/ 下的 Laravel 应用
培训目标:理解一个请求怎样进入系统、怎样被拦截、业务代码应该写在哪一层
使用方式:本文用于理解项目架构和日常开发方式。实际开发、评审和验收以 beta/DEVELOPMENT.md 及对应专项规范为准。
最后核验:2026-08-10(PHP 8.4、Laravel 13、当前模块化装配链路)。

本文中的路径、命令和代码示例以核验时的实现为准。开始修改前,先查看附录中的实际文件、php artisan route:list 和任务涉及的专项文档;Schema、RBAC、客户配置、部署和受控文件流程不要只凭通用 Laravel 经验处理。

阅读导航

建议学习顺序

场景建议内容
起步第一、四至十、十二、十六和二十二章
开始实际开发再读第十一、十三至十五和十七章,并以 beta/DEVELOPMENT.md 为开发规范
提交代码之前第十八至二十一章和本次任务对应的专项规范
遇到具体问题通过附录定位代码、权限、配置、Schema 或部署文档

第一次阅读可以先了解请求链路和分层。开始具体任务后,再按需要查阅对应章节和正式规范。

一、先看整体变化

以前常见的方式是“文件就是入口”:

  • 浏览器直接访问某个 PHP 文件;
  • 文件里 include 配置和函数;
  • 读取 $_GET$_POST 和全局变量;
  • 判断权限、执行 SQL、输出 HTML;
  • 一个文件可能包含完整流程。

Beta 使用 Laravel 后,变成“统一入口 + 路由 + 分层处理”:

对比项以前的常见方式Beta Laravel 方式
类加载手工 includerequireComposer 按 PSR-4 自动加载
请求入口每个 PHP 文件都可能是入口统一进入 beta/public/index.php
功能定位文件路径、act 参数HTTP 方法、URI、路由
登录和权限各入口零散判断中间件、Request、Service 分层校验
参数验证手工逐项判断FormRequest 统一验证
业务逻辑页面、SQL、HTML 混在一起Controller 调用 Service
数据访问页面直接查库QueryService、Repository、Model
对象创建手工 new、依赖全局对象Laravel 容器依赖注入
错误处理echodie、各自返回统一异常和响应格式

原来的做法是“找到文件并执行”;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\FileCommandServiceapp/Modules/FileManagement/src/Services/FileCommandService.php
App\Modules\FileManagement\Models\FileManagementFileapp/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,
    ) {}
}

使用规则

  1. namespace 必须和目录一致。
  2. 类名必须和文件名一致。
  3. 引用类使用 use,不要为业务类写 require_once
  4. 不要修改 vendor/
  5. 不要在 Controller 中 new Service()
  6. 新模块或模块测试目录需要自动加载时,先补充对应的 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 主要做五件事:

  1. 检查维护模式;
  2. 加载 vendor/autoload.php
  3. 加载 bootstrap/app.php
  4. 捕获当前 Request;
  5. 交给 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"]

对应职责:

顺序环节主要工作
1Web Server/beta/* 转到统一入口
2Composer自动加载框架类和业务类
3bootstrap/app.php注册路由、中间件和异常处理
4全局 / web 中间件处理 Session、Cookie、用户态桥接
5Router按 HTTP 方法和 URI 匹配路由
6路由中间件检查登录、功能开关和权限
7FormRequest整理输入、授权、验证
8Controller接收已验证数据,调用 Service
9Service执行业务规则、事务、资源校验和审计
10Repository / Model查询、保存、关系和 Scope
11Response返回 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 方法
列表GETindexlist
详情GETshow
新增POSTstore
整体更新PUTupdate
局部更新PATCHupdate 或明确动作方法
删除DELETEdestroy

路由检查点

  1. 内部路由不写 /beta
  2. 路由只负责匹配,不写业务逻辑。
  3. 路由必须命名,后端生成链接使用 route()
  4. 数字 ID 使用 whereNumber() 等约束。
  5. 同一模块的前缀、名称和中间件使用 group() 统一声明。
  6. 模块路由放在模块自己的 routes/web.php

文件管理模块路由位置:

text
beta/app/Modules/FileManagement/routes/web.php

六、路由中间件

中间件负责在进入 Controller 前统一拦截请求。

允许访问时调用:

php
return $next($request);

不允许访问时根据入口类型重定向到登录页,或直接返回、抛出 401403

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 不再重复判断同一个 managereviewdownload,否则两处口径容易分叉。

个别入口只有一条路由,暂时没有值得复用的中间件或 Policy 时,Controller 可以做一次简短的 HTTP 入口拦截,例如 ApprovalSettingController。这是局部取舍,不是让 Controller 承载资源归属、数据范围或审批规则。

遗留登录态从哪里来

LegacySessionBridge 从遗留 Session 读取用户信息并写入 Laravel Session。业务代码不直接解析 Redis,也不直接读 $_SESSION

  • 路由用 legacy.auth 确认已登录;
  • Service 和 Data Scope 通过 CurrentUserService 取得用户、分中心和管理员语义;
  • 权限由 PermissionChecker 或模块权限 Service 判断;
  • 不信任请求传入的 user_idfzx_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 的职责只有三件事:

  1. 接收路由参数和已验证数据;
  2. 调用 Service;
  3. 返回 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 应负责

  1. 业务规则;
  2. 状态转换;
  3. 资源级权限和归属校验;
  4. 数据范围二次校验;
  5. 事务和行锁;
  6. 防重复操作和并发保护;
  7. 多表写入顺序;
  8. 文件操作与数据库操作的协调;
  9. 审计日志顺序;
  10. 可预期业务异常。

示例:

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 使用要点

  1. 遗留表名、主键和时间戳规则必须显式声明。
  2. fillable 只列允许批量赋值的字段。
  3. 不要把请求数据整包传给 create()update()
  4. 关系方法要明确外键和关联键。
  5. 可复用条件写成有业务含义的 Scope。
  6. 列表预加载关系,避免 N+1。
  7. 不要用全局 Scope 覆盖所有 LIMS 数据范围。
  8. 不要在 Model 事件中隐藏跨表长流程或外部副作用。

数据库查询优先使用 Eloquent 或 Query Builder 参数绑定,不拼接用户输入。

遗留表和多数据库兼容

Beta 会读写遗留表,也需要兼容不同客户的数据库。写查询时按达梦、MySQL、TiDB、PolarDB、金仓和瀚高的共同能力处理。

  1. 不依赖 MySQL 宽松 GROUP BYGROUP_CONCATINSERT ... SET 等专属行为。
  2. 遗留表的表名、主键、时间戳、NULL/0/1 语义要在 Model 或专用兼容层集中处理。
  3. 涉及分组、每组最新一条、日期计算和字符串函数时,要实际验证目标数据库。
  4. Migration 不假定所有客户已执行相同的历史脚本,变更前先查表和字段契约。
  5. 一个事务只覆盖一个完整的原子操作;文件已写入但数据库失败等情况,必须有明确的清理或补偿办法。

涉及 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.phpsettings.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/ 中,按 UnitFeature 分类。lims:make-module 会预建 Http/ControllersHttp/RequestsServicesModels,方便开始开发。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

处理过程:

步骤项目落点发生的事情
1Web Server/beta/* 转到统一入口
2beta/public/index.php加载 Composer 和 Laravel
3beta/bootstrap/app.php注册中间件和异常处理
4FileManagement/routes/web.php匹配 files.store,检查登录和 manage 权限
5FileStoreRequest兼容旧字段,校验分类、日期和附件
6FileController::store()获取 validated() 数据,调用 Service
7FileCommandService::create()执行业务校验、附件保存和事务
8FileRepository / FileManagementFile查询并写入数据库
9Controller / 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

十七、新增功能的推荐步骤

  1. 确定 URL、HTTP 方法和路由名。
  2. 确定登录、功能开关和动作权限。
  3. 明确输入字段、兼容字段和验证规则。
  4. 创建 FormRequest。
  5. 明确业务规则、资源权限、数据范围和事务边界。
  6. 创建或复用 Service。
  7. 判断是否需要 QueryService、Repository、DTO 或 Resource。
  8. 在 Controller 中注入 Service,只保留调用和响应。
  9. 需要接口绑定或中间件别名时,更新 ServiceProvider。
  10. 补成功、验证失败、无权限、越权和非法状态测试。
  11. 运行检查并确认没有破坏旧 URL、字段和多站点行为。

开发前先回答五个问题

  1. 谁能进入这个入口?
  2. 谁能操作这一条数据?
  3. 输入的类型、边界和兼容规则是什么?
  4. 一个完整事务包含哪些写入和审计?
  5. 成功和失败的响应契约是什么?

十八、常见错误

错误做法正确做法
Controller 中写事务和长 SQL移到 Service、QueryService 或 Repository
$request->all() 直接写库只使用 validated(),再组装明确字段
只隐藏前端按钮后端中间件 + 资源权限 + Data Scope
业务类直接读取 $_POST$_SESSIONRequest 处理输入,CurrentUserService 处理用户
手工 require 业务类使用 PSR-4、namespace 和 use
Controller 中 new Service()使用构造函数依赖注入
路由中重复写 /beta内部只写应用路径
修改 vendor/通过 Composer、配置或扩展点处理
Model 事件里隐藏跨表流程在 Service 中显式处理
列表循环访问未预加载关系使用 with()、批量查询或聚合
相信请求中的 fzx_iduser_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

首次验证建议完成以下闭环:

  1. PHP 版本满足 8.4+,Composer 依赖安装成功;
  2. php artisan about 能正常加载应用,没有配置或扩展错误;
  3. route:list 中能找到准备访问的路由;
  4. 从遗留 LIMS 正常登录后访问 /beta/*,确认请求带有有效的遗留 Session;
  5. 运行一个与当前模块相关的最小测试集;
  6. 再开始修改代码,不要通过关闭中间件或伪造管理员身份绕过环境问题。

常见状态码和排查顺序

现象含义优先检查
404路由或资源不存在route:list 核对 HTTP 方法、内部 URI 和路由参数;确认内部路由没有重复 /beta
302 到遗留登录页legacy.auth 未识别到本次请求桥接的有效身份遗留系统是否已登录、Session Cookie 是否传入、Redis Session、LegacySessionBridgelegacy.auth 是否正常
401Laravel 认证异常的 JSON/AJAX 响应检查认证 Guard、请求类型和异常处理;后台模块的遗留登录态失败通常优先表现为重定向
403已登录但无权限路由中间件、RBAC 权限码、数据库用户管理员标记、资源归属和 Data Scope
419CSRF Token 或 Session 失效写请求是否处于 web 中间件、页面 Token、Cookie、域名和 Session 配置
422参数验证失败或可预期输入错误查看 JSON 中的字段错误,核对 FormRequest 的整理逻辑和验证规则,不要先改成 request()->all() 绕过验证
500未处理的程序或环境错误查看应用日志和异常链,检查数据库、缓存、文件权限和配置;不要向前端返回堆栈和敏感配置

排查时先确认请求的 HTTP 方法、URI、状态码和响应,再定位对应环节。重定向到登录页、401403422 通常是边界按设计拒绝请求,不应一律当作程序异常处理。应用异常默认查看 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、路由中间件、FormRequestFeature 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.phpsettings.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_atupdated_at时间字段创建和最后修改时间

不要为本练习关联遗留 users 表建立外键;训练环境与客户环境的遗留 Schema 未必具有一致的外键约束。author_id 的写入和查询范围必须由当前登录用户决定。

创建:

text
app/Modules/TrainingBlog/src/Models/TrainingBlogPost.php

Model 负责表名、可批量赋值字段和必要的字段转换;不得在 Model 事件中写入当前用户、做资源授权或隐藏跨表流程。

2. 路由和页面

路由文件必须保持在:

text
app/Modules/TrainingBlog/routes/web.php

所有页面路由必须经过 weblegacy.auth。外部浏览器地址含 /beta,Laravel 路由 URI 不含 /beta

HTTP 方法Laravel URI路由名Controller 方法用途
GET/training-blog/poststraining-blog.posts.indexindex分页显示当前用户的文章
GET/training-blog/posts/createtraining-blog.posts.createcreate新建页面
POST/training-blog/poststraining-blog.posts.storestore创建文章
GET/training-blog/posts/{post}training-blog.posts.showshow查看一篇自己的文章
GET/training-blog/posts/{post}/edittraining-blog.posts.editedit编辑页面
PUT/training-blog/posts/{post}training-blog.posts.updateupdate更新文章
DELETE/training-blog/posts/{post}training-blog.posts.destroydestroy删除文章

{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. 输入、数据范围和业务规则

创建独立的 BlogPostStoreRequestBlogPostUpdateRequest。两者规则相同:

字段校验规则
title必填、字符串、去除首尾空白后长度 1200
content必填、字符串、去除首尾空白后长度 110000

Controller 只能使用 $request->validated(),不得从请求读取或写入 author_id,不得直接调用 $_POST$_SESSION,也不得在 Controller 中拼接查询。

当前用户范围是本练习的硬性边界:

  1. 列表只能显示 author_id 等于当前登录用户 ID 的文章;
  2. 查看、编辑、更新和删除都只能操作当前用户自己的文章;
  3. 访问其他用户文章时,统一返回 404,不泄露文章是否存在;
  4. 创建文章时由 Service 通过 CurrentUserService 写入 author_id
  5. 前端伪造 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:

  1. 已登录用户可以创建文章,数据库记录的 author_id 等于当前用户,且不受请求传入 author_id 影响;
  2. 用户列表只返回自己的文章,不能返回另一用户的文章;
  3. 用户可以查看、编辑、更新和删除自己的文章;
  4. 用户尝试查看、编辑、更新或删除另一用户的文章时返回 404,另一用户数据不变;
  5. 对带有 Accept: application/json 的写请求,空标题、超过 200 字符的标题、空正文和超过 10000 字符的正文返回 422;普通 Blade 表单验证失败时应重定向回表单并显示字段错误;
  6. 未建立遗留登录态的请求会被 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
  • [ ] 所有模块路由经过 weblegacy.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-4beta/composer.json
统一入口beta/public/index.php
应用、中间件、异常beta/bootstrap/app.php
全局路由入口beta/routes/web.phpbeta/routes/beta.php
模块装配beta/app/Foundation/Module/ModuleServiceProvider.php
文件模块 Providerbeta/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/代码规范.mdbeta/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 管数据,容器负责把这些对象组装起来。