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; + } +}