新增:个性装扮支持多份购买,同款续购自动叠加天数

This commit is contained in:
pllx
2026-04-28 13:07:10 +08:00
parent 243e06915e
commit a2b09da730
3 changed files with 77 additions and 20 deletions
+33 -10
View File
@@ -52,12 +52,14 @@ class DecorationService
* 购买装扮:扣金币、写购买记录、更新 users.active_decorations。
*
* 同槽位的旧装扮会被新购买覆盖(旧装扮不退款),不同槽位可并行持有。
* 若购买的是已激活的同款样式,则自动叠加天数而非覆盖重置。
*
* @param User $user 购买用户
* @param ShopItem $item 装扮商品
* @param int $quantity 购买份数
* @return array{ok:bool, message:string, balance_after?:int, slot?:string, style?:string, expires_at?:string}
*/
public function purchase(User $user, ShopItem $item): array
public function purchase(User $user, ShopItem $item, int $quantity = 1): array
{
// 根据商品类型映射到对应槽位
$slot = self::TYPE_TO_SLOT[$item->type] ?? null;
@@ -65,14 +67,29 @@ class DecorationService
return ['ok' => false, 'message' => '未知装扮类型'];
}
$totalPrice = $item->price * $quantity;
// 校验金币余额
if ($user->jjb < $item->price) {
return ['ok' => false, 'message' => "金币不足,购买 [{$item->name}] 需要 {$item->price} 金币,当前仅有 {$user->jjb} 金币。"];
if ($user->jjb < $totalPrice) {
return ['ok' => false, 'message' => "金币不足,购买 {$quantity} [{$item->name}] 需要 {$totalPrice} 金币,当前仅有 {$user->jjb} 金币。"];
}
// 计算过期时间(至少 1 天)
$days = max(1, (int) ($item->duration_days ?? 1));
$expiresAt = Carbon::now()->addDays($days);
$totalDays = $days * $quantity;
// 检查同一槽位是否已激活相同样式 → 叠加天数
$decorations = $this->getActiveDecorations($user);
$isSameActive = ! empty($decorations[$slot])
&& ($decorations[$slot]['style'] ?? '') === $item->slug;
if ($isSameActive) {
// 在现有到期时间上追加天数
$existingExpires = Carbon::parse($decorations[$slot]['expires_at']);
$expiresAt = $existingExpires->copy()->addDays($totalDays);
} else {
$expiresAt = Carbon::now()->addDays($totalDays);
}
// 按装扮类型使用不同的流水来源标识,便于后台按类型筛选消费记录
$source = match ($item->type) {
@@ -84,11 +101,11 @@ class DecorationService
};
// 事务包裹:扣金币、写购买记录、更新激活状态三步原子操作
DB::transaction(function () use ($user, $item, $slot, $days, $expiresAt, $source) {
DB::transaction(function () use ($user, $item, $slot, $totalPrice, $totalDays, $expiresAt, $source) {
// ① 通过统一积分服务扣除金币(含流水记录)
$this->currencyService->change(
$user, 'gold', -$item->price, $source,
"购买装扮:{$item->name}{$days}天)"
$user, 'gold', -$totalPrice, $source,
"购买装扮:{$item->name}{$totalDays}天)"
);
// ② 写入购买记录(用于后台统计与用户回溯)
@@ -96,11 +113,11 @@ class DecorationService
'user_id' => $user->id,
'shop_item_id' => $item->id,
'status' => 'active',
'price_paid' => $item->price,
'price_paid' => $totalPrice,
'expires_at' => $expiresAt,
]);
// ③ 更新用户 active_decorations JSON 字段(同槽位覆盖,不同槽位合并
// ③ 更新用户 active_decorations JSON 字段(同槽位合并,不同槽位追加
$decorations = $this->getActiveDecorations($user);
$decorations[$slot] = [
'style' => $item->slug,
@@ -113,13 +130,19 @@ class DecorationService
// 重新读取最新余额,避免缓存脏数据
$balanceAfter = (int) $user->fresh()->jjb;
// 计算叠加后的总天数显示(如果是续费,显示累计总天数)
$displayDays = $isSameActive
? (int) Carbon::now()->diffInDays(Carbon::parse($expiresAt), false) + 1
: $totalDays;
return [
'ok' => true,
'message' => "购买成功!{$item->icon} {$item->name} 已激活({$days}天有效)",
'message' => "购买成功!{$item->icon} {$item->name} 已激活({$displayDays}天有效)",
'balance_after' => $balanceAfter,
'slot' => $slot,
'style' => $item->slug,
'expires_at' => $expiresAt->toIso8601String(),
'quantity' => $quantity,
];
}
+4 -4
View File
@@ -49,10 +49,10 @@ class ShopService
'auto_fishing' => $this->buyAutoFishingCard($user, $item),
ShopItem::TYPE_SIGN_REPAIR => $this->buySignRepairCard($user, $item, $quantity),
// ── 个人装扮购买(委托给 DecorationService)───────────────
'msg_bubble' => $this->decorationService->purchase($user, $item),
'msg_name_color' => $this->decorationService->purchase($user, $item),
'msg_text_color' => $this->decorationService->purchase($user, $item),
'avatar_frame' => $this->decorationService->purchase($user, $item),
'msg_bubble' => $this->decorationService->purchase($user, $item, $quantity),
'msg_name_color' => $this->decorationService->purchase($user, $item, $quantity),
'msg_text_color' => $this->decorationService->purchase($user, $item, $quantity),
'avatar_frame' => $this->decorationService->purchase($user, $item, $quantity),
default => ['ok' => false, 'message' => '未知商品类型'],
};
}