线下订单
parent
dddef607d3
commit
7f4c44aa5e
|
|
@ -0,0 +1,192 @@
|
|||
<?php
|
||||
|
||||
namespace App\Admin\Controllers;
|
||||
|
||||
use App\Admin\Repositories\OfflineOrder as OfflineOrderRepository;
|
||||
use App\Admin\Widgets\InfoBox;
|
||||
use App\Enums\OfflineOrderStatus;
|
||||
use App\Enums\PayWay;
|
||||
use App\Models\OfflineOrder;
|
||||
use App\Models\OfflineOrderItem;
|
||||
use App\Models\Order;
|
||||
use App\Models\Store\Store;
|
||||
use App\Models\UserInfo;
|
||||
use Dcat\Admin\Admin;
|
||||
use Dcat\Admin\Grid;
|
||||
use Dcat\Admin\Grid\Column;
|
||||
use Dcat\Admin\Http\Controllers\AdminController;
|
||||
use Dcat\Admin\Layout\Row;
|
||||
use Dcat\Admin\Show;
|
||||
use Dcat\Admin\Widgets\Box;
|
||||
|
||||
class OfflineOrderController extends AdminController
|
||||
{
|
||||
protected function grid()
|
||||
{
|
||||
Admin::style(
|
||||
<<<CSS
|
||||
.card-header {
|
||||
margin-top: 1.5rem !important;
|
||||
margin-bottom: -1rem !important;
|
||||
}
|
||||
CSS
|
||||
);
|
||||
|
||||
$grid = new Grid(OfflineOrderRepository::with(['user', 'userInfo', 'store']));
|
||||
|
||||
$grid->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 <<<HTML
|
||||
<img src="{$avatar}" width="45" />
|
||||
<span class="label bg-danger">{$nickname}</span>
|
||||
<span class="label bg-success">{$phone}</span>
|
||||
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));
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Admin\Repositories;
|
||||
|
||||
use App\Models\OfflineOrder as Model;
|
||||
use Dcat\Admin\Repositories\EloquentRepository;
|
||||
|
||||
class OfflineOrder extends EloquentRepository
|
||||
{
|
||||
protected $eloquentClass = Model::class;
|
||||
}
|
||||
|
|
@ -186,6 +186,10 @@ Route::group([
|
|||
'only' => ['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');
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace App\Endpoint\Api\Http\Controllers;
|
||||
|
||||
use App\Endpoint\Api\Http\Controllers\Controller;
|
||||
use App\Enums\PayWay;
|
||||
use App\Models\OfflineOrder;
|
||||
use App\Models\OfflineOrderPreview;
|
||||
use App\Services\OfflineOrderService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Throwable;
|
||||
|
||||
class OfflineOrderController extends Controller
|
||||
{
|
||||
/**
|
||||
* 确认订单
|
||||
*/
|
||||
public function check(Request $request, OfflineOrderService $offlineOrderService)
|
||||
{
|
||||
$request->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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace App\Endpoint\Api\Http\Controllers;
|
||||
|
||||
use App\Endpoint\Api\Http\Controllers\Controller;
|
||||
use App\Exceptions\BizException;
|
||||
use App\Models\OfflineOrderPreview;
|
||||
use App\Models\Store\Store;
|
||||
use EasyWeChat\Factory;
|
||||
use EasyWeChat\Kernel\Http\StreamResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class OfflineOrderPreviewController extends Controller
|
||||
{
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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']);
|
||||
});
|
||||
|
||||
// 微信小程序
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Dcat\Admin\Admin;
|
||||
|
||||
enum OfflineOrderStatus: int {
|
||||
case Pending = 0;
|
||||
case Paid = 1;
|
||||
case Revoked = 10;
|
||||
|
||||
public function label()
|
||||
{
|
||||
$background = Admin::color()->get($this->color());
|
||||
|
||||
return "<span class='label' style='background: $background;'>{$this->text()}</span>";
|
||||
}
|
||||
|
||||
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 => '已取消',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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 '<i class="fa fa-circle" style="font-size: 13px;color: '.$color.'"></i> '.$this->text() ?? 'Unknown';
|
||||
}
|
||||
|
||||
public function color()
|
||||
{
|
||||
return match ($this) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\OfflineOrderStatus;
|
||||
use App\Enums\PayWay;
|
||||
use App\Models\Store\Store;
|
||||
use Dcat\Admin\Traits\HasDateTimeFormatter;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OfflineOrder extends Model
|
||||
{
|
||||
use HasFactory, HasDateTimeFormatter;
|
||||
|
||||
protected $attributes = [
|
||||
'discount_reduction_amount' => 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Dcat\Admin\Traits\HasDateTimeFormatter;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OfflineOrderItem extends Model
|
||||
{
|
||||
use HasFactory, HasDateTimeFormatter;
|
||||
|
||||
protected $attributes = [
|
||||
'discount_reduction_amount' => 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');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Store\Store;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OfflineOrderPreview extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'payload' => 'json',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'staff_id', 'payload', 'qrcode',
|
||||
];
|
||||
|
||||
public function store()
|
||||
{
|
||||
return $this->belongsTo(Store::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,238 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\OfflineOrderStatus;
|
||||
use App\Enums\PayWay;
|
||||
use App\Enums\PointLogAction;
|
||||
use App\Enums\SocialiteType;
|
||||
use App\Enums\WxpayTradeType;
|
||||
use App\Exceptions\BizException;
|
||||
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\Services\Payment\WxpayService;
|
||||
|
||||
|
||||
class OfflineOrderService
|
||||
{
|
||||
public function create(User $user, OfflineOrderPreview $orderPreview, int $points = 0): OfflineOrder
|
||||
{
|
||||
$items = $this->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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateOfflineOrderPreviewsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('offline_order_previews', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateOfflineOrdersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('offline_orders', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateOfflineOrderItemsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('offline_order_items', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -368,6 +368,11 @@ class AdminMenuSeeder extends Seeder
|
|||
'icon' => '',
|
||||
'uri' => 'offline-product-categories',
|
||||
],
|
||||
[
|
||||
'title' => '线下订单',
|
||||
'icon' => '',
|
||||
'uri' => 'offline-orders',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'labels' => [
|
||||
'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' => [
|
||||
],
|
||||
];
|
||||
Loading…
Reference in New Issue