|
@@ -4,10 +4,12 @@ namespace App\Services\Agent;
|
|
|
|
|
|
|
|
use App\Models\Agent\Agent;
|
|
use App\Models\Agent\Agent;
|
|
|
use App\Models\Agent\AgentBalanceLog;
|
|
use App\Models\Agent\AgentBalanceLog;
|
|
|
|
|
+use App\Models\Agent\AgentManualCreditLock;
|
|
|
use App\Models\Agent\AgentProfitCommissionProfile;
|
|
use App\Models\Agent\AgentProfitCommissionProfile;
|
|
|
use App\Models\Agent\AgentProfitEntry;
|
|
use App\Models\Agent\AgentProfitEntry;
|
|
|
use App\Models\Agent\AgentRechargeLock;
|
|
use App\Models\Agent\AgentRechargeLock;
|
|
|
use App\Models\Agent\AgentSetting;
|
|
use App\Models\Agent\AgentSetting;
|
|
|
|
|
+use DateTimeInterface;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -36,6 +38,7 @@ class AgentWalletService
|
|
|
'credit' => '人工加款', 'debit' => '人工扣款', 'recharge' => '充值', 'withdraw_refund' => '提现退回',
|
|
'credit' => '人工加款', 'debit' => '人工扣款', 'recharge' => '充值', 'withdraw_refund' => '提现退回',
|
|
|
'withdraw_freeze' => '提现冻结', 'equity_purchase' => '购买股份', 'margin_credit' => '人工增加保证金',
|
|
'withdraw_freeze' => '提现冻结', 'equity_purchase' => '购买股份', 'margin_credit' => '人工增加保证金',
|
|
|
'margin_refund' => '退还保证金', 'profit_share' => '合营分成', 'commission' => '佣金(旧)',
|
|
'margin_refund' => '退还保证金', 'profit_share' => '合营分成', 'commission' => '佣金(旧)',
|
|
|
|
|
+ 'credit_unlock' => '人工加款解锁',
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
public function adjust(
|
|
public function adjust(
|
|
@@ -47,7 +50,8 @@ class AgentWalletService
|
|
|
string $operatorType,
|
|
string $operatorType,
|
|
|
?int $operatorId,
|
|
?int $operatorId,
|
|
|
string $referenceType = '',
|
|
string $referenceType = '',
|
|
|
- ?int $referenceId = null
|
|
|
|
|
|
|
+ ?int $referenceId = null,
|
|
|
|
|
+ bool $freezeEquityCredit = false,
|
|
|
): AgentBalanceLog {
|
|
): AgentBalanceLog {
|
|
|
if (!isset(self::DIRECTIONS[$type])) {
|
|
if (!isset(self::DIRECTIONS[$type])) {
|
|
|
throw new \InvalidArgumentException('资金变动类型错误');
|
|
throw new \InvalidArgumentException('资金变动类型错误');
|
|
@@ -56,7 +60,7 @@ class AgentWalletService
|
|
|
throw new \InvalidArgumentException('金额必须大于0');
|
|
throw new \InvalidArgumentException('金额必须大于0');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return DB::transaction(function () use ($agentId, $amount, $type, $remark, $idempotencyKey, $operatorType, $operatorId, $referenceType, $referenceId) {
|
|
|
|
|
|
|
+ return DB::transaction(function () use ($agentId, $amount, $type, $remark, $idempotencyKey, $operatorType, $operatorId, $referenceType, $referenceId, $freezeEquityCredit) {
|
|
|
$agent = Agent::query()->lockForUpdate()->findOrFail($agentId);
|
|
$agent = Agent::query()->lockForUpdate()->findOrFail($agentId);
|
|
|
$existing = AgentBalanceLog::query()->where('idempotency_key', $idempotencyKey)->lockForUpdate()->first();
|
|
$existing = AgentBalanceLog::query()->where('idempotency_key', $idempotencyKey)->lockForUpdate()->first();
|
|
|
if ($existing) {
|
|
if ($existing) {
|
|
@@ -64,15 +68,27 @@ class AgentWalletService
|
|
|
return $existing;
|
|
return $existing;
|
|
|
}
|
|
}
|
|
|
$signed = self::DIRECTIONS[$type] > 0 ? bcadd($amount, '0', 4) : bcsub('0', $amount, 4);
|
|
$signed = self::DIRECTIONS[$type] > 0 ? bcadd($amount, '0', 4) : bcsub('0', $amount, 4);
|
|
|
|
|
+ $freezeCredit = $freezeEquityCredit && $type === 'credit' && $agent->isEquity();
|
|
|
|
|
+ $purchaseFromFrozen = $type === 'equity_purchase' ? $this->consumeManualCredits($agent, $amount) : '0.0000';
|
|
|
|
|
+ $balanceChange = $freezeCredit ? '0.0000' : bcadd($signed, $purchaseFromFrozen, 4);
|
|
|
$before = bcadd((string) $agent->balance, '0', 4);
|
|
$before = bcadd((string) $agent->balance, '0', 4);
|
|
|
- $after = bcadd($before, $signed, 4);
|
|
|
|
|
- $realtime = bcadd((string) $agent->realtime_balance, $signed, 4);
|
|
|
|
|
|
|
+ $after = bcadd($before, $balanceChange, 4);
|
|
|
|
|
+ $realtime = bcadd((string) $agent->realtime_balance, $balanceChange, 4);
|
|
|
// 扣款后,待入账分成按结算日依次入账时余额也不能为负(同时保证实时余额不为负)。
|
|
// 扣款后,待入账分成按结算日依次入账时余额也不能为负(同时保证实时余额不为负)。
|
|
|
if (self::DIRECTIONS[$type] < 0 && bccomp(bcadd($after, $this->pendingFloor($agent), 4), '0', 4) < 0) {
|
|
if (self::DIRECTIONS[$type] < 0 && bccomp(bcadd($after, $this->pendingFloor($agent), 4), '0', 4) < 0) {
|
|
|
throw new \RuntimeException('代理余额不足');
|
|
throw new \RuntimeException('代理余额不足');
|
|
|
}
|
|
}
|
|
|
$agent->balance = $after;
|
|
$agent->balance = $after;
|
|
|
$agent->realtime_balance = $realtime;
|
|
$agent->realtime_balance = $realtime;
|
|
|
|
|
+ if ($freezeCredit) {
|
|
|
|
|
+ $lock = AgentManualCreditLock::query()->create([
|
|
|
|
|
+ 'agent_id' => $agentId, 'amount' => $signed, 'remaining' => $signed,
|
|
|
|
|
+ 'unlock_at' => now()->addDays(AgentManualCreditLock::LOCK_DAYS),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $agent->frozen_balance = bcadd((string) $agent->frozen_balance, $signed, 4);
|
|
|
|
|
+ $referenceType = AgentManualCreditLock::REFERENCE_TYPE;
|
|
|
|
|
+ $referenceId = (int) $lock->id;
|
|
|
|
|
+ }
|
|
|
if (($type === 'recharge' && $agent->isJoint()) || $type === 'margin_credit') {
|
|
if (($type === 'recharge' && $agent->isJoint()) || $type === 'margin_credit') {
|
|
|
$agent->margin_amount = bcadd((string) $agent->margin_amount, $amount, 4);
|
|
$agent->margin_amount = bcadd((string) $agent->margin_amount, $amount, 4);
|
|
|
} elseif ($type === 'margin_refund') {
|
|
} elseif ($type === 'margin_refund') {
|
|
@@ -92,13 +108,96 @@ class AgentWalletService
|
|
|
if ($type === 'recharge' && $agent->isEquity()) {
|
|
if ($type === 'recharge' && $agent->isEquity()) {
|
|
|
$this->lockRecharge($agent, $log, $amount, $referenceType, $referenceId);
|
|
$this->lockRecharge($agent, $log, $amount, $referenceType, $referenceId);
|
|
|
} elseif ($type === 'equity_purchase') {
|
|
} elseif ($type === 'equity_purchase') {
|
|
|
- $this->consumeLocks((int) $agent->id, $amount);
|
|
|
|
|
|
|
+ $this->consumeLocks($agentId, bcsub($amount, $purchaseFromFrozen, 4));
|
|
|
|
|
+ } elseif ($agent->isEquity() && ($type === 'debit' || ($type === 'credit' && !$freezeCredit))) {
|
|
|
|
|
+ // 人工扣款先用可提现金额。加款前按旧余额清理历史超额锁定,避免锁住新到账的钱。
|
|
|
|
|
+ $this->capRechargeLocks($agent, $type === 'debit' ? $after : $before, now());
|
|
|
}
|
|
}
|
|
|
$this->evaluateActivation($agent);
|
|
$this->evaluateActivation($agent);
|
|
|
return $log;
|
|
return $log;
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private function manualCreditBalance(Agent $agent): string
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!$agent->isEquity()) return '0.0000';
|
|
|
|
|
+ return bcadd((string) AgentManualCreditLock::query()->pending()->where('agent_id', $agent->id)->sum('remaining'), '0', 4);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function purchaseBalance(Agent $agent): string
|
|
|
|
|
+ {
|
|
|
|
|
+ return bcadd((string) $agent->balance, $this->manualCreditBalance($agent), 4);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function withdrawalFrozenBalance(Agent $agent): string
|
|
|
|
|
+ {
|
|
|
|
|
+ return bcsub((string) $agent->frozen_balance, $this->manualCreditBalance($agent), 4);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 购股先扣人工加款冻结额,再使用余额中的充值锁定额和可提现金额。调用方已锁住代理。 */
|
|
|
|
|
+ private function consumeManualCredits(Agent $agent, string $amount): string
|
|
|
|
|
+ {
|
|
|
|
|
+ $left = $amount;
|
|
|
|
|
+ $locks = AgentManualCreditLock::query()->pending()->where('agent_id', $agent->id)
|
|
|
|
|
+ ->orderBy('unlock_at')->orderBy('id')->lockForUpdate()->get();
|
|
|
|
|
+ if ($locks->isEmpty()) return '0.0000';
|
|
|
|
|
+ if (bccomp($this->withdrawalFrozenBalance($agent), '0', 4) < 0) {
|
|
|
|
|
+ throw new \RuntimeException('代理冻结余额异常');
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach ($locks as $lock) {
|
|
|
|
|
+ if (bccomp($left, '0', 4) <= 0) break;
|
|
|
|
|
+ $take = bccomp($lock->remaining, $left, 4) < 0 ? $lock->remaining : $left;
|
|
|
|
|
+ $lock->remaining = bcsub($lock->remaining, $take, 4);
|
|
|
|
|
+ $lock->save();
|
|
|
|
|
+ $left = bcsub($left, $take, 4);
|
|
|
|
|
+ }
|
|
|
|
|
+ $spent = bcsub($amount, $left, 4);
|
|
|
|
|
+ $agent->frozen_balance = bcsub((string) $agent->frozen_balance, $spent, 4);
|
|
|
|
|
+ return $spent;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function releaseManualCredits(int $agentId, DateTimeInterface $at): int
|
|
|
|
|
+ {
|
|
|
|
|
+ return DB::transaction(function () use ($agentId, $at) {
|
|
|
|
|
+ $agent = Agent::query()->lockForUpdate()->findOrFail($agentId);
|
|
|
|
|
+ $due = AgentManualCreditLock::query()->pending()->where('agent_id', $agentId)
|
|
|
|
|
+ ->where('unlock_at', '<=', $at)->orderBy('unlock_at')->orderBy('id')->lockForUpdate()->get();
|
|
|
|
|
+ if ($due->isEmpty()) return 0;
|
|
|
|
|
+ if (bccomp($this->withdrawalFrozenBalance($agent), '0', 4) < 0) {
|
|
|
|
|
+ throw new \RuntimeException('代理冻结余额异常');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $this->capRechargeLocks($agent, (string) $agent->balance, $at);
|
|
|
|
|
+ foreach ($due as $lock) {
|
|
|
|
|
+ $amount = $lock->remaining;
|
|
|
|
|
+ $before = $agent->balance;
|
|
|
|
|
+ $agent->balance = bcadd($before, $amount, 4);
|
|
|
|
|
+ $agent->realtime_balance = bcadd((string) $agent->realtime_balance, $amount, 4);
|
|
|
|
|
+ $agent->frozen_balance = bcsub((string) $agent->frozen_balance, $amount, 4);
|
|
|
|
|
+ AgentBalanceLog::query()->create([
|
|
|
|
|
+ 'agent_id' => $agentId, 'type' => 'credit_unlock', 'amount' => $amount,
|
|
|
|
|
+ 'before_balance' => $before, 'after_balance' => $agent->balance,
|
|
|
|
|
+ 'operator_type' => 'system', 'idempotency_key' => 'agent-credit-unlock:' . $lock->id,
|
|
|
|
|
+ 'reference_type' => AgentManualCreditLock::REFERENCE_TYPE, 'reference_id' => $lock->id,
|
|
|
|
|
+ 'remark' => '人工加款冻结期满',
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $lock->remaining = '0.0000';
|
|
|
|
|
+ $lock->released_at = $at;
|
|
|
|
|
+ $lock->save();
|
|
|
|
|
+ }
|
|
|
|
|
+ $agent->save();
|
|
|
|
|
+ return $due->count();
|
|
|
|
|
+ }, 3);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function capRechargeLocks(Agent $agent, string $balance, DateTimeInterface $at): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $excess = bcsub($this->lockedAmount($agent, $at), $balance, 4);
|
|
|
|
|
+ if (bccomp($excess, '0', 4) > 0) {
|
|
|
|
|
+ $this->consumeLocks((int) $agent->id, $excess, $at);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/** 入股代理的充值锁定 N 天。调用方已在事务内锁住代理。 */
|
|
/** 入股代理的充值锁定 N 天。调用方已在事务内锁住代理。 */
|
|
|
private function lockRecharge(Agent $agent, AgentBalanceLog $log, string $amount, string $referenceType, ?int $referenceId): void
|
|
private function lockRecharge(Agent $agent, AgentBalanceLog $log, string $amount, string $referenceType, ?int $referenceId): void
|
|
|
{
|
|
{
|
|
@@ -111,11 +210,12 @@ class AgentWalletService
|
|
|
]);
|
|
]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 购买股份先用锁定中的充值(先到期的先用),用完再用可提现余额。 */
|
|
|
|
|
- private function consumeLocks(int $agentId, string $amount): void
|
|
|
|
|
|
|
+ /** 扣减仍在锁定期的充值,先到期的先扣。 */
|
|
|
|
|
+ private function consumeLocks(int $agentId, string $amount, ?DateTimeInterface $at = null): void
|
|
|
{
|
|
{
|
|
|
$left = bcadd($amount, '0', 4);
|
|
$left = bcadd($amount, '0', 4);
|
|
|
- $locks = AgentRechargeLock::query()->where('agent_id', $agentId)->where('unlock_at', '>', now())
|
|
|
|
|
|
|
+ if (bccomp($left, '0', 4) <= 0) return;
|
|
|
|
|
+ $locks = AgentRechargeLock::query()->where('agent_id', $agentId)->where('unlock_at', '>', $at ?? now())
|
|
|
->where('remaining', '>', 0)->orderBy('unlock_at')->orderBy('id')->lockForUpdate()->get();
|
|
->where('remaining', '>', 0)->orderBy('unlock_at')->orderBy('id')->lockForUpdate()->get();
|
|
|
foreach ($locks as $lock) {
|
|
foreach ($locks as $lock) {
|
|
|
if (bccomp($left, '0', 4) <= 0) break;
|
|
if (bccomp($left, '0', 4) <= 0) break;
|
|
@@ -127,10 +227,10 @@ class AgentWalletService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 入股代理仍在锁定期内的充值金额。 */
|
|
/** 入股代理仍在锁定期内的充值金额。 */
|
|
|
- public function lockedAmount(Agent $agent): string
|
|
|
|
|
|
|
+ public function lockedAmount(Agent $agent, ?DateTimeInterface $at = null): string
|
|
|
{
|
|
{
|
|
|
if (!$agent->isEquity()) return '0.0000';
|
|
if (!$agent->isEquity()) return '0.0000';
|
|
|
- $sum = AgentRechargeLock::query()->where('agent_id', $agent->id)->where('unlock_at', '>', now())->sum('remaining');
|
|
|
|
|
|
|
+ $sum = AgentRechargeLock::query()->where('agent_id', $agent->id)->where('unlock_at', '>', $at ?? now())->sum('remaining');
|
|
|
return bcadd((string) $sum, '0', 4);
|
|
return bcadd((string) $sum, '0', 4);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -238,7 +338,7 @@ class AgentWalletService
|
|
|
];
|
|
];
|
|
|
} else {
|
|
} else {
|
|
|
$data['equity_percent'] = bcadd((string) $agent->equity_percent, '0', 4);
|
|
$data['equity_percent'] = bcadd((string) $agent->equity_percent, '0', 4);
|
|
|
- // 锁定金额最多展示到当前余额(人工扣款等可能让余额低于锁定合计)
|
|
|
|
|
|
|
+ // 兼容历史记录,锁定金额最多展示到当前余额。
|
|
|
$locked = $this->lockedAmount($agent);
|
|
$locked = $this->lockedAmount($agent);
|
|
|
$shown = bccomp($locked, $balance, 4) > 0 ? $balance : $locked;
|
|
$shown = bccomp($locked, $balance, 4) > 0 ? $balance : $locked;
|
|
|
$data['locked_amount'] = bccomp($shown, '0', 4) > 0 ? $shown : '0.0000';
|
|
$data['locked_amount'] = bccomp($shown, '0', 4) > 0 ? $shown : '0.0000';
|