[['value' => self::AUDIT_TYPE, 'label' => '代理佣金']]]; } public function list(array $filters): array { $params = Validator::make($filters, [ 'page' => ['nullable', 'integer', 'min:1'], 'limit' => ['nullable', 'integer', 'min:1', 'max:200'], 'status' => ['nullable', Rule::in(['pending', 'approved', 'rejected'])], // This endpoint currently reads only agent-commission claims. 'audit_type' => ['nullable', 'string', Rule::in([self::AUDIT_TYPE])], 'account' => ['nullable', 'string', 'max:100'], 'user_id' => ['nullable', 'integer', 'min:1'], 'start_date' => ['nullable', 'date_format:Y-m-d'], 'end_date' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start_date'], ])->validate(); $query = DB::table('share_claims as c')->leftJoin('users as u', 'u.id', '=', 'c.user_id'); if (!empty($params['status'])) $query->where('c.status', $params['status']); if (!empty($params['account'])) $query->where('u.account', 'like', '%' . $params['account'] . '%'); if (!empty($params['user_id'])) $query->where('c.user_id', $params['user_id']); if (!empty($params['start_date'])) $query->where('c.created_at', '>=', $params['start_date'] . ' 00:00:00'); if (!empty($params['end_date'])) $query->where('c.created_at', '<=', $params['end_date'] . ' 23:59:59'); $total = $query->count(); $rows = $query->select(['c.*', 'u.account', 'u.member_id', 'u.first_name as nickname'])->orderByDesc('c.id') ->forPage((int)($params['page'] ?? 1), (int)($params['limit'] ?? 20))->get(); return ['total' => $total, 'page' => (int)($params['page'] ?? 1), 'limit' => (int)($params['limit'] ?? 20), 'data' => $rows->map(fn ($row) => $this->claim($row))->all()]; } public function detail(int $id): array { $row = DB::table('share_claims as c')->leftJoin('users as u', 'u.id', '=', 'c.user_id') ->where('c.id', $id)->select(['c.*', 'u.account', 'u.member_id', 'u.first_name as nickname'])->first(); if (!$row) $this->invalid('id', '领取申请不存在'); $account = DB::table('share_accounts')->where('user_id', $row->user_id)->first(); $earned = (string)($account->earned_amount ?? '0'); $paid = (string)($account->paid_amount ?? '0'); $reserved = (string)($account->reserved_amount ?? '0'); $balance = bcsub($earned, $paid, 10); $spendable = bcsub($balance, $reserved, 10); return $this->claim($row) + ['commission_account' => [ 'earned_amount' => $this->money($earned), 'paid_amount' => $this->money($paid), 'reserved_amount' => $this->money($reserved), 'pending_amount' => $this->money($this->positive($balance)), 'spendable_amount' => $this->money($this->positive($spendable)), 'debt_amount' => $this->money($this->positive(bcsub($paid, $earned, 10))), ], 'can_approve' => $row->status === 'pending' && bccomp($balance, (string)$row->amount, 10) >= 0]; } public function audit(int $id, string $action, int $adminId, string $remark = ''): array { Validator::make(compact('id', 'action', 'remark'), ['id' => ['required', 'integer', 'min:1'], 'action' => ['required', Rule::in(['approve', 'reject'])], 'remark' => ['string', 'max:2000']])->validate(); if ($adminId <= 0) $this->invalid('admin', '请使用后台管理员身份审核'); $userId = DB::table('share_claims')->where('id', $id)->value('user_id'); if (!$userId) $this->invalid('id', '领取申请不存在'); return DB::transaction(function () use ($id, $action, $adminId, $remark, $userId) { // Every claim mutation uses the same lock order as member requests and settlement corrections. $account = DB::table('share_accounts')->where('user_id', $userId)->lockForUpdate()->first(); if (!$account) $this->invalid('id', '会员佣金账户不存在'); $claim = DB::table('share_claims')->where('id', $id)->lockForUpdate()->first(); if (!$claim || (int)$claim->user_id !== (int)$userId) $this->invalid('id', '领取申请不存在'); $targetStatus = $action === 'approve' ? 'approved' : 'rejected'; if ($claim->status === $targetStatus) return $this->detail($id); if ($claim->status !== 'pending') $this->invalid('id', '领取申请已审核,不可更改结果'); $amount = (string)$claim->amount; if (bccomp($amount, '0.01', 10) < 0 || bccomp((string)$account->reserved_amount, $amount, 10) < 0) { $this->invalid('id', '申请金额或预留佣金异常,请核对'); } $now = Carbon::now(ShareDefaults::TIMEZONE)->format('Y-m-d H:i:s'); $changes = ['reserved_amount' => bcsub((string)$account->reserved_amount, $amount, 10), 'updated_at' => $now]; $claimChanges = ['status' => $targetStatus, 'active_user_id' => null, 'audit_admin_id' => $adminId, 'audit_remark' => $remark, 'audited_at' => $now, 'updated_at' => $now]; if ($action === 'approve') { if (bccomp(bcsub((string)$account->earned_amount, (string)$account->paid_amount, 10), $amount, 10) < 0) { $this->invalid('amount', '佣金修正后余额不足,请驳回原申请后由会员重新申请'); } $user = User::query()->find($userId); if (!$user) $this->invalid('user_id', '会员不存在'); $wallet = Wallet::query()->where('member_id', $user->member_id)->orderBy('id')->lockForUpdate()->first(); if (!$wallet) $this->invalid('user_id', '会员钱包不存在'); $before = (string)$wallet->available_balance; $after = bcadd($before, $amount, 10); $wallet->available_balance = $after; $wallet->save(); $log = BalanceLogService::addLog($user->member_id, $amount, $before, $after, '分享佣金领取', $id, $remark); $claimChanges['credited_log_id'] = (int)$log->id; $changes['paid_amount'] = bcadd((string)$account->paid_amount, $amount, 10); } DB::table('share_accounts')->where('user_id', $userId)->update($changes); DB::table('share_claims')->where('id', $id)->update($claimChanges); OperationAuditService::record('share_claim', $id, $action, (array)$claim, (array)DB::table('share_claims')->where('id', $id)->first()); return $this->detail($id); }); } private function claim(object $row): array { return ['id' => (int)$row->id, 'audit_type' => self::AUDIT_TYPE, 'activity_name' => '代理佣金', 'user_id' => (int)$row->user_id, 'member_id' => (string)($row->member_id ?? ''), 'account' => (string)($row->account ?? ''), 'nickname' => (string)($row->nickname ?? ''), 'request_id' => (string)$row->request_id, 'amount' => $this->money($row->amount), 'status' => $row->status, 'audit_admin_id' => $row->audit_admin_id === null ? null : (int)$row->audit_admin_id, 'audit_remark' => (string)($row->audit_remark ?? ''), 'audited_at' => $row->audited_at, 'credited_log_id' => $row->credited_log_id === null ? null : (int)$row->credited_log_id, 'created_at' => $row->created_at, 'updated_at' => $row->updated_at]; } private function positive(string $value): string { return bccomp($value, '0', 10) > 0 ? $value : '0'; } private function money($value): string { return bcadd((string)$value, '0', 2); } private function invalid(string $key, string $message): void { throw ValidationException::withMessages([$key => $message]); } }