Initial commit

This commit is contained in:
18338781771
2025-05-14 13:19:23 +08:00
commit 1ab607f65a
313 changed files with 27595 additions and 0 deletions
@@ -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('删除失败');
}
}
+134
View File
@@ -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;
}
}
+198
View File
@@ -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;
}
}
+257
View File
@@ -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);
}
}
+76
View File
@@ -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);
}
}
+65
View File
@@ -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);
}
}
+584
View File
@@ -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;
}
}
+59
View File
@@ -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);
}
}
+57
View File
@@ -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;
}
}