From 2471bec5e10f1f5b3dc9d32cfc078eb4d6baaf75 Mon Sep 17 00:00:00 2001 From: panliang <1163816051@qq.com> Date: Sun, 28 Jun 2026 20:13:29 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BA=BF=E4=B8=8B=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Admin/Controllers/CouponController.php | 2 +- .../Controllers/OfflineOrderController.php | 28 ++--- .../OfflineOrderItemStatisticController.php | 48 ++++++++ app/Admin/routes.php | 1 + app/Console/Commands/OfflineOrderShip.php | 47 +++++++ app/Console/Commands/OfflineOrderSuccess.php | 56 +++++++++ .../Api/Filters/OfflineOrderFilter.php | 36 ++++++ app/Endpoint/Api/Filters/UserCouponFilter.php | 3 + .../Controllers/OfflineOrderController.php | 32 ++++- .../OfflineOrderPreviewController.php | 47 ++++++- .../Http/Controllers/UserCouponController.php | 71 ++++++++++- .../Resources/OfflineOrderItemResource.php | 26 ++++ .../Resources/OfflineOrderPreviewResource.php | 31 +++++ .../Http/Resources/OfflineOrderResource.php | 41 +++++++ .../Api/Http/Resources/UserResource.php | 16 +++ app/Endpoint/Api/routes.php | 7 ++ app/Models/Coupon.php | 10 +- app/Models/OfflineOrder.php | 16 ++- app/Models/OfflineOrderPreview.php | 17 ++- app/Models/UserCoupon.php | 21 ++++ app/Services/OfflineOrderService.php | 116 +++++++++++++++++- app/Services/PayService.php | 2 +- app/Services/WxMpService.php | 42 +++++++ resources/lang/zh_CN/offline-order.php | 1 + 24 files changed, 681 insertions(+), 36 deletions(-) create mode 100644 app/Console/Commands/OfflineOrderShip.php create mode 100644 app/Console/Commands/OfflineOrderSuccess.php create mode 100644 app/Endpoint/Api/Filters/OfflineOrderFilter.php create mode 100644 app/Endpoint/Api/Http/Resources/OfflineOrderItemResource.php create mode 100644 app/Endpoint/Api/Http/Resources/OfflineOrderPreviewResource.php create mode 100644 app/Endpoint/Api/Http/Resources/OfflineOrderResource.php create mode 100644 app/Endpoint/Api/Http/Resources/UserResource.php create mode 100644 app/Services/WxMpService.php diff --git a/app/Admin/Controllers/CouponController.php b/app/Admin/Controllers/CouponController.php index f1deac4b..888f205b 100644 --- a/app/Admin/Controllers/CouponController.php +++ b/app/Admin/Controllers/CouponController.php @@ -120,7 +120,7 @@ class CouponController extends AdminController return $this->use_end_at?->format('Y-m-d H:i:s'); }); $grid->column('is_use', '是否使用')->display(function () { - return $this->is_use ? '已使用' : '未使用'; + return $this->is_use ? $this->use_at?->format('Y-m-d H:i:s') : '未使用'; }); $grid->disableActions(); diff --git a/app/Admin/Controllers/OfflineOrderController.php b/app/Admin/Controllers/OfflineOrderController.php index 45a42905..d960e4da 100644 --- a/app/Admin/Controllers/OfflineOrderController.php +++ b/app/Admin/Controllers/OfflineOrderController.php @@ -46,29 +46,11 @@ class OfflineOrderController extends AdminController }); $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; + return $this->user?->phone; }); $grid->column('store_id')->display(fn() => $this->store?->title); $grid->column('staff_id')->display(function () { - if (is_null($this->staff_id)) { - return ''; - } - - $nickname = $this->staffInfo?->nickname ?? '---'; - $avatar = $this->staffInfo?->avatar ?? 'https://via.placeholder.com/45x45.png'; - $phone = $this->staff?->phone; - return << - {$nickname} - {$phone} - HTML; + return $this->staff?->phone; }); $grid->column('items')->display(function ($items) { return $items->map(function ($item) { @@ -85,6 +67,7 @@ class OfflineOrderController extends AdminController $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('coupon_discount_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->dot()); $grid->column('payment_method')->display(fn($v) => $v?->dot()); @@ -175,7 +158,7 @@ class OfflineOrderController extends AdminController { return function (Row $row) use ($id) { $row->column(5, function ($column) use ($id) { - $builder = OfflineOrderRepository::with(['user', 'userInfo', 'store', 'staff', 'staffInfo']); + $builder = OfflineOrderRepository::with(['user', 'userInfo', 'store', 'staff', 'staffInfo', 'coupon']); $column->row(Show::make($id, $builder, function (Show $show) { $show->row(function (Show\Row $show) { $show->width(6)->field('sn'); @@ -195,6 +178,9 @@ class OfflineOrderController extends AdminController $show->field('points_deduction_amount') ->as(fn ($v) => bcdiv($v, 100, 2)) ->prepend('-¥'); + $show->field('coupon_discount_amount') + ->as(fn ($v) => bcdiv($v, 100, 2) . '('.$this->coupon?->coupon_name.')') + ->prepend('-¥'); $show->field('payment_amount') ->as(fn ($v) => bcdiv($v, 100, 2)) ->prepend('¥'); diff --git a/app/Admin/Controllers/OfflineOrderItemStatisticController.php b/app/Admin/Controllers/OfflineOrderItemStatisticController.php index 9eb55147..e24f17af 100644 --- a/app/Admin/Controllers/OfflineOrderItemStatisticController.php +++ b/app/Admin/Controllers/OfflineOrderItemStatisticController.php @@ -7,9 +7,11 @@ use App\Models\OfflineOrder; use App\Models\OfflineOrderItem; use App\Models\OfflineProductCategory; use App\Models\Store\Store; +use App\Models\User; use Dcat\Admin\Grid; use Dcat\Admin\Http\Controllers\AdminController; use Illuminate\Support\Facades\DB; +use Dcat\Admin\Layout\Content; class OfflineOrderItemStatisticController extends AdminController { @@ -109,4 +111,50 @@ class OfflineOrderItemStatisticController extends AdminController return $grid; } + + /** + * 员工业绩统计 + */ + public function staff(Content $content) + { + $table = (new OfflineOrder())->getTable(); + $stores = Store::pluck('title', 'id'); + + $builder = OfflineOrder::query()->with(['staff', 'store'])->select([ + "{$table}.staff_id", + "{$table}.store_id", + DB::raw("count(1) as count"), + DB::raw("sum({$table}.products_total_amount) as products_total_amount"), + DB::raw("sum({$table}.discount_reduction_amount) as discount_reduction_amount"), + DB::raw("sum({$table}.points_deduction_amount) as points_deduction_amount"), + DB::raw("sum({$table}.coupon_discount_amount) as coupon_discount_amount"), + DB::raw("sum({$table}.payment_amount) as payment_amount"), + ])->where('status', OfflineOrderStatus::Paid)->groupBy(["staff_id", "store_id"]); + + $grid = Grid::make($builder); + + $grid->filter(function (Grid\Filter $filter) use ($stores) { + $filter->panel(); + $filter->equal('store_id')->select($stores)->width(4); + $filter->between('created_at', "下单时间")->datetime()->width(4); + }); + $grid->column("staff_id", "员工")->display(fn () => $this->staff?->phone); + $grid->column("store_id", "门店")->display(fn () => $this->store?->title); + $grid->column("count", "订单数"); + $grid->column('products_total_amount', "订单总额")->display(fn ($v) => bcdiv($v, 100, 2)); + $grid->column('discount_reduction_amount', "折扣优惠")->display(fn ($v) => bcdiv($v, 100, 2)); + $grid->column('points_deduction_amount', "积分抵扣")->display(fn ($v) => bcdiv($v, 100, 2)); + $grid->column('coupon_discount_amount', "优惠券抵扣")->display(fn ($v) => bcdiv($v, 100, 2)); + $grid->column('payment_amount', "实付金额")->display(fn ($v) => bcdiv($v, 100, 2)); + + $grid->disablePagination(); + $grid->disableFilterButton(); + $grid->disableActions(); + + return $content + ->translation($this->translation()) + ->title("员工业绩统计") + ->description($this->description()['index'] ?? trans('admin.list')) + ->body($grid); + } } diff --git a/app/Admin/routes.php b/app/Admin/routes.php index ea4b9213..5ebe2b09 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -191,6 +191,7 @@ Route::group([ ])->names('offline_orders'); $router->get('offline-order-item-statistics', 'OfflineOrderItemStatisticController@index')->name('offline_order_item_statistics.index'); + $router->get('offline-order-staff-statistics', 'OfflineOrderItemStatisticController@staff')->name('offline_order_item_statistics.staff'); /** 调试接口 **/ // $router->get('test', 'HomeController@test'); diff --git a/app/Console/Commands/OfflineOrderShip.php b/app/Console/Commands/OfflineOrderShip.php new file mode 100644 index 00000000..84afeeb9 --- /dev/null +++ b/app/Console/Commands/OfflineOrderShip.php @@ -0,0 +1,47 @@ +argument('id'); + $order = OfflineOrder::findOrFail($id); + OfflineOrderService::make()->orderShip($order); + return 0; + } +} diff --git a/app/Console/Commands/OfflineOrderSuccess.php b/app/Console/Commands/OfflineOrderSuccess.php new file mode 100644 index 00000000..382d15f6 --- /dev/null +++ b/app/Console/Commands/OfflineOrderSuccess.php @@ -0,0 +1,56 @@ +argument("id"); + $order = OfflineOrder::findOrFail($id); + if ($order->status !== OfflineOrderStatus::Pending) { + $this->warn("订单已处理: " . $order->status->text()); + } + OfflineOrderService::make()->paySuccess($order, [ + "payment_sn" => "000", + "payment_method" => "wxpay_mp", + "payment_time" => now(), + "out_trade_no" => "000" + ]); + return 0; + } +} diff --git a/app/Endpoint/Api/Filters/OfflineOrderFilter.php b/app/Endpoint/Api/Filters/OfflineOrderFilter.php new file mode 100644 index 00000000..b6fa4582 --- /dev/null +++ b/app/Endpoint/Api/Filters/OfflineOrderFilter.php @@ -0,0 +1,36 @@ + [input_key1, input_key2]]. + * + * @var array + */ + public $relations = []; + + public function staff(string $value) + { + $this->where('staff_id', $value); + } + + public function user(string $value) + { + $this->where('user_id', $value); + } + + public function createdStart(string $value) + { + $this->where('created_at', '>=', $value); + } + + public function createdEnd(string $value) + { + $this->where('created_at', '<=', $value); + } +} diff --git a/app/Endpoint/Api/Filters/UserCouponFilter.php b/app/Endpoint/Api/Filters/UserCouponFilter.php index e3039165..0fc5f18f 100644 --- a/app/Endpoint/Api/Filters/UserCouponFilter.php +++ b/app/Endpoint/Api/Filters/UserCouponFilter.php @@ -9,6 +9,9 @@ class UserCouponFilter extends ModelFilter public function status($status) { switch ($status) { + case 'available': + $this->onlyAvailable(); + break; case 'unuse': $this->onlyUnuse(); break; diff --git a/app/Endpoint/Api/Http/Controllers/OfflineOrderController.php b/app/Endpoint/Api/Http/Controllers/OfflineOrderController.php index c85b89c4..a9b474e9 100644 --- a/app/Endpoint/Api/Http/Controllers/OfflineOrderController.php +++ b/app/Endpoint/Api/Http/Controllers/OfflineOrderController.php @@ -3,7 +3,9 @@ namespace App\Endpoint\Api\Http\Controllers; use App\Endpoint\Api\Http\Controllers\Controller; +use App\Endpoint\Api\Http\Resources\OfflineOrderResource; use App\Enums\PayWay; +use App\Exceptions\BizException; use App\Models\OfflineOrder; use App\Models\OfflineOrderPreview; use App\Services\OfflineOrderService; @@ -13,6 +15,32 @@ use Throwable; class OfflineOrderController extends Controller { + public function index(Request $request) + { + $user = $request->user(); + + $query = OfflineOrder::query()->with(['store'])->filter($request->all())->orderBy('created_at', 'desc'); + // 员工身份, 查询我创建的订单 + if ($request->input('use_staff')) { + $query->where('staff_id', $user->id); + } + // 用户身份, 查询我支付的订单 + if ($request->input('use_user')) { + $query->where('user_id', $user->id); + } + $list = $query->paginate($request->input('per_page')); + return OfflineOrderResource::collection($list); + } + + public function show(string $id) + { + $info = OfflineOrder::find($id); + if (!$info) { + throw new BizException("记录不存在"); + } + return OfflineOrderResource::make($info); + } + /** * 确认订单 */ @@ -24,7 +52,7 @@ class OfflineOrderController extends Controller $preview = OfflineOrderPreview::findOrFail($request->input('order_preview_id')); - return $offlineOrderService->check($request->user(), $preview->payload['items']); + return $offlineOrderService->check($request->user(), $preview); } /** @@ -46,7 +74,7 @@ class OfflineOrderController extends Controller $request->user(), $preview, bcmul($request->input('points', 0), 100), - ['user_remark' => $request->input('user_remark')] + ['user_remark' => $request->input('user_remark'), 'coupon_id' => $preview->coupon_id] ); DB::commit(); diff --git a/app/Endpoint/Api/Http/Controllers/OfflineOrderPreviewController.php b/app/Endpoint/Api/Http/Controllers/OfflineOrderPreviewController.php index 4a6ad9d3..9ebbb122 100644 --- a/app/Endpoint/Api/Http/Controllers/OfflineOrderPreviewController.php +++ b/app/Endpoint/Api/Http/Controllers/OfflineOrderPreviewController.php @@ -3,9 +3,11 @@ namespace App\Endpoint\Api\Http\Controllers; use App\Endpoint\Api\Http\Controllers\Controller; +use App\Endpoint\Api\Http\Resources\OfflineOrderPreviewResource; use App\Exceptions\BizException; use App\Models\OfflineOrderPreview; use App\Models\Store\Store; +use App\Models\UserCoupon; use EasyWeChat\Factory; use EasyWeChat\Kernel\Http\StreamResponse; use Illuminate\Http\Request; @@ -13,6 +15,21 @@ use Illuminate\Support\Facades\Storage; class OfflineOrderPreviewController extends Controller { + public function index(Request $request) + { + $user = $request->user(); + $query = OfflineOrderPreview::query()->with(['store', 'coupon', 'order'])->where('staff_id', $user->id)->orderBy('created_at', 'desc'); + $list = $query->paginate($request->input('per_page')); + + return OfflineOrderPreviewResource::collection($list); + } + + public function detail(string $id) + { + $info = OfflineOrderPreview::with(['store', 'coupon', 'order.user'])->findOrFail($id); + return OfflineOrderPreviewResource::make($info); + } + public function store(Request $request) { $request->validate([ @@ -30,12 +47,38 @@ class OfflineOrderPreviewController extends Controller } $store = Store::findOrFail($request->input('store_id')); + $couponId = $request->input('coupon_id'); + $userId = $request->input('user_id'); + $coupon = null; + $couponDiscount = 0; + if ($couponId) { + $coupon = UserCoupon::query()->onlyAvailable()->find($couponId); + if (!$coupon) { + throw new BizException('优惠券不可用'); + } + $userId = $coupon->user_id; + } + $items = $request->input('items'); + $totalAmount = 0; + $paymentAmount = 0; + $discountAmount = 0; + foreach($items as $item) { + $totalAmount += $item['products_total_amount']; + $paymentAmount += bcmul($item['products_total_amount'], $item['discount'] / 10, 2); + } + $discountAmount = bcsub($totalAmount, $paymentAmount, 2); + if ($coupon) { + $couponDiscount = bcdiv($coupon->getDiscountAmount($paymentAmount), 100, 2); + $paymentAmount = bcsub($paymentAmount, $couponDiscount, 2); + } $preview = OfflineOrderPreview::create([ 'store_id' => $store->id, 'staff_id' => $user->id, - 'payload' => ['items' => $request->input('items')], - 'remark' => $request->input('remark') + 'payload' => ['items' => $items, 'total_amount' => $totalAmount, 'discount_amount' => $discountAmount, 'coupon_discount' => $couponDiscount, 'payment_amount' => $paymentAmount], + 'remark' => $request->input('remark'), + 'user_id' => $userId, + 'coupon_id' => $couponId, ]); if (config('app.env') != "local") { diff --git a/app/Endpoint/Api/Http/Controllers/UserCouponController.php b/app/Endpoint/Api/Http/Controllers/UserCouponController.php index 1e65fd5e..72962fa5 100644 --- a/app/Endpoint/Api/Http/Controllers/UserCouponController.php +++ b/app/Endpoint/Api/Http/Controllers/UserCouponController.php @@ -3,7 +3,10 @@ namespace App\Endpoint\Api\Http\Controllers; use App\Endpoint\Api\Http\Resources\UserCouponResource; +use App\Exceptions\BizException; use App\Helpers\Paginator as PaginatorHelper; +use App\Models\User; +use App\Models\UserCoupon; use Illuminate\Http\Request; class UserCouponController extends Controller @@ -17,11 +20,73 @@ class UserCouponController extends Controller public function index(Request $request) { $user = $request->user(); + $query = $user->coupons(); - $coupons = $user->coupons() - ->filter($request->all()) - ->simplePaginate(PaginatorHelper::resolvePerPage('per_page', 20, 50)); + $coupons = $query->filter($request->all())->simplePaginate(PaginatorHelper::resolvePerPage('per_page', 20, 50)); return UserCouponResource::collection($coupons); } + + /** + * 查询所有用户的优惠券 + */ + public function list(Request $request) + { + $user = $request->user(); + // 员工身份 + if (!$user->userInfo->is_company) { + throw new BizException("非员工身份"); + } + $query = UserCoupon::query(); + if ($request->input('user_phone')) { + $user = User::where('phone', $request->input('user_phone'))->first(); + if (!$user) { + throw new BizException("手机号不存在"); + } + $query->where('user_id', $user->id); + } + + $coupons = $query->filter($request->all())->simplePaginate(PaginatorHelper::resolvePerPage('per_page', 20, 50)); + + return UserCouponResource::collection($coupons); + } + + /** + * 计算优惠券抵扣金额 + */ + public function discount(Request $request) + { + $request->validate([ + 'id' => 'required' + ], [ + 'id.required' => '优惠券id必填' + ]); + $id = $request->input('id'); + $coupon = UserCoupon::query()->onlyAvailable()->findOrFail($id); + $discount = $coupon->getDiscountAmount($request->input('total', 0)); + + return response()->json(['discount' => bcdiv($discount, 100, 2)]); + } + + /** + * 直接核销优惠券 + */ + public function check(Request $request) + { + $user = $request->user(); + // 员工身份 + if (!$user->userInfo->is_company) { + throw new BizException("非员工身份"); + } + $request->validate([ + 'id' => 'requreid' + ], [ + 'id.required' => '优惠券id必填' + ]); + $id = $request->input('id'); + $coupon = UserCoupon::query()->onlyAvailable()->findOrFail($id); + $coupon->markAsUse(); + + return response()->json([]); + } } diff --git a/app/Endpoint/Api/Http/Resources/OfflineOrderItemResource.php b/app/Endpoint/Api/Http/Resources/OfflineOrderItemResource.php new file mode 100644 index 00000000..ef2da9c5 --- /dev/null +++ b/app/Endpoint/Api/Http/Resources/OfflineOrderItemResource.php @@ -0,0 +1,26 @@ + $this->id, + 'order_id' => $this->order_id, + 'product_category_id' => $this->product_category_id, + 'product_category' => OfflineProductCategoryResource::make($this->whenLoaded('productCategory')), + 'products_total_amount' => $this->products_total_amount, + 'discount_reduction_amount' => $this->discount_reduction_amount, + ]; + } +} diff --git a/app/Endpoint/Api/Http/Resources/OfflineOrderPreviewResource.php b/app/Endpoint/Api/Http/Resources/OfflineOrderPreviewResource.php new file mode 100644 index 00000000..dfc9043e --- /dev/null +++ b/app/Endpoint/Api/Http/Resources/OfflineOrderPreviewResource.php @@ -0,0 +1,31 @@ + $this->id, + 'store_id' => $this->store_id, + 'store' => StoreResource::make($this->whenLoaded('store')), + 'payload' => $this->payload, + 'remark' => $this->remark, + 'coupon_id' => $this->coupon_id, + 'coupon' => UserCouponResource::make($this->whenLoaded('coupon')), + 'order' => OfflineOrderResource::make($this->whenLoaded('order')), + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'orders' => OfflineOrderResource::collection($this->whenLoaded('orders')), + 'qrcode' => $this->qrcode, + ]; + } +} diff --git a/app/Endpoint/Api/Http/Resources/OfflineOrderResource.php b/app/Endpoint/Api/Http/Resources/OfflineOrderResource.php new file mode 100644 index 00000000..85ce234a --- /dev/null +++ b/app/Endpoint/Api/Http/Resources/OfflineOrderResource.php @@ -0,0 +1,41 @@ + $this->id, + 'user_id' => $this->user_id, + 'store_id' => $this->store_id, + 'store' => StoreResource::make($this->whenLoaded('store')), + 'user' => UserResource::make($this->whenLoaded('user')), + 'staff_id' => $this->staff_id, + 'sn' => $this->sn, + 'products_total_amount' => bcdiv($this->products_total_amount, 100, 2), + 'discount_reduction_amount' => bcdiv($this->discount_reduction_amount, 100, 2), + 'points_deduction_amount' => bcdiv($this->points_deduction_amount, 100, 2), + 'coupon_discount_amount' => bcdiv($this->coupon_discount_amount, 100, 2), + 'payment_amount' => bcdiv($this->payment_amount, 100, 2), + 'status' => $this->status, + 'status_text' => $this->status->text(), + 'payment_time' => $this->payment_time?->format('Y-m-d H:i:s'), + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'payment_sn' => $this->payment_sn, + 'user_remark' => $this->user_remark, + 'staff_remark' => $this->staff_remark, + 'coupon_id' => $this->coupon_id, + 'items' => OfflineOrderItemResource::collection($this->whenLoaded('items')), + ]; + } +} diff --git a/app/Endpoint/Api/Http/Resources/UserResource.php b/app/Endpoint/Api/Http/Resources/UserResource.php new file mode 100644 index 00000000..7e187d3e --- /dev/null +++ b/app/Endpoint/Api/Http/Resources/UserResource.php @@ -0,0 +1,16 @@ + $this->id, + 'phone' => $this->phone, + ]; + } +} diff --git a/app/Endpoint/Api/routes.php b/app/Endpoint/Api/routes.php index 8afd8297..0481d57b 100644 --- a/app/Endpoint/Api/routes.php +++ b/app/Endpoint/Api/routes.php @@ -173,6 +173,9 @@ Route::group([ // 我的优惠券 Route::get('coupons', [UserCouponController::class, 'index']); + // 查询优惠券记录 + Route::get('coupon-list', [UserCouponController::class, 'list']); + Route::post('coupons/discount', [UserCouponController::class, 'discount']); //签到 Route::get('click', [ClickController::class, 'index']); @@ -222,11 +225,15 @@ 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', [App\Endpoint\Api\Http\Controllers\OfflineOrderPreviewController::class, 'index']); + Route::get('offline-order-previews/{id}/detail', [App\Endpoint\Api\Http\Controllers\OfflineOrderPreviewController::class, 'detail']); 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']); + Route::get('offline-orders', [App\Endpoint\Api\Http\Controllers\OfflineOrderController::class, 'index']); + Route::get('offline-orders/{id}', [App\Endpoint\Api\Http\Controllers\OfflineOrderController::class, 'show']); }); // 微信小程序 diff --git a/app/Models/Coupon.php b/app/Models/Coupon.php index 756a5101..1712053f 100644 --- a/app/Models/Coupon.php +++ b/app/Models/Coupon.php @@ -12,8 +12,14 @@ class Coupon extends Model use HasFactory; use HasDateTimeFormatter; - public const TYPE_AMOUNT = 1; // 抵扣券 - public const TYPE_DISCOUNT = 2; // 折扣券 + /** + * 抵扣券: 直接抵扣金额 + */ + public const TYPE_AMOUNT = 1; + /** + * 折扣券: 计算折扣 + */ + public const TYPE_DISCOUNT = 2; protected $casts = [ 'amount' => Price::class, diff --git a/app/Models/OfflineOrder.php b/app/Models/OfflineOrder.php index 80993870..25b6b77c 100644 --- a/app/Models/OfflineOrder.php +++ b/app/Models/OfflineOrder.php @@ -6,12 +6,13 @@ use App\Enums\OfflineOrderStatus; use App\Enums\PayWay; use App\Models\Store\Store; use Dcat\Admin\Traits\HasDateTimeFormatter; +use EloquentFilter\Filterable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class OfflineOrder extends Model { - use HasFactory, HasDateTimeFormatter; + use HasFactory, HasDateTimeFormatter, Filterable; protected $attributes = [ 'discount_reduction_amount' => 0, @@ -24,6 +25,7 @@ class OfflineOrder extends Model 'status' => OfflineOrderStatus::class, 'payment_method' => PayWay::class, 'revoked_at' => 'datetime', + 'ship_time' => 'datetime', ]; protected $fillable = [ @@ -45,6 +47,9 @@ class OfflineOrder extends Model 'orderable_id', 'user_remark', 'staff_remark', + 'ship_time', + 'coupon_id', + 'coupon_discount_amount' ]; public function store() @@ -56,6 +61,10 @@ class OfflineOrder extends Model { return $this->belongsTo(User::class, 'staff_id'); } + public function coupon() + { + return $this->belongsTo(UserCoupon::class, 'coupon_id'); + } public function staffInfo() { @@ -82,6 +91,11 @@ class OfflineOrder extends Model return $this->hasMany(OfflineOrderItem::class, 'order_id'); } + public function preview() + { + return $this->morphTo(OfflineOrderPreview::class, 'orderable_type', 'orderable_id', 'id'); + } + public function isPending(): bool { return $this->status === OfflineOrderStatus::Pending; diff --git a/app/Models/OfflineOrderPreview.php b/app/Models/OfflineOrderPreview.php index 765be689..2b0b2094 100644 --- a/app/Models/OfflineOrderPreview.php +++ b/app/Models/OfflineOrderPreview.php @@ -15,11 +15,26 @@ class OfflineOrderPreview extends Model ]; protected $fillable = [ - 'store_id', 'staff_id', 'payload', 'qrcode', 'remark' + 'store_id', 'staff_id', 'payload', 'qrcode', 'remark', 'user_id', 'coupon_id' ]; public function store() { return $this->belongsTo(Store::class); } + + public function coupon() + { + return $this->belongsTo(UserCoupon::class); + } + + public function order() + { + return $this->morphOne(OfflineOrder::class, 'orderable')->orderBy('created_at', 'desc'); + } + + public function orders() + { + return $this->morphMany(OfflineOrder::class, 'orderable')->orderBy('created_at', 'desc'); + } } diff --git a/app/Models/UserCoupon.php b/app/Models/UserCoupon.php index 640e5e59..9ada496f 100644 --- a/app/Models/UserCoupon.php +++ b/app/Models/UserCoupon.php @@ -25,6 +25,7 @@ class UserCoupon extends Model 'is_use' => 'bool', 'use_start_at' => 'datetime', 'use_end_at' => 'datetime', + 'use_at' => 'datetime' ]; /** @@ -43,6 +44,7 @@ class UserCoupon extends Model 'created_at', 'updated_at', 'activity_id', + 'use_at' ]; public function user() @@ -133,6 +135,7 @@ class UserCoupon extends Model { $this->forceFill([ 'is_use' => true, + 'use_at' => now(), ])->save(); } @@ -166,4 +169,22 @@ class UserCoupon extends Model bcdiv($this->attributes['coupon_threshold'], 100, 2) ); } + + /** + * 获取优惠金额 + */ + public function getDiscountAmount(int $total) + { + $couponDiscount = 0; + // 优惠券: 直接抵扣 + if ($this->coupon_type == Coupon::TYPE_AMOUNT) { + $couponDiscount = $this->coupon_amount; + } + // 优惠券: 折扣 + else if ($this->coupon_type == Coupon::TYPE_DISCOUNT) { + $couponDiscount = (int) bcmul($total, bcdiv(100 - $this->coupon_amount, 100, 2)); + } + + return $couponDiscount; + } } diff --git a/app/Services/OfflineOrderService.php b/app/Services/OfflineOrderService.php index 0e5d65e1..59854190 100644 --- a/app/Services/OfflineOrderService.php +++ b/app/Services/OfflineOrderService.php @@ -8,18 +8,28 @@ use App\Enums\PointLogAction; use App\Enums\SocialiteType; use App\Enums\WxpayTradeType; use App\Exceptions\BizException; +use App\Models\Coupon; use App\Models\OfflineOrder; use App\Models\OfflineOrderItem; use App\Models\OfflineOrderPreview; use App\Models\OfflineProductCategory; use App\Models\SocialiteUser; use App\Models\User; +use App\Models\UserCoupon; use App\Services\Payment\WxpayService; use function EasyWeChat\Kernel\data_get; +/** + * 线下订单 + */ class OfflineOrderService { + public static function make() + { + return new static(); + } + public function create(User $user, OfflineOrderPreview $orderPreview, int $points = 0, $params = []): OfflineOrder { $items = $this->mapItems($orderPreview->payload['items']); @@ -32,8 +42,20 @@ class OfflineOrderService // 积分抵扣金额 $pointsDeductionAmount = $points; - $paymentAmount -= $pointsDeductionAmount; + + // 优惠券抵扣 + $couponDiscount = 0; + $couponId = data_get($params, 'coupon_id'); + if ($couponId) { + $coupon = UserCoupon::where('user_id', $user->id)->onlyAvailable()->find($couponId); + if (!$coupon) { + throw new BizException("优惠券不可用"); + } + $couponDiscount = $coupon->getDiscountAmount($paymentAmount); + $paymentAmount -= $couponDiscount; + } + if ($paymentAmount < 0) { $paymentAmount = 0; } @@ -57,6 +79,8 @@ class OfflineOrderService 'orderable_id' => $orderPreview->id, 'user_remark' => data_get($params, 'user_remark'), 'staff_remark' => $orderPreview->remark, + 'coupon_id' => $couponId, + 'coupon_discount_amount' => $couponDiscount, ]); $this->insertOrderItems($order, $items); @@ -79,8 +103,12 @@ class OfflineOrderService return $order; } - public function check(User $user, array $items): array + public function check(User $user, OfflineOrderPreview $preview): array { + if ($preview->order && $preview->order->status != OfflineOrderStatus::Pending) { + throw new BizException('订单已支付, 请重新生成'); + } + $items = $preview->payload['items']; $productCategoryIds = collect($items)->pluck('product_category_id'); $productCategories = OfflineProductCategory::query() @@ -100,6 +128,20 @@ class OfflineOrderService $paymentAmount, ] = $this->calculateFees($mapItems); + // 优惠券 + $coupon = null; + $couponDiscount = 0; + if ($preview->coupon_id) { + $coupon = UserCoupon::where('user_id', $user->id)->onlyAvailable()->find($preview->coupon_id); + if ($coupon) { + $couponDiscount = $coupon->getDiscountAmount($paymentAmount); + } + } + $paymentAmount -= $couponDiscount; + if ($paymentAmount < 0) { + $paymentAmount = 0; + } + //--------------------------------------- // 积分当钱花 //--------------------------------------- @@ -119,6 +161,8 @@ class OfflineOrderService 'remaining_points' => bcdiv($remainingPoints, 100, 2), // 剩余积分 'available_points' => bcdiv($availablePoints, 100, 2), // 可用积分 'points_discount_amount' => bcdiv($availablePoints, 100, 2), // 积分抵扣金额 + // 优惠券 + 'coupon' => ['id' => $coupon?->id, 'name' => $coupon?->coupon_name, 'discount' => bcdiv($couponDiscount, 100, 2)], ]; } @@ -266,4 +310,72 @@ class OfflineOrderService return [$totalProductsTotalAmount, $totalDiscountReductionAmount, $totalPaymentAmount]; } + + /** + * 订单支付成功 + * @param $data [payment_sn:商户订单号, payment_method:支付方式, payment_time: 支付时间, out_trade_no: 流水号] + */ + public function paySuccess(OfflineOrder $order, array $data) + { + $data['status'] = OfflineOrderStatus::Paid; + $order->update($data); + + // 更新优惠券 + if ($order->coupon_id) { + $coupon = UserCoupon::find($order->coupon_id); + if ($coupon) { + $coupon->markAsUse(); + } + } + + if (config('app.env') != "local") { + try { + $this->orderShip($order); + } catch (BizException $e) { + logger()->error("线下订单-自动发货失败: " . $e->getMessage(), ['id' => $order->id, 'sn' => $order->sn]); + } + } + } + + /** + * 订单发货 + */ + public function orderShip(OfflineOrder $order) + { + $now = now(); + if (!$order->isPaid()) { + throw new BizException("订单未支付"); + } + $userId = $order->user_id; + $socialite = SocialiteUser::where('user_id', $userId)->where('type', SocialiteType::WechatMiniProgram)->first(); + if (!$socialite) { + throw new BizException("未找到用户的openid"); + } + $openid = $socialite->socialite_id; + $items = $order->items()->with(['productCategory'])->get(); + $desc = []; + foreach ($items as $item) { + $name = data_get($item, "productCategory.name", "定制服务"); + array_push($desc, $name); + } + $data = [ + "order_key" => [ + "order_number_type" => 1, + "out_trade_no" => $order->payment_sn, + ], + "logistics_type" => 4, + "delivery_mode" => 1, + "shipping_list" => [ + "item_desc" => implode(";", $desc) + ], + "upload_time" => $now, + "payer" => [ + "openid" => $openid + ] + ]; + WxMpService::make()->uploadShippingInfo($data); + + $order->ship_time = $now; + $order->save(); + } } diff --git a/app/Services/PayService.php b/app/Services/PayService.php index b27716d4..0006af92 100644 --- a/app/Services/PayService.php +++ b/app/Services/PayService.php @@ -78,7 +78,7 @@ class PayService throw new BizException('订单取消'); } - $payable->update([ + OfflineOrderService::make()->paySuccess($payable, [ 'payment_sn' => $payLog->pay_sn, 'payment_method' => $payLog->pay_way, 'payment_time' => $payLog->pay_at, diff --git a/app/Services/WxMpService.php b/app/Services/WxMpService.php new file mode 100644 index 00000000..17b9116b --- /dev/null +++ b/app/Services/WxMpService.php @@ -0,0 +1,42 @@ +access_token->getToken()['access_token']; + $url = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token=" . $accessToken; + $data = [ + "order_key" => [ + "order_number_type" => 1, + "out_trade_no" => "", + ], + "logistics_type" => 4 + ]; + $response = Http::post($url, $data); + + $response->throw(); + + $result = $response->json(); + $code = data_get($result, "errcode"); + if ($code != 0) { + throw new BizException("微信小程序-发货信息录入, 失败: (".$code.")" . data_get($result, "errmsg")); + } + } +} diff --git a/resources/lang/zh_CN/offline-order.php b/resources/lang/zh_CN/offline-order.php index f23de3c7..3dcd0c67 100644 --- a/resources/lang/zh_CN/offline-order.php +++ b/resources/lang/zh_CN/offline-order.php @@ -25,6 +25,7 @@ return [ 'items' => '品类', 'user_remark' => '用户备注', 'staff_remark' => '员工备注', + 'coupon_discount_amount' => '优惠券抵扣', ], 'options' => [ ],