createCeremony($marriage, null, 0, $payerType, $ceremonyType, $ceremonyAt); } $tier = WeddingTier::find($tierId); if (! $tier || ! $tier->is_active) { return ['ok' => false, 'message' => '所选婚礼档位不存在或已关闭。', 'ceremony_id' => null]; } // 验证余额 $groom = $marriage->user; $partner = $marriage->partner; $total = $tier->amount; if ($payerType === 'groom') { if (($groom->jjb ?? 0) < $total) { return ['ok' => false, 'message' => "金币不足,{$tier->name}需要 {$total} 金币,您当前只有 {$groom->jjb} 金币。", 'ceremony_id' => null]; } } else { $half = (int) ceil($total / 2); if (($groom->jjb ?? 0) < $half) { return ['ok' => false, 'message' => "金币不足,男方需要 {$half} 金币,当前只有 {$groom->jjb} 金币。", 'ceremony_id' => null]; } if (($partner->jjb ?? 0) < $half) { return ['ok' => false, 'message' => "金币不足,女方需要 {$half} 金币,当前只有 {$partner->jjb} 金币。", 'ceremony_id' => null]; } } return $this->createCeremony($marriage, $tier, $total, $payerType, $ceremonyType, $ceremonyAt); } /** * 创建婚礼记录,并预扣(或冻结)金币。 */ private function createCeremony( Marriage $marriage, ?WeddingTier $tier, int $total, string $payerType, string $ceremonyType, ?Carbon $ceremonyAt, ): array { return DB::transaction(function () use ($marriage, $tier, $total, $payerType, $ceremonyType, $ceremonyAt) { $groom = $marriage->user; $partner = $marriage->partner; $groomAmount = 0; $partnerAmount = 0; if ($total > 0) { if ($payerType === 'groom') { $groomAmount = $total; } else { $groomAmount = (int) floor($total / 2); $partnerAmount = $total - $groomAmount; } if ($ceremonyType === 'immediate') { // 立即扣款 $this->currency->change($groom, 'gold', -$groomAmount, CurrencySource::WEDDING_ENV_SEND, "婚礼红包发送({$tier->name})"); if ($partnerAmount > 0) { $this->currency->change($partner, 'gold', -$partnerAmount, CurrencySource::WEDDING_ENV_SEND, "婚礼红包发送({$tier->name})"); } } else { // 定时婚礼:冻结金币 $groom->increment('frozen_jjb', $groomAmount); $groom->decrement('jjb', $groomAmount); if ($partnerAmount > 0) { $partner->increment('frozen_jjb', $partnerAmount); $partner->decrement('jjb', $partnerAmount); } } } $expireHours = $this->config->get('envelope_expire_hours', 24); $at = $ceremonyType === 'immediate' ? now() : ($ceremonyAt ?? now()); $ceremony = WeddingCeremony::create([ 'marriage_id' => $marriage->id, 'tier_id' => $tier?->id, 'total_amount' => $total, 'payer_type' => $payerType, 'groom_amount' => $groomAmount, 'partner_amount' => $partnerAmount, 'ceremony_type' => $ceremonyType, 'ceremony_at' => $at, 'status' => $ceremonyType === 'immediate' ? 'active' : 'pending', 'expires_at' => $at->copy()->addHours($expireHours), ]); return ['ok' => true, 'message' => '婚礼设置成功!', 'ceremony_id' => $ceremony->id]; }); } // ──────────────────────────── 婚礼生命周期钩子 ─────────────────── /** * 将预先设置好的定时婚礼(因求婚冻结)转为即刻开始(解冻并记录消费)。 */ public function confirmCeremony(WeddingCeremony $ceremony): void { DB::transaction(function () use ($ceremony) { $marriage = $ceremony->marriage; $tierName = $ceremony->tier?->name ?? '婚礼'; if ($ceremony->ceremony_type === 'scheduled') { // 解除冻结,正式扣款记账 if ($ceremony->groom_amount > 0) { $groom = clone $marriage->user; $marriage->user->decrement('frozen_jjb', $ceremony->groom_amount); $this->currency->change($groom, 'gold', 0, CurrencySource::WEDDING_ENV_SEND, "求婚成功,正式发红包({$tierName})"); } if ($ceremony->partner_amount > 0) { $partner = clone $marriage->partner; $marriage->partner->decrement('frozen_jjb', $ceremony->partner_amount); $this->currency->change($partner, 'gold', 0, CurrencySource::WEDDING_ENV_SEND, "求婚成功,正式发红包({$tierName})"); } // 将类型转为即时开始 $ceremony->update([ 'ceremony_type' => 'immediate', 'ceremony_at' => now(), 'expires_at' => now()->addHours($this->config->get('envelope_expire_hours', 24)), ]); } }); } /** * 撤销由于求婚设置的婚礼,并且解冻/退还因为该婚礼冻结的金币。 */ public function cancelAndRefund(WeddingCeremony $ceremony): void { DB::transaction(function () use ($ceremony) { $ceremony->update(['status' => 'cancelled']); if ($ceremony->ceremony_type === 'scheduled') { $marriage = $ceremony->marriage; if ($ceremony->groom_amount > 0) { $marriage->user?->decrement('frozen_jjb', $ceremony->groom_amount); $marriage->user?->increment('jjb', $ceremony->groom_amount); } if ($ceremony->partner_amount > 0) { $marriage->partner?->decrement('frozen_jjb', $ceremony->partner_amount); $marriage->partner?->increment('jjb', $ceremony->partner_amount); } } }); } // ──────────────────────────── 触发婚礼 ──────────────────────────── /** * 触发婚礼:获取在线用户 → 分配红包 → 写入 claims 表。 * 由立即婚礼(setup 后直接调用)或 TriggerScheduledWeddings Job 调用。 * * @param WeddingCeremony $ceremony 婚礼记录 * @return array{ok: bool, message: string, online_count: int} */ public function trigger(WeddingCeremony $ceremony): array { if (! in_array($ceremony->status, ['pending', 'active'])) { return ['ok' => false, 'message' => '婚礼状态异常。', 'online_count' => 0]; } // 获取所有在线用户(含新郎新娘本人,让所有人都能领红包) $onlineIds = $this->getOnlineUserIds(roomId: $ceremony->marriage->room_id ?? 1); // 在线人数为0时金币退还 if (count($onlineIds) === 0 && $ceremony->total_amount > 0) { $this->refundCeremony($ceremony); return ['ok' => false, 'message' => '当前没有在线用户,红包已退还。', 'online_count' => 0]; } // 随机分配红包金额 $amounts = $ceremony->total_amount > 0 ? $this->distributeRedPacket($ceremony->total_amount, count($onlineIds)) : array_fill(0, count($onlineIds), 0); DB::transaction(function () use ($ceremony, $onlineIds, $amounts) { $now = now(); $claims = []; foreach ($onlineIds as $i => $userId) { $claims[] = [ 'ceremony_id' => $ceremony->id, 'user_id' => $userId, 'amount' => $amounts[$i] ?? 0, 'claimed' => false, 'created_at' => $now, ]; } WeddingEnvelopeClaim::insert($claims); $ceremony->update([ 'status' => 'active', 'online_count' => count($onlineIds), 'ceremony_at' => now(), ]); }); return ['ok' => true, 'message' => '婚礼触发成功!', 'online_count' => count($onlineIds)]; } // ──────────────────────────── 领取红包 ──────────────────────────── /** * 用户领取婚礼红包。 * * @param WeddingCeremony $ceremony 婚礼记录 * @param User $claimer 领取用户 * @return array{ok: bool, message: string, amount: int} */ public function claim(WeddingCeremony $ceremony, User $claimer): array { $claim = WeddingEnvelopeClaim::query() ->where('ceremony_id', $ceremony->id) ->where('user_id', $claimer->id) ->where('claimed', false) ->lockForUpdate() ->first(); if (! $claim) { return ['ok' => false, 'message' => '没有待领取的红包,或红包已被领取。', 'amount' => 0]; } if ($ceremony->expires_at && $ceremony->expires_at->isPast()) { return ['ok' => false, 'message' => '红包已过期。', 'amount' => 0]; } DB::transaction(function () use ($claim, $ceremony, $claimer) { $claim->update(['claimed' => true, 'claimed_at' => now()]); $ceremony->increment('claimed_count'); $ceremony->increment('claimed_amount', $claim->amount); // 金币入账 if ($claim->amount > 0) { $marriage = $ceremony->marriage; $remark = "婚礼红包:{$marriage->user->username} × {$marriage->partner->username}"; $this->currency->change($claimer, 'gold', $claim->amount, CurrencySource::WEDDING_ENV_RECV, $remark); } }); return ['ok' => true, 'message' => "已领取 {$claim->amount} 金币!", 'amount' => $claim->amount]; } // ──────────────────────────── 随机红包算法 ───────────────────────── /** * 随机红包分配(二倍均值算法)。 * 保证每人至少 1 金币,总和精确等于 totalAmount。 * * @param int $totalAmount 总金额 * @param int $count 人数 * @return array 每人分配金额 */ private function distributeRedPacket(int $totalAmount, int $count): array { if ($count <= 0 || $totalAmount <= 0) { return []; } // 人数多于金额时,部分人分到0 if ($totalAmount < $count) { $amounts = array_fill(0, $count, 0); for ($i = 0; $i < $totalAmount; $i++) { $amounts[$i] = 1; } shuffle($amounts); return $amounts; } $amounts = []; $remaining = $totalAmount; for ($i = 0; $i < $count - 1; $i++) { $remainingPeople = $count - $i; $avgDouble = (int) floor($remaining * 2 / $remainingPeople); $max = max(1, min($avgDouble - 1, $remaining - ($remainingPeople - 1))); $amounts[] = random_int(1, $max); $remaining -= end($amounts); } $amounts[] = $remaining; // 最后一人拿剩余 shuffle($amounts); // 随机打乱 return $amounts; } /** * 退还定时婚礼金币(在线人数为0时)。 */ private function refundCeremony(WeddingCeremony $ceremony): void { $ceremony->update(['status' => 'cancelled']); // 解冻退还(定时婚礼金币已被冻结在 frozen_jjb) if ($ceremony->ceremony_type === 'scheduled') { $marriage = $ceremony->marriage; if ($ceremony->groom_amount > 0) { $marriage->user?->decrement('frozen_jjb', $ceremony->groom_amount); $marriage->user?->increment('jjb', $ceremony->groom_amount); } if ($ceremony->partner_amount > 0) { $marriage->partner?->decrement('frozen_jjb', $ceremony->partner_amount); $marriage->partner?->increment('jjb', $ceremony->partner_amount); } } } /** * 从 Redis room:{roomId}:users Hash 获取当前在线用户 ID 列表。 * 若 Hash value 中无 user_id(旧版登录用户),则用 username 批量查库补齐。 * * @param int $roomId 房间 ID * @return array */ private function getOnlineUserIds(int $roomId = 1): array { try { $key = "room:{$roomId}:users"; $users = Redis::hgetall($key); if (empty($users)) { return []; } $ids = []; $fallbacks = []; // 需要 fallback 查库的用户名 foreach ($users as $username => $jsonInfo) { $info = json_decode($jsonInfo, true); if (isset($info['user_id'])) { // 新版登录:user_id 直接存在 Redis $ids[] = (int) $info['user_id']; } else { // 旧版登录(修复前):user_id 缺失,记录 username 待批量查库 $fallbacks[] = $username; } } // 对旧用户批量查库补齐 user_id if (! empty($fallbacks)) { $dbIds = User::whereIn('username', $fallbacks) ->pluck('id') ->map(fn ($id) => (int) $id) ->all(); $ids = array_merge($ids, $dbIds); } return array_values(array_unique($ids)); } catch (\Throwable) { return []; } } /** * 获取用户在指定婚礼中的待领取红包信息。 */ public function getUnclaimedEnvelope(WeddingCeremony $ceremony, int $userId): ?WeddingEnvelopeClaim { return WeddingEnvelopeClaim::query() ->where('ceremony_id', $ceremony->id) ->where('user_id', $userId) ->where('claimed', false) ->first(); } /** * 返回所有激活档位(前台选择用)。 * * @return Collection */ public function activeTiers(): Collection { return WeddingTier::query()->where('is_active', true)->orderBy('tier')->get(); } }