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/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/app/Http/Controllers/Api/KeywordController.php b/app/Http/Controllers/Api/KeywordController.php index 92dbf90..793ddf6 100644 --- a/app/Http/Controllers/Api/KeywordController.php +++ b/app/Http/Controllers/Api/KeywordController.php @@ -2,19 +2,16 @@ namespace App\Http\Controllers\Api; +use App\Http\Resources\KeywordResource; use App\Models\Keyword; use Illuminate\Http\Request; -use App\Http\Resources\KeywordResource; -/** - * 数据字典 - */ class KeywordController extends Controller { public function index(Request $request) { - $list = Keyword::filter($request->all())->sort()->get(); + $keywords = Keyword::filter($request->input())->get(); - return KeywordResource::collection($list); + 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/app/Http/Controllers/Api/ReimbursementController.php b/app/Http/Controllers/Api/ReimbursementController.php index b9ced3a..8b1582d 100644 --- a/app/Http/Controllers/Api/ReimbursementController.php +++ b/app/Http/Controllers/Api/ReimbursementController.php @@ -2,52 +2,157 @@ namespace App\Http\Controllers\Api; -use App\Models\{Reimbursement, WorkflowCheck, WorkflowLog}; -use Illuminate\Http\{Request, Response}; +use App\Admin\Services\WorkFlowService; +use App\Exceptions\RuntimeException; use App\Http\Resources\ReimbursementResource; +use App\Models\Keyword; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Illuminate\Validation\Rule; +use Throwable; -/** - * 报销管理 - */ class ReimbursementController extends Controller { - /** - * 申请记录 - */ public function index(Request $request) { - $user = $this->guard()->user(); - $query = Reimbursement::where('employee_id', $user->id)->filter($request->all())->sort(); - $list = $query->paginate($request->input('per_page')); + /** @var \App\Models\Employee */ + $user = $request->user(); - return ReimbursementResource::collection($list); - } - /** - * 添加申请 - */ - public function store(Request $request) - { - $user = $this->guard()->user(); - $request->valiodate([ - 'reimbursement_type_id' => 'required', - 'expense' => 'required', - ]); + $reimbursements = $user->reimbursements() + ->filter($request->input()) + ->latest('id') + ->simplePaginate($request->input('per_page', 20)); - $data = $request->all(); - $data['employee_id'] = $user->id; - $info = Reimbursement::create($data); - - return response('', Response::HTTP_OK); + return ReimbursementResource::collection( + $reimbursements->loadMissing(['type', 'workflow']), + ); } - /** - * 审核记录 - */ - public function checkList(Request $request) + public function store(Request $request, WorkFlowService $workFlowService): ReimbursementResource { - $user = $this->guard()->user(); - $store = $user->store; - $jobs = $user->jobs; - $query = WorkflowLog::with(['check'])->whereHas('check', fn($q) => $q->where('subject_type', (new Reimbursement)->getMorphClass())); + $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 index 988aad7..481f615 100644 --- a/app/Http/Resources/ReimbursementResource.php +++ b/app/Http/Resources/ReimbursementResource.php @@ -14,6 +14,15 @@ class ReimbursementResource extends JsonResource */ public function toArray(Request $request): array { - return parent::toArray($request); + 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 761df9f..39bb90b 100644 --- a/app/Models/Reimbursement.php +++ b/app/Models/Reimbursement.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\CheckStatus; use App\Traits\HasCheckable; use App\Traits\HasDateTimeFormatter; use EloquentFilter\Filterable; @@ -42,6 +43,16 @@ class Reimbursement extends Model return $this->belongsTo(Keyword::class, 'reimbursement_type_id', 'key'); } + 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 91a7162..5ac7dc2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,9 +4,11 @@ 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; -use App\Http\Controllers\Api\Reimbursement\ReimbursementTypeController; +use App\Http\Controllers\Api\ReimbursementController; use App\Http\Controllers\Api\StatsController; use Illuminate\Support\Facades\Route; @@ -19,6 +21,11 @@ Route::get('keyword', [\App\Http\Controllers\Api\KeywordController::class, 'inde Route::group([ 'middleware' => ['auth:api'], ], function () { + // 字典表 + Route::get('keywords', [KeywordController::class, 'index']); + // 文件上传 + Route::post('fileupload', FileUploadController::class); + // 当前账户信息 Route::get('auth/profile', [\App\Http\Controllers\Api\Auth\UserController::class, 'profile']); // 修改账户信息 @@ -37,9 +44,7 @@ Route::group([ Route::get('/ledger/lottery-types', [LotteryTypeController::class, 'index']); // 报销管理 - Route::group(['prefix' => 'reimbursement'], function () { - Route::get('reimbursement-types', [ReimbursementTypeController::class, 'index']); - }); + Route::apiResource('reimbursements', ReimbursementController::class); // 举报投诉 Route::post('complaints', [ComplaintController::class, 'store']);