6
0
Fork 0

支付流水记录

release
李静 2021-12-27 18:56:48 +08:00
parent 95112ff7e4
commit a0aa1c0eec
9 changed files with 278 additions and 193 deletions

View File

@ -1,32 +0,0 @@
<?php
namespace App\Constants;
use App\Services\WeChatPayService;
class PayWay
{
/*
|--------------------------------------------
| 微信支付
|--------------------------------------------
*/
public const WXPAY_APP = 'wxpay_app'; // App 支付
public const WXPAY_JSAPI = 'wxpay_jsapi'; // JSAPI 支付
public const WXPAY_MINI = 'wxpay_mini'; // 小程序支付
public const WXPAY_H5 = 'wxpay_h5'; // H5 支付
public const WXPAY_NATIVE = 'wxpay_native'; // Native 支付
/**
* 微信支付交易类型
*
* @var array
*/
public static $wxpayTradeTypes = [
self::WXPAY_APP => 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,
];
}

View File

@ -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) {

View File

@ -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']),
]);

View File

@ -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;
}
}

View File

@ -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],
]);
}
}
/**

View File

@ -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('支付记录状态异常');
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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('支付时间');
// 收货信息