diff --git a/app/Endpoint/Api/Http/Controllers/Account/WalletController.php b/app/Endpoint/Api/Http/Controllers/Account/WalletController.php index 5ec2642c..d5d49473 100644 --- a/app/Endpoint/Api/Http/Controllers/Account/WalletController.php +++ b/app/Endpoint/Api/Http/Controllers/Account/WalletController.php @@ -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'], [ diff --git a/app/Exceptions/BalanceNotEnoughException.php b/app/Exceptions/BalanceNotEnoughException.php new file mode 100644 index 00000000..a159b6ee --- /dev/null +++ b/app/Exceptions/BalanceNotEnoughException.php @@ -0,0 +1,11 @@ + 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', + ]; +} diff --git a/app/Models/BalanceLog.php b/app/Models/BalanceLog.php new file mode 100644 index 00000000..412fb67d --- /dev/null +++ b/app/Models/BalanceLog.php @@ -0,0 +1,33 @@ +attributes['change_balance'], 100, 2)); + } +} diff --git a/app/Models/PayLog.php b/app/Models/PayLog.php index 98c471eb..9d996763 100644 --- a/app/Models/PayLog.php +++ b/app/Models/PayLog.php @@ -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; + } } diff --git a/app/Models/User.php b/app/Models/User.php index 2fdc153c..f6b1227b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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(); - } - /** * 创建用户 * diff --git a/app/Services/BalanceService.php b/app/Services/BalanceService.php new file mode 100644 index 00000000..4642b47b --- /dev/null +++ b/app/Services/BalanceService.php @@ -0,0 +1,61 @@ +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, + ]); + } +} diff --git a/app/Services/PayService.php b/app/Services/PayService.php index 7e22ec91..3a1b9ce8 100644 --- a/app/Services/PayService.php +++ b/app/Services/PayService.php @@ -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([ diff --git a/app/Services/WalletService.php b/app/Services/WalletService.php index b14a8ab5..654e8ef4 100644 --- a/app/Services/WalletService.php +++ b/app/Services/WalletService.php @@ -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([ diff --git a/database/migrations/2021_12_28_094908_create_balances_table.php b/database/migrations/2021_12_28_094908_create_balances_table.php new file mode 100644 index 00000000..b76f715e --- /dev/null +++ b/database/migrations/2021_12_28_094908_create_balances_table.php @@ -0,0 +1,35 @@ +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'); + } +} diff --git a/database/migrations/2021_12_28_095136_create_balance_logs_table.php b/database/migrations/2021_12_28_095136_create_balance_logs_table.php new file mode 100644 index 00000000..c5d5d3d4 --- /dev/null +++ b/database/migrations/2021_12_28_095136_create_balance_logs_table.php @@ -0,0 +1,39 @@ +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'); + } +}