功能:婚姻系统第4-6步(Services + Models)
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)
This commit is contained in:
+144
-16
@@ -1,37 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:婚姻关系模型
|
||||
* 文件功能:婚姻关系模型(改良版)
|
||||
*
|
||||
* 对应原 ASP 文件:hy / lh 表
|
||||
* 对应 marriages 表,管理配对双方的婚姻状态、亲密度、等级及离婚信息。
|
||||
* 原 ASP 字段(hyname/hyname1/hytime 等)保留向后兼容,新业务使用 user_id/partner_id。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class Marriage extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
/** @var list<string> */
|
||||
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<string, string>
|
||||
*/
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:婚姻参数配置 Model
|
||||
*
|
||||
* 对应 marriage_configs 表,存储所有婚姻系统的可调参数。
|
||||
* 管理员可在后台「婚姻管理 → 参数配置」页面修改,通过 MarriageConfigService 读取。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MarriageConfig extends Model
|
||||
{
|
||||
/** @var list<string> */
|
||||
protected $fillable = [
|
||||
'group',
|
||||
'key',
|
||||
'value',
|
||||
'label',
|
||||
'description',
|
||||
'min',
|
||||
'max',
|
||||
];
|
||||
|
||||
/**
|
||||
* 字段类型转换。
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'value' => 'integer',
|
||||
'min' => 'integer',
|
||||
'max' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:婚姻亲密度日志 Model
|
||||
*
|
||||
* 对应 marriage_intimacy_logs 表,记录婚姻亲密度的每一次变更。
|
||||
* 亲密度属于婚姻对,独立于用户个人的 user_currency_logs。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class MarriageIntimacyLog extends Model
|
||||
{
|
||||
/** 无 updated_at 字段 */
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
/** @var list<string> */
|
||||
protected $fillable = [
|
||||
'marriage_id',
|
||||
'amount',
|
||||
'balance_after',
|
||||
'source',
|
||||
'remark',
|
||||
];
|
||||
|
||||
/**
|
||||
* 字段类型转换。
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'amount' => 'integer',
|
||||
'balance_after' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 所属婚姻关系。
|
||||
*/
|
||||
public function marriage(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Marriage::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WeddingCeremony extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WeddingEnvelopeClaim extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WeddingTier extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:婚姻参数配置服务
|
||||
*
|
||||
* 提供带缓存的婚姻系统参数读取与写入接口。
|
||||
* 管理员修改参数后自动清除缓存,所有婚姻相关 Service 均通过此类读取配置,
|
||||
* 不在代码中硬编码任何奖惩数值。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\MarriageConfig;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class MarriageConfigService
|
||||
{
|
||||
/** 缓存 KEY 前缀 */
|
||||
private const CACHE_PREFIX = 'marriage_config:';
|
||||
|
||||
/** 缓存 TTL(分钟) */
|
||||
private const CACHE_TTL = 60;
|
||||
|
||||
/** 全量缓存 KEY(分组展示用) */
|
||||
private const ALL_CACHE_KEY = 'marriage_config:__all__';
|
||||
|
||||
/**
|
||||
* 读取单个配置值(带缓存)。
|
||||
*
|
||||
* @param string $key 配置键名
|
||||
* @param int $default 找不到时的默认值
|
||||
*/
|
||||
public function get(string $key, int $default = 0): int
|
||||
{
|
||||
return Cache::remember(
|
||||
self::CACHE_PREFIX.$key,
|
||||
now()->addMinutes(self::CACHE_TTL),
|
||||
fn () => MarriageConfig::where('key', $key)->value('value') ?? $default
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量读取同一分组所有配置(后台页面用)。
|
||||
*
|
||||
* @param string $group 分组名
|
||||
* @return Collection<int, MarriageConfig>
|
||||
*/
|
||||
public function getGroup(string $group): Collection
|
||||
{
|
||||
return MarriageConfig::where('group', $group)->orderBy('id')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分组名返回所有配置(后台页面用)。
|
||||
*
|
||||
* @return Collection<string, Collection<int, MarriageConfig>>
|
||||
*/
|
||||
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<string, int> $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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:婚姻亲密度操作服务
|
||||
*
|
||||
* 统一管理婚姻亲密度的增减逻辑:
|
||||
* - 写入 marriage_intimacy_logs 日志
|
||||
* - 更新 marriages.intimacy 和 marriages.level
|
||||
* - 通过 Redis 计数器控制每日来源上限
|
||||
* - 所有参数从 MarriageConfigService 读取,不硬编码
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\IntimacySource;
|
||||
use App\Models\Marriage;
|
||||
use App\Models\MarriageIntimacyLog;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class MarriageIntimacyService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MarriageConfigService $config,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 增加婚姻亲密度(带每日上限检查)。
|
||||
*
|
||||
* @param Marriage $marriage 婚姻记录
|
||||
* @param int $amount 增加量(必须为正整数)
|
||||
* @param IntimacySource $source 来源枚举
|
||||
* @param string $remark 备注
|
||||
* @param bool $skipCap 是否跳过每日上限(管理员调整/结婚初始时传 true)
|
||||
*/
|
||||
public function add(
|
||||
Marriage $marriage,
|
||||
int $amount,
|
||||
IntimacySource $source,
|
||||
string $remark = '',
|
||||
bool $skipCap = false,
|
||||
): void {
|
||||
if ($amount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 每日上限检查(管理员调整和结婚初始加成跳过)
|
||||
if (! $skipCap) {
|
||||
$cap = $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
<?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 $ringPurchaseId 使用的戒指购买记录 ID
|
||||
* @return array{ok: bool, message: string, marriage_id: int|null}
|
||||
*/
|
||||
public function propose(User $proposer, User $target, int $ringPurchaseId): array
|
||||
{
|
||||
// 不能向自己求婚
|
||||
if ($proposer->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class WeddingService
|
||||
{
|
||||
/**
|
||||
* Create a new class instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user