From d96d0106c8aca514af9ac4f71338254d8d4458cc Mon Sep 17 00:00:00 2001 From: vine_liutk <961510893@qq.com> Date: Thu, 29 Jun 2023 16:54:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Admin/Controllers/OldmenController.php | 21 ++++- app/Admin/routes.php | 2 + app/Exceptions/BizException.php | 37 +++++++++ app/Exceptions/ImportException.php | 11 +++ app/Imports/ImportBase.php | 75 +++++++++++++++++ app/Imports/Oldmen.php | 60 ++++++++++++++ app/Jobs/ImportJob.php | 41 ++++++++++ app/Models/ImportJob.php | 19 +++++ app/Models/ImportJobLog.php | 24 ++++++ app/Services/Admin/ImportService.php | 47 +++++++++++ composer.json | 1 + composer.lock | 77 +++++++++++++++++- ..._06_29_152138_create_import_jobs_table.php | 44 ++++++++++ public/tmp/客人信息导入模板.xlsx | Bin 11376 -> 11352 bytes 14 files changed, 456 insertions(+), 3 deletions(-) create mode 100644 app/Exceptions/BizException.php create mode 100644 app/Exceptions/ImportException.php create mode 100644 app/Imports/ImportBase.php create mode 100644 app/Imports/Oldmen.php create mode 100644 app/Jobs/ImportJob.php create mode 100644 app/Models/ImportJob.php create mode 100644 app/Models/ImportJobLog.php create mode 100644 app/Services/Admin/ImportService.php create mode 100644 database/migrations/2023_06_29_152138_create_import_jobs_table.php diff --git a/app/Admin/Controllers/OldmenController.php b/app/Admin/Controllers/OldmenController.php index f651b5d..664136d 100644 --- a/app/Admin/Controllers/OldmenController.php +++ b/app/Admin/Controllers/OldmenController.php @@ -7,7 +7,6 @@ use Slowlyo\OwlAdmin\Models\AdminSetting; use Slowlyo\OwlAdmin\Renderers\Page; use Slowlyo\OwlAdmin\Renderers\Form; use Slowlyo\OwlAdmin\Renderers\TableColumn; -use Slowlyo\OwlAdmin\Renderers\TextControl; use Slowlyo\OwlAdmin\Controllers\AdminController; use App\Services\Admin\OldmenService; use Illuminate\Http\Request; @@ -15,6 +14,9 @@ use App\Admin\Components; use App\Models\ConstFlow; use App\Models\Oldmen; use Carbon\Carbon; +use App\Services\Admin\ImportService; +use App\Models\ImportJob; +use App\Imports\Oldmen as OldmenImport; /** * @property OldmenService $service @@ -40,7 +42,7 @@ class OldmenController extends AdminController amisMake()->DialogAction()->dialog( amisMake()->Dialog()->title('导入客人信息')->body([ amisMake()->Form()->title('') - ->api('')//处理实际上传逻辑-todo + ->api(admin_url('oldmen-import'))//处理实际上传逻辑 ->body([ amisMake()->FileControl('file', '导入文件')->accept('.xlsx')->receiver('/upload_file'),//文件上传地址待处理 ]), @@ -502,4 +504,19 @@ class OldmenController extends AdminController } return $this->response()->success($page); } + + public function import(Request $request){ + + + $job = new ImportJob(); + $job->name = '客人基础信息导入【'.now()->toDateTimeString().'】'; + $job->file = $request->input('file'); + $job->type = OldmenImport::class; + $job->status = 1; + $job->save(); + $importService = new ImportService(); + $importService->import($job); + + return $this->response()->success(); + } } diff --git a/app/Admin/routes.php b/app/Admin/routes.php index 08c8fd9..e22af3a 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -25,6 +25,8 @@ Route::group([ //客人管理 $router->resource('oldmen', \App\Admin\Controllers\OldmenController::class)->names('oldmen'); + $router->post('oldmen-import', '\App\Admin\Controllers\OldmenController@import'); + $router->get('live-feelist', '\App\Admin\Controllers\OldmenController@liveFeelist'); $router->get('exit-feelist', '\App\Admin\Controllers\OldmenController@exitFeelist'); $router->get('live-fee-form', '\App\Admin\Controllers\OldmenController@liveSchemaForm'); diff --git a/app/Exceptions/BizException.php b/app/Exceptions/BizException.php new file mode 100644 index 0000000..f4d3520 --- /dev/null +++ b/app/Exceptions/BizException.php @@ -0,0 +1,37 @@ +status = $status; + + return $this; + } + + /** + * 报告异常 + * + * @return mixed + */ + public function report() + { + } +} diff --git a/app/Exceptions/ImportException.php b/app/Exceptions/ImportException.php new file mode 100644 index 0000000..12ad709 --- /dev/null +++ b/app/Exceptions/ImportException.php @@ -0,0 +1,11 @@ +put($path, file_get_contents(str_replace(config('filesystems.disks.aliyun.domain'), config('filesystems.disks.aliyun.bucket').'.'.config('filesystems.disks.aliyun.endpoint'), $url))); + $file = new File(storage_path('app'.$path)); + return $this->readFile($file); + } + + public function readFile($file) + { + $reader = ReaderEntityFactory::createXLSXReader(); + $reader->open($file); + + $success = 0; + $fails = 0; + $errors = []; + + foreach ($reader->getSheetIterator() as $sheet) { + foreach ($sheet->getRowIterator() as $num => $row) { + if ($num === 1) { + continue; + } + try { + DB::beginTransaction(); + $this->loadRow($row); + $success++; + DB::commit(); + } catch (ImportException $e) { + DB::rollBack(); + $fails++; + $errors[] = [ + 'row'=>$num, + 'reason'=>$e->getMessage(), + ]; + } catch (Throwable $e) { + DB::rollBack(); + $fails++; + $errors[] = [ + 'row'=>$num, + 'reason'=>$e->getMessage(), + ]; + } + } + + break; + } + + $reader->close(); + return [ + 'success'=>$success, + 'fails'=>$fails, + 'errors'=>$errors, + ]; + } + + public function loadRow($row) + { + return; + } +} diff --git a/app/Imports/Oldmen.php b/app/Imports/Oldmen.php new file mode 100644 index 0000000..f69b010 --- /dev/null +++ b/app/Imports/Oldmen.php @@ -0,0 +1,60 @@ +getCellAtIndex(0)?->getValue() ?? throw new ImportException('未填写身份证号码');//身份证号 + $name = $row->getCellAtIndex(1)?->getValue() ?? throw new ImportException('未填写姓名');//姓名 + $sex = $row->getCellAtIndex(2)?->getValue() ?? throw new ImportException('未填写性别');//性别 + $birthDate = $row->getCellAtIndex(3)?->getValue() ?? throw new ImportException('未填写生日');//生日 + $cardCity = $row->getCellAtIndex(4)?->getValue() ?? throw new ImportException('未填写地址省份');//省 + $cardProvince = $row->getCellAtIndex(5)?->getValue() ?? throw new ImportException('未填写地址-市');//市 + $cardArea = $row->getCellAtIndex(6)?->getValue() ?? throw new ImportException('未填写地址-区');//区 + $cardAddress = $row->getCellAtIndex(7)?->getValue() ?? throw new ImportException('未填写详细地址');//详细地址 + $agreementNo = $row->getCellAtIndex(8)?->getValue() ?? '';//协议号码 + $nurseLvName = $row->getCellAtIndex(9)?->getValue() ?? throw new ImportException('未填写护理等级');//护理等级 + $clientName = $row->getCellAtIndex(10)?->getValue() ?? throw new ImportException('未填写监护人姓名');//监护人姓名 + $clientPhone = $row->getCellAtIndex(11)?->getValue() ?? throw new ImportException('未填写监护人手机号码');//监护人手机号 + $clientCity = $row->getCellAtIndex(12)?->getValue() ?? throw new ImportException('未填写监护人地址省份'); + $clientProvince = $row->getCellAtIndex(13)?->getValue() ?? throw new ImportException('未填写监护人地址-市');//市 + $clientArea = $row->getCellAtIndex(14)?->getValue() ?? throw new ImportException('未填写监护人地址-区');//区 + $clientAddress = $row->getCellAtIndex(15)?->getValue() ?? throw new ImportException('未填写监护人详细地址');//详细地址 + + if(ModelsOldmen::where('card_no', $cardNo)->exists()){//如果已存在,则为更新 + $oldman = ModelsOldmen::where('card_no', $cardNo)->first(); + $newLv = Keyword::where(['type_key'=>'nurse_lv', 'name'=>$nurseLvName])->value('value'); + if($oldman->nurse_lv !== $newLv && $oldman->live_in > 0){ + throw new ImportException('当前入住状态无法直接变更护理等级'); + }K; + }else{ + $oldman = new ModelsOldmen(); + $oldman->card_no = $cardNo; + $oldman->nurse_lv = Keyword::where(['type_key'=>'nurse_lv', 'name'=>$nurseLvName])->value('value'); + } + $oldman->name = $name; + $sexArr = [ + '未知'=>0, + '男'=>1, + '女'=>2 + ]; + $oldman->sex = $sexArr[$sex]; + $oldman->birthday = Carbon::parse($birthDate); + $oldman->card_city_code = Zone::where(['name' => $cardArea, 'type'=>'area'])->value('code') ?? ''; + $oldman->card_address = $cardAddress; + $oldman->agreement_no = $agreementNo; + $oldman->client_name = $clientName; + $oldman->client_phone = $clientPhone; + $oldman->client_city_code = Zone::where(['name' => $clientArea, 'type'=>'area'])->value('code') ?? ''; + $oldman->client_address = $clientAddress; + $oldman->save(); + } +} diff --git a/app/Jobs/ImportJob.php b/app/Jobs/ImportJob.php new file mode 100644 index 0000000..f0b31cf --- /dev/null +++ b/app/Jobs/ImportJob.php @@ -0,0 +1,41 @@ +importJob = $job; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if(env('APP_DEBUG')){ + \Log::info('执行文件导入'); + } + (new ImportService())->import($this->importJob); + } +} diff --git a/app/Models/ImportJob.php b/app/Models/ImportJob.php new file mode 100644 index 0000000..65eafea --- /dev/null +++ b/app/Models/ImportJob.php @@ -0,0 +1,19 @@ +'失败', + self::STATUS_SUCCESS=>'成功', + ]; + + public function job() + { + return $this->belongsTo(ImportJob::class); + } +} diff --git a/app/Services/Admin/ImportService.php b/app/Services/Admin/ImportService.php new file mode 100644 index 0000000..9c6b0d8 --- /dev/null +++ b/app/Services/Admin/ImportService.php @@ -0,0 +1,47 @@ +status == 1) { + $this->driver = new $job->type(); + if(strpos($job->file, 'http') !== false) { + $res = $this->driver->readFileByUrl($job->file); + }else{ + $file = new File(public_path('storage/'.$job->file)); + $res = $this->driver->readFile($file); + } + + if ($res) { + $job->update([ + 'status'=>2, + 'success' => $res['success']??0, + 'fails'=> $res['fails']??0, + ]); + } + if (isset($res['errors']) && count($res['errors']) > 0) { + $this->createErrorLogs($job, $res['errors']); + } + } + } + + public function createErrorLogs(ImportJob $job, array $errors) + { + ImportJobLog::insert(array_map(function ($value) use ($job) { + return array_merge($value, [ + 'job_id'=>$job->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + }, $errors)); + } +} diff --git a/composer.json b/composer.json index 9c7a4f1..a1f7b1c 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "license": "MIT", "require": { "php": "^8.1", + "box/spout": "^3.3", "guzzlehttp/guzzle": "^7.2", "laravel/framework": "^10.10", "laravel/sanctum": "^3.2", diff --git a/composer.lock b/composer.lock index 9f3cdd3..5d777cf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,83 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a483aafe1cb9cc540a4db897e1c35ac4", + "content-hash": "4a5297a88397d5f65572b83c55b33701", "packages": [ + { + "name": "box/spout", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/box/spout.git", + "reference": "9bdb027d312b732515b884a341c0ad70372c6295" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/box/spout/zipball/9bdb027d312b732515b884a341c0ad70372c6295", + "reference": "9bdb027d312b732515b884a341c0ad70372c6295", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-zip": "*", + "php": ">=7.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2", + "phpunit/phpunit": "^8" + }, + "suggest": { + "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)", + "ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Box\\Spout\\": "src/Spout" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Adrien Loison", + "email": "adrien@box.com" + } + ], + "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way", + "homepage": "https://www.github.com/box/spout", + "keywords": [ + "OOXML", + "csv", + "excel", + "memory", + "odf", + "ods", + "office", + "open", + "php", + "read", + "scale", + "spreadsheet", + "stream", + "write", + "xlsx" + ], + "support": { + "issues": "https://github.com/box/spout/issues", + "source": "https://github.com/box/spout/tree/v3.3.0" + }, + "abandoned": true, + "time": "2021-05-14T21:18:09+00:00" + }, { "name": "brick/math", "version": "0.11.0", diff --git a/database/migrations/2023_06_29_152138_create_import_jobs_table.php b/database/migrations/2023_06_29_152138_create_import_jobs_table.php new file mode 100644 index 0000000..23cdd1e --- /dev/null +++ b/database/migrations/2023_06_29_152138_create_import_jobs_table.php @@ -0,0 +1,44 @@ +id(); + $table->string('name')->nullable()->comment('名称'); + $table->string('file')->comment('导入的文件路径'); + $table->string('type')->comment('导入执行类'); + $table->unsignedTinyInteger('status')->default(0)->comment('状态:0未开始,1导入中,2完成'); + $table->unsignedInteger('success')->default(0)->comment('成功条数'); + $table->unsignedInteger('fails')->default(0)->comment('失败条数'); + // $table->unsignedBigInteger('') + $table->timestamps(); + }); + + Schema::create('import_job_logs', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('job_id')->comment('任务ID'); + $table->unsignedInteger('row')->default(0)->comment('行数'); + $table->unsignedTinyInteger('status')->default(0)->comment('状态1成功'); + $table->text('reason')->nullable()->comment('原因'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('import_jobs'); + Schema::dropIfExists('import_job_logs'); + } +}; diff --git a/public/tmp/客人信息导入模板.xlsx b/public/tmp/客人信息导入模板.xlsx index 502b98623dea27d946feb9ad47204fc675f18849..7014f87a379119dfa4c71076b93fc8208efb14cb 100644 GIT binary patch delta 5038 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