From eaef1d26a61cc1653ec75e304799a27f549a2083 Mon Sep 17 00:00:00 2001
From: panliang <1163816051@qq.com>
Date: Wed, 31 Aug 2022 15:35:34 +0800
Subject: [PATCH] init
---
.gitignore | 7 +
README.md | 26 ++
composer.json | 33 ++
.../2022_08_15_083640_create_orders_table.php | 112 +++++
lang/en/order.php | 5 +
lang/zh_CN/order.php | 44 ++
routes/admin.php | 12 +
routes/api.php | 20 +
src/Action/ShowCancel.php | 58 +++
src/Action/ShowDelete.php | 45 ++
src/Action/ShowPay.php | 35 ++
src/Action/ShowPayQuery.php | 52 +++
src/Action/ShowReceive.php | 57 +++
src/Action/ShowRemarks.php | 24 ++
src/Action/ShowShip.php | 35 ++
src/Action/ShowShipQrcode.php | 33 ++
src/Console/OrdeReceive.php | 60 +++
src/Console/OrderCancel.php | 58 +++
src/Enums/OrderScene.php | 51 +++
src/Enums/OrderShipStatus.php | 72 ++++
src/Enums/OrderStatus.php | 75 ++++
src/Enums/PayStatus.php | 58 +++
src/Enums/PayWay.php | 48 +++
src/Enums/RefundStatus.php | 68 +++
src/Enums/ShipStatus.php | 59 +++
src/Enums/ShipWay.php | 51 +++
src/Enums/WxPayStatus.php | 30 ++
src/Events/OrderCanceled.php | 24 ++
src/Events/OrderCreated.php | 23 +
src/Events/OrderDeleted.php | 24 ++
src/Events/OrderPaid.php | 23 +
src/Events/OrderReceived.php | 23 +
src/Events/OrderShipped.php | 29 ++
src/Exceptions/OrderException.php | 13 +
src/Filters/OrderFilter.php | 46 ++
src/Form/PayForm.php | 68 +++
src/Form/RemarksForm.php | 46 ++
src/Form/ShipForm.php | 105 +++++
src/Http/Admin/OrderController.php | 197 +++++++++
src/Http/Api/OrderController.php | 407 ++++++++++++++++++
src/Http/Api/WxPayNotifyController.php | 52 +++
src/Http/Resources/OrderGoodsResource.php | 36 ++
src/Http/Resources/OrderResource.php | 58 +++
src/Http/Resources/OrderShipResouce.php | 25 ++
src/Listeners/OrderDiscountProfit.php | 49 +++
src/Listeners/OrderInviteProfit.php | 51 +++
src/Listeners/OrderMakeShipQrcode.php | 21 +
src/Listeners/OrderSendProfit.php | 76 ++++
src/Listeners/UpdateGoodsSoldCount.php | 43 ++
src/Listeners/UpdateGoodsStock.php | 37 ++
src/Models/Order.php | 243 +++++++++++
src/Models/OrderGoods.php | 50 +++
src/Models/OrderOption.php | 27 ++
src/Models/OrderShip.php | 41 ++
src/Models/OrderShipGoods.php | 26 ++
src/OrderService.php | 400 +++++++++++++++++
src/OrderServiceProvider.php | 35 ++
src/OrderStore.php | 316 ++++++++++++++
src/Renderable/ShipLog.php | 13 +
59 files changed, 3855 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 composer.json
create mode 100644 database/2022_08_15_083640_create_orders_table.php
create mode 100644 lang/en/order.php
create mode 100644 lang/zh_CN/order.php
create mode 100644 routes/admin.php
create mode 100644 routes/api.php
create mode 100644 src/Action/ShowCancel.php
create mode 100644 src/Action/ShowDelete.php
create mode 100644 src/Action/ShowPay.php
create mode 100644 src/Action/ShowPayQuery.php
create mode 100644 src/Action/ShowReceive.php
create mode 100644 src/Action/ShowRemarks.php
create mode 100644 src/Action/ShowShip.php
create mode 100644 src/Action/ShowShipQrcode.php
create mode 100644 src/Console/OrdeReceive.php
create mode 100644 src/Console/OrderCancel.php
create mode 100644 src/Enums/OrderScene.php
create mode 100644 src/Enums/OrderShipStatus.php
create mode 100644 src/Enums/OrderStatus.php
create mode 100644 src/Enums/PayStatus.php
create mode 100644 src/Enums/PayWay.php
create mode 100644 src/Enums/RefundStatus.php
create mode 100644 src/Enums/ShipStatus.php
create mode 100644 src/Enums/ShipWay.php
create mode 100644 src/Enums/WxPayStatus.php
create mode 100644 src/Events/OrderCanceled.php
create mode 100644 src/Events/OrderCreated.php
create mode 100644 src/Events/OrderDeleted.php
create mode 100644 src/Events/OrderPaid.php
create mode 100644 src/Events/OrderReceived.php
create mode 100644 src/Events/OrderShipped.php
create mode 100644 src/Exceptions/OrderException.php
create mode 100644 src/Filters/OrderFilter.php
create mode 100644 src/Form/PayForm.php
create mode 100644 src/Form/RemarksForm.php
create mode 100644 src/Form/ShipForm.php
create mode 100644 src/Http/Admin/OrderController.php
create mode 100644 src/Http/Api/OrderController.php
create mode 100644 src/Http/Api/WxPayNotifyController.php
create mode 100644 src/Http/Resources/OrderGoodsResource.php
create mode 100644 src/Http/Resources/OrderResource.php
create mode 100644 src/Http/Resources/OrderShipResouce.php
create mode 100644 src/Listeners/OrderDiscountProfit.php
create mode 100644 src/Listeners/OrderInviteProfit.php
create mode 100644 src/Listeners/OrderMakeShipQrcode.php
create mode 100644 src/Listeners/OrderSendProfit.php
create mode 100644 src/Listeners/UpdateGoodsSoldCount.php
create mode 100644 src/Listeners/UpdateGoodsStock.php
create mode 100644 src/Models/Order.php
create mode 100644 src/Models/OrderGoods.php
create mode 100644 src/Models/OrderOption.php
create mode 100644 src/Models/OrderShip.php
create mode 100644 src/Models/OrderShipGoods.php
create mode 100644 src/OrderService.php
create mode 100644 src/OrderServiceProvider.php
create mode 100644 src/OrderStore.php
create mode 100644 src/Renderable/ShipLog.php
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9d4b362
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+phpunit.phar
+/vendor
+composer.phar
+composer.lock
+*.project
+.idea/
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..373eb9d
--- /dev/null
+++ b/README.md
@@ -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,
+ ]
+];
+```
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..773535b
--- /dev/null
+++ b/composer.json
@@ -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"
+ ]
+ }
+ }
+}
diff --git a/database/2022_08_15_083640_create_orders_table.php b/database/2022_08_15_083640_create_orders_table.php
new file mode 100644
index 0000000..519a465
--- /dev/null
+++ b/database/2022_08_15_083640_create_orders_table.php
@@ -0,0 +1,112 @@
+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');
+ }
+};
diff --git a/lang/en/order.php b/lang/en/order.php
new file mode 100644
index 0000000..ca5d8ed
--- /dev/null
+++ b/lang/en/order.php
@@ -0,0 +1,5 @@
+ [
+ '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' => '配送费',
+ ]
+];
diff --git a/routes/admin.php b/routes/admin.php
new file mode 100644
index 0000000..2b07f5f
--- /dev/null
+++ b/routes/admin.php
@@ -0,0 +1,12 @@
+ config('admin.route.prefix'),
+ 'middleware' => config('admin.route.middleware'),
+], function () {
+ Route::resource('orders', OrderController::class)->only(['index', 'show', 'destroy'])->names('dcat.admin.order');
+});
diff --git a/routes/api.php b/routes/api.php
new file mode 100644
index 0000000..81a694e
--- /dev/null
+++ b/routes/api.php
@@ -0,0 +1,20 @@
+ '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);
+});
diff --git a/src/Action/ShowCancel.php b/src/Action/ShowCancel.php
new file mode 100644
index 0000000..8a8c44e
--- /dev/null
+++ b/src/Action/ShowCancel.php
@@ -0,0 +1,58 @@
+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();
+ }
+}
diff --git a/src/Action/ShowDelete.php b/src/Action/ShowDelete.php
new file mode 100644
index 0000000..ed623dd
--- /dev/null
+++ b/src/Action/ShowDelete.php
@@ -0,0 +1,45 @@
+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();
+ }
+}
diff --git a/src/Action/ShowPay.php b/src/Action/ShowPay.php
new file mode 100644
index 0000000..12d58a1
--- /dev/null
+++ b/src/Action/ShowPay.php
@@ -0,0 +1,35 @@
+parent->model();
+ return Modal::make()
+ ->lg()
+ ->title($this->title)
+ ->body(PayForm::make()->payload(['id' => $model->id, 'pay_money' => $model->pay_money]))
+ ->button('');
+ }
+
+ protected function authorize($user): bool
+ {
+ return $user->can('dcat.admin.order.pay');
+ }
+
+ public function allowed()
+ {
+ $model = $this->parent->model();
+ return $model->canPay();
+ }
+}
diff --git a/src/Action/ShowPayQuery.php b/src/Action/ShowPayQuery.php
new file mode 100644
index 0000000..abd0c8b
--- /dev/null
+++ b/src/Action/ShowPayQuery.php
@@ -0,0 +1,52 @@
+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;
+ }
+}
diff --git a/src/Action/ShowReceive.php b/src/Action/ShowReceive.php
new file mode 100644
index 0000000..a92453a
--- /dev/null
+++ b/src/Action/ShowReceive.php
@@ -0,0 +1,57 @@
+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();
+ }
+}
diff --git a/src/Action/ShowRemarks.php b/src/Action/ShowRemarks.php
new file mode 100644
index 0000000..9f2c097
--- /dev/null
+++ b/src/Action/ShowRemarks.php
@@ -0,0 +1,24 @@
+parent->model();
+ return Modal::make()
+ ->lg()
+ ->title($this->title)
+ ->body(RemarksForm::make(['remarks' => $model->remarks])->payload(['id' => $model->id]))
+ ->button('');
+ }
+}
diff --git a/src/Action/ShowShip.php b/src/Action/ShowShip.php
new file mode 100644
index 0000000..35dd7d6
--- /dev/null
+++ b/src/Action/ShowShip.php
@@ -0,0 +1,35 @@
+parent->model();
+ return Modal::make()
+ ->xl()
+ ->title($this->title())
+ ->body(ShipForm::make()->payload(['id' => $model->id, 'ship_way' => $model->ship_way]))
+ ->button('');
+ }
+
+ protected function authorize($user): bool
+ {
+ return $user->can('dcat.admin.order.ship');
+ }
+
+ public function allowed()
+ {
+ $model = $this->parent->model();
+ return $model->canShip();
+ }
+}
diff --git a/src/Action/ShowShipQrcode.php b/src/Action/ShowShipQrcode.php
new file mode 100644
index 0000000..930f9f4
--- /dev/null
+++ b/src/Action/ShowShipQrcode.php
@@ -0,0 +1,33 @@
+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;
+ }
+}
diff --git a/src/Console/OrdeReceive.php b/src/Console/OrdeReceive.php
new file mode 100644
index 0000000..71c4f6c
--- /dev/null
+++ b/src/Console/OrdeReceive.php
@@ -0,0 +1,60 @@
+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());
+ }
+ }
+}
diff --git a/src/Console/OrderCancel.php b/src/Console/OrderCancel.php
new file mode 100644
index 0000000..17d9f5d
--- /dev/null
+++ b/src/Console/OrderCancel.php
@@ -0,0 +1,58 @@
+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());
+ }
+ }
+}
diff --git a/src/Enums/OrderScene.php b/src/Enums/OrderScene.php
new file mode 100644
index 0000000..91087c0
--- /dev/null
+++ b/src/Enums/OrderScene.php
@@ -0,0 +1,51 @@
+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 "{$name}";
+ }
+
+ public function dot()
+ {
+ $color = $this->color();
+
+ $name = $this->text();
+
+ return " {$name}";
+ }
+}
diff --git a/src/Enums/OrderShipStatus.php b/src/Enums/OrderShipStatus.php
new file mode 100644
index 0000000..ec94710
--- /dev/null
+++ b/src/Enums/OrderShipStatus.php
@@ -0,0 +1,72 @@
+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 "{$name}";
+ }
+
+ public function dot()
+ {
+ $color = $this->color();
+
+ $name = $this->text();
+
+ return " {$name}";
+ }
+}
diff --git a/src/Enums/OrderStatus.php b/src/Enums/OrderStatus.php
new file mode 100644
index 0000000..86cd425
--- /dev/null
+++ b/src/Enums/OrderStatus.php
@@ -0,0 +1,75 @@
+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 "{$name}";
+ }
+
+ 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 " {$name}";
+ }
+}
diff --git a/src/Enums/PayStatus.php b/src/Enums/PayStatus.php
new file mode 100644
index 0000000..8fc3143
--- /dev/null
+++ b/src/Enums/PayStatus.php
@@ -0,0 +1,58 @@
+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 "{$name}";
+ }
+
+ 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 " {$name}";
+ }
+}
diff --git a/src/Enums/PayWay.php b/src/Enums/PayWay.php
new file mode 100644
index 0000000..e3bb4f1
--- /dev/null
+++ b/src/Enums/PayWay.php
@@ -0,0 +1,48 @@
+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 "{$name}";
+ }
+
+ public function dot()
+ {
+ $color = $this->color();
+
+ $name = $this->text();
+
+ return " {$name}";
+ }
+}
diff --git a/src/Enums/RefundStatus.php b/src/Enums/RefundStatus.php
new file mode 100644
index 0000000..ab5d82e
--- /dev/null
+++ b/src/Enums/RefundStatus.php
@@ -0,0 +1,68 @@
+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 "{$name}";
+ }
+
+ 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 " {$name}";
+ }
+}
diff --git a/src/Enums/ShipStatus.php b/src/Enums/ShipStatus.php
new file mode 100644
index 0000000..8683f05
--- /dev/null
+++ b/src/Enums/ShipStatus.php
@@ -0,0 +1,59 @@
+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 "{$name}";
+ }
+
+ public function dot()
+ {
+ $color = $this->color();
+
+ $name = $this->text();
+
+ return " {$name}";
+ }
+}
diff --git a/src/Enums/ShipWay.php b/src/Enums/ShipWay.php
new file mode 100644
index 0000000..c2c1178
--- /dev/null
+++ b/src/Enums/ShipWay.php
@@ -0,0 +1,51 @@
+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 "{$name}";
+ }
+
+ public function dot()
+ {
+ $color = $this->color();
+
+ $name = $this->text();
+
+ return " {$name}";
+ }
+}
diff --git a/src/Enums/WxPayStatus.php b/src/Enums/WxPayStatus.php
new file mode 100644
index 0000000..6f756d0
--- /dev/null
+++ b/src/Enums/WxPayStatus.php
@@ -0,0 +1,30 @@
+value => '支付成功',
+ self::REFUND->value => '转入退款',
+ self::NOTPAY->value => '未支付',
+ self::CLOSED->value => '已关闭',
+ // 仅付款码支付会返回
+ self::REVOKED->value => '已撤销',
+ // 仅付款码支付会返回
+ self::USERPAYING->value => '用户支付中',
+ // 仅付款码支付会返回
+ self::PAYERROR->value => '支付失败',
+ ];
+ }
+}
diff --git a/src/Events/OrderCanceled.php b/src/Events/OrderCanceled.php
new file mode 100644
index 0000000..e02798a
--- /dev/null
+++ b/src/Events/OrderCanceled.php
@@ -0,0 +1,24 @@
+order = $order;
+ }
+}
diff --git a/src/Events/OrderCreated.php b/src/Events/OrderCreated.php
new file mode 100644
index 0000000..fbdbfe1
--- /dev/null
+++ b/src/Events/OrderCreated.php
@@ -0,0 +1,23 @@
+order = $order;
+ }
+}
diff --git a/src/Events/OrderDeleted.php b/src/Events/OrderDeleted.php
new file mode 100644
index 0000000..d5869f0
--- /dev/null
+++ b/src/Events/OrderDeleted.php
@@ -0,0 +1,24 @@
+order = $order;
+ }
+}
diff --git a/src/Events/OrderPaid.php b/src/Events/OrderPaid.php
new file mode 100644
index 0000000..e154c96
--- /dev/null
+++ b/src/Events/OrderPaid.php
@@ -0,0 +1,23 @@
+order = $order;
+ }
+}
diff --git a/src/Events/OrderReceived.php b/src/Events/OrderReceived.php
new file mode 100644
index 0000000..a89d977
--- /dev/null
+++ b/src/Events/OrderReceived.php
@@ -0,0 +1,23 @@
+order = $order;
+ }
+}
diff --git a/src/Events/OrderShipped.php b/src/Events/OrderShipped.php
new file mode 100644
index 0000000..1894531
--- /dev/null
+++ b/src/Events/OrderShipped.php
@@ -0,0 +1,29 @@
+order = $order;
+ $this->ship = $ship;
+ }
+}
diff --git a/src/Exceptions/OrderException.php b/src/Exceptions/OrderException.php
new file mode 100644
index 0000000..fea5ff1
--- /dev/null
+++ b/src/Exceptions/OrderException.php
@@ -0,0 +1,13 @@
+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('未知的订单状态');
+ }
+ }
+}
diff --git a/src/Form/PayForm.php b/src/Form/PayForm.php
new file mode 100644
index 0000000..615d979
--- /dev/null
+++ b/src/Form/PayForm.php
@@ -0,0 +1,68 @@
+ 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(),
+ ];
+ }
+}
diff --git a/src/Form/RemarksForm.php b/src/Form/RemarksForm.php
new file mode 100644
index 0000000..2171774
--- /dev/null
+++ b/src/Form/RemarksForm.php
@@ -0,0 +1,46 @@
+ 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');
+ }
+}
\ No newline at end of file
diff --git a/src/Form/ShipForm.php b/src/Form/ShipForm.php
new file mode 100644
index 0000000..a8cb77a
--- /dev/null
+++ b/src/Form/ShipForm.php
@@ -0,0 +1,105 @@
+ 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 .= ''.$i['value'].'';
+ }
+ 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();
+ }
+}
diff --git a/src/Http/Admin/OrderController.php b/src/Http/Admin/OrderController.php
new file mode 100644
index 0000000..822f80c
--- /dev/null
+++ b/src/Http/Admin/OrderController.php
@@ -0,0 +1,197 @@
+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 .= <<
+ $item->goods_name
+ $item->amount
+
+ 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;
+ }
+}
diff --git a/src/Http/Api/OrderController.php b/src/Http/Api/OrderController.php
new file mode 100644
index 0000000..20f8651
--- /dev/null
+++ b/src/Http/Api/OrderController.php
@@ -0,0 +1,407 @@
+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;
+ }
+}
diff --git a/src/Http/Api/WxPayNotifyController.php b/src/Http/Api/WxPayNotifyController.php
new file mode 100644
index 0000000..870b8b8
--- /dev/null
+++ b/src/Http/Api/WxPayNotifyController.php
@@ -0,0 +1,52 @@
+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();
+ }
+}
diff --git a/src/Http/Resources/OrderGoodsResource.php b/src/Http/Resources/OrderGoodsResource.php
new file mode 100644
index 0000000..5f46699
--- /dev/null
+++ b/src/Http/Resources/OrderGoodsResource.php
@@ -0,0 +1,36 @@
+ $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,
+ ];
+ }
+}
diff --git a/src/Http/Resources/OrderResource.php b/src/Http/Resources/OrderResource.php
new file mode 100644
index 0000000..255713f
--- /dev/null
+++ b/src/Http/Resources/OrderResource.php
@@ -0,0 +1,58 @@
+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'),
+ ];
+ }
+}
diff --git a/src/Http/Resources/OrderShipResouce.php b/src/Http/Resources/OrderShipResouce.php
new file mode 100644
index 0000000..caa14fb
--- /dev/null
+++ b/src/Http/Resources/OrderShipResouce.php
@@ -0,0 +1,25 @@
+ $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'),
+ ];
+ }
+}
diff --git a/src/Listeners/OrderDiscountProfit.php b/src/Listeners/OrderDiscountProfit.php
new file mode 100644
index 0000000..275d407
--- /dev/null
+++ b/src/Listeners/OrderDiscountProfit.php
@@ -0,0 +1,49 @@
+ 扣除用户的积分
+ * 订单已退款 => 返还用户扣除的积分
+ */
+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,
+ ]);
+ }
+ }
+ }
+}
diff --git a/src/Listeners/OrderInviteProfit.php b/src/Listeners/OrderInviteProfit.php
new file mode 100644
index 0000000..a3a56ba
--- /dev/null
+++ b/src/Listeners/OrderInviteProfit.php
@@ -0,0 +1,51 @@
+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,
+ ]);
+ }
+ }
+}
diff --git a/src/Listeners/OrderMakeShipQrcode.php b/src/Listeners/OrderMakeShipQrcode.php
new file mode 100644
index 0000000..085a6cf
--- /dev/null
+++ b/src/Listeners/OrderMakeShipQrcode.php
@@ -0,0 +1,21 @@
+order;
+ if ($order->scene === OrderScene::Merchant && $order->ship_way === ShipWay::Pick) {
+ $order->generateShipQrcode();
+ }
+ }
+}
diff --git a/src/Listeners/OrderSendProfit.php b/src/Listeners/OrderSendProfit.php
new file mode 100644
index 0000000..257c172
--- /dev/null
+++ b/src/Listeners/OrderSendProfit.php
@@ -0,0 +1,76 @@
+ 增加用户积分, 扣除商户积分
+ * 订单已退款 => 积分原路返还
+ */
+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,
+ ]);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Listeners/UpdateGoodsSoldCount.php b/src/Listeners/UpdateGoodsSoldCount.php
new file mode 100644
index 0000000..e94080e
--- /dev/null
+++ b/src/Listeners/UpdateGoodsSoldCount.php
@@ -0,0 +1,43 @@
+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);
+ }
+ }
+ }
+}
diff --git a/src/Listeners/UpdateGoodsStock.php b/src/Listeners/UpdateGoodsStock.php
new file mode 100644
index 0000000..1f4c4e4
--- /dev/null
+++ b/src/Listeners/UpdateGoodsStock.php
@@ -0,0 +1,37 @@
+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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Models/Order.php b/src/Models/Order.php
new file mode 100644
index 0000000..f3f5d26
--- /dev/null
+++ b/src/Models/Order.php
@@ -0,0 +1,243 @@
+ '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');
+ }
+}
diff --git a/src/Models/OrderGoods.php b/src/Models/OrderGoods.php
new file mode 100644
index 0000000..374d48d
--- /dev/null
+++ b/src/Models/OrderGoods.php
@@ -0,0 +1,50 @@
+ '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');
+ }
+}
diff --git a/src/Models/OrderOption.php b/src/Models/OrderOption.php
new file mode 100644
index 0000000..070bfcc
--- /dev/null
+++ b/src/Models/OrderOption.php
@@ -0,0 +1,27 @@
+ 'json'
+ ];
+
+ public function order()
+ {
+ return $this->belongsTo(Order::class, 'order_id');
+ }
+
+ public function user()
+ {
+ return $this->morphTo();
+ }
+}
diff --git a/src/Models/OrderShip.php b/src/Models/OrderShip.php
new file mode 100644
index 0000000..145dfc6
--- /dev/null
+++ b/src/Models/OrderShip.php
@@ -0,0 +1,41 @@
+ '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');
+ }
+}
diff --git a/src/Models/OrderShipGoods.php b/src/Models/OrderShipGoods.php
new file mode 100644
index 0000000..f71bb6f
--- /dev/null
+++ b/src/Models/OrderShipGoods.php
@@ -0,0 +1,26 @@
+belongsTo(OrderShip::class, 'ship_id');
+ }
+
+ public function orderGoods()
+ {
+ return $this->belongsTo(OrderGoods::class, 'order_goods_id');
+ }
+}
diff --git a/src/OrderService.php b/src/OrderService.php
new file mode 100644
index 0000000..8fe60d5
--- /dev/null
+++ b/src/OrderService.php
@@ -0,0 +1,400 @@
+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;
+ }
+}
diff --git a/src/OrderServiceProvider.php b/src/OrderServiceProvider.php
new file mode 100644
index 0000000..6825fcf
--- /dev/null
+++ b/src/OrderServiceProvider.php
@@ -0,0 +1,35 @@
+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');
+ }
+}
diff --git a/src/OrderStore.php b/src/OrderStore.php
new file mode 100644
index 0000000..15be7fb
--- /dev/null
+++ b/src/OrderStore.php
@@ -0,0 +1,316 @@
+ 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);
+ }
+}
diff --git a/src/Renderable/ShipLog.php b/src/Renderable/ShipLog.php
new file mode 100644
index 0000000..f794586
--- /dev/null
+++ b/src/Renderable/ShipLog.php
@@ -0,0 +1,13 @@
+id;
+ }
+}