'exp_num', 'gold' => 'jjb', 'charm' => 'meili', ]; /** * 统一变更用户货币属性并写入流水记录。 * 使用数据库事务保证原子性:用户属性更新 + 流水写入同时成功或同时回滚。 * * @param User $user 目标用户 * @param string $currency 货币类型('exp' / 'gold' / 'charm') * @param int $amount 变更量,正数增加,负数扣除 * @param CurrencySource $source 来源活动枚举 * @param string $remark 备注说明 * @param int|null $roomId 所在房间 ID(可选) */ public function change( User $user, string $currency, int $amount, CurrencySource $source, string $remark = '', ?int $roomId = null, ): void { if ($amount === 0) { return; // 变更量为 0 不写记录 } $field = self::FIELD_MAP[$currency] ?? null; if (! $field) { return; // 未知货币类型,静默忽略(不抛异常,避免影响主流程) } DB::transaction(function () use ($user, $currency, $amount, $source, $remark, $roomId, $field) { // 原子性更新用户属性(用 increment/decrement 防并发竞态) if ($amount > 0) { $user->increment($field, $amount); } else { // 扣除时不让余额低于 0 $user->decrement($field, min(abs($amount), $user->$field ?? 0)); } // 重新读取最新余额(避免缓存脏数据) $balanceAfter = (int) $user->fresh()->$field; // 写入流水记录(快照当前用户名,排行 JOIN 时再取最新名) UserCurrencyLog::create([ 'user_id' => $user->id, 'username' => $user->username, 'currency' => $currency, 'amount' => $amount, 'balance_after' => $balanceAfter, 'source' => $source->value, 'remark' => $remark, 'room_id' => $roomId, ]); }); } /** * 批量变更多个用户的货币属性(适用于自动存点:一次操作多人)。 * 每位用户仍独立走事务,单人失败不影响其他人。 * * @param array $items [['user' => User, 'changes' => ['exp'=>1,'gold'=>2]], ...] */ public function batchChange(array $items, CurrencySource $source, ?int $roomId = null): void { foreach ($items as $item) { $user = $item['user']; $changes = $item['changes'] ?? []; foreach ($changes as $currency => $amount) { $this->change($user, $currency, (int) $amount, $source, '', $roomId); } } } /** * 查询某日各来源活动的产出统计(后台统计页面使用)。 * 返回格式:Collection of stdClass { source, currency, total_amount, participant_count } * * @param string|null $date 日期字符串如 '2026-02-28',默认今日 */ public function activityStats(?string $date = null): Collection { $date = $date ?? today()->toDateString(); return UserCurrencyLog::query() ->whereDate('created_at', $date) ->selectRaw('source, currency, SUM(amount) as total_amount, COUNT(DISTINCT user_id) as participant_count') ->groupBy('source', 'currency') ->orderBy('currency') ->orderByRaw('ABS(SUM(amount)) DESC') ->get(); } /** * 今日排行榜(按 user_id 聚合,展示最新用户名)。 * 只统计正向变更(amount > 0),不因消耗而扣分。 * * @param string $currency 'exp' | 'gold' | 'charm' * @param int $limit 返回条数 * @param string|null $date 日期,默认今日 */ public function todayLeaderboard(string $currency, int $limit = 20, ?string $date = null): Collection { $date = $date ?? today()->toDateString(); return UserCurrencyLog::query() ->whereDate('created_at', $date) ->where('currency', $currency) // 计算净收益,包含正向与负向消耗 ->selectRaw('user_id, SUM(amount) as total') ->groupBy('user_id') ->havingRaw('SUM(amount) > 0') // 只有今日净收益为正数才能上榜 ->orderByRaw('SUM(amount) DESC') ->limit($limit) ->get() ->map(function ($row) { // JOIN 取最新用户名(避免改名后显示旧名) $user = User::select('id', 'username', 'user_level', 'sex', 'headface') ->find($row->user_id); return (object) [ 'user_id' => $row->user_id, 'username' => $user?->username ?? '未知用户', 'level' => $user?->user_level ?? 0, 'sex' => $user?->sex ?? 1, 'headface' => $user?->headface ?? '1.gif', 'total' => $row->total, ]; }); } /** * 用户个人流水明细(用户查询自己的日志)。 * * @param int $userId 用户 ID * @param string|null $currency 为 null 时返回所有货币类型 * @param int $days 查询最近多少天 */ public function userLogs(int $userId, ?string $currency = null, int $days = 7): Collection { return UserCurrencyLog::query() ->where('user_id', $userId) ->when($currency, fn ($q) => $q->where('currency', $currency)) ->where('created_at', '>=', now()->subDays($days)) ->orderByDesc('created_at') ->limit(200) ->get(); } /** * 货币类型中文名映射(用于视图展示)。 */ public static function currencyLabel(string $currency): string { return match ($currency) { 'exp' => '经验', 'gold' => '金币', 'charm' => '魅力', default => $currency, }; } }