From 2d07b032d98b0177ec853667cf6ea1865aec03b2 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sun, 1 Mar 2026 15:03:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=A9=9A=E5=A7=BB?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=AC=AC4-6=E6=AD=A5=EF=BC=88Services=20+=20?= =?UTF-8?q?Models=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 4 - MarriageConfigService: - 带60min Cache 的配置读取/写入 - 支持单项/分组/全量读取,管理员保存后自动清缓存 Step 5 - MarriageIntimacyService: - 亲密度增加 + 日志写入 + 等级自动更新 - Redis 每日上限计数器(各来源独立控制) - onFlowerSent/onPrivateChat/onlineTick 接入点方法 - dailyBatch 批量处理(Horizon Job 用) Step 6 - MarriageService(核心业务): - propose/accept/reject/divorce/confirmDivorce/forceDissolve - 所有金币魅力通过 UserCurrencyService 统一记账 - 冷静期检查/超时处理/强制离婚金币全转对方 Models 改良(Marriage/MarriageConfig/MarriageIntimacyLog) --- app/Models/Marriage.php | 160 ++++++++- app/Models/MarriageConfig.php | 44 +++ app/Models/MarriageIntimacyLog.php | 54 +++ app/Models/WeddingCeremony.php | 10 + app/Models/WeddingEnvelopeClaim.php | 10 + app/Models/WeddingTier.php | 10 + app/Services/MarriageConfigService.php | 112 +++++++ app/Services/MarriageIntimacyService.php | 240 ++++++++++++++ app/Services/MarriageService.php | 401 +++++++++++++++++++++++ app/Services/WeddingService.php | 14 + 10 files changed, 1039 insertions(+), 16 deletions(-) create mode 100644 app/Models/MarriageConfig.php create mode 100644 app/Models/MarriageIntimacyLog.php create mode 100644 app/Models/WeddingCeremony.php create mode 100644 app/Models/WeddingEnvelopeClaim.php create mode 100644 app/Models/WeddingTier.php create mode 100644 app/Services/MarriageConfigService.php create mode 100644 app/Services/MarriageIntimacyService.php create mode 100644 app/Services/MarriageService.php create mode 100644 app/Services/WeddingService.php diff --git a/app/Models/Marriage.php b/app/Models/Marriage.php index 4f923d4..468d0da 100644 --- a/app/Models/Marriage.php +++ b/app/Models/Marriage.php @@ -1,37 +1,43 @@ - */ + /** @var list */ protected $fillable = [ - 'hyname', - 'hyname1', - 'hytime', - 'hygb', - 'hyjb', - 'i', + // 旧字段(兼容保留) + 'hyname', 'hyname1', 'hytime', 'hygb', 'hyjb', 'i', + // 新字段 + 'user_id', 'partner_id', + 'ring_item_id', 'ring_purchase_id', + 'status', + 'proposed_at', 'expires_at', 'married_at', 'divorced_at', + 'divorce_type', 'divorcer_id', 'divorce_requested_at', + 'intimacy', 'level', + 'online_minutes', 'flower_count', 'chat_count', + 'admin_note', ]; /** - * Get the attributes that should be cast. + * 字段类型转换。 * * @return array */ @@ -39,6 +45,128 @@ class Marriage extends Model { return [ 'hytime' => 'datetime', + 'proposed_at' => 'datetime', + 'expires_at' => 'datetime', + 'married_at' => 'datetime', + 'divorced_at' => 'datetime', + 'divorce_requested_at' => 'datetime', + 'intimacy' => 'integer', + 'level' => 'integer', + 'online_minutes' => 'integer', + 'flower_count' => 'integer', + 'chat_count' => 'integer', ]; } + + // ──────────────────────────── 关联关系 ──────────────────────────── + + /** + * 发起方用户。 + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } + + /** + * 被求婚方用户。 + */ + public function partner(): BelongsTo + { + return $this->belongsTo(User::class, 'partner_id'); + } + + /** + * 使用的戒指道具。 + */ + public function ringItem(): BelongsTo + { + return $this->belongsTo(ShopItem::class, 'ring_item_id'); + } + + /** + * 亲密度变更日志。 + */ + public function intimacyLogs(): HasMany + { + return $this->hasMany(MarriageIntimacyLog::class); + } + + /** + * 婚礼仪式记录。 + */ + public function ceremonies(): HasMany + { + return $this->hasMany(WeddingCeremony::class); + } + + /** + * 最新一场婚礼。 + */ + public function latestCeremony(): HasOne + { + return $this->hasOne(WeddingCeremony::class)->latestOfMany(); + } + + // ──────────────────────────── 查询 Scope ────────────────────────── + + /** + * 仅返回已婚记录。 + */ + public function scopeMarried(Builder $query): Builder + { + return $query->where('status', 'married'); + } + + /** + * 仅返回求婚中记录。 + */ + public function scopePending(Builder $query): Builder + { + return $query->where('status', 'pending'); + } + + // ──────────────────────────── 业务方法 ──────────────────────────── + + /** + * 判断指定用户是否为婚姻一方。 + * + * @param int $userId 用户 ID + */ + public function involves(int $userId): bool + { + return $this->user_id === $userId || $this->partner_id === $userId; + } + + /** + * 判断两人是否已婚(静态工厂方法)。 + * + * @param int $userA 用户A ID + * @param int $userB 用户B ID + */ + public static function areMárried(int $userA, int $userB): bool + { + return static::query() + ->where('status', 'married') + ->where(function (Builder $q) use ($userA, $userB) { + $q->where(fn ($q) => $q->where('user_id', $userA)->where('partner_id', $userB)) + ->orWhere(fn ($q) => $q->where('user_id', $userB)->where('partner_id', $userA)); + }) + ->exists(); + } + + /** + * 获取用户当前有效婚姻(pending 或 married 状态)。 + * + * @param int $userId 用户 ID + */ + public static function currentFor(int $userId): ?static + { + return static::query() + ->whereIn('status', ['pending', 'married']) + ->where(function (Builder $q) use ($userId) { + $q->where('user_id', $userId)->orWhere('partner_id', $userId); + }) + ->first(); + } } diff --git a/app/Models/MarriageConfig.php b/app/Models/MarriageConfig.php new file mode 100644 index 0000000..6359b04 --- /dev/null +++ b/app/Models/MarriageConfig.php @@ -0,0 +1,44 @@ + */ + protected $fillable = [ + 'group', + 'key', + 'value', + 'label', + 'description', + 'min', + 'max', + ]; + + /** + * 字段类型转换。 + * + * @return array + */ + protected function casts(): array + { + return [ + 'value' => 'integer', + 'min' => 'integer', + 'max' => 'integer', + ]; + } +} diff --git a/app/Models/MarriageIntimacyLog.php b/app/Models/MarriageIntimacyLog.php new file mode 100644 index 0000000..fced937 --- /dev/null +++ b/app/Models/MarriageIntimacyLog.php @@ -0,0 +1,54 @@ + */ + protected $fillable = [ + 'marriage_id', + 'amount', + 'balance_after', + 'source', + 'remark', + ]; + + /** + * 字段类型转换。 + * + * @return array + */ + protected function casts(): array + { + return [ + 'amount' => 'integer', + 'balance_after' => 'integer', + 'created_at' => 'datetime', + ]; + } + + /** + * 所属婚姻关系。 + */ + public function marriage(): BelongsTo + { + return $this->belongsTo(Marriage::class); + } +} diff --git a/app/Models/WeddingCeremony.php b/app/Models/WeddingCeremony.php new file mode 100644 index 0000000..a3927a0 --- /dev/null +++ b/app/Models/WeddingCeremony.php @@ -0,0 +1,10 @@ +addMinutes(self::CACHE_TTL), + fn () => MarriageConfig::where('key', $key)->value('value') ?? $default + ); + } + + /** + * 批量读取同一分组所有配置(后台页面用)。 + * + * @param string $group 分组名 + * @return Collection + */ + public function getGroup(string $group): Collection + { + return MarriageConfig::where('group', $group)->orderBy('id')->get(); + } + + /** + * 按分组名返回所有配置(后台页面用)。 + * + * @return Collection> + */ + public function allGrouped(): Collection + { + return Cache::remember( + self::ALL_CACHE_KEY, + now()->addMinutes(self::CACHE_TTL), + fn () => MarriageConfig::orderBy('id')->get()->groupBy('group') + ); + } + + /** + * 写入单个配置值,并清除相关缓存。 + * + * @param string $key 配置键名 + * @param int $value 新值 + */ + public function set(string $key, int $value): bool + { + $rows = MarriageConfig::where('key', $key)->update(['value' => $value]); + + // 清除单项缓存及全量缓存 + Cache::forget(self::CACHE_PREFIX.$key); + Cache::forget(self::ALL_CACHE_KEY); + + return $rows > 0; + } + + /** + * 批量写入配置(后台表单批量保存,接受 ['key' => value] 格式)。 + * + * @param array $data 键值对 + */ + public function batchSet(array $data): void + { + foreach ($data as $key => $value) { + $this->set($key, (int) $value); + } + } + + /** + * 清除全部婚姻配置缓存(迁移/重新 Seed 后调用)。 + */ + public function clearAll(): void + { + $keys = MarriageConfig::pluck('key'); + foreach ($keys as $key) { + Cache::forget(self::CACHE_PREFIX.$key); + } + Cache::forget(self::ALL_CACHE_KEY); + } +} diff --git a/app/Services/MarriageIntimacyService.php b/app/Services/MarriageIntimacyService.php new file mode 100644 index 0000000..e64a06e --- /dev/null +++ b/app/Services/MarriageIntimacyService.php @@ -0,0 +1,240 @@ +getDailyCap($source); + if ($cap > 0 && ! $this->checkAndIncrRedis($marriage->id, $source, $amount, $cap)) { + return; // 已达到每日上限,静默忽略 + } + } + + DB::transaction(function () use ($marriage, $amount, $source, $remark) { + // 原子加亲密度 + $marriage->increment('intimacy', $amount); + $newIntimacy = $marriage->fresh()->intimacy; + + // 更新婚姻等级 + $newLevel = self::calcLevel($newIntimacy, $this->config); + if ($newLevel !== $marriage->level) { + $marriage->update(['level' => $newLevel]); + } + + // 写入日志 + MarriageIntimacyLog::create([ + 'marriage_id' => $marriage->id, + 'amount' => $amount, + 'balance_after' => $newIntimacy, + 'source' => $source->value, + 'remark' => $remark, + ]); + }); + } + + /** + * 每日结婚天数批量加亲密度(ProcessMarriageIntimacy Job 调用)。 + * 给所有 married 状态的婚姻对加 intimacy_daily_time 配置的积分。 + */ + public function dailyBatch(): void + { + $amount = $this->config->get('intimacy_daily_time', 10); + $remark = '每日结婚天数奖励'; + + Marriage::query() + ->where('status', 'married') + ->cursor() + ->each(function (Marriage $marriage) use ($amount, $remark) { + $this->add($marriage, $amount, IntimacySource::DAILY_TIME, $remark, true); + }); + } + + /** + * 双方同时在线每分钟加亲密度(AutoSaveJob 调用)。 + */ + public function onlineTick(Marriage $marriage): void + { + $amount = $this->config->get('intimacy_online_per_min', 1); + $this->add($marriage, $amount, IntimacySource::ONLINE_TOGETHER, '双方同时在线'); + } + + /** + * 送花触发亲密度(GiftController 调用)。 + * 自动判断当前用户是送花方还是收花方,并使用对应加成。 + * + * @param Marriage $marriage 婚姻关系 + * @param int $giverId 送花者 user.id + * @param int $flowerQty 送花数量 + */ + public function onFlowerSent(Marriage $marriage, int $giverId, int $flowerQty = 1): void + { + // 送花方:send_flower 加成 + $sendAmount = $this->config->get('intimacy_send_flower', 1) * $flowerQty; + $this->add($marriage, $sendAmount, IntimacySource::SEND_FLOWER, "向伴侣送花×{$flowerQty}"); + + // 收花方(两人共享同一段婚姻记录,无需区分):recv_flower 加成 + $recvAmount = $this->config->get('intimacy_recv_flower', 2) * $flowerQty; + $this->add($marriage, $recvAmount, IntimacySource::RECV_FLOWER, "伴侣送花×{$flowerQty}"); + } + + /** + * 私聊消息触发亲密度(WhisperController 调用)。 + * 使用 Redis 计数,每 2 条触发 1 次加分。 + * + * @param Marriage $marriage 婚姻关系 + */ + public function onPrivateChat(Marriage $marriage): void + { + $redisKey = "marriage:{$marriage->id}:whisper_count:".now()->toDateString(); + $count = Redis::incr($redisKey); + + // 设置 TTL(次日 00:05 过期) + if ($count === 1) { + Redis::expireAt($redisKey, Carbon::tomorrow()->addMinutes(5)->timestamp); + } + + // 每2条触发1次 + if ($count % 2 === 0) { + $amount = $this->config->get('intimacy_private_chat', 1); + $this->add($marriage, $amount, IntimacySource::PRIVATE_CHAT, '私聊消息'); + } + } + + /** + * 根据亲密度计算婚姻等级(1-4)。 + * + * @param int $intimacy 当前亲密度 + * @param MarriageConfigService $config 配置服务实例 + */ + public static function calcLevel(int $intimacy, MarriageConfigService $config): int + { + if ($intimacy >= $config->get('level4_threshold', 1500)) { + return 4; + } + if ($intimacy >= $config->get('level3_threshold', 600)) { + return 3; + } + if ($intimacy >= $config->get('level2_threshold', 200)) { + return 2; + } + + return 1; + } + + /** + * 返回等级图标(用于前端展示)。 + */ + public static function levelIcon(int $level): string + { + return match ($level) { + 2 => '💕', + 3 => '💞', + 4 => '👑', + default => '💑', + }; + } + + /** + * 返回等级名称。 + */ + public static function levelName(int $level): string + { + return match ($level) { + 2 => '恩爱夫妻', + 3 => '情深意重', + 4 => '白头偕老', + default => '新婚燕尔', + }; + } + + /** + * 获取指定来源的每日上限配置值(0=不限)。 + */ + private function getDailyCap(IntimacySource $source): int + { + return match ($source) { + IntimacySource::ONLINE_TOGETHER => $this->config->get('intimacy_online_daily_cap', 120), + IntimacySource::RECV_FLOWER => $this->config->get('intimacy_recv_flower_cap', 40), + IntimacySource::SEND_FLOWER => $this->config->get('intimacy_send_flower_cap', 20), + IntimacySource::PRIVATE_CHAT => $this->config->get('intimacy_private_chat_cap', 10), + default => 0, // 不限 + }; + } + + /** + * 检查 Redis 每日计数器是否超限,未超限则增加计数。 + * 返回 true 表示可以继续加分,false 表示已达上限。 + * + * @param int $marriageId 婚姻 ID + * @param IntimacySource $source 来源 + * @param int $amount 本次加分量 + * @param int $cap 每日上限 + */ + private function checkAndIncrRedis(int $marriageId, IntimacySource $source, int $amount, int $cap): bool + { + $redisKey = "marriage:{$marriageId}:intimacy:{$source->value}:".now()->toDateString(); + $current = (int) Redis::get($redisKey); + + if ($current >= $cap) { + return false; // 已达上限 + } + + // 本次可加量(不超过剩余额度) + $actualAdd = min($amount, $cap - $current); + Redis::incrBy($redisKey, $actualAdd); + + // 设置 TTL(次日 00:05 过期) + if ($current === 0) { + Redis::expireAt($redisKey, Carbon::tomorrow()->addMinutes(5)->timestamp); + } + + return true; + } +} diff --git a/app/Services/MarriageService.php b/app/Services/MarriageService.php new file mode 100644 index 0000000..0761e70 --- /dev/null +++ b/app/Services/MarriageService.php @@ -0,0 +1,401 @@ +id === $target->id) { + 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) { + // 戒指状态改为占用中 + $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, + ]); + + 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); + } + }); + + 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' => '无权操作此求婚。']; + } + + DB::transaction(function () use ($marriage) { + $marriage->update(['status' => 'rejected']); + // 戒指消失(lost = 不退还) + 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})"); + } + }); + } + + /** + * 处理协议离婚超时自动升级为强制(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()) { + $remaining = now()->diffInDays($cooldownEnds, false); + + return "您还在离婚冷静期,还需 {$remaining} 天后才能再次结婚。"; + } + + return null; + } +} diff --git a/app/Services/WeddingService.php b/app/Services/WeddingService.php new file mode 100644 index 0000000..b59d023 --- /dev/null +++ b/app/Services/WeddingService.php @@ -0,0 +1,14 @@ +