259 lines
9.2 KiB
PHP
259 lines
9.2 KiB
PHP
<?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,
|
|
];
|
|
}
|
|
|
|
public function revoke(OfflineOrder $order)
|
|
{
|
|
if ($order->isPaid()) {
|
|
throw new BizException('订单已付款');
|
|
}
|
|
|
|
if ($order->isRevoked()) {
|
|
throw new BizException('订单已取消');
|
|
}
|
|
|
|
if ($order->points_deduction_amount > 0) {
|
|
(new PointService())->change($order->user, $order->points_deduction_amount, PointLogAction::Refund, "线下订单{$order->sn}退还积分", $order);
|
|
}
|
|
|
|
$order->update([
|
|
'status' => OfflineOrderStatus::Revoked,
|
|
'revoked_at' => now(),
|
|
]);
|
|
}
|
|
|
|
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 = bcsub($productsTotalAmount, 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];
|
|
}
|
|
}
|