Files
chatroom/app/Services/UserCurrencyService.php

193 lines
6.8 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文件功能:用户积分统一变更服务
* 所有修改 exp_num经验、jjb金币、meili魅力 的操作必须经由此服务,
* 禁止在 Controller 中直接操作 User 属性并 save()。
* 本服务负责:原子性更新用户属性、写入流水记录、提供统计与排行数据。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Services;
use App\Enums\CurrencySource;
use App\Models\User;
use App\Models\UserCurrencyLog;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class UserCurrencyService
{
/**
* currency 标识与 users 表字段名的映射关系。
* 以后新增货币类型,在此加一行即可。
*/
private const FIELD_MAP = [
'exp' => '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,
};
}
}