改为独立座驾模块
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台座驾独立管理控制器。
|
||||
*
|
||||
* 提供座驾列表、新增、编辑、上下架切换与删除能力,不依赖商店商品模块。
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreRideRequest;
|
||||
use App\Http\Requests\UpdateRideRequest;
|
||||
use App\Models\Ride;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 后台座驾管理控制器
|
||||
* 负责独立 rides 表的后台管理流程。
|
||||
*/
|
||||
class RideController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示座驾管理列表页。
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$rides = Ride::query()
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
return view('admin.rides.index', compact('rides'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增座驾(仅 id=1 超级站长)。
|
||||
*/
|
||||
public function store(StoreRideRequest $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
Ride::create($data);
|
||||
|
||||
return redirect()->route('admin.rides.index')->with('success', '座驾「'.$data['name'].'」创建成功!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新座驾信息。
|
||||
*/
|
||||
public function update(UpdateRideRequest $request, Ride $ride): RedirectResponse
|
||||
{
|
||||
$ride->update($request->validated());
|
||||
|
||||
return redirect()->route('admin.rides.index')->with('success', '座驾「'.$ride->name.'」更新成功!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换座驾上下架状态。
|
||||
*/
|
||||
public function toggle(Ride $ride): RedirectResponse
|
||||
{
|
||||
$ride->update(['is_active' => ! $ride->is_active]);
|
||||
$status = $ride->is_active ? '上架' : '下架';
|
||||
|
||||
return redirect()->route('admin.rides.index')->with('success', "「{$ride->name}」已{$status}。");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除座驾(仅 id=1 超级站长)。
|
||||
*/
|
||||
public function destroy(Ride $ride): RedirectResponse
|
||||
{
|
||||
abort_unless(Auth::id() === 1, 403);
|
||||
|
||||
$name = $ride->name;
|
||||
$ride->delete();
|
||||
|
||||
return redirect()->route('admin.rides.index')->with('success', "「{$name}」已删除。");
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\BuyRideRequest;
|
||||
use App\Models\Ride;
|
||||
use App\Models\Room;
|
||||
use App\Models\ShopItem;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\RideService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -39,7 +39,7 @@ class RideController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'items' => $this->rideService->activeItems()
|
||||
->map(fn (ShopItem $item) => $this->rideService->formatItem($item))
|
||||
->map(fn (Ride $item) => $this->rideService->formatItem($item))
|
||||
->values(),
|
||||
'current_ride' => $this->rideService->formatCurrentRide($user),
|
||||
'purchases' => $this->rideService->purchaseRecords($user),
|
||||
@@ -60,7 +60,7 @@ class RideController extends Controller
|
||||
return response()->json(['status' => 'error', 'message' => '请先进入当前房间后再购买座驾。'], 403);
|
||||
}
|
||||
|
||||
$item = ShopItem::query()->findOrFail((int) $request->integer('item_id'));
|
||||
$item = Ride::query()->findOrFail((int) $request->integer('item_id'));
|
||||
$result = $this->rideService->buy($user, $item);
|
||||
|
||||
if (! $result['ok']) {
|
||||
|
||||
@@ -42,7 +42,6 @@ class ShopController extends Controller
|
||||
$user = Auth::user();
|
||||
$items = ShopItem::query()
|
||||
->where('is_active', true)
|
||||
->where('type', '!=', ShopItem::TYPE_RIDE)
|
||||
->orderBy('sort_order')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* 文件功能:前台座驾购买请求验证。
|
||||
*
|
||||
* 校验用户购买座驾时传入的商品与房间上下文,避免未进房直接购买聊天室座驾。
|
||||
* 校验用户购买座驾时传入的座驾与房间上下文,避免未进房直接购买聊天室座驾。
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
@@ -13,7 +13,7 @@ use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* 座驾购买请求
|
||||
* 负责校验座驾商品 ID 与当前房间 ID。
|
||||
* 负责校验座驾 ID 与当前房间 ID。
|
||||
*/
|
||||
class BuyRideRequest extends FormRequest
|
||||
{
|
||||
@@ -33,7 +33,7 @@ class BuyRideRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'item_id' => ['required', 'integer', 'exists:shop_items,id'],
|
||||
'item_id' => ['required', 'integer', 'exists:rides,id'],
|
||||
'room_id' => ['required', 'integer', 'exists:rooms,id'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台新增座驾请求验证。
|
||||
*
|
||||
* 校验座驾独立模块的名称、特效 key、价格、使用天数、欢迎语和上下架状态。
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* 后台新增座驾请求
|
||||
* 负责新增座驾时的权限与字段校验。
|
||||
*/
|
||||
class StoreRideRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* 判断当前用户是否允许新增座驾。
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->id === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取新增座驾验证规则。
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'slug' => ['required', 'string', 'max:100', 'regex:/^ride_[a-z0-9_]+$/', Rule::unique('rides', 'slug')],
|
||||
'effect_key' => ['required', 'string', 'max:50', 'regex:/^[a-z0-9_]+$/', Rule::unique('rides', 'effect_key')],
|
||||
'icon' => ['required', 'string', 'max:20'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'price' => ['required', 'integer', 'min:0'],
|
||||
'duration_days' => ['required', 'integer', 'min:1'],
|
||||
'welcome_message' => ['nullable', 'string', 'max:255'],
|
||||
'sort_order' => ['required', 'integer', 'min:0'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取新增座驾中文错误提示。
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'slug.regex' => '座驾标识必须使用 ride_ 开头,例如 ride_j35。',
|
||||
'effect_key.regex' => '特效 key 只能包含小写字母、数字和下划线。',
|
||||
'duration_days.min' => '使用天数至少为 1 天。',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* 文件功能:后台新增商店商品请求验证。
|
||||
*
|
||||
* 统一校验商店商品字段,包含座驾欢迎语字段。
|
||||
* 统一校验商店商品字段。
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
@@ -45,7 +45,6 @@ class StoreShopItemRequest extends FormRequest
|
||||
'duration_minutes' => ['nullable', 'integer', 'min:0'],
|
||||
'intimacy_bonus' => ['nullable', 'integer', 'min:0'],
|
||||
'charm_bonus' => ['nullable', 'integer', 'min:0'],
|
||||
'welcome_message' => ['nullable', 'string', 'max:255'],
|
||||
'sort_order' => ['required', 'integer', 'min:0'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
@@ -69,7 +68,6 @@ class StoreShopItemRequest extends FormRequest
|
||||
'msg_name_color',
|
||||
'msg_text_color',
|
||||
'avatar_frame',
|
||||
ShopItem::TYPE_RIDE,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台更新座驾请求验证。
|
||||
*
|
||||
* 校验座驾编辑时的唯一标识、价格、使用天数和欢迎语配置。
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* 后台更新座驾请求
|
||||
* 负责编辑座驾时的权限与字段校验。
|
||||
*/
|
||||
class UpdateRideRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* 判断当前用户是否允许编辑座驾。
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更新座驾验证规则。
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$ride = $this->route('ride');
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'slug' => ['required', 'string', 'max:100', 'regex:/^ride_[a-z0-9_]+$/', Rule::unique('rides', 'slug')->ignore($ride?->id)],
|
||||
'effect_key' => ['required', 'string', 'max:50', 'regex:/^[a-z0-9_]+$/', Rule::unique('rides', 'effect_key')->ignore($ride?->id)],
|
||||
'icon' => ['required', 'string', 'max:20'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'price' => ['required', 'integer', 'min:0'],
|
||||
'duration_days' => ['required', 'integer', 'min:1'],
|
||||
'welcome_message' => ['nullable', 'string', 'max:255'],
|
||||
'sort_order' => ['required', 'integer', 'min:0'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更新座驾中文错误提示。
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'slug.regex' => '座驾标识必须使用 ride_ 开头,例如 ride_j35。',
|
||||
'effect_key.regex' => '特效 key 只能包含小写字母、数字和下划线。',
|
||||
'duration_days.min' => '使用天数至少为 1 天。',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* 文件功能:后台更新商店商品请求验证。
|
||||
*
|
||||
* 统一校验商店商品编辑字段,包含座驾欢迎语字段。
|
||||
* 统一校验商店商品编辑字段。
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
@@ -47,7 +47,6 @@ class UpdateShopItemRequest extends FormRequest
|
||||
'duration_minutes' => ['nullable', 'integer', 'min:0'],
|
||||
'intimacy_bonus' => ['nullable', 'integer', 'min:0'],
|
||||
'charm_bonus' => ['nullable', 'integer', 'min:0'],
|
||||
'welcome_message' => ['nullable', 'string', 'max:255'],
|
||||
'sort_order' => ['required', 'integer', 'min:0'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
@@ -71,7 +70,6 @@ class UpdateShopItemRequest extends FormRequest
|
||||
'msg_name_color',
|
||||
'msg_text_color',
|
||||
'avatar_frame',
|
||||
ShopItem::TYPE_RIDE,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:聊天室座驾模型。
|
||||
*
|
||||
* 对应 rides 表,保存座驾名称、特效 key、价格、使用天数、欢迎语与上下架状态。
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* 聊天室座驾模型
|
||||
* 负责提供座驾定义、全屏特效 key 和购买记录关系。
|
||||
*/
|
||||
class Ride extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name', 'slug', 'effect_key', 'icon', 'description', 'price',
|
||||
'duration_days', 'welcome_message', 'sort_order', 'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取座驾对应的所有购买记录。
|
||||
*/
|
||||
public function purchases(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserRidePurchase::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取座驾全屏特效 key。
|
||||
*/
|
||||
public function rideKey(): string
|
||||
{
|
||||
return $this->effect_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有上架座驾。
|
||||
*
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function active(): Collection
|
||||
{
|
||||
return static::query()
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
+1
-23
@@ -15,8 +15,6 @@ class ShopItem extends Model
|
||||
{
|
||||
public const TYPE_SIGN_REPAIR = 'sign_repair';
|
||||
|
||||
public const TYPE_RIDE = 'ride';
|
||||
|
||||
public const DECORATION_TYPES = ['msg_bubble', 'msg_name_color', 'msg_text_color', 'avatar_frame'];
|
||||
|
||||
protected $table = 'shop_items';
|
||||
@@ -24,7 +22,7 @@ class ShopItem extends Model
|
||||
protected $fillable = [
|
||||
'name', 'slug', 'description', 'icon', 'price',
|
||||
'type', 'duration_days', 'duration_minutes', 'sort_order', 'is_active',
|
||||
'intimacy_bonus', 'charm_bonus', 'welcome_message',
|
||||
'intimacy_bonus', 'charm_bonus',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -63,14 +61,6 @@ class ShopItem extends Model
|
||||
return in_array($this->type, self::DECORATION_TYPES, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为聊天室座驾商品。
|
||||
*/
|
||||
public function isRide(): bool
|
||||
{
|
||||
return $this->type === self::TYPE_RIDE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为特效类商品(instant 或 duration,slug 以 once_ 或 week_ 开头)
|
||||
*/
|
||||
@@ -110,18 +100,6 @@ class ShopItem extends Model
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取座驾全屏特效 key(去掉 ride_ 前缀)。
|
||||
*/
|
||||
public function rideKey(): ?string
|
||||
{
|
||||
if (str_starts_with($this->slug, 'ride_')) {
|
||||
return substr($this->slug, 5);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有上架商品(按排序)
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户座驾购买记录模型。
|
||||
*
|
||||
* 对应 user_ride_purchases 表,追踪用户座驾购买、续期、替换和过期状态。
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 用户座驾购买记录模型
|
||||
* 负责连接用户与座驾,并判断当前记录是否仍有效。
|
||||
*/
|
||||
class UserRidePurchase extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id', 'ride_id', 'status', 'price_paid', 'expires_at', 'used_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
'used_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取购买记录所属用户。
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购买记录对应座驾。
|
||||
*/
|
||||
public function ride(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Ride::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断座驾购买记录是否仍然有效。
|
||||
*/
|
||||
public function isAlive(): bool
|
||||
{
|
||||
if ($this->status !== 'active') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->expires_at && $this->expires_at->isPast()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ShopItem;
|
||||
use App\Models\Ride;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPurchase;
|
||||
use App\Models\UserRidePurchase;
|
||||
use App\Support\ChatContentSanitizer;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -18,23 +18,18 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 聊天室座驾服务
|
||||
* 负责复用商店商品和用户购买记录完成座驾购买、续期、替换与进房展示。
|
||||
* 负责通过 rides 与 user_ride_purchases 完成座驾购买、续期、替换与进房展示。
|
||||
*/
|
||||
class RideService
|
||||
{
|
||||
/**
|
||||
* 获取全部上架座驾商品。
|
||||
*
|
||||
* @return Collection<int, ShopItem>
|
||||
* @return Collection<int, Ride>
|
||||
*/
|
||||
public function activeItems(): Collection
|
||||
{
|
||||
return ShopItem::query()
|
||||
->where('type', ShopItem::TYPE_RIDE)
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
return Ride::active();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +37,7 @@ class RideService
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function formatItem(ShopItem $item): array
|
||||
public function formatItem(Ride $item): array
|
||||
{
|
||||
return [
|
||||
'id' => $item->id,
|
||||
@@ -60,13 +55,12 @@ class RideService
|
||||
/**
|
||||
* 获取用户当前有效座驾,若已过期则自动标记为 expired。
|
||||
*/
|
||||
public function currentRide(User $user): ?UserPurchase
|
||||
public function currentRide(User $user): ?UserRidePurchase
|
||||
{
|
||||
$purchase = UserPurchase::query()
|
||||
->with('shopItem')
|
||||
$purchase = UserRidePurchase::query()
|
||||
->with('ride')
|
||||
->where('user_id', $user->id)
|
||||
->where('status', 'active')
|
||||
->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_RIDE))
|
||||
->orderByDesc('expires_at')
|
||||
->first();
|
||||
|
||||
@@ -92,7 +86,7 @@ class RideService
|
||||
public function formatCurrentRide(User $user): ?array
|
||||
{
|
||||
$purchase = $this->currentRide($user);
|
||||
if (! $purchase || ! $purchase->shopItem) {
|
||||
if (! $purchase || ! $purchase->ride) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -106,14 +100,13 @@ class RideService
|
||||
*/
|
||||
public function purchaseRecords(User $user, int $limit = 20): array
|
||||
{
|
||||
return UserPurchase::query()
|
||||
->with('shopItem')
|
||||
return UserRidePurchase::query()
|
||||
->with('ride')
|
||||
->where('user_id', $user->id)
|
||||
->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_RIDE))
|
||||
->latest()
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(fn (UserPurchase $purchase) => $this->formatPurchase($purchase))
|
||||
->map(fn (UserRidePurchase $purchase) => $this->formatPurchase($purchase))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
@@ -123,9 +116,9 @@ class RideService
|
||||
*
|
||||
* @return array{ok:bool, message:string, current_ride?:array<string, mixed>}
|
||||
*/
|
||||
public function buy(User $user, ShopItem $item): array
|
||||
public function buy(User $user, Ride $item): array
|
||||
{
|
||||
if (! $item->isRide() || ! $item->is_active) {
|
||||
if (! $item->is_active) {
|
||||
return ['ok' => false, 'message' => '该座驾暂未上架。'];
|
||||
}
|
||||
|
||||
@@ -142,35 +135,33 @@ class RideService
|
||||
$now = Carbon::now();
|
||||
|
||||
// 先清理已过期的 active 座驾,避免旧状态影响替换判断。
|
||||
UserPurchase::query()
|
||||
UserRidePurchase::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('status', 'active')
|
||||
->whereNotNull('expires_at')
|
||||
->where('expires_at', '<=', $now)
|
||||
->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_RIDE))
|
||||
->update(['status' => 'expired']);
|
||||
|
||||
$activeRide = UserPurchase::query()
|
||||
->with('shopItem')
|
||||
$activeRide = UserRidePurchase::query()
|
||||
->with('ride')
|
||||
->where('user_id', $user->id)
|
||||
->where('status', 'active')
|
||||
->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_RIDE))
|
||||
->orderByDesc('expires_at')
|
||||
->first();
|
||||
|
||||
// 座驾购买必须先扣金币,后续续期或替换都在同一个事务内完成。
|
||||
$user->decrement('jjb', $item->price);
|
||||
|
||||
if ($activeRide && (int) $activeRide->shop_item_id === (int) $item->id) {
|
||||
if ($activeRide && (int) $activeRide->ride_id === (int) $item->id) {
|
||||
$baseTime = $activeRide->expires_at && $activeRide->expires_at->greaterThan($now)
|
||||
? $activeRide->expires_at
|
||||
: $now;
|
||||
|
||||
// 同款续购先取消旧 active,再创建新 active,既保留购买记录,又保持当前座驾唯一。
|
||||
$activeRide->update(['status' => 'cancelled']);
|
||||
UserPurchase::create([
|
||||
UserRidePurchase::create([
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $item->id,
|
||||
'ride_id' => $item->id,
|
||||
'status' => 'active',
|
||||
'price_paid' => $item->price,
|
||||
'expires_at' => $baseTime->copy()->addDays($days),
|
||||
@@ -184,9 +175,9 @@ class RideService
|
||||
$activeRide->update(['status' => 'cancelled']);
|
||||
}
|
||||
|
||||
UserPurchase::create([
|
||||
UserRidePurchase::create([
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $item->id,
|
||||
'ride_id' => $item->id,
|
||||
'status' => 'active',
|
||||
'price_paid' => $item->price,
|
||||
'expires_at' => $now->copy()->addDays($days),
|
||||
@@ -208,7 +199,7 @@ class RideService
|
||||
public function buildPresencePayload(User $user): ?array
|
||||
{
|
||||
$purchase = $this->currentRide($user);
|
||||
$item = $purchase?->shopItem;
|
||||
$item = $purchase?->ride;
|
||||
$rideKey = $item?->rideKey();
|
||||
|
||||
if (! $purchase || ! $item || ! $rideKey) {
|
||||
@@ -234,9 +225,9 @@ class RideService
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatPurchase(UserPurchase $purchase): array
|
||||
private function formatPurchase(UserRidePurchase $purchase): array
|
||||
{
|
||||
$item = $purchase->shopItem;
|
||||
$item = $purchase->ride;
|
||||
|
||||
return [
|
||||
'id' => $purchase->id,
|
||||
|
||||
@@ -1,104 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:为商店商品加入聊天室座驾类型与欢迎语字段。
|
||||
* 文件功能:保留旧迁移文件名但不再扩展商店表。
|
||||
*
|
||||
* 座驾复用 shop_items 和 user_purchases,并预置当前四个军事主题座驾。
|
||||
* 座驾已经改为独立 rides 模块,本迁移保持空操作以避免新环境继续污染 shop_items。
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 方法功能:扩展商品类型、增加欢迎语字段并写入默认座驾。
|
||||
* 方法功能:不再向商店表添加座驾字段。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('shop_items', function (Blueprint $table) {
|
||||
$table->string('welcome_message', 255)->nullable()->after('charm_bonus')->comment('座驾入场欢迎语模板');
|
||||
});
|
||||
|
||||
if (DB::getDriverName() === 'mysql') {
|
||||
DB::statement("ALTER TABLE `shop_items` MODIFY `type` ENUM('instant','duration','one_time','ring','auto_fishing','sign_repair','msg_bubble','msg_name_color','avatar_frame','msg_text_color','ride') NOT NULL COMMENT '道具类型'");
|
||||
}
|
||||
|
||||
$rides = [
|
||||
[
|
||||
'name' => '歼-35隐身战机',
|
||||
'slug' => 'ride_j35',
|
||||
'description' => '驾驶歼-35划破长空入场,附带全屏战机掠过特效。',
|
||||
'icon' => '🛩️',
|
||||
'price' => 18888,
|
||||
'duration_days' => 7,
|
||||
'sort_order' => 80,
|
||||
'welcome_message' => '【{name}】驾驶【{ride}】划破长空,震撼降临聊天室!',
|
||||
],
|
||||
[
|
||||
'name' => '99A主战坦克',
|
||||
'slug' => 'ride_99a',
|
||||
'description' => '驾驶 99A 主战坦克重装入场,附带履带尘土与炮击冲击特效。',
|
||||
'icon' => '🛡️',
|
||||
'price' => 18888,
|
||||
'duration_days' => 7,
|
||||
'sort_order' => 81,
|
||||
'welcome_message' => '【{name}】驾驶【{ride}】重装入场,地面都为之一震!',
|
||||
],
|
||||
[
|
||||
'name' => '东风-5C战略导弹',
|
||||
'slug' => 'ride_df5c',
|
||||
'description' => '乘东风-5C 发射升空入场,附带尾焰、烟尘和雷达 HUD 特效。',
|
||||
'icon' => '🚀',
|
||||
'price' => 28888,
|
||||
'duration_days' => 7,
|
||||
'sort_order' => 82,
|
||||
'welcome_message' => '【{name}】乘【{ride}】点火升空,战略级排面拉满!',
|
||||
],
|
||||
[
|
||||
'name' => '福建舰航母',
|
||||
'slug' => 'ride_fujian',
|
||||
'description' => '乘福建舰破浪入场,附带海浪、舰载机和甲板 HUD 特效。',
|
||||
'icon' => '⚓',
|
||||
'price' => 28888,
|
||||
'duration_days' => 7,
|
||||
'sort_order' => 83,
|
||||
'welcome_message' => '【{name}】乘【{ride}】破浪而来,全场列队欢迎!',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($rides as $ride) {
|
||||
DB::table('shop_items')->updateOrInsert(
|
||||
['slug' => $ride['slug']],
|
||||
$ride + [
|
||||
'type' => 'ride',
|
||||
'duration_minutes' => 0,
|
||||
'intimacy_bonus' => 0,
|
||||
'charm_bonus' => 0,
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法功能:移除座驾默认商品并回滚字段与类型。
|
||||
* 方法功能:空迁移无需回滚结构。
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('shop_items')->whereIn('slug', ['ride_j35', 'ride_99a', 'ride_df5c', 'ride_fujian'])->delete();
|
||||
|
||||
if (DB::getDriverName() === 'mysql') {
|
||||
DB::statement("UPDATE `shop_items` SET `type` = 'one_time' WHERE `type` = 'ride'");
|
||||
DB::statement("ALTER TABLE `shop_items` MODIFY `type` ENUM('instant','duration','one_time','ring','auto_fishing','sign_repair','msg_bubble','msg_name_color','avatar_frame','msg_text_color') NOT NULL COMMENT '道具类型'");
|
||||
}
|
||||
|
||||
Schema::table('shop_items', function (Blueprint $table) {
|
||||
$table->dropColumn('welcome_message');
|
||||
});
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:创建聊天室座驾独立数据表。
|
||||
*
|
||||
* 座驾定义和用户座驾购买记录独立于商店模块,支持后台单独配置价格、使用天数和欢迎语。
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 方法功能:创建 rides 与 user_ride_purchases 并预置默认座驾。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('rides', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 100)->comment('座驾名称');
|
||||
$table->string('slug', 100)->unique()->comment('座驾唯一标识,格式 ride_key');
|
||||
$table->string('effect_key', 50)->unique()->comment('全屏特效 key');
|
||||
$table->string('icon', 20)->default('🚘')->comment('座驾图标');
|
||||
$table->text('description')->nullable()->comment('座驾说明');
|
||||
$table->unsignedInteger('price')->default(0)->comment('购买价格');
|
||||
$table->unsignedInteger('duration_days')->default(7)->comment('使用天数');
|
||||
$table->string('welcome_message', 255)->nullable()->comment('入场欢迎语模板');
|
||||
$table->unsignedInteger('sort_order')->default(0)->comment('排序权重');
|
||||
$table->boolean('is_active')->default(true)->comment('是否上架');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('user_ride_purchases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('ride_id')->constrained('rides')->cascadeOnDelete();
|
||||
$table->enum('status', ['active', 'expired', 'cancelled'])->default('active')->comment('座驾状态');
|
||||
$table->unsignedInteger('price_paid')->default(0)->comment('实际支付金币');
|
||||
$table->timestamp('expires_at')->nullable()->comment('到期时间');
|
||||
$table->timestamp('used_at')->nullable()->comment('首次使用时间');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'status', 'expires_at']);
|
||||
});
|
||||
|
||||
$this->seedDefaultRides();
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法功能:删除座驾独立数据表。
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_ride_purchases');
|
||||
Schema::dropIfExists('rides');
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法功能:写入当前默认四个全屏座驾。
|
||||
*/
|
||||
private function seedDefaultRides(): void
|
||||
{
|
||||
$now = now();
|
||||
$rides = [
|
||||
[
|
||||
'name' => '歼-35隐身战机',
|
||||
'slug' => 'ride_j35',
|
||||
'effect_key' => 'j35',
|
||||
'description' => '驾驶歼-35划破长空入场,附带全屏战机掠过特效。',
|
||||
'icon' => '🛩️',
|
||||
'price' => 18888,
|
||||
'duration_days' => 7,
|
||||
'sort_order' => 80,
|
||||
'welcome_message' => '【{name}】驾驶【{ride}】划破长空,震撼降临聊天室!',
|
||||
],
|
||||
[
|
||||
'name' => '99A主战坦克',
|
||||
'slug' => 'ride_99a',
|
||||
'effect_key' => '99a',
|
||||
'description' => '驾驶 99A 主战坦克重装入场,附带履带尘土与炮击冲击特效。',
|
||||
'icon' => '🛡️',
|
||||
'price' => 18888,
|
||||
'duration_days' => 7,
|
||||
'sort_order' => 81,
|
||||
'welcome_message' => '【{name}】驾驶【{ride}】重装入场,地面都为之一震!',
|
||||
],
|
||||
[
|
||||
'name' => '东风-5C战略导弹',
|
||||
'slug' => 'ride_df5c',
|
||||
'effect_key' => 'df5c',
|
||||
'description' => '乘东风-5C 发射升空入场,附带尾焰、烟尘和雷达 HUD 特效。',
|
||||
'icon' => '🚀',
|
||||
'price' => 28888,
|
||||
'duration_days' => 7,
|
||||
'sort_order' => 82,
|
||||
'welcome_message' => '【{name}】乘【{ride}】点火升空,战略级排面拉满!',
|
||||
],
|
||||
[
|
||||
'name' => '福建舰航母',
|
||||
'slug' => 'ride_fujian',
|
||||
'effect_key' => 'fujian',
|
||||
'description' => '乘福建舰破浪入场,附带海浪、舰载机和甲板 HUD 特效。',
|
||||
'icon' => '⚓',
|
||||
'price' => 28888,
|
||||
'duration_days' => 7,
|
||||
'sort_order' => 83,
|
||||
'welcome_message' => '【{name}】乘【{ride}】破浪而来,全场列队欢迎!',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($rides as $ride) {
|
||||
DB::table('rides')->insert($ride + [
|
||||
'is_active' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:清理旧版商店座驾数据。
|
||||
*
|
||||
* 如果环境曾跑过“座驾复用商店”的旧迁移,本迁移会移除商店里的座驾字段和残留记录。
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 方法功能:删除 shop_items 中旧座驾记录并移除欢迎语字段。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('shop_items')->where('slug', 'like', 'ride_%')->delete();
|
||||
|
||||
if (DB::getDriverName() === 'mysql' && $this->shopItemTypeContainsRide()) {
|
||||
DB::statement("UPDATE `shop_items` SET `type` = 'one_time' WHERE `type` = 'ride'");
|
||||
DB::statement("ALTER TABLE `shop_items` MODIFY `type` ENUM('instant','duration','one_time','ring','auto_fishing','sign_repair','msg_bubble','msg_name_color','avatar_frame','msg_text_color') NOT NULL COMMENT '道具类型'");
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('shop_items', 'welcome_message')) {
|
||||
Schema::table('shop_items', function (Blueprint $table) {
|
||||
$table->dropColumn('welcome_message');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法功能:清理迁移不恢复旧商店座驾结构。
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法功能:判断当前 shop_items.type 枚举是否包含旧座驾类型。
|
||||
*/
|
||||
private function shopItemTypeContainsRide(): bool
|
||||
{
|
||||
$column = DB::selectOne("SHOW COLUMNS FROM `shop_items` LIKE 'type'");
|
||||
|
||||
return $column && str_contains((string) $column->Type, "'ride'");
|
||||
}
|
||||
};
|
||||
@@ -1,31 +1,33 @@
|
||||
---
|
||||
name: chatroom-ride-development
|
||||
description: "开发 /Users/pllx/Web/Herd/chatroom 的聊天室座驾。适用于新增 ride_<key> 座驾商品、全屏 Canvas 特效、座驾音效、后台配置、前台座驾页面和进房欢迎语。"
|
||||
description: "开发 /Users/pllx/Web/Herd/chatroom 的聊天室座驾。适用于新增 ride_<key> 独立座驾、全屏 Canvas 特效、座驾音效、后台配置、前台座驾页面和进房欢迎语。"
|
||||
---
|
||||
|
||||
# Chatroom Ride Development
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 新增或修改聊天室座驾商品。
|
||||
- 新增或修改聊天室独立座驾。
|
||||
- 新增 `resources/js/effects/<key>.js` 全屏座驾特效。
|
||||
- 调整座驾购买、续期、替换、入场欢迎语或后台价格/天数配置。
|
||||
- 排查座驾进房后特效不播放、欢迎语不显示、购买记录不正确的问题。
|
||||
|
||||
## 必须遵守
|
||||
|
||||
- 座驾商品类型固定为 `ride`,商品 slug 固定为 `ride_<effect_key>`。
|
||||
- 座驾必须使用独立 `rides` 和 `user_ride_purchases`,不要写入 `shop_items` 或 `user_purchases`。
|
||||
- 座驾 slug 固定为 `ride_<effect_key>`。
|
||||
- `<effect_key>` 必须同时出现在:
|
||||
- `shop_items.slug`
|
||||
- `ShopItem::rideKey()` 可解析结果
|
||||
- `rides.slug`
|
||||
- `rides.effect_key`
|
||||
- `Ride::rideKey()` 可解析结果
|
||||
- `EffectBroadcast::TYPES`
|
||||
- `resources/js/effects/effect-manager.js`
|
||||
- `resources/js/effects/effect-sounds.js`
|
||||
- `resources/js/effects/<effect_key>.js`
|
||||
- 新增座驾默认通过迁移或 Seeder 写入 `shop_items`,字段至少包含名称、slug、图标、价格、`duration_days`、排序和 `welcome_message`。
|
||||
- 新增座驾默认通过迁移或 Seeder 写入 `rides`,字段至少包含名称、slug、`effect_key`、图标、价格、`duration_days`、排序和 `welcome_message`。
|
||||
- `welcome_message` 支持 `{name}` 和 `{ride}`,输出前必须转义,不能直接信任后台输入。
|
||||
- 当前版本只允许用户同时拥有一个 active 座驾;同款续购叠加有效期,不同款替换旧座驾并把旧记录置为 `cancelled`。
|
||||
- 不要另建座驾购买表;购买记录继续使用 `user_purchases`。
|
||||
- 用户购买记录必须写入 `user_ride_purchases`,不要复用商店购买记录。
|
||||
|
||||
## 新增座驾步骤
|
||||
|
||||
@@ -33,7 +35,7 @@ description: "开发 /Users/pllx/Web/Herd/chatroom 的聊天室座驾。适用
|
||||
2. 在 `effect-manager.js` 注册模块加载和启动分支。
|
||||
3. 在 `effect-sounds.js` 注册音效启动分支。
|
||||
4. 在 `EffectBroadcast::TYPES` 加入 `<effect_key>`。
|
||||
5. 在迁移或 Seeder 中新增 `shop_items` 记录,slug 使用 `ride_<effect_key>`。
|
||||
5. 在迁移或 Seeder 中新增 `rides` 记录,slug 使用 `ride_<effect_key>`,`effect_key` 使用 `<effect_key>`。
|
||||
6. 若后台预览需要,把按钮加入管理菜单预览区。
|
||||
7. 更新座驾相关 PHPUnit 测试,至少覆盖列表、购买、续期、替换和进房触发。
|
||||
|
||||
|
||||
@@ -84,6 +84,10 @@
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.shop.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
{!! '🛒 商店管理' !!}
|
||||
</a>
|
||||
<a href="{{ route('admin.rides.index') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.rides.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
🚘 座驾管理
|
||||
</a>
|
||||
<a href="{{ route('admin.marriages.index') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.marriages.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
{!! '💒 婚姻管理' !!}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
{{--
|
||||
文件功能:后台座驾独立管理页面
|
||||
支持查看、新增、编辑、上下架切换、删除座驾,以及配置价格、使用天数和欢迎语。
|
||||
--}}
|
||||
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', '🚘 座驾管理')
|
||||
|
||||
@section('content')
|
||||
@php require resource_path('views/admin/partials/list-theme.php'); @endphp
|
||||
|
||||
@php
|
||||
$isSuperAdmin = Auth::id() === 1;
|
||||
@endphp
|
||||
|
||||
<div x-data="{
|
||||
showForm: false,
|
||||
editing: null,
|
||||
form: {
|
||||
name: '',
|
||||
slug: 'ride_',
|
||||
effect_key: '',
|
||||
icon: '🚘',
|
||||
description: '',
|
||||
price: 1000,
|
||||
duration_days: 7,
|
||||
welcome_message: '',
|
||||
sort_order: 80,
|
||||
is_active: true,
|
||||
},
|
||||
|
||||
openCreate() {
|
||||
this.editing = null;
|
||||
this.form = {
|
||||
name: '',
|
||||
slug: 'ride_',
|
||||
effect_key: '',
|
||||
icon: '🚘',
|
||||
description: '',
|
||||
price: 1000,
|
||||
duration_days: 7,
|
||||
welcome_message: '',
|
||||
sort_order: 80,
|
||||
is_active: true,
|
||||
};
|
||||
this.showForm = true;
|
||||
this.$nextTick(() => this.$refs.nameInput?.focus());
|
||||
},
|
||||
|
||||
openEdit(ride) {
|
||||
this.editing = ride;
|
||||
this.form = {
|
||||
name: ride.name,
|
||||
slug: ride.slug,
|
||||
effect_key: ride.effect_key,
|
||||
icon: ride.icon,
|
||||
description: ride.description || '',
|
||||
price: ride.price,
|
||||
duration_days: ride.duration_days || 7,
|
||||
welcome_message: ride.welcome_message || '',
|
||||
sort_order: ride.sort_order,
|
||||
is_active: ride.is_active,
|
||||
};
|
||||
this.showForm = true;
|
||||
this.$nextTick(() => this.$refs.nameInput?.focus());
|
||||
},
|
||||
|
||||
closeForm() {
|
||||
this.showForm = false;
|
||||
this.editing = null;
|
||||
}
|
||||
}">
|
||||
<div class="{{ $adminListHeaderCardClass }} mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="{{ $adminListHeaderTitleClass }}">聊天室座驾列表</h2>
|
||||
<p class="{{ $adminListHeaderSubtitleClass }}">单独管理座驾价格、使用天数、入场欢迎语和全屏特效 key。</p>
|
||||
</div>
|
||||
@if ($isSuperAdmin)
|
||||
<button type="button" @click="openCreate()" class="{{ $adminListPrimaryButtonClass }}">
|
||||
+ 新增座驾
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="{{ $adminListCardClass }}">
|
||||
<table class="{{ $adminListTableClass }}">
|
||||
<thead class="{{ $adminListTableHeadRowClass }}">
|
||||
<tr>
|
||||
<th class="{{ $adminListTableHeadCellClass }}">座驾</th>
|
||||
<th class="{{ $adminListTableHeadCellClass }}">特效 Key</th>
|
||||
<th class="{{ $adminListTableHeadCellClass }} text-right">价格</th>
|
||||
<th class="{{ $adminListTableHeadCellClass }} text-center">使用天数</th>
|
||||
<th class="{{ $adminListTableHeadCellClass }} text-center">排序</th>
|
||||
<th class="{{ $adminListTableHeadCellClass }} text-center">状态</th>
|
||||
<th class="{{ $adminListTableHeadCellClass }} text-center">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="{{ $adminListTableBodyClass }}">
|
||||
@forelse ($rides as $ride)
|
||||
<tr class="{{ $adminListTableRowClass }} {{ $ride->is_active ? '' : 'opacity-50' }}">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-2xl leading-none">{{ $ride->icon }}</span>
|
||||
<div>
|
||||
<p class="{{ $adminListPrimaryTextClass }}">{{ $ride->name }}</p>
|
||||
<p class="{{ $adminListSecondaryTextClass }} font-mono">{{ $ride->slug }}</p>
|
||||
@if ($ride->description)
|
||||
<p class="mt-0.5 max-w-xs truncate text-xs text-gray-500" title="{{ $ride->description }}">
|
||||
{{ $ride->description }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 font-mono text-xs text-gray-600">{{ $ride->effect_key }}</td>
|
||||
<td class="px-4 py-3 text-right font-mono font-bold text-amber-600">
|
||||
{{ number_format($ride->price) }} 金
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-xs text-gray-500">{{ $ride->duration_days }} 天</td>
|
||||
<td class="px-4 py-3 text-center font-mono text-xs text-gray-400">{{ $ride->sort_order }}</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<form method="POST" action="{{ route('admin.rides.toggle', $ride) }}" class="inline">
|
||||
@csrf @method('PATCH')
|
||||
<button type="submit"
|
||||
class="rounded-full px-2.5 py-1 text-xs font-bold transition {{ $ride->is_active ? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200' : 'bg-gray-100 text-gray-500 hover:bg-gray-200' }}">
|
||||
{{ $ride->is_active ? '上架中' : '已下架' }}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button
|
||||
@click="openEdit({{ json_encode([
|
||||
'id' => $ride->id,
|
||||
'name' => $ride->name,
|
||||
'slug' => $ride->slug,
|
||||
'effect_key' => $ride->effect_key,
|
||||
'icon' => $ride->icon,
|
||||
'description' => $ride->description,
|
||||
'price' => $ride->price,
|
||||
'duration_days' => $ride->duration_days,
|
||||
'welcome_message' => $ride->welcome_message,
|
||||
'sort_order' => $ride->sort_order,
|
||||
'is_active' => (bool) $ride->is_active,
|
||||
], JSON_UNESCAPED_UNICODE) }})"
|
||||
class="{{ $adminListActionButtonClass }} text-indigo-600 hover:bg-indigo-50 hover:text-indigo-800">
|
||||
编辑
|
||||
</button>
|
||||
@if ($isSuperAdmin)
|
||||
<form method="POST" action="{{ route('admin.rides.destroy', $ride) }}"
|
||||
data-admin-confirm="确定要删除「{{ $ride->name }}」吗?此操作不可撤销!">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit"
|
||||
class="{{ $adminListActionButtonClass }} text-red-500 hover:bg-red-50 hover:text-red-700">
|
||||
删除
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="{{ $adminListEmptyClass }}">暂无座驾数据</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div x-show="showForm" x-cloak class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background: rgba(0,0,0,0.45);">
|
||||
<div @click.stop class="max-h-[90vh] w-full max-w-xl overflow-y-auto rounded-2xl bg-white shadow-2xl"
|
||||
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="translate-y-4 opacity-0"
|
||||
x-transition:enter-end="translate-y-0 opacity-100">
|
||||
<div class="flex items-center justify-between border-b px-6 py-4">
|
||||
<h3 class="text-lg font-bold text-gray-800" x-text="editing ? '编辑座驾:' + editing.name : '新增座驾'"></h3>
|
||||
<button type="button" @click="closeForm()" class="text-2xl leading-none text-gray-400 hover:text-gray-600">×</button>
|
||||
</div>
|
||||
|
||||
<form method="POST"
|
||||
:action="editing ? '{{ url('admin/rides') }}/' + editing.id : '{{ route('admin.rides.store') }}'"
|
||||
class="space-y-4 px-6 py-5">
|
||||
@csrf
|
||||
<template x-if="editing"><input type="hidden" name="_method" value="PUT"></template>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-2">
|
||||
<label class="mb-1 block text-xs font-semibold text-gray-600">座驾名称 <span class="text-red-500">*</span></label>
|
||||
<input x-ref="nameInput" type="text" name="name" x-model="form.name" required maxlength="100"
|
||||
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400">
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold text-gray-600">Slug <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="slug" x-model="form.slug" required maxlength="100"
|
||||
class="w-full rounded-lg border border-gray-300 px-3 py-2 font-mono text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400">
|
||||
<p class="mt-1 text-[11px] text-gray-500">格式:ride_j35、ride_df5c。</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold text-gray-600">特效 Key <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="effect_key" x-model="form.effect_key" required maxlength="50"
|
||||
class="w-full rounded-lg border border-gray-300 px-3 py-2 font-mono text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400">
|
||||
<p class="mt-1 text-[11px] text-gray-500">对应 resources/js/effects/<key>.js。</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold text-gray-600">图标 <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="icon" x-model="form.icon" required maxlength="20"
|
||||
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400">
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold text-gray-600">价格(金币)<span class="text-red-500">*</span></label>
|
||||
<input type="number" name="price" x-model="form.price" required min="0"
|
||||
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold text-gray-600">座驾描述</label>
|
||||
<textarea name="description" x-model="form.description" rows="2" maxlength="500"
|
||||
class="w-full resize-none rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold text-gray-600">使用天数 <span class="text-red-500">*</span></label>
|
||||
<input type="number" name="duration_days" x-model="form.duration_days" required min="1"
|
||||
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400">
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold text-gray-600">排序权重</label>
|
||||
<input type="number" name="sort_order" x-model="form.sort_order" min="0"
|
||||
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold text-gray-600">入场欢迎语句</label>
|
||||
<textarea name="welcome_message" x-model="form.welcome_message" rows="2" maxlength="255"
|
||||
placeholder="支持 {name} 用户名、{ride} 座驾名,例如:【{name}】驾驶【{ride}】震撼入场!"
|
||||
class="w-full resize-none rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="hidden" name="is_active" :value="form.is_active ? 1 : 0">
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<input type="checkbox" x-model="form.is_active" class="peer sr-only">
|
||||
<div class="h-5 w-10 rounded-full bg-gray-200 transition peer-checked:bg-indigo-500 peer-focus:ring-2 peer-focus:ring-indigo-300"></div>
|
||||
<div class="absolute left-0.5 top-0.5 h-4 w-4 rounded-full bg-white shadow transition peer-checked:translate-x-5"></div>
|
||||
</label>
|
||||
<span class="text-sm text-gray-600" x-text="form.is_active ? '上架显示' : '下架隐藏'"></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end gap-3 border-t pt-2">
|
||||
<button type="button" @click="closeForm()"
|
||||
class="rounded-lg border border-gray-300 px-5 py-2 text-sm text-gray-600 transition hover:bg-gray-50">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="rounded-lg bg-indigo-600 px-6 py-2 text-sm font-bold text-white shadow transition hover:bg-indigo-700">
|
||||
<span x-text="editing ? '保存修改' : '创建座驾'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -26,7 +26,6 @@
|
||||
'msg_name_color' => ['label' => '昵称颜色', 'color' => 'bg-pink-100 text-pink-700'],
|
||||
'msg_text_color' => ['label' => '文字颜色', 'color' => 'bg-cyan-100 text-cyan-700'],
|
||||
'avatar_frame' => ['label' => '头像框', 'color' => 'bg-amber-100 text-amber-700'],
|
||||
'ride' => ['label' => '聊天室座驾', 'color' => 'bg-slate-900 text-white'],
|
||||
];
|
||||
$isSuperAdmin = Auth::id() === 1;
|
||||
@endphp
|
||||
@@ -45,7 +44,6 @@
|
||||
duration_minutes: 0,
|
||||
intimacy_bonus: 0,
|
||||
charm_bonus: 0,
|
||||
welcome_message: '',
|
||||
sort_order: 0,
|
||||
is_active: true,
|
||||
},
|
||||
@@ -63,7 +61,6 @@
|
||||
duration_minutes: 0,
|
||||
intimacy_bonus: 0,
|
||||
charm_bonus: 0,
|
||||
welcome_message: '',
|
||||
sort_order: 0,
|
||||
is_active: true,
|
||||
};
|
||||
@@ -84,7 +81,6 @@
|
||||
duration_minutes: item.duration_minutes || 0,
|
||||
intimacy_bonus: item.intimacy_bonus || 0,
|
||||
charm_bonus: item.charm_bonus || 0,
|
||||
welcome_message: item.welcome_message || '',
|
||||
sort_order: item.sort_order,
|
||||
is_active: item.is_active,
|
||||
};
|
||||
@@ -196,7 +192,6 @@
|
||||
'duration_minutes' => $item->duration_minutes,
|
||||
'intimacy_bonus' => $item->intimacy_bonus,
|
||||
'charm_bonus' => $item->charm_bonus,
|
||||
'welcome_message' => $item->welcome_message,
|
||||
'sort_order' => $item->sort_order,
|
||||
'is_active' => (bool) $item->is_active,
|
||||
]) }})"
|
||||
@@ -300,19 +295,10 @@
|
||||
<option value="msg_name_color">msg_name_color — 昵称颜色</option>
|
||||
<option value="msg_text_color">msg_text_color — 文字颜色</option>
|
||||
<option value="avatar_frame">avatar_frame — 头像框</option>
|
||||
<option value="ride">ride — 聊天室座驾</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="form.type === 'ride'">
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">座驾欢迎语句</label>
|
||||
<textarea name="welcome_message" x-model="form.welcome_message" rows="2" maxlength="255"
|
||||
placeholder="支持 {name} 用户名、{ride} 座驾名,例如:【{name}】驾驶【{ride}】震撼入场!"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none resize-none"></textarea>
|
||||
<p class="mt-1 text-[11px] text-gray-500">仅座驾类型生效;不填写时使用系统默认欢迎语。</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">有效天数</label>
|
||||
|
||||
@@ -519,6 +519,11 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad
|
||||
Route::put('/shop/{shopItem}', [\App\Http\Controllers\Admin\ShopItemController::class, 'update'])->name('shop.update');
|
||||
Route::patch('/shop/{shopItem}/toggle', [\App\Http\Controllers\Admin\ShopItemController::class, 'toggle'])->name('shop.toggle');
|
||||
Route::delete('/shop/{shopItem}', [\App\Http\Controllers\Admin\ShopItemController::class, 'destroy'])->name('shop.destroy');
|
||||
Route::get('/rides', [\App\Http\Controllers\Admin\RideController::class, 'index'])->name('rides.index');
|
||||
Route::post('/rides', [\App\Http\Controllers\Admin\RideController::class, 'store'])->name('rides.store');
|
||||
Route::put('/rides/{ride}', [\App\Http\Controllers\Admin\RideController::class, 'update'])->name('rides.update');
|
||||
Route::patch('/rides/{ride}/toggle', [\App\Http\Controllers\Admin\RideController::class, 'toggle'])->name('rides.toggle');
|
||||
Route::delete('/rides/{ride}', [\App\Http\Controllers\Admin\RideController::class, 'destroy'])->name('rides.destroy');
|
||||
|
||||
// 💒 婚姻管理(superlevel 及以上)
|
||||
Route::prefix('marriages')->name('marriages.')->group(function () {
|
||||
|
||||
@@ -12,12 +12,12 @@ use App\Events\MessageSent;
|
||||
use App\Models\Department;
|
||||
use App\Models\Gift;
|
||||
use App\Models\Position;
|
||||
use App\Models\Ride;
|
||||
use App\Models\Room;
|
||||
use App\Models\ShopItem;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPosition;
|
||||
use App\Models\UserPurchase;
|
||||
use App\Models\UserRidePurchase;
|
||||
use App\Models\VipLevel;
|
||||
use App\Support\PositionPermissionRegistry;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
@@ -1104,20 +1104,20 @@ class ChatControllerTest extends TestCase
|
||||
{
|
||||
$room = Room::create(['room_name' => 'rideroom']);
|
||||
$user = User::factory()->create(['has_received_new_gift' => true]);
|
||||
$ride = ShopItem::query()->updateOrCreate(['slug' => 'ride_j35'], [
|
||||
$ride = Ride::query()->updateOrCreate(['slug' => 'ride_j35'], [
|
||||
'name' => '歼-35测试座驾',
|
||||
'effect_key' => 'j35',
|
||||
'description' => '测试座驾',
|
||||
'icon' => '🛩️',
|
||||
'price' => 18888,
|
||||
'type' => 'ride',
|
||||
'duration_days' => 7,
|
||||
'sort_order' => 80,
|
||||
'is_active' => true,
|
||||
'welcome_message' => '【{name}】驾驶【{ride}】震撼入场',
|
||||
]);
|
||||
UserPurchase::create([
|
||||
UserRidePurchase::create([
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $ride->id,
|
||||
'ride_id' => $ride->id,
|
||||
'status' => 'active',
|
||||
'price_paid' => 18888,
|
||||
'expires_at' => now()->addDays(3),
|
||||
@@ -1143,20 +1143,20 @@ class ChatControllerTest extends TestCase
|
||||
{
|
||||
$room = Room::create(['room_name' => '过期房']);
|
||||
$user = User::factory()->create(['has_received_new_gift' => true]);
|
||||
$ride = ShopItem::query()->updateOrCreate(['slug' => 'ride_df5c'], [
|
||||
$ride = Ride::query()->updateOrCreate(['slug' => 'ride_df5c'], [
|
||||
'name' => '过期座驾',
|
||||
'effect_key' => 'df5c',
|
||||
'description' => '测试座驾',
|
||||
'icon' => '🚀',
|
||||
'price' => 18888,
|
||||
'type' => 'ride',
|
||||
'duration_days' => 7,
|
||||
'sort_order' => 80,
|
||||
'is_active' => true,
|
||||
'welcome_message' => '【{name}】驾驶【{ride}】震撼入场',
|
||||
]);
|
||||
$purchase = UserPurchase::create([
|
||||
$purchase = UserRidePurchase::create([
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $ride->id,
|
||||
'ride_id' => $ride->id,
|
||||
'status' => 'active',
|
||||
'price_paid' => 18888,
|
||||
'expires_at' => now()->subMinute(),
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Models\Room;
|
||||
use App\Models\ShopItem;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPurchase;
|
||||
use App\Models\UserRidePurchase;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Tests\TestCase;
|
||||
@@ -41,9 +41,9 @@ class RideControllerTest extends TestCase
|
||||
$user = User::factory()->create();
|
||||
$ride = $this->createRide(['name' => '歼-35测试座驾', 'slug' => 'ride_j35']);
|
||||
|
||||
UserPurchase::create([
|
||||
UserRidePurchase::create([
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $ride->id,
|
||||
'ride_id' => $ride->id,
|
||||
'status' => 'active',
|
||||
'price_paid' => 18888,
|
||||
'expires_at' => now()->addDays(3),
|
||||
@@ -76,9 +76,9 @@ class RideControllerTest extends TestCase
|
||||
->assertJsonPath('status', 'error');
|
||||
|
||||
$this->assertSame(100, (int) $user->fresh()->jjb);
|
||||
$this->assertDatabaseMissing('user_purchases', [
|
||||
$this->assertDatabaseMissing('user_ride_purchases', [
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $ride->id,
|
||||
'ride_id' => $ride->id,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -102,9 +102,9 @@ class RideControllerTest extends TestCase
|
||||
->assertJsonPath('current_ride.item.slug', 'ride_99a')
|
||||
->assertJsonPath('jjb', 11112);
|
||||
|
||||
$this->assertDatabaseHas('user_purchases', [
|
||||
$this->assertDatabaseHas('user_ride_purchases', [
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $ride->id,
|
||||
'ride_id' => $ride->id,
|
||||
'status' => 'active',
|
||||
'price_paid' => 18888,
|
||||
]);
|
||||
@@ -120,9 +120,9 @@ class RideControllerTest extends TestCase
|
||||
$this->joinRoom($user, $room);
|
||||
$ride = $this->createRide(['price' => 1000, 'duration_days' => 7]);
|
||||
|
||||
$purchase = UserPurchase::create([
|
||||
$purchase = UserRidePurchase::create([
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $ride->id,
|
||||
'ride_id' => $ride->id,
|
||||
'status' => 'active',
|
||||
'price_paid' => 1000,
|
||||
'expires_at' => now()->addDays(2),
|
||||
@@ -136,14 +136,14 @@ class RideControllerTest extends TestCase
|
||||
$response->assertOk();
|
||||
|
||||
$this->assertSame('cancelled', $purchase->fresh()->status);
|
||||
$activePurchase = UserPurchase::query()
|
||||
$activePurchase = UserRidePurchase::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('shop_item_id', $ride->id)
|
||||
->where('ride_id', $ride->id)
|
||||
->where('status', 'active')
|
||||
->firstOrFail();
|
||||
$this->assertSame(1000, (int) $activePurchase->price_paid);
|
||||
$this->assertTrue($activePurchase->expires_at->greaterThan(now()->addDays(8)));
|
||||
$this->assertSame(1, UserPurchase::query()->where('user_id', $user->id)->where('status', 'active')->count());
|
||||
$this->assertSame(1, UserRidePurchase::query()->where('user_id', $user->id)->where('status', 'active')->count());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,9 +157,9 @@ class RideControllerTest extends TestCase
|
||||
$oldRide = $this->createRide(['slug' => 'ride_j35', 'price' => 1000]);
|
||||
$newRide = $this->createRide(['slug' => 'ride_df5c', 'price' => 2000]);
|
||||
|
||||
$oldPurchase = UserPurchase::create([
|
||||
$oldPurchase = UserRidePurchase::create([
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $oldRide->id,
|
||||
'ride_id' => $oldRide->id,
|
||||
'status' => 'active',
|
||||
'price_paid' => 1000,
|
||||
'expires_at' => now()->addDays(3),
|
||||
@@ -174,66 +174,78 @@ class RideControllerTest extends TestCase
|
||||
->assertJsonPath('current_ride.item.slug', 'ride_df5c');
|
||||
|
||||
$this->assertSame('cancelled', $oldPurchase->fresh()->status);
|
||||
$this->assertDatabaseHas('user_purchases', [
|
||||
$this->assertDatabaseHas('user_ride_purchases', [
|
||||
'user_id' => $user->id,
|
||||
'shop_item_id' => $newRide->id,
|
||||
'ride_id' => $newRide->id,
|
||||
'status' => 'active',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试后台商店可以保存座驾类型和欢迎语字段。
|
||||
* 测试后台座驾独立模块可以保存欢迎语字段。
|
||||
*/
|
||||
public function test_admin_can_store_ride_with_welcome_message(): void
|
||||
{
|
||||
$admin = User::factory()->create(['id' => 1, 'user_level' => 100]);
|
||||
|
||||
$response = $this->actingAs($admin)->post(route('admin.shop.store'), [
|
||||
$response = $this->actingAs($admin)->post(route('admin.rides.store'), [
|
||||
'name' => '测试座驾',
|
||||
'slug' => 'ride_test',
|
||||
'effect_key' => 'test',
|
||||
'icon' => '🚘',
|
||||
'description' => '测试座驾说明',
|
||||
'price' => 12345,
|
||||
'type' => 'ride',
|
||||
'duration_days' => 7,
|
||||
'duration_minutes' => 0,
|
||||
'intimacy_bonus' => 0,
|
||||
'charm_bonus' => 0,
|
||||
'welcome_message' => '【{name}】驾驶【{ride}】入场',
|
||||
'sort_order' => 99,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('admin.shop.index'));
|
||||
$this->assertDatabaseHas('shop_items', [
|
||||
$response->assertRedirect(route('admin.rides.index'));
|
||||
$this->assertDatabaseHas('rides', [
|
||||
'slug' => 'ride_test',
|
||||
'type' => 'ride',
|
||||
'effect_key' => 'test',
|
||||
'welcome_message' => '【{name}】驾驶【{ride}】入场',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试用座驾商品。
|
||||
* 测试后台座驾管理页面可以显示独立座驾数据。
|
||||
*/
|
||||
public function test_admin_can_view_ride_management_page(): void
|
||||
{
|
||||
$admin = User::factory()->create(['id' => 1, 'user_level' => 100]);
|
||||
$ride = $this->createRide(['name' => '后台可见座驾', 'slug' => 'ride_admin_visible']);
|
||||
|
||||
$response = $this->actingAs($admin)->get(route('admin.rides.index'));
|
||||
|
||||
$response->assertOk()
|
||||
->assertSee('座驾管理')
|
||||
->assertSee($ride->name)
|
||||
->assertSee('ride_admin_visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试用独立座驾。
|
||||
*
|
||||
* @param array<string, mixed> $attributes 覆盖字段
|
||||
*/
|
||||
private function createRide(array $attributes = []): ShopItem
|
||||
private function createRide(array $attributes = []): Ride
|
||||
{
|
||||
$data = array_merge([
|
||||
'name' => '测试座驾',
|
||||
'slug' => 'ride_test_'.str()->random(8),
|
||||
'effect_key' => 'test_'.str()->random(8),
|
||||
'description' => '测试座驾说明',
|
||||
'icon' => '🚘',
|
||||
'price' => 1000,
|
||||
'type' => 'ride',
|
||||
'duration_days' => 7,
|
||||
'duration_minutes' => 0,
|
||||
'sort_order' => 80,
|
||||
'is_active' => true,
|
||||
'welcome_message' => '【{name}】驾驶【{ride}】入场',
|
||||
], $attributes);
|
||||
|
||||
return ShopItem::query()->updateOrCreate(
|
||||
return Ride::query()->updateOrCreate(
|
||||
['slug' => $data['slug']],
|
||||
$data,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user