Initial commit
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
namespace app\mzl\common\controller;
|
||||
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use support\exception\BusinessException;
|
||||
use app\mzl\common\traits\DataTransformer;
|
||||
use app\mzl\common\traits\ResponseTrait;
|
||||
use app\mzl\common\utils\SimpleQueryBuilder;
|
||||
|
||||
abstract class BaseController
|
||||
{
|
||||
use ResponseTrait, DataTransformer;
|
||||
|
||||
/**
|
||||
* 可配置的验证器(类名/实例/闭包)
|
||||
*/
|
||||
protected mixed $validator = null;
|
||||
|
||||
/**
|
||||
* 子类必须实现:返回对应的 Service 实例
|
||||
*
|
||||
* @return mixed Service 实例
|
||||
*/
|
||||
abstract protected function getService();
|
||||
|
||||
/**
|
||||
* 获取请求参数
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return array 请求参数数组
|
||||
*/
|
||||
protected function input(Request $request): array
|
||||
{
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
*
|
||||
* @return int|null 当前用户ID,如果未登录则返回null
|
||||
*/
|
||||
protected function getUserId(): ?int
|
||||
{
|
||||
return request()->user->user_id ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入字段
|
||||
* 如果数据中不存在指定字段,则注入当前用户ID
|
||||
*
|
||||
* @param array $data 数据数组
|
||||
* @param string $field 字段名,默认为 'user_id'
|
||||
* @return array 注入字段后的数据数组
|
||||
*/
|
||||
protected function injectAdd(array $data, string $field = 'user_id'): array
|
||||
{
|
||||
return !isset($data[$field]) ? $data + [$field => $this->getUserId()] : $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数预处理入口,按场景分发
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $scene
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareParams(Request $request, string $scene): array
|
||||
{
|
||||
// 默认所有场景都注入 user_id
|
||||
return $this->injectAdd($request->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* 后置处理单条记录:默认不变
|
||||
*
|
||||
* 子类可以重写这个方法来做额外字段加工,
|
||||
* 比如加 computed 字段、日期格式化、脱敏等等。
|
||||
*
|
||||
* @param array $item 单条记录(pruned 后的数组)
|
||||
* @return array 处理后的数组
|
||||
*/
|
||||
protected function postProcessItem(array $item, string $scene): array
|
||||
{
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 queryScenes 配置构造 fieldsMap
|
||||
*
|
||||
* @param array $config 必须包含 keys: fields, relations
|
||||
* @return array
|
||||
*/
|
||||
// protected function buildFieldsMap2(array $config): array
|
||||
// {
|
||||
// $relations = $config['relations'] ?? [];
|
||||
// $fieldsMap = [];
|
||||
|
||||
// // 根层:主表字段 + 所有一级关联名
|
||||
// $rootFields = $config['fields'] ?? [];
|
||||
// foreach (array_keys($relations) as $path) {
|
||||
// $rootFields[] = explode('.', $path, 2)[0];
|
||||
// }
|
||||
// $fieldsMap[''] = array_values(array_unique($rootFields));
|
||||
|
||||
// // 每个关联层:自己字段 + 直接子关联名
|
||||
// foreach ($relations as $path => $ownFields) {
|
||||
// $fieldsMap[$path] = $ownFields;
|
||||
// $segments = explode('.', $path);
|
||||
// $depth = count($segments);
|
||||
// if ($depth > 1) {
|
||||
// for ($i = 1; $i < $depth; $i++) {
|
||||
// $parent = implode('.', array_slice($segments, 0, $i));
|
||||
// $child = $segments[$i];
|
||||
// $fieldsMap[$parent][] = $child;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 去重每一层
|
||||
// foreach ($fieldsMap as $k => $arr) {
|
||||
// $fieldsMap[$k] = array_values(array_unique($arr));
|
||||
// }
|
||||
|
||||
// return $fieldsMap;
|
||||
// }
|
||||
// protected function buildFieldsMap(array $config): array
|
||||
// {
|
||||
// $relations = $config['relations'] ?? [];
|
||||
|
||||
// // 1) 处理主表层字段:把 "col as alias" -> alias
|
||||
// $rawFields = $config['fields'] ?? [];
|
||||
// $root = [];
|
||||
// foreach ($rawFields as $f) {
|
||||
// if (preg_match('/\s+as\s+/i', $f)) {
|
||||
// // alias 是 as 之后的部分
|
||||
// [$_, $alias] = preg_split('/\s+as\s+/i', $f);
|
||||
// $root[] = $alias;
|
||||
// } else {
|
||||
// $root[] = $f;
|
||||
// }
|
||||
// }
|
||||
// // 再把顶级关联名也加进来
|
||||
// foreach (array_keys($relations) as $path) {
|
||||
// $root[] = explode('.', $path, 2)[0];
|
||||
// }
|
||||
// $fieldsMap = ['' => array_values(array_unique($root))];
|
||||
|
||||
// // 2) 每个关联路径,自身字段 & 子关联名
|
||||
// foreach ($relations as $path => $rawCols) {
|
||||
// $arr = [];
|
||||
// // 同样把 alias 拆出来
|
||||
// foreach ($rawCols as $col) {
|
||||
// if (preg_match('/\s+as\s+/i', $col)) {
|
||||
// [, $alias] = preg_split('/\s+as\s+/i', $col);
|
||||
// $arr[] = $alias;
|
||||
// } else {
|
||||
// $arr[] = $col;
|
||||
// }
|
||||
// }
|
||||
// // 再把它的直接子关联名也加上
|
||||
// $segments = explode('.', $path);
|
||||
// $depth = count($segments);
|
||||
// if ($depth > 1) {
|
||||
// for ($i = 1; $i < $depth; $i++) {
|
||||
// $parent = implode('.', array_slice($segments, 0, $i));
|
||||
// $child = $segments[$i];
|
||||
// $fieldsMap[$parent][] = $child;
|
||||
// }
|
||||
// }
|
||||
// $fieldsMap[$path] = array_values(array_unique($arr));
|
||||
// }
|
||||
|
||||
// return $fieldsMap;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 验证请求
|
||||
*
|
||||
* @param array $data 请求数据
|
||||
* @param string $scene 验证场景
|
||||
* @return array 验证后的数据
|
||||
* @throws BusinessException 验证失败时抛出业务异常
|
||||
*/
|
||||
protected function validateRequest(array $data, string $scene = ''): array
|
||||
{
|
||||
if (!$this->validator) {
|
||||
return $data;
|
||||
}
|
||||
$validator = is_callable($this->validator)
|
||||
? call_user_func($this->validator)
|
||||
: (is_string($this->validator) ? new $this->validator : $this->validator);
|
||||
|
||||
if ($scene && method_exists($validator, 'scene')) {
|
||||
$validator->scene($scene);
|
||||
}
|
||||
if (!$validator->check($data)) {
|
||||
throw new BusinessException($validator->getError());
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @param string $scene 场景名称,默认为 'default'
|
||||
* @param callable|null $postProcessor 额外字段处理回调函数
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
protected function listWithTransform(
|
||||
array $params,
|
||||
string $scene = 'default',
|
||||
bool $paginate = true
|
||||
): Response {
|
||||
$collection = $paginate
|
||||
? $this->getService()->list($params, $scene)
|
||||
: $this->getService()->all($params, $scene);
|
||||
|
||||
$config = $this->getService()->queryScenes()[$scene]($params);
|
||||
// $fieldsMap = $this->buildFieldsMap($config);
|
||||
|
||||
// —— 如果 fields 为空,就不做任何裁剪,直接原样输出 ——
|
||||
if (empty($config['fields'])) {
|
||||
$data = $this->transformData(
|
||||
$collection,
|
||||
fn(array $item) => $this->postProcessItem($item, $scene)
|
||||
);
|
||||
return $this->successResponse($data);
|
||||
}
|
||||
|
||||
$fieldsMap = SimpleQueryBuilder::buildFieldsMap($config);
|
||||
|
||||
$result = $this->transformAndPrune(
|
||||
$collection,
|
||||
$fieldsMap,
|
||||
null,
|
||||
fn(array $item) => $this->postProcessItem($item, $scene)
|
||||
);
|
||||
|
||||
return $this->successResponse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源列表
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @param string $scene 场景名称,默认为 'default'
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function index(Request $request, string $scene = 'default'): Response
|
||||
{
|
||||
return $this->listWithTransform(
|
||||
$this->prepareParams($request, $scene),
|
||||
$scene
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有资源(不分页)
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @param string $scene 场景名称,默认为 'default'
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function all(Request $request, string $scene = 'default'): Response
|
||||
{
|
||||
return $this->listWithTransform(
|
||||
$this->prepareParams($request, $scene),
|
||||
$scene,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public function oneWithTransform(array $params, string $scene = 'default', int $id = 0): Response
|
||||
{
|
||||
$model = $id
|
||||
? $this->getService()->getById($id, $params, $scene)
|
||||
: $this->getService()->getOne($params, $scene);
|
||||
|
||||
if (!$model) {
|
||||
return $this->errorResponse('资源不存在', 1, 404);
|
||||
}
|
||||
|
||||
$config = $this->getService()->queryScenes()[$scene]($params);
|
||||
// $fieldsMap = $this->buildFieldsMap($config);
|
||||
|
||||
// —— 如果 fields 为空,就不做任何裁剪,直接原样输出 ——
|
||||
if (empty($config['fields'])) {
|
||||
$data = $this->transformData(
|
||||
$model,
|
||||
fn(array $item) => $this->postProcessItem($item, $scene)
|
||||
);
|
||||
return $this->successResponse($data);
|
||||
}
|
||||
|
||||
$fieldsMap = SimpleQueryBuilder::buildFieldsMap($config);
|
||||
|
||||
$data = $this->transformAndPrune(
|
||||
$model,
|
||||
$fieldsMap,
|
||||
null,
|
||||
fn(array $item) => $this->postProcessItem($item, $scene)
|
||||
);
|
||||
|
||||
return $this->successResponse($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /resource/{id}
|
||||
*/
|
||||
public function show(Request $request, int $id, string $scene = 'default'): Response
|
||||
{
|
||||
return $this->oneWithTransform(
|
||||
$this->prepareParams($request, $scene),
|
||||
$scene,
|
||||
$id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /resource/one (按任意条件取单条)
|
||||
*/
|
||||
public function one(Request $request, string $scene = 'default'): Response
|
||||
{
|
||||
return $this->oneWithTransform(
|
||||
$this->prepareParams($request, $scene),
|
||||
$scene
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新资源
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function store(Request $request): Response
|
||||
{
|
||||
$data = $this->validateRequest($request->all(), 'store');
|
||||
$model = $this->getService()->create($data);
|
||||
return $this->successResponse($this->transformData($model));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新现有资源
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @param int $id 资源ID
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function update(Request $request, int $id): Response
|
||||
{
|
||||
$data = $this->validateRequest($request->all(), 'update');
|
||||
$result = $this->getService()->update($id, $data);
|
||||
return $result
|
||||
? $this->successResponse(['id' => $id])
|
||||
: $this->errorResponse('更新失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除资源
|
||||
*
|
||||
* @param int $id 资源ID
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function destroy(int $id): Response
|
||||
{
|
||||
$result = $this->getService()->delete($id);
|
||||
return $result
|
||||
? $this->successResponse(['id' => $id])
|
||||
: $this->errorResponse('删除失败');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace app\mzl\common\org;
|
||||
|
||||
|
||||
class XunfeiFanyi
|
||||
{
|
||||
|
||||
protected $error = "";
|
||||
|
||||
public function tocurl($url, $header, $content)
|
||||
{
|
||||
$ch = curl_init();
|
||||
if(substr($url,0,5)=='https'){
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在
|
||||
curl_setopt($ch, CURLOPT_SSLVERSION, 1);
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
if (is_array($header)) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
if (!empty($content)) {
|
||||
if (is_array($content)) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($content));
|
||||
} else if (is_string($content)) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
|
||||
}
|
||||
}
|
||||
$response = curl_exec($ch);
|
||||
$error=curl_error($ch);
|
||||
//var_dump($error);
|
||||
if($error){
|
||||
die($error);
|
||||
}
|
||||
$header = curl_getinfo($ch);
|
||||
curl_close($ch);
|
||||
$data = array('header' => $header,'body' => $response);
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
public function xfyun($text = "中华人民共和国于1949年成立", $from = "cn", $to = "en")
|
||||
{
|
||||
//在控制台-我的应用-机器翻译获取
|
||||
$app_id = "3d46198c";
|
||||
//在控制台-我的应用-机器翻译获取
|
||||
$api_sec = "ZWY2MmUyNjJjYmFkOTFlYTVmYWVhMGYy";
|
||||
//在控制台-我的应用-机器翻译获取
|
||||
$api_key = "b6d805f240176ecaea85130cb5f2bca1";
|
||||
// 机器翻译接口地址
|
||||
$url = "https://ntrans.xfyun.cn/v2/ots";
|
||||
//body组装
|
||||
// $text = "中华人民共和国于1949年成立";//待翻译文本
|
||||
$body = json_encode($this->getBody($app_id, $from, $to, $text));
|
||||
|
||||
// 组装http请求头
|
||||
$date =gmdate('D, d M Y H:i:s') . ' GMT';
|
||||
|
||||
$digestBase64 = "SHA-256=".base64_encode(hash("sha256", $body, true));
|
||||
$builder = sprintf("host: %s
|
||||
date: %s
|
||||
POST /v2/ots HTTP/1.1
|
||||
digest: %s", "ntrans.xfyun.cn", $date, $digestBase64);
|
||||
// echo($builder);
|
||||
$sha = base64_encode(hash_hmac("sha256", $builder, $api_sec, true));
|
||||
|
||||
$authorization = sprintf("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", $api_key,"hmac-sha256",
|
||||
"host date request-line digest", $sha);
|
||||
|
||||
$header = [
|
||||
"Authorization: ".$authorization,
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json,version=1.0',
|
||||
'Host: ntrans.xfyun.cn',
|
||||
'Date: ' .$date,
|
||||
'Digest: '.$digestBase64
|
||||
];
|
||||
$response = $this->tocurl($url, $header, $body);
|
||||
|
||||
// var_dump($response['body']);
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
public function getBody($app_id, $from, $to, $text)
|
||||
{
|
||||
$common_param = [
|
||||
'app_id' => $app_id
|
||||
];
|
||||
|
||||
$business = [
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
];
|
||||
|
||||
$data = [
|
||||
"text" => base64_encode($text)
|
||||
];
|
||||
|
||||
return $body = [
|
||||
'common' => $common_param,
|
||||
'business' => $business,
|
||||
'data' => $data
|
||||
];
|
||||
}
|
||||
|
||||
// 翻译
|
||||
public function fanyi($text = "", $from = "", $to = "")
|
||||
{
|
||||
try {
|
||||
$res = $this->xfyun($text, $from, $to);
|
||||
$data = json_decode($res["body"], true);
|
||||
if ($data["code"] === 0) {
|
||||
return $data["data"]["result"];
|
||||
} else {
|
||||
$this->error = $data["message"];
|
||||
return false;
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
$this->error = $th->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 异常
|
||||
public function error()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace app\mzl\common\services;
|
||||
|
||||
use app\mzl\common\utils\SimpleQueryBuilder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* 通用基础服务类 BaseService
|
||||
*/
|
||||
abstract class BaseService
|
||||
{
|
||||
/**
|
||||
* Eloquent 模型实例
|
||||
* @var Model
|
||||
*/
|
||||
protected Model $model;
|
||||
|
||||
/**
|
||||
* 允许的字段列表
|
||||
*/
|
||||
protected $allowedFields = [];
|
||||
|
||||
// 声明需要多语言翻译
|
||||
protected array $translatable = [];
|
||||
|
||||
/**
|
||||
* 从$data中提取出源文本字段,并从$data里删除它们
|
||||
*/
|
||||
protected function extractSourceFields(array &$data): array
|
||||
{
|
||||
$source = [];
|
||||
foreach ($this->translatable as $field) {
|
||||
if (array_key_exists($field, $data)) {
|
||||
$source[$field] = $data[$field];
|
||||
unset($data[$field]);
|
||||
}
|
||||
}
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子类必须实现:返回模型类名
|
||||
*
|
||||
* @return string 模型类名
|
||||
*/
|
||||
abstract public function getModel(): string;
|
||||
|
||||
/**
|
||||
* 构造函数,初始化模型实例
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new ($this->getModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* 子类定义:场景化查询配置
|
||||
* 返回一个关联数组,键为场景名称,值为查询配置数组或闭包
|
||||
*
|
||||
* @return array<string, array|\Closure> 查询配置数组或闭包
|
||||
*/
|
||||
public function queryScenes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据场景和请求参数,取到完整的 Builder 配置
|
||||
*
|
||||
* @param array $params 请求参数
|
||||
* @param string $scene 场景名称
|
||||
* @return array 查询配置数组
|
||||
*/
|
||||
protected function getQueryConfig(array $params, string $scene): array
|
||||
{
|
||||
$scenes = $this->queryScenes();
|
||||
$cfg = $scenes[$scene] ?? [];
|
||||
return is_callable($cfg) ? $cfg($params) : $cfg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造Builder
|
||||
*/
|
||||
protected function createQueryBuilder(array $params, string $scene): SimpleQueryBuilder
|
||||
{
|
||||
$config = $this->getQueryConfig($params, $scene);
|
||||
return (new SimpleQueryBuilder($this->model))->applyQueryConfig($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表
|
||||
*
|
||||
* @param array $params 请求参数
|
||||
* @param string $scene 场景名称,默认为 'default'
|
||||
* @return LengthAwarePaginator 分页结果
|
||||
*/
|
||||
public function list(array $params = [], string $scene = 'default'): LengthAwarePaginator
|
||||
{
|
||||
return $this->createQueryBuilder($params, $scene)->paginate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 不分页列表
|
||||
*
|
||||
* @param array $params 请求参数
|
||||
* @param string $scene 场景名称,默认为 'default'
|
||||
* @return Collection 查询结果集合
|
||||
*/
|
||||
public function all(array $params = [], string $scene = 'default'): Collection
|
||||
{
|
||||
return $this->createQueryBuilder($params, $scene)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义传入配置查询
|
||||
*
|
||||
* @param array $config 查询配置
|
||||
* @param bool $paginate 是否分页,默认为 true
|
||||
* @return LengthAwarePaginator|Collection 查询结果,分页或集合
|
||||
*/
|
||||
public function query(array $config, bool $paginate = true): LengthAwarePaginator|Collection
|
||||
{
|
||||
$builder = new SimpleQueryBuilder($this->model);
|
||||
$builder->applyQueryConfig($config);
|
||||
return $paginate ? $builder->paginate() : $builder->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键取一条(带关联)
|
||||
*/
|
||||
public function getById(int $id, array $params = [], string $scene = 'default'): ?Model
|
||||
{
|
||||
return $this->createQueryBuilder($params, $scene)
|
||||
->build()
|
||||
->whereKey($id)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按过滤条件取一条(带关联,不限主键)
|
||||
*/
|
||||
public function getOne(array $params = [], string $scene = 'default'): ?Model
|
||||
{
|
||||
return $this->createQueryBuilder($params, $scene)
|
||||
->build()
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找单个模型实例
|
||||
*
|
||||
* @param int $id 模型ID
|
||||
* @return Model|null 模型实例或null
|
||||
*/
|
||||
public function find(int $id): ?Model
|
||||
{
|
||||
return $this->model->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新模型实例
|
||||
*
|
||||
* @param array $data 创建数据
|
||||
* @return Model 新创建的模型实例
|
||||
*/
|
||||
public function create(array $data): Model
|
||||
{
|
||||
return $this->model->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新现有模型实例
|
||||
*
|
||||
* @param int $id 模型ID
|
||||
* @param array $data 更新数据
|
||||
* @return bool 更新是否成功
|
||||
*/
|
||||
public function update(int $id, array $data): bool
|
||||
{
|
||||
$record = $this->find($id);
|
||||
return $record ? $record->update($data) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型实例
|
||||
*
|
||||
* @param int $id 模型ID
|
||||
* @return bool 删除是否成功
|
||||
*/
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$record = $this->find($id);
|
||||
return $record ? $record->delete() : false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
namespace app\mzl\common\traits;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
trait DataTransformer
|
||||
{
|
||||
/**
|
||||
* 核心方法:对任意数据进行转换并返回数组
|
||||
*
|
||||
* @param mixed $rawData 原始数据,可为 Paginator / Model / Collection / array 等
|
||||
* @param callable|null $postProcessor 可选的后置处理回调,接受处理完的数组再返回新的数组
|
||||
* @param callable|null $fieldFormatter 可选的字段命名转换回调,如 snake_case
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function transformData(
|
||||
mixed $rawData,
|
||||
?callable $postProcessor = null,
|
||||
?callable $fieldFormatter = null
|
||||
): array {
|
||||
try {
|
||||
$fieldFormatter = $fieldFormatter ?: fn($field) => $field;
|
||||
|
||||
if ($rawData instanceof LengthAwarePaginator) {
|
||||
$formatted = $this->formatPaginated($rawData, $postProcessor, $fieldFormatter);
|
||||
} elseif ($rawData instanceof Model) {
|
||||
$formatted = $this->formatModel($rawData, $fieldFormatter);
|
||||
} elseif ($rawData instanceof Collection) {
|
||||
$formatted = $this->formatCollection($rawData, $fieldFormatter);
|
||||
} elseif (is_array($rawData)) {
|
||||
$formatted = $this->formatArray($rawData, $fieldFormatter);
|
||||
} else {
|
||||
$formatted = (array) $rawData;
|
||||
}
|
||||
|
||||
if (is_callable($postProcessor)) {
|
||||
$formatted = $postProcessor($formatted);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化分页数据:将每条记录也进行 transformData 递归处理
|
||||
*
|
||||
* @param LengthAwarePaginator $paginator
|
||||
* @param callable|null $postProcessor
|
||||
* @param callable $fieldFormatter
|
||||
* @return array
|
||||
*/
|
||||
protected function formatPaginated(
|
||||
LengthAwarePaginator $paginator,
|
||||
?callable $postProcessor,
|
||||
callable $fieldFormatter
|
||||
): array {
|
||||
$list = $paginator->getCollection()->map(function ($item) use ($postProcessor, $fieldFormatter) {
|
||||
return $this->transformData($item, $postProcessor, $fieldFormatter);
|
||||
})->toArray();
|
||||
|
||||
$meta = [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
'first_page_url' => $paginator->url(1),
|
||||
'last_page_url' => $paginator->url($paginator->lastPage()),
|
||||
'next_page_url' => $paginator->nextPageUrl(),
|
||||
'prev_page_url' => $paginator->previousPageUrl(),
|
||||
];
|
||||
|
||||
return [
|
||||
'list' => $list,
|
||||
'meta' => $meta,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 Eloquent 模型:递归嵌套关联
|
||||
*
|
||||
* @param Model $model
|
||||
* @param callable $fieldFormatter
|
||||
* @return array
|
||||
*/
|
||||
protected function formatModel(Model $model, callable $fieldFormatter): array
|
||||
{
|
||||
$formatted = [];
|
||||
|
||||
// 主表字段
|
||||
foreach ($model->getAttributes() as $key => $value) {
|
||||
$formatted[$fieldFormatter($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))
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化集合
|
||||
*
|
||||
* @param Collection $collection
|
||||
* @param callable $fieldFormatter
|
||||
* @return array
|
||||
*/
|
||||
protected function formatCollection(Collection $collection, callable $fieldFormatter): array
|
||||
{
|
||||
return $collection->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化普通数组
|
||||
*
|
||||
* @param array $array
|
||||
* @param callable $fieldFormatter
|
||||
* @return array
|
||||
*/
|
||||
protected function formatArray(array $array, callable $fieldFormatter): array
|
||||
{
|
||||
return array_map(fn($item) => $this->transformData($item, null, $fieldFormatter), $array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归裁剪数组,只保留指定字段
|
||||
*
|
||||
* @param array $data 当前要裁剪的数据(可能是单条关联,也可能是 list)
|
||||
* @param array $fieldsMap 白名单映射,key=>[allowed fields]
|
||||
* @param string $path 当前路径('' / 'items' / 'items.sub' …)
|
||||
* @return array
|
||||
*/
|
||||
protected function pruneFields(array $data, array $fieldsMap, string $path = ''): array
|
||||
{
|
||||
// —— 新增:如果这是一个纯列表(list),逐项递归 prune 并返回
|
||||
if ($this->isListArray($data)) {
|
||||
return array_map(
|
||||
fn($item) => is_array($item)
|
||||
? $this->pruneFields($item, $fieldsMap, $path)
|
||||
: $item,
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
// 如果当前层没有配置白名单,原样返回(或也可以 return [] 视业务)
|
||||
if (! isset($fieldsMap[$path])) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$allowed = $fieldsMap[$path];
|
||||
$out = [];
|
||||
|
||||
foreach ($allowed as $key) {
|
||||
if (! array_key_exists($key, $data)) {
|
||||
continue;
|
||||
}
|
||||
$val = $data[$key];
|
||||
|
||||
// 如果下一层还是个数组,继续递归
|
||||
if (is_array($val)) {
|
||||
$nextPath = ltrim($path . '.' . $key, '.');
|
||||
$out[$key] = $this->pruneFields($val, $fieldsMap, $nextPath);
|
||||
} else {
|
||||
$out[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 Paginator/Model/Collection/array 统一格式化,并按 fieldsMap 裁剪
|
||||
*
|
||||
* @param mixed $rawData
|
||||
* @param array|null $fieldsMap 顶层 '' + 各关联路径的白名单
|
||||
* @param callable|null $fieldFormatter 字段格式化回调
|
||||
* @param callable|null $postProcessor 单条记录后置处理回调
|
||||
* @return array
|
||||
*/
|
||||
protected function transformAndPrune(
|
||||
mixed $rawData,
|
||||
?array $fieldsMap = null,
|
||||
?callable $fieldFormatter = null,
|
||||
?callable $postProcessor = null
|
||||
): array {
|
||||
// 1) 统一转换成数组
|
||||
$data = $this->transformData($rawData, null, $fieldFormatter);
|
||||
|
||||
// 2) 如果没有白名单配置,直接返回原始数据
|
||||
if (empty($fieldsMap) || !isset($fieldsMap[''])) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// 3) 分页结构:['list'=>[...],'meta'=>[...]]
|
||||
if (isset($data['list'], $data['meta'])) {
|
||||
$data['list'] = array_map(
|
||||
fn($item) => $this->processItem($item, $fieldsMap, $postProcessor),
|
||||
$data['list']
|
||||
);
|
||||
return $data;
|
||||
}
|
||||
|
||||
// 4) 数字索引数组 => 当作“非分页列表”处理
|
||||
if ($this->isListArray($data)) {
|
||||
return array_map(
|
||||
fn($item) => $this->processItem($item, $fieldsMap, $postProcessor),
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
// 5) 单条记录
|
||||
return $this->processItem($data, $fieldsMap, $postProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪并做后置处理
|
||||
*/
|
||||
private function processItem(
|
||||
array $item,
|
||||
array $fieldsMap,
|
||||
?callable $postProcessor
|
||||
): array {
|
||||
$pruned = $this->pruneFields($item, $fieldsMap);
|
||||
return $postProcessor ? $postProcessor($pruned) : $pruned;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个数组是不是“索引从 0 开始的列表”
|
||||
*/
|
||||
private function isListArray(array $arr): bool
|
||||
{
|
||||
if (empty($arr)) {
|
||||
return true;
|
||||
}
|
||||
return array_keys($arr) === range(0, count($arr) - 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace app\mzl\common\traits;
|
||||
|
||||
use support\Response;
|
||||
|
||||
trait ResponseTrait
|
||||
{
|
||||
/**
|
||||
* 返回成功响应
|
||||
*
|
||||
* @param mixed $data 响应数据
|
||||
* @param string $message 响应消息,默认“请求成功”
|
||||
* @param int $code 内部状态码,默认 0 表示成功
|
||||
* @param int $httpStatus HTTP 状态码,默认 200
|
||||
* @param array $header 响应头,默认 JSON 格式
|
||||
* @return Response
|
||||
*/
|
||||
public function successResponse(
|
||||
$data = [],
|
||||
string $message = '请求成功',
|
||||
int $code = 0,
|
||||
int $httpStatus = 200,
|
||||
array $header = ['Content-Type' => 'application/json']
|
||||
): Response {
|
||||
return $this->result('success', $code, $httpStatus, $message, $data, $header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回错误响应
|
||||
*
|
||||
* @param string $message 响应错误消息,默认“请求失败”
|
||||
* @param int $code 内部状态码,默认 1 表示错误
|
||||
* @param int $httpStatus HTTP 状态码,默认 400
|
||||
* @param mixed $data 可选的错误数据
|
||||
* @param array $header 响应头,默认 JSON 格式
|
||||
* @return Response
|
||||
*/
|
||||
public function errorResponse(
|
||||
string $message = '请求失败',
|
||||
int $code = 1,
|
||||
int $httpStatus = 400,
|
||||
$data = [],
|
||||
array $header = ['Content-Type' => 'application/json']
|
||||
): Response {
|
||||
return $this->result('error', $code, $httpStatus, $message, $data, $header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一响应格式方法
|
||||
*
|
||||
* @param string $status 响应状态,例如 "success" 或 "error"
|
||||
* @param int $code 内部状态码
|
||||
* @param int $httpStatus HTTP 状态码
|
||||
* @param string $message 响应消息
|
||||
* @param mixed $data 响应数据
|
||||
* @param array $header 响应头
|
||||
* @return Response
|
||||
*/
|
||||
protected function result(
|
||||
string $status,
|
||||
int $code,
|
||||
int $httpStatus,
|
||||
string $message,
|
||||
$data,
|
||||
array $header
|
||||
): Response {
|
||||
$body = [
|
||||
'status' => $status,
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
];
|
||||
return response(json_encode($body, JSON_UNESCAPED_UNICODE), $httpStatus, $header);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace app\mzl\common\traits;
|
||||
|
||||
/**
|
||||
* 调用服务类的便捷工具
|
||||
*/
|
||||
trait ServiceCallTrait
|
||||
{
|
||||
/**
|
||||
* 实例缓存
|
||||
*/
|
||||
protected array $serviceCache = [];
|
||||
|
||||
/**
|
||||
* 调用指定 Service
|
||||
*
|
||||
* @param string $serviceClass 服务类名(必须继承 BaseService)
|
||||
* @return object
|
||||
*/
|
||||
protected function callService(string $serviceClass): object
|
||||
{
|
||||
if (!isset($this->serviceCache[$serviceClass])) {
|
||||
$this->serviceCache[$serviceClass] = new $serviceClass();
|
||||
}
|
||||
return $this->serviceCache[$serviceClass];
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷调用某服务下某个方法
|
||||
*
|
||||
* @param string $serviceClass
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
protected function callServiceMethod(string $serviceClass, string $method, array $args = []): mixed
|
||||
{
|
||||
$service = $this->callService($serviceClass);
|
||||
|
||||
if (!method_exists($service, $method)) {
|
||||
throw new \BadMethodCallException("Method {$method} does not exist in {$serviceClass}");
|
||||
}
|
||||
|
||||
return call_user_func_array([$service, $method], $args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace app\mzl\common\utils;
|
||||
|
||||
class SearchHelper
|
||||
{
|
||||
public static function parseFiltersAndRelations(array $params, array $mapping): array
|
||||
{
|
||||
$filters = [];
|
||||
$relations = [];
|
||||
$hasRelations = [];
|
||||
|
||||
// 解析过滤条件和关联字段
|
||||
foreach ($mapping as $paramKey => $definition) {
|
||||
$field = $definition['field'];
|
||||
$operator = $definition['operator'];
|
||||
$value = $params[$paramKey] ?? null;
|
||||
|
||||
if (self::isValidValue($value, $operator)) {
|
||||
$filters[] = compact('field', 'operator', 'value');
|
||||
if (str_contains($field, '.')) {
|
||||
$relations[] = self::extractRelationPath($field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 whereHas 关联字段(如 profile.age)
|
||||
foreach ($params as $key => $value) {
|
||||
if (str_contains($key, '.')) {
|
||||
$relation = explode('.', $key)[0]; // 获取关联名
|
||||
$hasRelations[$relation] = function ($query) use ($key, $value) {
|
||||
$query->where($key, '=', $value);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'filters' => $filters,
|
||||
'relations' => array_unique($relations),
|
||||
'hasRelations' => $hasRelations,
|
||||
];
|
||||
}
|
||||
|
||||
private static function isValidValue(mixed $value, string $operator): bool
|
||||
{
|
||||
$operator = strtolower($operator);
|
||||
|
||||
if (in_array($operator, ['in', 'not_in', 'between'])) {
|
||||
return is_array($value) && !empty($value);
|
||||
}
|
||||
|
||||
if (in_array($operator, ['null', 'not_null'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $value !== '' && $value !== null;
|
||||
}
|
||||
|
||||
private static function extractRelationPath(string $field): string
|
||||
{
|
||||
$parts = explode('.', $field);
|
||||
array_pop($parts);
|
||||
return implode('.', $parts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
<?php
|
||||
|
||||
namespace app\mzl\common\utils;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* 通用查询构建器 SimpleQueryBuilder
|
||||
*/
|
||||
class SimpleQueryBuilder
|
||||
{
|
||||
protected ?Model $model = null;
|
||||
protected ?Builder $query = null;
|
||||
|
||||
protected array $filters = [];
|
||||
protected array $sorts = [];
|
||||
protected array $fields = [];
|
||||
protected array $relations = [];
|
||||
protected array $hasRelations = [];
|
||||
protected array $pagination = [
|
||||
'page' => 1,
|
||||
'per_page' => 10,
|
||||
];
|
||||
|
||||
/** @var string[] 要执行 withCount 的关联 */
|
||||
protected array $counts = [];
|
||||
|
||||
public array $allowedFields = [];
|
||||
|
||||
protected ?\Closure $customQueryCallback = null;
|
||||
|
||||
public function __construct(?Model $model = null)
|
||||
{
|
||||
if ($model) {
|
||||
$this->setModel($model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型
|
||||
*/
|
||||
public function setModel(Model $model): self
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->query = $model->newQuery();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量应用查询配置
|
||||
*
|
||||
* @param array $config
|
||||
* @return $this
|
||||
*/
|
||||
public function applyQueryConfig(array $config): self
|
||||
{
|
||||
foreach ($config['filters'] ?? [] as $filter) {
|
||||
$this->addFilter($filter['field'], $filter['operator'], $filter['value']);
|
||||
}
|
||||
|
||||
foreach ($config['sorts'] ?? [] as $sort) {
|
||||
$this->addSort($sort['field'], $sort['direction'] ?? 'asc');
|
||||
}
|
||||
|
||||
if (!empty($config['fields'])) {
|
||||
$this->selectFields($config['fields']);
|
||||
}
|
||||
|
||||
foreach ($config['relations'] ?? [] as $relation => $fields) {
|
||||
$this->withRelation($relation, $fields);
|
||||
}
|
||||
|
||||
foreach ($config['hasRelations'] ?? [] as $relation => $callback) {
|
||||
if ($callback instanceof \Closure) {
|
||||
$this->addHasRelation($relation, $callback);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:处理 counts
|
||||
foreach ($config['counts'] ?? [] as $relation) {
|
||||
$this->addCount($relation);
|
||||
}
|
||||
|
||||
if (!empty($config['pagination'])) {
|
||||
$this->setPagination(
|
||||
$config['pagination']['page'] ?? 1,
|
||||
$config['pagination']['per_page'] ?? 15
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($config['custom']) && $config['custom'] instanceof \Closure) {
|
||||
$this->withCustomQuery($config['custom']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单个过滤条件
|
||||
*/
|
||||
public function addFilter(string $field, string $operator, mixed $value): self
|
||||
{
|
||||
$this->filters[] = compact('field', 'operator', 'value');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加排序字段
|
||||
*/
|
||||
public function addSort(string $field, string $direction = 'asc'): self
|
||||
{
|
||||
$this->sorts[$field] = strtolower($direction) === 'desc' ? 'desc' : 'asc';
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置查询字段(select),可配合 allowedFields 校验
|
||||
*/
|
||||
public function selectFields(array $fields): self
|
||||
{
|
||||
$this->fields = array_unique($fields);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载关联(支持指定字段)
|
||||
*/
|
||||
public function withRelation2(string $relation, array $fields = []): self
|
||||
{
|
||||
$this->relations[$relation] = !empty($fields) ? $fields : '*';
|
||||
return $this;
|
||||
}
|
||||
/**
|
||||
* 加载关联(支持指定字段 或 自定义闭包)
|
||||
*
|
||||
* @param string $relation 关联名,支持点式多级
|
||||
* @param array|\Closure $fields 字段数组 或 查询闭包
|
||||
*/
|
||||
public function withRelation(string $relation, $fields = []): self
|
||||
{
|
||||
if ($fields instanceof \Closure) {
|
||||
// 自定义关联查询闭包
|
||||
$this->relations[$relation] = $fields;
|
||||
} else {
|
||||
// 字段选择,空数组表示全部
|
||||
$this->relations[$relation] = !empty($fields) ? $fields : '*';
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加 whereHas 条件
|
||||
*/
|
||||
public function addHasRelation(string $relation, \Closure $callback): self
|
||||
{
|
||||
$this->hasRelations[] = compact('relation', 'callback');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分页
|
||||
*/
|
||||
public function setPagination(int $page, int $perPage): self
|
||||
{
|
||||
$this->pagination = [
|
||||
'page' => max(1, $page),
|
||||
'per_page' => min(max(1, $perPage), 100),
|
||||
];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入复杂查询闭包(适合业务层扩展)
|
||||
*/
|
||||
public function withCustomQuery(\Closure $callback): self
|
||||
{
|
||||
$this->customQueryCallback = $callback;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加关联数量统计 (withCount)
|
||||
*
|
||||
* @param string $relation 关联名,支持点式多层,如 "orders.items"
|
||||
* @return $this
|
||||
*/
|
||||
public function addCount(string $relation): self
|
||||
{
|
||||
$this->counts[] = $relation;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整查询(不执行)
|
||||
*/
|
||||
public function build(): Builder
|
||||
{
|
||||
if (!$this->query && $this->model) {
|
||||
$this->query = $this->model->newQuery();
|
||||
}
|
||||
|
||||
$this->applyFilters()
|
||||
->applySorts()
|
||||
->applyFields()
|
||||
->applyRelations()
|
||||
->applyWhereHas();
|
||||
|
||||
// ========== 新增:关联计数 ==========
|
||||
if (!empty($this->counts)) {
|
||||
// Laravel 支持 withCount(['rel1','rel2'])
|
||||
$this->query->withCount($this->counts);
|
||||
}
|
||||
|
||||
if ($this->customQueryCallback) {
|
||||
call_user_func($this->customQueryCallback, $this->query);
|
||||
}
|
||||
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合(不分页)
|
||||
*/
|
||||
public function get(): Collection
|
||||
{
|
||||
return $this->build()->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分页结果
|
||||
*/
|
||||
public function paginate(): LengthAwarePaginator
|
||||
{
|
||||
return $this->build()->paginate(
|
||||
$this->pagination['per_page'],
|
||||
['*'],
|
||||
'page',
|
||||
$this->pagination['page']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用过滤
|
||||
*/
|
||||
protected function applyFilters(): self
|
||||
{
|
||||
foreach ($this->filters as $filter) {
|
||||
$field = $filter['field'];
|
||||
$operator = $filter['operator'];
|
||||
$value = $filter['value'];
|
||||
|
||||
if (str_contains($field, '.')) {
|
||||
$segments = explode('.', $field);
|
||||
$relationPath = implode('.', array_slice($segments, 0, -1));
|
||||
$lastField = end($segments);
|
||||
|
||||
$this->query->whereHas($relationPath, function (Builder $q) use ($lastField, $operator, $value) {
|
||||
$this->applyCondition($q, $lastField, $operator, $value);
|
||||
});
|
||||
} else {
|
||||
$this->applyCondition($this->query, $field, $operator, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用排序
|
||||
*/
|
||||
protected function applySorts(): self
|
||||
{
|
||||
foreach ($this->sorts as $field => $direction) {
|
||||
$this->query->orderBy($field, $direction);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用字段选择
|
||||
*/
|
||||
// protected function applyFields2(): self
|
||||
// {
|
||||
// if (!empty($this->fields)) {
|
||||
// // 排除关联字段(如 class.teacher.realname)
|
||||
// $fields = array_filter($this->fields, fn($field) => !str_contains($field, '.'));
|
||||
|
||||
// // 白名单过滤
|
||||
// $fields = !empty($this->allowedFields)
|
||||
// ? array_intersect($fields, $this->allowedFields)
|
||||
// : $fields;
|
||||
|
||||
// if (empty($fields)) {
|
||||
// throw new \InvalidArgumentException('字段选择无效');
|
||||
// }
|
||||
|
||||
// $this->query->select(array_unique($fields));
|
||||
// }
|
||||
|
||||
// return $this;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 应用字段选择,支持普通列和带 alias 的 Raw 表达式
|
||||
*/
|
||||
protected function applyFields(): self
|
||||
{
|
||||
if (empty($this->fields)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// 只保留主表字段(排除带点的关联字段)
|
||||
$cols = array_filter($this->fields, fn($f) => ! str_contains($f, '.'));
|
||||
|
||||
// 如果设置了 allowedFields,做一次白名单交集
|
||||
if (! empty($this->allowedFields)) {
|
||||
$cols = array_intersect($cols, $this->allowedFields);
|
||||
}
|
||||
|
||||
if (empty($cols)) {
|
||||
throw new \InvalidArgumentException('字段选择无效');
|
||||
}
|
||||
|
||||
$plain = [];
|
||||
$raws = [];
|
||||
|
||||
foreach ($cols as $col) {
|
||||
// 简单判断带 “ as ” 的就归到 Raw
|
||||
if (preg_match('/\s+as\s+/i', $col)) {
|
||||
$raws[] = $col;
|
||||
} else {
|
||||
$plain[] = $col;
|
||||
}
|
||||
}
|
||||
|
||||
// 先 select 普通列
|
||||
if ($plain) {
|
||||
$this->query->select($plain);
|
||||
}
|
||||
// 再 selectRaw alias
|
||||
if ($raws) {
|
||||
// 注意:selectRaw 接受一整个字符串
|
||||
$this->query->selectRaw(implode(', ', $raws));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用关联加载,支持 dot-notation 多级关联
|
||||
*/
|
||||
// protected function applyRelations2(): self
|
||||
// {
|
||||
// if (empty($this->relations)) {
|
||||
// return $this;
|
||||
// }
|
||||
|
||||
// $with = [];
|
||||
// foreach ($this->relations as $path => $fields) {
|
||||
// $with[$path] = function ($q) use ($fields) {
|
||||
// if ($fields === '*' || empty($fields)) {
|
||||
// return;
|
||||
// }
|
||||
// // 保证 id 在内
|
||||
// $model = $q->getModel();
|
||||
// $key = $model->getKeyName();
|
||||
// $q->select(array_merge([$key], $fields));
|
||||
// };
|
||||
// }
|
||||
|
||||
// $this->query->with($with);
|
||||
|
||||
// return $this;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 应用多级关联加载,支持给关联字段加 alias
|
||||
*/
|
||||
protected function applyRelations2(): self
|
||||
{
|
||||
if (empty($this->relations)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$with = [];
|
||||
foreach ($this->relations as $path => $fields) {
|
||||
$with[$path] = function ($q) use ($fields) {
|
||||
if ($fields === '*' || empty($fields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 模型主键一定要带上
|
||||
$model = $q->getModel();
|
||||
$key = $model->getKeyName();
|
||||
|
||||
$plain = [];
|
||||
$raws = [];
|
||||
|
||||
foreach ($fields as $col) {
|
||||
if (preg_match('/\s+as\s+/i', $col)) {
|
||||
$raws[] = $col;
|
||||
} else {
|
||||
$plain[] = $col;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保主键至少出现一次
|
||||
if (! in_array($key, $plain, true)) {
|
||||
array_unshift($plain, $key);
|
||||
}
|
||||
|
||||
// select 普通列
|
||||
if ($plain) {
|
||||
$q->select($plain);
|
||||
}
|
||||
// selectRaw 带 alias 的列
|
||||
if ($raws) {
|
||||
$q->selectRaw(implode(', ', $raws));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$this->query->with($with);
|
||||
return $this;
|
||||
}
|
||||
/**
|
||||
* 应用多级关联加载,支持字段数组或直接传入闭包
|
||||
*/
|
||||
protected function applyRelations(): self
|
||||
{
|
||||
if (empty($this->relations)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$with = [];
|
||||
foreach ($this->relations as $path => $handler) {
|
||||
if ($handler instanceof \Closure) {
|
||||
// 直接使用用户传入的闭包
|
||||
$with[$path] = $handler;
|
||||
} else {
|
||||
// 按字段数组构造查询
|
||||
$fields = $handler;
|
||||
$with[$path] = function ($q) use ($fields) {
|
||||
if ($fields === '*' || empty($fields)) {
|
||||
return;
|
||||
}
|
||||
// 确保主键被选中
|
||||
$model = $q->getModel();
|
||||
$key = $model->getKeyName();
|
||||
|
||||
$plain = [];
|
||||
$raws = [];
|
||||
foreach ($fields as $col) {
|
||||
if (preg_match('/\s+as\s+/i', $col)) {
|
||||
$raws[] = $col;
|
||||
} else {
|
||||
$plain[] = $col;
|
||||
}
|
||||
}
|
||||
if (! in_array($key, $plain, true)) {
|
||||
array_unshift($plain, $key);
|
||||
}
|
||||
|
||||
if ($plain) {
|
||||
$q->select($plain);
|
||||
}
|
||||
if ($raws) {
|
||||
$q->selectRaw(implode(', ', $raws));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$this->query->with($with);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 应用 whereHas 关联过滤
|
||||
*/
|
||||
protected function applyWhereHas(): self
|
||||
{
|
||||
foreach ($this->hasRelations as $relationData) {
|
||||
$this->query->whereHas($relationData['relation'], $relationData['callback']);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用单个过滤条件
|
||||
*/
|
||||
protected function applyCondition(Builder $query, string $field, string $operator, mixed $value): void
|
||||
{
|
||||
switch (strtolower($operator)) {
|
||||
case 'between':
|
||||
$query->whereBetween($field, (array)$value);
|
||||
break;
|
||||
case 'like':
|
||||
$query->where($field, 'LIKE', "%{$value}%");
|
||||
break;
|
||||
case 'in':
|
||||
$query->whereIn($field, (array)$value);
|
||||
break;
|
||||
case 'not_in':
|
||||
$query->whereNotIn($field, (array)$value);
|
||||
break;
|
||||
case 'null':
|
||||
$query->whereNull($field);
|
||||
break;
|
||||
case 'not_null':
|
||||
$query->whereNotNull($field);
|
||||
break;
|
||||
default:
|
||||
$query->where($field, $operator, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 queryConfig 构造 fieldsMap
|
||||
* - 兼容 "col as alias"
|
||||
* - 自动把 counts 生成的 xxx_count 加入根字段
|
||||
*
|
||||
* @param array $config 完整的 applyQueryConfig 输入
|
||||
* @return array [''=>[主表字段…], 'relation'=>[字段…], …]
|
||||
*/
|
||||
public static function buildFieldsMap(array $config): array
|
||||
{
|
||||
$relations = $config['relations'] ?? [];
|
||||
$counts = $config['counts'] ?? [];
|
||||
$rawFields = $config['fields'] ?? [];
|
||||
|
||||
// 1) 根层字段:拆 alias + 自动加 count + 加顶级关联名
|
||||
$root = [];
|
||||
foreach ($rawFields as $f) {
|
||||
if (preg_match('/\s+as\s+/i', $f)) {
|
||||
[, $alias] = preg_split('/\s+as\s+/i', $f);
|
||||
$root[] = $alias;
|
||||
} else {
|
||||
$root[] = $f;
|
||||
}
|
||||
}
|
||||
// 把 withCount 生成的字段也加进来
|
||||
foreach ($counts as $rel) {
|
||||
$root[] = "{$rel}_count";
|
||||
}
|
||||
// 顶级关联名
|
||||
foreach (array_keys($relations) as $path) {
|
||||
$root[] = explode('.', $path, 2)[0];
|
||||
}
|
||||
$fieldsMap = ['' => array_values(array_unique($root))];
|
||||
|
||||
// 2) 每个关联:拆 alias + 加直接子关联
|
||||
foreach ($relations as $path => $rawCols) {
|
||||
$arr = [];
|
||||
$segments = explode('.', $path);
|
||||
$depth = count($segments);
|
||||
// 拆 alias
|
||||
foreach ($rawCols as $col) {
|
||||
if (preg_match('/\s+as\s+/i', $col)) {
|
||||
[, $alias] = preg_split('/\s+as\s+/i', $col);
|
||||
$arr[] = $alias;
|
||||
} else {
|
||||
$arr[] = $col;
|
||||
}
|
||||
}
|
||||
// 子关联名
|
||||
if ($depth > 1) {
|
||||
for ($i = 1; $i < $depth; $i++) {
|
||||
$parent = implode('.', array_slice($segments, 0, $i));
|
||||
$child = $segments[$i];
|
||||
$fieldsMap[$parent][] = $child;
|
||||
}
|
||||
}
|
||||
$fieldsMap[$path] = array_values(array_unique($arr));
|
||||
}
|
||||
|
||||
return $fieldsMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace app\mzl\common\utils;
|
||||
|
||||
|
||||
class SnGenerator
|
||||
{
|
||||
/**
|
||||
* 生成唯一的用户编号
|
||||
*
|
||||
* @param int $length 编号长度(6 或 8)
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function generateUniqueUserId($length = 6)
|
||||
{
|
||||
if (!in_array($length, [6, 8])) {
|
||||
throw new \Exception("长度必须是 6 或 8 位");
|
||||
}
|
||||
|
||||
$maxAttempts = 100; // 最大尝试次数,避免无限循环
|
||||
$attempts = 0;
|
||||
|
||||
do {
|
||||
$userId = $this->generateRandomNumber($length);
|
||||
$attempts++;
|
||||
if ($attempts > $maxAttempts) {
|
||||
throw new \Exception("无法生成唯一的用户编号,请重试");
|
||||
}
|
||||
} while ($this->isUserIdExists($userId));
|
||||
|
||||
return $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机数字
|
||||
*
|
||||
* @param int $length 数字长度
|
||||
* @return string
|
||||
*/
|
||||
private function generateRandomNumber($length)
|
||||
{
|
||||
$min = pow(10, $length - 1); // 最小值,如 6 位是 100000
|
||||
$max = pow(10, $length) - 1; // 最大值,如 6 位是 999999
|
||||
return (string) mt_rand($min, $max);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户编号是否已存在
|
||||
*
|
||||
* @param string $userId
|
||||
* @return bool
|
||||
*/
|
||||
private function isUserIdExists($userId)
|
||||
{
|
||||
$userIds = \support\Db::table('hs_users')->pluck('user_sn')->toArray();
|
||||
return in_array($userId, $userIds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace app\mzl\common\utils;
|
||||
|
||||
use support\Db;
|
||||
|
||||
class TableInspector
|
||||
{
|
||||
/**
|
||||
* 获取某个表的所有字段信息
|
||||
*/
|
||||
public static function getColumns(string $table, ?string $database = null): array
|
||||
{
|
||||
$database = $database ?: Db::connection()->getDatabaseName();
|
||||
|
||||
$columns = Db::select("
|
||||
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE,
|
||||
COLUMN_DEFAULT, COLUMN_COMMENT, EXTRA
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
||||
ORDER BY ORDINAL_POSITION
|
||||
", [$database, $table]);
|
||||
|
||||
return array_map(function ($col) {
|
||||
return [
|
||||
'name' => $col->COLUMN_NAME,
|
||||
'type' => $col->DATA_TYPE,
|
||||
'column_type' => $col->COLUMN_TYPE,
|
||||
'nullable' => $col->IS_NULLABLE === 'YES',
|
||||
'default' => $col->COLUMN_DEFAULT,
|
||||
'comment' => $col->COLUMN_COMMENT,
|
||||
'extra' => $col->EXTRA,
|
||||
];
|
||||
}, $columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段名列表
|
||||
*/
|
||||
public static function getColumnNames(string $table, ?string $database = null): array
|
||||
{
|
||||
return array_column(self::getColumns($table, $database), 'name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段注释 map:字段名 => 注释
|
||||
*/
|
||||
public static function getColumnComments(string $table, ?string $database = null): array
|
||||
{
|
||||
$columns = self::getColumns($table, $database);
|
||||
$map = [];
|
||||
foreach ($columns as $col) {
|
||||
$map[$col['name']] = $col['comment'];
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user