diff --git a/app/Admin/Controllers/OfflineOrderController.php b/app/Admin/Controllers/OfflineOrderController.php new file mode 100644 index 00000000..d0078a21 --- /dev/null +++ b/app/Admin/Controllers/OfflineOrderController.php @@ -0,0 +1,192 @@ +model()->orderBy('id', 'desc'); + + $grid->column('id')->sortable()->if(function () { + return Admin::user()->can('dcat.admin.offline_orders.show'); + })->then(function (Column $column) { + $column->link(function ($value) { + return admin_route('offline_orders.show', ['offline_order' => $value]); + }); + }); + $grid->column('sn')->copyable(); + $grid->column('user_id')->display(function () { + $nickname = $this->userInfo?->nickname ?? '---'; + $avatar = $this->userInfo?->avatar ?? 'https://via.placeholder.com/45x45.png'; + $phone = $this->user?->phone; + return <<
+ {$nickname} + {$phone} + HTML; + }); + $grid->column('store_id')->display(fn() => $this->store?->title); + $grid->column('products_total_amount')->display(fn($v) => bcdiv($v, 100, 2))->prepend('¥'); + $grid->column('discount_reduction_amount')->display(fn($v) => bcdiv($v, 100, 2))->prepend('¥'); + $grid->column('points_deduction_amount')->display(fn($v) => bcdiv($v, 100, 2))->prepend('¥'); + $grid->column('payment_amount')->display(fn($v) => bcdiv($v, 100, 2))->prepend('¥'); + $grid->column('status')->display(fn($v) => $v->label()); + $grid->column('payment_method')->display(fn($v) => $v?->dot()); + $grid->column('payment_time'); + $grid->column('created_at'); + + $grid->filter(function (Grid\Filter $filter) { + $filter->panel(); + $filter->like('sn')->width(3); + $filter->where('user_id', function ($q) { + $q->where(function ($q) { + $q->whereHas('user', fn($q) => $q->where('phone', 'like', '%'.$this->input.'%')) + ->orWhereHas('userinfo', fn($q) => $q->where('nickname', 'like', '%'.$this->input.'%')); + }); + })->width(3)->placeholder('昵称/手机号'); + $filter->like('payment_sn')->width(3); + $filter->like('out_trade_no')->width(3); + $filter->equal('store_id')->select(Store::pluck('title', 'id'))->width(3); + $filter->where('type', function ($builder) { + if ($this->input == 1) { + $builder->where('points_deduction_amount', '>', 0); + } else { + $builder->where('points_deduction_amount', 0); + } + }, '类型')->select([1 => '积分订单', 2 => '其它订单'])->width(3); + $filter->in('status')->multipleSelect(OfflineOrderStatus::options())->width(3); + $filter->equal('payment_method')->select([ + PayWay::WxpayMiniProgram->value => PayWay::WxpayMiniProgram->text(), + PayWay::None->value => PayWay::None->text(), + ])->width(3); + $filter->between('created_at')->dateTime()->width(6); + }); + + $user = Admin::user(); + + $grid->actions(function (Grid\Displayers\Actions $actions) use ($user) { + if ($user->can('dcat.admin.offline_orders.show')) { + $actions->disableView(false); + } + }); + + $grid->header(function ($collection) use ($grid) { + return tap(new Row(), function ($row) use ($grid) { + $query = OfflineOrder::query(); + $grid->model()->getQueries()->unique()->each(function ($value) use (&$query) { + if (in_array($value['method'], ['paginate', 'get', 'orderBy', 'orderByDesc'], true)) { + return; + } + + $query = call_user_func_array([$query, $value['method']], $value['arguments'] ?? []); + }); + $productsTotalAmount = (clone $query)->sum('products_total_amount'); + $discountReductionAmount = (clone $query)->sum('discount_reduction_amount'); + $pointsDiscountAmount = (clone $query)->sum('points_deduction_amount'); + $paymentAmount = (clone $query)->sum('payment_amount'); + + $row->column(2, new InfoBox('订单总额', bcdiv($productsTotalAmount, 100, 2), 'fa fa-ticket')); + $row->column(2, new InfoBox('折扣优惠', bcdiv($discountReductionAmount, 100, 2), 'fa fa-ticket')); + $row->column(2, new InfoBox('积分抵扣', bcdiv($pointsDiscountAmount, 100, 2), 'fa fa-ticket')); + $row->column(2, new InfoBox('实付金额', bcdiv($paymentAmount, 100, 2), 'fa fa-ticket')); + }); + }); + + return $grid; + } + + protected function detail($id) + { + return function (Row $row) use ($id) { + $row->column(5, function ($column) use ($id) { + $builder = OfflineOrderRepository::with(['user', 'userInfo', 'store', 'staff', 'staffInfo']); + $column->row(Show::make($id, $builder, function (Show $show) { + $show->row(function (Show\Row $show) { + $show->width(6)->field('sn'); + $show->width(6) + ->field('phone') + ->as(fn () => $this->user?->phone); + $show->field('store_id') + ->as(fn () => $this->store?->title); + $show->field('staff_id') + ->as(fn () => $this->staffInfo?->nickname ?? $this->staff?->phone); + $show->field('products_total_amount') + ->as(fn ($v) => bcdiv($v, 100, 2)) + ->prepend('¥'); + $show->field('discount_reduction_amount') + ->as(fn ($v) => bcdiv($v, 100, 2)) + ->prepend('-¥'); + $show->field('points_deduction_amount') + ->as(fn ($v) => bcdiv($v, 100, 2)) + ->prepend('-¥'); + $show->field('payment_amount') + ->as(fn ($v) => bcdiv($v, 100, 2)) + ->prepend('¥'); + $show->field('status') + ->escape(false) + ->as(fn () => $this->status->label()); + $show->field('payment_method') + ->escape(false) + ->as(fn () => $this->payment_method?->dot()); + $show->field('payment_time'); + $show->field('payment_sn'); + $show->field('out_trade_no'); + $show->field('created_at'); + }); + $show->panel()->tools(function (Show\Tools $tools) use ($show) { + $tools->disableEdit(); + $tools->disableDelete(); + }); + })); + }); + $row->column(7, function ($column) use ($id) { + $orderItemGrid = Grid::make(OfflineOrderItem::with(['productCategory'])->where('order_id', $id), function (Grid $grid) { + $grid->column('product_category_name') + ->display(fn() => $this->productCategory?->name ?: 'Unknown'); + $grid->column('products_total_amount', '商品总额') + ->display(fn ($v) => bcdiv($v, 100, 2))->prepend('¥'); + $grid->column('discount_reduction_amount', '折扣优惠') + ->display(fn ($v) => bcdiv($v, 100, 2))->prepend('¥'); + $grid->column('points_deduction_amount', '积分抵扣') + ->display(fn ($v) => bcdiv($v, 100, 2))->prepend('¥'); + $grid->column('payment_amount', '实付金额') + ->display(fn ($v) => bcdiv($v, 100, 2))->prepend('¥'); + $grid->disableActions(); + $grid->disablePagination(); + $grid->disableRefreshButton(); + }); + + $column->row(Box::make('订单明细', $orderItemGrid)); + }); + }; + } +} diff --git a/app/Admin/Repositories/OfflineOrder.php b/app/Admin/Repositories/OfflineOrder.php new file mode 100644 index 00000000..1e1e76e0 --- /dev/null +++ b/app/Admin/Repositories/OfflineOrder.php @@ -0,0 +1,11 @@ + ['index', 'create', 'store', 'edit', 'update', 'destroy'], ])->names('offline_product_categories'); + $router->resource('offline-orders', 'OfflineOrderController', [ + 'only' => ['index', 'show'], + ])->names('offline_orders'); + /** 调试接口 **/ // $router->get('test', 'HomeController@test'); diff --git a/app/Endpoint/Api/Http/Controllers/OfflineOrderController.php b/app/Endpoint/Api/Http/Controllers/OfflineOrderController.php new file mode 100644 index 00000000..06fd21f8 --- /dev/null +++ b/app/Endpoint/Api/Http/Controllers/OfflineOrderController.php @@ -0,0 +1,76 @@ +validate([ + 'order_preview_id' => ['bail', 'required', 'int'], + ]); + + $preview = OfflineOrderPreview::findOrFail($request->input('order_preview_id')); + + return $offlineOrderService->check($request->user(), $preview->payload['items']); + } + + /** + * 创建订单 + */ + public function store(Request $request, OfflineOrderService $offlineOrderService) + { + $request->validate([ + 'order_preview_id' => ['bail', 'required', 'int'], + 'points' => ['bail', 'nullable', 'numeric', 'min:0'], + ]); + + try { + DB::beginTransaction(); + + $preview = OfflineOrderPreview::findOrFail($request->input('order_preview_id')); + + $order = $offlineOrderService->create( + $request->user(), + $preview, + bcmul($request->input('points', 0), 100), + ); + + DB::commit(); + } catch (Throwable $th) { + report($th); + + throw $th; + } + + return $order; + } + + /** + * 订单付款 + */ + public function pay($id, Request $request) + { + $user = $request->user(); + + return DB::transaction(function () use ($id, $user) { + $order = OfflineOrder::where('user_id', $user->id) + ->where('id', $id) + ->firstOrFail(); + + return (new OfflineOrderService())->pay($order, PayWay::WxpayMiniProgram); + }); + } +} diff --git a/app/Endpoint/Api/Http/Controllers/OfflineOrderPreviewController.php b/app/Endpoint/Api/Http/Controllers/OfflineOrderPreviewController.php new file mode 100644 index 00000000..a3307a2b --- /dev/null +++ b/app/Endpoint/Api/Http/Controllers/OfflineOrderPreviewController.php @@ -0,0 +1,85 @@ +validate([ + 'store_id' => ['bail', 'required', 'int'], + 'items' => ['bail', 'required', 'array'], + 'items.*.product_category_id' => ['bail', 'required', 'int'], + 'items.*.products_total_amount' => ['bail', 'required', 'numeric', 'min:0', 'regex:/^([1-9]\d*|0)(\.\d{1,2})?$/'], + 'items.*.discount' => ['bail', 'nullable', 'numeric', 'gt:0', 'lt:10', 'regex:/^[0-9](\.\d{1,2})?$/'], + ]); + + $user = $request->user(); + + if (! $user->userInfo->is_company) { + throw new BizException('非内部员工'); + } + + $store = Store::findOrFail($request->input('store_id')); + + $preview = OfflineOrderPreview::create([ + 'store_id' => $store->id, + 'staff_id' => $user->id, + 'payload' => ['items' => $request->input('items')], + ]); + + $scene = http_build_query([ + 'offline_order' => $preview->id, + ]); + + // 生成小程序码 + $app = Factory::miniProgram(config('wechat.mini_program.default')); + + $response = $app->app_code->getUnlimit($scene, [ + 'page' => 'pages/welcome/index', + 'check_path' => false, + 'env_version' => app()->isProduction() ? 'release' : $request->input('env_version', 'trial'), + 'width' => $request->input('width', 200), + ]); + + // 保存小程序码 + if ($response instanceof StreamResponse) { + $directory = 'offline-order-preview'; + $filename = "{$preview->id}.png"; + + $disk = Storage::disk('public'); + + $response->save($disk->path($directory), $filename); + + $preview->update(['qrcode' => $disk->url("{$directory}/{$filename}")]); + + return response()->json([ + 'id' => $preview->id, + 'qrcode' => $preview->qrcode, + ]); + } + + logger('offline_order_preview 小程序码生成失败', $response); + + throw new BizException('生成失败, 请重试'); + } + + public function show($id) + { + $preview = OfflineOrderPreview::findOrFail($id); + + return response()->json([ + 'id' => $preview->id, + 'qrcode' => $preview->qrcode, + ]); + } +} diff --git a/app/Endpoint/Api/routes.php b/app/Endpoint/Api/routes.php index 7f0b2c03..c6132f15 100644 --- a/app/Endpoint/Api/routes.php +++ b/app/Endpoint/Api/routes.php @@ -221,6 +221,12 @@ Route::group([ // 抽奖活动抽奖 Route::post('draw-activities/{draw_activity}/draw', [App\Endpoint\Api\Http\Controllers\DrawActivityController::class, 'draw']); Route::put('draw-activities/{draw_activity}/logs/{log}', [App\Endpoint\Api\Http\Controllers\DrawLogController::class, 'update']); + + Route::get('offline-order-previews/{offline_order_preview}', [App\Endpoint\Api\Http\Controllers\OfflineOrderPreviewController::class, 'show']); + Route::post('offline-order-previews', [App\Endpoint\Api\Http\Controllers\OfflineOrderPreviewController::class, 'store']); + Route::post('offline-orders/check', [App\Endpoint\Api\Http\Controllers\OfflineOrderController::class, 'check']); + Route::post('offline-orders', [App\Endpoint\Api\Http\Controllers\OfflineOrderController::class, 'store']); + Route::post('offline-orders/{offline_order}/pay', [App\Endpoint\Api\Http\Controllers\OfflineOrderController::class, 'pay']); }); // 微信小程序 diff --git a/app/Enums/OfflineOrderStatus.php b/app/Enums/OfflineOrderStatus.php new file mode 100644 index 00000000..eb9d198e --- /dev/null +++ b/app/Enums/OfflineOrderStatus.php @@ -0,0 +1,42 @@ +get($this->color()); + + return "{$this->text()}"; + } + + public function text() + { + return static::options()[$this->value] ?? 'Unknown'; + } + + public function color() + { + return match ($this) { + static::Pending => 'primary', + static::Paid => 'success', + static::Revoked => '#b3b9bf', + default => 'warning', + }; + } + + public static function options(): array + { + return [ + self::Pending->value => '待付款', + self::Paid->value => '已付款', + self::Revoked->value => '已取消', + ]; + } +} diff --git a/app/Enums/PayWay.php b/app/Enums/PayWay.php index f7d92b23..51d722dc 100644 --- a/app/Enums/PayWay.php +++ b/app/Enums/PayWay.php @@ -2,6 +2,8 @@ namespace App\Enums; +use Dcat\Admin\Admin; + enum PayWay: string { case None = 'none'; case Offline = 'offline'; @@ -17,6 +19,15 @@ enum PayWay: string { // 阿里支付 case AlipayApp = 'alipay_app'; + public function dot() + { + $color = $this->color(); + + $color = Admin::color()->get($color, $color); + + return ' '.$this->text() ?? 'Unknown'; + } + public function color() { return match ($this) { diff --git a/app/Models/OfflineOrder.php b/app/Models/OfflineOrder.php new file mode 100644 index 00000000..4aee215f --- /dev/null +++ b/app/Models/OfflineOrder.php @@ -0,0 +1,92 @@ + 0, + 'points_deduction_amount' => 0, + 'status' => OfflineOrderStatus::Pending, + ]; + + protected $casts = [ + 'payment_time' => 'datetime', + 'status' => OfflineOrderStatus::class, + 'payment_method' => PayWay::class, + 'revoked_at' => 'datetime', + ]; + + protected $fillable = [ + 'user_id', + 'store_id', + 'staff_id', + 'sn', + 'products_total_amount', + 'discount_reduction_amount', + 'points_deduction_amount', + 'payment_amount', + 'payment_sn', + 'payment_method', + 'payment_time', + 'out_trade_no', + 'status', + 'revoked_at', + 'orderable_type', + 'orderable_id', + ]; + + public function store() + { + return $this->belongsTo(Store::class); + } + + public function staff() + { + return $this->belongsTo(User::class, 'staff_id'); + } + + public function staffInfo() + { + return $this->belongsTo(UserInfo::class, 'staff_id', 'user_id'); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function userInfo() + { + return $this->belongsTo(UserInfo::class, 'user_id', 'user_id'); + } + + public function payLogs() + { + return $this->morphMany(PayLog::class, 'payable'); + } + + public function isPending(): bool + { + return $this->status === OfflineOrderStatus::Pending; + } + + public function isPaid(): bool + { + return $this->status === OfflineOrderStatus::Paid; + } + + public function isRevoked(): bool + { + return $this->status === OfflineOrderStatus::Revoked; + } +} diff --git a/app/Models/OfflineOrderItem.php b/app/Models/OfflineOrderItem.php new file mode 100644 index 00000000..588f9cda --- /dev/null +++ b/app/Models/OfflineOrderItem.php @@ -0,0 +1,31 @@ + 0, + 'points_deduction_amount' => 0, + ]; + + protected $fillable = [ + 'order_id', + 'product_category_id', + 'products_total_amount', + 'discount_reduction_amount', + 'points_deduction_amount', + 'payment_amount', + ]; + + public function productCategory() + { + return $this->belongsTo(OfflineProductCategory::class, 'product_category_id'); + } +} diff --git a/app/Models/OfflineOrderPreview.php b/app/Models/OfflineOrderPreview.php new file mode 100644 index 00000000..151709c9 --- /dev/null +++ b/app/Models/OfflineOrderPreview.php @@ -0,0 +1,25 @@ + 'json', + ]; + + protected $fillable = [ + 'store_id', 'staff_id', 'payload', 'qrcode', + ]; + + public function store() + { + return $this->belongsTo(Store::class); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 28798ed3..6f20de05 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -58,6 +58,8 @@ class AppServiceProvider extends ServiceProvider 'user_vip' => \App\Models\UserVip::class, 'draw_log' => \App\Models\DrawLog::class, 'store_stock_batch' => \App\Models\Store\StockBatch::class, + 'offline_order' => \App\Models\OfflineOrder::class, + 'offline_order_preview' => \App\Models\OfflineOrderPreview::class, ]); JsonResource::withoutWrapping(); diff --git a/app/Services/OfflineOrderService.php b/app/Services/OfflineOrderService.php new file mode 100644 index 00000000..316b404c --- /dev/null +++ b/app/Services/OfflineOrderService.php @@ -0,0 +1,238 @@ +mapItems($orderPreview->payload['items']); + + [ + $productsTotalAmount, + $discountReductionAmount, + $paymentAmount, + ] = $this->calculateFees($items); + + // 积分抵扣金额 + $pointsDeductionAmount = $points; + + $paymentAmount -= $pointsDeductionAmount; + if ($paymentAmount < 0) { + $paymentAmount = 0; + } + + do { + $sn = serial_number(); + } while(OfflineOrder::where('sn', $sn)->exists()); + + /** @var \App\Models\OfflineOrder */ + $order = OfflineOrder::create([ + 'user_id' => $user->id, + 'store_id' => $orderPreview->store_id, + 'staff_id' => $orderPreview->staff_id, + 'sn' => $sn, + 'products_total_amount' => $productsTotalAmount, + 'discount_reduction_amount' => $discountReductionAmount, + 'points_deduction_amount' => $pointsDeductionAmount, + 'payment_amount' => $paymentAmount, + 'status' => OfflineOrderStatus::Pending, + 'orderable_type' => $orderPreview->getMorphClass(), + 'orderable_id' => $orderPreview->id, + ]); + + $this->insertOrderItems($order, $items); + + // 扣除积分 + if ($points > 0) { + (new PointService)->change($user, -$points, PointLogAction::Consumption, "线下订单{$order->sn}使用积分", $order); + } + + if ($order->payment_amount === 0) { + $this->pay($order, PayWay::None); + + $order->refresh(); + } + + return $order; + } + + public function check(User $user, array $items): array + { + $productCategoryIds = collect($items)->pluck('product_category_id'); + + $productCategories = OfflineProductCategory::query() + ->whereIn('id', $productCategoryIds->all()) + ->get() + ->keyBy('id'); + + if ($productCategories->count() != $productCategoryIds->count()) { + throw new BizException('商品分类异常'); + } + + $mapItems = $this->mapItems($items); + + [ + $productsTotalAmount, + $discountReductionAmount, + $paymentAmount, + ] = $this->calculateFees($mapItems); + + //--------------------------------------- + // 积分当钱花 + //--------------------------------------- + $remainingPoints = $user->userInfo->points; // 用户剩余积分 + $availablePoints = $paymentAmount > $remainingPoints ? $remainingPoints : $paymentAmount; // 可用积分 + + return [ + 'items' => collect($mapItems)->map(fn($item) => [ + 'product_category' => $productCategories->get($item['product_category_id']), + 'discount_reduction_amount' => bcdiv($item['discount_reduction_amount'], 100, 2), + 'products_total_amount' => bcdiv($item['products_total_amount'], 100, 2), + 'payment_amount' => bcdiv($item['payment_amount'], 100, 2), + ]), + 'products_total_amount' => bcdiv($productsTotalAmount, 100, 2), + 'discount_reduction_amount' => bcdiv($discountReductionAmount, 100, 2), + 'payment_amount' => bcdiv($paymentAmount, 100, 2), + 'remaining_points' => bcdiv($remainingPoints, 100, 2), // 剩余积分 + 'available_points' => bcdiv($availablePoints, 100, 2), // 可用积分 + 'points_discount_amount' => bcdiv($availablePoints, 100, 2), // 积分抵扣金额 + ]; + } + + public function pay(OfflineOrder $order, PayWay $payWay) + { + if (! $order->isPending()) { + throw new BizException('订单状态不是待付款'); + } + + $payLog = $order->payLogs()->create([ + 'pay_way' => $payWay, + ]); + + $data = null; + + if ($order->payment_amount === 0) { + (new PayService())->handleSuccess($payLog, [ + 'pay_at' => now(), + ]); + } elseif ($payLog->isWxpay()) { + if (is_null($tradeType = WxpayTradeType::tryFromPayWay($payLog->pay_way))) { + throw new BizException('支付方式 非法'); + } + $params = [ + 'body' => app_settings('app.app_name').'-线下订单', + 'out_trade_no' => $payLog->pay_sn, + 'total_fee' => $order->payment_amount, + 'trade_type' => $tradeType->value, + ]; + + if ($payLog->pay_way === PayWay::WxpayMiniProgram) { + $socialite = SocialiteUser::where([ + 'user_id' => $order->user_id, + 'socialite_type' => SocialiteType::WechatMiniProgram, + ])->first(); + + if ($socialite === null) { + throw new BizException('未绑定微信小程序'); + } + + $params['openid'] = $socialite->socialite_id; + } + + $data = (new WxpayService())->pay($params, match ($payLog->pay_way) { + PayWay::WxpayMiniProgram => 'mini_program', + default => 'default', + }); + } + + return [ + 'pay_way' => $payLog->pay_way, + 'data' => $data, + ]; + } + + protected function insertOrderItems(OfflineOrder $order, array $items) + { + $remainingPointDiscountAmount = $order->points_deduction_amount; + + OfflineOrderItem::insert( + collect($items)->map(function ($item) use ($order, &$remainingPointDiscountAmount) { + $pointsDeductionAmount = $item['payment_amount']; + if ($item['payment_amount'] > $remainingPointDiscountAmount) { + $pointsDeductionAmount = $remainingPointDiscountAmount; + } + $remainingPointDiscountAmount -= $pointsDeductionAmount; + + return [ + 'order_id' => $order->id, + 'product_category_id' => $item['product_category_id'], + 'products_total_amount' => $item['products_total_amount'], + 'discount_reduction_amount' => $item['discount_reduction_amount'], + 'points_deduction_amount' => $pointsDeductionAmount, + 'payment_amount' => $item['payment_amount'] - $pointsDeductionAmount, + 'created_at' => $order->created_at, + 'updated_at' => $order->updated_at, + ]; + })->all() + ); + } + + protected function mapItems(array $items): array + { + return collect($items)->map(function ($item) { + $productsTotalAmount = bcmul($item['products_total_amount'], 100); + if ($productsTotalAmount < 0) { + throw new BizException('商品总额不能小于0'); + } + + $discountReductionAmount = 0; + if (is_numeric($item['discount'])) { + if ($item['discount'] <= 0) { + throw new BizException('折扣必须大于0'); + } elseif ($item['discount'] >= 10) { + throw new BizException('折扣必须小于10'); + } + $discount = bcdiv($item['discount'], 10, 3); + $discountReductionAmount = round(bcmul($productsTotalAmount, $discount, 2)); + } + + return [ + 'product_category_id' => $item['product_category_id'], + 'products_total_amount' => (int) $productsTotalAmount, + 'discount_reduction_amount' => (int) $discountReductionAmount, + 'payment_amount' => (int) ($productsTotalAmount - $discountReductionAmount), + ]; + })->all(); + } + + protected function calculateFees(array $items) + { + $totalProductsTotalAmount = 0; + $totalDiscountReductionAmount = 0; + $totalPaymentAmount = 0; + + foreach ($items as $item) { + $totalProductsTotalAmount += $item['products_total_amount']; + $totalDiscountReductionAmount += $item['discount_reduction_amount']; + $totalPaymentAmount += $item['payment_amount']; + } + + return [$totalProductsTotalAmount, $totalDiscountReductionAmount, $totalPaymentAmount]; + } +} diff --git a/app/Services/PayService.php b/app/Services/PayService.php index f6a3bea2..b27716d4 100644 --- a/app/Services/PayService.php +++ b/app/Services/PayService.php @@ -2,10 +2,11 @@ namespace App\Services; +use App\Enums\OfflineOrderStatus; use App\Exceptions\BizException; use App\Exceptions\InvalidPaySerialNumberException; -use App\Models\{Order, OrderPre}; use App\Models\PayLog; +use App\Models\{OfflineOrder, Order, OrderPre}; class PayService { @@ -70,7 +71,21 @@ class PayService 'out_trade_no' => $payLog->out_trade_no, 'status' => Order::STATUS_PAID, ]); - } else if ($payable instanceof \App\Models\UserVip) { + } elseif ($payable instanceof OfflineOrder) { + if ($payable->isPaid()) { + throw new BizException('订单已支付'); + } elseif ($payable->isRevoked()) { + throw new BizException('订单取消'); + } + + $payable->update([ + 'payment_sn' => $payLog->pay_sn, + 'payment_method' => $payLog->pay_way, + 'payment_time' => $payLog->pay_at, + 'out_trade_no' => $payLog->out_trade_no, + 'status' => OfflineOrderStatus::Paid, + ]); + } elseif ($payable instanceof \App\Models\UserVip) { (new \App\Services\VipService())->success($payable, ['pay_at' => $payLog->pay_at]); } diff --git a/database/migrations/2023_10_30_133731_create_offline_order_previews_table.php b/database/migrations/2023_10_30_133731_create_offline_order_previews_table.php new file mode 100644 index 00000000..20fa0a64 --- /dev/null +++ b/database/migrations/2023_10_30_133731_create_offline_order_previews_table.php @@ -0,0 +1,35 @@ +id(); + $table->unsignedBigInteger('store_id')->nullable()->comment('门店ID'); + $table->unsignedBigInteger('staff_id')->nullable()->comment('店员ID'); + $table->text('payload')->nullable(); + $table->string('qrcode')->nullable()->comment('小程序码'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('offline_order_previews'); + } +} diff --git a/database/migrations/2023_10_30_165623_create_offline_orders_table.php b/database/migrations/2023_10_30_165623_create_offline_orders_table.php new file mode 100644 index 00000000..5029b223 --- /dev/null +++ b/database/migrations/2023_10_30_165623_create_offline_orders_table.php @@ -0,0 +1,51 @@ +id(); + $table->unsignedBigInteger('user_id')->comment('用户ID'); + $table->unsignedBigInteger('store_id')->nullable()->comment('门店ID'); + $table->unsignedBigInteger('staff_id')->nullable()->comment('店员ID'); + $table->string('sn')->comment('订单号'); + $table->unsignedBigInteger('products_total_amount')->default(0)->comment('订单总额'); + $table->unsignedBigInteger('discount_reduction_amount')->default(0)->comment('折扣减免金额'); + $table->unsignedBigInteger('points_deduction_amount')->default(0)->comment('积分抵扣金额'); + $table->unsignedBigInteger('payment_amount')->default(0)->comment('应付金额'); + $table->string('payment_sn')->nullable()->comment('支付单号'); + $table->string('payment_method')->nullable()->comment('支付方式'); + $table->timestamp('payment_time')->nullable()->comment('付款时间'); + $table->string('out_trade_no')->nullable()->comment('外部交易单号'); + $table->tinyInteger('status')->default(0)->comment('状态: 0 待付款, 1 已付款'); + $table->timestamp('revoked_at')->nullable()->comment('撤销时间'); + $table->nullableMorphs('orderable'); + $table->timestamps(); + + $table->index('user_id'); + $table->index('store_id'); + $table->index('staff_id'); + $table->index('status'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('offline_orders'); + } +} diff --git a/database/migrations/2023_10_31_151643_create_offline_order_items_table.php b/database/migrations/2023_10_31_151643_create_offline_order_items_table.php new file mode 100644 index 00000000..4fff8bcd --- /dev/null +++ b/database/migrations/2023_10_31_151643_create_offline_order_items_table.php @@ -0,0 +1,39 @@ +id(); + $table->unsignedBigInteger('order_id')->comment('线下订单ID'); + $table->unsignedBigInteger('product_category_id')->comment('商品分类ID'); + $table->unsignedBigInteger('products_total_amount')->default(0)->comment('商品总额'); + $table->unsignedBigInteger('discount_reduction_amount')->default(0)->comment('折扣减免金额'); + $table->unsignedBigInteger('points_deduction_amount')->default(0)->comment('积分抵扣金额'); + $table->unsignedBigInteger('payment_amount')->default(0)->comment('应付金额'); + $table->timestamps(); + + $table->index('order_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('offline_order_items'); + } +} diff --git a/database/seeders/AdminMenuSeeder.php b/database/seeders/AdminMenuSeeder.php index bce0c3d9..7c246c10 100644 --- a/database/seeders/AdminMenuSeeder.php +++ b/database/seeders/AdminMenuSeeder.php @@ -368,6 +368,11 @@ class AdminMenuSeeder extends Seeder 'icon' => '', 'uri' => 'offline-product-categories', ], + [ + 'title' => '线下订单', + 'icon' => '', + 'uri' => 'offline-orders', + ], ], ], ]; diff --git a/database/seeders/AdminPermissionSeeder.php b/database/seeders/AdminPermissionSeeder.php index 9e0f9373..83c47689 100644 --- a/database/seeders/AdminPermissionSeeder.php +++ b/database/seeders/AdminPermissionSeeder.php @@ -381,6 +381,11 @@ class AdminPermissionSeeder extends Seeder 'curd' => ['index', 'create', 'store', 'edit', 'update', 'destroy'], 'children' => [], ], + 'offline_orders' => [ + 'name' => '线下订单 - 线下订单', + 'curd' => ['index', 'show'], + 'children' => [], + ], ]; // try { // DB::begintransaction(); diff --git a/resources/lang/zh_CN/offline-order.php b/resources/lang/zh_CN/offline-order.php new file mode 100644 index 00000000..023d8615 --- /dev/null +++ b/resources/lang/zh_CN/offline-order.php @@ -0,0 +1,27 @@ + [ + 'OfflineOrder' => '线下订单', + 'offline-orders' => '线下订单', + ], + 'fields' => [ + 'sn' => '订单号', + 'user_id' => '用户', + 'store_id' => '门店', + 'staff_id' => '店员', + 'products_total_amount' => '订单总额', + 'discount_reduction_amount' => '折扣优惠', + 'points_deduction_amount' => '积分抵扣', + 'payment_amount' => '实付金额', + 'product_category_name' => '商品分类', + 'phone' => '手机号', + 'payment_sn' => '支付单号', + 'payment_method' => '支付方式', + 'payment_time' => '支付时间', + 'out_trade_no' => '外部交易单号', + 'status' => '状态', + ], + 'options' => [ + ], +];