id === $target->id) { return ['ok' => false, 'message' => '不能向自己求婚!', 'marriage_id' => null]; } // 只允许异性之间求婚(sex 字段:1=男 2=女 0=未设置) $validSexes = [1, 2]; if ( ! in_array((int) $proposer->sex, $validSexes, true) || ! in_array((int) $target->sex, $validSexes, true) || (int) $proposer->sex === (int) $target->sex ) { return ['ok' => false, 'message' => '只有男女双方才能互相求婚,请确认双方性别设置。', 'marriage_id' => null]; } // 检查求婚方是否在冷静期 if ($cooldownMsg = $this->checkCooldown($proposer)) { return ['ok' => false, 'message' => $cooldownMsg, 'marriage_id' => null]; } // 检查双方是否已有进行中的婚姻 if (Marriage::currentFor($proposer->id)) { return ['ok' => false, 'message' => '您已有进行中的婚姻或求婚,无法重复求婚。', 'marriage_id' => null]; } if (Marriage::currentFor($target->id)) { return ['ok' => false, 'message' => '对方已有婚姻关系,无法向其求婚。', 'marriage_id' => null]; } // 验证戒指 $ring = UserPurchase::query() ->where('id', $ringPurchaseId) ->where('user_id', $proposer->id) ->where('status', 'active') ->whereHas('item', fn ($q) => $q->where('type', 'ring')) ->with('item') ->first(); if (! $ring) { return ['ok' => false, 'message' => '未找到有效的戒指,请先到商城购买。', 'marriage_id' => null]; } $expireHours = $this->config->get('proposal_expire_hours', 48); return DB::transaction(function () use ($proposer, $target, $ring, $expireHours, $weddingTierId) { // 在求婚阶段同时进行婚礼设置(由求婚方一人出全资预扣,属于 "男方付 / scheduled冻结" 模式) if ($weddingTierId) { $tier = \App\Models\WeddingTier::find($weddingTierId); if ($tier && $tier->is_active) { if (($proposer->jjb ?? 0) < $tier->amount) { return ['ok' => false, 'message' => "金币不足,该婚礼档位需要 {$tier->amount} 金币。", 'marriage_id' => null]; } } } // 戒指状态改为占用中 $ring->update(['status' => 'used_pending']); // 创建婚姻记录 $marriage = Marriage::create([ 'user_id' => $proposer->id, 'partner_id' => $target->id, 'ring_item_id' => $ring->item_id, 'ring_purchase_id' => $ring->id, 'status' => 'pending', 'proposed_at' => now(), 'expires_at' => now()->addHours($expireHours), // 旧字段兼容 'hyname' => $proposer->username, 'hyname1' => $target->username, ]); if ($weddingTierId) { $weddingService = app(WeddingService::class); // 预扣冻结:payerType=groom, ceremonyType=scheduled $setupRes = $weddingService->setup($marriage, $weddingTierId, 'groom', 'scheduled'); if (! $setupRes['ok']) { throw new \Exception($setupRes['message']); } } return ['ok' => true, 'message' => '求婚成功,等待对方回应。', 'marriage_id' => $marriage->id]; }); } // ──────────────────────────── 接受求婚 ────────────────────────────── /** * 接受求婚,正式结婚。 * * @param Marriage $marriage 婚姻记录(必须为 pending 状态) * @param User $acceptor 接受方(必须为 partner) * @return array{ok: bool, message: string} */ public function accept(Marriage $marriage, User $acceptor): array { if ($marriage->status !== 'pending') { return ['ok' => false, 'message' => '该求婚已失效。']; } if ($marriage->partner_id !== $acceptor->id) { return ['ok' => false, 'message' => '无权操作此求婚。']; } if ($marriage->expires_at && $marriage->expires_at->isPast()) { $this->expireProposal($marriage); return ['ok' => false, 'message' => '求婚已超时失效,戒指已消失。']; } $ringItem = $marriage->ringItem; DB::transaction(function () use ($marriage, $ringItem) { // 正式结婚 $marriage->update([ 'status' => 'married', 'married_at' => now(), 'hytime' => now(), ]); // 戒指标记已使用 UserPurchase::where('id', $marriage->ring_purchase_id)->update(['status' => 'used']); // 双方各获魅力加成(通过戒指 slug 查配置) if ($ringItem) { $slug = $ringItem->slug; $charmKey = 'ring_'.str_replace('ring_', '', $slug).'_charm'; $intimacyKey = 'ring_'.str_replace('ring_', '', $slug).'_intimacy'; $charm = $this->config->get($charmKey, 50); $initIntimacy = $this->config->get($intimacyKey, 10); $proposer = $marriage->user; $partner = $marriage->partner; $ringName = $ringItem->name; $this->currency->change($proposer, 'charm', $charm, CurrencySource::MARRY_CHARM, "结婚魅力加成({$ringName})"); $this->currency->change($partner, 'charm', $charm, CurrencySource::MARRY_CHARM, "结婚魅力加成({$ringName})"); // 初始亲密度 $this->intimacy->add($marriage, $initIntimacy, IntimacySource::WEDDING_BONUS, "结婚戒指初始亲密度({$ringName})", true); } // 如果有预先设置的婚礼(随求婚一起设定的),则将冻结金币转为正式扣除,并触发红包 $ceremony = \App\Models\WeddingCeremony::where('marriage_id', $marriage->id)->where('status', 'pending')->first(); if ($ceremony) { $weddingService = app(WeddingService::class); $weddingService->confirmCeremony($ceremony); // 解冻扣除,转为 immediate $weddingService->trigger($ceremony); broadcast(new \App\Events\WeddingCelebration($ceremony, $marriage)); } }); return ['ok' => true, 'message' => '恭喜!你们已正式结婚!']; } // ──────────────────────────── 拒绝求婚 ────────────────────────────── /** * 拒绝求婚(戒指消失,不退还)。 * * @param Marriage $marriage 婚姻记录 * @param User $rejector 拒绝方 * @return array{ok: bool, message: string} */ public function reject(Marriage $marriage, User $rejector): array { if ($marriage->status !== 'pending') { return ['ok' => false, 'message' => '该求婚已失效。']; } if ($marriage->partner_id !== $rejector->id) { return ['ok' => false, 'message' => '无权操作此求婚。']; } return DB::transaction(function () use ($marriage) { $marriage->update(['status' => 'rejected']); // 检查是否有由于求婚冻结的婚礼金币,若有则退还 $ceremony = \App\Models\WeddingCeremony::where('marriage_id', $marriage->id)->where('status', 'pending')->first(); if ($ceremony) { app(WeddingService::class)->cancelAndRefund($ceremony); } // 戒指拒绝后遗失 UserPurchase::where('id', $marriage->ring_purchase_id)->update(['status' => 'lost']); // 记录戒指消失流水(amount=0,仅存档) if ($proposer = $marriage->user) { $this->currency->change($proposer, 'gold', 0, CurrencySource::RING_LOST, "求婚被拒,戒指消失({$marriage->ringItem?->name})"); } return ['ok' => true, 'message' => '已拒绝求婚。']; }); } // ──────────────────────────── 离婚 ────────────────────────────────── /** * 申请离婚或发起强制离婚。 * * @param Marriage $marriage 婚姻记录(必须为 married 状态) * @param User $initiator 发起方 * @param string $type 'mutual'=协议 | 'forced'=强制 * @return array{ok: bool, message: string} */ public function divorce(Marriage $marriage, User $initiator, string $type = 'mutual'): array { if ($marriage->status !== 'married') { return ['ok' => false, 'message' => '当前没有有效的婚姻关系。']; } if (! $marriage->involves($initiator->id)) { return ['ok' => false, 'message' => '无权操作此婚姻。']; } if ($type === 'forced') { return $this->forceDissolve($marriage, $initiator); } // 协议离婚:标记申请,等待对方确认(72h 后 Horizon 自动升级) $marriage->update([ 'divorce_type' => 'mutual', 'divorcer_id' => $initiator->id, 'divorce_requested_at' => now(), ]); return ['ok' => true, 'message' => '离婚申请已发送,等待对方确认(72小时内未回应将自动解除)。']; } /** * 确认协议离婚(被申请方接受)。 * * @param Marriage $marriage 婚姻记录 * @param User $confirmer 确认方(必须不是发起方) * @return array{ok: bool, message: string} */ public function confirmDivorce(Marriage $marriage, User $confirmer): array { if ($marriage->status !== 'married' || $marriage->divorce_type !== 'mutual') { return ['ok' => false, 'message' => '没有待确认的离婚申请。']; } if ($marriage->divorcer_id === $confirmer->id) { return ['ok' => false, 'message' => '不能确认自己发起的离婚申请,如需强制离婚请另行操作。']; } if (! $marriage->involves($confirmer->id)) { return ['ok' => false, 'message' => '无权操作此婚姻。']; } DB::transaction(function () use ($marriage) { $penalty = $this->config->get('divorce_mutual_charm', 100); // 双方扣魅力 $this->currency->change($marriage->user, 'charm', -$penalty, CurrencySource::DIVORCE_CHARM, '协议离婚魅力惩罚'); $this->currency->change($marriage->partner, 'charm', -$penalty, CurrencySource::DIVORCE_CHARM, '协议离婚魅力惩罚'); // 更新婚姻状态 $marriage->update([ 'status' => 'divorced', 'divorce_type' => 'mutual', 'divorced_at' => now(), 'intimacy' => 0, 'level' => 1, ]); }); return ['ok' => true, 'message' => '协议离婚已完成。']; } /** * 被申请方拒绝协议离婚申请(等同于强制离婚)。 * * 规则: * - 婚姻立即以 forced 类型解除 * - 申请人(divorcer)被扣除 divorce_forced_charm 点魅力作为惩罚 * - 申请人一半金币赔偿给被申请方 * - 申请人进入强制离婚冷静期 * * @param Marriage $marriage 婚姻记录 * @param User $respondent 被申请方(拒绝者) * @return array{ok: bool, message: string} */ public function rejectDivorce(Marriage $marriage, User $respondent): array { if ($marriage->status !== 'married' || $marriage->divorce_type !== 'mutual') { return ['ok' => false, 'message' => '没有待处理的离婚申请。']; } if ($marriage->divorcer_id === $respondent->id) { return ['ok' => false, 'message' => '不能拒绝自己发起的离婚申请。']; } if (! $marriage->involves($respondent->id)) { return ['ok' => false, 'message' => '无权操作此婚姻。']; } // 申请方(被拒的一方) $divorcer = $marriage->user_id === $marriage->divorcer_id ? $marriage->user : $marriage->partner; DB::transaction(function () use ($marriage, $divorcer, $respondent) { $charmPenalty = $this->config->get('divorce_forced_charm', 300); // 申请人扣魅力 $this->currency->change($divorcer, 'charm', -$charmPenalty, CurrencySource::DIVORCE_CHARM, '拒绝离婚时视为强制离婚,扣除魅力惩罚'); // 申请人一半金币赔偿给对方 $divorcerJjb = $divorcer->fresh()->jjb ?? 0; if ($divorcerJjb > 0) { $half = (int) floor($divorcerJjb / 2); if ($half > 0) { $this->currency->change($divorcer, 'gold', -$half, CurrencySource::FORCED_DIVORCE_TRANSFER, "协议离婚被拒,赔偿一半金币给 {$respondent->username}"); $this->currency->change($respondent, 'gold', $half, CurrencySource::FORCED_DIVORCE_TRANSFER, "对方协议离婚被拒,获得 {$divorcer->username} 一半金币赔偿"); } } // 更新婚姻为强制离婚 $marriage->update([ 'status' => 'divorced', 'divorce_type' => 'forced', 'divorcer_id' => $divorcer->id, 'divorced_at' => now(), 'intimacy' => 0, 'level' => 1, ]); }); $divorcerJjb = $divorcer->fresh()->jjb ?? 0; $half = (int) floor(($divorcer->jjb ?? 0) / 2); return ['ok' => true, 'message' => "你已拒绝离婚申请,视为强制离婚,{$divorcer->username} 赔偿了 {$half} 枚金币给你,婚姻已解除。"]; } /** * 强制离婚(单方立即生效,发起方金币全转对方)。 * * @param Marriage $marriage 婚姻记录 * @param User $initiator 强制发起方 * @return array{ok: bool, message: string} */ public function forceDissolve(Marriage $marriage, User $initiator, bool $byAdmin = false): array { if (! $byAdmin) { // 检查强制离婚间隔限制 $limitDays = $this->config->get('forced_divorce_limit_days', 60); $recentForced = Marriage::query() ->where('divorcer_id', $initiator->id) ->where('divorce_type', 'forced') ->where('divorced_at', '>=', now()->subDays($limitDays)) ->exists(); if ($recentForced) { return ['ok' => false, 'message' => "您每{$limitDays}天内只能强制离婚1次,冷静期未满。"]; } } $victim = $marriage->user_id === $initiator->id ? $marriage->partner : $marriage->user; DB::transaction(function () use ($marriage, $initiator, $victim, $byAdmin) { if (! $byAdmin) { $penalty = $this->config->get('divorce_forced_charm', 300); // 强制方扣魅力 $this->currency->change($initiator, 'charm', -$penalty, CurrencySource::DIVORCE_CHARM, '强制离婚魅力惩罚'); // 金币全转对方 $initiatorJjb = $initiator->fresh()->jjb ?? 0; if ($initiatorJjb > 0) { $this->currency->change($initiator, 'gold', -$initiatorJjb, CurrencySource::FORCED_DIVORCE_TRANSFER, "强制离婚,财产转让给{$victim->username}"); $this->currency->change($victim, 'gold', $initiatorJjb, CurrencySource::FORCED_DIVORCE_TRANSFER, "强制离婚,获得{$initiator->username}全部财产"); } } $marriage->update([ 'status' => 'divorced', 'divorce_type' => $byAdmin ? 'admin' : 'forced', 'divorcer_id' => $initiator->id, 'divorced_at' => now(), 'intimacy' => 0, 'level' => 1, ]); }); $msg = $byAdmin ? '管理员已强制解除婚姻关系。' : "强制离婚已完成,{$initiator->username} 的全部金币已转给 {$victim->username}。"; return ['ok' => true, 'message' => $msg]; } // ──────────────────────────── 内部工具 ──────────────────────────── /** * 处理超时的求婚记录(Horizon Job 调用)。 */ public function expireProposal(Marriage $marriage): void { if ($marriage->status !== 'pending') { return; } DB::transaction(function () use ($marriage) { $marriage->update(['status' => 'expired']); UserPurchase::where('id', $marriage->ring_purchase_id)->update(['status' => 'lost']); if ($proposer = $marriage->user) { $this->currency->change($proposer, 'gold', 0, CurrencySource::RING_LOST, "求婚超时,戒指消失({$marriage->ringItem?->name})"); } // 退还当时冻结的婚礼金币 $ceremony = \App\Models\WeddingCeremony::where('marriage_id', $marriage->id)->where('status', 'pending')->first(); if ($ceremony) { app(WeddingService::class)->cancelAndRefund($ceremony); } }); } /** * 处理协议离婚超时自动升级为强制(Horizon Job 调用)。 */ public function autoExpireDivorce(Marriage $marriage): void { $divorcer = User::find($marriage->divorcer_id); if (! $divorcer) { return; } $penalty = $this->config->get('divorce_auto_charm', 150); DB::transaction(function () use ($marriage, $divorcer, $penalty) { $this->currency->change($divorcer, 'charm', -$penalty, CurrencySource::DIVORCE_CHARM, '离婚申请超时,自动解除'); $marriage->update([ 'status' => 'divorced', 'divorce_type' => 'auto', 'divorced_at' => now(), 'intimacy' => 0, 'level' => 1, ]); }); } /** * 检查用户是否在冷静期,返回错误提示文字;无冷静期返回 null。 */ private function checkCooldown(User $user): ?string { // 查找最近一次离婚记录 $lastDivorce = Marriage::query() ->where('status', 'divorced') ->where(function ($q) use ($user) { $q->where('user_id', $user->id)->orWhere('partner_id', $user->id); }) ->orderByDesc('divorced_at') ->first(); if (! $lastDivorce) { return null; } $type = $lastDivorce->divorce_type ?? 'mutual'; // 强制方 $isForcer = $lastDivorce->divorcer_id === $user->id && in_array($type, ['forced', 'auto']); $cooldownKey = match (true) { $isForcer && $type === 'forced' => 'divorce_forced_cooldown', $isForcer && $type === 'auto' => 'divorce_auto_cooldown', default => 'divorce_mutual_cooldown', }; $cooldownDays = $this->config->get($cooldownKey, 70); $cooldownEnds = $lastDivorce->divorced_at?->addDays($cooldownDays); if ($cooldownEnds && $cooldownEnds->isFuture()) { // 取两者的完全相差天数,如果有部分不够一天的则向上取整为 1 天(例:还剩 2小时 = 1天) $diffInHours = now()->diffInHours($cooldownEnds); $remaining = max(1, (int) ceil($diffInHours / 24)); return "您还在离婚冷静期,还需 {$remaining} 天后才能再次结婚。"; } return null; } }