6
0
Fork 0

余额支付

release
李静 2021-12-28 11:11:16 +08:00
parent 0fb25292f5
commit e56998ff39
13 changed files with 323 additions and 39 deletions

View File

@ -5,15 +5,18 @@ namespace App\Endpoint\Api\Http\Controllers\Account;
use App\Endpoint\Api\Http\Controllers\Controller; use App\Endpoint\Api\Http\Controllers\Controller;
use App\Endpoint\Api\Http\Resources\DistributionPreIncomeResource; use App\Endpoint\Api\Http\Resources\DistributionPreIncomeResource;
use App\Endpoint\Api\Http\Resources\WalletLogResource; use App\Endpoint\Api\Http\Resources\WalletLogResource;
use App\Exceptions\BizException; use App\Exceptions\InvalidPaySerialNumberException;
use App\Helpers\Paginator as PaginatorHelper; use App\Helpers\Paginator as PaginatorHelper;
use App\Models\BalanceLog;
use App\Models\Order; use App\Models\Order;
use App\Models\PayLog; use App\Models\PayLog;
use App\Models\WalletLog; use App\Models\WalletLog;
use App\Services\BalanceService;
use App\Services\PayService; use App\Services\PayService;
use App\Services\WalletService; use App\Services\WalletService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Throwable; use Throwable;
class WalletController extends Controller class WalletController extends Controller
@ -88,29 +91,38 @@ class WalletController extends Controller
{ {
$validated = $request->validate([ $validated = $request->validate([
'pay_sn' => ['bail', 'required'], 'pay_sn' => ['bail', 'required'],
'pay_way' => ['bail', 'required'], 'pay_way' => ['bail', 'required', Rule::in([PayLog::PAY_WAY_WALLET, PayLog::PAY_WAY_BALANCE])],
]); ]);
$user = $request->user(); $user = $request->user();
// todo 校验支付密码
try { try {
DB::transaction(function () use ($user, $validated) { DB::transaction(function () use ($user, $validated) {
$payLog = PayLog::where('pay_sn', $validated['pay_sn'])->where('pay_way', $validated['pay_way'])->lockForUpdate()->firstOrFail(); $payLog = PayLog::where('pay_sn', $validated['pay_sn'])
->where('pay_way', $validated['pay_way'])
->lockForUpdate()
->first();
if ($payLog === null) {
throw new InvalidPaySerialNumberException();
}
$payable = $payLog->payable; $payable = $payLog->payable;
switch (get_class($payable)) { if ($payable instanceof Order) {
case Order::class: if ($payLog->isWallet()) {
(new WalletService())->changeBalance($user, -$payable->total_amount, WalletLog::ACTION_ORDER_PAY, '订单支付扣款', $payable); (new WalletService())->changeBalance($user, -$payable->total_amount, WalletLog::ACTION_ORDER_PAY, '订单-支付', $payable);
break; } else {
(new BalanceService())->changeBalance($user, -$payable->total_amount, BalanceLog::ACTION_ORDER_PAY, '订单-支付', $payable);
default: }
throw new BizException('非法操作');
break;
} }
(new PayService())->handleSuccess($payLog); (new PayService())->handleSuccess($payLog);
}); });
} catch (InvalidPaySerialNumberException $e) {
throw $e;
} catch (Throwable $e) { } catch (Throwable $e) {
try { try {
(new PayService())->handleFailedByPaySerialNumber($validated['pay_sn'], [ (new PayService())->handleFailedByPaySerialNumber($validated['pay_sn'], [

View File

@ -0,0 +1,11 @@
<?php
namespace App\Exceptions;
class BalanceNotEnoughException extends BizException
{
public function __construct()
{
parent::__construct('余额不足');
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Exceptions;
class InvalidPaySerialNumberException extends BizException
{
public function __construct()
{
parent::__construct('无效的支付流水号');
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Exceptions;
class WalletNotEnoughException extends BizException
{
public function __construct()
{
parent::__construct('可提不足');
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Balance extends Model
{
/**
* @var array
*/
protected $attributes = [
'balance' => 0,
'total_revenue' => 0,
'total_expenses' => 0,
'transferable' => true,
];
/**
* @var array
*/
protected $fillable = [
'user_id',
'balance',
'total_expenses',
'total_revenue',
'transferable',
];
/**
* @var array
*/
protected $casts = [
'transferable' => 'bool',
];
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class BalanceLog extends Model
{
public const ACTION_ORDER_PAY = 1;
/**
* @var array
*/
protected $fillable = [
'user_id',
'loggable_id',
'loggable_type',
'action',
'before_balance',
'change_balance',
'remarks',
];
/**
* 获取变动金额
*
* @return string
*/
public function getChangeBalanceFormatAttribute()
{
return trim_trailing_zeros(bcdiv($this->attributes['change_balance'], 100, 2));
}
}

View File

@ -97,4 +97,14 @@ class PayLog extends Model
{ {
return $this->pay_way === static::PAY_WAY_WALLET; return $this->pay_way === static::PAY_WAY_WALLET;
} }
/**
* 确认支付方式是否是余额支付
*
* @return bool
*/
public function isBalance(): bool
{
return $this->pay_way === static::PAY_WAY_BALANCE;
}
} }

View File

@ -196,6 +196,22 @@ class User extends Model implements AuthorizableContract, AuthenticatableContrac
return $this->hasMany(WalletLog::class, 'user_id'); return $this->hasMany(WalletLog::class, 'user_id');
} }
/**
* 用户的余额
*/
public function balance()
{
return $this->hasOne(Balance::class, 'user_id');
}
/**
* 用户的余额日志
*/
public function balanceLogs()
{
return $this->hasOne(BalanceLog::class, 'user_id');
}
/** /**
* 用户的预收益 * 用户的预收益
*/ */
@ -278,22 +294,6 @@ class User extends Model implements AuthorizableContract, AuthenticatableContrac
]; ];
} }
/**
* 获取此用户的钱包并加行锁
*
* @return \App\Models\Wallet
*/
public function lockWallet(): Wallet
{
if ($wallet = $this->wallet()->lockForUpdate()->first()) {
return $wallet;
}
$this->wallet()->create();
return $this->wallet()->lockForUpdate()->first();
}
/** /**
* 创建用户 * 创建用户
* *

View File

@ -0,0 +1,61 @@
<?php
namespace App\Services;
use App\Exceptions\BalanceNotEnoughException;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class BalanceService
{
/**
* 变更余额
*
* @param \App\Models\User $user
* @param int $changeBalance
* @param int $action
* @param string|null $remarks
* @param mixed $loggable
* @return void
*/
public function changeBalance(User $user, int $changeBalance, int $action, ?string $remarks = null, $loggable = null)
{
$balance = $user->balance()->lockForUpdate()->first();
if (is_null($balance)) {
throw new BalanceNotEnoughException();
}
// 变更前余额
$beforeBalance = $balance->balance;
$_changeBalance = abs($changeBalance);
if ($changeBalance > 0) {
// 收入
$user->balance()->update([
'balance' => DB::raw("balance + {$_changeBalance}"),
'total_revenue' => DB::raw("total_revenue + {$_changeBalance}"),
]);
} else {
// 支出
if ($balance->balance < $_changeBalance) {
throw new BalanceNotEnoughException();
}
$user->balance()->update([
'balance' => DB::raw("balance - {$_changeBalance}"),
'total_expenses' => DB::raw("total_expenses + {$_changeBalance}"),
]);
}
$user->balanceLogs()->create([
'loggable_id' => $loggable?->id,
'loggable_type' => $loggable?->getMorphClass(),
'before_balance' => $beforeBalance,
'change_balance' => $changeBalance,
'action' => $action,
'remarks' => $remarks,
]);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Services; namespace App\Services;
use App\Exceptions\BizException; use App\Exceptions\BizException;
use App\Exceptions\InvalidPaySerialNumberException;
use App\Models\DistributionPreIncomeJob; use App\Models\DistributionPreIncomeJob;
use App\Models\Order; use App\Models\Order;
use App\Models\PayLog; use App\Models\PayLog;
@ -17,11 +18,16 @@ class PayService
* @return \App\Models\PayLog * @return \App\Models\PayLog
* *
* @throws \App\Exceptions\BizException * @throws \App\Exceptions\BizException
* @throws \App\Exceptions\InvalidPaySerialNumberException
*/ */
public function handleSuccessByPaySerialNumber(string $paySerialNumber, array $params = []): PayLog public function handleSuccessByPaySerialNumber(string $paySerialNumber, array $params = []): PayLog
{ {
$payLog = PayLog::where('pay_sn', $paySerialNumber)->lockForUpdate()->first(); $payLog = PayLog::where('pay_sn', $paySerialNumber)->lockForUpdate()->first();
if ($payLog === null) {
throw new InvalidPaySerialNumberException();
}
return $this->handleSuccess($payLog, $params); return $this->handleSuccess($payLog, $params);
} }
@ -33,11 +39,12 @@ class PayService
* @return \App\Models\PayLog * @return \App\Models\PayLog
* *
* @throws \App\Exceptions\BizException * @throws \App\Exceptions\BizException
* @throws \App\Exceptions\InvalidPaySerialNumberException
*/ */
public function handleSuccess(PayLog $payLog, array $params = []): PayLog public function handleSuccess(PayLog $payLog, array $params = []): PayLog
{ {
if (! $payLog->isPending()) { if (! $payLog->isPending()) {
throw new BizException('支付异常'); throw new InvalidPaySerialNumberException();
} }
$payLog->update([ $payLog->update([
@ -46,24 +53,32 @@ class PayService
'status' => PayLog::STATUS_SUCCESS, 'status' => PayLog::STATUS_SUCCESS,
]); ]);
if ($payLog->payable instanceof Order) { $payable = $payLog->payable;
$order = $payLog->payable;
if ($order->isPaid()) { if ($payable instanceof Order) {
if ($payable->isPaid()) {
throw new BizException('订单已支付'); throw new BizException('订单已支付');
} }
if ($order->isCompleted()) { if ($payable->isCompleted()) {
throw new BizException('订单已完成'); throw new BizException('订单已完成');
} }
// 支付方式 // 支付方式
$payWay = $payLog->pay_way; $payWay = $payLog->pay_way;
if ($payLog->isWxpay()) { if ($payLog->isWxpay()) {
// 微信支付
$payWay = Order::PAY_WAY_WXPAY; $payWay = Order::PAY_WAY_WXPAY;
} elseif ($payLog->isWallet()) {
// 可提支付
$payWay = Order::PAY_WAY_WALLET;
} elseif ($payLog->isBalance()) {
// 余额支付
$payWay = Order::PAY_WAY_BALANCE;
} }
$order->update([ $payable->update([
'pay_sn' => $payLog->pay_sn, 'pay_sn' => $payLog->pay_sn,
'pay_way' => $payWay, 'pay_way' => $payWay,
'pay_at' => $payLog->pay_at, 'pay_at' => $payLog->pay_at,
@ -72,8 +87,8 @@ class PayService
]); ]);
DistributionPreIncomeJob::create([ DistributionPreIncomeJob::create([
'jobable_id' => $order->id, 'jobable_id' => $payable->id,
'jobable_type' => $order->getMorphClass(), 'jobable_type' => $payable->getMorphClass(),
'remarks' => '支付订单', 'remarks' => '支付订单',
]); ]);
} }
@ -89,11 +104,16 @@ class PayService
* @return \App\Models\PayLog * @return \App\Models\PayLog
* *
* @throws \App\Exceptions\BizException * @throws \App\Exceptions\BizException
* @throws \App\Exceptions\InvalidPaySerialNumberException
*/ */
public function handleFailedByPaySerialNumber(string $paySerialNumber, array $params = []): PayLog public function handleFailedByPaySerialNumber(string $paySerialNumber, array $params = []): PayLog
{ {
$payLog = PayLog::where('pay_sn', $paySerialNumber)->lockForUpdate()->first(); $payLog = PayLog::where('pay_sn', $paySerialNumber)->lockForUpdate()->first();
if ($payLog === null) {
throw new InvalidPaySerialNumberException();
}
return $this->handleFailed($payLog, $params); return $this->handleFailed($payLog, $params);
} }
@ -105,11 +125,12 @@ class PayService
* @return \App\Models\PayLog * @return \App\Models\PayLog
* *
* @throws \App\Exceptions\BizException * @throws \App\Exceptions\BizException
* @throws \App\Exceptions\InvalidPaySerialNumberException
*/ */
public function handleFailed(PayLog $payLog, array $params = []): PayLog public function handleFailed(PayLog $payLog, array $params = []): PayLog
{ {
if (! $payLog->isPending()) { if (! $payLog->isPending()) {
throw new BizException('支付异常'); throw new InvalidPaySerialNumberException();
} }
$payLog->update([ $payLog->update([

View File

@ -2,7 +2,7 @@
namespace App\Services; namespace App\Services;
use App\Exceptions\BizException; use App\Exceptions\WalletNotEnoughException;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -20,7 +20,11 @@ class WalletService
*/ */
public function changeBalance(User $user, int $changeBalance, int $action, ?string $remarks = null, $loggable = null) public function changeBalance(User $user, int $changeBalance, int $action, ?string $remarks = null, $loggable = null)
{ {
$wallet = $user->lockWallet(); $wallet = $user->wallet()->lockForUpdate()->first();
if (is_null($wallet)) {
throw new WalletNotEnoughException();
}
// 变更前余额 // 变更前余额
$beforeBalance = $wallet->balance; $beforeBalance = $wallet->balance;
@ -36,7 +40,7 @@ class WalletService
// 支出 // 支出
if ($wallet->balance < $_changeBalance) { if ($wallet->balance < $_changeBalance) {
throw new BizException('可提余额不足'); throw new WalletNotEnoughException();
} }
$user->wallet()->update([ $user->wallet()->update([

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateBalancesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('balances', function (Blueprint $table) {
$table->unsignedBigInteger('user_id')->primary()->comment('用户ID');
$table->unsignedBigInteger('balance')->default(0)->comment('余额(分)');
$table->unsignedBigInteger('total_expenses')->default(0)->comment('总支出(分)');
$table->unsignedBigInteger('total_revenue')->default(0)->comment('总收入(分)');
$table->boolean('transferable')->default(true)->comment('是否可转账');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('balances');
}
}

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateBalanceLogsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('balance_logs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->comment('用户ID');
$table->nullableMorphs('loggable');
$table->tinyInteger('action')->comment('操作类型');
$table->unsignedBigInteger('before_balance')->default(0)->comment('变更前的余额');
$table->bigInteger('change_balance')->default(0)->comment('变动余额(分)');
$table->string('remarks')->nullable()->comment('备注');
$table->timestamps();
$table->index('user_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('balance_logs');
}
}