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\Resources\DistributionPreIncomeResource;
use App\Endpoint\Api\Http\Resources\WalletLogResource;
use App\Exceptions\BizException;
use App\Exceptions\InvalidPaySerialNumberException;
use App\Helpers\Paginator as PaginatorHelper;
use App\Models\BalanceLog;
use App\Models\Order;
use App\Models\PayLog;
use App\Models\WalletLog;
use App\Services\BalanceService;
use App\Services\PayService;
use App\Services\WalletService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Throwable;
class WalletController extends Controller
@ -88,29 +91,38 @@ class WalletController extends Controller
{
$validated = $request->validate([
'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();
// todo 校验支付密码
try {
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;
switch (get_class($payable)) {
case Order::class:
(new WalletService())->changeBalance($user, -$payable->total_amount, WalletLog::ACTION_ORDER_PAY, '订单支付扣款', $payable);
break;
default:
throw new BizException('非法操作');
break;
if ($payable instanceof Order) {
if ($payLog->isWallet()) {
(new WalletService())->changeBalance($user, -$payable->total_amount, WalletLog::ACTION_ORDER_PAY, '订单-支付', $payable);
} else {
(new BalanceService())->changeBalance($user, -$payable->total_amount, BalanceLog::ACTION_ORDER_PAY, '订单-支付', $payable);
}
}
(new PayService())->handleSuccess($payLog);
});
} catch (InvalidPaySerialNumberException $e) {
throw $e;
} catch (Throwable $e) {
try {
(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 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');
}
/**
* 用户的余额
*/
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;
use App\Exceptions\BizException;
use App\Exceptions\InvalidPaySerialNumberException;
use App\Models\DistributionPreIncomeJob;
use App\Models\Order;
use App\Models\PayLog;
@ -17,11 +18,16 @@ class PayService
* @return \App\Models\PayLog
*
* @throws \App\Exceptions\BizException
* @throws \App\Exceptions\InvalidPaySerialNumberException
*/
public function handleSuccessByPaySerialNumber(string $paySerialNumber, array $params = []): PayLog
{
$payLog = PayLog::where('pay_sn', $paySerialNumber)->lockForUpdate()->first();
if ($payLog === null) {
throw new InvalidPaySerialNumberException();
}
return $this->handleSuccess($payLog, $params);
}
@ -33,11 +39,12 @@ class PayService
* @return \App\Models\PayLog
*
* @throws \App\Exceptions\BizException
* @throws \App\Exceptions\InvalidPaySerialNumberException
*/
public function handleSuccess(PayLog $payLog, array $params = []): PayLog
{
if (! $payLog->isPending()) {
throw new BizException('支付异常');
throw new InvalidPaySerialNumberException();
}
$payLog->update([
@ -46,24 +53,32 @@ class PayService
'status' => PayLog::STATUS_SUCCESS,
]);
if ($payLog->payable instanceof Order) {
$order = $payLog->payable;
$payable = $payLog->payable;
if ($order->isPaid()) {
if ($payable instanceof Order) {
if ($payable->isPaid()) {
throw new BizException('订单已支付');
}
if ($order->isCompleted()) {
if ($payable->isCompleted()) {
throw new BizException('订单已完成');
}
// 支付方式
$payWay = $payLog->pay_way;
if ($payLog->isWxpay()) {
// 微信支付
$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_way' => $payWay,
'pay_at' => $payLog->pay_at,
@ -72,8 +87,8 @@ class PayService
]);
DistributionPreIncomeJob::create([
'jobable_id' => $order->id,
'jobable_type' => $order->getMorphClass(),
'jobable_id' => $payable->id,
'jobable_type' => $payable->getMorphClass(),
'remarks' => '支付订单',
]);
}
@ -89,11 +104,16 @@ class PayService
* @return \App\Models\PayLog
*
* @throws \App\Exceptions\BizException
* @throws \App\Exceptions\InvalidPaySerialNumberException
*/
public function handleFailedByPaySerialNumber(string $paySerialNumber, array $params = []): PayLog
{
$payLog = PayLog::where('pay_sn', $paySerialNumber)->lockForUpdate()->first();
if ($payLog === null) {
throw new InvalidPaySerialNumberException();
}
return $this->handleFailed($payLog, $params);
}
@ -105,11 +125,12 @@ class PayService
* @return \App\Models\PayLog
*
* @throws \App\Exceptions\BizException
* @throws \App\Exceptions\InvalidPaySerialNumberException
*/
public function handleFailed(PayLog $payLog, array $params = []): PayLog
{
if (! $payLog->isPending()) {
throw new BizException('支付异常');
throw new InvalidPaySerialNumberException();
}
$payLog->update([

View File

@ -2,7 +2,7 @@
namespace App\Services;
use App\Exceptions\BizException;
use App\Exceptions\WalletNotEnoughException;
use App\Models\User;
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)
{
$wallet = $user->lockWallet();
$wallet = $user->wallet()->lockForUpdate()->first();
if (is_null($wallet)) {
throw new WalletNotEnoughException();
}
// 变更前余额
$beforeBalance = $wallet->balance;
@ -36,7 +40,7 @@ class WalletService
// 支出
if ($wallet->balance < $_changeBalance) {
throw new BizException('可提余额不足');
throw new WalletNotEnoughException();
}
$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');
}
}