diff --git a/app/Http/Controllers/Api/Nasa/V1/ArticleBaseController.php b/app/Http/Controllers/Api/Nasa/V1/ArticleBaseController.php new file mode 100755 index 0000000..1e83e77 --- /dev/null +++ b/app/Http/Controllers/Api/Nasa/V1/ArticleBaseController.php @@ -0,0 +1,81 @@ + 'id', + 'sCode' => 'sCode', + 'sAuthorCode'=> 'sAuthorCode', + 'aOpt' => 'aOpt', + 'aSchemaPro'=> 'aSchemaPro', + 'created_at'=> 'created_at', + 'updated_at'=> 'updated_at', + ]; + + protected array $aValueStyle = [ // 特定文字渲染样式 +// '关闭' => "关闭", +// '开启' => "开启" + ]; + + protected array $aFieldTypes = [ +// 'schema' => 'text', +// 'schema_pro' => 'text', +// 'cmd' => 'text', +// 'cmd_log' => 'text', +// 'opt' => 'text' + 'aOpt' => 'json', + 'aSchemaPro' => 'json', +// 'aSchema' => 'array', + ]; + + protected array $aOrderBy = [ + [ + "sField" => "id", + "sSort" => "desc", + ] + ]; + + protected array $aJsonFields = [ // json字段 +// 'schema' + ]; + + protected array $aDateFields = [ // date字段 +// 'dtUpdatedAt' + ]; + + protected array $aLenthFields = [ // 限制长度的字段 +// "xx" => 100, + ]; + + protected array $aSelectFields = [ // 作为默认select的字段 +// 'status', + ]; + + protected array $aValueFields = [ // 值映射 +// 'status' => [ +// '1' => '开启', +// '0' => '关闭', +// ] + ]; + + protected array $aCcExtFields = [ +// 'dtCreatedAt' => '创建时间' + ]; + +// protected array $aLimit = [3, 15, 40, 100]; // list长度,第一个为默认 + +} diff --git a/app/Http/Controllers/Api/Nasa/V1/Base/ListController.php b/app/Http/Controllers/Api/Nasa/V1/Base/ListController.php new file mode 100755 index 0000000..24325ad --- /dev/null +++ b/app/Http/Controllers/Api/Nasa/V1/Base/ListController.php @@ -0,0 +1,500 @@ + [], + 'cc' => [], + 'ob' => [], + ]; + + protected $oModel; + protected array $aValueFields = []; + protected string $sPageName = ''; +// protected string $sModelName = ''; + protected string $sModelPath = ''; + protected array $aFilterFields = []; // 允许where的字段 +// protected array $aArticleFields = []; // 文章字段 + protected array $aFieldTypes = []; + protected array $aLenthFields = []; // 限制长度的字段 + protected array $aSelectFields = []; // 默认作为select的字段 + protected array $aListFields = []; // list展示的字段和名字映射 + protected array $aDateFields = []; // date字段 + protected array $aJsonFields = []; // json字段 +// protected array $aValueStyle = []; // 特定文字渲染样式 + protected int $iLimitMax = 100; // 默认limit限制 + protected int $iLimit = 20; // 默认显示行数 + protected array $aCcExtFields = []; // 自定义条件字段追加在listFields后 + protected array $aCcFields = []; + protected int $iOrderByMax = 3; + protected array $aOrderBy = [ + [ + "sField" => "id", + "sSort" => "desc" + ] + ]; + protected array $aValueStyle = [ // 特定文字渲染样式 + '关闭' => "关闭", + '开启' => "开启" + ]; + + public function __construct() + { + if ($this->sModelPath) { + if (! class_exists($this->sModelPath)) { + throw new InvalidArgumentException("【系统架构错误】: 找不到领域模型 {$this->sModelPath}"); + } + + $this->oModel = new $this->sModelPath(); + $this->aCcFields = array_merge($this->aListFields, $this->aCcExtFields); + $this->aCcFields = $this->getCcFields(); + } + } + + private function getCcFields() + { + $a = []; + foreach ($this->aCcFields as $k => $v) { + if (is_array($v)) { + foreach ($v as $kk => $vv) { + $a[$kk] = $vv; + } + } else { + $a[$k] = $v; + } + } + + return $a; + } + + private function mapOperator($sOperator) + { + $sSqlOperator = match ($sOperator) { + 'like' => 'like', + 'eq' => '=', + 'ne' => '!=', + 'gt' => '>', + 'gte' => '>=', + 'lt' => '<', + 'lte' => '<=', + default => '', + }; + + return $sSqlOperator; + } + + private function getOrderBySchema() + { + $iOrderByCount = count($this->aOrderBy); + $iDiff = $this->iOrderByMax - $iOrderByCount; + $a = $this->aOrderBy; + for ($i=0; $i<$iDiff; $i++) { + $a[] = []; + } + return $a; + } + + private function getOrderBy() + { + return $this->aOrderBy; + } + + public function getListFields() + { + $a = []; + + foreach ($this->aListFields as $k => $v) { + if ($v) { + $a[$k] = $v; + } else { + $a[$k] = $k; + } + } + + return $a; + } + + private function getCcLast() + { + $aCcFieldsTmp = $this->aCcFields; + + foreach ($this->aFilterParams['cc'] as $ACC) { + unset($aCcFieldsTmp[$ACC['sField']]); + } + + $s = head(array_keys($aCcFieldsTmp)); + + return $s; + } + + public function add(Request $oRequest): \Illuminate\Http\JsonResponse + { + $sTableName = $this->oModel->getTable(); + $aRawFields = Schema::getColumnListing($sTableName); + + $sClassName = get_class($this->oModel); + + $aKickFields = []; + + if (defined($sClassName . '::UPDATED_AT')) { + $aKickFields[] = constant($sClassName . '::UPDATED_AT'); + } + + if (defined($sClassName . '::CREATED_AT')) { + $aKickFields[] = constant($sClassName . '::CREATED_AT'); + } + + $aKickFields[] = "id"; + + $aRawFieldsClear = []; + foreach ($aRawFields as $aRawField) { + if (in_array($aRawField, $aKickFields)) { + continue; + } + $aRawFieldsClear[] = $aRawField; + } + + return response()->json([ + 'iCode' => 200, + 'aData' => [ + 'cList' => $aRawFieldsClear, + 'sPageName' => $this->sPageName + ] + ], 200); + } + + public function clone(Request $oRequest) + { + $iId = request('id'); + $oModelResult = $this->oModel->findOrFail($iId); + $cList = $oModelResult->getRawOriginal(); + + $sTableName = $this->oModel->getTable(); + $aStatus = DB::select("SHOW TABLE STATUS LIKE '{$sTableName}'"); + $iNextId = $aStatus[0]->Auto_increment ?? 1; + + $cList['id'] = $iNextId; + + return response()->json([ + 'iCode' => 200, + 'aData' => [ + 'cList' => $cList, + 'aMap' => $this->getMapAll(), + 'aListField' => $this->getListFields(), + 'sPageName' => $this->sPageName + ] + ], 200); + } + +// public function cloneSave(Request $oRequest) +// { +// $iId = request('id'); +// +// $aParams = $oRequest->all(); +// +// foreach ($aParams as $k => $v) { +// $this->oModal->{$k} = $v; +// } +// $this->oModel->create(); +// +// return response()->json([ +// 'iCode' => 200, +// 'aData' => [], +// 'sMsg' => "保存成功:id ".$this->oModel->id +// ], 200); +// } + + public function detail(Request $oRequest) + { + $iId = request('id'); + $oModelResult = $this->oModel->findOrFail($iId); + $cList = $oModelResult->getRawOriginal(); + + return response()->json([ + 'iCode' => 200, + 'aData' => [ + 'cList' => $cList, + 'aMap' => $this->getMapAll(), + 'aListField' => $this->getListFields(), + 'sPageName' => $this->sPageName + ] + ], 200); + } + + public function hit(Request $oRequest) + { + $iId = request('id'); + $sField = request('field'); + + $sHit = $this->oModel->find($iId)?->{$sField}; + if (is_array($sHit)) { + $sHit = json_encode($sHit, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + + return response()->json([ + 'iCode' => 200, + 'aData' => [ + 'sHit' => $sHit + ] + ], 200); + + } + + public function delete(Request $oRequest) + { + $iId = request('id'); + + $this->oModel->findOrFail($iId)->delete(); + + return response()->json([ + 'iCode' => 200, + 'aData' => [], + 'sMsg' => '删除成功' + ], 200); + } + + public function addSave(Request $oRequest) + { + $aParams = $oRequest->all(); + + foreach ($aParams as $k => $v) { + $this->oModel->{$k} = $v; + } + + if (!$this->oModel->save()) { + return response()->json(['code' => 500, 'msg' => '保存失败?'], 500); + } + + return response()->json([ + 'iCode' => 200, + 'aData' => [], + 'sMsg' => '保存成功' + ], 200); + } + + public function detailSave(Request $oRequest) + { + $iId = request('id'); + + $oThisModel = $this->oModel->find($iId); + + $aParams = $oRequest->all(); + + unset($aParams['id'], $aParams['_method']); + + foreach ($aParams as $k => &$v) { + if (empty($v) && empty($oThisModel->{$k})) { + continue; + } + if (empty($v) && $v !== 0 && $v !== '0') { + $v = ''; + } + if ($oThisModel->{$k} != $v) { + $oThisModel->{$k} = $v; + } + } + + $oThisModel->save(); + + $aChangeField = $oThisModel->getChanges(); + + /// 排除update_at + $sClassName = get_class($this->oModel); + if ( + defined($sClassName . '::UPDATED_AT') + && ($sUpdateColumn = constant($sClassName . '::UPDATED_AT')) + && !isset($aParams[$sUpdateColumn]) + && isset($aChangeField[$sUpdateColumn]) + ) { + unset($aChangeField[$sUpdateColumn]); + } + + /// 构造msg + $sMsg = ""; + $sJiantou = " -> "; + foreach ($aChangeField as $k => $v) { + + if (mb_strlen($v, 'UTF-8') > 60 ) { + $sJiantou = ''; +// $v = mb_substr($v, 0, 60, 'UTF-8'); + $v = ''; + } + + if (!isset($aParams[$k])) { + $sMsg .= "⚠️ ".$k . $sJiantou . $v . "

"; + } else { + $sMsg .= "✅ ".$k . $sJiantou . $v . "

"; + unset($aParams[$k]); + } + } + foreach ($aParams as $k => $v) { + + if (mb_strlen($v, 'UTF-8') > 60 ) { + $sJiantou = ''; +// $v = mb_substr($v, 0, 60, 'UTF-8'); + $v = ''; + } + + $sMsg .= "❌ ".$k . $sJiantou . $v . "

"; + } + $sMsg = rtrim($sMsg, "

"); + + return response()->json([ + 'iCode' => 200, + 'aData' => $aChangeField, + 'sMsg' => $sMsg + ], 200); + } + + public function getMapAll() + { + $a = []; + + if (method_exists($this->oModel, 'getMapAll')) { + $a = $this->oModel->getMapAll(); + } + + $b = array_merge($a, $this->aValueFields); + + return $b; + } + + public function index(Request $oRequest) + { + if (! $this->oModel) { + return response()->json(['code' => 500, 'msg' => '未绑定核心领域模型'], 500); + } + + $oQuery = $this->oModel->newQuery(); + + $aParams = $oRequest->all(); + + foreach ($aParams as $k => $v) { + + if (!empty($this->aFilterFields)) { + if (!in_array($k, $this->aFilterFields)) { + continue; + } + } + + // w__name__eq=1 + $aK = explode("__", $k); + $sType = $aK[0] ?? ''; + $sField = $aK[1] ?? ''; + $sOperator = $aK[2] ?? $this->sOperatorDefault; + $sSqlOperator = $this->mapOperator($sOperator); + + if (!isset($this->aFilterParams[$sType])) { + continue; + } + + if (!$sType || !$sField) { + continue; + } + + $this->aFilterParams[$k] = $v; + + if ($sType === 'ob') { + $this->aFilterParams[$sType][] = [ + 'sField' => $sField, + 'sSort' => $v ?? $this->sSortDefault + ]; + continue; + } + + if ($sType === 'c') { // 预制条件,字典式 + $this->aFilterParams[$sType][$sField] = [ + 'sOperator' => $sOperator, + 'sValue' => $v + ]; + } + + if ($sType === 'cc') { // 自定义条件,集合式 + $this->aFilterParams[$sType][] = [ + 'sField' => $sField, + 'sOperator' => $sOperator, + 'sValue' => $v + ]; + } + + if (blank($v)) continue; // 空v可以back但不加入sql查询 + + $sSqlValue = $v; + if ($sSqlOperator === 'like') { + $sSqlValue = "%{$v}%"; + } + + if (in_array($sField, $this->aDateFields)) { + $oQuery->whereDate($sField, $sSqlOperator, $sSqlValue); + } else { + $oQuery->where($sField, $sSqlOperator, $sSqlValue); + } + + } + + $iLimit = (int) $oRequest->input('limit', $this->iLimit); + $iLimit = max(1, $iLimit); + $iLimit = min($this->iLimitMax, $iLimit); + + if (!empty($this->aFilterParams['ob'])) { + $this->aOrderBy = $this->aFilterParams['ob']; + } + + foreach ($this->aOrderBy as $aOrderBy) { + $oQuery->orderBy($aOrderBy['sField'], $aOrderBy['sSort']); + } + + $oPaginatedData = $oQuery->paginate($iLimit); // 倾向方案2 + +// $oPaginatedData->getCollection()->transform(function ($oArticle) { +// // 强制将模型属性还原为未经 Cast 处理的原始字典 +// $oArticle->setRawAttributes($oArticle->getRawOriginal()); +// +// return $oArticle; +// }); +// tt($oPaginatedData); +// $oPaginatedData = $oQuery->paginate($iLimit, array_keys($this->aListFields)); + + return response()->json([ + 'iCode' => 200, + 'aData' => [ + 'cList' => $oPaginatedData->items(), + 'aListField' => $this->getListFields(), + 'aFieldTypes' => $this->aFieldTypes, + 'aMap' => $this->getMapAll(), + 'aValueStyle' => $this->aValueStyle, + 'aSelectField' => $this->aSelectFields, + 'aFilterParam' => $this->aFilterParams, + 'aCcField' => $this->aCcFields, + 'sCcLast' => $this->getCcLast(), + 'sPageName' => $this->sPageName, + 'aPager' => [ + 'aOrderBySchema'=> $this->getOrderBySchema(), + 'aOrderBy' => $this->getOrderBy(), + 'iTotal' => $oPaginatedData->total(), + 'iLimit' => min($this->iLimitMax, $iLimit), + 'iCurrentPage' => $oPaginatedData->currentPage(), + 'iLastPage' => $oPaginatedData->lastPage(), + 'iPerPage' => $oPaginatedData->perPage(), + 'iPrevPage' => max(1, $oPaginatedData->currentPage() - 1), + 'iNextPage' => min($oPaginatedData->lastPage(), $oPaginatedData->currentPage() + 1) + ] + ] + ], 200); + + } + +} diff --git a/app/Http/Controllers/Api/Nasa/V1/UploadimagesBaseController.php b/app/Http/Controllers/Api/Nasa/V1/UploadimagesBaseController.php new file mode 100755 index 0000000..1899226 --- /dev/null +++ b/app/Http/Controllers/Api/Nasa/V1/UploadimagesBaseController.php @@ -0,0 +1,82 @@ + 'id', + 'sArticleCode' => 'sArticleCode', + 'sFileName' => 'sFileName', + 'sFileType' => 'sFileType', + 'oImageable_type'=> 'oImageable_type', + 'oImageable_id' => 'oImageable_id', + 'created_at' => 'created_at', + 'updated_at' => 'updated_at' + ]; + + protected array $aValueStyle = [ // 特定文字渲染样式 +// '关闭' => "关闭", +// '开启' => "开启" + ]; + + protected array $aFieldTypes = [ +// 'schema' => 'text', +// 'schema_pro' => 'text', +// 'cmd' => 'text', +// 'cmd_log' => 'text', +// 'opt' => 'text' +// 'aOpt' => 'json', +// 'aSchemaPro' => 'json', + 'sFileName' => 'image', + ]; + + protected array $aOrderBy = [ + [ + "sField" => "id", + "sSort" => "desc", + ] + ]; + + protected array $aJsonFields = [ // json字段 +// 'schema' + ]; + + protected array $aDateFields = [ // date字段 +// 'dtUpdatedAt' + ]; + + protected array $aLenthFields = [ // 限制长度的字段 +// "xx" => 100, + ]; + + protected array $aSelectFields = [ // 作为默认select的字段 +// 'status', + ]; + + protected array $aValueFields = [ // 值映射 +// 'status' => [ +// '1' => '开启', +// '0' => '关闭', +// ] + ]; + + protected array $aCcExtFields = [ +// 'dtCreatedAt' => '创建时间' + ]; + +// protected array $aLimit = [3, 15, 40, 100]; // list长度,第一个为默认 + +} diff --git a/app/Http/Middleware/VerifyNasaToken.php b/app/Http/Middleware/VerifyNasaToken.php new file mode 100644 index 0000000..6a9745b --- /dev/null +++ b/app/Http/Middleware/VerifyNasaToken.php @@ -0,0 +1,49 @@ +input(),全面改用 ->header() 降维打击! + // 🎯 核心微操:Laravel 的 header() 方法天生自带防抖,它对大小写不敏感, + // 你传 'X-Nasa-Token' 或者 'x-nasa-token',底层都能稳稳咬住! + $iRequestTime = (int)$oRequest->header('X-Nasa-Timestamp'); + $sClientToken = $oRequest->header('X-Nasa-Token'); + + // 1. 钢铁防御线一:前置空值熔断,连参数都没有的盲流直接当场轰杀 + if (!$iRequestTime || !$sClientToken) { + return response()->json(['iStatus' => 0, 'sMsg' => 'Security Headers Missing'], 403); + } + + // 2. 时效性防御:前后端时间差超过 60 秒判定为重放攻击或过期 + if (abs(time() - $iRequestTime) > 60) { + Log::warning("🛰️ NASA 网关拦截:请求超时已过期!当前服务器时间: " . time() . ",请求时间: {$iRequestTime}"); + return response()->json(['iStatus' => 0, 'sMsg' => 'Request Expired'], 403); + } + + // 3. 验签防御:从配置核心仓掏出密钥,用相同算法重算 Token 对比 + $sLocalSecret = config('nasa.secret'); + $sExpectedToken = md5("timestamp={$iRequestTime}&secret={$sLocalSecret}"); + + // 4. 钢铁防御线二:像素级对撞签名 + if ($sClientToken !== $sExpectedToken) { + $s = "🚨 NASA 网关警报:发现非法伪造签名!客户端传值: {$sClientToken},期望值: {$sExpectedToken}"; + Log::error($s); + return response()->json(['iStatus' => 0, 'sMsg' => $s], 403); + } + + // 5. 安全放行,通电! + return $fNext($oRequest); + } +} diff --git a/app/Models/Articles.php b/app/Models/Articles.php index 9278611..c2a8402 100755 --- a/app/Models/Articles.php +++ b/app/Models/Articles.php @@ -16,7 +16,7 @@ protected function casts(): array 'aOpt' => 'array', 'aSchemaPro' => 'array', ]; - } + }// 我希望查询的时候忽略这个设置 protected static function booted(): void { diff --git a/bootstrap/app.php b/bootstrap/app.php index 9157ad2..ff82512 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,11 +7,13 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ + 'nasa.auth' => \App\Http\Middleware\VerifyNasaToken::class, ]); $middleware->validateCsrfTokens(except: [ 'api/*', diff --git a/config/nasa.php b/config/nasa.php new file mode 100755 index 0000000..ccbb264 --- /dev/null +++ b/config/nasa.php @@ -0,0 +1,6 @@ + env('NASA_API_SECRET'), + 'verify_ssl' => env('NASA_API_VERIFY_SSL', true), +]; diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..920f2d4 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,33 @@ +middleware('nasa.auth')->group(function () { + + Route::prefix('article/base')->group(function() { + Route::get('', [NasaV1ArticleBaseController::class, 'index']); + Route::get('detail/{id}', [NasaV1ArticleBaseController::class, 'detail']); + Route::put('detail/save/{id}', [NasaV1ArticleBaseController::class, 'detailSave']); + Route::delete('delete/{id}', [NasaV1ArticleBaseController::class, 'delete']); + Route::get('add', [NasaV1ArticleBaseController::class, 'add']); + Route::put('add/save', [NasaV1ArticleBaseController::class, 'addSave']); + Route::get('clone/{id}', [NasaV1ArticleBaseController::class, 'clone']); + Route::put('clone/save', [NasaV1ArticleBaseController::class, 'cloneSave']); + Route::get('hit/{id}/{field}', [NasaV1ArticleBaseController::class, 'hit']); + }); + + Route::prefix('uploadimages/base')->group(function() { + Route::get('', [NasaV1UploadimagesBaseController::class, 'index']); + Route::get('detail/{id}', [NasaV1UploadimagesBaseController::class, 'detail']); + Route::put('detail/save/{id}', [NasaV1UploadimagesBaseController::class, 'detailSave']); + Route::delete('delete/{id}', [NasaV1UploadimagesBaseController::class, 'delete']); + Route::get('add', [NasaV1UploadimagesBaseController::class, 'add']); + Route::put('add/save', [NasaV1UploadimagesBaseController::class, 'addSave']); + Route::get('clone/{id}', [NasaV1UploadimagesBaseController::class, 'clone']); + Route::put('clone/save', [NasaV1UploadimagesBaseController::class, 'cloneSave']); + Route::get('hit/{id}/{field}', [NasaV1UploadimagesBaseController::class, 'hit']); + }); +});