4
0
Fork 0
master
panliang 2022-10-09 14:31:22 +08:00
parent cf537656bd
commit bdce01ac8e
20 changed files with 683 additions and 0 deletions

View File

@ -106,3 +106,19 @@ $menus = [
| source_id | bigint | null | - | 来源(多态关联) | | source_id | bigint | null | - | 来源(多态关联) |
| created_at | timestamp | null | - | 创建时间 | | created_at | timestamp | null | - | 创建时间 |
| updated_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 | - | 更新时间 |

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_withdraw', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,5 @@
<?php
return [
];

View File

@ -0,0 +1,23 @@
<?php
return [
'labels' => [
'withdraw' => '提现管理',
'Withdraw' => '提现管理',
],
'fields' => [
'balance' => '余额',
'amount' => '提现数量',
'payee' => '收款账户',
'payer' => '打款账户',
'remarks' => '备注',
'status' => '状态',
'user_id' => '申请人',
'user' => [
'phone' => '申请人'
],
'created_at' => '申请时间',
'finish_at' => '完成时间',
'reason' => '失败原因',
]
];

View File

@ -12,4 +12,6 @@ Route::group([
Route::resource('users', UserController::class)->names('dcat.admin.users'); Route::resource('users', UserController::class)->names('dcat.admin.users');
Route::resource('user-balance', UserBalanceController::class)->names('dcat.admin.user_balance'); Route::resource('user-balance', UserBalanceController::class)->names('dcat.admin.user_balance');
Route::resource('withdraw', WithdrawController::class)->names('dcat.admin.withdraw')->only(['index', 'show']);
}); });

View File

@ -26,5 +26,8 @@ Route::group([
Route::get('address/default', [AddressController::class, 'default']); Route::get('address/default', [AddressController::class, 'default']);
Route::apiResource('address', AddressController::class); Route::apiResource('address', AddressController::class);
Route::post('withdraw/{id}/cancel', [WithdrawController::class, 'cancel']);
Route::apiResource('withdraw', WithdrawController::class)->only(['index', 'show', 'store']);
}); });
}); });

View File

@ -0,0 +1,34 @@
<?php
namespace Peidikeji\User\Action\Withdraw;
use Dcat\Admin\Grid\BatchAction;
use Dcat\Admin\Widgets\Modal;
use Peidikeji\User\Form\Withdraw\CheckForm;
class BatchCheck extends BatchAction
{
protected $title = '审核';
protected function html()
{
$form = CheckForm::make()->payload(['remarks' => '']);
return Modal::make()
->lg()
->title($this->title)
->body($form)
->onLoad($this->getModalScript())
->button($this->title);
}
protected function getModalScript()
{
// 弹窗显示后往隐藏的id表单中写入批量选中的行ID
return <<<JS
// 获取选中的ID数组
var key = {$this->getSelectedKeysScript()}
$('#batch-check-id').val(key);
JS;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Peidikeji\User\Action\Withdraw;
use Dcat\Admin\Grid\RowAction;
use Dcat\Admin\Widgets\Modal;
use Peidikeji\User\Form\Withdraw\CheckForm;
class RowCheck extends RowAction
{
protected $title = '审核';
protected function html()
{
$model = $this->row();
$form = CheckForm::make()->payload(['remarks' => $model->remarks ?: '', 'id' => $model->id]);
return Modal::make()
->lg()
->title($this->title)
->body($form)
->button($this->title);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Peidikeji\User\Enums;
enum WithdrawStatus: int
{
case None = 0;
case Processing = 1;
case Success = 2;
case Fail = 3;
case Cancel = 4;
public static function options()
{
return [
self::None->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 "<span class='label bg-${color}'>{$name}</span>";
}
public function dot()
{
$color = $this->color();
$name = $this->text();
return "<i class='fa fa-circle text-$color'>&nbsp;&nbsp;{$name}</span>";
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Peidikeji\User\Exceptions;
use Dcat\Admin\Traits\JsonResponse;
use Exception;
class WitdrawException extends Exception
{
use JsonResponse;
protected $code = 400;
public function report()
{
return false;
}
public function render($request)
{
return $this->error($this->message, $this->code);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Peidikeji\User\Filters;
use Carbon\Carbon;
use EloquentFilter\ModelFilter;
class WithdrawFilter extends ModelFilter
{
public function user($v)
{
$this->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);
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Peidikeji\User\Form\Withdraw;
use Dcat\Admin\Contracts\LazyRenderable;
use Dcat\Admin\Traits\LazyWidget;
use Dcat\Admin\Widgets\Form;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Peidikeji\User\Enums\WithdrawStatus;
use Peidikeji\User\Models\UserWithdraw;
use Peidikeji\User\WithdrawService;
class CheckForm extends Form implements LazyRenderable
{
use LazyWidget;
protected $buttons = ['reset' => 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
];
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Peidikeji\User\Http\Admin;
use Carbon\Carbon;
use Dcat\Admin\Admin;
use Dcat\Admin\Grid;
use Dcat\Admin\Grid\Displayers\Actions;
use Dcat\Admin\Grid\Filter;
use Dcat\Admin\Grid\Tools\BatchActions;
use Dcat\Admin\Show;
use Dcat\Admin\Http\Controllers\AdminController;
use Peidikeji\User\Action\Withdraw\BatchCheck;
use Peidikeji\User\Action\Withdraw\RowCheck;
use Peidikeji\User\Enums\WithdrawStatus;
use Peidikeji\User\Models\UserWithdraw;
class WithdrawController extends AdminController
{
protected $translation = 'dcat-admin-user::withdraw';
protected function grid()
{
return Grid::make(UserWithdraw::with(['user']), function (Grid $grid) {
$grid->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();
});
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace Peidikeji\User\Http\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Peidikeji\User\Http\Resources\WithdrawResource;
use Peidikeji\User\WithdrawService;
class WithdrawController extends Controller
{
public function index(Request $request)
{
$user = auth('api')->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());
}
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Peidikeji\User\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class WithdrawResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $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,
];
}
}

View File

@ -75,6 +75,11 @@ class User extends Authenticatable
return $this->hasMany(UserCoupon::class, 'user_id'); return $this->hasMany(UserCoupon::class, 'user_id');
} }
public function withdraws()
{
return $this->hasMany(UserWithdraw::class, 'user_id');
}
public function scopeSort($q) public function scopeSort($q)
{ {
return $q->latest('id'); return $q->latest('id');

View File

@ -7,6 +7,8 @@ use Peidikeji\Region\Models\Region;
class UserAddress extends Model class UserAddress extends Model
{ {
protected $table = 'user_address';
protected $fillable = [ protected $fillable = [
'user_id', 'contact_name', 'phone', 'address', 'province_id', 'city_id', 'area_id', 'is_default', 'user_id', 'contact_name', 'phone', 'address', 'province_id', 'city_id', 'area_id', 'is_default',
]; ];

View File

@ -0,0 +1,46 @@
<?php
namespace Peidikeji\User\Models;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;
use Peidikeji\User\Enums\WithdrawStatus;
use Peidikeji\User\Filters\WithdrawFilter;
class UserWithdraw extends Model
{
use HasDateTimeFormatter;
use Filterable;
protected $table = 'user_withdraw';
protected $fillable = ['balance', 'amount', 'payee', 'payer', 'remarks', 'status', 'user_id', 'finish_at', 'reason'];
protected $casts = [
'status' => 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');
}
}

View File

@ -32,6 +32,7 @@ class UserServiceProvider extends ServiceProvider
['id' => 1, 'parent_id' => 0, 'title' => '用户模块', 'icon' => 'feather icon-user', 'uri' => ''], ['id' => 1, 'parent_id' => 0, 'title' => '用户模块', 'icon' => 'feather icon-user', 'uri' => ''],
['id' => 2, 'parent_id' => 1, 'title' => '用户管理', 'icon' => '', 'uri' => '/users'], ['id' => 2, 'parent_id' => 1, 'title' => '用户管理', 'icon' => '', 'uri' => '/users'],
['id' => 3, 'parent_id' => 1, 'title' => '余额流水', 'icon' => '', 'uri' => '/user-balance'], ['id' => 3, 'parent_id' => 1, 'title' => '余额流水', 'icon' => '', 'uri' => '/user-balance'],
['id' => 4, 'parent_id' => 1, 'title' => '提现管理', 'icon' => '', 'uri' => '/withdraw'],
]); ]);
}); });
} }

View File

@ -0,0 +1,115 @@
<?php
namespace Peidikeji\User;
use Peidikeji\User\Enums\WithdrawStatus;
use Peidikeji\User\Exceptions\WitdrawException;
use Peidikeji\User\Models\UserWithdraw;
class WithdrawService
{
public static function make(): static
{
return new static();
}
public function apply($user, $amount, $data = [])
{
// 扣除余额
$user->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);
}
}