SuperAPI AI Directory

laravel

General & other

View repository

Updated 10-16 05:00

About this skill

Laravel Backend Development Standards (Laravel + Eloquent)

This Skill is used with the youlai-laravel repository. Based on these standards, the AI assistant can create from 0 to 1 a Laravel backend functionally equivalent to youlai-boot.

Trigger Conditions

  • Developing Laravel Web projects
  • Using Eloquent ORM for data access
  • Using #[Fillable] / #[Hidden] and other PHP 8 attribute annotations
  • Implementing JWT authentication and role-based access control (RBAC)
  • Writing REST API controllers, Services, Validates
  • Developing the youlai-laravel admin module
  • Creating new modules or fixing issues based on the youlai-laravel repository

Part 1: Tech Stack

Layer Choice Description
Runtime Environment PHP 8.3+ PHP 8 attribute annotations, enums, readonly properties
Web Framework Laravel 13 Routing, container, middleware, queues
ORM Eloquent ActiveRecord style, comparable to MyBatis-Plus
Authentication tymon/jwt-auth JWT self-contained Token, guard name admin
Cache Redis 7.x cache() / Cache:: + Redis driver
Database MySQL 8.0+ This project's script database/youlai_admin_tenant.sql(multi-tenant version)
Captcha gregwar/captcha Graphical captcha base64 output
Password Hashing password_hash PASSWORD_DEFAULT(bcrypt)
API Documentation Swagger / OpenAPI /api/docs or Knife4j style
Code Generation Templates + stub templates/ under .stub/.tpl generate frontend and backend CRUD
Real-time Communication SSE php artisan sse:start independent resident process
Testing PHPUnit phpunit.xml + tests/
Package Management Composer composer.json
Frontend Build Vite vite.config.js
Code Quality Pint Laravel official code style

Includes multi-tenancy: youlai-laravel implements multi-tenancy (tenant_id field + sys_tenant table + switch tenant API), new modules must include tenant_id isolation condition in queries.


Part 2: Directory Structure

youlai-laravel/
├── app/
│   ├── Attributes/                # 自定义属性类(如 LogAction)
│   ├── Common/                    # 通用工具/枚举/常量
│   │   ├── Enums/                 # ResultCode 业务码枚举
│   │   ├── Constants/             # RedisKey 等常量
│   │   └── Interfaces/            # IResultCode 接口
│   ├── Console/                   # 命令行命令(sse:start 等)
│   ├── Exceptions/                # BusinessException 业务异常
│   ├── Extensions/                # 框架扩展
│   ├── Http/
│   │   ├── Controllers/           # 控制器
│   │   │   ├── Controller.php     # 抽象基类(ApiResponse + validateScene)
│   │   │   └── V1/                # API v1 版本控制器
│   │   │       ├── AuthController.php
│   │   │       └── SysUserController.php
│   │   ├── Middleware/            # jwt.admin / permission / 限流等中间件
│   │   └── Validates/             # 表单验证类(按模块划分)
│   ├── Models/                    # Eloquent 模型(继承 Base)
│   │   ├── Base.php               # 基础模型(状态/删除常量、时间字段)
│   │   └── SysUser.php
│   ├── Services/                  # 业务逻辑层(继承 BaseService)
│   │   ├── BaseService.php        # 基础服务(LoggableService)
│   │   └── SysUserService.php
│   ├── Pagination/                # 分页相关
│   ├── Providers/                 # 服务提供者
│   ├── Requests/                  # 请求验证类
│   ├── Scope/                     # 查询作用域
│   ├── Traits/                    # 可复用 Trait(ApiResponse 等)
│   └── Utils/                     # 工具类(VerifyCode / LogContext)
├── bootstrap/                     # 框架启动(app.php 注册中间件别名)
├── config/                        # 配置文件
├── database/                      # 迁移 / 工厂 / 填充 / SQL
│   └── youlai_admin_tenant.sql    # 系统数据库脚本(多租户版)
├── public/                        # 入口目录(index.php)
├── resources/                     # 视图 / 语言包 / 前端资源
├── routes/                        # 路由定义(api.php / web.php / console.php)
├── storage/                       # 运行时存储
├── templates/                     # 代码生成模板(.stub / .tpl)
├── tests/                         # PHPUnit 测试
├── artisan                        # Laravel 命令行入口
├── composer.json
└── .env                           # 环境配置

Layering and Dependencies:

  • Models/ is a pure Eloquent model, depends on Base
  • Services/ is the business layer, depends on Models/ and Validates/
  • Http/Controllers/V1/ is the interface layer, depends on Services/ and Validates/, reuses validators via validateScene() Reuse validator
  • Common/ are constants/enums/interfaces, shared by all layers
  • Traits/ApiResponse.php provides unified responses, shared by Controller and BaseService Shared

Part 3: Naming Conventions

3.1 File and Directory Naming

Type Convention Example
Controller Directory V1(uppercase API version) app/Http/Controllers/V1/
Controller Class Table name to PascalCase + Controller SysUserController、SysRoleController
Service Class Table name to PascalCase + Service SysUserService、SysMenuService
Validate Class Table name to PascalCase + Validate SysUserValidate、SysRoleValidate
Model Class Table name to PascalCase (remove prefix) SysUser、SysMenu、SysDept
Model directory Singular class name, plural table name sys_user → SysUser
Config file lowercase with underscores config/jwt.php、config/captcha.php
Constant/Enum directory See namespace app/Common/Enums/ResultCode.php

3.2 Type and Member Naming

Type Convention Example
Model class Remove table name prefix sys_ and convert to PascalCase SysUser、SysDictItem
Service class Module name + Service SysUserService
Validate class Module name + Validate SysUserValidate
Controller method RESTful standard actions index/show/store/update/destroy + business actions
Business method snake_case starting with a verb paginate/create/assignRoles/restPassword
Database field snake_case dept_id、create_time、is_deleted
JSON/API field camelCase (automatically converted in responses) deptId、createTime、roleIds
Route parameter snake_case or kebab {id}、{name}

3.3 Method Naming (Service Layer)

Operation Method name Example
Paginated list paginate paginate(array $filters, int $perPage)
Single detail show show(int $id): ?SysUser
Create create create(array $data): SysUser
Update update update(SysUser $model, array $data)
Delete (batch) delete delete(array $ids): int
Status change updateStatus Named by business semantics
Reset password restPassword restPassword(SysUser $user, array $data)
Assign roles assignRoles assignRoles(SysUser $user, array $roleIds)
Dropdown options getOptions getOptions(): array
User information getUserInfo getUserInfo(): array
Data formatting format format(SysUser $user): array

3.4 Variable Naming

Type Convention Example
Variable camelCase $userList、$roleCodes
Constant SNAKE_CASE (class constant) PER_PAGE、STATUS_ENABLE
Boolean variable is_/has_ prefix (PHP property) $isDeleted、$hasPerm
Database field snake_case dept_id、create_time
JSON key camelCase (automatically converted in the response layer) deptId、createTime、pageSize

Key convention: Fields inside Model / Service use snake_case (corresponding to MySQL column names); response output is automatically converted to camelCase by ApiResponse 's CaseConversion , so the JSON key names received by the frontend are camelCase. No need for manual toCamelCase。


Part 4: Routing and API Conventions

4.1 Route Files and Version Prefix

All APIs are defined in routes/api.php, uniformly prefixed with v1 , namespace App\Http\Controllers\V1:

// routes/api.php
Route::prefix('v1')->namespace('App\Http\Controllers\V1')->group(function () {
    // 公开
    Route::prefix('auth')->group(function () {
        Route::post('login', [AuthController::class, 'login']);
        Route::get('captcha', [AuthController::class, 'captcha']);
    });

    // 需要 JWT 认证
    Route::middleware(['jwt.admin'])->group(function () {
        Route::prefix('users')->group(function () {
            Route::get('me', [SysUserController::class, 'me']);
            Route::put('{id}/password/reset', [SysUserController::class, 'restPassword'])
                ->where(['id' => '[0-9]+']);
            Route::get('options', [SysUserController::class, 'options']);
            Route::get('{id}/form', [SysUserController::class, 'show'])
                ->where(['id' => '[0-9]+']);
            apiIdResource(SysUserController::class);
        });
    });
});
Module Prefix Description
Auth /api/v1/auth login / register / captcha / refresh-token
User /api/v1/users User management
Role /api/v1/roles Role management
Menu /api/v1/menus Menu management
Department /api/v1/depts Department management
Dictionary /api/v1/dicts、/api/v1/dicts/{name}/items Dictionary and dictionary items
Config /api/v1/configs System configuration
Notification /api/v1/notices Notifications and announcements
Log /api/v1/logs Operation log
File /api/v1/files File upload/delete
Code generation /api/v1/codegen Code generation
Tenant /api/v1/tenants、/api/v1/tenant-plans Multi-tenant

4.2 Standard CRUD Paths

Operation Method Path Description
Paginated list GET /api/v1/users Query:pageNum/pageSize/keywords/...
Form data GET /api/v1/users/{id}/form Path parameter id
Create POST /api/v1/users Body: snake_case fields
Update PUT /api/v1/users/{id} Body: snake_case fields
Batch delete DELETE /api/v1/users/{ids} ids comma-separated
Reset password PUT /api/v1/users/{id}/password/reset
Assign roles POST / PUT /api/v1/users/{id}/roles
Dropdown options GET /api/v1/roles/options Return [{value, label}]
Current user GET /api/v1/users/me Current logged-in user information
Menu routes GET /api/v1/menus/routes Dynamic route tree

4.3 Route Registration Order (Important!)

Fixed-path endpoints must be registered before path-parameter endpoints, otherwise Laravel will preferentially match {id}/form and other generalized routes:

Route::prefix('users')->group(function () {
    Route::get('me', [SysUserController::class, 'me']);                 // 固定 —— 在前
    Route::get('options', [SysUserController::class, 'options']);       // 固定 —— 在前
    Route::get('export', [SysUserController::class, 'export']);         // 固定 —— 在前
    Route::get('{id}/form', [SysUserController::class, 'show'])         // 路径参数
        ->where(['id' => '[0-9]+']);
    apiIdResource(SysUserController::class);                            // index/store/show/update/destroy
});

apiIdResource() is a project wrapper helper: it constrains the standard apiResource 's single-parameter id to a number, avoiding repeating ->where()in every module. For new modules, just pass the controller class name.

4.4 Controller Method Template

<?php
namespace App\Http\Controllers\V1;

use App\Common\Enums\ResultCode;
use App\Exceptions\BusinessException;
use App\Http\Controllers\Controller;
use App\Http\Validates\SysUserValidate;
use App\Models\SysUser;
use App\Services\SysUserService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class SysUserController extends Controller
{
    protected string $validatorClass = SysUserValidate::class;   // 绑定验证器

    public function __construct(
        protected SysUserService $userService,
    ) {}

    /**
     * 用户分页列表
     */
    public function index(Request $request): JsonResponse
    {
        $filters  = $request->only(['keywords', 'status', 'created_at', 'dept_id']);
        $pageSize = (int) $request->input('pageSize', self::PER_PAGE);
        $result   = $this->userService->paginate($filters, $pageSize);

        return $this->success($result);
    }

    /**
     * 创建用户
     */
    public function store(Request $request): JsonResponse
    {
        $validated = $this->validateScene($request, 'create');   // 调用 SysUserValidate::rules('create')
        $data = $this->userService->create($validated);
        return $this->success($this->userService->format($data), '创建成功');
    }
}

Part 5: HTTP Status Codes and Business Error Codes

5.1 Design Philosophy

Aligned with youlai-boot:HTTP status code is always 200, the business status code is passed through the body's code field. The frontend axios interceptor uniformly reads the business code from response.data.code .

5.2 Business Error Codes (ResultCode Enum)

The enum is defined in app/Common/Enums/ResultCode.php, implements the IResultCode interface, and provides getCode() / getMsg():

Error Code Enum Meaning
00000 SUCCESS Success
A0201 ACCOUNT_NOT_FOUND User account does not exist
A0202 ACCOUNT_FROZEN User account is frozen
A0205 USER_TOKEN_INVALID Invalid Token
A0206 USER_TOKEN_EXPIRED Token expired
A0210 USER_PASSWORD_ERROR Incorrect user password
A0211 DELETE_SUPER_ADMIN Deleting the super administrator is not allowed
A0212 CANNOT_DELETE_SELF Cannot delete the currently logged-in account
A0301 ACCESS_UNAUTHORIZED Access unauthorized
A0400 USER_REQUEST_PARAMETER_ERROR User request parameter error
B0001 SYSTEM_ERROR System execution error
C0300 DATABASE_SERVICE_ERROR Database service error

First-digit encoding rule:

  • 0 Success (00000)
  • A User-side error (authentication/permission/parameter)
  • B System-side error
  • C Third-party/database error

5.3 Throwing Business Exceptions

use App\Common\Enums\ResultCode;
use App\Exceptions\BusinessException;

// ✅ 参数校验失败(控制器里由 validateScene 自动抛出,无需手写)
throw new BusinessException(ResultCode::USER_REQUEST_PARAMETER_ERROR->getCode(), $validator->errors()->first());

// ✅ 数据不存在
throw new BusinessException(ResultCode::ACCOUNT_NOT_FOUND->getMsg());

// ✅ 权限不足
throw new BusinessException(ResultCode::ACCESS_UNAUTHORIZED->getMsg());

// ✅ 不能删除自己
throw new BusinessException(ResultCode::CANNOT_DELETE_SELF->getMsg());

BusinessException is rendered by bootstrap/app.php 's exception handler or global exception into a unified response (HTTP 200 + code/msg/data)。


Part 6: Unified Response Format

6.1 Result Structure

{
    "code": "00000",
    "msg": "操作成功",
    "data": { "…" },
    "traceId": "uuid-…"
}

The response is provided by App\Traits\ApiResponse , all controller base classes Controller already use ApiResponse:

// 成功(data 可空,自动 snake_case → camelCase)
return $this->success($data, '操作成功');

// 失败(不推荐直接用,优先用业务异常)
return $this->fail('操作失败', 'B0001');

// 业务错误(推荐,传枚举)
return $this->error(ResultCode::ACCESS_UNAUTHORIZED);

// 参数验证失败
return $this->validationError($errors, '参数验证失败');

6.2 Paginated Response

The Eloquent paginator (LengthAwarePaginator) is directly returned as data , and is automatically converted by CaseConversion into:

{
    "code": "00000",
    "msg": "操作成功",
    "data": {
        "list": [ "…" ],
        "total": 100,
        "pageNum": 1,
        "pageSize": 10
    }
}

Frontend convention: Pagination parameters are named pageNum / pageSize(camelCase). The Laravel side reads $request->input('pageSize') and passes it to paginate(), and the response outputs camelCase keys via the conversion Trait.

6.3 Automatic CamelCase Conversion

ApiResponse via CaseConversion trait's convertToCamelCase() When outputting, convert array keys / object properties from snake_case to camelCase. Therefore:

  • Model properties,$request->input() use snake_case (dept_id、create_time)
  • the returned data automatically becomes camelCase (deptId、createTime)
  • no need to manually in the Service toCamelCase, but format() when manually assembling arrays such as etc., please directly use camelCase key names

Part 7: Eloquent Model Specification

7.1 Model inherits Base

<?php
namespace App\Models;

use App\Traits\HasOperator;
use App\Traits\IsDeletedSoftDeletes;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Foundation\Auth\User as Authenticatable;

#[Fillable(['username', 'password', 'avatar', 'mobile', 'email', 'status'])]
#[Hidden(['password', 'salt'])]
class SysUser extends Authenticatable implements JWTSubject
{
    use HasOperator, IsDeletedSoftDeletes;

    protected $table      = 'sys_user';
    protected $primaryKey = 'id';

    const string SYS_USER_TOKEN = 'admin';   // JWT guard 名
    const int USER_FREEZE   = 0;
    const int USER_ACTIVATE = 1;
    const string CAPTCHA_KEY = 'captcha:';

    // 自定义时间字段名(覆盖 Eloquent 默认 created_at/updated_at)
    const string CREATED_AT = 'create_time';
    const string UPDATED_AT = 'update_time';

    protected $casts = [
        'id'          => 'string',
        'status'      => 'integer',
        'create_time' => 'datetime',
        'update_time' => 'datetime',
    ];

    // 统一序列化格式
    protected function serializeDate(\DateTimeInterface $date): string
    {
        return $date->format('Y-m-d H:i:s');
    }
}

7.2 Base model Base

app/Models/Base.php defines project-wide shared constants, and business models inherit it:

Constant Value Meaning
STATUS_ENABLE 1 Enabled
STATUS_DISABLE 0 Disabled
HAVE_DELETED / NOT_DELETED 1 / 0 Soft delete flag
CREATED_AT / UPDATED_AT 'create_time' / 'update_time' Timestamp field name

7.3 MySQL Type Mapping

MySQL type PHP type / cast Description
bigint string(cast) Primary key, foreign key (avoid JS precision loss)
int / tinyint integer Status, flag bit
varchar(n) string Variable-length string
text string Long text
datetime / timestamp datetime Time
json array / json JSON field

7.4 Soft Delete and Field Naming

  • Soft delete uses the is_deleted field (handled by the IsDeletedSoftDeletes trait), queries filter by default is_deleted = 0
  • Field naming comparison:
SQL column name Model property Response key (camelCase) Description
dept_id dept_id deptId Department ID
create_time create_time createTime Creation time
is_deleted is_deleted — Soft delete (internal)
parent_id parent_id parentId Parent node ID

Conversion rule: when Model → response, it is automatically completed by ApiResponse from snake_case → camelCase, no need to manually write toVo()。

7.5 Relations and Accessors

// 多对多:用户-角色(带 tenant_id pivot)
public function roles(): BelongsToMany
{
    return $this->belongsToMany(SysRole::class, 'sys_user_role', 'user_id', 'role_id')
        ->withPivot('tenant_id')
        ->where('sys_role.is_deleted', Base::NOT_DELETED)
        ->where('status', Base::STATUS_ENABLE);
}

// 访问器:输出时计算 dept_name
public function getDeptNameAttribute(): string
{
    return $this->dept?->name ?? '';
}

Part 8: Authentication and Authorization

8.1 JWT Middleware

Routes mount the authentication middleware via the jwt.admin alias (registered in bootstrap/app.php):

// bootstrap/app.php
$middleware->alias([
    'jwt.admin' => AdminAuthMiddleware::class,
    'permission' => PermissionMiddleware::class,
]);

AdminAuthMiddleware Behavior:

  • Use auth('admin')->check() to validate the Token (admin i.e. SysUser::SYS_USER_TOKEN)
  • expired throws USER_TOKEN_EXPIRED, invalid throws USER_NO_LOGIN
  • After passing, write authUser(from getJWTCustomClaims()) into $request->attributes->set('authUser', ...)
  • The current logged-in user is obtained with $request->user() or request()->user() Get

8.2 Login and Token

// AuthController::login
$user = $this->userService->accountLogin($account);   // 按 username/email/mobile 猜测字段
if (!password_verify($password, $user->password)) {
    throw new BusinessException(ResultCode::USER_PASSWORD_ERROR->getMsg());
}
return $this->success($this->buildToken($user), '登录成功');
// buildToken 返回 { access_token, refresh_token, token_type: Bearer, expires_in }

8.3 Permission Validation

  • Permission identifier format:模块:实体:操作(e.g. sys:user:query、sys:user:create)
  • Menu table sys_menu.perm stores permission strings, and user permissions are aggregated by role
  • Permission judgment is done in the Service layer through authUser['authorities'](role code ROLE_xxx) and the model's hasPermission() Complete
  • PermissionMiddleware is a placeholder implementation; the real authorization is handled at the business/route layer; when adding sensitive interfaces, follow the convention in the routes/api.php package permission middleware or validate within the Service

8.4 Multi-tenancy

  • Models have the tenant_id field; user JWT claims contain tenant_id
  • Switch tenant:POST /api/v1/auth/switch-tenant
  • Tenant management interface:/api/v1/tenants、/api/v1/tenant-plans
  • New queries need to carry tenant_id isolation conditions at the Service layer to avoid cross-tenant data leakage

Part 9: Database Operation Specification

9.1 Eloquent Queries

// ✅ 单条查询
$user = SysUser::with(['roles'])->findOrFail($id);

// ✅ 分页 + 筛选
$query = SysUser::query()->select(self::LIST_FIELDS)->with('dept')->orderBy('id', 'asc');
$this->applyFilters($query, $filters);     // 业务筛选
$query->paginate($perPage);

// ✅ 模糊搜索(orWhere 包子查询)
$query->where(function ($q) use ($keyword) {
    $q->whereLike('username', "%{$keyword}%")
      ->orWhere('nickname', 'like', "%{$keyword}%")
      ->orWhere('mobile', 'like', "%{$keyword}%");
});

// ✅ 关联存在性过滤(部门树)
$query->whereHas('dept', function ($q) use ($params) {
    $q->whereRaw("concat(',', concat(tree_path, ',', id), ',') like ?", ["%,{$params['dept_id']},%"]);
});

9.2 Transaction Management

// ✅ 多步写用事务
return DB::transaction(function () use ($ids) {
    SysUserRole::query()->whereIn('user_id', $ids)->delete();
    SysUser::query()->whereIn('id', $ids)->delete();
    return count($ids);
});

9.3 Batch and Relationship Operations

// ✅ 创建并同步多对多
$user = SysUser::query()->create($data);
if (!empty($data['role_ids'])) {
    $user->roles()->sync($data['role_ids']);   // 支持 tenant_id pivot
}

// ✅ 逻辑删除(is_deleted 标记)
SysUser::query()->whereIn('id', $ids)->update(['is_deleted' => Base::HAVE_DELETED]);

// ✅ 软删除 trait(IsDeletedSoftDeletes)自动过滤已删除

Part 10: Adding a New Module — Complete Tutorial

Using adding the "Dictionary Item" module as an example.

Step 1: Create Model

<?php
namespace App\Models;

use App\Traits\IsDeletedSoftDeletes;
use Illuminate\Database\Eloquent\Attributes\Fillable;

#[Fillable(['dict_code', 'value', 'label', 'tag_type', 'status', 'sort'])]
class SysDictItem extends Base
{
    protected $table = 'sys_dict_item';

    const string CREATED_AT = 'create_time';
    const string UPDATED_AT = 'update_time';
}

Step 2: Create Validate

<?php
namespace App\Http\Validates;

use Illuminate\Validation\Rule;

class SysDictItemValidate extends BaseValidate
{
    public function rules(string $scene = 'create', ?int $id = null): array
    {
        return match ($scene) {
            'create' => [
                'dict_code' => ['required', 'string', 'max:50'],
                'value'     => ['required', 'string', 'max:50'],
                'label'     => ['required', 'string', 'max:100'],
                'status'    => 'nullable|integer|in:0,1',
                'sort'      => 'nullable|integer',
            ],
            'update' => [
                'dict_code' => ['nullable', 'string', 'max:50'],
                'value'     => ['nullable', 'string', 'max:50'],
                'label'     => ['nullable', 'string', 'max:100'],
            ],
            default => [],
        };
    }

    public function messages(): array
    {
        return [
            'dict_code.required' => '字典编码不能为空',
            'value.required'     => '字典值不能为空',
            'label.required'     => '字典标签不能为空',
        ];
    }
}

Step 3: Create Service

<?php
namespace App\Services;

use App\Attributes\LogAction;
use App\Common\Enums\ActionType;
use App\Common\Enums\LogModule;
use App\Models\SysDictItem;

class SysDictItemService extends BaseService
{
    #[LogAction(module: LogModule::DICT, action: ActionType::QUERY)]
    protected function paginate(array $filters, int $perPage)
    {
        $query = SysDictItem::query()->where('is_deleted', 0);
        // … 筛选 + 分页
        return $query->paginate($perPage);
    }

    #[LogAction(module: LogModule::DICT, action: ActionType::CREATE)]
    protected function create(array $data): SysDictItem
    {
        return SysDictItem::query()->create($data);
    }

    protected function format(SysDictItem $item): array
    {
        return [
            'id'        => $item->id,
            'dictCode'  => $item->dict_code,
            'value'     => $item->value,
            'label'     => $item->label,
            'status'    => $item->status,
            'sort'      => $item->sort,
        ];
    }
}

Step 4: Create Controller

<?php
namespace App\Http\Controllers\V1;

use App\Http\Controllers\Controller;
use App\Http\Validates\SysDictItemValidate;
use App\Services\SysDictItemService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class SysDictItemController extends Controller
{
    protected string $validatorClass = SysDictItemValidate::class;

    public function __construct(protected SysDictItemService $service) {}

    public function index(Request $request): JsonResponse
    {
        $filters  = $request->only(['dict_code', 'value']);
        $pageSize = (int) $request->input('pageSize', self::PER_PAGE);
        return $this->success($this->service->paginate($filters, $pageSize));
    }

    public function store(Request $request): JsonResponse
    {
        $validated = $this->validateScene($request, 'create');
        return $this->success($this->service->format($this->service->create($validated)), '创建成功');
    }
}

Step 5: Register Route

// routes/api.php(在 jwt.admin 分组内)
Route::prefix('dicts')->group(function () {
    Route::get('{id}/form', [SysDictItemController::class, 'show'])
        ->where(['id' => '[0-9]+']);
    apiIdResource(SysDictItemController::class);
    Route::prefix('{name}/items')->group(function () {
        Route::get('/', [SysDictItemController::class, 'items']);
        Route::get('options', [SysDictItemController::class, 'options']);
    });
});

Step 6: Verify

php artisan serve          # 启动 HTTP
php artisan sse:start --auth=admin start   # 启动 SSE(如需)
# 访问 /api/v1/dicts 确认路由已注册

Part 11: Comment Conventions

11.1 General Principles

  • PHPDoc First: Use /** ... */ doc comments for all public classes and methods
  • Self-explanatory Code: Good naming and structure are self-explanatory; comments describe "why"
  • Chinese First: Use Chinese to explain the problem clearly; keep English for proper nouns (HTTP、JWT、Eloquent)
  • Keep in Sync: When code changes, doc comments must be updated accordingly

11.2 PHPDoc Format

/**
 * 用户分页列表
 * @param Request $request 含 pageNum / pageSize / keywords 等查询参数
 * @return JsonResponse 统一响应 { code, msg, data:{list,total} }
 * @throws BusinessException 参数校验失败 / 无权限
 */
public function index(Request $request): JsonResponse

11.3 Key Points for Comments at Each Layer

Layer Must Explain
Controller Method Functionality, request parameters, return value structure, possible exceptions
Service Method Business semantics, data permission impact, transaction scope
Model Table meaning,$table / time fields / soft delete strategy
Validate Validation rules and Chinese error messages for each scene

11.4 TODO / FIXME Markers

Marker Purpose Format
TODO To be implemented // TODO(标记人, 日期, 说明)
FIXME Known bug // FIXME(标记人, 日期, 说明)

Part 12: Code Quality Checklist

12.1 General Checks

  • All public classes/methods have PHPDoc
  • Responses are output via ApiResponse (success/error/fail) , do not manually response()->json
  • Data is automatically converted to camelCase via CaseConversion , manual arrays use camelCase keys
  • Configuration is managed via .env + config/ , not hardcoded
  • Sensitive information (passwords, tokens) is not written to logs or included in responses
  • composer lint(Laravel Pint) no style issues

12.2 Database Related

  • Model field names are snake_case, consistent with MySQL column names
  • All queries carry is_deleted = 0(or go through the soft delete trait)
  • Multi-step write operations use DB::transaction
  • id Use string cast to avoid JS precision issues
  • Time fields use create_time / update_time(overrides Eloquent defaults)

12.3 API Related

  • Route prefix is uniformly /api/v1/模块名
  • Fixed path endpoints are registered before {id} path parameters
  • Pagination parameters are named pageNum / pageSize
  • Response JSON keys are camelCase (deptId、createTime)
  • HTTP status code is always 200; business code is passed via the body's code passed
  • Errors use the ResultCode enum, no hardcoded strings
  • Sensitive endpoints mount the jwt.admin middleware
  • Multi-tenant queries carry tenant_id isolation

12.4 Service Layer

  • Controller injects Service (constructor), does not write queries directly
  • Return when data does not exist BusinessException(ACCOUNT_NOT_FOUND / NO_DATA_WAS_FOUND)
  • Check parameters are non-empty before batch deletion, protect the super admin
  • format() Uniformly convert Model to camelCase response array
  • Add for write operations #[LogAction] attribute to record operation logs

Part 13: Common Anti-Patterns

Anti-Pattern Problem Correct Practice
Writing SQL/queries in the controller Violates layering Put queries in Service
response()->json([...]) hand-written Inconsistent format, no traceId Use ApiResponse::success/error
Hand-writing snake_case keys in the response Frontend parsing fails Rely on CaseConversion automatic conversion, or manually use camelCase
SQL missing is_deleted = 0 Querying already-deleted data Add logical deletion condition to all queries / use soft delete trait
Fixed path is after {id} after Route is matched incorrectly Register fixed paths first
code="A0210" Hard-coded strings Error-prone, hard to maintain Use ResultCode::USER_PASSWORD_ERROR
Passwords stored in plaintext Security incident password_hash() / Hash::make()
Multi-tenancy missing tenant_id Cross-tenant leakage Service queries carry tenant isolation
dd() / dump() debugging Production leak Use Laravel logging info()/debug()
Validation written in controller Poor reusability Use Validate class + validateScene()

Part 14: Docker Deployment

# docker-compose.yml(示意)
services:
  app:
    build: .
    ports: ["8000:8000"]
    depends_on: [mysql, redis]
    environment:
      DB_HOST: mysql
      DB_DATABASE: youlai_admin
      DB_USERNAME: root
      DB_PASSWORD: "123456"
      REDIS_HOST: redis
    command: php artisan serve --host=0.0.0.0 --port=8000

  sse:
    build: .
    command: php artisan sse:start --auth=admin start
    depends_on: [app]

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: youlai_admin
      MYSQL_ROOT_PASSWORD: "123456"
    volumes:
      - ./database/youlai_admin_tenant.sql:/docker-entrypoint-initdb.d/init.sql

  redis:
    image: redis:7-alpine

Execute before deployment:composer install --no-dev Import database/youlai_admin_tenant.sql, and configure .env the APP_KEY、DB_*、REDIS_* and SSE_LISTEN_URL_ADMIN。


Part 15: Guide to Creating a Project from 0 to 1 (AI-Specific)

When an AI assistant is asked to "create the youlai-laravel module/project according to the specification," follow these steps.

Required Reading Files

  1. Repository:youlai-laravel(complete directory, routes, example modules)
  2. This Skill:youlai-skills/skills/laravel/SKILL.md(naming conventions, development conventions)
  3. Reference Implementation:app/Http/Controllers/V1/SysUserController.php、app/Services/SysUserService.php、app/Http/Validates/SysUserValidate.php

Creation Steps

  1. Prepare the database: import database/youlai_admin_tenant.sql into MySQL 8.0+
  2. Configure the environment: copy .env.example → .env, set APP_KEY、DB_*、REDIS_*, execute composer install
  3. Create the skeleton according to the directory structure(see Part 2)
  4. Add new modules following the Part 10 process: Model → Validate → Service → Controller → Routes
  5. Unified response: the controller extends Controller, use success()/error(); Model uses snake_case, responses are automatically camelCase
  6. Authentication: attach to sensitive routes jwt.admin middleware; Service uses $request->user() to get the current user
  7. Multi-tenancy: queries carry tenant_id isolation
  8. Logging: add to write operations #[LogAction] attribute
  9. Verification:php artisan serve + php artisan sse:start + PHPUnit

Key constraints

  • Database is MySQL 8.0+, logical deletion uses is_deleted
  • API path, response format, error codes 100% aligned with youlai-boot(see API contract document)
  • HTTP status code is always 200, business code is passed via the code field
  • JSON key names are camelCase (automatically converted by ApiResponse 's CaseConversion CaseConversion)
  • Default account:admin / 123456
  • includes multi-tenant, new modules must handle tenant_id isolation