From 783bbde30ee4809e0df36b02338be3995f7c3986 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Sat, 13 Apr 2024 16:57:49 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=AD=97=E5=85=B8=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Api/KeywordController.php | 17 +++++++++++++++++ .../ReimbursementTypeController.php | 17 ----------------- routes/api.php | 10 ++++------ 3 files changed, 21 insertions(+), 23 deletions(-) create mode 100644 app/Http/Controllers/Api/KeywordController.php delete mode 100644 app/Http/Controllers/Api/Reimbursement/ReimbursementTypeController.php diff --git a/app/Http/Controllers/Api/KeywordController.php b/app/Http/Controllers/Api/KeywordController.php new file mode 100644 index 0000000..793ddf6 --- /dev/null +++ b/app/Http/Controllers/Api/KeywordController.php @@ -0,0 +1,17 @@ +input())->get(); + + return KeywordResource::collection($keywords); + } +} diff --git a/app/Http/Controllers/Api/Reimbursement/ReimbursementTypeController.php b/app/Http/Controllers/Api/Reimbursement/ReimbursementTypeController.php deleted file mode 100644 index 284f9cd..0000000 --- a/app/Http/Controllers/Api/Reimbursement/ReimbursementTypeController.php +++ /dev/null @@ -1,17 +0,0 @@ - 'reimbursement_type'])->get(); - - return KeywordResource::collection($keywords); - } -} diff --git a/routes/api.php b/routes/api.php index 02c89b4..deae90a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,9 +4,9 @@ use App\Http\Controllers\Api\Account\StoreMasterCommissionController; use App\Http\Controllers\Api\Auth\AccessTokenController; use App\Http\Controllers\Api\ComplaintController; use App\Http\Controllers\Api\FeedbackController; +use App\Http\Controllers\Api\KeywordController; use App\Http\Controllers\Api\Ledger\LedgerController; use App\Http\Controllers\Api\Ledger\LotteryTypeController; -use App\Http\Controllers\Api\Reimbursement\ReimbursementTypeController; use App\Http\Controllers\Api\StatsController; use Illuminate\Support\Facades\Route; @@ -16,6 +16,9 @@ Route::delete('/auth/logout', [AccessTokenController::class, 'destroy']); Route::group([ 'middleware' => ['auth:api'], ], function () { + // 字典表 + Route::get('keywords', [KeywordController::class, 'index']); + // 当前账户信息 Route::get('auth/profile', [\App\Http\Controllers\Api\Auth\UserController::class, 'profile']); // 修改账户信息 @@ -33,11 +36,6 @@ Route::group([ Route::get('/ledger/ledgers/{date}', [LedgerController::class, 'show']); Route::get('/ledger/lottery-types', [LotteryTypeController::class, 'index']); - // 报销管理 - Route::group(['prefix' => 'reimbursement'], function () { - Route::get('reimbursement-types', [ReimbursementTypeController::class, 'index']); - }); - // 举报投诉 Route::post('complaints', [ComplaintController::class, 'store']); // 意见箱 From 86648e67fcd993c725bcde8086ba535a316d23c5 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Sat, 13 Apr 2024 17:59:15 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=8A=A5=E9=94=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Admin/Services/WorkFlowService.php | 14 +- app/Filters/ReimbursementFilter.php | 9 + .../Api/ReimbursementController.php | 158 ++++++++++++++++++ app/Http/Resources/ReimbursementResource.php | 28 ++++ app/Http/Resources/WorkflowCheckResource.php | 23 +++ app/Models/Employee.php | 8 + app/Models/Reimbursement.php | 17 ++ routes/api.php | 4 + 8 files changed, 253 insertions(+), 8 deletions(-) create mode 100644 app/Filters/ReimbursementFilter.php create mode 100644 app/Http/Controllers/Api/ReimbursementController.php create mode 100644 app/Http/Resources/ReimbursementResource.php create mode 100644 app/Http/Resources/WorkflowCheckResource.php diff --git a/app/Admin/Services/WorkFlowService.php b/app/Admin/Services/WorkFlowService.php index 87d8961..dedbd6e 100644 --- a/app/Admin/Services/WorkFlowService.php +++ b/app/Admin/Services/WorkFlowService.php @@ -2,7 +2,6 @@ namespace App\Admin\Services; -use App\Contracts\Checkable; use App\Enums\CheckStatus; use App\Enums\CheckType; use App\Models\Employee; @@ -34,16 +33,16 @@ class WorkFlowService extends BaseService public function apply(WorkflowCheck $check, Employee $user) { if ($check->check_status === CheckStatus::Success->value) { - return $this->setError('已经审核通过'); + admin_abort('已经审核通过'); } if ($check->check_status === CheckStatus::Processing->value) { - return $this->setError('正在审核中'); + admin_abort('正在审核中'); } $workflow = Workflow::where('key', $check->key)->first(); // 没有配置审核流程, 直接通过 if (! $workflow || ! $workflow->config) { - $this->success(); + $this->success($check); return true; } @@ -76,8 +75,7 @@ class WorkFlowService extends BaseService $checkValue = $checkUser->id; $checkName = $checkUser->name; } else { - return $this->setError('未知的审核类型: '.$item['type']); - break; + admin_abort('未知的审核类型: '.$item['type']); } $check->logs()->create([ 'batch_id' => $batchId, @@ -142,10 +140,10 @@ class WorkFlowService extends BaseService public function check(Employee $user, WorkflowLog $log, $status, $options = []) { if ($log->check_status != CheckStatus::Processing) { - return $this->setError('不可操作, 等待前面的审核完成'); + admin_abort('不可操作, 等待前面的审核完成'); } if (! $this->authCheck($user, $log)) { - return $this->setError('没有权限'); + admin_abort('没有权限'); } $attributes = ['check_status' => $status ? CheckStatus::Success : CheckStatus::Fail]; $attributes['checked_at'] = data_get($options, 'time', now()); diff --git a/app/Filters/ReimbursementFilter.php b/app/Filters/ReimbursementFilter.php new file mode 100644 index 0000000..cea21e5 --- /dev/null +++ b/app/Filters/ReimbursementFilter.php @@ -0,0 +1,9 @@ +user(); + + $reimbursements = $user->reimbursements() + ->filter($request->input()) + ->latest('id') + ->simplePaginate($request->input('per_page', 20)); + + return ReimbursementResource::collection( + $reimbursements->loadMissing(['type', 'workflow']), + ); + } + + public function store(Request $request, WorkFlowService $workFlowService): ReimbursementResource + { + $validated = $request->validate( + rules: [ + 'reimbursement_type_id' => ['bail', 'required', Rule::exists(Keyword::class, 'key')], + 'expense' => ['bail', 'required', 'numeric', 'min:0'], + 'reason' => ['bail', 'required', 'max:255'], + 'photos' => ['bail', 'required', 'array'], + ], + attributes: [ + 'reimbursement_type_id' => '报销分类', + 'expense' => '报销金额', + 'reason' => '报销原因', + 'photos' => '报销凭证', + ], + ); + + /** @var \App\Models\Employee */ + $user = $request->user(); + + try { + DB::beginTransaction(); + + /** @var \App\Models\Reimbursement */ + $reimbursement = $user->reimbursements()->create($validated); + + $workFlowService->apply($reimbursement->workflow, $user); + + DB::commit(); + } catch (Throwable $th) { + DB::rollBack(); + + throw tap($th, fn ($th) => report($th)); + } + + return ReimbursementResource::make( + $reimbursement->load(['type', 'workflow']), + ); + } + + public function show($id, Request $request): ReimbursementResource + { + /** @var \App\Models\Employee */ + $user = $request->user(); + + /** @var \App\Models\Reimbursement */ + $reimbursement = $user->reimbursements()->find($id); + + if (is_null($reimbursement)) { + throw new RuntimeException('报销记录未找到'); + } + + return ReimbursementResource::make( + $reimbursement->load(['type', 'workflow']), + ); + } + + public function update($id, Request $request, WorkFlowService $workFlowService): ReimbursementResource + { + $validated = $request->validate( + rules: [ + 'reimbursement_type_id' => ['bail', 'required', Rule::exists(Keyword::class, 'key')], + 'expense' => ['bail', 'required', 'numeric', 'min:0'], + 'reason' => ['bail', 'required', 'max:255'], + 'photos' => ['bail', 'required', 'array'], + ], + attributes: [ + 'reimbursement_type_id' => '报销分类', + 'expense' => '报销金额', + 'reason' => '报销原因', + 'photos' => '报销凭证', + ], + ); + + /** @var \App\Models\Employee */ + $user = $request->user(); + + /** @var \App\Models\Reimbursement */ + $reimbursement = $user->reimbursements()->find($id); + + if (is_null($reimbursement)) { + throw new RuntimeException('报销记录未找到'); + } + + if (! $reimbursement->canUpdate()) { + throw new RuntimeException('['.$reimbursement->workflow->check_status->text().']报销记录不可修改'); + } + + try { + DB::beginTransaction(); + + $reimbursement->update($validated); + + $workFlowService->apply($reimbursement->workflow, $user); + + DB::commit(); + } catch (Throwable $th) { + DB::rollBack(); + + throw tap($th, fn ($th) => report($th)); + } + + return ReimbursementResource::make( + $reimbursement->load(['type', 'workflow']), + ); + } + + public function destroy($id, Request $request) + { + /** @var \App\Models\Employee */ + $user = $request->user(); + + /** @var \App\Models\Reimbursement */ + $reimbursement = $user->reimbursements()->find($id); + + if (is_null($reimbursement)) { + throw new RuntimeException('报销记录未找到'); + } + + if (! $reimbursement->canDelete()) { + throw new RuntimeException('['.$reimbursement->workflow->check_status->text().']报销记录不可删除'); + } + + $reimbursement->delete(); + + return response()->noContent(); + } +} diff --git a/app/Http/Resources/ReimbursementResource.php b/app/Http/Resources/ReimbursementResource.php new file mode 100644 index 0000000..481f615 --- /dev/null +++ b/app/Http/Resources/ReimbursementResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'type' => KeywordResource::make($this->whenLoaded('type')), + 'workflow_check' => WorkflowCheckResource::make($this->whenLoaded('workflow')), + 'expense' => $this->expense, + 'reason' => $this->reason, + 'photos' => $this->photos, + 'created_at' => $this->created_at?->getTimestamp(), + 'updated_at' => $this->updated_at?->getTimestamp(), + ]; + } +} diff --git a/app/Http/Resources/WorkflowCheckResource.php b/app/Http/Resources/WorkflowCheckResource.php new file mode 100644 index 0000000..da6578e --- /dev/null +++ b/app/Http/Resources/WorkflowCheckResource.php @@ -0,0 +1,23 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'check_status' => $this->check_status, + 'checked_at' => $this->checked_at?->getTimestamp(), + 'check_remarks' => (string) $this->check_remarks, + ]; + } +} diff --git a/app/Models/Employee.php b/app/Models/Employee.php index 623394d..22a9b64 100644 --- a/app/Models/Employee.php +++ b/app/Models/Employee.php @@ -84,6 +84,14 @@ class Employee extends Model implements AuthenticatableContract return $this->hasMany(StoreMasterCommission::class, 'store_master_id'); } + /** + * 报销 + */ + public function reimbursements() + { + return $this->hasMany(Reimbursement::class); + } + // 管理的门店(店长) // public function masterStore() // { diff --git a/app/Models/Reimbursement.php b/app/Models/Reimbursement.php index abe9714..9c048e4 100644 --- a/app/Models/Reimbursement.php +++ b/app/Models/Reimbursement.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Enums\CheckStatus; +use App\Filters\ReimbursementFilter; use App\Traits\HasCheckable; use App\Traits\HasDateTimeFormatter; use EloquentFilter\Filterable; @@ -32,6 +34,21 @@ class Reimbursement extends Model return $this->belongsTo(Keyword::class, 'reimbursement_type_id', 'key'); } + public function modelFilter(): string + { + return ReimbursementFilter::class; + } + + public function canUpdate(): bool + { + return in_array($this->workflow?->check_status, [CheckStatus::None, CheckStatus::Fail, CheckStatus::Cancel]); + } + + public function canDelete(): bool + { + return in_array($this->workflow?->check_status, [CheckStatus::None, CheckStatus::Fail, CheckStatus::Cancel]); + } + protected function photos(): Attribute { return Attribute::make( diff --git a/routes/api.php b/routes/api.php index deae90a..9d57a19 100644 --- a/routes/api.php +++ b/routes/api.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Api\FeedbackController; use App\Http\Controllers\Api\KeywordController; use App\Http\Controllers\Api\Ledger\LedgerController; use App\Http\Controllers\Api\Ledger\LotteryTypeController; +use App\Http\Controllers\Api\ReimbursementController; use App\Http\Controllers\Api\StatsController; use Illuminate\Support\Facades\Route; @@ -36,6 +37,9 @@ Route::group([ Route::get('/ledger/ledgers/{date}', [LedgerController::class, 'show']); Route::get('/ledger/lottery-types', [LotteryTypeController::class, 'index']); + // 报销管理 + Route::apiResource('reimbursements', ReimbursementController::class); + // 举报投诉 Route::post('complaints', [ComplaintController::class, 'store']); // 意见箱 From 9b4e798fee868695dd6839f4263687799c9e1e44 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Sat, 13 Apr 2024 19:42:54 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Api/FileUploadController.php | 57 +++++++++++++++++++ routes/api.php | 3 + 2 files changed, 60 insertions(+) create mode 100644 app/Http/Controllers/Api/FileUploadController.php diff --git a/app/Http/Controllers/Api/FileUploadController.php b/app/Http/Controllers/Api/FileUploadController.php new file mode 100644 index 0000000..c33250d --- /dev/null +++ b/app/Http/Controllers/Api/FileUploadController.php @@ -0,0 +1,57 @@ +validate( + rules: [ + 'file' => [ + 'bail', + 'required', + File::types(['image/jpeg', 'image/png']) + ->extensions(['jpg', 'jpeg', 'png']) + ->max(20 * 1024), + ], + ], + attributes: [ + 'file' => '文件', + ], + ); + + /** @var \Illuminate\Http\UploadedFile */ + $file = $request->file('file'); + + if ($path = $file->storeAs(date('Ymd'), $this->filename($file))) { + return [ + 'url' => Storage::url($path), + ]; + } + + throw new RuntimeException('上传失败,请重试'); + } + + protected function filename(UploadedFile $file): string + { + $hash = Str::random(40); + + $extension = ''; + + if ($originalExtension = $file->getClientOriginalExtension()) { + $extension = '.'.$originalExtension; + } elseif ($guessExtension = $this->guessExtension()) { + $extension = '.'.$guessExtension; + } + + return $hash.$extension; + } +} diff --git a/routes/api.php b/routes/api.php index 9d57a19..45df734 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Api\Account\StoreMasterCommissionController; use App\Http\Controllers\Api\Auth\AccessTokenController; use App\Http\Controllers\Api\ComplaintController; use App\Http\Controllers\Api\FeedbackController; +use App\Http\Controllers\Api\FileUploadController; use App\Http\Controllers\Api\KeywordController; use App\Http\Controllers\Api\Ledger\LedgerController; use App\Http\Controllers\Api\Ledger\LotteryTypeController; @@ -19,6 +20,8 @@ Route::group([ ], function () { // 字典表 Route::get('keywords', [KeywordController::class, 'index']); + // 文件上传 + Route::post('fileupload', FileUploadController::class); // 当前账户信息 Route::get('auth/profile', [\App\Http\Controllers\Api\Auth\UserController::class, 'profile']);