From a0aa1c0eeca2313be6bed15ab6e6e6b3e733d646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=9D=99?= Date: Mon, 27 Dec 2021 18:56:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E4=BB=98=E6=B5=81=E6=B0=B4=E8=AE=B0?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Constants/PayWay.php | 32 --- .../Controllers/Order/OrderController.php | 44 ++-- .../Http/Controllers/WeChatPayController.php | 4 +- app/Models/PayLog.php | 36 ++- app/Services/OrderService.php | 241 ++++++++++-------- app/Services/PayService.php | 71 ++++-- app/Services/WeChatPayService.php | 12 +- app/helpers.php | 29 +++ .../2021_12_07_143655_create_orders_table.php | 2 +- 9 files changed, 278 insertions(+), 193 deletions(-) delete mode 100644 app/Constants/PayWay.php diff --git a/app/Constants/PayWay.php b/app/Constants/PayWay.php deleted file mode 100644 index d0c168e2..00000000 --- a/app/Constants/PayWay.php +++ /dev/null @@ -1,32 +0,0 @@ - WeChatPayService::TRADE_TYPE_APP, - self::WXPAY_JSAPI => WeChatPayService::TRADE_TYPE_JSAPI, - self::WXPAY_MINI => WeChatPayService::TRADE_TYPE_JSAPI, - self::WXPAY_H5 => WeChatPayService::TRADE_TYPE_H5, - self::WXPAY_NATIVE => WeChatPayService::TRADE_TYPE_NATIVE, - ]; -} diff --git a/app/Endpoint/Api/Http/Controllers/Order/OrderController.php b/app/Endpoint/Api/Http/Controllers/Order/OrderController.php index 4669388f..ce4afee6 100644 --- a/app/Endpoint/Api/Http/Controllers/Order/OrderController.php +++ b/app/Endpoint/Api/Http/Controllers/Order/OrderController.php @@ -44,24 +44,22 @@ class OrderController extends Controller */ public function store(Request $request) { - $rules = [ + $isQuick = $request->filled('product'); + + $rules = $isQuick ? [ + 'product.sku_id' => ['bail', 'required', 'int'], + 'product.quantity' => ['bail', 'required', 'int', 'min:1'], + 'shipping_address_id' => ['bail', 'required', 'int'], + 'coupon_id' => ['bail', 'nullable', 'int'], + 'note' => ['bail', 'nullable', 'string', 'max:255'], + ] : [ + 'shopping_cart' => ['bail', 'required', 'array'], 'shipping_address_id' => ['bail', 'required', 'int'], 'coupon_id' => ['bail', 'nullable', 'int'], 'note' => ['bail', 'nullable', 'string', 'max:255'], ]; - if ($isQuick = $request->filled('product')) { - $rules = array_merge($rules, [ - 'product.sku_id' => ['bail', 'required', 'int'], - 'product.quantity' => ['bail', 'required', 'int', 'min:1'], - ]); - } else { - $rules = array_merge($rules, [ - 'shopping_cart' => ['bail', 'required', 'array'], - ]); - } - - $request->validate($rules, [], [ + $input = $request->validate($rules, [], [ 'product.sku_id' => '商品', 'product.quantity' => '数量', 'shopping_cart' => '购物车商品', @@ -73,26 +71,26 @@ class OrderController extends Controller $user = $request->user(); try { - $order = DB::transaction(function () use ($isQuick, $user, $request) { + $order = DB::transaction(function () use ($isQuick, $user, $input) { $orderService = new OrderService(); if ($isQuick) { return $orderService->createQuickOrder( $user, - $request->input('product.sku_id'), - $request->input('product.quantity'), - $request->input('shipping_address_id'), - $request->input('coupon_id'), - $request->input('note'), + $input['product']['sku_id'], + $input['product']['quantity'], + $input['shipping_address_id'], + $input['coupon_id'] ?? null, + $input['note'] ?? null, ); } return $orderService->createShoppingCartOrder( $user, - $request->input('shopping_cart'), - $request->input('shipping_address_id'), - $request->input('coupon_id'), - $request->input('note'), + $input['shopping_cart'], + $input['shipping_address_id'], + $input['coupon_id'] ?? null, + $input['note'] ?? null, ); }); } catch (QueryException $e) { diff --git a/app/Endpoint/Callback/Http/Controllers/WeChatPayController.php b/app/Endpoint/Callback/Http/Controllers/WeChatPayController.php index a78793d0..771b531e 100644 --- a/app/Endpoint/Callback/Http/Controllers/WeChatPayController.php +++ b/app/Endpoint/Callback/Http/Controllers/WeChatPayController.php @@ -34,13 +34,13 @@ class WeChatPayController extends Controller $payService = new PayService(); if (data_get($message, 'result_code') !== 'SUCCESS') { - return $payService->payFailed($message['out_trade_no'], [ + return $payService->handleSuccessByPaySerialNumber($message['out_trade_no'], [ 'pay_sn' => $message['transaction_id'] ?? null, 'failed_reason' => '['.$message['err_code'].']'.$message['err_code_des'], ]); } - return $payService->paySuccess($message['out_trade_no'], [ + return $payService->handleFailedByPaySerialNumber($message['out_trade_no'], [ 'out_trade_no' => $message['transaction_id'], 'pay_at' => Carbon::parse($message['time_end']), ]); diff --git a/app/Models/PayLog.php b/app/Models/PayLog.php index 658dbf25..21b2833b 100644 --- a/app/Models/PayLog.php +++ b/app/Models/PayLog.php @@ -13,11 +13,12 @@ class PayLog extends Model public const STATUS_SUCCESS = 1; // 成功 public const STATUS_FAILED = 2; // 失败 - /** - * 支付方式 - */ - public const PAY_WAY_WXPAY = 'wxpay'; // 微信支付 - public const PAY_WAY_ALIPAY = 'alipay'; // 支付宝 + public const PAY_WAY_WXPAY_APP = 'wxpay_app'; // 微信支付 - App 支付 + public const PAY_WAY_WXPAY_JSAPI = 'wxpay_jsapi'; // 微信支付 - JSAPI 支付 + public const PAY_WAY_WXPAY_MINI = 'wxpay_mini'; // 微信支付 - 小程序支付 + public const PAY_WAY_WXPAY_H5 = 'wxpay_h5'; // 微信支付 - H5 支付 + public const PAY_WAY_BALANCE = 'balance'; // 余额支付 + public const PAY_WAY_OFFLINE = 'offline'; // 线下支付 /** * @var array @@ -60,4 +61,29 @@ class PayLog extends Model { return $this->status === static::STATUS_PENDING; } + + /** + * 确认支付方式是否是微信支付 + * + * @return bool + */ + public function isWxpay(): bool + { + return in_array($this->pay_way, [ + static::PAY_WAY_WXPAY_APP, + static::PAY_WAY_WXPAY_JSAPI, + static::PAY_WAY_WXPAY_MINI, + static::PAY_WAY_WXPAY_H5, + ]); + } + + /** + * 确认支付方式是否是线下支付 + * + * @return bool + */ + public function isOffline(): bool + { + return $this->pay_way === static::PAY_WAY_OFFLINE; + } } diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index 667ce550..36d1cac5 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -2,16 +2,17 @@ namespace App\Services; -use App\Constants\PayWay; use App\Endpoint\Api\Http\Resources\ProductSkuSimpleResource; use App\Endpoint\Api\Http\Resources\ShippingAddressResource; use App\Endpoint\Api\Http\Resources\UserCouponResource; use App\Exceptions\BizException; use App\Exceptions\ShippingNotSupportedException; use App\Helpers\Numeric; +use App\Models\Coupon; use App\Models\DistributionPreIncomeJob; use App\Models\Order; use App\Models\OrderProduct; +use App\Models\PayLog; use App\Models\ProductGift; use App\Models\ProductSku; use App\Models\ShippingAddress; @@ -22,16 +23,6 @@ use Illuminate\Support\Facades\DB; class OrderService { - /** - * @var array - */ - protected $wxpayWays = [ - PayWay::WXPAY_APP, - PayWay::WXPAY_JSAPI, - PayWay::WXPAY_MINI, - PayWay::WXPAY_H5, - ]; - /** * 快速下单 * @@ -135,39 +126,104 @@ class OrderService $productsTotalAmount, $vipDiscountAmount, $couponDiscountAmount, - $totalAmount, ) = $this->calculateFees($mapProducts); - // 订单总费用 - $totalAmount += $shippingFee; + $order = $this->storeOrder( + $user, + $shippingAddress, + $productsTotalAmount, + $couponDiscountAmount, + $vipDiscountAmount, + $shippingFee, + $note, + $coupon + ); - $orderAttrs = [ - 'sn' => serial_number(), - 'user_coupon_id' => $coupon?->id, - 'coupon_discount_amount' => $couponDiscountAmount, - 'vip_discount_amount' => $vipDiscountAmount, - 'shipping_fee' => $shippingFee, - 'products_total_amount' => $productsTotalAmount, - 'total_amount' => $totalAmount, - 'note' => $note, - // 收货地址 - 'consignee_name' => $shippingAddress->consignee, - 'consignee_telephone' => $shippingAddress->telephone, - 'consignee_zone' => $shippingAddress->zone, - 'consignee_address' => $shippingAddress->address, - ]; + $this->storeOrderProducts($order, $mapProducts); - if ($totalAmount === 0) { - $orderAttrs = array_merge([ - 'status' => Order::STATUS_PAID, - 'pay_sn' => serial_number(), - 'pay_way' => Order::PAY_WAY_BALANCE, - 'pay_at' => now(), - ]); + // 将优惠券标记为已使用 + $coupon?->markAsUse(); + + if ($order->total_amount === 0) { + $this->pay($order, PayLog::PAY_WAY_BALANCE); + + $order->refresh(); } - $order = $user->orders()->create($orderAttrs); + return $order; + } + /** + * 保存订单 + * + * @param \App\Models\User $user + * @param \App\Models\ShippingAddress $shippingAddress + * @param int $productsTotalAmount + * @param int $couponDiscountAmount + * @param int $vipDiscountAmount + * @param int $shippingFee + * @param string|null $note + * @param \App\Models\Coupon|null $coupon + * @return \App\Models\Order + */ + protected function storeOrder( + User $user, + ShippingAddress $shippingAddress, + int $productsTotalAmount, + int $couponDiscountAmount, + int $vipDiscountAmount, + int $shippingFee, + ?string $note = null, + ?Coupon $coupon = null, + ): Order { + // 订单支付金额=商品总额-券折扣金额-会员折扣金额+邮费 + $totalAmount = $productsTotalAmount - $couponDiscountAmount - $vipDiscountAmount; + + if ($totalAmount < 0) { + $totalAmount = 0; + } + + $totalAmount += $shippingFee; + + $totalAmount=0; + + do { + // 如果订单号重复,则直接重试 + try { + $attrs = [ + 'sn' => serial_number(), + 'user_coupon_id' => $coupon?->id, + 'coupon_discount_amount' => $couponDiscountAmount, + 'vip_discount_amount' => $vipDiscountAmount, + 'shipping_fee' => $shippingFee, + 'products_total_amount' => $productsTotalAmount, + 'total_amount' => $totalAmount, // 商品总额-券折扣金额-会员折扣金额+邮费 + 'note' => $note, + // 收货地址 + 'consignee_name' => $shippingAddress->consignee, + 'consignee_telephone' => $shippingAddress->telephone, + 'consignee_zone' => $shippingAddress->zone, + 'consignee_address' => $shippingAddress->address, + ]; + + return $user->orders()->create($attrs); + } catch (QueryException $e) { + if (strpos($e->getMessage(), 'Duplicate entry') === false) { + throw $e; + } + } + } while (true); + } + + /** + * 保存订单商品 + * + * @param \App\Models\Order $order + * @param array $mapProducts + * @return void + */ + public function storeOrderProducts(Order $order, array $mapProducts) + { $orderProducts = []; foreach ($mapProducts as $product) { @@ -231,13 +287,7 @@ class OrderService } } - // 处理赠品 OrderProduct::insert($orderProducts); - - // 将优惠券标记为已使用 - $coupon?->markAsUse(); - - return $order; } /** @@ -254,22 +304,16 @@ class OrderService 'stock' => DB::raw("stock - {$qty}"), // 库存 ]); - try { - return retry(5, function () use ($sku, $qty) { + // 如果是因为赠品库存不足引起的异常,则需重试 + do { + try { return $this->deductGifts($sku, $qty); - }, 0, function ($e) { - // 如果赠品库存不足时,需重试 - return $e instanceof QueryException - && strpos($e->getMessage(), 'Numeric value out of range') !== false; - }); - } catch (QueryException $e) { - // 赠品库存不足 - if (strpos($e->getMessage(), 'Numeric value out of range') !== false) { - $e = new BizException('下单人数过多,请稍后再试!'); + } catch (QueryException $e) { + if (strpos($e->getMessage(), 'Numeric value out of range') === false) { + throw $e; + } } - - throw $e; - } + } while (true); } /** @@ -405,10 +449,14 @@ class OrderService $productsTotalAmount, $vipDiscountAmount, $couponDiscountAmount, - $totalAmount, ) = $this->calculateFees($mapProducts); - // 订单总费用 + $totalAmount = $productsTotalAmount - $couponDiscountAmount - $vipDiscountAmount; + + if ($totalAmount < 0) { + $totalAmount = 0; + } + $totalAmount += $shippingFee; return [ @@ -441,21 +489,16 @@ class OrderService $vipDiscountAmount = 0; $couponDiscountAmount = 0; - foreach ($products as $product) { $productsTotalAmount += $product['total_amount']; $vipDiscountAmount += $product['vip_discount_amount']; $couponDiscountAmount += $product['coupon_discount_amount']; } - // 订单总额 - $totalAmount = $productsTotalAmount - $vipDiscountAmount - $couponDiscountAmount; - return [ $productsTotalAmount, $vipDiscountAmount, $couponDiscountAmount, - $totalAmount, ]; } @@ -706,54 +749,40 @@ class OrderService throw new BizException('订单状态不是待付款'); } - $payLog = $order->payLogs()->create([ - 'pay_sn' => serial_number(), - 'pay_way' => $payWay, - ]); + do { + $payLog = null; - switch ($payWay) { - case PayWay::WXPAY_APP: - case PayWay::WXPAY_H5: - case PayWay::WXPAY_JSAPI: - case PayWay::WXPAY_MINI: - return (new WeChatPayService())->pay([ - 'body' => app_settings('app_name').'-商城订单', - 'out_trade_no' => $payLog->pay_sn, - 'total_fee' => $order->total_amount, - 'trade_type' => PayWay::$wxpayTradeTypes[$payWay], + try { + $payLog = $order->payLogs()->create([ + 'pay_sn' => serial_number(), + 'pay_way' => $payWay, ]); - break; - } - } + } catch (QueryException $e) { + if (strpos($e->getMessage(), 'Duplicate entry') === false) { + throw $e; + } + } + } while ($payLog === null); - /** - * 支付成功 - * - * @param string $sn - * @param array $params - * @return \App\Models\Order - */ - public function paySuccess(Order $order, array $params = []): Order - { - if (! $order->isPending()) { - throw new BizException('订单状态不是待支付'); + // 如果支付方式为线下支付,或支付金额为 0,则按支付完成处理 + if ($payLog->isOffline() || $order->total_amount === 0) { + return (new PayService())->handleSuccess($payLog, [ + 'pay_at' => now(), + ]); } - $order->update([ - 'pay_sn' => $params['pay_sn'], - 'pay_way' => $params['pay_way'], - 'pay_at' => $params['pay_at'], - 'out_trade_no' => $params['out_trade_no'], - 'status' => Order::STATUS_PAID, - ]); + if ($payLog->isWxpay()) { + if (! isset(WeChatPayService::$tradeTypes[$payLog->pay_way])) { + throw new BizException('支付方式不支持'); + } - DistributionPreIncomeJob::create([ - 'jobable_id' => $order->id, - 'jobable_type' => $order->getMorphClass(), - 'remarks' => '支付订单', - ]); - - return $order; + return (new WeChatPayService())->pay([ + 'body' => app_settings('app_name').'-商城订单', + 'out_trade_no' => $payLog->pay_sn, + 'total_fee' => $order->total_amount, + 'trade_type' => WeChatPayService::$tradeTypes[$payLog->pay_way], + ]); + } } /** diff --git a/app/Services/PayService.php b/app/Services/PayService.php index a67efbd3..15965af7 100644 --- a/app/Services/PayService.php +++ b/app/Services/PayService.php @@ -2,15 +2,15 @@ namespace App\Services; -use App\Constants\PayWay; use App\Exceptions\BizException; +use App\Models\DistributionPreIncomeJob; use App\Models\Order; use App\Models\PayLog; class PayService { /** - * 支付成功 + * 根据支付流水号处理支付成功业务 * * @param string $sn * @param array $params @@ -18,10 +18,24 @@ class PayService * * @throws \App\Exceptions\BizException */ - public function paySuccess(string $sn, array $params = []): PayLog + public function handleSuccessByPaySerialNumber(string $paySerialNumber, array $params = []): PayLog { - $payLog = PayLog::where('pay_sn', $sn)->firstOrFail(); + $payLog = PayLog::where('pay_sn', $paySerialNumber)->lockForUpdate()->first(); + return $this->handleSuccess($payLog, $params); + } + + /** + * 处理支付成功业务 + * + * @param \App\Models\PayLog $payLog + * @param array $params + * @return \App\Models\PayLog + * + * @throws \App\Exceptions\BizException + */ + public function handleSuccess(PayLog $payLog, array $params = []): PayLog + { if (! $payLog->isPending()) { throw new BizException('支付记录状态异常'); } @@ -33,25 +47,30 @@ class PayService ]); if ($payLog->payable instanceof Order) { - switch ($payLog->pay_way) { - case PayWay::WXPAY_APP: - case PayWay::WXPAY_H5: - case PayWay::WXPAY_JSAPI: - case PayWay::WXPAY_MINI: - case PayWay::WXPAY_NATIVE: - $payWay = Order::PAY_WAY_WXPAY; - break; + $order = $payLog->payable; - default: - $payWay = $payLog->pay_way; - break; + // 支付方式 + $payWay = $payLog->pay_way; + if ($payLog->isWxpay()) { + $payWay = Order::PAY_WAY_WXPAY; } - (new OrderService())->paySuccess($payLog->payable, [ + if (! $order->isPending()) { + throw new BizException('订单状态不是待支付'); + } + + $order->update([ 'pay_sn' => $payLog->pay_sn, 'pay_way' => $payWay, 'pay_at' => $payLog->pay_at, 'out_trade_no' => $payLog->out_trade_no, + 'status' => Order::STATUS_PAID, + ]); + + DistributionPreIncomeJob::create([ + 'jobable_id' => $order->id, + 'jobable_type' => $order->getMorphClass(), + 'remarks' => '支付订单', ]); } @@ -59,7 +78,7 @@ class PayService } /** - * 支付失败 + * 根据支付流水号处理支付失败业务 * * @param string $sn * @param array $params @@ -67,10 +86,24 @@ class PayService * * @throws \App\Exceptions\BizException */ - public function payFailed(string $sn, array $params = []): PayLog + public function handleFailedByPaySerialNumber(string $paySerialNumber, array $params = []): PayLog { - $payLog = PayLog::where('pay_sn', $sn)->firstOrFail(); + $payLog = PayLog::where('pay_sn', $paySerialNumber)->lockForUpdate()->first(); + return $this->handleFailed($payLog, $params); + } + + /** + * 处理支付失败业务 + * + * @param \App\Models\PayLog $payLog + * @param array $params + * @return \App\Models\PayLog + * + * @throws \App\Exceptions\BizException + */ + public function handleFailed(PayLog $payLog, array $params = []): PayLog + { if (! $payLog->isPending()) { throw new BizException('支付记录状态异常'); } diff --git a/app/Services/WeChatPayService.php b/app/Services/WeChatPayService.php index a3ad2ed3..a4289169 100644 --- a/app/Services/WeChatPayService.php +++ b/app/Services/WeChatPayService.php @@ -3,6 +3,7 @@ namespace App\Services; use App\Exceptions\WeChatPayException; +use App\Models\PayLog; use Closure; use EasyWeChat\Factory; use EasyWeChat\Payment\Application; @@ -22,10 +23,11 @@ class WeChatPayService /** * @var array */ - public static $allowTradeTypes = [ - self::TRADE_TYPE_JSAPI, - self::TRADE_TYPE_APP, - self::TRADE_TYPE_H5, + public static $tradeTypes = [ + PayLog::PAY_WAY_WXPAY_APP => self::TRADE_TYPE_APP, + PayLog::PAY_WAY_WXPAY_JSAPI => self::TRADE_TYPE_JSAPI, + PayLog::PAY_WAY_WXPAY_MINI => self::TRADE_TYPE_JSAPI, + PayLog::PAY_WAY_WXPAY_H5 => self::TRADE_TYPE_H5, ]; /** @@ -64,7 +66,7 @@ class WeChatPayService $params['trade_type'] = static::TRADE_TYPE_APP; } - if (! in_array($params['trade_type'], static::$allowTradeTypes)) { + if (! in_array($params['trade_type'], static::$tradeTypes)) { throw new WeChatPayException(sprintf('交易类型 [%s] 暂不支持', $params['trade_type']), $params); } diff --git a/app/helpers.php b/app/helpers.php index 90febd2c..dac8d716 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -37,3 +37,32 @@ if (! function_exists('serial_number')) { return date('YmdHis').sprintf('%06d', mt_rand(1, 999999)); } } + +if (! function_exists('serial_number')) { + /** + * 生成流水号 + * + * @return string + */ + function serial_number(): string + { + return date('YmdHis').sprintf('%06d', mt_rand(1, 999999)); + } +} + +if (! function_exists('trim_trailing_zeros')) { + /** + * 去除数字字符串小数点后多余的 0 + * + * @param mixed $value + * @return mixed + */ + function trim_trailing_zeros($value) + { + if (is_numeric($value) && strpos($value, '.') !== false) { + $value = rtrim(rtrim($value, '0'), '.') ?: '0'; + } + + return $value; + } +} diff --git a/database/migrations/2021_12_07_143655_create_orders_table.php b/database/migrations/2021_12_07_143655_create_orders_table.php index 05e6cd1e..06631fcb 100644 --- a/database/migrations/2021_12_07_143655_create_orders_table.php +++ b/database/migrations/2021_12_07_143655_create_orders_table.php @@ -26,7 +26,7 @@ class CreateOrdersTable extends Migration $table->string('note')->nullable()->comment('客户备注'); $table->string('remark')->nullable()->comment('订单备注'); // 支付信息 - $table->string('pay_sn')->nullable()->comment('支付单号'); + $table->string('pay_sn')->nullable()->comment('支付流水号'); $table->string('pay_way')->nullable()->comment('支付方式'); $table->timestamp('pay_at')->nullable()->comment('支付时间'); // 收货信息