Files
chatroom/app/Services/UserCurrencyService.php
lkddi 0c5e218aa8 功能:新增用户积分流水系统
- 新建 user_currency_logs 流水表 (Migration)
- App\Enums\CurrencySource 来源枚举(可扩展)
- App\Models\UserCurrencyLog 流水模型
- App\Services\UserCurrencyService 统一积分变更服务
- FishingController:抛竿/收竿接入流水记录
- AutoSaveExp:自动存点接入流水记录
- Admin/UserManagerController:管理员调整接入流水记录
- LeaderboardController:新增今日三榜(经验/金币/魅力)+ 个人流水日志页
- Admin/CurrencyStatsController:后台活动统计页
- views:新增个人日志页、后台统计页;排行榜新增今日榜数据传递
- routes:新增个人日志路由 /my/currency-logs、后台路由 /admin/currency-stats
2026-02-28 12:49:26 +08:00

193 lines
7.0 KiB
PHP
Raw 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]], ...]
* @param CurrencySource $source
* @param int|null $roomId
*/
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)
->where('amount', '>', 0) // 只统计正向
->selectRaw('user_id, SUM(amount) as total')
->groupBy('user_id')
->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,
};
}
}