更精确的计费 采用 bc ath

This commit is contained in:
iVampireSP.com 2023-01-17 04:36:43 +08:00
parent 5214362404
commit e0d8fe9cdd
No known key found for this signature in database
GPG Key ID: 2F7B001CA27A8132
15 changed files with 223 additions and 306 deletions

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions\Transaction;
use Exception;
class TransactionFailedException extends Exception
{
//
}

View File

@ -5,7 +5,6 @@
use App\Http\Controllers\Controller;
use App\Models\Balance;
use App\Models\Host;
use App\Models\Transaction;
use App\Models\User;
use App\Models\UserGroup;
use App\Models\WorkOrder\WorkOrder;
@ -101,8 +100,6 @@ public function update(Request $request, User $user): RedirectResponse
'id_card' => 'nullable|string|size:18',
]);
$transaction = new Transaction();
if ($request->input('is_banned')) {
$user->banned_at = Carbon::now();
@ -125,9 +122,13 @@ public function update(Request $request, User $user): RedirectResponse
} else if ($one_time_action == 'stop_all_hosts') {
$user->hosts()->update(['status' => 'stopped', 'suspended_at' => null]);
} else if ($one_time_action == 'add_balance') {
$transaction->addAmount($user->id, 'console', $request->balance ?? 0, '管理员添加。', true);
$description = '管理员 ' . $request->user('admin')->name . " 增加。";
$user->charge($request->input('balance'), 'console', $description);
} else if ($one_time_action == 'reduce_balance') {
$transaction->reduceAmount($user->id, $request->balance ?? 0, '管理员扣除。');
$description = '管理员 ' . $request->user('admin')->name . " 扣除。";
$user->reduce($request->input('balance'), $description);
}
}

View File

@ -6,12 +6,14 @@
use App\Models\Transaction;
use App\Models\User;
use App\Support\RealNameSupport;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
class RealNameController extends Controller
{
public function verify(Request $request)
public function verify(Request $request): JsonResponse
{
$result = (new RealNameSupport())->verify($request->all());
@ -20,7 +22,7 @@ public function verify(Request $request)
return $this->error('实名认证失败。');
}
$user = User::find($result['user_id']);
$user = (new User)->find($result['user_id']);
$user->real_name = $result['name'];
$user->id_card = $result['id_card'];
$user->save();
@ -31,7 +33,7 @@ public function verify(Request $request)
return $this->success('实名认证成功。');
}
public function process()
public function process(): View
{
return view('real_name.process');
}

View File

@ -48,7 +48,6 @@ public function store(Request $request): RedirectResponse
*/
public function show(Request $request, Balance $balance): RedirectResponse|JsonResponse|View
{
if ($balance->isPaid()) {
if ($request->ajax()) {
return $this->success($balance);
@ -228,8 +227,7 @@ function notify(
}
if ($is_paid) {
(new Transaction)->addAmount($balance->user_id, $balance->payment, $balance->amount);
// $balance->user->charge($balance->amount, $balance->payment, $balance->order_id);
$balance->update([
'paid_at' => now()
]);

View File

@ -4,13 +4,15 @@
use App\Http\Controllers\Controller;
use App\Support\RealNameSupport;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class RealNameController extends Controller
{
public function store(Request $request)
public function store(Request $request): RedirectResponse
{
$request->validate([
'real_name' => 'required|string',
@ -54,7 +56,7 @@ public function store(Request $request)
return redirect($output);
}
public function create()
public function create(): View
{
return view('real_name.create');
}

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\Transaction;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@ -11,8 +10,6 @@
class TransferController extends Controller
{
//
public function index(Request $request): View
{
$user = $request->user();
@ -24,7 +21,7 @@ public function index(Request $request): View
public function transfer(Request $request): RedirectResponse
{
$request->validate([
'amount' => 'numeric|min:1|max:100',
'amount' => 'string|min:1|max:100',
'description' => 'nullable|string|max:100',
]);
@ -38,14 +35,15 @@ public function transfer(Request $request): RedirectResponse
return back()->withErrors(['to' => '不能转给自己。']);
}
$transaction = new Transaction();
$amount = $request->input('amount');
if ($user->balance < $request->input('amount')) {
return back()->withErrors(['amount' => '您的余额不足。']);
} else {
$transaction->transfer($user, $to, $request->input('amount'), $request->input('description'));
// 使用 bc 判断金额是否足够
if (bccomp($amount, $user->balance, 2) > 0) {
return back()->withErrors(['amount' => '余额不足。']);
}
$user->startTransfer($to, $amount, $request->input('description'));
return back()->with('success', '转账成功,已达对方账户。');
}
}

View File

@ -4,7 +4,6 @@
use App\Jobs\Job;
use App\Models\Balance;
use App\Models\Transaction;
use Illuminate\Contracts\Queue\ShouldQueue;
use Yansongda\LaravelPay\Facades\Pay;
use Yansongda\Pay\Exception\ContainerException;
@ -62,8 +61,6 @@ public function checkAndCharge(Balance $balance, $check = false): bool
return true;
}
(new Transaction)->addAmount($balance->user_id, 'alipay', $balance->amount);
$balance->update([
'paid_at' => now()
]);

View File

@ -48,6 +48,8 @@ protected static function boot()
if ($balance->paid_at) {
$balance->notify(new UserCharged());
broadcast(new Users($balance->user, 'balance.updated', $balance));
$balance->user->charge($balance->amount, $balance->payment, $balance->order_id);
}
}
});

View File

@ -45,11 +45,11 @@ protected static function boot()
$model->minute_at = now()->minute;
if ($model->price !== null) {
$model->price = round($model->price, 2);
$model->price = bcdiv($model->price, 1, 2);
}
if ($model->managed_price !== null) {
$model->managed_price = round($model->managed_price, 2);
$model->managed_price = bcdiv($model->managed_price, 1, 2);
}
});
@ -153,7 +153,7 @@ public function safeDelete(): bool
return true;
}
public function cost($amount = null, $auto = true): bool
public function cost(string $amount = null, $auto = true): bool
{
$this->load('user');
$user = $this->user;
@ -177,20 +177,23 @@ public function cost($amount = null, $auto = true): bool
$append_description = '';
if ($user_group) {
if ($user_group->discount !== 100 && $user_group->discount !== null) {
$real_price = $real_price * ($user_group->discount / 100);
$real_price = bcmul($real_price, bcdiv($user_group->discount, "100", 2), 2);
$append_description = ' (折扣 ' . $user_group->discount . '%)';
}
}
if ($auto) {
// 获取本月天数
$days = now()->daysInMonth;
// 本月每天的每小时的价格
$real_price = $real_price / $days / 24;
// 使用 bcmath 函数,解决浮点数计算精度问题
$real_price = bcdiv($real_price, $days, 4);
$real_price = bcdiv($real_price, 24, 4);
}
if ($real_price == 0) {
echo '价格为 0不扣费';
return true;
}
@ -199,9 +202,7 @@ public function cost($amount = null, $auto = true): bool
$real_price = 0.0001;
}
$real_price = round($real_price ?? 0, 4);
$transaction = new Transaction();
$real_price = bcdiv($real_price, 1, 4);
$month = now()->month;
@ -215,7 +216,7 @@ public function cost($amount = null, $auto = true): bool
$hosts_balances[$this->id] = $real_price;
}
$hosts_balances[$this->id] = round($hosts_balances[$this->id], 4);
$hosts_balances[$this->id] = bcdiv($hosts_balances[$this->id], 1, 4);
Cache::put($month_cache_key, $hosts_balances, 604800);
@ -229,7 +230,12 @@ public function cost($amount = null, $auto = true): bool
$description .= $append_description;
}
$left = $transaction->reduceHostAmount($this->user_id, $this->id, $this->module_id, $real_price, $description);
$data = [
'host_id' => $this->id,
'module_id' => $this->module_id,
];
$left = $user->reduce($real_price, $description, false, $data);
$this->addLog($real_price);
@ -244,9 +250,9 @@ public function cost($amount = null, $auto = true): bool
return true;
}
public function addLog(float|null $amount = 0): bool
public function addLog(string $amount = "0"): bool
{
if ($amount === 0 || $amount === null) {
if ($amount === "0") {
return false;
}
@ -256,12 +262,12 @@ public function addLog(float|null $amount = 0): bool
$cache_key = 'module_earning_' . $this->module_id;
$commission = (float)config('billing.commission');
$commission = config('billing.commission');
$should_amount = round($amount * $commission, 2);
$should_amount = bcmul($amount, $commission, 2);
// 应得的余额
$should_balance = $amount - $should_amount;
$should_balance = bcsub($amount, $should_amount, 2);
$earnings = Cache::get($cache_key, []);
@ -270,8 +276,10 @@ public function addLog(float|null $amount = 0): bool
}
if (isset($earnings[$current_year][$current_month])) {
$earnings[$current_year][$current_month]['balance'] += $amount;
$earnings[$current_year][$current_month]['should_balance'] += $should_balance;
$earnings[$current_year][$current_month]['balance'] = bcadd($earnings[$current_year][$current_month]['balance'], $amount, 2);
$earnings[$current_year][$current_month]['should_balance'] = bcadd($earnings[$current_year][$current_month]['should_balance'], $should_balance, 2);
} else {
$earnings[$current_year][$current_month] = [
'balance' => $amount,

View File

@ -2,11 +2,7 @@
namespace App\Models;
use App\Exceptions\User\BalanceNotEnoughException;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\HigherOrderBuilderProxy;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\HigherOrderCollectionProxy;
use Jenssegers\Mongodb\Eloquent\Model;
class Transaction extends Model
@ -27,7 +23,7 @@ class Transaction extends Model
];
protected $fillable = [
// 交易类型
// 类型
'type',
// 交易渠道
@ -36,15 +32,12 @@ class Transaction extends Model
// 描述
'description',
// 入账
'income',
// 交易金额,负数则是扣除
'amount',
// 出账
'outcome',
// 可用余额
'balances',
'balance',
// 剩余余额
'user_remain',
'module_remain',
// 赠送金额
'gift',
@ -59,219 +52,32 @@ public function scopeThisUser($query)
return $query->where('user_id', auth()->id());
}
public function reduceAmount($user_id, $amount = 0, $description = '扣除费用请求。')
// on create
protected static function boot()
{
parent::boot();
$lock = Cache::lock("user_balance_lock_" . $user_id, 10);
try {
static::creating(function (self $transaction) {
$user = null;
$module = null;
$lock->block(5);
$user = (new User)->findOrFail($user_id);
$user->balance -= $amount;
$user->save();
$this->addPayoutBalance($user_id, $amount, $description);
} finally {
optional($lock)->release();
}
return $user->balance;
}
public function addPayoutBalance($user_id, $amount, $description, $module_id = null)
{
$data = [
'type' => 'payout',
'payment' => 'balance',
'description' => $description,
'income' => 0,
'outcome' => (float)$amount,
];
if ($module_id) {
$data['module_id'] = $module_id;
}
return $this->addLog($user_id, $data);
}
private function addLog($user_id, $data)
{
$user = (new User)->find($user_id);
$current = [
'balance' => (float)$user->balance,
'user_id' => intval($user_id),
];
// merge
$data = array_merge($data, $current);
// add expired at
$data['expired_at'] = now()->addSeconds(7);
/** @noinspection PhpUndefinedMethodInspection */
return $this->create($data);
}
/**
* @throws BalanceNotEnoughException
*/
public function reduceAmountModuleFail($user_id, $module_id, $amount = 0, $description = '扣除费用请求。')
{
$lock = Cache::lock("user_balance_lock_" . $user_id, 10);
try {
$lock->block(5);
$user = (new User)->findOrFail($user_id);
$user->balance -= $amount;
// if balance < 0
if ($user->balance < 0) {
throw new BalanceNotEnoughException('余额不足。');
if ($transaction->user_id) {
$user = (new User)->find($transaction->user_id);
}
$user->save();
$this->addPayoutBalance($user_id, $amount, $description, $module_id);
} finally {
optional($lock)->release();
}
return $user->balance;
}
public function reduceHostAmount($user_id, $host_id, $module_id, $amount = 0, $description = '扣除费用请求。')
{
$lock = Cache::lock("user_balance_lock_" . $user_id, 10);
try {
$lock->block(5);
$user = (new User)->findOrFail($user_id);
$user->balance -= $amount;
$user->save();
$this->addHostPayoutBalance($user_id, $host_id, $module_id, $amount, $description);
} finally {
optional($lock)->release();
}
return $user->balance;
}
public function addHostPayoutBalance($user_id, $host_id, $module_id, $amount, $description)
{
$data = [
'type' => 'payout',
'payment' => 'balance',
'description' => $description,
'income' => 0,
'outcome' => (float)$amount,
'host_id' => $host_id,
'module_id' => $module_id,
];
return $this->addLog($user_id, $data);
}
/**
* @param $user_id
* @param string $payment
* @param int $amount
* @param null $description
* @param bool $add_charge_log
*
* @return float|HigherOrderBuilderProxy|HigherOrderCollectionProxy|int|mixed|string
*/
public function addAmount($user_id, string $payment = 'console', int $amount = 0, $description = null, bool $add_charge_log = false): mixed
{
$lock = Cache::lock("user_balance_lock_" . $user_id, 10);
try {
$lock->block(5);
$user = (new User)->findOrFail($user_id);
$left_balance = $user->balance + $amount;
$user->increment('balance', $amount);
if (!$description) {
$description = '充值 ' . $amount . ' 元';
if ($transaction->module_id) {
$module = (new Module)->find($transaction->module_id);
}
if ($add_charge_log) {
$data = [
'user_id' => $user_id,
'amount' => $amount,
'payment' => $payment,
'paid_at' => Carbon::now(),
];
(new Balance)->create($data);
if ($user) {
$transaction->user_remain = $user->balance;
}
$this->addIncomeBalance($user_id, $payment, $amount, $description);
} finally {
optional($lock)->release();
}
return $left_balance;
}
public function addIncomeBalance($user_id, $payment, $amount, $description)
{
$data = [
'type' => 'income',
'payment' => $payment,
'description' => $description,
'income' => (float)$amount,
'outcome' => 0,
];
return $this->addLog($user_id, $data);
}
public function transfer(User $user, User $to, float $amount, string|null $description): float
{
$lock = Cache::lock("user_balance_lock_" . $user->id, 10);
$lock_to = Cache::lock("user_balance_lock_" . $to->id, 10);
try {
$lock->block(5);
$lock_to->block(5);
$user->balance -= $amount;
$user->save();
$to->balance += $amount;
$to->save();
if (!$description) {
$description = '完成。';
if ($module) {
$transaction->module_remain = $module->balance;
}
$description_new = "转账给 $to->name($to->email) $amount 元,$description";
$this->addPayoutBalance($user->id, $amount, $description_new);
$description_new = "收到来自 $user->name($user->email) 转来的 $amount 元, $description";
$this->addIncomeBalance($to->id, 'transfer', $amount, $description_new);
} finally {
optional($lock)->release();
optional($lock_to)->release();
}
return $user->balance;
$transaction->expired_at = Carbon::now()->addSeconds(7)->toString();
});
}
}

View File

@ -3,6 +3,7 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Exceptions\User\BalanceNotEnoughException;
use Carbon\Exceptions\InvalidFormatException;
use GeneaLabs\LaravelModelCaching\Traits\Cachable;
use Illuminate\Contracts\Encryption\DecryptException;
@ -12,6 +13,7 @@
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Laravel\Sanctum\HasApiTokens;
@ -111,9 +113,9 @@ private function getBirthdayFromIdCard(): string
$idCard = $this->id_card;
$bir = substr($idCard, 6, 8);
$year = (int) substr($bir, 0, 4);
$month = (int) substr($bir, 4, 2);
$day = (int) substr($bir, 6, 2);
$year = (int)substr($bir, 0, 4);
$month = (int)substr($bir, 4, 2);
$day = (int)substr($bir, 6, 2);
return $year . '-' . $month . '-' . $day;
}
@ -164,4 +166,107 @@ public function selectPublic(): User
// 过滤掉私有字段
return $this->select(['id', 'name', 'email_md5', 'created_at']);
}
/**
* 扣除费用
*
* @param string $amount
* @param string $description
* @param bool $fail
* @param array $options
*
* @return string
*/
public function reduce(string $amount = "0", string $description = "消费", bool $fail = false, array $options = []): string
{
Cache::lock('user_balance_' . $this->id, 10)->block(10, function () use ($amount, $fail, $description, $options) {
$this->refresh();
if ($this->balance < $amount) {
if ($fail) {
throw new BalanceNotEnoughException();
}
}
$this->balance = bcsub($this->balance, $amount, 2);
$this->save();
$data = [
'user_id' => $this->id,
'amount' => $amount,
'description' => $description,
'payment' => 'balance',
'type' => 'payout',
];
if ($options) {
$data = array_merge($data, $options);
}
(new Transaction)->create($data);
});
return $this->balance;
}
/**
* 增加余额
*
* @param string $amount
* @param string $payment
* @param string $description
* @param array $options
*
* @return string
*/
public function charge(string $amount = "0", string $payment = 'console', string $description = '充值', array $options = []): string
{
Cache::lock('user_balance_' . $this->id, 10)->block(10, function () use ($amount, $description, $payment, $options) {
$this->refresh();
$this->balance = bcadd($this->balance, $amount, 2);
$this->save();
$data = [
'user_id' => $this->id,
'amount' => $amount,
'payment' => $payment,
'description' => $description,
'type' => 'income',
];
if ($options) {
$data = array_merge($data, $options);
}
(new Transaction)->create($data);
(new Balance)->create([
'user_id' => $this->id,
'amount' => $amount,
'payment' => $payment,
'description' => $description,
'paid_at' => now(),
]);
});
return $this->balance;
}
public function startTransfer(User $to, string $amount, string|null $description)
{
$description_from = "转账给 $to->name($to->email)";
$description_to = "收到 $this->name($this->email) 的转账";
if ($description) {
$description_from .= ",备注:$description";
$description_to .= ",备注:$description";
}
$this->reduce($amount, $description_from, true);
$to->charge($amount, 'transfer', $description_to);
return $this->balance;
}
}

View File

@ -7,14 +7,14 @@
class Payment extends Component
{
public string $payment = '';
public string|null $payment = '';
/**
* Create a new component instance.
*
* @return void
*/
public function __construct(string $payment)
public function __construct(string|null $payment)
{
//
$this->payment = $payment;

View File

@ -30,7 +30,8 @@
"spatie/laravel-tags": "^4.3",
"spiral/roadrunner": "^2.8.2",
"symfony/psr-http-message-bridge": "^2.1",
"yansongda/laravel-pay": "~3.2.0"
"yansongda/laravel-pay": "~3.2.0",
"ext-bcmath": "*"
},
"require-dev": {
"beyondcode/laravel-query-detector": "^1.6",

View File

@ -13,13 +13,12 @@
<table class="table table-hover">
<thead>
<tr>
<th scope="col">类型与模块</th>
<th scope="col">模块</th>
<th scope="col">支付方式</th>
<th scope="col">说明</th>
<th scope="col">用户 ID</th>
<th scope="col">主机 ID</th>
<th scope="col">入账</th>
<th scope="col">支出</th>
<th scope="col">金额</th>
<th scope="col">余额</th>
<th scope="col">交易时间</th>
</tr>
@ -27,19 +26,8 @@
<tbody>
@foreach ($transactions as $t)
<tr>
<td>
@if ($t->type === 'payout')
<span class="text-danger">
支出
</span>
@elseif($t->type === 'income')
<span class="text-success">
收入
</span>
@endif
&nbsp;
<td> &nbsp;
<span class="module_name" module="{{ $t->module_id }}">{{ $t->module_id }}</span>
</td>
<td>
<x-payment :payment="$t->payment"></x-payment>
@ -61,16 +49,21 @@
<a href="?host_id={{ $t->host_id }}">筛选</a>
@endif
</td>
<td class="text-success">
{{ $t->income }}
</td>
<td class="text-danger">
{{ $t->outcome }}
<td>
@if ($t->type === 'payout')
<span class="text-danger">
支出 {{ $t->amount }}
</span>
@elseif($t->type === 'income')
<span class="text-success">
收入 {{ $t->amount }}
</span>
@endif
</td>
<td>
{{ $t->balance ?? $t->balances }}
{{ $t->user_remain ?? $t->balance }}
</td>
<td>
{{ $t->created_at }}

View File

@ -13,11 +13,10 @@
<table class="table table-hover">
<thead>
<tr>
<th scope="col">类型与模块</th>
<th scope="col">模块</th>
<th scope="col">支付方式</th>
<th scope="col">说明</th>
<th scope="col">入账</th>
<th scope="col">支出</th>
<th scope="col">金额</th>
<th scope="col">余额</th>
<th scope="col">交易时间</th>
</tr>
@ -26,17 +25,7 @@
@foreach ($transactions as $t)
<tr>
<td>
@if ($t->type === 'payout')
<span class="text-danger">
支出
</span>
@elseif($t->type === 'income')
<span class="text-success">
收入
</span>
@endif
&nbsp;
<td> &nbsp;
<span class="module_name" module="{{ $t->module_id }}">{{ $t->module_id }}</span>
</td>
@ -46,16 +35,21 @@
<td>
{{ $t->description }}
</td>
<td class="text-success">
{{ $t->income }}
</td>
<td class="text-danger">
{{ $t->outcome }}
<td>
@if ($t->type === 'payout')
<span class="text-danger">
支出 {{ $t->amount }}
</span>
@elseif($t->type === 'income')
<span class="text-success">
收入 {{ $t->amount }}
</span>
@endif
</td>
<td>
{{ $t->balance ?? $t->balances }}
{{ $t->amount ?? $t->balance }}
</td>
<td>
{{ $t->created_at }}