变更:修复求婚同意消息未收到问题,重构求婚流程支持直接选婚礼档位

This commit is contained in:
2026-03-01 17:53:43 +08:00
parent b7ded61523
commit 52c252f525
5 changed files with 162 additions and 36 deletions
+2 -1
View File
@@ -111,6 +111,7 @@ class MarriageController extends Controller
$data = $request->validate([
'target_username' => 'required|string',
'ring_purchase_id' => 'required|integer',
'wedding_tier_id' => 'nullable|integer',
]);
$proposer = $request->user();
@@ -120,7 +121,7 @@ class MarriageController extends Controller
return response()->json(['ok' => false, 'message' => '用户不存在。'], 404);
}
$result = $this->marriage->propose($proposer, $target, $data['ring_purchase_id']);
$result = $this->marriage->propose($proposer, $target, $data['ring_purchase_id'], $data['wedding_tier_id'] ?? null);
if ($result['ok']) {
$marriage = Marriage::find($result['marriage_id']);
+46 -5
View File
@@ -37,10 +37,10 @@ class MarriageService
*
* @param User $proposer 求婚方
* @param User $target 被求婚方
* @param int $ringPurchaseId 使用的戒指购买记录 ID
* @param int $weddingTierId 可选的婚礼档位 ID
* @return array{ok: bool, message: string, marriage_id: int|null}
*/
public function propose(User $proposer, User $target, int $ringPurchaseId): array
public function propose(User $proposer, User $target, int $ringPurchaseId, ?int $weddingTierId = null): array
{
// 不能向自己求婚
if ($proposer->id === $target->id) {
@@ -85,7 +85,17 @@ class MarriageService
$expireHours = $this->config->get('proposal_expire_hours', 48);
return DB::transaction(function () use ($proposer, $target, $ring, $expireHours) {
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']);
@@ -103,6 +113,15 @@ class MarriageService
'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];
});
}
@@ -161,6 +180,15 @@ class MarriageService
// 初始亲密度
$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' => '恭喜!你们已正式结婚!'];
@@ -184,9 +212,16 @@ class MarriageService
return ['ok' => false, 'message' => '无权操作此求婚。'];
}
DB::transaction(function () use ($marriage) {
return DB::transaction(function () use ($marriage) {
$marriage->update(['status' => 'rejected']);
// 戒指消失(lost = 不退还)
// 检查是否有由于求婚冻结的婚礼金币,若有则退还
$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) {
@@ -343,6 +378,12 @@ class MarriageService
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);
}
});
}
+60
View File
@@ -144,6 +144,66 @@ class WeddingService
});
}
// ──────────────────────────── 婚礼生命周期钩子 ───────────────────
/**
* 将预先设置好的定时婚礼(因求婚冻结)转为即刻开始(解冻并记录消费)。
*
* @param WeddingCeremony $ceremony
*/
public function confirmCeremony(WeddingCeremony $ceremony): void
{
DB::transaction(function () use ($ceremony) {
$marriage = $ceremony->marriage;
$tierName = $ceremony->tier?->name ?? '婚礼';
if ($ceremony->ceremony_type === 'scheduled') {
// 解除冻结,正式扣款记账
if ($ceremony->groom_amount > 0) {
$groom = clone $marriage->user;
$marriage->user->decrement('frozen_jjb', $ceremony->groom_amount);
$this->currency->change($groom, 'gold', 0, CurrencySource::WEDDING_ENV_SEND, "求婚成功,正式发红包({$tierName}");
}
if ($ceremony->partner_amount > 0) {
$partner = clone $marriage->partner;
$marriage->partner->decrement('frozen_jjb', $ceremony->partner_amount);
$this->currency->change($partner, 'gold', 0, CurrencySource::WEDDING_ENV_SEND, "求婚成功,正式发红包({$tierName}");
}
// 将类型转为即时开始
$ceremony->update([
'ceremony_type' => 'immediate',
'ceremony_at' => now(),
'expires_at' => now()->addHours($this->config->get('envelope_expire_hours', 24))
]);
}
});
}
/**
* 撤销由于求婚设置的婚礼,并且解冻/退还因为该婚礼冻结的金币。
*
* @param WeddingCeremony $ceremony
*/
public function cancelAndRefund(WeddingCeremony $ceremony): void
{
DB::transaction(function () use ($ceremony) {
$ceremony->update(['status' => 'cancelled']);
if ($ceremony->ceremony_type === 'scheduled') {
$marriage = $ceremony->marriage;
if ($ceremony->groom_amount > 0) {
$marriage->user?->decrement('frozen_jjb', $ceremony->groom_amount);
$marriage->user?->increment('jjb', $ceremony->groom_amount);
}
if ($ceremony->partner_amount > 0) {
$marriage->partner?->decrement('frozen_jjb', $ceremony->partner_amount);
$marriage->partner?->increment('jjb', $ceremony->partner_amount);
}
}
});
}
// ──────────────────────────── 触发婚礼 ────────────────────────────
/**
+3 -3
View File
@@ -77,19 +77,19 @@ export function initChat(roomId) {
);
})
// ─── 婚姻系统:全局事件(广播给整个房间) ────────────────
.listen("MarriageAccepted", (e) => {
.listen(".marriage.accepted", (e) => {
console.log("结婚公告:", e);
window.dispatchEvent(
new CustomEvent("chat:marriage-accepted", { detail: e }),
);
})
.listen("MarriageDivorced", (e) => {
.listen(".marriage.divorced", (e) => {
console.log("离婚公告:", e);
window.dispatchEvent(
new CustomEvent("chat:marriage-divorced", { detail: e }),
);
})
.listen("WeddingCelebration", (e) => {
.listen(".wedding.celebration", (e) => {
console.log("婚礼庆典:", e);
window.dispatchEvent(
new CustomEvent("chat:wedding-celebration", { detail: e }),
@@ -114,36 +114,46 @@
</template>
</div>
{{-- ── 婚礼费用提示面板 ── --}}
{{-- ── 婚礼档位选择与费用提示面板 ── --}}
@php
$minWedding = (int) \App\Models\WeddingTier::where('is_active', true)
->orderBy('amount')
->value('amount');
$activeTiers = \App\Models\WeddingTier::where('is_active', true)->orderBy('amount')->get();
@endphp
@if ($minWedding > 0)
<div style="margin-bottom:14px;">
@php $canAfford = ($user->jjb >= $minWedding); @endphp
<div style="margin-bottom:14px; text-align:left;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
<div
style="border-radius:12px; padding:12px 14px; font-size:12px; line-height:1.7;
background:{{ $canAfford ? '#f0fdf4' : '#fef2f2' }};
border:1.5px solid {{ $canAfford ? '#bbf7d0' : '#fecaca' }};">
<div
style="font-weight:700; color:{{ $canAfford ? '#15803d' : '#dc2626' }}; margin-bottom:4px;">
{{ $canAfford ? '✅ 您的金币可以举办婚礼' : '⚠️ 金币可能不足以举办婚礼' }}
</div>
<div style="color:#6b7280;">
结婚最低花费:<strong style="color:#f43f5e;">🪙 {{ number_format($minWedding) }}</strong>
金币
</div>
<div style="color:#{{ $canAfford ? '15803d' : 'dc2626' }};">
当前余额:<strong>🪙 {{ number_format($user->jjb) }}</strong> 金币
</div>
@if (!$canAfford)
<div style="color:#9ca3af; font-size:11px; margin-top:4px;">可先求婚,婚礼设置时再准备金币</div>
@endif
style="width:3px; height:14px; background:linear-gradient(#f59e0b,#d97706); border-radius:2px;">
</div>
<span style="font-size:12px; font-weight:700; color:#4b5563;">预设婚礼档位</span>
</div>
<select x-model="selectedTierId"
style="width:100%; padding:8px 10px; border-radius:8px; border:1px solid #d1d5db; background:#fff; font-size:13px; color:#1f2937; margin-bottom:10px;">
<option value="">(不举办撒红包婚礼)</option>
<template x-for="tier in tiers" :key="tier.id">
<option :value="tier.id" x-text="`${tier.icon} ${tier.name} (🪙 ${tier.amount})`">
</option>
</template>
</select>
<div x-show="selectedTier" x-transition
style="border-radius:12px; padding:12px 14px; font-size:12px; line-height:1.7; transition:all .2s;"
:style="canAfford ? 'background:#f0fdf4; border:1.5px solid #bbf7d0;' :
'background:#fef2f2; border:1.5px solid #fecaca;'">
<div style="font-weight:700; margin-bottom:4px;"
:style="canAfford ? 'color:#15803d' : 'color:#dc2626'"
x-text="canAfford ? '✅ 您的金币足以预定该婚礼' : '⚠️ 金币不足,请降低档位或准备金币'">
</div>
<div style="color:#6b7280;">
婚礼预冻结:<strong style="color:#f43f5e;"
x-text="'🪙 ' + (selectedTier ? Number(selectedTier.amount).toLocaleString() : 0)"></strong>
金币
</div>
<div :style="canAfford ? 'color:#15803d' : 'color:#dc2626'">
当前余额:<strong>🪙 {{ number_format($user->jjb) }}</strong> 金币
</div>
<div style="color:#9ca3af; font-size:11px; margin-top:4px;">需男方独自承担预冻结金币,对方同意后即刻举行。被拒则全额退回!
</div>
</div>
@endif
</div>
{{-- 底部按钮:样式修复并参照大卡片弹窗 --}}
<div style="display:flex; gap:12px;">
@@ -154,9 +164,10 @@
onmouseover="this.style.background='#e2e8f0'" onmouseout="this.style.background='#f1f5f9'">
取消
</button>
<button x-on:click="doPropose()" :disabled="sending || !selectedRing || rings.length === 0"
<button x-on:click="doPropose()"
:disabled="sending || !selectedRing || rings.length === 0 || !canAfford"
style="flex:1; padding:10px 0; border-radius:8px; font-size:13px; font-weight:bold; border:none; transition:all .2s;"
:style="(sending || !selectedRing || rings.length === 0) ?
:style="(sending || !selectedRing || rings.length === 0 || !canAfford) ?
{ background: '#f1f5f9', color: '#94a3b8', cursor: 'not-allowed', boxShadow: 'none' } :
{ background: 'linear-gradient(135deg,#be185d,#f43f5e,#ec4899)', color: '#fff',
cursor: 'pointer', boxShadow: '0 4px 12px rgba(244,63,94,0.3)' }">
@@ -457,10 +468,22 @@
marriageId: null, // 当前对方婚姻/求婚记录 IDaccept/reject 用)
rings: [],
selectedRing: null,
tiers: @json(\App\Models\WeddingTier::where('is_active', true)->orderBy('amount')->get()),
selectedTierId: '',
loading: false,
sending: false,
error: '',
get selectedTier() {
if (!this.selectedTierId) return null;
return this.tiers.find(t => t.id == this.selectedTierId);
},
get canAfford() {
const amount = this.selectedTier ? Number(this.selectedTier.amount) : 0;
return window.chatContext.userJjb >= amount;
},
async open(username) {
this.targetUsername = username;
this.selectedRing = null;
@@ -520,6 +543,7 @@
body: JSON.stringify({
target_username: this.targetUsername,
ring_purchase_id: this.selectedRing,
wedding_tier_id: this.selectedTierId || null,
room_id: window.chatContext.roomId
})
});