上手开发综合篇
2026年1月12日大约 6 分钟
laravel-fast-api-youhujun 上手开发篇
注意
建议在实际开发中可以参照laravel-fast-api-youhujun,组件化开发自己的业务逻辑.这样便于后续维护以及扩展
接口文档
提示
接口文档使用apipost生成,可以直接调试.
文档地址-https://docs.apipost.net/docs/detail/6e2a44ce7879000?target_id=cb91024fde15f&locale=zh-cn
ApiPost官网-https://www.apipost.cn
自定义模版
注意
除了有准备好的stub模版,在项目不同模块目录下也分别准备了*.php.example的模版,目地就是为了方便开发过程中参考和复制修改
元数据注解
提示
新增自研元数据注解规范DocNote和DocParams
DocNote用于简单单行注解
DocParams用于带参数,返回值的复杂注解,第一个参数是说明,实际参数和返回值用二维数组注释说明和类型,便于长期维护
- 示例1:
<?php
/*
* @Description:
* @version: v1
* @Author: youhujun youhu8888@163.com & xueer
* @Date: 2026-08-20 11:10:44
* @LastEditors: youhujun youhu8888@163.com & xueer
* @LastEditTime: 2026-09-17 14:36:35
* @FilePath: \youhu-laravel-api-13\app\DTOs\YouHuShop\V1\Admin\Goods\GetGoodsDTO.php
* Copyright (C) 2026 youhujun & xueer . All rights reserved.
*/
namespace App\DTOs\YouHuShop\V1\Admin\Goods;
use App\Attributes\Common\DocNote;
use App\Attributes\Common\DocParams;
use App\DTOs\Traits\BaseDTOTrait;
use Illuminate\Support\Facades\Validator;
use App\Exceptions\Common\RuleException;
use App\Rules\Pub\Required;
use App\Rules\Pub\Numeric;
use App\Rules\Pub\CheckUnique;
use App\Rules\Pub\CheckString;
use App\Rules\Pub\CheckBetween;
use App\Rules\Pub\CheckArray;
use App\Rules\Pub\FormatTime;
use App\Rules\Pub\ChineseCodeNumberLine;
/**
* @see \App\Http\Controllers\YouHuShop\V1\Admin\Goods\GoodsController
*/
#[DocNote('DTO')]
class GetGoodsDTO
{
use BaseDTOTrait;
#[DocParams('分页-总数',['required'=>0,'type'=>'number'])]
public int $total = 1;
#[DocParams('分页-当前页',['required'=>1,'type'=>'number'])]
public int $currentPage = 1;
#[DocParams('分页-每页条数',['required'=>1,'type'=>'number'])]
public int $pageSize = 10;
#[DocParams('排序类型',['required'=>1,'type'=>'SortTypeType'])]
public int $sortType = 2;
#[DocParams('是否导出',['required'=>0,'type'=>'IsExpportType'])]
public ?int $isExport = null;
#[DocParams('导出类型',['required'=>1,'type'=>'ExpportTypeType'])]
public ?int $exportType = null;
#[DocParams('查找内容',['required'=>0,'type'=>'string'])]
public ?string $find = null;
#[DocParams('配合搜索内容指定下标',['required'=>1,'type'=>'number'])]
public int $findSelectIndex = 0;
#[DocParams('时间范围',['required'=>0,'type'=>'string[]'])]
public array $timeRange = [];
#[DocParams('店铺uid',['required'=>0,'type'=>'UidType'])]
public int $shop_uid = 0;
#[DocParams('商品spu',['required'=>0,'type'=>'string'])]
public string $goods_spu = '';
#[DocParams('商品编码',['required'=>0,'type'=>'string'])]
public string $goods_code = '';
#[DocParams('商品条形码',['required'=>0,'type'=>'string'])]
public string $goods_line_code = '';
#[DocParams('商品所有者类型 10平台代理 20店铺 30工厂',['required'=>0,'type'=>'OwnerType'])]
public int $owner_type = 0;
#[DocParams('商品类型 10实体 20服务 30虚拟',['required'=>0,'type'=>'GoodsType'])]
public int $goods_type = 0;
#[DocParams('是否上架 1上架 0下架(未上架)',['required'=>0,'type'=>'IsOnSaleType'])]
public int|null $is_on_sale = null;
#[DocParams('审核状态 0 10待审核 20审核中 30已通过 40 拒绝',['required'=>0,'type'=>'CheckStatusType'])]
public int $check_status = 0;
#[DocParams('认证状态 0 10待认证 20认证中 30已通过 40 拒绝',['required'=>0,'type'=>'AuthStatusType'])]
public int $auth_status = 0;
#[DocParams('是否置顶 0否 1是',['required'=>0,'type'=>'IsTopType'])]
public int|null $is_top = null;
#[DocParams('是否推荐 0否 1是',['required'=>0,'type'=>'IsRecommendType'])]
public int|null $is_recommend = null;
#[DocParams('是否新品 0否 1是',['required'=>0,'type'=>'IsNewType'])]
public int|null $is_new = null;
#[DocParams('是否特惠 0否 1是 特惠无分润',['required'=>0,'type'=>'IsDiscountType'])]
public int|null $is_discount = null;
#[DocParams('产品星级0 无 10一星 20 二星 30三星 40四星 50五星',['required'=>0,'type'=>'GoodsStarType'])]
public int|null $goods_star = null;
#[DocParams('字段映射,核心约定必须实现')]
public function getFieldMap(): array
{
return ['total','currentPage', 'pageSize', 'sortType', 'isExport', 'exportType', 'find', 'findSelectIndex', 'timeRange','shop_uid','goods_spu','goods_code','goods_line_code', 'owner_type','goods_type','is_on_sale','check_status','auth_status','is_top','is_recommend','is_new','is_discount','goods_star'];
}
#[DocParams('校验规则')]
public function rules(): array
{
return [
'total' => ['bail', 'nullable', new Numeric()],
'currentPage' => ['bail', 'nullable', new Numeric()],
'pageSize' => ['bail', 'nullable', new Numeric()],
'sortType' => ['bail', new Required(), new Numeric()],
'isExport' => ['bail', 'nullable', new Numeric()],
'exportType' => ['bail', 'nullable', new Numeric()],
'find' => ['bail', 'nullable', new CheckString()],
'findSelectIndex' => ['bail', 'nullable', new Numeric()],
'timeRange' => ['bail', 'nullable', new CheckArray()],
'shop_uid' => ['bail', 'nullable', new Numeric()],
'goods_spu' => ['bail', 'nullable', new CheckString()],
'goods_code' => ['bail', 'nullable', new CheckString()],
'goods_line_code' => ['bail', 'nullable', new CheckString()],
'owner_type' => ['bail', 'nullable', new Numeric()],
'goods_type' => ['bail', 'nullable', new Numeric()],
'is_on_sale' => ['bail', 'nullable', new Numeric()],
'check_status' => ['bail', 'nullable', new Numeric()],
'auth_status' => ['bail', 'nullable', new Numeric()],
'is_top' => ['bail', 'nullable', new Numeric()],
'is_recommend' => ['bail', 'nullable', new Numeric()],
'is_discount' => ['bail', 'nullable', new Numeric()],
'goods_star' => ['bail', 'nullable', new Numeric()],
];
}
#[DocParams('验证字段')]
public function validate(array $data): self
{
$validator = Validator::make($data, $this->rules(), []);
$validated = $validator->validated();
$requiredFields = ['sortType'];
foreach ($requiredFields as $field) {
if (!isset($validated[$field])) {
throw new RuleException('RuleRequiredError', $field);
}
}
$this->fill($validated);
$this->formatFields();
return $this;
}
#[DocParams('格式化|过滤|处理字段')]
public function formatFields(): self
{
foreach ($this->getFieldMap() as $field) {
$this->$field = f($this->$field);
}
return $this;
}
}- 示例2:
<?php
namespace App\Rules\Pub;
use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use App\Exceptions\Common\RuleException;
use App\Facades\Common\V1\Es\EsQueryFacade;
use App\Attributes\Common\DocNote;
use App\Attributes\Common\DocParams;
#[DocNote('检查唯一性规则,自定义用于es索引')]
class CheckUnique implements DataAwareRule, ValidationRule
{
#[DocNote('设置验证数据')]
protected array $dataArray = [];
#[DocNote('es索引名')]
protected string $eIndexName;
#[DocNote('字段名')]
protected string $field;
#[DocNote('忽略的uid')]
protected ?string $ignore_uid;
#[DocNote('忽略的uid字段名,默认user_uid')]
protected string $ignore_uid_field;
public function __construct(string $eIndexName, string $field, string $ignore_uid_field = 'user_uid', ?string $ignore_uid = null)
{
$this->eIndexName = $eIndexName;
$this->field = $field;
$this->ignore_uid_field = $ignore_uid_field;
$this->ignore_uid = $ignore_uid;
}
#[DocParams('设置正在被验证的数据',['dataArray'=>['type'=>'array<string, mixed>','note'=>'正在被验证的数据'],'return'=>['type'=>'static','note'=>'当前对象']])]
public function setData(array $dataArray): static
{
$this->dataArray = $dataArray;
return $this;
}
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
#[DocParams('返回验证规则',['attribute'=>['type'=>'string','note'=>'字段名'],'value'=>['type'=>'string','note'=>'字段值'],'fail'=>['type'=>'\Illuminate\Translation\PotentiallyTranslatedString $fail','note'=>'失败资源'],'return'=>['type'=>'void']])]
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$result = true;
//为了可读性,将变量赋值到局部变量,不要修改删除
$field = $this->field;
$ignore_uid_field = $this->ignore_uid_field;
$ignore_uid = $this->ignore_uid;
$eIndexName = $this->eIndexName;
if(!$eIndexName){
throw new RuleException('EsIndexEmptyError', $attribute);
}
//如果有忽略的UID,则是更新检查
if ($ignore_uid) {
// 精确匹配搜索
$esTotalNumber = $this->getEsSelectTotalNumber($eIndexName, $field, $value);
//超过1个肯定不对
if ($esTotalNumber > 1) {
$result = false;
}
//有一个的情况下,需要判断是否是当前的UID
if ($esTotalNumber == 1) {
$esObject = EsQueryFacade::index($eIndexName)->whereNull('deleted_at')->where($field, $value)->get()->first();
if (isset($esObject->{$ignore_uid_field})) {
if ($esObject->{$ignore_uid_field} !== $ignore_uid) {
$result = false;
}
}
}
} else {
$esTotalNumber = $this->getEsSelectTotalNumber($eIndexName, $field, $value);
if ($esTotalNumber) {
$result = false;
}
}
if (!$result) {
throw new RuleException('RuleCheckUniqueError', $attribute);
}
}
/**
* 获取es查询总数
*
* @param string $indexName
* @return integer
*/
#[DocParams('获取es查询总数',['indexName'=>['type'=>'string','note'=>'es索引名'],'field'=>['type'=>'string','note'=>'字段名'],'value'=>['type'=>'string','note'=>'字段值'],'return'=>['type'=>'int','note'=>'查询总数']])]
private function getEsSelectTotalNumber(string $indexName, string $field, string $value): int
{
$totalNumber = 0;
$esCollection = EsQueryFacade::index($indexName)->whereNull('deleted_at')->where($field, $value)->limit(1000)->get();
$totalNumber = $esCollection->count();
return $totalNumber;
}
}路由
- 路由
注意
建议参考laravel-fast-api-youhujun的路由进行开发,按照功能和模块划分出不同的路由文件,开发阶段可以使用Route::any定义,等到项目开发完成以后,再根据需要区分get和post
控制器
开发思路:
控制器用来过滤参数和权限验证,总体来说控制器只负责接收和处理参数,真正的业务逻辑放在门面代理的服务层实现.如果业务逻辑复杂有两种实现方案
- 第一种:
门面代理的服务层负责主要业务逻辑实现,其他因此产生的关联业务逻辑放在相关事件中去实现
- 第二种:
用不同的门面去处理不同模块的业务逻
