线下订单
parent
93e30b0496
commit
2471bec5e1
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 <<<HTML
|
||||
<img src="{$avatar}" width="45" />
|
||||
<span class="label bg-danger">{$nickname}</span>
|
||||
<span class="label bg-success">{$phone}</span>
|
||||
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 <<<HTML
|
||||
<img src="{$avatar}" width="45" />
|
||||
<span class="label bg-danger">{$nickname}</span>
|
||||
<span class="label bg-success">{$phone}</span>
|
||||
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('¥');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\OfflineOrder;
|
||||
use App\Services\OfflineOrderService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class OfflineOrderShip extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'offline-order:ship {id}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '线下订单发货';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = $this->argument('id');
|
||||
$order = OfflineOrder::findOrFail($id);
|
||||
OfflineOrderService::make()->orderShip($order);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\OfflineOrderStatus;
|
||||
use App\Models\OfflineOrder;
|
||||
use App\Services\OfflineOrderService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class OfflineOrderSuccess extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'offline-order:success {id}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = $this->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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Endpoint\Api\Filters;
|
||||
|
||||
use EloquentFilter\ModelFilter;
|
||||
|
||||
class OfflineOrderFilter extends ModelFilter
|
||||
{
|
||||
/**
|
||||
* Related Models that have ModelFilters as well as the method on the ModelFilter
|
||||
* As [relationMethod => [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);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,9 @@ class UserCouponFilter extends ModelFilter
|
|||
public function status($status)
|
||||
{
|
||||
switch ($status) {
|
||||
case 'available':
|
||||
$this->onlyAvailable();
|
||||
break;
|
||||
case 'unuse':
|
||||
$this->onlyUnuse();
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Endpoint\Api\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class OfflineOrderItemResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Endpoint\Api\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class OfflineOrderPreviewResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Endpoint\Api\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class OfflineOrderResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $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')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Endpoint\Api\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'phone' => $this->phone,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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']);
|
||||
});
|
||||
|
||||
// 微信小程序
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\BizException;
|
||||
use EasyWeChat\Factory;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class WxMpService
|
||||
{
|
||||
public static function make()
|
||||
{
|
||||
return new static();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发货信息录入
|
||||
* https://developers.weixin.qq.com/miniprogram/dev/server/API/order_shipping/api_uploadshippinginfo.html
|
||||
*/
|
||||
public function uploadShippingInfo($data)
|
||||
{
|
||||
$app = Factory::miniProgram(config('wechat.mini_program.default'));
|
||||
$accessToken = $app->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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ return [
|
|||
'items' => '品类',
|
||||
'user_remark' => '用户备注',
|
||||
'staff_remark' => '员工备注',
|
||||
'coupon_discount_amount' => '优惠券抵扣',
|
||||
],
|
||||
'options' => [
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in New Issue