Files
chatroom/app/Services/ShopService.php
lkddi 63679a622f 功能:随机浮漂钓鱼防挂机 + 商店自动钓鱼卡
核心变更:
1. FishingController 重写
   - cast(): 生成随机浮漂坐标(x/y%) + 一次性 token
   - reel(): 必须携带 token 才能收竿(防脚本绕过)
   - 检测自动钓鱼卡剩余时间并返回给前端

2. 前端钓鱼逻辑重写
   - 抛竿后显示随机位置 🪝 浮漂动画(全屏飘动)
   - 鱼上钩时浮漂「下沉」动画,8秒内点击浮漂才能收竿
   - 超时未点击:鱼跑了,token 也失效
   - 持有自动钓鱼卡:自动点击,紫色提示剩余时间

3. 商店新增「🎣 自动钓鱼卡」分组
   - 3档:2h(800金)/8h(2500金)/24h(6000金)
   - 图标徽章显示剩余有效时间(紫色)
   - 购买后即时激活,无需手动操作

4. 数据库
   - shop_items.type 加 auto_fishing 枚举
   - shop_items.duration_minutes 新字段(分钟精度)
   - Seeder 写入 3 张卡数据

防挂机原理:按钮 → 浮漂随机位置,脚本无法固定坐标点击
2026-03-01 16:19:45 +08:00

303 lines
10 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
/**
* 文件功能:商店业务逻辑服务层
* 处理商品购买、周卡激活/覆盖、改名卡使用等全部核心业务
*/
namespace App\Services;
use App\Models\ShopItem;
use App\Models\User;
use App\Models\UsernameBlacklist;
use App\Models\UserPurchase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class ShopService
{
/**
* 购买商品入口:扣金币、按类型分发处理
*
* @return array{ok:bool, message:string, play_effect?:string}
*/
public function buyItem(User $user, ShopItem $item): array
{
// 校验金币是否足够
if ($user->jjb < $item->price) {
return ['ok' => false, 'message' => "金币不足,购买 [{$item->name}] 需要 {$item->price} 金币,当前仅有 {$user->jjb} 金币。"];
}
return match ($item->type) {
'instant' => $this->buyInstant($user, $item),
'duration' => $this->buyWeekCard($user, $item),
'one_time' => $this->buyRenameCard($user, $item),
'ring' => $this->buyRing($user, $item),
'auto_fishing' => $this->buyAutoFishingCard($user, $item),
default => ['ok' => false, 'message' => '未知商品类型'],
};
}
/**
* 购买单次特效卡:立即扣金币,记录已用,返回需要播放的特效 key
*
* @return array{ok:bool, message:string, play_effect?:string}
*/
public function buyInstant(User $user, ShopItem $item): array
{
DB::transaction(function () use ($user, $item) {
// 扣除金币
$user->decrement('jjb', $item->price);
// 写入已使用记录(用于统计)
UserPurchase::create([
'user_id' => $user->id,
'shop_item_id' => $item->id,
'status' => 'used',
'price_paid' => $item->price,
'used_at' => Carbon::now(),
]);
});
return [
'ok' => true,
'message' => "购买成功!{$item->icon} {$item->name} 正在为您播放...",
'play_effect' => $item->effectKey(), // 返回特效 key 让前端立即播放
];
}
/**
* 购买周卡取消旧周卡金币不退激活新周卡有效期7天
*
* @return array{ok:bool, message:string}
*/
public function buyWeekCard(User $user, ShopItem $item): array
{
DB::transaction(function () use ($user, $item) {
// 将所有已激活的周卡标记为 cancelled金币不退
UserPurchase::where('user_id', $user->id)
->where('status', 'active')
->whereHas('shopItem', fn ($q) => $q->where('type', 'duration'))
->update(['status' => 'cancelled']);
// 扣除金币
$user->decrement('jjb', $item->price);
// 写入新的激活记录
UserPurchase::create([
'user_id' => $user->id,
'shop_item_id' => $item->id,
'status' => 'active',
'price_paid' => $item->price,
'expires_at' => Carbon::now()->addDays($item->duration_days ?? 7),
]);
});
return ['ok' => true, 'message' => "购买成功!{$item->icon} {$item->name} 已激活下次登录自动生效连续7天"];
}
/**
* 购买改名卡:扣金币、写 active 记录备用
*
* @return array{ok:bool, message:string}
*/
public function buyRenameCard(User $user, ShopItem $item): array
{
// 检查是否已有未使用的改名卡
$existing = UserPurchase::where('user_id', $user->id)
->where('status', 'active')
->whereHas('shopItem', fn ($q) => $q->where('slug', 'rename_card'))
->exists();
if ($existing) {
return ['ok' => false, 'message' => '您已有一张未使用的改名卡,使用后再购买。'];
}
DB::transaction(function () use ($user, $item) {
$user->decrement('jjb', $item->price);
UserPurchase::create([
'user_id' => $user->id,
'shop_item_id' => $item->id,
'status' => 'active',
'price_paid' => $item->price,
]);
});
return ['ok' => true, 'message' => '改名卡购买成功!请在商店中使用改名卡修改昵称。'];
}
/**
* 使用改名卡:校验新名、加黑名单、更新用户名
*
* @param string $newName 新昵称
* @return array{ok:bool, message:string}
*/
public function useRenameCard(User $user, string $newName): array
{
$newName = trim($newName);
// 格式校验1-10字符中英文数字下划线
if (! preg_match('/^[\x{4e00}-\x{9fa5}a-zA-Z0-9_]{1,10}$/u', $newName)) {
return ['ok' => false, 'message' => '新昵称格式不合法1-10字中英文/数字/下划线)。'];
}
// 不能与自己现有名相同
if ($newName === $user->username) {
return ['ok' => false, 'message' => '新昵称与当前昵称相同,请重新输入。'];
}
// 不能与其他用户重名
if (User::where('username', $newName)->exists()) {
return ['ok' => false, 'message' => '该昵称已被他人注册,请换一个。'];
}
// 不能在黑名单保留期内
if (UsernameBlacklist::isReserved($newName)) {
return ['ok' => false, 'message' => '该昵称处于保护期,暂时无法使用。'];
}
// 查找有效的改名卡记录
$purchase = UserPurchase::where('user_id', $user->id)
->where('status', 'active')
->whereHas('shopItem', fn ($q) => $q->where('slug', 'rename_card'))
->first();
if (! $purchase) {
return ['ok' => false, 'message' => '您没有可用的改名卡,请先购买。'];
}
DB::transaction(function () use ($user, $purchase, $newName) {
$oldName = $user->username;
// 修改用户名
$user->username = $newName;
$user->save();
// 旧名入黑名单保留30天
UsernameBlacklist::updateOrCreate(
['username' => $oldName],
['reserved_until' => Carbon::now()->addDays(30), 'created_at' => Carbon::now()]
);
// 标记改名卡为已使用
$purchase->update(['status' => 'used', 'used_at' => Carbon::now()]);
});
return ['ok' => true, 'message' => "改名成功!您的新昵称为【{$newName}旧昵称将保留30天黑名单。注意历史消息中的旧名不会同步修改。"];
}
/**
* 获取用户当前激活的周卡特效 key如 fireworks/rain/lightning/snow
* 过期的周卡会自动标记为 expired
*/
public function getActiveWeekEffect(User $user): ?string
{
$purchase = UserPurchase::with('shopItem')
->where('user_id', $user->id)
->where('status', 'active')
->whereHas('shopItem', fn ($q) => $q->where('type', 'duration'))
->first();
if (! $purchase) {
return null;
}
// 检查是否过期
if ($purchase->expires_at && $purchase->expires_at->isPast()) {
$purchase->update(['status' => 'expired']);
return null;
}
return $purchase->shopItem->effectKey();
}
/**
* 购买结婚戒指扣金币、写入背包active 状态,等待结婚时消耗)。
*
* @return array{ok:bool, message:string}
*/
public function buyRing(User $user, ShopItem $item): array
{
DB::transaction(function () use ($user, $item): void {
// 扣除金币
$user->decrement('jjb', $item->price);
// 写入背包active = 未使用,求婚时变为 used_pending→used 或 lost
UserPurchase::create([
'user_id' => $user->id,
'shop_item_id' => $item->id,
'status' => 'active',
'price_paid' => $item->price,
]);
});
return [
'ok' => true,
'message' => "💍 {$item->name} 购买成功!已放入背包,可用于求婚。",
];
}
/**
* 获取用户当前激活的改名卡(是否持有未用改名卡)
*/
public function hasRenameCard(User $user): bool
{
return UserPurchase::where('user_id', $user->id)
->where('status', 'active')
->whereHas('shopItem', fn ($q) => $q->where('slug', 'rename_card'))
->exists();
}
/**
* 购买自动钓鱼卡:手刺金币,写入 active 记录,到期时间 = 现在 + duration_minutes。
*
* @return array{ok:bool, message:string}
*/
public function buyAutoFishingCard(User $user, ShopItem $item): array
{
$minutes = (int) $item->duration_minutes;
if ($minutes <= 0) {
return ['ok' => false, 'message' => '该钓鱼卡配置异常,请联系管理员。'];
}
DB::transaction(function () use ($user, $item, $minutes): void {
// 手刺金币
$user->decrement('jjb', $item->price);
// 写入背包active刻起计时
UserPurchase::create([
'user_id' => $user->id,
'shop_item_id' => $item->id,
'status' => 'active',
'price_paid' => $item->price,
'expires_at' => Carbon::now()->addMinutes($minutes),
]);
});
$hours = round($minutes / 60, 1);
return [
'ok' => true,
'message' => "🎣 {$item->name}购买成功!{$hours}小时内鬼鱼自动收篼,尽情摆烂!",
];
}
/**
* 获取用户当前有效的自动钓鱼卡剩余分钟数(没有则返回 0
*/
public function getActiveAutoFishingMinutesLeft(User $user): int
{
$purchase = UserPurchase::where('user_id', $user->id)
->where('status', 'active')
->whereNotNull('expires_at')
->whereHas('shopItem', fn ($q) => $q->where('type', 'auto_fishing'))
->where('expires_at', '>', Carbon::now())
->orderByDesc('expires_at') // 取最晚过期的
->first();
if (! $purchase) {
return 0;
}
return (int) Carbon::now()->diffInMinutes($purchase->expires_at, false);
}
}