6
0
Fork 0
jiqu-library-server/app/Services/OfflineOrderService.php

395 lines
14 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\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']);
[
$productsTotalAmount,
$discountReductionAmount,
$paymentAmount,
] = $this->calculateFees($items);
// 积分抵扣金额
$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;
}
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,
'user_remark' => data_get($params, 'user_remark'),
'staff_remark' => $orderPreview->remark,
'coupon_id' => $couponId,
'coupon_discount_amount' => $couponDiscount,
]);
$this->insertOrderItems($order, $items);
// 扣除积分
if ($points > 0) {
(new PointService)->change($user, -$points, PointLogAction::Consumption, [
'remark' => "线下订单{$order->sn}使用积分",
'loggable' => $order,
'store_id' => $orderPreview->store_id,
]);
}
if ($order->payment_amount === 0) {
$this->pay($order, PayWay::None);
$order->refresh();
}
return $order;
}
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()
->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);
// 优惠券
$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;
}
//---------------------------------------
// 积分当钱花
//---------------------------------------
$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), // 积分抵扣金额
// 优惠券
'coupon' => ['id' => $coupon?->id, 'name' => $coupon?->coupon_name, 'discount' => bcdiv($couponDiscount, 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, [
'remark' => "线下订单{$order->sn}退还积分",
'loggable' => $order,
'store_id' => $order->store_id,
]);
}
$order->update([
'status' => OfflineOrderStatus::Revoked,
'revoked_at' => now(),
]);
}
protected function insertOrderItems(OfflineOrder $order, array $items)
{
$remainingPointDiscountAmount = $order->points_deduction_amount;
$remainCoupon = $order->coupon_discount_amount;
OfflineOrderItem::insert(
collect($items)->map(function ($item) use ($order, &$remainingPointDiscountAmount, &$remainCoupon) {
$paymentAmount = $item['payment_amount'];
// 优惠券
$couponDiscount = $paymentAmount;
if ($paymentAmount > $remainCoupon) {
$couponDiscount = $remainCoupon;
}
$remainCoupon -= $couponDiscount;
$paymentAmount -= $couponDiscount;
$pointsDeductionAmount = $paymentAmount;
if ($item['payment_amount'] > $remainingPointDiscountAmount) {
$pointsDeductionAmount = $remainingPointDiscountAmount;
}
$remainingPointDiscountAmount -= $pointsDeductionAmount;
$paymentAmount -= $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,
'coupon_discount_amount' => $couponDiscount,
'payment_amount' => $paymentAmount,
'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];
}
/**
* 订单支付成功
* @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('socialite_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();
}
}