4
0
Fork 0
master
panliang 2022-08-31 15:35:34 +08:00
commit eaef1d26a6
59 changed files with 3855 additions and 0 deletions

7
.gitignore vendored 100644
View File

@ -0,0 +1,7 @@
.DS_Store
phpunit.phar
/vendor
composer.phar
composer.lock
*.project
.idea/

26
README.md 100644
View File

@ -0,0 +1,26 @@
# Dcat Admin Extension
订单管理
## 安装
- `composer config repositories.peidikeji/dcat-admin-order git git@gitee.com:paddy_technology/dcat-admin-order.git`
- `composer require peidikeji/dcat-admin-order dev-develop`
- `php artisan vendor:publish --provider=Peidikeji\Order\OrderServiceProvider`
## 配置
### app\Providers\EventServiceProvider.php
```php
use Peidikeji\Order\Events\OrderCreated;
use Peidikeji\Order\Listeners\UpdateGoodsSoldCount;
use Peidikeji\Order\Listeners\UpdateGoodsStock;
protected $listen = [
OrderCreated::class => [
UpdateGoodsStock::class,
UpdateGoodsSoldCount::class,
]
];
```

33
composer.json 100644
View File

@ -0,0 +1,33 @@
{
"name": "peidikeji/dcat-admin-order",
"alias": "订单管理",
"description": "订单管理",
"type": "library",
"keywords": ["dcat-admin", "extension", "order", "shop"],
"homepage": "https://gitee.com/paddy_technology/dcat-admin-extension-order",
"license": "MIT",
"authors": [
{
"name": "panliang",
"email": "1163816051@qq.com"
}
],
"require": {
"php": ">=7.1.0",
"peidikeji/dcat-admin": "*",
"tucker-eric/eloquentfilter": "^3.1",
"laravel/framework": "^9.0"
},
"autoload": {
"psr-4": {
"Peidikeji\\Order\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Peidikeji\\Order\\OrderServiceProvider"
]
}
}
}

View File

@ -0,0 +1,112 @@
<?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('orders', function (Blueprint $table) {
$table->id('id');
$table->string('sn')->comment('订单号');
$table->unsignedBigInteger('merchant_id')->nullable()->comment('商户ID');
$table->unsignedBigInteger('user_id')->comment('下单用户, 关联 users.id');
$table->decimal('total_money', 12, 2)->default(0)->comment('总金额');
$table->decimal('score_discount_money')->default(0)->comment('积分抵扣金额');
$table->decimal('score_discount_amount')->default(0)->comment('使用积分数量');
$table->decimal('score_discount_ratio')->default(0)->comment('积分抵扣比例');
$table->decimal('pay_money', 12, 2)->default(0)->comment('支付金额');
$table->dateTime('pay_at')->nullable()->comment('支付时间');
$table->unsignedInteger('pay_way')->nullable()->comment('支付方式');
$table->string('pay_no')->nullable()->comment('支付平台流水号');
$table->unsignedInteger('pay_status')->default(0)->comment('支付状态');
$table->unsignedInteger('refund_status')->default(0)->comment('退款状态');
$table->unsignedInteger('ship_status')->default(0)->comment('配送状态');
$table->string('ship_way')->nullable()->comment('配送方式');
$table->decimal('ship_money', 12, 2)->default(0)->comment('配送费');
$table->json('ship_address')->nullable()->comment('配送地址');
$table->timestamp('ship_at')->nullable()->comment('发货时间');
$table->timestamp('receive_at')->nullable()->comment('收货时间');
$table->integer('is_closed')->default(0)->comment('订单是否关闭');
$table->timestamp('auto_close_at')->nullable()->comment('自动关闭时间');
$table->timestamp('review_at')->nullable()->comment('评价时间');
$table->string('remarks')->nullable()->comment('系统备注');
$table->string('user_remarks')->nullable()->comment('用户备注');
$table->text('extra')->nullable()->comment('其他');
$table->timestamps();
$table->comment('订单');
});
Schema::create('order_goods', function (Blueprint $table) {
$table->id('id');
$table->unsignedBigInteger('order_id')->comment('订单ID, orders.id');
$table->unsignedBigInteger('merchant_id')->nullable()->comment('商户ID');
$table->unsignedBigInteger('goods_id')->comment('所属商品, 关联 goods.id');
$table->string('goods_name');
$table->string('goods_sn')->nullable();
$table->string('cover_image')->nullable()->comment('封面图');
$table->decimal('price', 12, 2)->comment('售价');
$table->decimal('score_max_amount', 12, 2)->default(0)->comment('积分抵扣最大值');
$table->string('goods_sku_sn')->nullable();
$table->unsignedBigInteger('goods_sku_id')->nullable()->comment('所属商品SKU, 关联 goods_skus.id');
$table->json('attr')->nullable()->comment('属性');
$table->json('spec')->nullable()->comment('规格');
$table->json('part')->nullable()->comment('配件');
$table->unsignedInteger('amount')->default(1)->comment('数量');
$table->decimal('money', 12, 2)->comment('商品总价');
$table->unsignedInteger('ship_amount')->default(0)->comment('累计发货数量');
$table->unsignedBigInteger('shipping_tmp_id')->nullable()->comment('配送模板id');
$table->decimal('weight', 12, 2)->nullable()->comment('重量');
$table->decimal('volume', 12, 2)->nullable()->comment('体积');
$table->comment('订单-商品');
});
Schema::create('order_options', function (Blueprint $table) {
$table->id('id');
// 操作人(用户, 管理员)
$table->nullableMorphs('user');
$table->unsignedBigInteger('order_id')->comment('订单ID, orders.id');
$table->string('description')->nullable()->comment('描述');
$table->json('attribute')->comment('变更属性');
$table->timestamps();
$table->comment('订单操作记录');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('order_options');
Schema::dropIfExists('order_goods');
Schema::dropIfExists('orders');
}
};

View File

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

View File

@ -0,0 +1,44 @@
<?php
return [
'labels' => [
'Orders' => '订单管理',
'Order' => '订单管理',
'order' => '订单管理',
'orders' => '订单管理',
],
'fields' => [
'sn' => '订单号',
'user_id' => '用户',
'user' => [
'name' => '用户',
'phone' => '用户',
],
'merchant_id' => '店铺',
'merchant' => [
'name' => '店铺'
],
'total_money' => '订单金额',
'order_status' => '订单状态',
'created_at' => '下单时间',
'pay_status' => '支付状态',
'pay_no' => '流水号',
'pay_at' => '支付时间',
'pay_way' => '支付方式',
'pay_money' => '支付金额',
'remarks' => '系统备注',
'score_discount_money' => '积分金额',
'score_discount_amount' => '积分数量',
'score_discount_ratio' => '积分比例',
'vip_discount_money' => '会员优惠',
'ship_way' => '配送方式',
'ship_status' => '发货状态',
'ship_address' => '配送地址',
'ship_at' => '发货时间',
'user_remarks' => '用户备注',
'status' => '状态',
'user_get_profit' => '用户获得积分',
'scene' => '订单类别',
'ship_money' => '配送费',
]
];

12
routes/admin.php 100644
View File

@ -0,0 +1,12 @@
<?php
namespace Peidikeji\Order\Http\Admin;
use Illuminate\Support\Facades\Route;
Route::group([
'prefix' => config('admin.route.prefix'),
'middleware' => config('admin.route.middleware'),
], function () {
Route::resource('orders', OrderController::class)->only(['index', 'show', 'destroy'])->names('dcat.admin.order');
});

20
routes/api.php 100644
View File

@ -0,0 +1,20 @@
<?php
namespace Peidikeji\Order\Http\Api;
use Illuminate\Support\Facades\Route;
Route::any('api/order/notify', [WxPayNotifyController::class, 'pay']);
Route::group([
'prefix' => 'api',
'middleware' => ['api', 'auth:api'],
], function () {
Route::post('order/{id}/pay', [OrderController::class, 'pay']);
Route::post('order/{id}/cancel', [OrderController::class, 'cancel']);
Route::post('order/{id}/receive', [OrderController::class, 'receive']);
Route::get('order/{id}/ship', [OrderController::class, 'ships']);
Route::get('order/total', [OrderController::class, 'total']);
Route::post('order/sure', [OrderController::class, 'sure']);
Route::apiResource('order', OrderController::class);
});

View File

@ -0,0 +1,58 @@
<?php
namespace Peidikeji\Order\Action;
use Dcat\Admin\Admin;
use Dcat\Admin\Show\AbstractTool;
use Illuminate\Support\Facades\DB;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\OrderService;
class ShowCancel extends AbstractTool
{
protected $style = 'btn btn-sm btn-warning';
protected $title = '取消订单';
public function handle()
{
try {
DB::beginTransaction();
$order = Order::findOrFail($this->getKey());
OrderService::make()->cancel($order, 'admin');
$admin = Admin::user();
$order->options()->create([
'user_type' => get_class($admin),
'user_id' => $admin->id,
'description' => '管理员: ' . $admin->name . ' 取消订单',
'attribute' => [
'is_closed' => 1
],
]);
DB::commit();
return $this->response()->success('操作成功')->refresh();
} catch (\Exception $e) {
DB::rollBack();
return $this->response()->error($e->getMessage());
}
}
public function confirm()
{
$model = $this->parent->model();
return ['是否确定?', $model->pay_status === PayStatus::Success ? '同时返还支付金额' : ''];
}
protected function authorize($user): bool
{
return $user->can('dcat.admin.order.cancel');
}
public function allowed()
{
$model = $this->parent->model();
return $model->canCancel();
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Peidikeji\Order\Action;
use Dcat\Admin\Show\AbstractTool;
use Illuminate\Support\Facades\DB;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\OrderService;
class ShowDelete extends AbstractTool
{
protected $style = 'btn btn-sm btn-danger';
protected $title = '删除订单';
public function handle()
{
try {
DB::beginTransaction();
$order = Order::findOrFail($this->getKey());
OrderService::make()->delete($order);
DB::commit();
return $this->response()->success('操作成功')->redirect('orders');
} catch (\Exception $e) {
DB::rollBack();
return $this->response()->error($e->getMessage());
}
}
public function confirm()
{
return ['是否确定?', '删除后不可恢复'];
}
protected function authorize($user): bool
{
return $user->can('dcat.admin.order.destroy');
}
public function allowed()
{
$model = $this->parent->model();
return $model->canDelete();
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Peidikeji\Order\Action;
use Dcat\Admin\Widgets\Modal;
use Peidikeji\Order\Form\PayForm;
use Dcat\Admin\Show\AbstractTool;
class ShowPay 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(PayForm::make()->payload(['id' => $model->id, 'pay_money' => $model->pay_money]))
->button('<button type="button" class="'.$this->style.'">'.$this->title.'</button>');
}
protected function authorize($user): bool
{
return $user->can('dcat.admin.order.pay');
}
public function allowed()
{
$model = $this->parent->model();
return $model->canPay();
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Peidikeji\Order\Action;
use Carbon\Carbon;
use Dcat\Admin\Show\AbstractTool;
use Overtrue\LaravelWeChat\EasyWeChat;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Enums\PayWay;
use Peidikeji\Order\Enums\WxPayStatus;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\OrderService;
class ShowPayQuery extends AbstractTool
{
protected $style = 'btn btn-sm btn-primary';
protected $title = '获取支付结果';
public function handle()
{
$order = Order::findOrFail($this->getKey());
$payment = EasyWeChat::pay();
$client = $payment->getClient();
$response = $client->get('/v3/pay/transactions/out-trade-no/' . $order->sn, [
'query'=>[
'mchid' => $payment->getMerchant()->getMerchantId()
]
]);
$result = $response->toArray(false);
if (data_get($result, 'code')) {
return $this->response()->error(data_get($result, 'message'));
}
// 支付成功
$state = data_get($result, 'trade_state');
if ($state === 'SUCCESS') {
OrderService::make()->paySuccess($order, [
'pay_at' => Carbon::parse(data_get($result, 'success_time')),
'pay_no' => data_get($result, 'transaction_id')
]);
return $this->response()->success('订单支付成功')->refresh();
}
return $this->response()->warning(data_get(WxPayStatus::options(), $state, $state));
}
public function allowed()
{
$model = $this->parent->model();
return $model->pay_status === PayStatus::Processing && $model->pay_way === PayWay::WxMini;
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Peidikeji\Order\Action;
use Dcat\Admin\Admin;
use Dcat\Admin\Show\AbstractTool;
use Illuminate\Support\Facades\DB;
use Peidikeji\Order\Enums\ShipStatus;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\OrderService;
class ShowReceive extends AbstractTool
{
protected $style = 'btn btn-sm btn-primary';
protected $title = '确认收货';
public function handle()
{
try {
DB::beginTransaction();
$order = Order::findOrFail($this->getKey());
OrderService::make()->receive($order, true);
$admin = Admin::user();
$order->options()->create([
'user_type' => get_class($admin),
'user_id' => $admin->id,
'description' => '管理员: ' . $admin->name . ' 确认收货',
'attribute' => [
'ship_status' => ShipStatus::Received
],
]);
DB::commit();
return $this->response()->success('操作成功')->refresh();
} catch (\Exception $e) {
DB::rollBack();
return $this->response()->success($e->getMessage());
}
}
public function confirm()
{
return ['是否确定?', '所有发货单都已经签收'];
}
protected function authorize($user): bool
{
return $user->can('dcat.admin.order.receive');
}
public function allowed()
{
$model = $this->parent->model();
return $model->canReceive();
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Peidikeji\Order\Action;
use Dcat\Admin\Show\AbstractTool;
use Dcat\Admin\Widgets\Modal;
use Peidikeji\Order\Form\RemarksForm;
class ShowRemarks extends AbstractTool
{
protected $style = 'btn btn-sm btn-outline-primary';
protected $title = '设置备注';
public function html()
{
$model = $this->parent->model();
return Modal::make()
->lg()
->title($this->title)
->body(RemarksForm::make(['remarks' => $model->remarks])->payload(['id' => $model->id]))
->button('<button type="button" class="'.$this->style.'">'.$this->title.'</button>');
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Peidikeji\Order\Action;
use Dcat\Admin\Show\AbstractTool;
use Dcat\Admin\Widgets\Modal;
use Peidikeji\Order\Form\ShipForm;
class ShowShip extends AbstractTool
{
protected $style = 'btn btn-sm btn-primary';
protected $title = '订单发货';
public function html()
{
$model = $this->parent->model();
return Modal::make()
->xl()
->title($this->title())
->body(ShipForm::make()->payload(['id' => $model->id, 'ship_way' => $model->ship_way]))
->button('<button type="button" class="'.$this->style.'">'.$this->title.'</button>');
}
protected function authorize($user): bool
{
return $user->can('dcat.admin.order.ship');
}
public function allowed()
{
$model = $this->parent->model();
return $model->canShip();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Peidikeji\Order\Action;
use Dcat\Admin\Show\AbstractTool;
use Peidikeji\Order\Enums\OrderScene;
use Peidikeji\Order\Models\Order;
class ShowShipQrcode extends AbstractTool
{
protected $style = 'btn btn-sm btn-primary';
protected $title = '生成提货码';
public function handle()
{
$id = $this->getKey();
$order = Order::findOrFail($id);
$url = $order->generateShipQrcode();
if (!$url) {
return $this->response()->error('生成提货码失败');
}
return $this->response()->success('操作成功')->refresh();
}
public function allowed()
{
$model = $this->parent->model();
$code = data_get($model, 'extra.ship_qrcode');
return $model->scene === OrderScene::Merchant && !$code;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Peidikeji\Order\Console;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Enums\ShipStatus;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\OrderService;
use Peidikeji\Setting\Models\Setting;
class OrdeReceive extends Command
{
protected $signature = 'order:receive {id?}';
protected $description = '订单自动确认收货';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$id = $this->argument('id');
if ($id) {
$this->receive(Order::findOrFail($id));
} else {
$day = Setting::where('slug', 'order_receive_day')->value('value');
$query = Order::where('ship_status', ShipStatus::Finished)
->where('is_closed', 0)
->where('ship_at', '<', now()->subDays($day));
$list = $query->get();
foreach($list as $item) {
$this->receive($item);
}
}
}
protected function receive(Order $order)
{
try {
DB::beginTransaction();
OrderService::make()->receive($order);
$order->options()->create([
'description' => '系统自动确认收货',
'attribute' => [
'ship_status' => ShipStatus::Received
],
]);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
$this->error('系统自动确认收货失败: ' . $order->id);
$this->error($e->getMessage());
}
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Peidikeji\Order\Console;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\OrderService;
use Peidikeji\Setting\Models\Setting;
class OrderCancel extends Command
{
protected $signature = 'order:cancel {id?}';
protected $description = '自动取消订单';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$id = $this->argument('id');
if ($id) {
$this->cancel(Order::findOrFail($id));
} else {
$hour = Setting::where('slug', 'order_cancel_hour')->value('value');
$query = Order::where('pay_status', PayStatus::None)->where('is_closed', 0)->where('created_at', '<', now()->subHours($hour));
$list = $query->get();
foreach($list as $item) {
$this->cancel($item);
}
}
}
protected function cancel(Order $order)
{
try {
DB::beginTransaction();
OrderService::make()->cancel($order, 'admin');
$order->options()->create([
'description' => '订单超时, 自动取消',
'attribute' => [
'is_closed' => 1,
'auto_close_at' => now(),
],
]);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
$this->error('订单自动取消失败: ' . $order->id);
$this->error($e->getMessage());
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Peidikeji\Order\Enums;
enum OrderScene: int
{
case Online = 0;
case Merchant = 1;
case Scan = 2;
public static function options()
{
return [
self::Online->value => '线上商城',
self::Merchant->value => '线下门店',
self::Scan->value => '扫码付款',
];
}
public function text()
{
return data_get(self::options(), $this->value);
}
public function color()
{
return match ($this) {
static::Online => 'primary',
static::Merchant => 'warning',
static::Scan => '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,72 @@
<?php
namespace Peidikeji\Order\Enums;
/**
* 发货单物流状态
*/
enum OrderShipStatus: int
{
case Ontheway = 0;
case Wait = 1;
case Question = 2;
case Check = 3;
case Refund = 4;
case Distribute = 5;
case Refuse = 6;
case Other = 10;
case Autocheck = 11;
public static function options()
{
return [
static::Ontheway->value => '途中',
static::Wait->value => '揽收',
static::Question->value => '问题包裹',
static::Check->value => '签收',
static::Refund->value => '退签',
static::Distribute->value => '派送',
static::Refuse->value => '拒签',
static::Other->value => '其他',
static::Autocheck->value => '自动签收',
];
}
public function text()
{
return data_get(self::options(), $this->value);
}
public function color()
{
return match ($this) {
static::Wait => 'primary',
static::Ontheway => 'primary',
static::Distribute => 'primary',
static::Check => 'success',
static::Question => 'danger',
static::Refund => 'danger',
static::Refuse => 'danger',
static::Other => 'warning',
static::Autocheck => '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,75 @@
<?php
namespace Peidikeji\Order\Enums;
enum OrderStatus: int
{
case None = 0;
case Paying = 1;
case Paid = 2;
case Send = 3;
case SendFull = 4;
case Receive = 5;
case Review = 6;
case PayFail = 7;
case Cancel = 8;
public static function options()
{
return [
self::None->value => '待付款',
self::Paying->value => '付款中',
self::Paid->value => '已付款',
// 部分发货
self::Send->value => '已发货(部分)',
// 全部发货
self::SendFull->value => '已发货',
self::Receive->value => '已完成',
// 已评价
self::Review->value => '已评价',
self::PayFail->value => '支付失败',
self::Cancel->value => '已取消',
];
}
public function label()
{
$color = $this->color();
$name = $this->text();
return "<span class='label bg-${color}'>{$name}</span>";
}
public function text()
{
return data_get(self::options(), $this->value, $this->value);
}
public function color()
{
return match ($this) {
self::None => 'dark',
self::Paying => 'secondary',
self::Paid => 'success',
self::Send => 'primary',
self::SendFull => 'primary',
self::Receive => 'primary',
self::Review => 'success',
self::PayFail => 'danger',
self::Cancel => 'secondary',
default => 'dark'
};
}
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,58 @@
<?php
namespace Peidikeji\Order\Enums;
enum PayStatus: int
{
case None = 0;
case Processing = 1;
case Success = 2;
case Fail = 3;
case Refund = 4;
public static function options()
{
return [
self::None->value => '未付款',
self::Processing->value => '付款中',
self::Success->value => '已付款',
self::Fail->value => '支付失败',
self::Refund->value => '已退款',
];
}
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 color()
{
return match ($this) {
static::None => 'secondary',
static::Processing => 'primary',
self::Success => 'success',
self::Fail => 'danger',
self::Refund => 'secondary',
default => 'dark',
};
}
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,48 @@
<?php
namespace Peidikeji\Order\Enums;
enum PayWay: int
{
case Offline = 0;
case WxMini = 1;
public static function options()
{
return [
self::Offline->value => '线下支付',
self::WxMini->value => '微信小程序',
];
}
public function text()
{
return data_get(self::options(), $this->value);
}
public function color()
{
return match ($this) {
static::Offline => 'warning',
static::WxMini => '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,68 @@
<?php
namespace Peidikeji\Order\Enums;
use Dcat\Admin\Admin;
enum RefundStatus: int
{
case None = 0;
case Apply = 1;
case Processing = 2;
case Success = 3;
case Fail = 4;
case Cancel = 5;
public static function options()
{
return [
self::None->value => '未申请',
self::Apply->value => '已申请',
self::Processing->value => '处理中',
self::Success->value => '退款成功',
self::Fail->value => '退款失败',
self::Cancel->value => '已取消',
];
}
public function text()
{
return data_get(self::options(), $this->value);
}
public function label()
{
$color = match ($this) {
static::None => 'secondary',
static::Apply => 'primary',
static::Processing => 'primary',
static::Success => 'success',
static::Fail => 'danger',
static::Cancel => 'secondary',
};
$background = Admin::color()->get($color, $color);
$name = static::options()[$this->value] ?? '其它';
return "<span class='label' style='background: $background;'>{$name}</span>";
}
public function dot()
{
$color = match ($this) {
static::None => 'secondary',
static::Apply => 'primary',
static::Processing => 'primary',
static::Success => 'success',
static::Fail => 'danger',
static::Cancel => 'secondary',
};
$background = Admin::color()->get($color, $color);
$name = static::options()[$this->value] ?? '其它';
return "<i class='fa fa-circle' style='font-size: 13px;color: $background;'>&nbsp;&nbsp;{$name}</span>";
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Peidikeji\Order\Enums;
use Dcat\Admin\Admin;
/**
* 订单发货状态
*/
enum ShipStatus: int
{
case None = 0;
case Processing = 1;
case Finished = 3;
case Received = 4;
public static function options()
{
return [
self::None->value => '未发货',
self::Processing->value => '已发货(部分)',
self::Finished->value => '已发货(全部)',
self::Received->value => '已收货',
];
}
public function text()
{
return data_get(self::options(), $this->value);
}
public function color()
{
return match ($this) {
static::None => 'dark',
static::Processing => 'warning',
static::Finished => 'danger',
static::Received => '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,51 @@
<?php
namespace Peidikeji\Order\Enums;
enum ShipWay: string
{
case None = 'none';
case Express = 'express';
case Pick = 'pick';
public static function options()
{
return [
self::None->value => '无',
self::Express->value => '快递',
self::Pick->value => '自提',
];
}
public function text()
{
return data_get(self::options(), $this->value);
}
public function color()
{
return match ($this) {
static::None => 'secondary',
static::Express => 'primary',
static::Pick => 'warning',
};
}
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,30 @@
<?php
namespace Peidikeji\Order\Enums;
enum WxPayStatus: string
{
case SUCCESS = 'SUCCESS';
case REFUND = 'REFUND';
case NOTPAY = 'NOTPAY';
case CLOSED = 'CLOSED';
case REVOKED = 'REVOKED';
case USERPAYING = 'USERPAYING';
case PAYERROR = 'PAYERROR';
public static function options()
{
return [
self::SUCCESS->value => '支付成功',
self::REFUND->value => '转入退款',
self::NOTPAY->value => '未支付',
self::CLOSED->value => '已关闭',
// 仅付款码支付会返回
self::REVOKED->value => '已撤销',
// 仅付款码支付会返回
self::USERPAYING->value => '用户支付中',
// 仅付款码支付会返回
self::PAYERROR->value => '支付失败',
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Peidikeji\Order\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Peidikeji\Order\Models\Order;
/**
* 订单取消
*
*/
class OrderCanceled
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Peidikeji\Order\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Peidikeji\Order\Models\Order;
/**
* 订单创建成功
*/
class OrderCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Peidikeji\Order\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Peidikeji\Order\Models\Order;
/**
* 订单删除
*
*/
class OrderDeleted
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Peidikeji\Order\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Peidikeji\Order\Models\Order;
/**
* 订单支付成功
*/
class OrderPaid
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Peidikeji\Order\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Peidikeji\Order\Models\Order;
/**
* 订单确认收货
*/
class OrderReceived
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Peidikeji\Order\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Peidikeji\Order\Models\Order;
/**
* 订单发货
* 每次发货触发一次
*/
class OrderShipped
{
use Dispatchable, InteractsWithSockets, SerializesModels;
// 订单
public $order;
// 发货单
public $ship;
public function __construct(Order $order, $ship)
{
$this->order = $order;
$this->ship = $ship;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Peidikeji\Order\Exceptions;
use App\Exceptions\BizException;
class OrderException extends BizException
{
public function __construct(string $message, int $code = 400)
{
parent::__construct($message, $code);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Peidikeji\Order\Filters;
use EloquentFilter\ModelFilter;
use Peidikeji\Order\Enums\OrderStatus;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Enums\ShipStatus;
use Peidikeji\Order\Exceptions\OrderException;
class OrderFilter extends ModelFilter
{
public function status($v)
{
$status = intval($v);
if ($status === OrderStatus::None->value) {
$this->where('pay_status', PayStatus::None)->where('is_closed', 0);
}
else if ($status === OrderStatus::Paying->value) {
$this->where('pay_status', PayStatus::Processing)->where('is_closed', 0);
}
else if ($status === OrderStatus::Paid->value) {
$this->where('pay_status', PayStatus::Success)->where('ship_status', ShipStatus::None)->where('is_closed', 0);
}
else if ($status === OrderStatus::Send->value) {
$this->where('pay_status', PayStatus::Success)->where('ship_status', ShipStatus::Processing)->where('is_closed', 0);
}
else if ($status === OrderStatus::SendFull->value) {
$this->where('pay_status', PayStatus::Success)->where('ship_status', ShipStatus::Finished)->where('is_closed', 0);
}
else if ($status === OrderStatus::Receive->value) {
$this->where('pay_status', PayStatus::Success)->where('ship_status', ShipStatus::Received)->where('is_closed', 0);
}
else if ($status === OrderStatus::Review->value) {
$this->where('pay_status', PayStatus::Success)->where('ship_status', ShipStatus::Received)->whereNotNull('review_at')->where('is_closed', 0);
}
else if ($status === OrderStatus::PayFail->value) {
$this->where('pay_status', PayStatus::Fail)->where('is_closed', 0);
}
else if ($status === OrderStatus::Cancel->value) {
$this->where('is_closed', 1);
} else {
throw new OrderException('未知的订单状态');
}
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Peidikeji\Order\Form;
use Dcat\Admin\Admin;
use Dcat\Admin\Contracts\LazyRenderable;
use Dcat\Admin\Models\Administrator;
use Dcat\Admin\Traits\LazyWidget;
use Dcat\Admin\Widgets\Form;
use Illuminate\Support\Facades\DB;
use Peidikeji\Order\Enums\PayWay;
use Peidikeji\Order\Exceptions\OrderException;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\OrderService;
class PayForm extends Form implements LazyRenderable
{
use LazyWidget;
protected $buttons = ['reset' => false, 'submit' => true];
public function handle(array $input)
{
$id = $this->payload['id'];
try {
DB::beginTransaction();
$order = Order::findOrFail($id);
// 验证积分余额是否足够
if ($order->score_discount_amount > 0 && $order->user) {
if ($order->user->profit < $order->score_discount_amount) {
throw new OrderException('用户积分余额不足');
}
}
OrderService::make()->paySuccess($order, $input);
$admin = Admin::user();
$order->options()->create([
'user_type' => Administrator::class,
'user_id' => $admin->id,
'description' => '管理员: ' . $admin->name . ' 支付订单',
'attribute' => $input,
]);
DB::commit();
return $this->response()->success('操作成功')->refresh();
} catch (\Exception $e) {
DB::rollBack();
return $this->response()->error($e->getMessage());
}
}
public function form()
{
$this->display('pay_money');
$this->text('pay_no')->required();
$this->datetime('pay_at')->required();
$this->radio('pay_way')->options(PayWay::options());
}
public function default()
{
return [
'pay_way' => PayWay::WxMini->value,
'pay_money' => $this->payload['pay_money'],
'pay_at' => now(),
];
}
}

View File

@ -0,0 +1,46 @@
<?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\Facades\DB;
use Peidikeji\Order\Models\Order;
class RemarksForm 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']);
$order->update(['remarks' => $input['remarks']]);
$admin = Admin::user();
$order->options()->create([
'user_type' => get_class($admin),
'user_id' => $admin->id,
'description' => '管理员: ' . $admin->name . ' 修改系统备注',
'attribute' => [
'remarks' => $input['remarks']
],
]);
DB::commit();
return $this->response()->success('操作成功')->refresh();
} catch (\Exception $e) {
DB::rollBack();
return $this->response()->error($e->getMessage());
}
}
public function form()
{
$this->textarea('remarks');
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace Peidikeji\Order\Form;
use App\Services\Kuaidi100Service;
use Dcat\Admin\Admin;
use Dcat\Admin\Contracts\LazyRenderable;
use Dcat\Admin\Form\NestedForm;
use Dcat\Admin\Models\Administrator;
use Dcat\Admin\Traits\LazyWidget;
use Dcat\Admin\Widgets\Form;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Peidikeji\Order\Enums\ShipWay;
use Peidikeji\Order\Exceptions\OrderException;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\Models\OrderGoods;
use Peidikeji\Order\OrderService;
class ShipForm extends Form implements LazyRenderable
{
use LazyWidget;
protected $buttons = ['reset' => false, 'submit' => true];
public function handle(array $input)
{
try {
if (count($input['goods']) === 0) {
throw new OrderException('请选择要发货的商品');
}
DB::beginTransaction();
$order = Order::findOrFail($this->payload['id']);
$code = $input['company_code'];
$company = data_get(array_flip(Kuaidi100Service::$codeArr), $code, '未知');
$sn = $input['sn'];
OrderService::make()->ship($order, $input['goods'], compact('code', 'company', 'sn'));
$admin = Admin::user();
$order->options()->create([
'user_type' => get_class($admin),
'user_id' => $admin->id,
'description' => '管理员: ' . $admin->name . ' 发货',
'attribute' => $input,
]);
DB::commit();
return $this->response()->success('操作成功')->refresh();
} catch (\Exception $e) {
DB::rollBack();
return $this->response()->error($e->getMessage());
}
}
public function form()
{
$orderGoods = OrderGoods::where('order_id', $this->payload['id'])->get();
$goods = [];
foreach ($orderGoods as $item) {
array_push($goods, [
'order_goods_id' => $item->id,
'cover_image' => $item->cover_image,
'goods_name' => $item->goods_name,
'amount' => $item->amount - $item->ship_amount,
'spec' => $item->spec,
'ship_amount' => $item->ship_amount,
'total_amount' => $item->amount,
]);
}
$this->fill(['goods' => $goods]);
$way = $this->payload['ship_way'];
if ($way === ShipWay::Express->value) {
$this->select('company_code', '快递公司')->options(array_flip(Kuaidi100Service::$codeArr));
$this->text('sn', '快递单号')->required();
}
$this->table('goods', '', function (NestedForm $table) {
$spec = data_get($table->model(), 'goods.spec');
$table->hidden('order_goods_id');
$table->hidden('cover_image');
$table->display('goods_name', '商品');
if ($spec) {
$table->display('spec', '规格')->with(function ($v) {
if (!$v) {
return '';
}
if (is_string($v)) {
return $v;
}
$html = '';
foreach($v as $i) {
$html .= '<span class="label bg-primary mr-1">'.$i['value'].'</span>';
}
return $html;
});
}
$table->display('total_amount', '总数量');
$table->display('ship_amount', '已发');
$table->number('amount', '数量')->default(1)->min(1)->options(['upClass' => null, 'downClass' => null]);
})->horizontal(false)->setFieldClass('col-12')->options([
'allowCreate' => false,
])->required();
}
}

View File

@ -0,0 +1,197 @@
<?php
namespace Peidikeji\Order\Http\Admin;
use Carbon\Carbon;
use Dcat\Admin\Admin;
use Dcat\Admin\Grid;
use Dcat\Admin\Grid\Displayers\Modal;
use Dcat\Admin\Grid\Filter;
use Dcat\Admin\Http\Controllers\AdminController;
use Dcat\Admin\Layout\Column;
use Dcat\Admin\Layout\Row;
use Dcat\Admin\Models\Administrator;
use Dcat\Admin\Show;
use Dcat\Admin\Show\Tools;
use Dcat\Admin\Widgets\Box;
use Illuminate\Support\Arr;
use Peidikeji\Merchant\Models\Merchant;
use Peidikeji\Order\Action\ShowCancel;
use Peidikeji\Order\Action\ShowDelete;
use Peidikeji\Order\Action\ShowPay;
use Peidikeji\Order\Action\ShowPayQuery;
use Peidikeji\Order\Action\ShowReceive;
use Peidikeji\Order\Action\ShowRemarks;
use Peidikeji\Order\Action\ShowShip;
use Peidikeji\Order\Action\ShowShipQrcode;
use Peidikeji\Order\Enums\OrderStatus;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\Models\OrderGoods;
use Peidikeji\Order\Models\OrderOption;
use Peidikeji\Order\Models\OrderShip;
use Peidikeji\Order\Renderable\ShipLog;
use Peidikeji\User\Models\User;
class OrderController extends AdminController
{
protected $translation = 'dcat-admin-order::order';
protected function grid()
{
return Grid::make(Order::with(['user', 'merchant']), function (Grid $grid) {
$grid->model()->sort();
$grid->filter(function (Filter $filter) {
$filter->panel();
$filter->like('sn')->width(4);
$filter->equal('merchant_id')->select()->ajax('api/merchants?_paginate=1')->model(Merchant::class, 'id', 'name')->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->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(4);
});
$grid->column('scene')->display(fn() => $this->scene->label());
$grid->column('sn')->link(fn() => admin_url('orders', ['id' => $this->id]));
$grid->column('merchant.name');
$grid->column('user.phone');
$grid->column('total_money');
$grid->column('order_status')->display(fn() => $this->status()->dot());
$grid->column('created_at');
$user = Admin::user();
$grid->showViewButton($user->can('dcat.admin.orders.show'));
});
}
protected function detail($id)
{
$info = Order::with(['user', 'merchant'])->findOrFail($id);
$row = new Row();
$row->column(6, Show::make($info, function (Show $show) {
$info = $show->model();
$show->row(function (Show\Row $show) {
$show->width(6)->field('scene')->as(fn() => $this->scene->label())->unescape();
$show->width(6)->field('user.phone');
$show->width(6)->field('merchant.name');
$show->width(6)->field('sn');
$show->width(6)->field('status', '状态')->as(fn() => $this->status()->label())->unescape();
$show->width(6)->field('created_at');
});
$show->row(function (Show\Row $show) {
$show->width(6)->field('total_money');
$show->width(6)->field('user_remarks');
});
$show->row(function (Show\Row $show) {
$show->width(6)->field('score_discount_money');
$show->width(6)->field('score_discount_amount');
$show->width(6)->field('score_discount_ratio');
$show->width(6)->field('vip_discount_money');
});
$show->row(function (Show\Row $show) use ($info) {
$show->width(6)->field('pay_money');
$show->width(6)->field('pay_status')->as(fn() => $this->pay_status->label())->unescape();
$show->width(6)->field('pay_way')->as(fn() => $this->pay_way?->label())->unescape();
if ($info->pay_status === PayStatus::Success) {
$show->width(6)->field('pay_at');
$show->width(6)->field('pay_no');
}
});
$show->row(function (Show\Row $show) use ($info) {
$show->width(6)->field('ship_status')->as(fn() => $this->ship_status->label())->unescape();
$show->width(6)->field('ship_money');
$show->width(6)->field('ship_address')->as(fn($v) => $v ? implode(',', Arr::only($v, ['name', 'phone', 'address'])) : '');
$show->width(6)->field('ship_way')->as(fn() => $this->ship_way?->label())->unescape();
$show->width(6)->field('ship_at');
});
$show->row(function (Show\Row $show) use ($info) {
$show->width(6)->field('user_get_profit');
$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 ShowShipQrcode());
$tools->append(new ShowDelete());
$tools->append(new ShowRemarks());
});
}));
$row->column(6, function (Column $column) use ($info) {
$goodsGrid = Grid::make(OrderGoods::where('order_id', $info->id), function (Grid $grid) {
$grid->disablePagination();
$grid->disableActions();
$grid->disableRefreshButton();
$grid->column('goods_name', '商品');
$grid->column('price', '价格');
$grid->column('amount', '数量');
$grid->column('ship_amount', '已发');
$grid->column('spec', '规格')->display(fn($v) => $v ? array_column($v, 'value') : '')->label();
$grid->column('money', '合计');
});
$column->row(Box::make('订单商品', $goodsGrid));
$column->row(Box::make('发货单', Grid::make(OrderShip::with(['goods'])->where('order_id', $info->id)->sort(), function (Grid $grid) {
$grid->disablePagination();
$grid->disableActions();
$grid->disableRefreshButton();
$grid->column('company', '物流公司')->display(fn() => data_get($this->ship_data, 'company'));
$grid->column('sn', '运单号')->copyable();
$grid->column('goods', '商品')->display(function () {
$html = '';
foreach($this->goods as $item) {
$html .= <<<HTML
<div>
$item->goods_name
<span class="badge bg-primary">$item->amount</span>
</div>
HTML;
}
return $html;
});
$grid->column('ship_status', '状态')->display(fn() => $this->ship_status->text())->modal(function (Modal $modal) {
$modal->icon('fa-truck');
$modal->title('物流信息');
return ShipLog::make(['id' => $this->id, 'ship' => $this->ship_data])->render();
});
$grid->column('created_at', '发货时间');
$grid->column('finish_at', '收货时间');
})));
$column->row(Box::make('操作记录', Grid::make(OrderOption::with(['user'])->where('order_id', $info->id)->orderBy('created_at', 'desc'), function (Grid $grid) {
$grid->disablePagination();
$grid->disableActions();
$grid->disableRefreshButton();
$grid->column('user_id', '操作人')->display(function () {
return match($this->user_type) {
Administrator::class => $this->user->name,
User::class => $this->user->phone,
null => '',
default => $this->user_type.': '.$this->user_id
};
});
$grid->column('description', '描述');
$grid->column('created_at', '时间');
})));
});
return $row;
}
}

View File

@ -0,0 +1,407 @@
<?php
namespace Peidikeji\Order\Http\Api;
use Peidikeji\Order\Exceptions\OrderException;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserAddressResource;
use App\Models\UserAddress;
use App\Models\Zone;
use App\Services\Kuaidi100Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Peidikeji\Merchant\Models\Merchant;
use Peidikeji\Order\Enums\OrderScene;
use Peidikeji\Order\Enums\OrderStatus;
use Peidikeji\Order\Enums\PayWay;
use Peidikeji\Order\Enums\ShipWay;
use Peidikeji\Order\Http\Resources\OrderResource;
use Peidikeji\Order\Http\Resources\OrderShipResouce;
use Peidikeji\Order\OrderService;
use Peidikeji\Order\OrderStore;
class OrderController extends Controller
{
public function index(Request $request)
{
$user = auth('api')->user();
$query = $user->orders()->with(['goods', 'merchant'])->filter($request->all());
$list = $query->sort()->paginate($request->input('per_page'));
return $this->json(OrderResource::collection($list));
}
public function total()
{
$user = auth('api')->user();
$query = $user->orders();
$maps = [
'unpay' => OrderStatus::None,
'paid' => OrderStatus::Paid,
'send_full' => OrderStatus::SendFull,
'receive' => OrderStatus::Receive
];
$data = [];
foreach($maps as $key => $status) {
$data[$key] = $query->clone()->filter(['status' => $status->value])->count();
}
return $this->json($data);
}
public function sure(Request $request)
{
$request->validate([
'scene' => ['required', new Enum(OrderScene::class)],
'goods' => [Rule::requiredIf(!$request->filled('cart_id')), 'array'],
'cart_id' => ['array'],
'merchant_id' => [Rule::requiredIf($request->input('scene') === OrderScene::Merchant->value)],
]);
$user = auth('api')->user();
$service = OrderStore::init()->user($user)->scene($request->input('scene'));
if ($request->filled('merchant_id')) {
$merchant = Merchant::findOrFail($request->input('merchant_id'));
if (!$merchant->enable()) {
throw new OrderException('店铺不可用');
}
$service->merchant($merchant);
}
$goods = $request->input('goods');
if ($request->filled('cart_id')) {
$goods = $this->formatCartGoods($user->carts()->whereIn('id', $request->input('cart_id'))->get());
}
$service->goods($goods);
// 收货地址
$address = null;
if ($request->filled('address_id')) {
$address = $user->addresses()->find($request->input('address_id'));
if (!$address) {
return $this->error('收货地址不存在');
}
}
if (!$address) {
$address = $user->addresses()->orderBy('is_default', 'desc')->orderBy('created_at', 'desc')->first();
}
$service->ship(ShipWay::Express->value, $this->formatAddress($address));
// 计算最大积分抵扣
$maxScore = floatval($service->orderGoodsList->sum('score_max_amount'));
$score = $request->input('score');
$userScore = floatval($user->profit);
if (!$score) {
$score = $userScore > $maxScore ? $maxScore : $userScore;
}
$service->score($score);
$goodsList = $service->orderGoodsList;
$dataList = collect();
foreach($goodsList->groupBy('merchant_id') as $id => $list) {
$dataList->push([
'merchant' => $id ? Merchant::select('id', 'name')->find($id) : ['id' => null, 'name' => '自营店铺'],
'list' => $list
]);
}
// 付款金额
$payMoney = $service->getPayMoney();
return $this->json([
'address' => $address ? UserAddressResource::make($address) : null,
'list' => $dataList,
'score' => [
'has' => $userScore,
'use' => $score,
'max' => $maxScore,
'bonus' => $service->getUserBonus($payMoney),
],
'price' => [
'goods' => floatval($goodsList->sum('price')),
'ship' => $service->ship['money'],
'coupon' => 0,
'vip' => $service->vip['money'],
'score' => $service->score['money'],
'total' => $payMoney,
]
]);
}
public function store(Request $request)
{
$user = auth('api')->user();
$request->validate([
'scene' => ['required', new Enum(OrderScene::class)]
]);
$scene = $request->input('scene');
$store = OrderStore::init()->scene($scene)->user($user)->remarks($request->input('remarks'));
// 自营商城下单
if ($scene === OrderScene::Online->value) {
$request->validate([
'goods' => [Rule::requiredIf(!$request->filled('cart_id')), 'array'],
'cart_id' => ['array'],
'address' => [Rule::requiredIf(!$request->filled('address_id')), 'array'],
]);
$goods = $request->input('goods');
if ($request->filled('cart_id')) {
$goods = $this->formatCartGoods($user->carts()->whereIn('id', $request->input('cart_id'))->get());
}
$store->goods($goods);
$address = null;
if ($request->filled('address_id')) {
$address = $user->addresses()->find($request->input('address_id'));
if (!$address) {
return $this->error('收货地址不存在');
}
}
if (!$address) {
$address = $request->input('address');
}
$store->ship(ShipWay::Express->value, $this->formatAddress($address));
$store->score($request->input('score', 0));
}
// 店铺下单
else if ($scene === OrderScene::Merchant->value) {
$request->validate([
'goods' => [Rule::requiredIf(!$request->filled('cart_id')), 'array'],
'cart_id' => ['array'],
'merchant_id' => 'required',
]);
$merchant = Merchant::findOrFail($request->input('merchant_id'));
if (!$merchant->enable()) {
throw new OrderException('店铺不可用');
}
$store->merchant($merchant);
$goods = $request->input('goods');
if ($request->filled('cart_id')) {
$goods = $this->formatCartGoods($user->carts()->whereIn('id', $request->input('cart_id'))->get());
}
$store->goods($goods);
$store->ship(ShipWay::Pick->value);
// 验证店铺返现余额是否足够
$payMoney = $store->getPayMoney();
$ratio = $merchant->profit_ratio / 100;
$userProfit = round($payMoney * $ratio, 2, PHP_ROUND_HALF_DOWN);
$merchantUser = $merchant->user;
if ($merchantUser->profit < $userProfit) {
throw new OrderException('店铺返现账户余额不足');
}
}
// 扫码付款
else if ($scene === OrderScene::Scan->value) {
$request->validate([
'money' => 'required',
'merchant_id' => 'required',
]);
$merchant = Merchant::findOrFail($request->input('merchant_id'));
if (!$merchant->enable()) {
throw new OrderException('店铺不可用');
}
$store->merchant($merchant);
$store->money($request->input('money'));
} else {
return $this->error('未知下单场景');
}
try {
DB::beginTransaction();
$order = $store->create();
// 删除购物车商品
if ($request->filled('cart_id')) {
$user->carts()->whereIn('id', $request->input('cart_id'))->delete();
}
$result = OrderResource::make($order);
if ($request->filled('pay_way')) {
$result = [];
$payData = OrderService::make()->pay($order, $request->input('pay_way'));
$result['order'] = OrderResource::make($order);
$result['payment'] = $payData;
}
DB::commit();
return $this->json($result);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
public function show($id)
{
$user = auth('api')->user();
$order = $user->orders()->with(['goods'])->findOrFail($id);
return $this->json(OrderResource::make($order));
}
public function pay($id, Request $request)
{
$request->validate([
'pay_way' => ['required', new Enum(PayWay::class)]
]);
$user = auth('api')->user();
$order = $user->orders()->findOrFail($id);
try {
DB::beginTransaction();
$result = OrderService::make()->pay($order, PayWay::WxMini->value);
DB::commit();
return $this->json($result);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
public function cancel($id, Request $request)
{
$user = auth('api')->user();
$order = $user->orders()->findOrFail($id);
try {
DB::beginTransaction();
OrderService::make()->cancel($order);
$order->options()->create([
'user_type' => get_class($user),
'user_id' => $user->id,
'description' => '用户取消订单',
'attribute' => [
'is_closed' => 1
]
]);
DB::commit();
return $this->success('订单成功取消');
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
public function receive($id)
{
$user = auth('api')->user();
$order = $user->orders()->findOrFail($id);
try {
DB::beginTransaction();
OrderService::make()->receive($order);
$order->options()->create([
'user_type' => get_class($user),
'user_id' => $user->id,
'description' => '用户 确认收货',
'attribute' => [
'is_closed' => 1
]
]);
DB::commit();
return $this->success('收货成功');
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
public function destroy($id)
{
$user = auth('api')->user();
$order = $user->orders()->findOrFail($id);
try {
DB::beginTransaction();
OrderService::make()->delete($order);
DB::commit();
return $this->success('删除成功');
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
public function ships($id)
{
$user = auth('api')->user();
$order = $user->orders()->findOrFail($id);
$list = $order->ships()->with(['goods'])->sort()->get();
return $this->json(OrderShipResouce::collection($list));
}
public function logistics(Request $request)
{
$request->validate([
'order_id' => 'required',
'ship_id' => 'required',
]);
$user = auth('api')->user();
$order = $user->orders()->findOrFail($request->input('order_id'));
$ship = $order->ships()->findOrFail($request->input('ship_id'));
return $this->success();
}
protected function formatCartGoods($carts)
{
$goods = [];
foreach($carts as $item) {
array_push($goods, [
'id' => $item->goods_id,
'amount' => $item->amount,
'spec' => $item->spec,
'part' => $item->part,
]);
}
return $goods;
}
protected function formatAddress($userAddress)
{
$address = [];
if ($userAddress instanceof UserAddress) {
$address = [
'name' => $userAddress->contact_name,
'phone' => $userAddress->phone,
'region' => [$userAddress->province?->name, $userAddress->city?->name, $userAddress->area?->name],
'zone' => [$userAddress->province_id, $userAddress->city_id, $userAddress->area_id],
'address' => $userAddress->address,
'address_id' => $userAddress->id
];
} else if (is_array($userAddress)) {
$address = $userAddress;
$region = data_get($address, 'region');
if (!$region || count($region) === 0) {
throw new OrderException('收货地址格式不正确');
}
$area = Zone::where('name', $region[2])->where('type', 'area')->firstOrFail();
$city = Zone::findOrFail($area->parent_id);
$address['zone'] = [$city->parent_id, $area->parent_id, $area->id];
}
return $address;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Peidikeji\Order\Http\Api;
use App\Http\Controllers\Controller;
use EasyWeChat\Pay\Message;
use Illuminate\Http\Request;
use Overtrue\LaravelWeChat\EasyWeChat;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\OrderService;
use Illuminate\Support\Str;
use Peidikeji\Order\Enums\PayWay;
class WxPayNotifyController extends Controller
{
public function pay(Request $request)
{
if (config('app.debug') && $request->filled('out_trade_no')) {
$order = Order::where('sn', $request->input('out_trade_no'))->first();
if ($order) {
OrderService::make()->paySuccess($order, [
'pay_at' => now(),
'pay_no' => Str::uuid(),
'pay_way' => PayWay::WxMini
]);
return response()->json(['code' => 'SUCCESS', 'message' => '']);
}
return response()->json(['code' => 'FAIL', 'message' => '订单不存在']);
}
$app = EasyWeChat::pay();
$server = $app->getServer();
$server->handlePaid(function (Message $message, \Closure $next) {
// $message->out_trade_no 获取商户订单号
// $message->payer['openid'] 获取支付者 openid
// 🚨🚨🚨 注意:推送信息不一定靠谱哈,请务必验证
// 建议是拿订单号调用微信支付查询接口,以查询到的订单状态为准
$order = Order::where('sn', $message->out_trade_no)->first();
if ($order) {
OrderService::make()->paySuccess($order, [
'pay_at' => now(),
'pay_no' => $message->transaction_id
]);
}
return $next($message);
});
return $server->serve();
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Peidikeji\Order\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderGoodsResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'goods_id' => $this->goods_id,
'merchant_id' => $this->merchant_id,
'order_id' => $this->order_id,
'order' => OrderResource::make($this->whenLoaded('order')),
'goods_name' => $this->goods_name,
'goods_sn' => $this->goods_sn,
'goods_sku_sn' => $this->goods_sku_sn,
'cover_image' => $this->cover_image,
'attr' => $this->attr,
'spec' => $this->spec,
'part' => $this->part,
'price' => floatval($this->price),
'vip_price' => floatval($this->vip_price),
'user_price' => floatval($this->user_price),
'amount' => $this->amount,
'money' => floatval($this->money),
'ship_amount' => $this->ship_amount,
];
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Peidikeji\Order\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Peidikeji\Merchant\Http\Resources\MerchantTinyResource;
use Peidikeji\Order\Enums\OrderScene;
class OrderResource extends JsonResource
{
public function toArray($request)
{
$status = $this->status();
return [
'id' => $this->id,
'sn' => $this->sn,
'scene' => $this->scene,
'scene_text' => $this->scene->text(),
'user_id' => $this->user_id,
'merchant_id' => $this->merchant_id,
'merchant' => $this->merchant_id ? MerchantTinyResource::make($this->whenLoaded('merchant')) : ['id' => '', 'name' => '自营商城'],
'total_money' => floatval($this->total_money),
'status' => $status,
'status_text' => $status->text(),
'pay_status' => $this->pay_status,
'pay_at' => $this->pay_at?->timestamp,
'pay_money' => floatval($this->pay_money),
'pay_no' => $this->pay_no,
'pay_way' => $this->pay_way,
'vip_discount_money' => $this->vip_discount_money,
'score_discount_amount' => floatval($this->score_discount_amount),
'score_discount_money' => floatval($this->score_discount_money),
'score_discount_ratio' => floatval($this->score_discount_ratio),
'ship_status' => $this->ship_status,
'ship_status_text' => $this->ship_status->text(),
'ship_way' => $this->ship_way,
'ship_address' => $this->ship_address,
'ship_at' => $this->ship_at?->timestamp,
'ship_money' => $this->ship_money,
'user_remarks' => $this->user_remarks,
'created_at' => $this->created_at->timestamp,
'user_get_profit' => floatval($this->user_get_profit),
'goods' => OrderGoodsResource::collection($this->whenLoaded('goods')),
'ship_qrcode' => data_get($this->extra, 'ship_qrcode'),
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Peidikeji\Order\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderShipResouce extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'order_id' => $this->order_id,
'sn' => $this->sn,
'ship_status' => $this->ship_status,
'ship_status_text' => $this->ship_status->text(),
'ship_data' => $this->ship_data,
'ship_address' => $this->ship_address,
'finish_at' => $this->finish_at?->timestamp,
'created_at' => $this->created_at?->timestamp,
'goods' => $this->whenLoaded('goods'),
];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Peidikeji\Order\Listeners;
use App\Models\BalanceLog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Models\Order;
/**
* 订单使用积分抵扣
* 订单已支付 => 扣除用户的积分
* 订单已退款 => 返还用户扣除的积分
*/
class OrderDiscountProfit implements ShouldQueue
{
public function handle($event)
{
$order = $event->order;
$user = $order->user;
$amount = floatval($order->score_discount_amount);
if ($amount > 0 && $user) {
if ($order->pay_status === PayStatus::Success) {
$user->decrement('profit', $amount);
$user->balanceLogs()->create([
'type' => BalanceLog::TYPE_PROFIT,
'cate' => BalanceLog::CATE_PROFIT_ORDER_OUT,
'amount' => 0 - $amount,
'balance' => $user->profit,
'description' => '使用 '.$amount.' 积分抵扣 '.floatval($order->score_discount_money).' 元',
'source_id' => $order->id,
'source_type' => Order::class,
]);
}
else if ($order->pay_status === PayStatus::Refund) {
$user->increment('profit', $amount);
$user->balanceLogs()->create([
'type' => BalanceLog::TYPE_PROFIT,
'cate' => BalanceLog::CATE_PROFIT_ORDER_OUT,
'amount' => $amount,
'balance' => $user->profit,
'description' => '订单退款, 返还 '.$amount.' 积分',
'source_id' => $order->id,
'source_type' => Order::class,
]);
}
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Peidikeji\Order\Listeners;
use App\Models\BalanceLog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Arr;
use Peidikeji\Order\Enums\OrderScene;
use Peidikeji\Order\Models\Order;
use Peidikeji\Setting\Models\Setting;
use Peidikeji\User\Models\User;
/**
* 邀请返现
*/
class OrderInviteProfit implements ShouldQueue
{
public function handle($event)
{
$order = $event->order;
$user = $order->user;
if ($order->scene === OrderScene::Online && $user && $user->inviter_path) {
// -, -1-, -1-2-, -3-16-21-41-54-64-
$parentIds = array_reverse(explode('-', $user->inviter_path));
$ratio = Setting::where('slug', 'invite_profit_ratio')->value('value');
if (count($parentIds) > 2 && $ratio > 0) {
$amount = round($order->total_money * $ratio / 100, 2, PHP_ROUND_HALF_DOWN);
foreach(Arr::only($parentIds, [1, 2]) as $id) {
$this->giveUser($id, $order, $amount);
}
}
}
}
protected function giveUser($id, $order, $money = 0)
{
$user = User::find($id);
if ($user && $money > 0) {
$user->increment('balance', $money);
$user->balanceLogs()->create([
'type' => BalanceLog::TYPE_BALANCE,
'amount' => $money,
'balance' => $user->balance,
'cate' => BalanceLog::CATE_BALANCE_ORDER_PROFIT,
'description' => '邀请用户 '.$order->user->getSubPhone().' 下单得到返利 ' . $money,
'source_id' => $order->id,
'source_type' => Order::class,
]);
}
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Peidikeji\Order\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Peidikeji\Order\Enums\OrderScene;
use Peidikeji\Order\Enums\ShipWay;
/**
* 订单-生成取货的二维码
*/
class OrderMakeShipQrcode implements ShouldQueue
{
public function handle($event)
{
$order = $event->order;
if ($order->scene === OrderScene::Merchant && $order->ship_way === ShipWay::Pick) {
$order->generateShipQrcode();
}
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Peidikeji\Order\Listeners;
use App\Models\BalanceLog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Peidikeji\Order\Enums\OrderScene;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Models\Order;
/**
* 赠送用户积分
* 订单已支付 => 增加用户积分, 扣除商户积分
* 订单已退款 => 积分原路返还
*/
class OrderSendProfit implements ShouldQueue
{
public function handle($event)
{
$order = $event->order;
$user = $order->user;
$merchant = $order->merchant;
if ($order->scene === OrderScene::Merchant && $merchant) {
$merchantUser = $merchant->user;
$amount = floatval($order->user_get_profit);
if ($amount > 0 && $user && $merchantUser) {
if ($order->pay_status === PayStatus::Success) {
$user->increment('profit', $amount);
$user->balanceLogs()->create([
'type' => BalanceLog::TYPE_PROFIT,
'cate' => BalanceLog::CATE_PROFIT_ORDER_IN,
'amount' => $amount,
'balance' => $user->profit,
'description' => '在商户 ' . $merchant->name.' 下单, 赠送 '.$amount.' 积分',
'source_id' => $order->id,
'source_type' => Order::class,
]);
$merchantUser->decrement('profit', $amount);
$merchantUser->balanceLogs()->create([
'type' => BalanceLog::TYPE_PROFIT,
'cate' => BalanceLog::CATE_PROFIT_ORDER_USE,
'amount' => 0 - $amount,
'balance' => $merchantUser->profit,
'description' => '用户 ' . $user->getSubPhone() . ' 下单送出 '.$amount.' 积分',
'source_id' => $order->id,
'source_type' => Order::class,
]);
}
else if ($order->pay_status === PayStatus::Refund) {
$user->decrement('profit', $amount);
$user->balanceLogs()->create([
'type' => BalanceLog::TYPE_PROFIT,
'cate' => BalanceLog::CATE_PROFIT_ORDER_IN,
'amount' => 0 - $amount,
'balance' => $user->profit,
'description' => '订单退款, 扣除赠送的 '.$amount.' 积分',
'source_id' => $order->id,
'source_type' => Order::class,
]);
$merchantUser->increment('profit', $amount);
$merchantUser->balanceLogs()->create([
'type' => BalanceLog::TYPE_PROFIT,
'cate' => BalanceLog::CATE_PROFIT_ORDER_USE,
'amount' => $amount,
'balance' => $merchantUser->profit,
'description' => '用户 ' . $user->getSubPhone() . ' 订单退款返还 '.$amount.' 积分',
'source_id' => $order->id,
'source_type' => Order::class,
]);
}
}
}
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Peidikeji\Order\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Peidikeji\Goods\Models\Goods;
use Peidikeji\Merchant\Models\Merchant;
/**
* 更新商品销量
*/
class UpdateGoodsSoldCount implements ShouldQueue
{
public function handle($event)
{
$order = $event->order;
// 订单是否取消
// true = 返还商品销量
// false = 扣除商品销量
$close = $order->is_closed;
$merchants = [];
foreach($order->goods as $item) {
if ($close) {
Goods::where('id', $item->goods_id)->decrement('sold_count', $item->amount);
} else {
Goods::where('id', $item->goods_id)->increment('sold_count', $item->amount);
}
if ($item->merchant_id) {
$amount = data_get($merchants, $item->merchant_id, 0) + $item->amount;
$merchants[$item->merchant_id] = $amount;
}
}
// 增加店铺销量
foreach($merchants as $id => $amount) {
if ($close) {
Merchant::where('id', $id)->decrement('sold_count', $amount);
} else {
Merchant::where('id', $id)->increment('sold_count', $amount);
}
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Peidikeji\Order\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Peidikeji\Goods\Models\Goods;
use Peidikeji\Goods\Models\GoodsSku;
/**
* 更新商品库存
*/
class UpdateGoodsStock implements ShouldQueue
{
public function handle($event)
{
$order = $event->order;
// 订单是否取消
// true = 返还商品库存
// false = 扣除商品库存
$close = $order->is_closed;
foreach($order->goods as $item) {
if ($item->goods_sku_id) {
if ($close) {
GoodsSku::where('id', $item->goods_sku_id)->increment('stock', $item->amount);
} else {
GoodsSku::where('id', $item->goods_sku_id)->decrement('stock', $item->amount);
}
} else {
if ($close) {
Goods::where('id', $item->goods_id)->increment('stock', $item->amount);
} else {
Goods::where('id', $item->goods_id)->decrement('stock', $item->amount);
}
}
}
}
}

View File

@ -0,0 +1,243 @@
<?php
namespace Peidikeji\Order\Models;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Overtrue\LaravelWeChat\EasyWeChat;
use Peidikeji\Merchant\Models\Merchant;
use Peidikeji\Order\Enums\RefundStatus;
use Peidikeji\Order\Enums\OrderScene;
use Peidikeji\Order\Enums\OrderStatus;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Enums\PayWay;
use Peidikeji\Order\Enums\ShipStatus;
use Peidikeji\Order\Enums\ShipWay;
use Peidikeji\Order\Exceptions\OrderException;
use Peidikeji\Order\Filters\OrderFilter;
use Peidikeji\User\Models\User;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
class Order extends Model
{
use HasDateTimeFormatter;
use Filterable;
protected $fillable = [
'merchant_id', 'user_id',
'total_money', 'sn',
'pay_at', 'pay_money', 'pay_no', 'pay_status', 'pay_way',
'score_discount_amount', 'score_discount_money', 'score_discount_ratio',
'ship_way', 'ship_address', 'ship_at', 'ship_status', 'ship_money',
'refund_status', 'is_closed', 'auto_close_at', 'review_at', 'receive_at',
'extra', 'user_remarks', 'remarks', 'scene', 'user_get_profit',
];
protected $casts = [
'ship_address' => 'json',
'extra' => 'json',
'pay_status' => PayStatus::class,
'pay_way' => PayWay::class,
'ship_status' => ShipStatus::class,
'scene' => OrderScene::class,
'ship_way' => ShipWay::class,
'refund_status' => RefundStatus::class,
];
protected $attributes = [
'pay_status' => PayStatus::None,
'ship_way' => ShipWay::None,
'ship_status' => ShipStatus::None,
'refund_status' => RefundStatus::None
];
protected $dates = ['pay_at', 'ship_at', 'receive_at'];
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function merchant()
{
return $this->belongsTo(Merchant::class, 'merchant_id');
}
public function goods()
{
return $this->hasMany(OrderGoods::class, 'order_id');
}
public function ships()
{
return $this->hasMany(OrderShip::class, 'order_id');
}
public function options()
{
return $this->hasMany(OrderOption::class, 'order_id');
}
public function status()
{
if ($this->is_closed) {
return OrderStatus::Cancel;
}
if ($this->pay_status === PayStatus::None) {
return OrderStatus::None;
}
if ($this->pay_status === PayStatus::Processing) {
return OrderStatus::Paying;
}
if ($this->pay_status === PayStatus::Fail) {
return OrderStatus::PayFail;
}
if ($this->pay_status === PayStatus::Success) {
if ($this->ship_status === ShipStatus::Processing) {
return OrderStatus::Send;
}
if ($this->ship_status === ShipStatus::Finished) {
return OrderStatus::SendFull;
}
if ($this->ship_status === ShipStatus::Received) {
return OrderStatus::Receive;
}
return OrderStatus::Paid;
}
return OrderStatus::None;
}
public function modelFilter()
{
return OrderFilter::class;
}
public function canCancel($throw = false)
{
if ($this->is_closed) {
if ($throw) {
throw new OrderException('订单已经取消');
}
return false;
}
if ($this->ship_status !== ShipStatus::None) {
if ($throw) {
throw new OrderException('订单已发货, 无法取消');
}
return false;
}
return true;
}
public function canPay($throw = false)
{
if ($this->is_closed) {
if ($throw) {
throw new OrderException('订单已经取消');
}
return false;
}
if ($this->pay_status === PayStatus::Success) {
if ($throw) {
throw new OrderException('订单已经支付');
}
return false;
}
if ($this->pay_status === PayStatus::Refund) {
if ($throw) {
throw new OrderException('订单已经退款');
}
return false;
}
return true;
}
public function canShip($throw = false)
{
if ($this->pay_status !== PayStatus::Success) {
if ($throw) {
throw new OrderException('订单尚未支付成功');
}
return false;
}
if ($this->ship_status === ShipStatus::Finished) {
if ($throw) {
throw new OrderException('订单已经全部发货');
}
return false;
}
if ($this->ship_status === ShipStatus::Received) {
if ($throw) {
throw new OrderException('订单已经收货');
}
return false;
}
return true;
}
public function canReceive($throw = false)
{
if ($this->ship_status === ShipStatus::None) {
if ($throw) {
throw new OrderException('订单还未发货');
}
return false;
}
if ($this->ship_status === ShipStatus::Processing) {
if ($throw) {
throw new OrderException('订单还未全部发货');
}
return false;
}
if ($this->ship_status === ShipStatus::Received) {
if ($throw) {
throw new OrderException('订单已经收货');
}
return false;
}
return true;
}
public function canDelete($throw = false)
{
if (!$this->is_closed) {
if ($throw) {
throw new OrderException('订单未取消, 无法删除');
}
return false;
}
return true;
}
public function generateShipQrcode()
{
$extra = $this->extra ?: [];
$url = data_get($extra, 'ship_qrcode');
if (!$url) {
$disk = Storage::disk('public');
$path = 'order/qrcode/'.$this->id.'.svg';
$result = QrCode::generate(json_encode([
'type' => 'ship',
'order_id' => $this->id
]));
$disk->put($path, $result);
$url = $disk->url($path);
$extra['ship_qrcode'] = $url;
$this->update(['extra' => $extra]);
}
return $url;
}
public function scopeSort($q)
{
return $q->orderBy('created_at', 'desc');
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Peidikeji\Order\Models;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Model;
use Peidikeji\Merchant\Models\Merchant;
use Peidikeji\User\Models\User;
class OrderGoods extends Model
{
use HasDateTimeFormatter;
protected $table = 'order_goods';
public $timestamps = false;
protected $fillable = [
'order_id', 'merchant_id',
'goods_id', 'goods_name', 'goods_sn', 'cover_image', 'price', 'vip_price', 'score_max_amount',
'goods_sku_id', 'goods_sku_sn', 'spec', 'part', 'attr',
'amount', 'money',
'refund_status', 'refund_at',
'ship_amount',
'weight', 'volume', 'shipping_tmp_id',
];
protected $casts = [
'spec' => 'json',
'part' => 'json',
'attr' => 'json',
];
protected $dates = ['refund_at'];
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function order()
{
return $this->belongsTo(Order::class, 'order_id');
}
public function merchant()
{
return $this->belongsTo(Merchant::class, 'merchant_id');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Peidikeji\Order\Models;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Model;
class OrderOption extends Model
{
use HasDateTimeFormatter;
protected $fillable = ['user_type', 'user_id', 'order_id', 'description', 'attribute'];
protected $casts = [
'attribute' => 'json'
];
public function order()
{
return $this->belongsTo(Order::class, 'order_id');
}
public function user()
{
return $this->morphTo();
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Peidikeji\Order\Models;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Model;
use Peidikeji\Order\Enums\OrderShipStatus;
class OrderShip extends Model
{
use HasDateTimeFormatter;
protected $fillable = ['order_id', 'sn', 'ship_status', 'ship_data', 'ship_address', 'finish_at'];
protected $casts = [
'ship_address' => 'json',
'ship_data' => 'json',
'ship_status' => OrderShipStatus::class,
];
protected $dates = ['finish_at'];
protected $attributes = [
'ship_status' => OrderShipStatus::Wait,
];
public function order()
{
return $this->belongsTo(Order::class, 'order_id');
}
public function goods()
{
return $this->hasMany(OrderShipGoods::class, 'ship_id');
}
public function scopeSort($q)
{
return $q->orderBy('created_at', 'desc');
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Peidikeji\Order\Models;
use Illuminate\Database\Eloquent\Model;
class OrderShipGoods extends Model
{
protected $table = 'order_ship_goods';
protected $fillable = ['ship_id', 'order_goods_id', 'goods_name', 'cover_image', 'amount'];
protected $dates = ['ship_at', 'finish_at'];
public $timestamps = false;
public function ship()
{
return $this->belongsTo(OrderShip::class, 'ship_id');
}
public function orderGoods()
{
return $this->belongsTo(OrderGoods::class, 'order_goods_id');
}
}

View File

@ -0,0 +1,400 @@
<?php
namespace Peidikeji\Order;
use Illuminate\Support\Arr;
use Overtrue\LaravelWeChat\EasyWeChat;
use Peidikeji\Order\Enums\OrderScene;
use Peidikeji\Order\Enums\OrderShipStatus;
use Peidikeji\Order\Enums\OrderStatus;
use Peidikeji\Order\Enums\PayStatus;
use Peidikeji\Order\Enums\PayWay;
use Peidikeji\Order\Enums\RefundStatus;
use Peidikeji\Order\Enums\ShipStatus;
use Peidikeji\Order\Events\OrderCanceled;
use Peidikeji\Order\Events\OrderDeleted;
use Peidikeji\Order\Events\OrderPaid;
use Peidikeji\Order\Events\OrderReceived;
use Peidikeji\Order\Events\OrderShipped;
use Peidikeji\Order\Exceptions\OrderException;
use Peidikeji\Order\Models\Order;
use Peidikeji\Order\Models\OrderShip;
use Peidikeji\User\Enums\SocialiteType;
class OrderService
{
public static function make(...$params)
{
return new static(...$params);
}
public function generateSn()
{
return date('YmdHis') . random_int(1000, 9999);
}
/**
* 订单支付
*
* @param Order $orders
* @param PayWay $way 支付方式
* @return mixed
* @throws OrderException
*/
public function pay($order, $way)
{
if ($order->pay_status === PayStatus::Processing) {
throw new OrderException('支付处理中');
}
if ($order->pay_status === PayStatus::Success) {
throw new OrderException('订单已经支付');
}
$out_trade_no = $order->sn;
if (!$out_trade_no) {
$out_trade_no = $order->sn = $this->generateSn();
}
if ($way) {
$order->pay_way = $way;
}
// $order->pay_status = PayStatus::Processing;
$order->save();
$user = $order->user;
if (!$user) {
throw new OrderException('没有找到下单用户');
}
// 验证积分余额是否足够
$profit = $order->score_discount_amount;
if ($user->profit < $profit) {
throw new OrderException('e品额不足');
}
// 支付金额
$total = $order->pay_money;
// 金额为0
if ($total <= 0) {
$this->paySuccess($order);
return true;
} else if (config('app.debug')) {
$total = 0.01;
}
// 支付备注
$body = config('app.name') . '-支付订单';
// 微信小程序
if ($way == PayWay::WxMini->value) {
if (!$user) {
throw new OrderException('未找到用户');
}
$userSocial = $user->socialites()->where('type', SocialiteType::WxMini)->first();
if (!$userSocial) {
throw new OrderException('未找到用户的小程序授权信息');
}
$openid = $userSocial->openid;
try {
$payment = EasyWeChat::pay();
$client = $payment->getClient();
$config = $payment->getConfig();
$params = [
'appid' => $config->get('app_id'),
'mchid' => $config->get('mch_id'),
'out_trade_no' => $out_trade_no,
'notify_url' => url('api/order/notify'),
'description' => $body,
'amount' => [
'total' => intval($total * 100)
],
'payer' => [
'openid' => $openid
],
];
// 服务商模式
$subAppId = $config->get('sub_app_id');
$subMchId = $config->get('sub_mch_id');
if ($subAppId && $subMchId) {
$params['sp_appid'] = $params['appid'];
$params['sp_mchid'] = $params['mchid'];
$params['sub_appid'] = $subAppId;
$params['sub_mchid'] = $subMchId;
$params['payer'] = [
'sub_openid' => $openid,
];
unset($params['appid'], $params['mchid']);
}
$response = $client->postJson('/v3/pay/transactions/jsapi', $params);
$result = $response->toArray(false);
if (data_get($result, 'code')) {
throw new OrderException(data_get($result, 'message'));
}
$prepayId = data_get($result, 'prepay_id');
$utils = $payment->getUtils();
$sdk = $utils->buildMiniAppConfig($prepayId, $config->get('app_id'), 'RSA');
return $sdk;
} catch (\Exception $e) {
report($e);
throw new OrderException($e->getMessage());
}
}
// 线下支付
else if ($way == PayWay::Offline->value) {
return true;
}
throw new OrderException('暂不支持 ' . $way);
}
/**
* 订单支付成功
*
* @param Order $order
* @param Array $data ['pay_at', 'pay_no', 'pay_way]
* @throws OrderException
*/
public function paySuccess(Order $order, $data = [])
{
if ($order->pay_status === PayStatus::Success) {
throw new OrderException('订单已经支付');
}
$order->pay_status = PayStatus::Success;
$order->pay_at = data_get($data, 'pay_at', now());
if (data_get($data, 'pay_no')) {
$order->pay_no = data_get($data, 'pay_no');
}
if (data_get($data, 'pay_way') !== null) {
$order->pay_way = data_get($data, 'pay_way');
}
$order->save();
// 扫码付款的订单, 支付成功后, 直接完成
if ($order->scene === OrderScene::Scan) {
$order->ship_status = ShipStatus::Finished;
$order->save();
$this->receive($order);
}
event(new OrderPaid($order));
}
/**
* 订单发货
* @param Order $order
* @param array $goods 发货商品信息: [{order_goods_id, amount}], : 全部发货
* @param array $data 发货信息{company: 快递公司, code: 快递公司编码, sn: 快递单号}
*/
public function ship(Order $order, $goods = null, $data = [])
{
if (!$order->canShip()) {
throw new OrderException('订单不能发货');
}
// 是否全部发货
$full = true;
// 订单商品
$goodsList = $order->goods()->get();
$params = [];
foreach($goodsList as $item) {
$subParam = null;
if (!$goods) {
$subParam = ['order_goods_id' => $item->id, 'amount' => $item->amount, 'goods_name' => $item->goods_name];
} else {
$filter = array_filter($goods, fn($subItem) => $subItem['order_goods_id'] == $item->id);
$amount = data_get($filter, '0.amount');
if ($amount) {
$subParam = ['order_goods_id' => $item->id, 'amount' => $amount, 'goods_name' => $item->goods_name, 'cover_image' => $item->cover_image];
}
}
if ($subParam) {
if ($item->ship_amount + $subParam['amount'] > $item->amount) {
throw new OrderException($subParam['goods_name'] . ' 发货数量超过购买数量');
}
if ($item->ship_amount + $subParam['amount'] < $item->amount) {
$full = false;
}
$item->increment('ship_amount', $subParam['amount']);
array_push($params, $subParam);
}
}
$order->ship_status = $full ? ShipStatus::Finished : ShipStatus::Processing;
$order->ship_at = now();
$order->save();
// 添加发货记录
$ship = $order->ships()->create([
'sn' => data_get($data, 'sn'),
'ship_data' => $data,
'ship_address' => $order->ship_address,
]);
$ship->goods()->createMany($params);
event(new OrderShipped($order, $ship));
}
/**
* 订单确认收货
*
* @param Order $order 订单
* @param bool $ship 是否修改发货单状态
*/
public function receive(Order $order, $ship = false)
{
// 订单确认收货
if (!$order->canReceive()) {
throw new OrderException('订单不能收货');
}
$order->ship_status = ShipStatus::Received;
$order->receive_at = now();
$order->save();
if ($ship) {
$order->ships()->whereNull('finish_at')->update([
'finish_at' => $order->receive_at,
'ship_status' => OrderShipStatus::Autocheck,
]);
}
event(new OrderReceived($order));
}
/**
* 发货单确认收货
*
* @param OrderShip $ship 发货单
*/
public function receiveByShip(OrderShip $ship)
{
if ($ship->finish_at) {
throw new OrderException('已经收货了, 请勿重复操作');
}
$ship->finish_at = now();
$ship->ship_status = OrderShipStatus::Check;
$ship->save();
$order = $ship->order;
// 订单全部发货, 发货单全部收货完成
if ($order->ship_status === ShipStatus::Finished && !$order->ships()->whereNull('finish_at')->exists()) {
$this->receive($order);
}
}
/**
* 取消订单
*
* @param Order $order 订单
* @throws OrderException
*/
public function cancel(Order $order)
{
if (!$order->canCancel()) {
throw new OrderException('订单无法取消');
}
// 订单已经支付, 全额退款
if ($order->pay_status === PayStatus::Success) {
// 如果赠送给用户积分, 验证用户余额是否足够扣除
if ($order->user_get_profit > 0 && $order->user->profit < $order->user_get_profit) {
throw new OrderException('订单无法取消, 用户余额不足');
}
// 支付金额
if ($order->pay_money > 0) {
$result = $this->refundPay($order, $order->pay_money, '取消订单, 全额退款');
if (!$result['status']) {
throw new OrderException($result['message']);
}
$order->refund_status = RefundStatus::Success;
$extra = Arr::only($result, ['refund_sn', 'refund_no']);
$order->extra = array_merge($order->extra ?: [], $extra);
}
$order->pay_status = PayStatus::Refund;
}
$order->is_closed = 1;
$order->save();
event(new OrderCanceled($order));
}
/**
* 删除订单
*
* @param Order $order 订单
* @throws OrderException
*/
public function delete(Order $order)
{
if (!$order->canDelete()) {
throw new OrderException('订单无法删除');
}
$order->goods()->delete();
$order->options()->delete();
$order->ships()->delete();
$order->delete();
event(new OrderDeleted($order));
}
/**
* 按照支付方式退款
*
* @param Order $order 订单
* @param Float $money 退款金额
* @param String $desc 退款备注
* @param String $sn 退款订单号
* @throws OrderException
*
* @return Array {status: 状态, message: 错误信息, refund_sn: 退款订单号, refund_no: 退款流水号}
*/
public function refundPay(Order $order, $money, $desc = '退款', $sn = null)
{
$result = [
'status' => true,
'message' => '',
'refund_sn' => '',
'refund_no' => ''
];
if ($order->pay_way === PayWay::WxMini) {
$total = intval($order->pay_money * 100);
if (config('app.debug')) {
$money = 1;
$total = 1;
}
if (!$sn) {
$sn = $this->generateSn();
}
$payment = EasyWeChat::pay();
$client = $payment->getClient();
$response = $client->postJson('/v3/refund/domestic/refunds', [
'out_trade_no' => $order->sn,
'out_refund_no' => $sn,
'reason' => $desc,
'amount' => [
'refund' => $money,
'total' => $total,
'currency' => 'CNY',
]
]);
$data = $response->toArray(false);
if (data_get($data, 'code')) {
$result['status'] = false;
$result['message'] = '微信支付: ' .data_get($data, 'message');
}
$result['refund_sn'] = data_get($data, 'out_refund_no');
$result['refund_no'] = data_get($data, 'refund_id');
}
return $result;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Peidikeji\Order;
use Illuminate\Support\ServiceProvider;
use Peidikeji\Order\Console\OrderCancel;
use Peidikeji\Order\Console\OrdeReceive;
class OrderServiceProvider extends ServiceProvider
{
public function register()
{
}
public function boot()
{
if ($this->app->runningInConsole()) {
$this->commands([
OrderCancel::class,
OrdeReceive::class,
]);
}
$this->loadRoutesFrom(__DIR__.'/../routes/admin.php');
$this->loadRoutesFrom(__DIR__.'/../routes/api.php');
$this->publishes([
__DIR__.'/../database/' => database_path('migrations')
], 'dcat-admin-order-migrations');
$this->loadMigrationsFrom(__DIR__.'/../database/');
$this->loadTranslationsFrom(__DIR__.'/../lang', 'dcat-admin-order');
}
}

316
src/OrderStore.php 100644
View File

@ -0,0 +1,316 @@
<?php
namespace Peidikeji\Order;
use App\Services\ShippingMoneyService;
use Peidikeji\Goods\Models\Goods;
use Peidikeji\Order\Enums\OrderScene;
use Peidikeji\Order\Enums\ShipWay;
use Peidikeji\Order\Events\OrderCreated;
use Peidikeji\Order\Exceptions\OrderException;
use Peidikeji\Order\Models\Order;
use Peidikeji\Setting\Models\Setting;
class OrderStore
{
// 订单商品列表
public $orderGoodsList;
// 商品列表
public $goodsList;
// 订单场景
public $scene;
// 下单用户
public $user;
// 指定店铺
public $merchant;
// 订单总额/商品总额
public $money;
// 用户备注
public $remarks;
// amount: 使用积分, money: 积分抵扣金额, ratio: 抵扣比例
public $score = ['amount' => 0, 'money' => 0, 'ratio' => 0];
// is: 是否Vip, money: 会员累计优惠金额
public $vip = ['is' => false, 'money' => 0];
// address: 配送地址, money: 配送费, way: 配送方式
public $ship = ['address' => null, 'money' => 0, 'way' => null];
public function __construct($scene = null)
{
$this->scene = $scene;
$this->orderGoodsList = collect();
}
public function scene($scene)
{
$this->scene = $scene;
return $this;
}
public function user($user)
{
$this->user = $user;
return $this;
}
public function merchant($merchant)
{
$this->merchant = $merchant;
return $this;
}
public function remarks($remarks)
{
$this->remarks = $remarks;
return $this;
}
/**
* 购买商品
*
* @param array $goods 商品参数 [{id, amount, spec: [{name, value}]}]
* @param bool $vip 是否使用Vip价格
*/
public function goods($params, $vip = null)
{
$merchant = $this->merchant;
$query = Goods::show()->whereIn('id', array_column($params, 'id'));
if ($this->scene === OrderScene::Online->value) {
$query->whereNull('merchant_id');
}
else if ($this->scene === OrderScene::Merchant->value || $this->scene === OrderScene::Scan->value) {
if (!$merchant) {
throw new OrderException('请选择店铺');
}
$query->where('merchant_id', $merchant->id);
}
$goodsList = $query->get();
if ($goodsList->count() === 0) {
throw new OrderException('请选择有效商品');
}
$orderGoodsList = collect();
$user = $this->user;
if ($vip === null && $user) {
$vip = $user->isVip();
}
foreach($params as $item) {
$goods = $goodsList->firstWhere('id', $item['id']);
if (!$goods->on_sale) {
throw new OrderException($goods->name . ' 未上架');
}
$stock = $goods->stock;
$orderGoods = [
'goods_id' => $goods->id,
'merchant_id' => $goods->merchant_id,
'goods_name' => $goods->name,
'goods_sn' => $goods->sn,
'cover_image' => $goods->cover_image,
'price' => $goods->price,
'vip_price' => $goods->vip_price,
'score_max_amount' => $goods->score_max_amount,
'attr' => $goods->attr,
'spec' => data_get($item, 'spec'),
'part' => data_get($item, 'part'),
'amount' => data_get($item, 'amount', 1),
'money' => 0,
'shipping_tmp_id' => $goods->shipping_tmp_id,
'weight' => $goods->weight,
'volume' => $goods->volume,
];
if (isset($item['spec']) && $item['spec']) {
$sku = $goods->skus()->jsonArray($item['spec'])->first();
if (!$sku) {
throw new OrderException($goods->name . ' 规格不存在');
}
$stock = $sku->stock;
$orderGoods['goods_sku_id'] = $sku->id;
$orderGoods['goods_sku_sn'] = $sku->sn;
$orderGoods['vip_price'] = $sku->vip_price;
$orderGoods['price'] = $sku->price;
$orderGoods['shipping_tmp_id'] = $sku->shipping_tmp_id;
$orderGoods['weight'] = $sku->weight;
$orderGoods['volume'] = $sku->volume;
}
if ($orderGoods['amount'] > $stock) {
throw new OrderException($goods->name . ' 库存不足');
}
$orderGoods['money'] = round($orderGoods['price'] * $orderGoods['amount'], 2, PHP_ROUND_HALF_DOWN);
$orderGoodsList->push($orderGoods);
}
$this->goodsList = $goodsList;
$this->orderGoodsList = $orderGoodsList;
$this->money = $orderGoodsList->sum('money');
$this->vip = ['is' => $vip, 'money' => $vip ? $orderGoodsList->sum(fn($item) => $item['price'] - $item['vip_price']) : 0];
return $this;
}
/**
* 配送
*
* @param string $way 配送方式
* @param array $address 地址 ['name' => '收货人', 'phone' => '18223350967', 'region' => ['重庆市', '重庆市', '渝北区'], 'zone' => [2221, 2222, 2234], 'address' => '线外城市花园3栋20楼'];
*/
public function ship($way, $address = null)
{
$money = 0;
if ($way === ShipWay::Express->value && $address) {
$shipService = new ShippingMoneyService();
$money = $shipService->getShippingMoney($this->orderGoodsList->map(fn($item) => [
'shipping_tmp_id' => $item['shipping_tmp_id'],
'weight' => $item['weight'],
'volume' => $item['volume'],
'num' => $item['amount'],
'price' => $item['price'],
]), data_get($address, 'zone.2'));
}
$this->ship = ['way' => $way, 'address' => $address, 'money' => $money];
return $this;
}
/**
* 设置订单总金额
*/
public function money($money)
{
$this->money = $money;
return $this;
}
/**
* 使用积分抵扣
*
* @param float $amount 积分数量
* @param float $ratio 抵扣比例(0-1)
* @param float $money 金额(默认 = $amount * $ratio)
*/
public function score($amount, $ratio = null, $money = null)
{
$user = $this->user;
if ($amount > 0 && $user) {
// 最大抵扣数量
$maxAmount = $this->orderGoodsList->sum('score_max_amount');
if ($amount > $maxAmount) {
throw new OrderException($maxAmount > 0 ? '本次订单最多使用 '.$maxAmount.' 积分抵扣' : '订单不支持使用积分抵扣');
}
if ($user->profit < $amount) {
throw new OrderException('用户积分不足');
}
if ($ratio === null) {
$ratio = round(Setting::where('slug', 'discount_profit_ratio')->value('value'), 2, PHP_ROUND_HALF_DOWN);
}
if ($ratio === null) {
throw new OrderException('未配置积分抵扣比例');
}
if ($money === null) {
$money = $amount * $ratio;
}
$money = round($money, 2, PHP_ROUND_HALF_DOWN);
$this->score = compact('money', 'amount', 'ratio');
}
return $this;
}
/**
* 生成订单
*
* @return Order
*/
public function create()
{
$order = new Order([
'sn' => OrderService::make()->generateSn(),
]);
$order->scene = $this->scene;
$order->user_id = $this->user?->id;
$order->merchant_id = $this->merchant?->id;
$order->ship_address = $this->ship['address'];
$order->ship_way = $this->ship['way'];
$order->ship_money = $this->ship['money'];
$order->user_remarks = $this->remarks;
$order->total_money = $this->money;
$order->score_discount_amount = $this->score['amount'];
$order->score_discount_ratio = $this->score['ratio'];
$order->score_discount_money = $this->score['money'];
$order->vip_discount_money = $this->vip['money'];
$order->pay_money = $this->getPayMoney();
$order->user_get_profit = $this->getUserBonus($order->pay_money);
$order->save();
$order->goods()->createMany($this->orderGoodsList);
event(new OrderCreated($order));
return $order;
}
/**
* 获取付款金额
*
* @return float
*/
public function getPayMoney()
{
$money = $this->money;
// 积分抵扣
$money -= $this->score['money'];
// Vip 优惠
$money -= $this->vip['money'];
// 配送费
$money += $this->ship['money'];
return $money;
}
/**
* 用户应得奖励
*
* @param float $payMoney 支付金额
* @return float
*/
public function getUserBonus($payMoney = null)
{
$money = 0;
if ($this->scene === OrderScene::Merchant->value && $this->merchant) {
$payMoney = is_null($payMoney) ? $this->getPayMoney() : $payMoney;
$money = round($payMoney * ($this->merchant->profit_ratio / 100), 2, PHP_ROUND_HALF_DOWN);
}
return $money;
}
public static function init(...$params)
{
return new static(...$params);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Peidikeji\Order\Renderable;
use Dcat\Admin\Support\LazyRenderable;
class ShipLog extends LazyRenderable
{
public function render()
{
return $this->id;
}
}