| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- <?php
- namespace App\Services;
- use App\Models\BalanceLog;
- use App\Models\ShareCommissionRecord;
- use App\Models\User;
- use Brick\Math\BigDecimal;
- use Brick\Math\RoundingMode;
- use Carbon\Carbon;
- use Illuminate\Database\Eloquent\Builder;
- use Illuminate\Support\Facades\DB;
- /** Registers rebates that have already been paid; never credits a wallet. */
- class ShareCommissionRecorder
- {
- public const SOURCE_TYPE = 'recharge_rebate_log';
- public function preview(BalanceLog $rebate): array
- {
- $result = $this->prepare($rebate);
- if ($result['status'] === 'skipped') return $result;
- $result['status'] = $this->recordQuery($result['attributes'])->exists() ? 'existing' : 'pending';
- return $result;
- }
- /** Context supplies the original base and percentage during a new rebate payout. */
- public function record(BalanceLog $rebate, array $context = []): array
- {
- return DB::transaction(function () use ($rebate, $context) {
- $locked = BalanceLog::where('id', $rebate->id)->lockForUpdate()->first();
- if (!$locked) return ['status' => 'skipped', 'reason' => 'missing_log'];
- $result = $this->prepare($locked, $context);
- if ($result['status'] === 'skipped') return $result;
- // Current read also protects callers whose transaction snapshot predates a backfill.
- if ($this->recordQuery($result['attributes'])->lockForUpdate()->first()) {
- $result['status'] = 'existing';
- return $result;
- }
- ShareCommissionRecord::create($result['attributes']);
- $result['status'] = 'created';
- return $result;
- });
- }
- private function recordQuery(array $attributes): Builder
- {
- return ShareCommissionRecord::where('source_type', self::SOURCE_TYPE)
- ->where('source_id', $attributes['source_id'])
- ->where('type', 'commission');
- }
- private function prepare(BalanceLog $rebate, array $context = []): array
- {
- if (!$rebate->exists || $rebate->change_type !== '充值返现' || (int)$rebate->type !== 1) {
- return ['status' => 'skipped', 'reason' => 'not_recharge_rebate'];
- }
- $amount = $this->decimal($rebate->amount);
- $before = $this->decimal($rebate->before_balance, 10);
- $after = $this->decimal($rebate->after_balance, 10);
- if ($amount === null || bccomp($amount, '0', 4) <= 0) {
- return ['status' => 'skipped', 'reason' => 'invalid_amount'];
- }
- if ($before === null || $after === null || $this->decimal(bcsub($after, $before, 10)) !== $amount) {
- return ['status' => 'skipped', 'reason' => 'unbalanced_log'];
- }
- $promoters = User::where('member_id', $rebate->member_id)->limit(2)->get(['id']);
- if ($promoters->count() !== 1) {
- return ['status' => 'skipped', 'reason' => $promoters->isEmpty() ? 'missing_promoter' : 'ambiguous_promoter'];
- }
- $rawDate = $rebate->getRawOriginal('created_at');
- if (!$rawDate) return ['status' => 'skipped', 'reason' => 'missing_date'];
- try {
- $zone = config('app.timezone', 'Asia/Shanghai');
- $date = Carbon::parse($rawDate, $zone)->setTimezone($zone)->toDateString();
- } catch (\Throwable $e) {
- return ['status' => 'skipped', 'reason' => 'invalid_date'];
- }
- $inviteeId = null;
- $base = '0.0000';
- $rate = '0.0000';
- if ($context) {
- $base = $this->decimal($context['base_amount'] ?? null);
- $rate = $this->decimal($context['rate'] ?? null);
- $inviteeId = $context['invitee_user_id'] ?? null;
- if ($base === null || bccomp($base, '0', 4) <= 0 || $rate === null
- || bccomp($rate, '0', 4) < 0 || bccomp($rate, '10000', 4) >= 0 || !$inviteeId) {
- return ['status' => 'skipped', 'reason' => 'invalid_context'];
- }
- } elseif (preg_match('/^被邀请人:([0-9]{1,64})充值金额为:(\d{1,16}(?:\.\d{1,10})?)$/u', (string)$rebate->remark, $match)) {
- // Use the historical receipt, not today's rate or the invitee's current referral link.
- $base = $this->decimal($match[2]) ?? '0.0000';
- $invitees = User::where('member_id', $match[1])->limit(2)->get(['id']);
- if ($invitees->count() === 1) $inviteeId = $invitees->first()->id;
- if (bccomp($base, '0', 4) > 0) {
- $effectiveRate = $this->decimal(bcdiv(bcmul($amount, '100', 10), $base, 10));
- if ($effectiveRate !== null && bccomp($effectiveRate, '10000', 4) < 0) $rate = $effectiveRate;
- }
- }
- return [
- 'status' => 'pending',
- 'incomplete_metadata' => $inviteeId === null || bccomp($base, '0', 4) <= 0,
- 'attributes' => [
- 'stat_date' => $date,
- 'promoter_user_id' => $promoters->first()->id,
- 'invitee_user_id' => $inviteeId,
- 'type' => 'commission',
- 'category' => 'recharge',
- 'base_amount' => $base,
- 'rate' => $rate,
- 'amount' => $amount,
- 'source_type' => self::SOURCE_TYPE,
- 'source_id' => (string)$rebate->id,
- 'status' => 1,
- ],
- ];
- }
- private function decimal($value, int $scale = 4): ?string
- {
- if (is_float($value) && is_finite($value)) $value = sprintf('%.10F', $value);
- if (!is_scalar($value) || !preg_match('/^-?\d{1,16}(?:\.\d+)?$/', (string)$value)) return null;
- return (string)BigDecimal::of((string)$value)->toScale($scale, RoundingMode::HALF_UP);
- }
- }
|