Merge branch 'develop'

This commit is contained in:
mzl
2025-07-29 09:00:04 +08:00
parent a551507e4d
commit d74b374198
106 changed files with 6754 additions and 1346 deletions
+80 -64
View File
@@ -1,47 +1,51 @@
<?php
namespace app\common\traits;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* 数据转换器 Trait
* 用于将原始数据(如模型、集合、分页等)统一转换为数组格式,
* 并支持字段级与结果级的灵活后置加工。
*/
trait DataTransformer
{
/**
* 核心方法:对任意数据进行转换并返回数组
* 核心转换方法
*
* @param mixed $rawData 原始数据,可为 Paginator / Model / Collection / array 等
* @param callable|null $postProcessor 可选的后置处理回调,接受处理完的数组再返回新的数组
* @param callable|null $fieldFormatter 可选的字段命名转换回调,如 snake_case
* @param mixed $rawData 原始数据:Model、Collection、Paginator、数组等
* @param array $opts 可选配置:
* - 'fieldFormatter' => callable 字段名格式化回调,如 fn($f)=>snake_case($f)
* - 'postFormatter' => callable 结果级加工回调,接收整个数组并返回
*
* @return array
* @return array 转换并加工后的纯 PHP 数组
*/
protected function transformData(
mixed $rawData,
?callable $postProcessor = null,
?callable $postFormatter = null,
?callable $fieldFormatter = null
): array {
try {
$fieldFormatter = $fieldFormatter ?: fn($field) => $field;
if ($rawData instanceof LengthAwarePaginator) {
$formatted = $this->formatPaginated($rawData, $postProcessor, $fieldFormatter);
$arr = $this->formatPaginated($rawData, $postFormatter, $fieldFormatter);
} elseif ($rawData instanceof Model) {
$formatted = $this->formatModel($rawData, $fieldFormatter);
$arr = $this->formatModel($rawData, $fieldFormatter);
} elseif ($rawData instanceof Collection) {
$formatted = $this->formatCollection($rawData, $fieldFormatter);
$arr = $this->formatCollection($rawData, $fieldFormatter);
} elseif (is_array($rawData)) {
$formatted = $this->formatArray($rawData, $fieldFormatter);
$arr = $this->formatArray($rawData, $fieldFormatter);
} else {
$formatted = (array) $rawData;
$arr = (array)$rawData;
}
if (is_callable($postProcessor)) {
$formatted = $postProcessor($formatted);
if (is_callable($postFormatter)) {
$arr = $postFormatter($arr);
}
return $formatted;
return $arr;
} catch (\Throwable $e) {
return [
'error' => true,
@@ -52,84 +56,96 @@ trait DataTransformer
}
/**
* 格式化分页数据:将每条记录也进行 transformData 递归处理
* 格式化分页数据:转换每条记录
*
* @param LengthAwarePaginator $paginator
* @param callable|null $postProcessor
* @param callable $fieldFormatter
* @return array
* @param LengthAwarePaginator $p
* @param callable $fmt
* @return array{records: array, total: int, current: int, size: int}
*/
protected function formatPaginated(
LengthAwarePaginator $paginator,
?callable $postProcessor,
callable $fieldFormatter
LengthAwarePaginator $p,
?callable $postFormatter,
callable $fmt
): array {
$list = $paginator->getCollection()->map(function ($item) use ($postProcessor, $fieldFormatter) {
return $this->transformData($item, $postProcessor, $fieldFormatter);
})->toArray();
$records = $p->getCollection()
->map(fn($item) => $this->transformData($item, $postFormatter, $fmt))
->toArray();
return [
'records' => $list,
'total' => $paginator->total(),
'current' => $paginator->currentPage(),
'size' => $paginator->perPage()
'records' => $records,
'total' => $p->total(),
'current' => $p->currentPage(),
'size' => $p->perPage(),
];
}
/**
* 格式化 Eloquent 模型:递归嵌套关联
* 格式化 Eloquent 模型:主表属性 + 关联递归
*
* @param Model $model
* @param callable $fieldFormatter
* @param callable $fmt
* @return array
*/
protected function formatModel(Model $model, callable $fieldFormatter): array
{
$formatted = [];
// 主表字段
protected function formatModel(
Model $model,
callable $fmt
): array {
$out = [];
// 1) 主表字段
foreach ($model->getAttributes() as $key => $value) {
$formatted[$fieldFormatter($key)] = $value;
$out[$fmt($key)] = $value;
}
// 关联数据,递归嵌套
foreach ($model->getRelations() as $relation => $relationData) {
if ($relationData instanceof Model) {
// 单个关联模型
$formatted[$relation] = $this->formatModel($relationData, $fieldFormatter);
} elseif ($relationData instanceof Collection) {
// 关联集合
$formatted[$relation] = $relationData
->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
// 2) 关联递归
foreach ($model->getRelations() as $rel => $relData) {
if ($relData instanceof Model) {
$out[$rel] = $this->formatModel($relData, $fmt);
} elseif ($relData instanceof Collection) {
$out[$rel] = $relData
->map(fn($item) => $this->transformData($item, null, $fmt))
->toArray();
} else {
$out[$rel] = $relData;
}
}
return $formatted;
return $out;
}
/**
* 格式化集合
* 格式化集合数据:对每个元素递归调用 transformData
*
* @param Collection $collection
* @param callable $fieldFormatter
* @param Collection $c
* @param callable $fmt
* @return array
*/
protected function formatCollection(Collection $collection, callable $fieldFormatter): array
{
return $collection->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
->toArray();
protected function formatCollection(
Collection $c,
callable $fmt
): array {
return $c
->map(fn($item) => $this->transformData($item, null, $fmt))
->toArray();
}
/**
* 格式化普通数组
* 格式化普通数组:递归处理可能出现的模型或集合
*
* @param array $array
* @param callable $fieldFormatter
* @param array $arr
* @param callable $fmt
* @return array
*/
protected function formatArray(array $array, callable $fieldFormatter): array
{
return array_map(fn($item) => $this->transformData($item, null, $fieldFormatter), $array);
protected function formatArray(
array $arr,
callable $fmt
): array {
return array_map(function($item) use ($fmt) {
if (is_array($item)
|| $item instanceof Model
|| $item instanceof Collection
|| $item instanceof LengthAwarePaginator
) {
return $this->transformData($item, null, $fmt);
}
return $item;
}, $arr);
}
}
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace app\common\traits;
use support\Request;
trait FormatKvTrait
{
/**
* 主入口:一行把前端扁平化参数变成完整的查询配置
*
* @param Request $request
* @param string $modelClass Eloquent 模型类名
* @return array{
* model: string,
* where: array,
* order: array<string,string>,
* fields: string[],
* include: string[]
* }
*/
protected function buildQueryConfig(Request $request, string $modelClass): array
{
$all = $request->all();
return [
'model' => $modelClass,
'where' => $this->parseFilters($all),
'order' => $this->parseSort($all['sort'] ?? ''),
'fields' => $this->parseCsv($all['fields'] ?? ''),
'include' => $this->parseCsv($all['include'] ?? ''),
];
}
/**
* 把除 sort/fields/include/page/limit 以外的所有参数
* 智能转换为 where 条件
*
* 支持:
* - 等值:foo=bar
* - NOT:foo=!bar 或 foo=!bar1,bar2
* - 范围:foo=2025-07-01,2025-07-15 → between
* - 列表:foo=a,b,c → in
* - NULL:foo=null
* - NOT NULL:foo=!null
* - 通配符:foo=xia* → like 'xia%'
*
* 关联字段直接写 relation.field=xxx
*/
protected function parseFilters(array $params): array
{
$where = [];
$reserved = ['sort','fields','include','page','limit'];
foreach ($params as $key => $val) {
if (in_array($key, $reserved, true)) {
continue;
}
// NULL / NOT NULL
if ($val === 'null') {
$where[$key] = ['is null'];
continue;
}
if ($val === '!null') {
$where[$key] = ['is not null'];
continue;
}
// NOT equal or NOT IN
if (str_starts_with($val, '!')) {
$v = substr($val, 1);
if (str_contains($v, ',')) {
$list = array_map('trim', explode(',', $v));
$where[$key] = ['not in', $list];
} else {
$where[$key] = ['<>', $this->cast($v)];
}
continue;
}
// BETWEEN or IN
if (str_contains($val, ',')) {
$parts = array_map('trim', explode(',', $val));
if (count($parts) === 2 && $this->isRangeValue($parts[0], $parts[1])) {
$where[$key] = ['between', $parts];
} else {
$where[$key] = ['in', $this->castArray($parts)];
}
continue;
}
// LIKE
if (str_contains($val, '*') || str_contains($val, '%')) {
$pattern = str_replace('*', '%', $val);
$where[$key] = ['like', $pattern];
continue;
}
// 默认 EQ
$where[$key] = $this->cast($val);
}
return $where;
}
/**
* 解析 sort=id,-created_at → ['id'=>'asc','created_at'=>'desc']
*/
protected function parseSort(string $sort): array
{
$order = [];
foreach (explode(',', $sort) as $piece) {
$f = trim($piece);
if ($f === '') {
continue;
}
if ($f[0] === '-') {
$order[substr($f, 1)] = 'desc';
} else {
$order[$f] = 'asc';
}
}
return $order;
}
/**
* 逗号分割成数组
*/
protected function parseCsv(string $csv): array
{
if ($csv === '') {
return [];
}
return array_filter(array_map('trim', explode(',', $csv)));
}
/** 简单类型转换:全数字→int/float,其余保留原字符串 */
protected function cast(string $v): mixed
{
if (is_numeric($v)) {
return strpos($v, '.') !== false ? (float)$v : (int)$v;
}
return $v;
}
/** 批量类型转换 */
protected function castArray(array $arr): array
{
return array_map(fn($v) => $this->cast($v), $arr);
}
/** 判断两端是否可做 BETWEEN(数字或 YYYY-MM-DD 日期) */
protected function isRangeValue(string $a, string $b): bool
{
return (is_numeric($a) && is_numeric($b))
|| (preg_match('/^\d{4}-\d{2}-\d{2}$/', $a)
&& preg_match('/^\d{4}-\d{2}-\d{2}$/', $b));
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace app\common\traits;
use think\Validate;
use support\Request;
use support\exception\BusinessException;
trait RequestTrait
{
/**
* 验证数据
*
* @param Validate $validator 验证器实例
* @param array $data 待验证的数据
* @param string $scene 验证场景
* @return array 验证后的数据
* @throws BusinessException 验证失败时抛出业务异常
*/
public function validate(Validate $validator, array $data, string $scene = ''): array
{
// 如果指定了验证场景,并且验证器支持场景方法,则设置验证场景
if ($scene && method_exists($validator, 'scene')) {
$validator->scene($scene);
}
// 执行数据验证
if (!$validator->check($data)) {
// 验证失败,抛出业务异常,异常信息为验证器返回的错误信息
throw new BusinessException($validator->getError());
}
// 验证成功,返回原始数据
return $data;
}
}
@@ -0,0 +1,148 @@
<?php
namespace app\common\traits\crud;
use support\Request;
/**
* CRUD 操作控制器 Trait
* 提供通用的增删改查控制器方法,通过调用 service 层实现业务逻辑解耦。
*/
trait CrudControllerTrait
{
/**
* 获取列表
*
* @param Request $request 请求对象
* @return mixed 返回分页数据及总数
*/
public function index(Request $request)
{
try {
$raw = $this->service->serviceList($request);
$data = $this->transformData($raw, method_exists($this, 'indexFormat')
? [$this, 'indexFormat']
: null);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 获取详情
*
* @param Request $request 请求对象
* @param int|string $id 资源 ID
* @return mixed 返回资源详情数据
*/
public function show(Request $request)
{
try {
$raw = $this->service->serviceShow($request);
$data = $this->transformData($raw, method_exists($this, 'showFormat')
? [$this, 'showFormat']
: null);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 创建
*
* @param Request $request 请求对象
* @return mixed 返回创建成功的数据或信息
*/
public function store(Request $request)
{
try {
$data = $this->validate($this->service->getValidator(), $request->post(), 'store');
$res = $this->service->serviceStore($request, $data);
return $this->success($res, '创建成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 更新
*
* @param Request $request 请求对象
* @return mixed 返回更新成功的数据或信息
*/
public function update(Request $request)
{
try {
$data = $this->validate($this->service->getValidator(), $request->post(), 'update');
$res = $this->service->serviceUpdate($request, $data);
return $this->success([], '更新成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 删除
*
* @param Request $request 请求对象
* @return mixed 返回删除操作结果
*/
public function destroy(Request $request)
{
try {
$count = $this->service->serviceDestroy($request);
return $this->success([], "成功删除了 {$count} 条");
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
// ----------------------------------------------------------------
// 以下三个钩子方法,**在使用此 Trait 的 Controller** 中实现
// ----------------------------------------------------------------
/**
* 字段名格式化:例如全部下划线
*
* @param string $field
* @return string
*/
// protected function fieldFormatter(string $field): string
// {
// return snake_case($field);
// }
/**
* 字段级后置加工规则
*
* @return array<string,callable>
* 键为 “点号嵌套”或 “通配符” 字段路径,
* 值为 fn($value, $row)=>mixed
*/
// protected function getFieldTransforms(): array
// {
// return [
// 'name' => fn($v,$row)=>strtoupper($v),
// 'orders.*.total' => fn($v,$row)=>number_format($v,2),
// ];
// }
/**
* 结果级后置加工:接收整个数组,可任意增删改
*
* @param array $data
* @return array
*/
// protected function postFormat(array $data): array
// {
// // 例如重命名 records 为 items
// if (isset($data['records'])) {
// $data['items'] = $data['records'];
// unset($data['records']);
// }
// return $data;
// }
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace app\common\traits\crud;
use app\common\utils\CrudUtil;
/**
* Trait CrudLogicTrait
*
* 提供最基础的 CRUD 和复杂查询方法,基于 CrudUtil。
* 使用此 Trait 的类,须定义:
* protected string $modelClass = \App\common\model\YourModel::class;
*/
trait CrudLogicTrait
{
/**
* 分页列表
*/
public function logicList(array $config, int $limit, int $page)
{
$config['model'] = $this->modelClass;
return CrudUtil::paginate(CrudUtil::buildQuery($config), $limit, $page);
}
/**
* 全量列表
*/
public function logicListAll(array $config)
{
$config['model'] = $this->modelClass;
return CrudUtil::getAll(CrudUtil::buildQuery($config));
}
/**
* 查询单条
*/
public function logicShow(array $config)
{
return CrudUtil::findOneBy(CrudUtil::buildQuery($config));
}
/**
* 创建(可带 dataLimit)
*/
public function logicCreate(array $data, array $dataLimit = [])
{
return CrudUtil::create($this->modelClass, $data, $dataLimit);
}
/**
* 任意条件更新(可带 dataLimit)
*/
public function logicUpdateBy(array $config, array $data)
{
return CrudUtil::updateBy(CrudUtil::buildQuery($config), $data);
}
/**
* 任意条件删除(可带 dataLimit)
*/
public function logicDeleteBy(array $config)
{
return CrudUtil::deleteBy(CrudUtil::buildQuery($config));
}
/**
* 复杂查询回调
*/
public function logicComplex(callable $callback)
{
return CrudUtil::executeComplex($this->modelClass, $callback);
}
/**
* 根据主键查询
*/
public function findByPk(int $id, array $with = [])
{
return CrudUtil::findByPk($this->modelClass, $id, $with);
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
namespace app\common\traits\crud;
use support\Request;
/**
* Trait CrudServiceTrait
*
* 提供标准的增删改查 Service 接口,依赖 CrudLogicTrait。
* 使用此 Trait 的 Service 类,须:
* use CrudLogicTrait, CrudServiceTrait;
* protected string $modelClass = \App\common\model\YourModel::class;
*/
trait CrudServiceTrait
{
use CrudLogicTrait;
/** 具体 Service 里定义:protected string $modelClass = YourModel::class; */
/**
* 获取验证器
*/
public function getValidator()
{
return $this->validator;
}
/** 分页列表 */
public function serviceList(Request $request)
{
if (method_exists($this, 'complexQuery')) {
$result = $this->complexQuery($request->get());
} else {
$config = $this->buildQueryConfig($request, 'index');
$limit = (int)$request->get('size', 15);
$page = (int)$request->get('current', 1);
$result = $this->logicList($config, $limit, $page);
}
return $this->postProcess('List', $result);
}
/** 全量列表 */
public function serviceListAll(Request $request)
{
if (method_exists($this, 'complexQuery')) {
$result = $this->complexQuery($request->get());
} else {
$config = $this->buildQueryConfig($request, 'default');
$result = $this->logicListAll($config);
}
return $this->postProcess('List', $result);
}
/** 查看单条 */
public function serviceShow(Request $request)
{
$config = $this->buildQueryConfig($request, 'show');
$item = $this->logicShow($config);
return $this->postProcess('Show', $item);
}
/** 创建 */
public function serviceStore(Request $request, array $data = [])
{
$data = $this->preProcess('Store', $data, $request);
$dl = $this->getDataLimitFromRequest($request);
$created = $this->logicCreate($data, $dl);
return $this->postProcess('Store', $created);
}
/** 更新 */
public function serviceUpdate(Request $request, array $data = [])
{
$data = $this->preProcess('Update', $data, $request);
$config = $this->buildQueryConfig($request, 'update');
$updated = $this->logicUpdateBy($config, $data);
return $this->postProcess('Update', $updated);
}
/** 删除 */
public function serviceDestroy(Request $request)
{
$config = $this->buildQueryConfig($request, 'destroy');
$deleted = $this->logicDeleteBy($config);
return $this->postProcess('Destroy', $deleted);
}
/** ----------------- 公共辅助方法 ----------------- */
/**
* 构造 CrudUtil 查询配置
*/
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'default':
$config['where']['status'] = 1;
$config['order']['sort'] = 'desc';
$config['order']['id'] = 'desc';
break;
case 'index':
$config['where']['status'] = 1;
$config['order']['sort'] = 'desc';
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
$config['where']['status'] = 1;
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
/** 解析 withFields 字符串 "rel:field,field;..." */
protected function parseWithFields(string $str): array
{
if (!$str) {
return [];
}
$out = [];
foreach (explode(';', $str) as $pair) {
[$rel, $flds] = explode(':', $pair);
$out[$rel] = explode(',', $flds);
}
return $out;
}
/** 从请求读取 dataLimitType, dataLimitField */
protected function getDataLimitFromRequest(Request $req): array
{
$type = $req->get('dataLimitType');
if (!$type) {
return [];
}
return [
'type' => $type,
'field' => $req->get('dataLimitField', 'admin_id'),
];
}
/**
* $data 前置处理
*
* @param array $data
* @param Request $request
* @return array
*/
protected function preProcess(string $op, mixed $data, Request $request): mixed
{
$hook = "beforeService{$op}";
return method_exists($this, $hook)
? $this->{$hook}($data, $request)
: $data;
}
/**
* 根据操作类型,调用对应的后置钩子
*
* @param string $op 操作:List, Show, Store, Update, Destroy
* @param mixed $data 原始返回值
* @return mixed 处理后的返回值
*/
protected function postProcess(string $op, mixed $data): mixed
{
$hook = "afterService{$op}";
return method_exists($this, $hook)
? $this->{$hook}($data)
: $data;
}
/**
* 处理复杂查询
* 覆盖此方法,实现复杂查询逻辑
*/
// protected function complexQuery(array $params): array
// {
// throw new \LogicException('请在子类实现 complexQuery');
// }
}