Files
chatroom/app/Services/MarriageIntimacyService.php
lkddi 2d07b032d9 功能:婚姻系统第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)
2026-03-01 15:03:34 +08:00

241 lines
7.8 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
/**
* 文件功能:婚姻亲密度操作服务
*
* 统一管理婚姻亲密度的增减逻辑:
* - 写入 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;
}
}