4
0
Fork 0

add origin

master
panliang 2022-10-12 11:15:29 +08:00
parent bc58992d3a
commit 68161a1e55
17 changed files with 563 additions and 29 deletions

View File

@ -32,6 +32,7 @@ return new class extends Migration
$table->unsignedInteger('pay_status')->default(0)->comment('支付状态');
$table->unsignedInteger('refund_status')->default(0)->comment('退款状态');
$table->decimal('refund_money', 12, 2)->default(0)->comment('已退款金额');
$table->unsignedInteger('ship_status')->default(0)->comment('配送状态');
$table->string('ship_way')->nullable()->comment('配送方式');

View File

@ -0,0 +1,43 @@
<?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('order_refunds', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('order_id')->comment('订单ID, orders.id');
$table->string('sn')->comment('退款流水号');
$table->unsignedInteger('refund_status')->default(0)->comment('退款状态');
$table->string('refund_way')->nullable()->comment('退款方式');
$table->decimal('refund_money', 12, 2)->nullable()->comment('退款金额');
$table->dateTime('finish_at')->nullable()->comment('完成时间');
$table->string('refund_reason')->nullable()->comment('退款原因');
$table->json('refund_data')->nullable()->comment('退款备注');
$table->timestamps();
$table->comment('订单退款');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('order_refunds');
}
};

View File

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

View File

@ -0,0 +1,22 @@
<?php
return [
'labels' => [
'Refund' => '退款申请',
'order-refunds' => '退款申请',
],
'fields' => [
'order_id' => '订单',
'order' => [
'sn' => '订单号'
],
'sn' => '退款单号',
'refund_status' => '退款状态',
'refund_way' => '退款方式',
'refund_money' => '退款金额',
'finish_at' => '完成时间',
'refund_reason' => '原因',
'refund_data' => '其他',
'created_at' => '申请时间',
]
];

View File

@ -9,4 +9,5 @@ Route::group([
'middleware' => config('admin.route.middleware'),
], function () {
Route::resource('orders', OrderController::class)->only(['index', 'show', 'destroy'])->names('dcat.admin.order');
Route::resource('order-refunds', RefundController::class)->only(['index', 'show', 'edit', 'update'])->names('dcat.admin.order_refunds');
});

View File

@ -0,0 +1,37 @@
<?php
namespace Peidikeji\Order\Action;
use Dcat\Admin\Show\AbstractTool;
use Dcat\Admin\Widgets\Modal;
use Peidikeji\Order\Form\RefundForm;
class ShowRefund extends AbstractTool
{
protected $style = 'btn btn-sm btn-danger';
protected $title = '申请退款';
public function html()
{
$model = $this->parent->model();
return Modal::make()
->lg()
->title($this->title)
->body(RefundForm::make()->payload(['id' => $model->id, 'pay_money' => $model->pay_money, 'refund_money' => $model->refund_money]))
->button('<button type="button" class="'.$this->style.'">'.$this->title.'</button>');
}
protected function authorize($user): bool
{
return $user->can('dcat.admin.orders.refund');
}
public function allowed()
{
$model = $this->parent->model();
return $model->canRefund();
}
}

View File

@ -15,6 +15,10 @@ enum OrderStatus: int
case PayFail = 7;
case Cancel = 8;
case Refunding = 9;
case Refund = 10;
case RefundFull = 11;
public static function options()
{
return [
@ -31,6 +35,10 @@ enum OrderStatus: int
self::PayFail->value => '支付失败',
self::Cancel->value => '已取消',
self::Refunding->value => '退款中',
self::Refund->value => '已退款(部分)',
self::RefundFull->value => '已退款(全额)',
];
}

View File

@ -17,7 +17,7 @@ enum RefundStatus: int
{
return [
self::None->value => '未申请',
self::Apply->value => '已申请',
self::Apply->value => '待审核',
self::Processing->value => '处理中',
self::Success->value => '退款成功',
self::Fail->value => '退款失败',

View File

@ -0,0 +1,48 @@
<?php
namespace Peidikeji\Order\Enums;
enum RefundWay: int
{
case Offline = 0;
case Origin = 1;
public static function options()
{
return [
self::Offline->value => '线下',
self::Origin->value => '原路退回',
];
}
public function text()
{
return data_get(self::options(), $this->value);
}
public function color()
{
return match ($this) {
static::Offline => 'warning',
static::Origin => 'success',
};
}
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,66 @@
<?php
namespace Peidikeji\Order\Form;
use Dcat\Admin\Admin;
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\Order\Enums\RefundWay;
use Peidikeji\Order\Exceptions\OrderException;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\RefundService;
class RefundForm extends Form implements LazyRenderable
{
use LazyWidget;
protected $buttons = ['reset' => false, 'submit' => true];
public function handle(array $input)
{
try {
DB::beginTransaction();
$order = Order::findOrFail($this->payload['id']);
$money = $input['refund_money'];
$attributes = Arr::only($input, ['refund_reason', 'refund_way', 'refund_money', 'refund_sn']);
$info = RefundService::make()->apply($order, $attributes);
$admin = Admin::user();
$attributes['refund_id'] = $info->id;
$order->options()->create([
'user_type' => $admin->getMorphClass(),
'user_id' => $admin->id,
'description' => '管理员: '.$admin->name.' 申请退款: ' . $money,
'attribute' => $attributes,
]);
DB::commit();
return $this->response()->success('申请成功, 等待审核')->refresh();
} catch (OrderException $e) {
DB::rollBack();
return $this->response()->error($e->getMessage());
}
}
public function form()
{
// 支付金额
$payMoney = $this->payload['pay_money'];
// 已退款金额
$refundedMoney = $this->payload['refund_money'];
$this->text('refund_reason', __('dcat-admin-order::refund.fields.refund_reason'))->required();
$this->select('refund_way', __('dcat-admin-order::refund.fields.refund_way'))->options(RefundWay::options())->required();
$this->currency('refund_money', __('dcat-admin-order::refund.fields.refund_money'))->symbol('¥')->help('最大值: ' . floatval($payMoney - $refundedMoney));
$this->text('refund_sn', __('dcat-admin-order::refund.fields.sn'));
}
public function default()
{
return [
'refund_way' => RefundWay::Origin,
'refund_money' => floatval($this->payload['pay_money'] - $this->payload['refund_money'])
];
}
}

View File

@ -15,14 +15,6 @@ use Dcat\Admin\Show;
use Dcat\Admin\Show\Tools;
use Dcat\Admin\Widgets\Box;
use Illuminate\Support\Arr;
use Peidikeji\Order\Action\ShowCancel;
use Peidikeji\Order\Action\ShowDelete;
use Peidikeji\Order\Action\ShowPay;
use Peidikeji\Order\Action\ShowPayQuery;
use Peidikeji\Order\Action\ShowPrice;
use Peidikeji\Order\Action\ShowReceive;
use Peidikeji\Order\Action\ShowRemarks;
use Peidikeji\Order\Action\ShowShip;
use Peidikeji\Order\Enums\OrderStatus;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Models\Order;
@ -44,9 +36,9 @@ class OrderController extends AdminController
$grid->filter(function (Filter $filter) {
$filter->panel();
$filter->like('sn')->width(4);
$filter->equal('user_id')->select()->ajax('api/user?_paginate=1')->model(User::class, 'id', 'phone')->width(4);
$filter->where('order_status', fn($q) => $q->filter(['status' => $this->input]))->select(OrderStatus::options())->width(4);
$filter->like('sn')->width(3);
$filter->equal('user_id')->select()->ajax('api/user?_paginate=1')->model(User::class, 'id', 'phone')->width(3);
$filter->where('order_status', fn($q) => $q->filter(['status' => $this->input]))->select(OrderStatus::options())->width(3);
$filter->whereBetween('created_at', function ($q) {
$start = data_get($this->input, 'start');
$start = $start ? Carbon::createFromFormat('Y-m-d', $start) : null;
@ -60,7 +52,7 @@ class OrderController extends AdminController
} else if ($end) {
$q->where('created_at', '<=', $end);
}
})->date()->width(4);
})->date()->width(6);
});
$grid->column('sn')->copyable();
@ -128,14 +120,15 @@ class OrderController extends AdminController
$show->width(6)->field('remarks');
});
$show->tools(function (Tools $tools) {
$tools->append(new ShowCancel());
$tools->append(new ShowPay());
$tools->append(new ShowPayQuery());
$tools->append(new ShowShip());
$tools->append(new ShowReceive());
$tools->append(new ShowDelete());
$tools->append(new ShowPrice());
$tools->append(new ShowRemarks());
$tools->append(new \Peidikeji\Order\Action\ShowCancel());
$tools->append(new \Peidikeji\Order\Action\ShowPay());
$tools->append(new \Peidikeji\Order\Action\ShowPayQuery());
$tools->append(new \Peidikeji\Order\Action\ShowShip());
$tools->append(new \Peidikeji\Order\Action\ShowReceive());
$tools->append(new \Peidikeji\Order\Action\ShowDelete());
$tools->append(new \Peidikeji\Order\Action\ShowPrice());
$tools->append(new \Peidikeji\Order\Action\ShowRefund());
$tools->append(new \Peidikeji\Order\Action\ShowRemarks());
});
}));

View File

@ -0,0 +1,101 @@
<?php
namespace Peidikeji\Order\Http\Admin;
use Carbon\Carbon;
use Dcat\Admin\Admin;
use Dcat\Admin\Grid;
use Dcat\Admin\Grid\Filter;
use Dcat\Admin\Http\Controllers\AdminController;
use Dcat\Admin\Show;
use Dcat\Admin\Form;
use Dcat\Admin\Grid\Displayers\Actions;
use Peidikeji\Order\Enums\RefundStatus;
use Peidikeji\Order\Enums\RefundWay;
use Peidikeji\Order\Models\OrderRefund;
class RefundController extends AdminController
{
protected $translation = 'dcat-admin-order::refund';
protected function grid()
{
return Grid::make(OrderRefund::with(['order']), function (Grid $grid) {
$grid->model()->sort();
$grid->column('order.sn');
$grid->column('sn');
$grid->column('refund_reason');
$grid->column('refund_money');
$grid->column('refund_status')->display(fn() => $this->refund_status->label());
$grid->column('created_at');
$grid->filter(function (Filter $filter) {
$filter->panel();
$filter->like('order.sn')->width(3);
$filter->like('sn')->width(3);
$filter->equal('refund_status')->select(RefundStatus::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->disableRowSelector();
$grid->disableCreateButton();
$grid->disableDeleteButton();
$user = Admin::user();
$grid->actions(function (Actions $actions) use ($user) {
$row = $actions->row;
$actions->edit($row->refund_status === RefundStatus::Apply && $user->can('dcat.admin.order_refunds.edit'));
});
});
}
protected function detail($id)
{
return Show::make($id, OrderRefund::with(['order']), function (Show $show) {
$show->field('order.sn');
$show->field('sn');
$show->field('refund_reason');
$show->field('refund_money');
$show->field('refund_way')->as(fn() => $this->refund_way->label())->unescape();
$show->field('refund_status')->as(fn() => $this->refund_status->label())->unescape();
$show->field('finish_at');
$show->field('created_at');
$show->field('refund_data')->json();
$show->disableDeleteButton();
$show->disableEditButton();
});
}
protected function form()
{
return Form::make(OrderRefund::with(['order']), function (Form $form) {
$form->display('order.sn');
$form->text('sn')->required();
$form->select('refund_way')->options(RefundWay::options())->required();
$form->currency('refund_money')->symbol('¥')->required();
$form->text('refund_reason')->required();
$form->disableEditingCheck();
$form->disableCreatingCheck();
$form->disableViewCheck();
$form->disableViewButton();
$form->disableDeleteButton();
});
}
}

View File

@ -42,8 +42,6 @@ class OrderController extends Controller
$request->validate([
'goods' => [Rule::requiredIf(!$request->filled('cart_id')), 'array'],
'cart_id' => ['array'],
'address' => [Rule::requiredIf(!$request->filled('address_id')), 'array'],
'address_id' => ['numeric']
]);
$user = auth('api')->user();
@ -107,8 +105,7 @@ class OrderController extends Controller
$request->validate([
'goods' => [Rule::requiredIf(!$request->filled('cart_id')), 'array'],
'cart_id' => ['array'],
'address' => [Rule::requiredIf(!$request->filled('address_id')), 'array'],
'cart_id' => ['array']
]);
$store = OrderStore::init()->user($user)->remarks($request->input('remarks'));
@ -118,8 +115,8 @@ class OrderController extends Controller
}
$store->goods($goods);
$address = $this->getAddress();
$store->ship($request->input('ship_way', ShipWay::None->value), $address);
$shipWay = $request->input('ship_way', ShipWay::None->value);
$store->ship($shipWay, $shipWay === ShipWay::Express ? $this->getAddress() : null);
$store->score($request->input('score', 0));

View File

@ -25,7 +25,8 @@ class Order extends Model
'ship_address', 'ship_at', 'ship_money', 'ship_status', 'ship_way', 'receive_at',
'score_discount_amount', 'score_discount_money', 'score_discount_ratio',
'pay_at', 'pay_money', 'pay_no', 'pay_status', 'pay_way', 'discount_price',
'auto_close_at', 'extra', 'is_closed', 'refund_status', 'remarks', 'review_at', 'user_remarks'
'refund_status', 'refund_money',
'auto_close_at', 'extra', 'is_closed', 'remarks', 'review_at', 'user_remarks'
];
protected $casts = [
@ -67,6 +68,11 @@ class Order extends Model
return $this->hasMany(OrderOption::class, 'order_id');
}
public function refunds()
{
return $this->hasMany(OrderRefund::class, 'order_id');
}
public function status()
{
if ($this->is_closed) {
@ -82,6 +88,12 @@ class Order extends Model
return OrderStatus::PayFail;
}
if ($this->pay_status === PayStatus::Success) {
if ($this->refund_status === RefundStatus::Apply || $this->refund_status === RefundStatus::Processing) {
return OrderStatus::Refunding;
}
if ($this->refund_status === RefundStatus::Success) {
return $this->refund_money >= $this->pay_money ? OrderStatus::RefundFull : OrderStatus::Refund;
}
if ($this->ship_status === ShipStatus::Processing) {
return OrderStatus::Send;
}
@ -203,6 +215,35 @@ class Order extends Model
return true;
}
public function canRefund($throw = false)
{
if ($this->pay_status === PayStatus::None || $this->pay_status === PayStatus::Processing) {
if ($throw) {
throw new OrderException('订单未支付, 无法申请退款');
}
return false;
}
if ($this->pay_status === PayStatus::Refund) {
if ($throw) {
throw new OrderException('订单已经退款');
}
return false;
}
if ($this->refund_status === RefundStatus::Apply || $this->refund_status === RefundStatus::Processing) {
if ($throw) {
throw new OrderException('退款申请处理中');
}
return false;
}
if ($this->refund_money >= $this->pay_money) {
if ($throw) {
throw new OrderException('订单已经全额退款, 不能再申请退款');
}
return false;
}
return true;
}
public function scopeSort($q)
{
return $q->orderBy('created_at', 'desc');

View File

@ -0,0 +1,37 @@
<?php
namespace Peidikeji\Order\Models;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Model;
use Peidikeji\Order\Enums\RefundStatus;
use Peidikeji\Order\Enums\RefundWay;
class OrderRefund extends Model
{
use HasDateTimeFormatter;
protected $fillable = ['order_id', 'sn', 'refund_status', 'refund_way', 'refund_money', 'finish_at', 'refund_reason', 'refund_data'];
protected $casts = [
'refund_data' => 'json',
'refund_status' => RefundStatus::class,
'refund_way' => RefundWay::class,
];
protected $dates = ['finish_at'];
protected $attributes = [
'refund_status' => RefundStatus::None,
];
public function order()
{
return $this->belongsTo(Order::class, 'order_id');
}
public function scopeSort($q)
{
return $q->orderBy('created_at', 'desc');
}
}

View File

@ -52,7 +52,8 @@ class OrderServiceProvider extends ServiceProvider
// ]],
$menu->add([
['id' => 1, 'parent_id' => 0, 'title' => '订单模块', 'icon' => 'feather icon-book', 'uri' => '', 'permission' => ['orders']],
['id' => 2, 'parent_id' => 1, 'title' => '订单管理', 'icon' => '', 'uri' => '/orders', 'permission' => 'orders.index']
['id' => 2, 'parent_id' => 1, 'title' => '订单管理', 'icon' => '', 'uri' => '/orders', 'permission' => 'orders.index'],
['id' => 3, 'parent_id' => 1, 'title' => '退款申请', 'icon' => '', 'uri' => '/order-refunds', 'permission' => 'order_refunds.index'],
]);
});
}

View File

@ -0,0 +1,133 @@
<?php
namespace Peidikeji\Order;
use Peidikeji\Order\Enums\RefundStatus;
use Peidikeji\Order\Enums\RefundWay;
use Peidikeji\Order\Exceptions\OrderException;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\Models\OrderRefund;
class RefundService
{
public static function make()
{
return new static();
}
/**
* 订单申请退款
*
* @param Order $order
* @param array $params [refund_reason, refund_way, refund_money, refund_sn]
* @return OrderRefund
* @throws OrderException
*/
public function apply(Order $order, $params = [])
{
$order->canRefund(true);
$money = data_get($params, 'refund_money', $order->pay_money - $order->refund_money);
if ($money + $order->refund_money > $order->pay_money) {
throw new OrderException('退款金额超过支付金额');
}
if ($order->refunds()->whereIn('refund_status', [RefundStatus::Apply, RefundStatus::Processing])->exists()) {
throw new OrderException('退款申请处理中, 请勿重复申请');
}
$orderService = OrderService::make();
$sn = $orderService->generateSn();
$way = data_get($params, 'way', RefundWay::Origin->value);
$info = $order->refunds()->create([
'sn' => $sn,
'refund_status' => RefundStatus::Apply,
'refund_way' => $way,
'refund_money' => $money,
'refund_reason' => data_get($params, 'refund_reason'),
]);
$order->update(['refund_status' => RefundStatus::Apply]);
return $info;
}
/**
* 处理退款申请
*
* @param Order $order
* @param bool $status 同意/不同意
* @param string $reason 不同意的原因
* @throws OrderException
*/
public function handle(OrderRefund $info, bool $status, $reason = '')
{
if ($info->refund_status !== RefundStatus::Apply && $info->refund_status !== RefundStatus::Fail) {
throw new OrderException('退款已经处理');
}
$info->refund_status = RefundStatus::Processing;
if ($status) {
if ($info->refund_way === RefundWay::Origin) {
$orderService = OrderService::make();
$result = $orderService->refundPay($info->order, $info->refund_money, $info->refund_reason, $info->sn);
if ($result) {
$this->success($info, $result);
} else {
$this->fail($info, $result['message']);
}
}
else if ($info->refund_way === RefundWay::Offline) {
$this->success($info);
}
} else {
$this->fail($info, $reason);
}
}
public function success(OrderRefund $info, array $params = null)
{
if ($info->refund_status !== RefundStatus::Processing) {
throw new OrderException('退款已经处理');
}
$info->update([
'refund_status' => RefundStatus::Success,
'finish_at' => data_get($params, 'finish_at', now()),
'refund_data' => $params
]);
$order = $info->order;
if ($order) {
$order->increment('refund_money', $info->refund_money, ['refund_status' => RefundStatus::Success]);
}
}
public function fail(OrderRefund $info, $reason = '')
{
if ($info->refund_status !== RefundStatus::Processing) {
throw new OrderException('退款已经处理');
}
$info->update([
'refund_status' => RefundStatus::Fail,
'finish_at' => now(),
'refund_data' => ['fail_reason' => $reason],
]);
$order = $info->order;
if ($order) {
$order->update(['refund_status' => RefundStatus::Fail]);
}
}
public function cancel(OrderRefund $info)
{
$info->update([
'refund_status' => RefundStatus::Cancel,
'finish_at' => now(),
]);
$order = $info->order;
if ($order) {
$order->update(['refund_status' => RefundStatus::Cancel]);
}
}
}