新增每日签到与补签卡功能
This commit is contained in:
@@ -51,6 +51,16 @@ class ChatUserPresenceService
|
||||
$payload['daily_status_expires_at'] = $activeStatus['expires_at'];
|
||||
}
|
||||
|
||||
$signIdentity = $user->currentSignInIdentity();
|
||||
if ($signIdentity !== null) {
|
||||
$payload['sign_identity_key'] = $signIdentity->badge_code;
|
||||
$payload['sign_identity_label'] = $signIdentity->badge_name;
|
||||
$payload['sign_identity_icon'] = $signIdentity->badge_icon ?? '✅';
|
||||
$payload['sign_identity_color'] = $signIdentity->badge_color ?? '#0f766e';
|
||||
$payload['sign_identity_expires_at'] = $signIdentity->expires_at?->toIso8601String();
|
||||
$payload['sign_identity_streak_days'] = (int) data_get($signIdentity->metadata, 'streak_days', 0);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,13 +19,19 @@ class ShopService
|
||||
/**
|
||||
* 购买商品入口:扣金币、按类型分发处理
|
||||
*
|
||||
* @return array{ok:bool, message:string, play_effect?:string}
|
||||
* @return array{ok:bool, message:string, play_effect?:string, quantity?:int, total_price?:int}
|
||||
*/
|
||||
public function buyItem(User $user, ShopItem $item): array
|
||||
public function buyItem(User $user, ShopItem $item, int $quantity = 1): array
|
||||
{
|
||||
if ($quantity !== 1 && $item->type !== ShopItem::TYPE_SIGN_REPAIR) {
|
||||
return ['ok' => false, 'message' => '该商品暂不支持批量购买。'];
|
||||
}
|
||||
|
||||
$totalPrice = $item->price * $quantity;
|
||||
|
||||
// 校验金币是否足够
|
||||
if ($user->jjb < $item->price) {
|
||||
return ['ok' => false, 'message' => "金币不足,购买 [{$item->name}] 需要 {$item->price} 金币,当前仅有 {$user->jjb} 金币。"];
|
||||
if ($user->jjb < $totalPrice) {
|
||||
return ['ok' => false, 'message' => "金币不足,购买 [{$item->name}] 需要 {$totalPrice} 金币,当前仅有 {$user->jjb} 金币。"];
|
||||
}
|
||||
|
||||
return match ($item->type) {
|
||||
@@ -34,6 +40,7 @@ class ShopService
|
||||
'one_time' => $this->buyRenameCard($user, $item),
|
||||
'ring' => $this->buyRing($user, $item),
|
||||
'auto_fishing' => $this->buyAutoFishingCard($user, $item),
|
||||
ShopItem::TYPE_SIGN_REPAIR => $this->buySignRepairCard($user, $item, $quantity),
|
||||
default => ['ok' => false, 'message' => '未知商品类型'],
|
||||
};
|
||||
}
|
||||
@@ -243,6 +250,39 @@ class ShopService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买签到补签卡:扣金币并写入可消耗背包记录。
|
||||
*
|
||||
* @return array{ok:bool, message:string, quantity:int, total_price:int}
|
||||
*/
|
||||
public function buySignRepairCard(User $user, ShopItem $item, int $quantity = 1): array
|
||||
{
|
||||
$quantity = max(1, min(99, $quantity));
|
||||
$totalPrice = $item->price * $quantity;
|
||||
|
||||
DB::transaction(function () use ($user, $item, $quantity, $totalPrice): void {
|
||||
// 补签卡支持一次购买多张,扣款必须按总价执行。
|
||||
$user->decrement('jjb', $totalPrice);
|
||||
|
||||
for ($i = 0; $i < $quantity; $i++) {
|
||||
// 每张补签卡保留独立购买记录,补签时逐张消耗。
|
||||
UserPurchase::create([
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $item->id,
|
||||
'status' => 'active',
|
||||
'price_paid' => $item->price,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => "🗓️ {$item->name} × {$quantity} 购买成功!可在签到日历中补签漏掉的日期。",
|
||||
'quantity' => $quantity,
|
||||
'total_price' => $totalPrice,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户当前激活的改名卡(是否持有未用改名卡)
|
||||
*/
|
||||
@@ -254,6 +294,18 @@ class ShopService
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户可用补签卡数量。
|
||||
*/
|
||||
public function getSignRepairCardCount(User $user): int
|
||||
{
|
||||
return UserPurchase::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('status', 'active')
|
||||
->whereHas('shopItem', fn ($q) => $q->where('type', ShopItem::TYPE_SIGN_REPAIR))
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买自动钓鱼卡:手刺金币,写入 active 记录,到期时间 = 现在 + duration_minutes。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:每日签到服务。
|
||||
*
|
||||
* 负责查询签到状态、领取每日签到奖励、防止重复领取、刷新签到身份徽章。
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Models\DailySignIn;
|
||||
use App\Models\ShopItem;
|
||||
use App\Models\SignInRewardRule;
|
||||
use App\Models\User;
|
||||
use App\Models\UserIdentityBadge;
|
||||
use App\Models\UserPurchase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* 类功能:封装每日签到状态查询与领取奖励的核心业务流程。
|
||||
*/
|
||||
class SignInService
|
||||
{
|
||||
/**
|
||||
* 构造每日签到服务。
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly UserCurrencyService $currencyService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 查询用户当前签到状态与今日可领取预览。
|
||||
*
|
||||
* @return array{
|
||||
* signed_today: bool,
|
||||
* can_claim: bool,
|
||||
* current_streak_days: int,
|
||||
* claimable_streak_days: int,
|
||||
* today_sign_in: DailySignIn|null,
|
||||
* latest_sign_in: DailySignIn|null,
|
||||
* matched_rule: SignInRewardRule|null,
|
||||
* current_identity: UserIdentityBadge|null
|
||||
* }
|
||||
*/
|
||||
public function status(User $user): array
|
||||
{
|
||||
$today = today();
|
||||
|
||||
$todaySignIn = DailySignIn::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereDate('sign_in_date', $today)
|
||||
->first();
|
||||
|
||||
$latestSignIn = DailySignIn::query()
|
||||
->where('user_id', $user->id)
|
||||
->latest('sign_in_date')
|
||||
->first();
|
||||
|
||||
$signedToday = $todaySignIn !== null;
|
||||
$claimableStreakDays = $signedToday
|
||||
? (int) $todaySignIn->streak_days
|
||||
: $this->calculateNextStreakDays($latestSignIn, $today);
|
||||
|
||||
return [
|
||||
'signed_today' => $signedToday,
|
||||
'can_claim' => ! $signedToday,
|
||||
'current_streak_days' => (int) ($latestSignIn?->streak_days ?? 0),
|
||||
'claimable_streak_days' => $claimableStreakDays,
|
||||
'today_sign_in' => $todaySignIn,
|
||||
'latest_sign_in' => $latestSignIn,
|
||||
'matched_rule' => $this->matchRewardRule($claimableStreakDays),
|
||||
'current_identity' => $user->currentSignInIdentity(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定月份的签到日历状态。
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function calendar(User $user, ?string $month = null): array
|
||||
{
|
||||
$monthStart = $month
|
||||
? Carbon::createFromFormat('Y-m', $month)->startOfMonth()
|
||||
: today()->startOfMonth();
|
||||
$monthEnd = $monthStart->copy()->endOfMonth();
|
||||
$today = today();
|
||||
// 补签卡只允许修补当前自然月,查看历史月份时不再开放补签入口。
|
||||
$isCurrentMonth = $monthStart->isSameMonth($today);
|
||||
|
||||
$signIns = DailySignIn::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereBetween('sign_in_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
||||
->get()
|
||||
->keyBy(fn (DailySignIn $signIn): string => $signIn->sign_in_date->toDateString());
|
||||
|
||||
$days = [];
|
||||
for ($date = $monthStart->copy(); $date->lte($monthEnd); $date->addDay()) {
|
||||
$dateKey = $date->toDateString();
|
||||
$signIn = $signIns->get($dateKey);
|
||||
$isFuture = $date->gt($today);
|
||||
|
||||
$days[] = [
|
||||
'date' => $dateKey,
|
||||
'day' => (int) $date->day,
|
||||
'weekday' => (int) $date->dayOfWeek,
|
||||
'signed' => $signIn !== null,
|
||||
'is_today' => $date->isSameDay($today),
|
||||
'is_future' => $isFuture,
|
||||
'can_makeup' => $isCurrentMonth && $signIn === null && $date->lt($today),
|
||||
'is_makeup' => (bool) ($signIn?->is_makeup ?? false),
|
||||
'streak_days' => (int) ($signIn?->streak_days ?? 0),
|
||||
'reward_text' => $signIn ? $this->buildRewardText($signIn) : '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'month' => $monthStart->format('Y-m'),
|
||||
'month_label' => $monthStart->format('Y年n月'),
|
||||
'prev_month' => $monthStart->copy()->subMonth()->format('Y-m'),
|
||||
'next_month' => $monthStart->copy()->addMonth()->format('Y-m'),
|
||||
'days' => $days,
|
||||
'reward_rules' => $this->rewardRulePreview(),
|
||||
'makeup_card_count' => $this->availableSignRepairCardCount($user),
|
||||
'sign_repair_card_item' => $this->activeSignRepairCard()?->only(['id', 'name', 'slug', 'icon', 'price', 'description']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取每日签到奖励。
|
||||
*/
|
||||
public function claim(User $user, ?int $roomId = null): DailySignIn
|
||||
{
|
||||
return DB::transaction(function () use ($user, $roomId): DailySignIn {
|
||||
$today = today();
|
||||
|
||||
// 锁定用户行,将同一用户的并发签到串行化,避免“查无今日记录”时并发重复发奖。
|
||||
$lockedUser = User::query()
|
||||
->whereKey($user->id)
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
|
||||
$existingSignIn = DailySignIn::query()
|
||||
->where('user_id', $lockedUser->id)
|
||||
->whereDate('sign_in_date', $today)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($existingSignIn !== null) {
|
||||
return $existingSignIn->load('rewardRule');
|
||||
}
|
||||
|
||||
$latestSignIn = DailySignIn::query()
|
||||
->where('user_id', $lockedUser->id)
|
||||
->whereDate('sign_in_date', '<', $today)
|
||||
->latest('sign_in_date')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
$streakDays = $this->calculateNextStreakDays($latestSignIn, $today);
|
||||
$rewardRule = $this->matchRewardRule($streakDays);
|
||||
|
||||
$dailySignIn = DailySignIn::query()->create([
|
||||
'user_id' => $lockedUser->id,
|
||||
'room_id' => $roomId,
|
||||
'sign_in_date' => $today->toDateString(),
|
||||
'streak_days' => $streakDays,
|
||||
'reward_rule_id' => $rewardRule?->id,
|
||||
'gold_reward' => (int) ($rewardRule?->gold_reward ?? 0),
|
||||
'exp_reward' => (int) ($rewardRule?->exp_reward ?? 0),
|
||||
'charm_reward' => (int) ($rewardRule?->charm_reward ?? 0),
|
||||
'identity_badge_code' => $rewardRule?->identity_badge_code,
|
||||
'identity_badge_name' => $rewardRule?->identity_badge_name,
|
||||
'identity_badge_icon' => $rewardRule?->identity_badge_icon,
|
||||
'identity_badge_color' => $rewardRule?->identity_badge_color,
|
||||
]);
|
||||
|
||||
$this->grantRewards($lockedUser, $dailySignIn, $roomId);
|
||||
$this->refreshSignInIdentityBadge($lockedUser, $rewardRule, $streakDays);
|
||||
|
||||
return $dailySignIn->load('rewardRule');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用补签卡补签指定历史日期。
|
||||
*/
|
||||
public function makeup(User $user, string $targetDate, ?int $roomId = null): DailySignIn
|
||||
{
|
||||
return DB::transaction(function () use ($user, $targetDate, $roomId): DailySignIn {
|
||||
$date = Carbon::parse($targetDate)->startOfDay();
|
||||
|
||||
if ($date->greaterThanOrEqualTo(today())) {
|
||||
throw ValidationException::withMessages(['target_date' => '只能补签今天之前的漏签日期。']);
|
||||
}
|
||||
|
||||
// 后端再次限制补签月份,避免绕过前端日历直接提交历史月份。
|
||||
if (! $date->isSameMonth(today())) {
|
||||
throw ValidationException::withMessages(['target_date' => '补签卡只能补签本月的未签到日期。']);
|
||||
}
|
||||
|
||||
// 锁定用户行,保证补签卡消耗和签到记录创建不会并发重复执行。
|
||||
$lockedUser = User::query()
|
||||
->whereKey($user->id)
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
|
||||
$existingSignIn = DailySignIn::query()
|
||||
->where('user_id', $lockedUser->id)
|
||||
->whereDate('sign_in_date', $date)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($existingSignIn !== null) {
|
||||
throw ValidationException::withMessages(['target_date' => '这一天已经签到过,不能重复补签。']);
|
||||
}
|
||||
|
||||
$repairCard = $this->nextAvailableSignRepairCard($lockedUser);
|
||||
if ($repairCard === null) {
|
||||
throw ValidationException::withMessages(['target_date' => '没有可用补签卡,请先购买补签卡。']);
|
||||
}
|
||||
|
||||
$latestBeforeTarget = DailySignIn::query()
|
||||
->where('user_id', $lockedUser->id)
|
||||
->whereDate('sign_in_date', '<', $date)
|
||||
->latest('sign_in_date')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
$streakDays = $this->calculateNextStreakDays($latestBeforeTarget, $date);
|
||||
|
||||
$dailySignIn = DailySignIn::query()->create([
|
||||
'user_id' => $lockedUser->id,
|
||||
'room_id' => $roomId,
|
||||
'is_makeup' => true,
|
||||
'makeup_purchase_id' => $repairCard->id,
|
||||
'makeup_at' => now(),
|
||||
'sign_in_date' => $date->toDateString(),
|
||||
'streak_days' => $streakDays,
|
||||
'reward_rule_id' => null,
|
||||
'gold_reward' => 0,
|
||||
'exp_reward' => 0,
|
||||
'charm_reward' => 0,
|
||||
'identity_badge_code' => null,
|
||||
'identity_badge_name' => null,
|
||||
'identity_badge_icon' => null,
|
||||
'identity_badge_color' => null,
|
||||
]);
|
||||
|
||||
// 补签卡与补签记录必须在同一事务里完成状态流转。
|
||||
$repairCard->update(['status' => 'used', 'used_at' => now()]);
|
||||
$this->recalculateFutureStreaks($lockedUser, $date);
|
||||
$currentStreakDays = $this->latestStreakDays($lockedUser);
|
||||
$rewardRule = $this->matchRewardRule($currentStreakDays);
|
||||
$dailySignIn->update([
|
||||
'reward_rule_id' => $rewardRule?->id,
|
||||
'gold_reward' => (int) ($rewardRule?->gold_reward ?? 0),
|
||||
'exp_reward' => (int) ($rewardRule?->exp_reward ?? 0),
|
||||
'charm_reward' => (int) ($rewardRule?->charm_reward ?? 0),
|
||||
'identity_badge_code' => $rewardRule?->identity_badge_code,
|
||||
'identity_badge_name' => $rewardRule?->identity_badge_name,
|
||||
'identity_badge_icon' => $rewardRule?->identity_badge_icon,
|
||||
'identity_badge_color' => $rewardRule?->identity_badge_color,
|
||||
]);
|
||||
$this->grantRewards(
|
||||
$lockedUser,
|
||||
$dailySignIn->fresh(),
|
||||
$roomId,
|
||||
"补签 {$date->toDateString()}:当前连续签到 {$currentStreakDays} 天"
|
||||
);
|
||||
$this->refreshIdentityFromLatestSignIn($lockedUser);
|
||||
|
||||
return $dailySignIn->fresh('rewardRule');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算下一次签到应获得的连续天数。
|
||||
*/
|
||||
private function calculateNextStreakDays(?DailySignIn $latestSignIn, ?Carbon $targetDate = null): int
|
||||
{
|
||||
$targetDate ??= today();
|
||||
|
||||
if ($latestSignIn === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$yesterday = $targetDate->copy()->subDay()->toDateString();
|
||||
|
||||
if ($latestSignIn->sign_in_date?->toDateString() === $yesterday) {
|
||||
// 签到满一年后开启新周期,下一天重新从第 1 天计算。
|
||||
if ((int) $latestSignIn->streak_days >= $this->daysInSignInYear($latestSignIn)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (int) $latestSignIn->streak_days + 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按签到记录所在年份计算当年天数,闰年为 366 天。
|
||||
*/
|
||||
private function daysInSignInYear(DailySignIn $dailySignIn): int
|
||||
{
|
||||
return $dailySignIn->sign_in_date?->isLeapYear() ? 366 : 365;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配当前连续天数可用的最高奖励规则。
|
||||
*/
|
||||
private function matchRewardRule(int $streakDays): ?SignInRewardRule
|
||||
{
|
||||
return SignInRewardRule::query()
|
||||
->where('is_enabled', true)
|
||||
->where('streak_days', '<=', $streakDays)
|
||||
->orderByDesc('streak_days')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按签到记录快照发放金币、经验和魅力。
|
||||
*/
|
||||
private function grantRewards(User $user, DailySignIn $dailySignIn, ?int $roomId, ?string $remark = null): void
|
||||
{
|
||||
$remark ??= "每日签到:连续 {$dailySignIn->streak_days} 天";
|
||||
|
||||
$this->currencyService->change($user, 'gold', $dailySignIn->gold_reward, CurrencySource::SIGN_IN, $remark, $roomId);
|
||||
$this->currencyService->change($user, 'exp', $dailySignIn->exp_reward, CurrencySource::SIGN_IN, $remark, $roomId);
|
||||
$this->currencyService->change($user, 'charm', $dailySignIn->charm_reward, CurrencySource::SIGN_IN, $remark, $roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新用户当前签到身份徽章。
|
||||
*/
|
||||
private function refreshSignInIdentityBadge(User $user, ?SignInRewardRule $rewardRule, int $streakDays): void
|
||||
{
|
||||
UserIdentityBadge::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('source', UserIdentityBadge::SOURCE_SIGN_IN)
|
||||
->where('is_active', true)
|
||||
->update(['is_active' => false]);
|
||||
|
||||
if ($rewardRule?->identity_badge_code === null || $rewardRule->identity_badge_name === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 同一来源同一徽章唯一,重复命中时更新状态和连续天数快照即可。
|
||||
UserIdentityBadge::query()->updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'source' => UserIdentityBadge::SOURCE_SIGN_IN,
|
||||
'badge_code' => $rewardRule->identity_badge_code,
|
||||
],
|
||||
[
|
||||
'badge_name' => $rewardRule->identity_badge_name,
|
||||
'badge_icon' => $rewardRule->identity_badge_icon,
|
||||
'badge_color' => $rewardRule->identity_badge_color,
|
||||
'acquired_at' => now(),
|
||||
// 身份有效期由后台规则控制,0 表示长期有效。
|
||||
'expires_at' => $rewardRule->identity_duration_days > 0
|
||||
? now()->addDays($rewardRule->identity_duration_days)
|
||||
: null,
|
||||
'is_active' => true,
|
||||
'metadata' => [
|
||||
'streak_days' => $streakDays,
|
||||
'reward_rule_id' => $rewardRule->id,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按日期向后重算连续天数,补齐断点后让后续日历展示同步变化。
|
||||
*/
|
||||
private function recalculateFutureStreaks(User $user, Carbon $fromDate): void
|
||||
{
|
||||
$records = DailySignIn::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereDate('sign_in_date', '>=', $fromDate)
|
||||
->orderBy('sign_in_date')
|
||||
->lockForUpdate()
|
||||
->get();
|
||||
|
||||
$previous = DailySignIn::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereDate('sign_in_date', '<', $fromDate)
|
||||
->latest('sign_in_date')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
$previousDate = $previous?->sign_in_date;
|
||||
$previousStreak = (int) ($previous?->streak_days ?? 0);
|
||||
|
||||
$records->each(function (DailySignIn $record) use (&$previousDate, &$previousStreak): void {
|
||||
$streakDays = $previousDate?->copy()->addDay()->toDateString() === $record->sign_in_date?->toDateString()
|
||||
? $previousStreak + 1
|
||||
: 1;
|
||||
|
||||
if ((int) $record->streak_days !== $streakDays) {
|
||||
// 只重算连续天数快照,不追溯变更已发放奖励流水。
|
||||
$record->update(['streak_days' => $streakDays]);
|
||||
}
|
||||
|
||||
$previousDate = $record->sign_in_date;
|
||||
$previousStreak = $streakDays;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用最新签到记录刷新当前签到身份。
|
||||
*/
|
||||
private function refreshIdentityFromLatestSignIn(User $user): void
|
||||
{
|
||||
$latestSignIn = DailySignIn::query()
|
||||
->where('user_id', $user->id)
|
||||
->latest('sign_in_date')
|
||||
->first();
|
||||
|
||||
if ($latestSignIn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->refreshSignInIdentityBadge($user, $this->matchRewardRule((int) $latestSignIn->streak_days), (int) $latestSignIn->streak_days);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户最新签到记录的连续天数。
|
||||
*/
|
||||
private function latestStreakDays(User $user): int
|
||||
{
|
||||
return (int) DailySignIn::query()
|
||||
->where('user_id', $user->id)
|
||||
->latest('sign_in_date')
|
||||
->value('streak_days');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户下一张可消耗的补签卡。
|
||||
*/
|
||||
private function nextAvailableSignRepairCard(User $user): ?UserPurchase
|
||||
{
|
||||
return UserPurchase::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('status', 'active')
|
||||
->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_SIGN_REPAIR))
|
||||
->oldest()
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户可用补签卡数量。
|
||||
*/
|
||||
private function availableSignRepairCardCount(User $user): int
|
||||
{
|
||||
return UserPurchase::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('status', 'active')
|
||||
->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_SIGN_REPAIR))
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前上架的补签卡商品。
|
||||
*/
|
||||
private function activeSignRepairCard(): ?ShopItem
|
||||
{
|
||||
return ShopItem::query()
|
||||
->where('type', ShopItem::TYPE_SIGN_REPAIR)
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询启用中的签到奖励档位,供前台展示目标奖励。
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function rewardRulePreview(): array
|
||||
{
|
||||
return SignInRewardRule::query()
|
||||
->where('is_enabled', true)
|
||||
->orderBy('streak_days')
|
||||
->get()
|
||||
->map(fn (SignInRewardRule $rule): array => [
|
||||
'streak_days' => (int) $rule->streak_days,
|
||||
'gold_reward' => (int) $rule->gold_reward,
|
||||
'exp_reward' => (int) $rule->exp_reward,
|
||||
'charm_reward' => (int) $rule->charm_reward,
|
||||
'identity_badge_name' => $rule->identity_badge_name,
|
||||
'identity_badge_icon' => $rule->identity_badge_icon,
|
||||
'identity_badge_color' => $rule->identity_badge_color,
|
||||
'identity_duration_days' => (int) $rule->identity_duration_days,
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成日历内展示的签到奖励文本。
|
||||
*/
|
||||
private function buildRewardText(DailySignIn $dailySignIn): string
|
||||
{
|
||||
$items = [];
|
||||
if ($dailySignIn->gold_reward > 0) {
|
||||
$items[] = $dailySignIn->gold_reward.'金';
|
||||
}
|
||||
if ($dailySignIn->exp_reward > 0) {
|
||||
$items[] = $dailySignIn->exp_reward.'经验';
|
||||
}
|
||||
if ($dailySignIn->charm_reward > 0) {
|
||||
$items[] = $dailySignIn->charm_reward.'魅力';
|
||||
}
|
||||
|
||||
return $items === [] ? '已记录' : implode(' + ', $items);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user