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); } }