Files
chatroom/app/Services/MarriageService.php

455 lines
19 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
/**
* 文件功能:婚姻核心业务服务
*
* 处理求婚、接受、拒绝、离婚全流程。
* 所有金币/魅力变更通过 UserCurrencyService
* 亲密度变更通过 MarriageIntimacyService
* 参数通过 MarriageConfigService 读取。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Services;
use App\Enums\CurrencySource;
use App\Enums\IntimacySource;
use App\Models\Marriage;
use App\Models\User;
use App\Models\UserPurchase;
use Illuminate\Support\Facades\DB;
class MarriageService
{
public function __construct(
private readonly UserCurrencyService $currency,
private readonly MarriageConfigService $config,
private readonly MarriageIntimacyService $intimacy,
) {}
// ──────────────────────────── 求婚 ────────────────────────────────
/**
* 发起求婚。
*
* @param User $proposer 求婚方
* @param User $target 被求婚方
* @param int $weddingTierId 可选的婚礼档位 ID
* @return array{ok: bool, message: string, marriage_id: int|null}
*/
public function propose(User $proposer, User $target, int $ringPurchaseId, ?int $weddingTierId = null): array
{
// 不能向自己求婚
if ($proposer->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' => '协议离婚已完成。'];
}
/**
* 强制离婚(单方立即生效,发起方金币全转对方)。
*
* @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;
}
}