This Skill is used with the
youlai-laravelrepository. 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-laravelrepository
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_idfield +sys_tenanttable + switch tenant API), new modules must includetenant_idisolation 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 onBaseServices/is the business layer, depends onModels/andValidates/Http/Controllers/V1/is the interface layer, depends onServices/andValidates/, reuses validators viavalidateScene()Reuse validatorCommon/are constants/enums/interfaces, shared by all layersTraits/ApiResponse.phpprovides unified responses, shared byControllerandBaseServiceShared
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'sCaseConversion, so the JSON key names received by the frontend are camelCase. No need for manualtoCamelCase。
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 standardapiResource's single-parameteridto 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:
0Success (00000)AUser-side error (authentication/permission/parameter)BSystem-side errorCThird-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 topaginate(), 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
dataautomatically becomes camelCase (deptId、createTime) - no need to manually in the Service
toCamelCase, butformat()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_deletedfield (handled by theIsDeletedSoftDeletestrait), queries filter by defaultis_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
ApiResponsefrom snake_case → camelCase, no need to manually writetoVo()。
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 (admini.e.SysUser::SYS_USER_TOKEN) - expired throws
USER_TOKEN_EXPIRED, invalid throwsUSER_NO_LOGIN - After passing, write
authUser(fromgetJWTCustomClaims()) into$request->attributes->set('authUser', ...) - The current logged-in user is obtained with
$request->user()orrequest()->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.permstores permission strings, and user permissions are aggregated by role - Permission judgment is done in the Service layer through
authUser['authorities'](role codeROLE_xxx) and the model'shasPermission()Complete PermissionMiddlewareis a placeholder implementation; the real authorization is handled at the business/route layer; when adding sensitive interfaces, follow the convention in theroutes/api.phppackagepermissionmiddleware or validate within the Service
8.4 Multi-tenancy
- Models have the
tenant_idfield; user JWT claims containtenant_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_idisolation 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 manuallyresponse()->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 -
idUsestringcast 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
codepassed - Errors use the
ResultCodeenum, no hardcoded strings - Sensitive endpoints mount the
jwt.adminmiddleware - Multi-tenant queries carry
tenant_idisolation
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-devImportdatabase/youlai_admin_tenant.sql, and configure.envtheAPP_KEY、DB_*、REDIS_*andSSE_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
- Repository:
youlai-laravel(complete directory, routes, example modules) - This Skill:
youlai-skills/skills/laravel/SKILL.md(naming conventions, development conventions) - Reference Implementation:
app/Http/Controllers/V1/SysUserController.php、app/Services/SysUserService.php、app/Http/Validates/SysUserValidate.php
Creation Steps
- Prepare the database: import
database/youlai_admin_tenant.sqlinto MySQL 8.0+ - Configure the environment: copy
.env.example→.env, setAPP_KEY、DB_*、REDIS_*, executecomposer install - Create the skeleton according to the directory structure(see Part 2)
- Add new modules following the Part 10 process: Model → Validate → Service → Controller → Routes
- Unified response: the controller extends
Controller, usesuccess()/error(); Model uses snake_case, responses are automatically camelCase - Authentication: attach to sensitive routes
jwt.adminmiddleware; Service uses$request->user()to get the current user - Multi-tenancy: queries carry
tenant_idisolation - Logging: add to write operations
#[LogAction]attribute - 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
codefield - JSON key names are camelCase (automatically converted by
ApiResponse'sCaseConversionCaseConversion) - Default account:
admin/123456 - includes multi-tenant, new modules must handle
tenant_idisolation