diff --git a/README.md b/README.md index 9eaa5da..32f4abf 100644 --- a/README.md +++ b/README.md @@ -106,3 +106,19 @@ $menus = [ | source_id | bigint | null | - | 来源(多态关联) | | created_at | timestamp | null | - | 创建时间 | | updated_at | timestamp | null | - | 更新时间 | + +## 收货地址: user_address + +| column | type | nullable | default | comment | +| - | - | - | - | - | +| id | bigint | not null | - | 主键 | +| user_id | bigint | not null | - | 外键关联 users.id | +| contact_name | varchar(191) | not null | - | 联系人 | +| phone | varchar(191) | not null | - | 电话 | +| address | varchar(191) | not null | - | 地址 | +| province_id | bigint | null | - | 所属地区 | +| city_id | bigint | null | - | 所属地区 | +| area_id | bigint | null | - | 所属地区 | +| is_default | int | not null | 0 | 默认地址 | +| created_at | timestamp | null | - | 创建时间 | +| updated_at | timestamp | null | - | 更新时间 | diff --git a/database/migrations/2022_10_09_091648_create_user_withdraw_table.php b/database/migrations/2022_10_09_091648_create_user_withdraw_table.php new file mode 100644 index 0000000..a942921 --- /dev/null +++ b/database/migrations/2022_10_09_091648_create_user_withdraw_table.php @@ -0,0 +1,40 @@ +id(); + $table->unsignedBigInteger('user_id')->comment('申请人'); + $table->decimal('amount', 12, 2)->comment('提现数量'); + $table->decimal('balance', 12, 2)->comment('剩余'); + $table->unsignedInteger('status')->default(0)->comment('状态'); + $table->json('payee')->nullable()->comment('收款账户'); + $table->json('payer')->nullable()->comment('打款账户'); + $table->string('reason')->nullable()->comment('原因'); + $table->string('remarks')->nullable()->comment('备注'); + $table->timestamp('finish_at')->nullable()->comment('完成时间'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_withdraw'); + } +}; diff --git a/lang/en/withdraw.php b/lang/en/withdraw.php new file mode 100644 index 0000000..ca5d8ed --- /dev/null +++ b/lang/en/withdraw.php @@ -0,0 +1,5 @@ + [ + 'withdraw' => '提现管理', + 'Withdraw' => '提现管理', + ], + 'fields' => [ + 'balance' => '余额', + 'amount' => '提现数量', + 'payee' => '收款账户', + 'payer' => '打款账户', + 'remarks' => '备注', + 'status' => '状态', + 'user_id' => '申请人', + 'user' => [ + 'phone' => '申请人' + ], + 'created_at' => '申请时间', + 'finish_at' => '完成时间', + 'reason' => '失败原因', + ] +]; diff --git a/routes/admin.php b/routes/admin.php index 58078ff..a3d5fdb 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -12,4 +12,6 @@ Route::group([ Route::resource('users', UserController::class)->names('dcat.admin.users'); Route::resource('user-balance', UserBalanceController::class)->names('dcat.admin.user_balance'); + + Route::resource('withdraw', WithdrawController::class)->names('dcat.admin.withdraw')->only(['index', 'show']); }); diff --git a/routes/api.php b/routes/api.php index 50e497e..bfaf550 100644 --- a/routes/api.php +++ b/routes/api.php @@ -26,5 +26,8 @@ Route::group([ Route::get('address/default', [AddressController::class, 'default']); Route::apiResource('address', AddressController::class); + + Route::post('withdraw/{id}/cancel', [WithdrawController::class, 'cancel']); + Route::apiResource('withdraw', WithdrawController::class)->only(['index', 'show', 'store']); }); }); diff --git a/src/Action/Withdraw/BatchCheck.php b/src/Action/Withdraw/BatchCheck.php new file mode 100644 index 0000000..016a2a5 --- /dev/null +++ b/src/Action/Withdraw/BatchCheck.php @@ -0,0 +1,34 @@ +payload(['remarks' => '']); + return Modal::make() + ->lg() + ->title($this->title) + ->body($form) + ->onLoad($this->getModalScript()) + ->button($this->title); + } + + protected function getModalScript() + { + // 弹窗显示后往隐藏的id表单中写入批量选中的行ID + return <<getSelectedKeysScript()} + + $('#batch-check-id').val(key); + JS; + } +} diff --git a/src/Action/Withdraw/RowCheck.php b/src/Action/Withdraw/RowCheck.php new file mode 100644 index 0000000..2c4cd5c --- /dev/null +++ b/src/Action/Withdraw/RowCheck.php @@ -0,0 +1,23 @@ +row(); + $form = CheckForm::make()->payload(['remarks' => $model->remarks ?: '', 'id' => $model->id]); + return Modal::make() + ->lg() + ->title($this->title) + ->body($form) + ->button($this->title); + } +} diff --git a/src/Enums/WithdrawStatus.php b/src/Enums/WithdrawStatus.php new file mode 100644 index 0000000..ee5cdcd --- /dev/null +++ b/src/Enums/WithdrawStatus.php @@ -0,0 +1,58 @@ +value => '待处理', + self::Processing->value => '处理中', + self::Success->value => '成功', + self::Fail->value => '失败', + self::Cancel->value => '已取消', + ]; + } + + public function color() + { + return match ($this) { + static::None => 'warning', + static::Processing => 'secondary', + static::Success => 'success', + static::Fail => 'danger', + static::Cancel => 'secondary', + default => 'dark', + }; + } + + public function text() + { + return data_get(self::options(), $this->value); + } + + public function label() + { + $color = $this->color(); + + $name = $this->text(); + + return "{$name}"; + } + + public function dot() + { + $color = $this->color(); + + $name = $this->text(); + + return "  {$name}"; + } +} diff --git a/src/Exceptions/WitdrawException.php b/src/Exceptions/WitdrawException.php new file mode 100644 index 0000000..ba14416 --- /dev/null +++ b/src/Exceptions/WitdrawException.php @@ -0,0 +1,23 @@ +error($this->message, $this->code); + } +} diff --git a/src/Filters/WithdrawFilter.php b/src/Filters/WithdrawFilter.php new file mode 100644 index 0000000..aa8f22e --- /dev/null +++ b/src/Filters/WithdrawFilter.php @@ -0,0 +1,31 @@ +where('user_id', $v); + } + + public function status($v) + { + $this->whereIn('status', is_array($v) ? $v : explode(',', $v)); + } + + public function createdStart($v) + { + $time = Carbon::createFromTimestamp(strtotime($v)); + $this->where('created_at', '>=', $time); + } + + public function createdEnd($v) + { + $time = Carbon::createFromTimestamp(strtotime($v)); + $this->where('created_at', '<=', $time); + } +} diff --git a/src/Form/Withdraw/CheckForm.php b/src/Form/Withdraw/CheckForm.php new file mode 100644 index 0000000..b3bc462 --- /dev/null +++ b/src/Form/Withdraw/CheckForm.php @@ -0,0 +1,62 @@ + false, 'submit' => true]; + + public function handle(array $input) + { + $ids = $input['id'] ? explode(',', $input['id']) : null; + if (!$ids) { + $ids = [$this->payload['id']]; + } + + try { + DB::beginTransaction(); + foreach(UserWithdraw::where('status', WithdrawStatus::None)->get() as $info) { + if ($input['status'] == WithdrawStatus::Success->value) { + WithdrawService::make()->success($info); + } else { + WithdrawService::make()->fail($info, ['reason' => $input['reason']]); + } + $info->update(['remarks' => $input['remarks']]); + } + DB::commit(); + return $this->response()->success('操作成功')->refresh(); + } catch (\Exception $e) { + DB::rollBack(); + + return $this->response()->error($e->getMessage()); + } + } + + public function form() + { + $options = WithdrawStatus::options(); + $this->radio('status')->options(Arr::only($options, [WithdrawStatus::Success->value, WithdrawStatus::Fail->value]))->required(); + $this->text('reason'); + $this->text('remarks'); + $this->hidden('id')->attribute('id', 'batch-check-id'); + } + + public function default() + { + return [ + 'remarks' => $this->payload['remarks'], + 'status' => WithdrawStatus::Success->value + ]; + } +} diff --git a/src/Http/Admin/WithdrawController.php b/src/Http/Admin/WithdrawController.php new file mode 100644 index 0000000..5e18b38 --- /dev/null +++ b/src/Http/Admin/WithdrawController.php @@ -0,0 +1,94 @@ +model()->sort(); + + $grid->rowSelector()->disable(function ($row) { + return $row->status !== WithdrawStatus::None; + }); + $grid->column('user.phone'); + $grid->column('amount'); + $grid->column('status')->display(fn() => $this->status->label()); + $grid->column('created_at'); + + $grid->filter(function (Filter $filter) { + $filter->panel(); + $filter->equal('user_id')->select()->ajax('api/users?_paginate=1')->width(3); + $filter->equal('status')->select(WithdrawStatus::options())->width(3); + $filter->whereBetween('created_at', function ($q) { + $start = data_get($this->input, 'start'); + $start = $start ? Carbon::createFromFormat('Y-m-d', $start) : null; + $end = data_get($this->input, 'end'); + $end = $end ? Carbon::createFromFormat('Y-m-d', $end) : null; + if ($start) { + if ($end) { + $q->whereBetween('created_at', [$start, $end]); + } + $q->where('created_at', '>=', $start); + } else if ($end) { + $q->where('created_at', '<=', $end); + } + })->date()->width(6); + }); + + $grid->showRowSelector(); + $grid->disableCreateButton(); + $grid->disableDeleteButton(); + $grid->disableEditButton(); + $grid->disableBatchDelete(); + + $user = Admin::user(); + $grid->actions(function (Actions $actions) use ($user) { + $row = $actions->row; + if ($user->can('dcat.admin.withdraw.check') && $row->status === WithdrawStatus::None) { + $actions->append(new RowCheck()); + } + }); + + $grid->batchActions(function (BatchActions $actions) use ($user) { + if ($user->can('dcat.admin.withdraw.check')) { + $actions->add(new BatchCheck()); + } + }); + }); + } + + protected function detail($id) + { + return Show::make($id, UserWithdraw::with(['user']), function (Show $show) { + + $show->field('user_id')->as(fn() => $this->user?->phone); + $show->field('amount'); + $show->field('balance'); + $show->field('status')->as(fn() => $this->status->label())->unescape(); + $show->field('created_at'); + $show->field('finish_at'); + $show->field('reason'); + $show->field('remarks'); + + $show->disableEditButton(); + $show->disableDeleteButton(); + }); + } +} diff --git a/src/Http/Api/WithdrawController.php b/src/Http/Api/WithdrawController.php new file mode 100644 index 0000000..6f5ecd5 --- /dev/null +++ b/src/Http/Api/WithdrawController.php @@ -0,0 +1,74 @@ +user(); + + $query = $user->withdraws()->filter($request->all()); + + $list = $query->paginate($request->input('per_page')); + + return $this->json(WithdrawResource::collection($list)); + } + + public function show($id) + { + $user = auth('api')->user(); + $info = $user->withdraws()->findOrFail($id); + + return $this->json(WithdrawResource::make($info)); + } + + public function store(Request $request) + { + $user = auth('api')->user(); + $request->validate([ + 'amount' => 'numeric|gt:0|lte:' . floatval($user->balance) + ], [ + 'amount.gt' => '提现数量必须大于 0', + 'amount.lte' => '余额不足' + ]); + $amount = $request->input('amount'); + + try { + DB::beginTransaction(); + + $info = WithdrawService::make()->apply($user, $amount); + DB::commit(); + return $this->success('申请成功', WithdrawResource::make($info)); + } catch (\Exception $e) { + DB::rollBack(); + + return $this->error($e->getMessage()); + } + + } + + public function cancel($id) + { + $user = auth('api')->user(); + $info = $user->withdraws()->findOrFail($id); + + try { + DB::beginTransaction(); + + WithdrawService::make()->cancel($info); + DB::commit(); + return $this->success('取消成功'); + } catch (\Exception $e) { + DB::rollBack(); + + return $this->error($e->getMessage()); + } + } +} diff --git a/src/Http/Resources/WithdrawResource.php b/src/Http/Resources/WithdrawResource.php new file mode 100644 index 0000000..deb8567 --- /dev/null +++ b/src/Http/Resources/WithdrawResource.php @@ -0,0 +1,26 @@ + $this->id, + 'balance' => floatval($this->balance), + 'amount' => floatval($this->amount), + 'payee' => $this->payee, + 'payer' => $this->payer, + 'remarks' => $this->remarks, + 'status' => $this->status, + 'status_text' => $this->status->text(), + 'user_id' => $this->id, + 'finish_at' => $this->finish_at?->timestamp, + 'reason' => $this->reason, + 'created_at' => $this->created_at?->timestamp, + ]; + } +} diff --git a/src/Models/User.php b/src/Models/User.php index be2a1f6..2388ea8 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -75,6 +75,11 @@ class User extends Authenticatable return $this->hasMany(UserCoupon::class, 'user_id'); } + public function withdraws() + { + return $this->hasMany(UserWithdraw::class, 'user_id'); + } + public function scopeSort($q) { return $q->latest('id'); diff --git a/src/Models/UserAddress.php b/src/Models/UserAddress.php index 637b07e..697934a 100644 --- a/src/Models/UserAddress.php +++ b/src/Models/UserAddress.php @@ -7,6 +7,8 @@ use Peidikeji\Region\Models\Region; class UserAddress extends Model { + protected $table = 'user_address'; + protected $fillable = [ 'user_id', 'contact_name', 'phone', 'address', 'province_id', 'city_id', 'area_id', 'is_default', ]; diff --git a/src/Models/UserWithdraw.php b/src/Models/UserWithdraw.php new file mode 100644 index 0000000..1314bc7 --- /dev/null +++ b/src/Models/UserWithdraw.php @@ -0,0 +1,46 @@ + WithdrawStatus::class, + 'payee' => 'json', + 'payer' => 'json', + ]; + + protected $dates = ['finish_at']; + + protected $attributes = [ + 'status' => WithdrawStatus::None + ]; + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function modelFilter() + { + return WithdrawFilter::class; + } + + public function scopeSort($q) + { + return $q->latest('created_at'); + } +} diff --git a/src/UserServiceProvider.php b/src/UserServiceProvider.php index 26a5392..55e701e 100644 --- a/src/UserServiceProvider.php +++ b/src/UserServiceProvider.php @@ -32,6 +32,7 @@ class UserServiceProvider extends ServiceProvider ['id' => 1, 'parent_id' => 0, 'title' => '用户模块', 'icon' => 'feather icon-user', 'uri' => ''], ['id' => 2, 'parent_id' => 1, 'title' => '用户管理', 'icon' => '', 'uri' => '/users'], ['id' => 3, 'parent_id' => 1, 'title' => '余额流水', 'icon' => '', 'uri' => '/user-balance'], + ['id' => 4, 'parent_id' => 1, 'title' => '提现管理', 'icon' => '', 'uri' => '/withdraw'], ]); }); } diff --git a/src/WithdrawService.php b/src/WithdrawService.php new file mode 100644 index 0000000..0a56f14 --- /dev/null +++ b/src/WithdrawService.php @@ -0,0 +1,115 @@ +decrement('balance', $amount); + $balance = $user->balance; + + // 添加申请记录 + $info = $user->withdraws()->create([ + 'balance' => $balance, + 'amount' => $amount, + 'payee' => data_get($data, 'payee') + ]); + + // 添加流水记录 + $user->balanceLogs()->create([ + 'user_name' => $user->phone, + 'amount' => 0-$amount, + 'balance' => $balance, + 'cate' => '提现', + 'description' => '申请提现, 扣除余额', + 'source_id' => $info->id, + 'source_type' => (new UserWithdraw())->getMorphClass() + ]); + + return $info; + } + + public function cancel(UserWithdraw $info) + { + if ($info->status !== WithdrawStatus::None) { + throw new WitdrawException('申请已处理, 无法取消'); + } + + $info->update(['status' => WithdrawStatus::Cancel]); + + $this->refund($info); + } + + public function refund(UserWithdraw $info) + { + $user = $info->user; + $amount = $info->amount; + // 退回余额 + $user->increment('balance', $amount); + $balance = $user->balance; + + $cate = match($info->status) { + WithdrawStatus::Cancel => '提现', + WithdrawStatus::Fail => '提现', + default => '提现' + }; + $description = match($info->status) { + WithdrawStatus::Cancel => '取消提现申请, 退回余额', + WithdrawStatus::Fail => $info->reason, + default => '退回余额' + }; + + // 添加流水记录 + $user->balanceLogs()->create([ + 'user_name' => $user->phone, + 'amount' => $amount, + 'balance' => $balance, + 'cate' => $cate, + 'description' => $description, + 'source_id' => $info->id, + 'source_type' => $info->getMorphClass() + ]); + } + + public function success(UserWithdraw $info, $time = null) + { + if ($info->status !== WithdrawStatus::None) { + throw new WitdrawException('申请已处理'); + } + $info->update([ + 'status' => WithdrawStatus::Success, + 'finish_at' => $time ?: now(), + ]); + } + + /** + * 申请失败 + * + * @param UserWithdraw $info + * @param array $options 其他参数(reason) + */ + public function fail(UserWithdraw $info, $options = []) + { + if ($info->status !== WithdrawStatus::None) { + throw new WitdrawException('申请已处理'); + } + $info->update([ + 'status' => WithdrawStatus::Fail, + 'reason' => data_get($options, 'reason'), + 'finish_at' => data_get($options, 'finish_at', now()) + ]); + + $this->refund($info); + } +}