From e1d67476cae593a76b074912629c41008c20958d Mon Sep 17 00:00:00 2001 From: wanghongjun <1445693971@qq.com> Date: Tue, 3 Mar 2026 16:33:08 +0800 Subject: [PATCH] =?UTF-8?q?VIP=E5=90=8D=E5=8D=95=E5=A2=9E=E5=88=A0?= =?UTF-8?q?=E6=94=B9=E6=9F=A5=EF=BC=8C=E5=AF=BC=E5=85=A5=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Exports/AdminVipListExport.php | 18 + .../AdminVipListImportTemplateExport.php | 20 + .../Controllers/Admin/VipListController.php | 260 ++++++++++++ app/Imports/AdminVipListImport.php | 24 ++ app/Models/AdminUsers.php | 9 + app/Models/AdminVipList.php | 25 ++ app/Services/AdminVipListService.php | 131 ++++++ config/excel.php | 380 ++++++++++++++++++ resources/lang/zh-CN/exports.php | 8 + 9 files changed, 875 insertions(+) create mode 100644 app/Exports/AdminVipListExport.php create mode 100644 app/Exports/AdminVipListImportTemplateExport.php create mode 100644 app/Http/Controllers/Admin/VipListController.php create mode 100644 app/Imports/AdminVipListImport.php create mode 100644 app/Models/AdminVipList.php create mode 100644 app/Services/AdminVipListService.php create mode 100644 config/excel.php create mode 100644 resources/lang/zh-CN/exports.php diff --git a/app/Exports/AdminVipListExport.php b/app/Exports/AdminVipListExport.php new file mode 100644 index 0000000..6892cc5 --- /dev/null +++ b/app/Exports/AdminVipListExport.php @@ -0,0 +1,18 @@ +responseService = $responseService; + $this->vipListService = $vipListService; + } + + /** + * @param Request $request + * @return JsonResponse + */ + public function index(Request $request): JsonResponse + { + try { + $query = AdminVipList::query(); + + // 分页 + $page = $request->input('page', 1); + $perPage = $request->input('per_page', 10); + + $total = $query->count(); + $items = $query->latest()->forPage($page, $perPage)->get()->each( + function ($item) { + $item['username'] = AdminUsers::getUsername( + $item['user_id'] + ); + return $item; + } + ); + + return $this->responseService->success([ + 'items' => $items, + 'total' => $total, + 'page' => $page, + 'per_page' => $perPage, + 'last_page' => ceil($total / $perPage), + ]); + } catch (Exception $e) { + $m_prefix = __('exception.get_user_info_list_failed'); + return $this->responseService->systemError( + $m_prefix . ':' . $e->getMessage() + ); + } + } + + /** + * @param Request $request + * @param string $id + * @return JsonResponse + * @throws CustomException + * @throws ValidationException + */ + public function update(Request $request, string $id): JsonResponse + { + try { + $this->saveValidator($request->all(), $id); + $this->vipListService->updateModel($request->all(), $id); + return $this->responseService->success( + null, + __('admin.update_succeeded') + ); + } catch (ValidationException|CustomException $e) { + throw $e; + } catch (Exception $e) { + return $this->responseService->systemError( + __('exception.admin_vip_list.update_failed') . ':' + . $e->getMessage() + ); + } + } + + /** + * @param array $data + * @param int $id + * @return void + * @throws ValidationException + */ + protected function saveValidator(array $data, int $id = 0): void + { + $rules = [ + 'license' => 'required|max:20' + ]; + $messages = [ + 'license.required' => __('validation.admin_list_vip.l_empty'), + 'license.max' => __('validation.admin_list_vip.l_max') + ]; + if ($id) { + $this->validateId($id, AdminVipList::class); + } + $validator = Validator::make($data, $rules, $messages); + + if ($validator->fails()) { + throw new ValidationException($validator); + } + } + + /** + * @param string $id + * @return JsonResponse + * @throws CustomException + * @throws ValidationException + */ + public function destroy(string $id): JsonResponse + { + try { + $this->validateId($id, AdminVipList::class); + $this->vipListService->deleteModel($id); + return $this->responseService->success( + null, + __('admin.delete_succeeded') + ); + } catch (ValidationException|CustomException $e) { + throw $e; + } catch (Exception $e) { + return $this->responseService->systemError( + __('exception.admin_vip_list.destroy_failed') . ':' + . $e->getMessage() + ); + } + } + + /** + * @param Request $request + * @return JsonResponse + * @throws ValidationException + */ + public function import(Request $request): JsonResponse + { + try { + // 1. 验证上传的文件 + $request->validate([ + 'file' => 'required|mimes:xlsx,xls,csv|max:2048' // 限制文件类型和大小 + ]); + $validator = Validator::make($request->file('file'), [ + 'file' => 'required|mimes:xlsx,xls,csv|max:2048' + ], [ + 'file.required' => __('validation.admin_list_vip.file_empty'), + 'file.mimes' => __('validation.admin_list_vip.file_mimes'), + 'file.max' => __('validation.admin_list_vip.file_max'), + ]); + if ($validator->fails()) { + throw new ValidationException($validator); + } + + // 2. 获取上传的文件 + $file = $request->file('file'); + + // 3. 正确的做法:先存储文件,再获取绝对路径进行导入 + // 将文件存储到 storage/app/imports 目录下 + $path = $file->store('imports'); + + // 4. 执行导入(使用存储后的绝对路径) + // storage_path('app') 获取 storage/app 的绝对路径 + Excel::import( + new AdminVipListImport(), + storage_path('app/' . $path) + ); + + // 5. (可选)导入完成后删除临时文件 + Storage::delete($path); + + return $this->responseService->systemError( + __('controller.import.success') + ); + } catch (ValidationException $e) { + throw $e; + } catch (Exception $e) { + return $this->responseService->systemError( + __('exception.admin_vip_list.import_failed') . ':' + . $e->getMessage() + ); + } + } + + /** + * @param Request $request + * @return JsonResponse + * @throws CustomException + * @throws ValidationException + */ + public function store(Request $request): JsonResponse + { + try { + $this->saveValidator($request->all()); + $this->vipListService->createModel($request->all()); + + return $this->responseService->success( + null, + __('admin.save_succeeded') + ); + } catch (ValidationException|CustomException $e) { + throw $e; + } catch (Exception $e) { + return $this->responseService->systemError( + __('exception.admin_vip_list.create_failed') . ':' + . $e->getMessage() + ); + } + } + + /** + * @return BinaryFileResponse + */ + public function importTemplate(): BinaryFileResponse + { + return Excel::download( + new AdminVipListImportTemplateExport(), + __('exports.vip_list.import_template') . '.xlsx' + ); + } + + /** + * @return BinaryFileResponse + */ + public function export(): BinaryFileResponse + { + return Excel::download( + new AdminVipListExport(), + __('exports.vip_list.list') . '.xlsx' + ); + } +} diff --git a/app/Imports/AdminVipListImport.php b/app/Imports/AdminVipListImport.php new file mode 100644 index 0000000..bb02dc6 --- /dev/null +++ b/app/Imports/AdminVipListImport.php @@ -0,0 +1,24 @@ + $row['license'], + 'user_id' => Auth::guard('sanctum')->user()['id'], + 'created_at' => get_datetime() + ]); + } +} diff --git a/app/Models/AdminUsers.php b/app/Models/AdminUsers.php index fc4243c..b1fa2e5 100644 --- a/app/Models/AdminUsers.php +++ b/app/Models/AdminUsers.php @@ -53,4 +53,13 @@ class AdminUsers extends Model { return $this->belongsToMany(AdminRoles::class, AdminRoleUsers::class, 'user_id', 'role_id'); } + + /** + * @param $user_id + * @return mixed + */ + public static function getUsername($user_id): mixed + { + return self::query()->where('id', $user_id)->value('username'); + } } diff --git a/app/Models/AdminVipList.php b/app/Models/AdminVipList.php new file mode 100644 index 0000000..1baaf50 --- /dev/null +++ b/app/Models/AdminVipList.php @@ -0,0 +1,25 @@ +logService = $logService; + } + + + /** + * @param array $data + * @return Model|Builder + * @throws Exception + */ + public function createModel(array $data): Model|Builder + { + try { + DB::beginTransaction(); + + if (AdminVipList::query()->where('license', $data['license']) + ->exists() + ) { + throw new Exception( + __('service.admin_vip_list.license_exists') + ); + } + + $model = AdminVipList::query()->create([ + 'license' => $data['license'], + 'user_id' => Auth::guard('sanctum')->user()['id'], + 'created_at' => get_datetime() + ]); + + $this->logService->logCreated($model, '创建VIP名单'); + + DB::commit(); + return $model; + } catch (Exception $e) { + DB::rollBack(); + throw $e; + } + } + + + + /** + * @param array $data + * @param int $id + * @return Model|Builder + * @throws Exception + */ + public function updateModel(array $data, int $id): Model|Builder + { + try { + DB::beginTransaction(); + + // 验证 + $existsWhere = [ + ['license', '=', $data['license']], + ['id', '<>', $id] + ]; + if (AdminVipList::query()->where($existsWhere)->exists()) { + throw new Exception(__('service.admin_vip_list.license_exists')); + } + + // 更新 + $model = AdminVipList::query()->findOrFail($id); + $oldValues = $model->toArray(); + + $model->update([ + 'license' => $data['license'], + 'user_id' => Auth::guard('sanctum')->user()['id'], + 'updated_at' => get_datetime() + ]); + + $this->logService->logUpdated($model, $oldValues, '更新VIP名单'); + + DB::commit(); + return $model; + } catch (Exception $e) { + DB::rollBack(); + throw $e; + } + } + + /** + * @param $id + * @return bool + * @throws Exception + */ + public function deleteModel($id): bool + { + try { + DB::beginTransaction(); + + $model = AdminVipList::query()->findOrFail($id); + + $this->logService->logDeleted($model, '删除VIP名单'); + + $model->delete(); + + DB::commit(); + return true; + } catch (Exception $e) { + DB::rollBack(); + throw $e; + } + } + +} diff --git a/config/excel.php b/config/excel.php new file mode 100644 index 0000000..c1fd34a --- /dev/null +++ b/config/excel.php @@ -0,0 +1,380 @@ + [ + + /* + |-------------------------------------------------------------------------- + | Chunk size + |-------------------------------------------------------------------------- + | + | When using FromQuery, the query is automatically chunked. + | Here you can specify how big the chunk should be. + | + */ + 'chunk_size' => 1000, + + /* + |-------------------------------------------------------------------------- + | Pre-calculate formulas during export + |-------------------------------------------------------------------------- + */ + 'pre_calculate_formulas' => false, + + /* + |-------------------------------------------------------------------------- + | Enable strict null comparison + |-------------------------------------------------------------------------- + | + | When enabling strict null comparison empty cells ('') will + | be added to the sheet. + */ + 'strict_null_comparison' => false, + + /* + |-------------------------------------------------------------------------- + | CSV Settings + |-------------------------------------------------------------------------- + | + | Configure e.g. delimiter, enclosure and line ending for CSV exports. + | + */ + 'csv' => [ + 'delimiter' => ',', + 'enclosure' => '"', + 'line_ending' => PHP_EOL, + 'use_bom' => false, + 'include_separator_line' => false, + 'excel_compatibility' => false, + 'output_encoding' => '', + 'test_auto_detect' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Worksheet properties + |-------------------------------------------------------------------------- + | + | Configure e.g. default title, creator, subject,... + | + */ + 'properties' => [ + 'creator' => '', + 'lastModifiedBy' => '', + 'title' => '', + 'description' => '', + 'subject' => '', + 'keywords' => '', + 'category' => '', + 'manager' => '', + 'company' => '', + ], + ], + + 'imports' => [ + + /* + |-------------------------------------------------------------------------- + | Read Only + |-------------------------------------------------------------------------- + | + | When dealing with imports, you might only be interested in the + | data that the sheet exists. By default we ignore all styles, + | however if you want to do some logic based on style data + | you can enable it by setting read_only to false. + | + */ + 'read_only' => true, + + /* + |-------------------------------------------------------------------------- + | Ignore Empty + |-------------------------------------------------------------------------- + | + | When dealing with imports, you might be interested in ignoring + | rows that have null values or empty strings. By default rows + | containing empty strings or empty values are not ignored but can be + | ignored by enabling the setting ignore_empty to true. + | + */ + 'ignore_empty' => false, + + /* + |-------------------------------------------------------------------------- + | Heading Row Formatter + |-------------------------------------------------------------------------- + | + | Configure the heading row formatter. + | Available options: none|slug|custom + | + */ + 'heading_row' => [ + 'formatter' => 'slug', + ], + + /* + |-------------------------------------------------------------------------- + | CSV Settings + |-------------------------------------------------------------------------- + | + | Configure e.g. delimiter, enclosure and line ending for CSV imports. + | + */ + 'csv' => [ + 'delimiter' => null, + 'enclosure' => '"', + 'escape_character' => '\\', + 'contiguous' => false, + 'input_encoding' => Csv::GUESS_ENCODING, + ], + + /* + |-------------------------------------------------------------------------- + | Worksheet properties + |-------------------------------------------------------------------------- + | + | Configure e.g. default title, creator, subject,... + | + */ + 'properties' => [ + 'creator' => '', + 'lastModifiedBy' => '', + 'title' => '', + 'description' => '', + 'subject' => '', + 'keywords' => '', + 'category' => '', + 'manager' => '', + 'company' => '', + ], + + /* + |-------------------------------------------------------------------------- + | Cell Middleware + |-------------------------------------------------------------------------- + | + | Configure middleware that is executed on getting a cell value + | + */ + 'cells' => [ + 'middleware' => [ + //\Maatwebsite\Excel\Middleware\TrimCellValue::class, + //\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class, + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Extension detector + |-------------------------------------------------------------------------- + | + | Configure here which writer/reader type should be used when the package + | needs to guess the correct type based on the extension alone. + | + */ + 'extension_detector' => [ + 'xlsx' => Excel::XLSX, + 'xlsm' => Excel::XLSX, + 'xltx' => Excel::XLSX, + 'xltm' => Excel::XLSX, + 'xls' => Excel::XLS, + 'xlt' => Excel::XLS, + 'ods' => Excel::ODS, + 'ots' => Excel::ODS, + 'slk' => Excel::SLK, + 'xml' => Excel::XML, + 'gnumeric' => Excel::GNUMERIC, + 'htm' => Excel::HTML, + 'html' => Excel::HTML, + 'csv' => Excel::CSV, + 'tsv' => Excel::TSV, + + /* + |-------------------------------------------------------------------------- + | PDF Extension + |-------------------------------------------------------------------------- + | + | Configure here which Pdf driver should be used by default. + | Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF + | + */ + 'pdf' => Excel::DOMPDF, + ], + + /* + |-------------------------------------------------------------------------- + | Value Binder + |-------------------------------------------------------------------------- + | + | PhpSpreadsheet offers a way to hook into the process of a value being + | written to a cell. In there some assumptions are made on how the + | value should be formatted. If you want to change those defaults, + | you can implement your own default value binder. + | + | Possible value binders: + | + | [x] Maatwebsite\Excel\DefaultValueBinder::class + | [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class + | [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class + | + */ + 'value_binder' => [ + 'default' => Maatwebsite\Excel\DefaultValueBinder::class, + ], + + 'cache' => [ + /* + |-------------------------------------------------------------------------- + | Default cell caching driver + |-------------------------------------------------------------------------- + | + | By default PhpSpreadsheet keeps all cell values in memory, however when + | dealing with large files, this might result into memory issues. If you + | want to mitigate that, you can configure a cell caching driver here. + | When using the illuminate driver, it will store each value in the + | cache store. This can slow down the process, because it needs to + | store each value. You can use the "batch" store if you want to + | only persist to the store when the memory limit is reached. + | + | Drivers: memory|illuminate|batch + | + */ + 'driver' => 'memory', + + /* + |-------------------------------------------------------------------------- + | Batch memory caching + |-------------------------------------------------------------------------- + | + | When dealing with the "batch" caching driver, it will only + | persist to the store when the memory limit is reached. + | Here you can tweak the memory limit to your liking. + | + */ + 'batch' => [ + 'memory_limit' => 60000, + ], + + /* + |-------------------------------------------------------------------------- + | Illuminate cache + |-------------------------------------------------------------------------- + | + | When using the "illuminate" caching driver, it will automatically use + | your default cache store. However if you prefer to have the cell + | cache on a separate store, you can configure the store name here. + | You can use any store defined in your cache config. When leaving + | at "null" it will use the default store. + | + */ + 'illuminate' => [ + 'store' => null, + ], + + /* + |-------------------------------------------------------------------------- + | Cache Time-to-live (TTL) + |-------------------------------------------------------------------------- + | + | The TTL of items written to cache. If you want to keep the items cached + | indefinitely, set this to null. Otherwise, set a number of seconds, + | a \DateInterval, or a callable. + | + | Allowable types: callable|\DateInterval|int|null + | + */ + 'default_ttl' => 10800, + ], + + /* + |-------------------------------------------------------------------------- + | Transaction Handler + |-------------------------------------------------------------------------- + | + | By default the import is wrapped in a transaction. This is useful + | for when an import may fail and you want to retry it. With the + | transactions, the previous import gets rolled-back. + | + | You can disable the transaction handler by setting this to null. + | Or you can choose a custom made transaction handler here. + | + | Supported handlers: null|db + | + */ + 'transactions' => [ + 'handler' => 'db', + 'db' => [ + 'connection' => null, + ], + ], + + 'temporary_files' => [ + + /* + |-------------------------------------------------------------------------- + | Local Temporary Path + |-------------------------------------------------------------------------- + | + | When exporting and importing files, we use a temporary file, before + | storing reading or downloading. Here you can customize that path. + | permissions is an array with the permission flags for the directory (dir) + | and the create file (file). + | + */ + 'local_path' => storage_path('framework/cache/laravel-excel'), + + /* + |-------------------------------------------------------------------------- + | Local Temporary Path Permissions + |-------------------------------------------------------------------------- + | + | Permissions is an array with the permission flags for the directory (dir) + | and the create file (file). + | If omitted the default permissions of the filesystem will be used. + | + */ + 'local_permissions' => [ + // 'dir' => 0755, + // 'file' => 0644, + ], + + /* + |-------------------------------------------------------------------------- + | Remote Temporary Disk + |-------------------------------------------------------------------------- + | + | When dealing with a multi server setup with queues in which you + | cannot rely on having a shared local temporary path, you might + | want to store the temporary file on a shared disk. During the + | queue executing, we'll retrieve the temporary file from that + | location instead. When left to null, it will always use + | the local path. This setting only has effect when using + | in conjunction with queued imports and exports. + | + */ + 'remote_disk' => null, + 'remote_prefix' => null, + + /* + |-------------------------------------------------------------------------- + | Force Resync + |-------------------------------------------------------------------------- + | + | When dealing with a multi server setup as above, it's possible + | for the clean up that occurs after entire queue has been run to only + | cleanup the server that the last AfterImportJob runs on. The rest of the server + | would still have the local temporary file stored on it. In this case your + | local storage limits can be exceeded and future imports won't be processed. + | To mitigate this you can set this config value to be true, so that after every + | queued chunk is processed the local temporary file is deleted on the server that + | processed it. + | + */ + 'force_resync_remote' => null, + ], +]; diff --git a/resources/lang/zh-CN/exports.php b/resources/lang/zh-CN/exports.php new file mode 100644 index 0000000..b48acb6 --- /dev/null +++ b/resources/lang/zh-CN/exports.php @@ -0,0 +1,8 @@ + [ + 'license' => '车牌号码', + 'import_template' => 'VIP名单导入模板', + 'list' => 'VIP名单' + ] +];