mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-14 11:20:53 +08:00
feat: multiple improvements and bug fixes
- Add gift card redemption feature - Resolve custom range selection issue in overview - Allow log page size to be modified - Add subscription path change notification - Improve dynamic node rate feature - Support markdown documentation display for plugins - Reduce power reset service logging - Fix backend version number not updating after update
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\TrafficResetService;
|
||||
use App\Utils\Helper;
|
||||
use Carbon\Carbon;
|
||||
@@ -54,8 +55,6 @@ class ResetTraffic extends Command
|
||||
// 设置最大执行时间
|
||||
set_time_limit($maxTime);
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
if ($dryRun) {
|
||||
$result = $this->performDryRun($batchSize);
|
||||
@@ -129,7 +128,7 @@ class ResetTraffic extends Command
|
||||
{
|
||||
$this->info("🔍 扫描需要重置的用户...");
|
||||
|
||||
$totalUsers = \App\Models\User::where('next_reset_at', '<=', time())
|
||||
$totalUsers = User::where('next_reset_at', '<=', time())
|
||||
->whereNotNull('next_reset_at')
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>', time())
|
||||
@@ -160,7 +159,7 @@ class ResetTraffic extends Command
|
||||
|
||||
// 显示前几个用户的详情作为示例
|
||||
if ($this->option('verbose') || $totalUsers <= 20) {
|
||||
$sampleUsers = \App\Models\User::where('next_reset_at', '<=', time())
|
||||
$sampleUsers = User::where('next_reset_at', '<=', time())
|
||||
->whereNotNull('next_reset_at')
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>', time())
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\UpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class XboardUpdate extends Command
|
||||
@@ -39,9 +41,11 @@ class XboardUpdate extends Command
|
||||
public function handle()
|
||||
{
|
||||
$this->info('正在导入数据库请稍等...');
|
||||
\Artisan::call("migrate");
|
||||
$this->info(\Artisan::output());
|
||||
\Artisan::call('horizon:terminate');
|
||||
Artisan::call("migrate");
|
||||
$this->info(Artisan::output());
|
||||
Artisan::call('horizon:terminate');
|
||||
$updateService = new UpdateService();
|
||||
$updateService->updateVersionCache();
|
||||
$this->info('更新完毕,队列服务已重启,你无需进行任何操作。');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ trait ApiResponse
|
||||
* @param array $codeResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function success($data = null, $codeResponse=ResponseEnum::HTTP_OK): JsonResponse
|
||||
public function success($data = null, $codeResponse = ResponseEnum::HTTP_OK): JsonResponse
|
||||
{
|
||||
return $this->jsonResponse('success', $codeResponse, $data, null);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ trait ApiResponse
|
||||
* @param mixed $error
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function fail($codeResponse=ResponseEnum::HTTP_ERROR, $data = null, $error=null): JsonResponse
|
||||
public function fail($codeResponse = ResponseEnum::HTTP_ERROR, $data = null, $error = null): JsonResponse
|
||||
{
|
||||
return $this->jsonResponse('fail', $codeResponse, $data, $error);
|
||||
}
|
||||
@@ -46,49 +46,24 @@ trait ApiResponse
|
||||
list($code, $message) = $codeResponse;
|
||||
return response()
|
||||
->json([
|
||||
'status' => $status,
|
||||
// 'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data ?? null,
|
||||
'error' => $error,
|
||||
],(int)substr(((string) $code),0,3));
|
||||
'status' => $status,
|
||||
// 'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data ?? null,
|
||||
'error' => $error,
|
||||
], (int) substr(((string) $code), 0, 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功分页返回
|
||||
* @param $page
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected function successPaginate($page): JsonResponse
|
||||
{
|
||||
return $this->success($this->paginate($page));
|
||||
}
|
||||
|
||||
private function paginate($page)
|
||||
public function paginate(LengthAwarePaginator $page)
|
||||
{
|
||||
if ($page instanceof LengthAwarePaginator){
|
||||
return [
|
||||
'total' => $page->total(),
|
||||
'page' => $page->currentPage(),
|
||||
'limit' => $page->perPage(),
|
||||
'pages' => $page->lastPage(),
|
||||
'list' => $page->items()
|
||||
];
|
||||
}
|
||||
if ($page instanceof Collection){
|
||||
$page = $page->toArray();
|
||||
}
|
||||
if (!is_array($page) && !is_object($page)){
|
||||
return $page;
|
||||
}
|
||||
$total = count($page);
|
||||
return [
|
||||
'total' => $total, //数据总数
|
||||
'page' => 1, // 当前页码
|
||||
'limit' => $total, // 每页的数据条数
|
||||
'pages' => 1, // 最后一页的页码
|
||||
'list' => $page // 数据
|
||||
];
|
||||
return response()->json([
|
||||
'total' => $page->total(),
|
||||
'current_page' => $page->currentPage(),
|
||||
'per_page' => $page->perPage(),
|
||||
'last_page' => $page->lastPage(),
|
||||
'data' => $page->items()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +72,7 @@ trait ApiResponse
|
||||
* @param string $info
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function throwBusinessException(array $codeResponse=ResponseEnum::HTTP_ERROR, string $info = '')
|
||||
public function throwBusinessException(array $codeResponse = ResponseEnum::HTTP_ERROR, string $info = '')
|
||||
{
|
||||
throw new BusinessException($codeResponse, $info);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class ShadowsocksTidalabController extends Controller
|
||||
foreach ($data as $item) {
|
||||
$formatData[$item['user_id']] = [$item['u'], $item['d']];
|
||||
}
|
||||
$userService->trafficFetch($server->toArray(), 'shadowsocks', $formatData);
|
||||
$userService->trafficFetch($server, 'shadowsocks', $formatData);
|
||||
|
||||
return response([
|
||||
'ret' => 1,
|
||||
|
||||
@@ -62,7 +62,7 @@ class TrojanTidalabController extends Controller
|
||||
foreach ($data as $item) {
|
||||
$formatData[$item['user_id']] = [$item['u'], $item['d']];
|
||||
}
|
||||
$userService->trafficFetch($server->toArray(), 'trojan', $formatData);
|
||||
$userService->trafficFetch($server, 'trojan', $formatData);
|
||||
|
||||
return response([
|
||||
'ret' => 1,
|
||||
|
||||
@@ -79,7 +79,7 @@ class UniProxyController extends Controller
|
||||
);
|
||||
|
||||
$userService = new UserService();
|
||||
$userService->trafficFetch($node->toArray(), $nodeType, $data);
|
||||
$userService->trafficFetch($node, $nodeType, $data);
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
|
||||
185
app/Http/Controllers/V1/User/GiftCardController.php
Normal file
185
app/Http/Controllers/V1/User/GiftCardController.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\GiftCardCheckRequest;
|
||||
use App\Http\Requests\User\GiftCardRedeemRequest;
|
||||
use App\Models\GiftCardUsage;
|
||||
use App\Services\GiftCardService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GiftCardController extends Controller
|
||||
{
|
||||
/**
|
||||
* 查询兑换码信息
|
||||
*/
|
||||
public function check(GiftCardCheckRequest $request)
|
||||
{
|
||||
try {
|
||||
$giftCardService = new GiftCardService($request->input('code'));
|
||||
$giftCardService->setUser($request->user());
|
||||
|
||||
// 1. 验证礼品卡本身是否有效 (如不存在、已过期、已禁用)
|
||||
$giftCardService->validateIsActive();
|
||||
|
||||
// 2. 检查用户是否满足使用条件,但不在此处抛出异常
|
||||
$eligibility = $giftCardService->checkUserEligibility();
|
||||
|
||||
// 3. 获取卡片信息和奖励预览
|
||||
$codeInfo = $giftCardService->getCodeInfo();
|
||||
$rewardPreview = $giftCardService->previewRewards();
|
||||
|
||||
return $this->success([
|
||||
'code_info' => $codeInfo, // 这里面已经包含 plan_info
|
||||
'reward_preview' => $rewardPreview,
|
||||
'can_redeem' => $eligibility['can_redeem'],
|
||||
'reason' => $eligibility['reason'],
|
||||
]);
|
||||
|
||||
} catch (ApiException $e) {
|
||||
// 这里只捕获 validateIsActive 抛出的异常
|
||||
return $this->fail([400, $e->getMessage()]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('礼品卡查询失败', [
|
||||
'code' => $request->input('code'),
|
||||
'user_id' => $request->user()->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '查询失败,请稍后重试']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用兑换码
|
||||
*/
|
||||
public function redeem(GiftCardRedeemRequest $request)
|
||||
{
|
||||
try {
|
||||
$giftCardService = new GiftCardService($request->input('code'));
|
||||
$giftCardService->setUser($request->user());
|
||||
$giftCardService->validate();
|
||||
|
||||
// 使用礼品卡
|
||||
$result = $giftCardService->redeem([
|
||||
// 'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
]);
|
||||
|
||||
Log::info('礼品卡使用成功', [
|
||||
'code' => $request->input('code'),
|
||||
'user_id' => $request->user()->id,
|
||||
'rewards' => $result['rewards'],
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'message' => '兑换成功!',
|
||||
'rewards' => $result['rewards'],
|
||||
'invite_rewards' => $result['invite_rewards'],
|
||||
'template_name' => $result['template_name'],
|
||||
]);
|
||||
|
||||
} catch (ApiException $e) {
|
||||
return $this->fail([400, $e->getMessage()]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('礼品卡使用失败', [
|
||||
'code' => $request->input('code'),
|
||||
'user_id' => $request->user()->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
return $this->fail([500, '兑换失败,请稍后重试']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户兑换记录
|
||||
*/
|
||||
public function history(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'page' => 'integer|min:1',
|
||||
'per_page' => 'integer|min:1|max:100',
|
||||
]);
|
||||
|
||||
$perPage = $request->input('per_page', 15);
|
||||
|
||||
$usages = GiftCardUsage::with(['template', 'code'])
|
||||
->where('user_id', $request->user()->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
$data = $usages->getCollection()->map(function ($usage) {
|
||||
return [
|
||||
'id' => $usage->id,
|
||||
'code' => substr($usage->code->code, 0, 8) . '****', // 脱敏处理
|
||||
'template_name' => $usage->template->name,
|
||||
'template_type' => $usage->template->type,
|
||||
'template_type_name' => $usage->template->type_name,
|
||||
'rewards_given' => $usage->rewards_given,
|
||||
'invite_rewards' => $usage->invite_rewards,
|
||||
'multiplier_applied' => $usage->multiplier_applied,
|
||||
'created_at' => $usage->created_at,
|
||||
];
|
||||
});
|
||||
$usages->setCollection($data);
|
||||
|
||||
return $this->paginate($usages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兑换记录详情
|
||||
*/
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_usage,id',
|
||||
]);
|
||||
|
||||
$usage = GiftCardUsage::with(['template', 'code', 'inviteUser'])
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('id', $request->input('id'))
|
||||
->first();
|
||||
|
||||
if (!$usage) {
|
||||
return $this->fail([404, '记录不存在']);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'id' => $usage->id,
|
||||
'code' => $usage->code->code,
|
||||
'template' => [
|
||||
'name' => $usage->template->name,
|
||||
'description' => $usage->template->description,
|
||||
'type' => $usage->template->type,
|
||||
'type_name' => $usage->template->type_name,
|
||||
'icon' => $usage->template->icon,
|
||||
'theme_color' => $usage->template->theme_color,
|
||||
],
|
||||
'rewards_given' => $usage->rewards_given,
|
||||
'invite_rewards' => $usage->invite_rewards,
|
||||
'invite_user' => $usage->inviteUser ? [
|
||||
'id' => $usage->inviteUser->id,
|
||||
'email' => substr($usage->inviteUser->email, 0, 3) . '***@***',
|
||||
] : null,
|
||||
'user_level_at_use' => $usage->user_level_at_use,
|
||||
'plan_id_at_use' => $usage->plan_id_at_use,
|
||||
'multiplier_applied' => $usage->multiplier_applied,
|
||||
// 'ip_address' => $usage->ip_address,
|
||||
'notes' => $usage->notes,
|
||||
'created_at' => $usage->created_at,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的礼品卡类型
|
||||
*/
|
||||
public function types(Request $request)
|
||||
{
|
||||
return $this->success([
|
||||
'types' => \App\Models\GiftCardTemplate::getTypeMap(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@ use App\Services\MailService;
|
||||
use App\Services\TelegramService;
|
||||
use App\Services\ThemeService;
|
||||
use App\Utils\Dict;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class ConfigController extends Controller
|
||||
|
||||
@@ -46,10 +46,7 @@ class CouponController extends Controller
|
||||
$coupons = $builder
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate($pageSize, ["*"], 'page', $current);
|
||||
return response([
|
||||
'data' => $coupons->items(),
|
||||
'total' => $coupons->total()
|
||||
]);
|
||||
return $this->paginate($coupons);
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
|
||||
641
app/Http/Controllers/V2/Admin/GiftCardController.php
Normal file
641
app/Http/Controllers/V2/Admin/GiftCardController.php
Normal file
@@ -0,0 +1,641 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GiftCardCode;
|
||||
use App\Models\GiftCardTemplate;
|
||||
use App\Models\GiftCardUsage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class GiftCardController extends Controller
|
||||
{
|
||||
/**
|
||||
* 获取礼品卡模板列表
|
||||
*/
|
||||
public function templates(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'type' => 'integer|min:1|max:10',
|
||||
'status' => 'integer|in:0,1',
|
||||
'page' => 'integer|min:1',
|
||||
'per_page' => 'integer|min:1|max:1000',
|
||||
]);
|
||||
|
||||
$query = GiftCardTemplate::query();
|
||||
|
||||
if ($request->has('type')) {
|
||||
$query->where('type', $request->input('type'));
|
||||
}
|
||||
|
||||
if ($request->has('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$templates = $query->orderBy('sort', 'asc')
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
$data = $templates->getCollection()->map(function ($template) {
|
||||
return [
|
||||
'id' => $template->id,
|
||||
'name' => $template->name,
|
||||
'description' => $template->description,
|
||||
'type' => $template->type,
|
||||
'type_name' => $template->type_name,
|
||||
'status' => $template->status,
|
||||
'conditions' => $template->conditions,
|
||||
'rewards' => $template->rewards,
|
||||
'limits' => $template->limits,
|
||||
'special_config' => $template->special_config,
|
||||
'icon' => $template->icon,
|
||||
'background_image' => $template->background_image,
|
||||
'theme_color' => $template->theme_color,
|
||||
'sort' => $template->sort,
|
||||
'admin_id' => $template->admin_id,
|
||||
'created_at' => $template->created_at,
|
||||
'updated_at' => $template->updated_at,
|
||||
// 统计信息
|
||||
'codes_count' => $template->codes()->count(),
|
||||
'used_count' => $template->usages()->count(),
|
||||
];
|
||||
});
|
||||
|
||||
$templates->setCollection($data);
|
||||
return $this->paginate($templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建礼品卡模板
|
||||
*/
|
||||
public function createTemplate(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'type' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::in(array_keys(GiftCardTemplate::getTypeMap()))
|
||||
],
|
||||
'status' => 'boolean',
|
||||
'conditions' => 'nullable|array',
|
||||
'rewards' => 'required|array',
|
||||
'limits' => 'nullable|array',
|
||||
'special_config' => 'nullable|array',
|
||||
'icon' => 'nullable|string|max:255',
|
||||
'background_image' => 'nullable|string|url|max:255',
|
||||
'theme_color' => 'nullable|string|regex:/^#[0-9A-Fa-f]{6}$/',
|
||||
'sort' => 'integer|min:0',
|
||||
], [
|
||||
'name.required' => '礼品卡名称不能为空',
|
||||
'type.required' => '礼品卡类型不能为空',
|
||||
'type.in' => '无效的礼品卡类型',
|
||||
'rewards.required' => '奖励配置不能为空',
|
||||
'theme_color.regex' => '主题色格式不正确',
|
||||
'background_image.url' => '背景图片必须是有效的URL',
|
||||
]);
|
||||
|
||||
try {
|
||||
$template = GiftCardTemplate::create([
|
||||
'name' => $request->input('name'),
|
||||
'description' => $request->input('description'),
|
||||
'type' => $request->input('type'),
|
||||
'status' => $request->input('status', true),
|
||||
'conditions' => $request->input('conditions'),
|
||||
'rewards' => $request->input('rewards'),
|
||||
'limits' => $request->input('limits'),
|
||||
'special_config' => $request->input('special_config'),
|
||||
'icon' => $request->input('icon'),
|
||||
'background_image' => $request->input('background_image'),
|
||||
'theme_color' => $request->input('theme_color', '#1890ff'),
|
||||
'sort' => $request->input('sort', 0),
|
||||
'admin_id' => $request->user()->id,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
|
||||
return $this->success($template);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('创建礼品卡模板失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'data' => $request->all(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '创建失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新礼品卡模板
|
||||
*/
|
||||
public function updateTemplate(Request $request)
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_template,id',
|
||||
'name' => 'sometimes|required|string|max:255',
|
||||
'description' => 'sometimes|nullable|string',
|
||||
'type' => [
|
||||
'sometimes',
|
||||
'required',
|
||||
'integer',
|
||||
Rule::in(array_keys(GiftCardTemplate::getTypeMap()))
|
||||
],
|
||||
'status' => 'sometimes|boolean',
|
||||
'conditions' => 'sometimes|nullable|array',
|
||||
'rewards' => 'sometimes|required|array',
|
||||
'limits' => 'sometimes|nullable|array',
|
||||
'special_config' => 'sometimes|nullable|array',
|
||||
'icon' => 'sometimes|nullable|string|max:255',
|
||||
'background_image' => 'sometimes|nullable|string|url|max:255',
|
||||
'theme_color' => 'sometimes|nullable|string|regex:/^#[0-9A-Fa-f]{6}$/',
|
||||
'sort' => 'sometimes|integer|min:0',
|
||||
]);
|
||||
|
||||
$template = GiftCardTemplate::find($validatedData['id']);
|
||||
if (!$template) {
|
||||
return $this->fail([404, '模板不存在']);
|
||||
}
|
||||
|
||||
try {
|
||||
$updateData = collect($validatedData)->except('id')->all();
|
||||
|
||||
if (empty($updateData)) {
|
||||
return $this->success($template);
|
||||
}
|
||||
|
||||
$updateData['updated_at'] = time();
|
||||
|
||||
$template->update($updateData);
|
||||
|
||||
return $this->success($template->fresh());
|
||||
} catch (\Exception $e) {
|
||||
Log::error('更新礼品卡模板失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'template_id' => $template->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '更新失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除礼品卡模板
|
||||
*/
|
||||
public function deleteTemplate(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_template,id',
|
||||
]);
|
||||
|
||||
$template = GiftCardTemplate::find($request->input('id'));
|
||||
if (!$template) {
|
||||
return $this->fail([404, '模板不存在']);
|
||||
}
|
||||
|
||||
// 检查是否有关联的兑换码
|
||||
if ($template->codes()->exists()) {
|
||||
return $this->fail([400, '该模板下存在兑换码,无法删除']);
|
||||
}
|
||||
|
||||
try {
|
||||
$template->delete();
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('删除礼品卡模板失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'template_id' => $template->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '删除失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成兑换码
|
||||
*/
|
||||
public function generateCodes(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'template_id' => 'required|integer|exists:v2_gift_card_template,id',
|
||||
'count' => 'required|integer|min:1|max:10000',
|
||||
'prefix' => 'nullable|string|max:10|regex:/^[A-Z0-9]*$/',
|
||||
'expires_hours' => 'nullable|integer|min:1',
|
||||
'max_usage' => 'integer|min:1|max:1000',
|
||||
], [
|
||||
'template_id.required' => '请选择礼品卡模板',
|
||||
'count.required' => '请指定生成数量',
|
||||
'count.max' => '单次最多生成10000个兑换码',
|
||||
'prefix.regex' => '前缀只能包含大写字母和数字',
|
||||
]);
|
||||
|
||||
$template = GiftCardTemplate::find($request->input('template_id'));
|
||||
if (!$template->isAvailable()) {
|
||||
return $this->fail([400, '模板已被禁用']);
|
||||
}
|
||||
|
||||
try {
|
||||
$options = [
|
||||
'prefix' => $request->input('prefix', 'GC'),
|
||||
'max_usage' => $request->input('max_usage', 1),
|
||||
];
|
||||
|
||||
if ($request->has('expires_hours')) {
|
||||
$options['expires_at'] = time() + ($request->input('expires_hours') * 3600);
|
||||
}
|
||||
|
||||
$batchId = GiftCardCode::batchGenerate(
|
||||
$request->input('template_id'),
|
||||
$request->input('count'),
|
||||
$options
|
||||
);
|
||||
|
||||
// 查询本次生成的所有兑换码
|
||||
$codes = GiftCardCode::where('batch_id', $batchId)->get();
|
||||
|
||||
// 判断是否导出 CSV
|
||||
if ($request->input('download_csv')) {
|
||||
$headers = [
|
||||
'Content-Type' => 'text/csv',
|
||||
'Content-Disposition' => 'attachment; filename="gift_codes.csv"',
|
||||
];
|
||||
$callback = function () use ($codes, $template) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
// 表头
|
||||
fputcsv($handle, [
|
||||
'兑换码',
|
||||
'前缀',
|
||||
'有效期',
|
||||
'最大使用次数',
|
||||
'批次号',
|
||||
'创建时间',
|
||||
'模板名称',
|
||||
'模板类型',
|
||||
'模板奖励',
|
||||
'状态',
|
||||
'使用者',
|
||||
'使用时间',
|
||||
'备注'
|
||||
]);
|
||||
foreach ($codes as $code) {
|
||||
$expireDate = $code->expires_at ? date('Y-m-d H:i:s', $code->expires_at) : '长期有效';
|
||||
$createDate = date('Y-m-d H:i:s', $code->created_at);
|
||||
$templateName = $template->name ?? '';
|
||||
$templateType = $template->type ?? '';
|
||||
$templateRewards = $template->rewards ? json_encode($template->rewards, JSON_UNESCAPED_UNICODE) : '';
|
||||
// 状态判断
|
||||
if ($code->disabled) {
|
||||
$status = '已禁用';
|
||||
} elseif ($code->used_at) {
|
||||
$status = '已使用';
|
||||
} elseif ($code->expires_at && $code->expires_at < time()) {
|
||||
$status = '已过期';
|
||||
} else {
|
||||
$status = '未使用';
|
||||
}
|
||||
$usedBy = $code->user_id ?? '';
|
||||
$usedAt = $code->used_at ? date('Y-m-d H:i:s', $code->used_at) : '';
|
||||
$remark = $code->remark ?? '';
|
||||
fputcsv($handle, [
|
||||
$code->code,
|
||||
$code->prefix ?? '',
|
||||
$expireDate,
|
||||
$code->max_usage,
|
||||
$code->batch_id,
|
||||
$createDate,
|
||||
$templateName,
|
||||
$templateType,
|
||||
$templateRewards,
|
||||
$status,
|
||||
$usedBy,
|
||||
$usedAt,
|
||||
$remark,
|
||||
]);
|
||||
}
|
||||
fclose($handle);
|
||||
};
|
||||
return response()->streamDownload($callback, 'gift_codes.csv', $headers);
|
||||
}
|
||||
|
||||
Log::info('批量生成兑换码', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'template_id' => $request->input('template_id'),
|
||||
'count' => $request->input('count'),
|
||||
'batch_id' => $batchId,
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'batch_id' => $batchId,
|
||||
'count' => $request->input('count'),
|
||||
'message' => '生成成功',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('生成兑换码失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'data' => $request->all(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '生成失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兑换码列表
|
||||
*/
|
||||
public function codes(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'template_id' => 'integer|exists:v2_gift_card_template,id',
|
||||
'batch_id' => 'string',
|
||||
'status' => 'integer|in:0,1,2,3',
|
||||
'page' => 'integer|min:1',
|
||||
'per_page' => 'integer|min:1|max:100',
|
||||
]);
|
||||
|
||||
$query = GiftCardCode::with(['template', 'user']);
|
||||
|
||||
if ($request->has('template_id')) {
|
||||
$query->where('template_id', $request->input('template_id'));
|
||||
}
|
||||
|
||||
if ($request->has('batch_id')) {
|
||||
$query->where('batch_id', $request->input('batch_id'));
|
||||
}
|
||||
|
||||
if ($request->has('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$codes = $query->orderBy('created_at', 'desc')->paginate($perPage);
|
||||
|
||||
$data = $codes->getCollection()->map(function ($code) {
|
||||
return [
|
||||
'id' => $code->id,
|
||||
'template_id' => $code->template_id,
|
||||
'template_name' => $code->template->name,
|
||||
'code' => $code->code,
|
||||
'batch_id' => $code->batch_id,
|
||||
'status' => $code->status,
|
||||
'status_name' => $code->status_name,
|
||||
'user_id' => $code->user_id,
|
||||
'user_email' => $code->user ? substr($code->user->email, 0, 3) . '***@***' : null,
|
||||
'used_at' => $code->used_at,
|
||||
'expires_at' => $code->expires_at,
|
||||
'usage_count' => $code->usage_count,
|
||||
'max_usage' => $code->max_usage,
|
||||
'created_at' => $code->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
$codes->setCollection($data);
|
||||
return $this->paginate($codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用/启用兑换码
|
||||
*/
|
||||
public function toggleCode(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_code,id',
|
||||
'action' => 'required|string|in:disable,enable',
|
||||
]);
|
||||
|
||||
$code = GiftCardCode::find($request->input('id'));
|
||||
if (!$code) {
|
||||
return $this->fail([404, '兑换码不存在']);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($request->input('action') === 'disable') {
|
||||
$code->markAsDisabled();
|
||||
} else {
|
||||
if ($code->status === GiftCardCode::STATUS_DISABLED) {
|
||||
$code->status = GiftCardCode::STATUS_UNUSED;
|
||||
$code->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => $request->input('action') === 'disable' ? '已禁用' : '已启用',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail([500, '操作失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出兑换码
|
||||
*/
|
||||
public function exportCodes(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'batch_id' => 'required|string|exists:v2_gift_card_code,batch_id',
|
||||
]);
|
||||
|
||||
$codes = GiftCardCode::where('batch_id', $request->input('batch_id'))
|
||||
->orderBy('created_at', 'asc')
|
||||
->get(['code']);
|
||||
|
||||
$content = $codes->pluck('code')->implode("\n");
|
||||
|
||||
return response($content)
|
||||
->header('Content-Type', 'text/plain')
|
||||
->header('Content-Disposition', 'attachment; filename="gift_cards_' . $request->input('batch_id') . '.txt"');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取使用记录
|
||||
*/
|
||||
public function usages(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'template_id' => 'integer|exists:v2_gift_card_template,id',
|
||||
'user_id' => 'integer|exists:v2_user,id',
|
||||
'page' => 'integer|min:1',
|
||||
'per_page' => 'integer|min:1|max:100',
|
||||
]);
|
||||
|
||||
$query = GiftCardUsage::with(['template', 'code', 'user', 'inviteUser']);
|
||||
|
||||
if ($request->has('template_id')) {
|
||||
$query->where('template_id', $request->input('template_id'));
|
||||
}
|
||||
|
||||
if ($request->has('user_id')) {
|
||||
$query->where('user_id', $request->input('user_id'));
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$usages = $query->orderBy('created_at', 'desc')->paginate($perPage);
|
||||
|
||||
$data = $usages->getCollection()->map(function ($usage) {
|
||||
return [
|
||||
'id' => $usage->id,
|
||||
'code' => $usage->code->code,
|
||||
'template_name' => $usage->template->name,
|
||||
'user_email' => $usage->user->email,
|
||||
'invite_user_email' => $usage->inviteUser ? substr($usage->inviteUser->email, 0, 3) . '***@***' : null,
|
||||
'rewards_given' => $usage->rewards_given,
|
||||
'invite_rewards' => $usage->invite_rewards,
|
||||
'multiplier_applied' => $usage->multiplier_applied,
|
||||
// 'ip_address' => $usage->ip_address,
|
||||
'created_at' => $usage->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'pagination' => [
|
||||
'current_page' => $usages->currentPage(),
|
||||
'last_page' => $usages->lastPage(),
|
||||
'per_page' => $usages->perPage(),
|
||||
'total' => $usages->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计数据
|
||||
*/
|
||||
public function statistics(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'start_date' => 'date_format:Y-m-d',
|
||||
'end_date' => 'date_format:Y-m-d',
|
||||
]);
|
||||
|
||||
$startDate = $request->input('start_date', date('Y-m-d', strtotime('-30 days')));
|
||||
$endDate = $request->input('end_date', date('Y-m-d'));
|
||||
|
||||
// 总体统计
|
||||
$totalStats = [
|
||||
'templates_count' => GiftCardTemplate::count(),
|
||||
'active_templates_count' => GiftCardTemplate::where('status', 1)->count(),
|
||||
'codes_count' => GiftCardCode::count(),
|
||||
'used_codes_count' => GiftCardCode::where('status', GiftCardCode::STATUS_USED)->count(),
|
||||
'usages_count' => GiftCardUsage::count(),
|
||||
];
|
||||
|
||||
// 每日使用统计
|
||||
$driver = GiftCardUsage::query()->getConnection()->getDriverName();
|
||||
$dateExpression = "date(created_at, 'unixepoch')"; // Default for SQLite
|
||||
if ($driver === 'mysql') {
|
||||
$dateExpression = 'DATE(FROM_UNIXTIME(created_at))';
|
||||
} elseif ($driver === 'pgsql') {
|
||||
$dateExpression = 'date(to_timestamp(created_at))';
|
||||
}
|
||||
|
||||
$dailyUsages = GiftCardUsage::selectRaw("{$dateExpression} as date, COUNT(*) as count")
|
||||
->whereRaw("{$dateExpression} BETWEEN ? AND ?", [$startDate, $endDate])
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
// 类型统计
|
||||
$typeStats = GiftCardUsage::with('template')
|
||||
->selectRaw('template_id, COUNT(*) as count')
|
||||
->groupBy('template_id')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'template_name' => $item->template->name,
|
||||
'type_name' => $item->template->type_name,
|
||||
'count' => $item->count,
|
||||
];
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'total_stats' => $totalStats,
|
||||
'daily_usages' => $dailyUsages,
|
||||
'type_stats' => $typeStats,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的礼品卡类型
|
||||
*/
|
||||
public function types()
|
||||
{
|
||||
return $this->success(GiftCardTemplate::getTypeMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个兑换码
|
||||
*/
|
||||
public function updateCode(Request $request)
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_code,id',
|
||||
'expires_at' => 'sometimes|nullable|integer',
|
||||
'max_usage' => 'sometimes|integer|min:1|max:1000',
|
||||
'status' => 'sometimes|integer|in:0,1,2,3',
|
||||
]);
|
||||
|
||||
$code = GiftCardCode::find($validatedData['id']);
|
||||
if (!$code) {
|
||||
return $this->fail([404, '礼品卡不存在']);
|
||||
}
|
||||
|
||||
try {
|
||||
$updateData = collect($validatedData)->except('id')->all();
|
||||
|
||||
if (empty($updateData)) {
|
||||
return $this->success($code);
|
||||
}
|
||||
|
||||
$updateData['updated_at'] = time();
|
||||
$code->update($updateData);
|
||||
|
||||
return $this->success($code->fresh());
|
||||
} catch (\Exception $e) {
|
||||
Log::error('更新礼品卡信息失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'code_id' => $code->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '更新失败']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除礼品卡
|
||||
*/
|
||||
public function deleteCode(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_code,id',
|
||||
]);
|
||||
|
||||
$code = GiftCardCode::find($request->input('id'));
|
||||
if (!$code) {
|
||||
return $this->fail([404, '礼品卡不存在']);
|
||||
}
|
||||
|
||||
// 检查是否已被使用
|
||||
if ($code->status === GiftCardCode::STATUS_USED) {
|
||||
return $this->fail([400, '该礼品卡已被使用,无法删除']);
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否有关联的使用记录
|
||||
if ($code->usages()->exists()) {
|
||||
return $this->fail([400, '该礼品卡存在使用记录,无法删除']);
|
||||
}
|
||||
|
||||
$code->delete();
|
||||
return $this->success(['message' => '删除成功']);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('删除礼品卡失败', [
|
||||
'admin_id' => $request->user()->id,
|
||||
'code_id' => $code->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '删除失败']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,13 +54,13 @@ class OrderController extends Controller
|
||||
page: $current
|
||||
);
|
||||
|
||||
$paginatedResults->getCollection()->transform(function($order) {
|
||||
$paginatedResults->getCollection()->transform(function ($order) {
|
||||
$orderArray = $order->toArray();
|
||||
$orderArray['period'] = PlanService::getLegacyPeriod((string) $order->period);
|
||||
return $orderArray;
|
||||
});
|
||||
|
||||
return response()->json($paginatedResults);
|
||||
return $this->paginate($paginatedResults);
|
||||
}
|
||||
|
||||
private function applyFiltersAndSorts(Request $request, Builder $builder): void
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Services\Plugin\PluginManager;
|
||||
use App\Services\Plugin\PluginConfigService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PluginController extends Controller
|
||||
{
|
||||
@@ -44,6 +45,11 @@ class PluginController extends Controller
|
||||
$installed = isset($installedPlugins[$code]);
|
||||
// 使用配置服务获取配置
|
||||
$pluginConfig = $installed ? $this->configService->getConfig($code) : ($config['config'] ?? []);
|
||||
$readmeFile = collect(['README.md', 'readme.md'])
|
||||
->map(fn($f) => $directory . '/' . $f)
|
||||
->first(fn($path) => File::exists($path));
|
||||
$readmeContent = $readmeFile ? File::get($readmeFile) : '';
|
||||
|
||||
$plugins[] = [
|
||||
'code' => $config['code'],
|
||||
'name' => $config['name'],
|
||||
@@ -53,6 +59,7 @@ class PluginController extends Controller
|
||||
'is_installed' => $installed,
|
||||
'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false,
|
||||
'config' => $pluginConfig,
|
||||
'readme' => $readmeContent,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,10 +169,7 @@ class UserController extends Controller
|
||||
return self::transformUserData($user);
|
||||
});
|
||||
|
||||
return response([
|
||||
'data' => $users->items(),
|
||||
'total' => $users->total()
|
||||
]);
|
||||
return $this->paginate($users);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -95,6 +95,11 @@ class ServerSave extends FormRequest
|
||||
'excludes' => 'nullable|array',
|
||||
'ips' => 'nullable|array',
|
||||
'rate' => 'required|numeric',
|
||||
'rate_time_enable' => 'nullable|boolean',
|
||||
'rate_time_ranges' => 'nullable|array',
|
||||
'rate_time_ranges.*.start' => 'required_with:rate_time_ranges|string|date_format:H:i',
|
||||
'rate_time_ranges.*.end' => 'required_with:rate_time_ranges|string|date_format:H:i',
|
||||
'rate_time_ranges.*.rate' => 'required_with:rate_time_ranges|numeric|min:0',
|
||||
'protocol_settings' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
28
app/Http/Requests/User/GiftCardCheckRequest.php
Normal file
28
app/Http/Requests/User/GiftCardCheckRequest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class GiftCardCheckRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
44
app/Http/Requests/User/GiftCardRedeemRequest.php
Normal file
44
app/Http/Requests/User/GiftCardRedeemRequest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class GiftCardRedeemRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'code' => 'required|string|min:8|max:32',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'code.required' => '请输入兑换码',
|
||||
'code.min' => '兑换码长度不能少于8位',
|
||||
'code.max' => '兑换码长度不能超过32位',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ class PlanResource extends JsonResource
|
||||
'capacity_limit' => $this->getFormattedCapacityLimit(),
|
||||
'transfer_enable' => $this->resource['transfer_enable'],
|
||||
'speed_limit' => $this->resource['speed_limit'],
|
||||
'device_limit' => $this->resource['device_limit'],
|
||||
'show' => (bool) $this->resource['show'],
|
||||
'sell' => (bool) $this->resource['sell'],
|
||||
'renew' => (bool) $this->resource['renew'],
|
||||
|
||||
@@ -3,6 +3,7 @@ namespace App\Http\Routes\V1;
|
||||
|
||||
use App\Http\Controllers\V1\User\CommController;
|
||||
use App\Http\Controllers\V1\User\CouponController;
|
||||
use App\Http\Controllers\V1\User\GiftCardController;
|
||||
use App\Http\Controllers\V1\User\InviteController;
|
||||
use App\Http\Controllers\V1\User\KnowledgeController;
|
||||
use App\Http\Controllers\V1\User\NoticeController;
|
||||
@@ -61,6 +62,12 @@ class UserRoute
|
||||
$router->get('/server/fetch', [ServerController::class, 'fetch']);
|
||||
// Coupon
|
||||
$router->post('/coupon/check', [CouponController::class, 'check']);
|
||||
// Gift Card
|
||||
$router->post('/gift-card/check', [GiftCardController::class, 'check']);
|
||||
$router->post('/gift-card/redeem', [GiftCardController::class, 'redeem']);
|
||||
$router->get('/gift-card/history', [GiftCardController::class, 'history']);
|
||||
$router->get('/gift-card/detail', [GiftCardController::class, 'detail']);
|
||||
$router->get('/gift-card/types', [GiftCardController::class, 'types']);
|
||||
// Telegram
|
||||
$router->get('/telegram/getBotInfo', [TelegramController::class, 'getBotInfo']);
|
||||
// Comm
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Http\Controllers\V2\Admin\StatController;
|
||||
use App\Http\Controllers\V2\Admin\NoticeController;
|
||||
use App\Http\Controllers\V2\Admin\TicketController;
|
||||
use App\Http\Controllers\V2\Admin\CouponController;
|
||||
use App\Http\Controllers\V2\Admin\GiftCardController;
|
||||
use App\Http\Controllers\V2\Admin\KnowledgeController;
|
||||
use App\Http\Controllers\V2\Admin\PaymentController;
|
||||
use App\Http\Controllers\V2\Admin\SystemController;
|
||||
@@ -158,6 +159,32 @@ class AdminRoute
|
||||
$router->post('/update', [CouponController::class, 'update']);
|
||||
});
|
||||
|
||||
// Gift Card
|
||||
$router->group([
|
||||
'prefix' => 'gift-card'
|
||||
], function ($router) {
|
||||
// Template management
|
||||
$router->any('/templates', [GiftCardController::class, 'templates']);
|
||||
$router->post('/create-template', [GiftCardController::class, 'createTemplate']);
|
||||
$router->post('/update-template', [GiftCardController::class, 'updateTemplate']);
|
||||
$router->post('/delete-template', [GiftCardController::class, 'deleteTemplate']);
|
||||
|
||||
// Code management
|
||||
$router->post('/generate-codes', [GiftCardController::class, 'generateCodes']);
|
||||
$router->any('/codes', [GiftCardController::class, 'codes']);
|
||||
$router->post('/toggle-code', [GiftCardController::class, 'toggleCode']);
|
||||
$router->get('/export-codes', [GiftCardController::class, 'exportCodes']);
|
||||
$router->post('/update-code', [GiftCardController::class, 'updateCode']);
|
||||
$router->post('/delete-code', [GiftCardController::class, 'deleteCode']);
|
||||
|
||||
// Usage records
|
||||
$router->any('/usages', [GiftCardController::class, 'usages']);
|
||||
|
||||
// Statistics
|
||||
$router->any('/statistics', [GiftCardController::class, 'statistics']);
|
||||
$router->get('/types', [GiftCardController::class, 'types']);
|
||||
});
|
||||
|
||||
// Knowledge
|
||||
$router->group([
|
||||
'prefix' => 'knowledge'
|
||||
|
||||
259
app/Models/GiftCardCode.php
Normal file
259
app/Models/GiftCardCode.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* App\Models\GiftCardCode
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $template_id 模板ID
|
||||
* @property GiftCardTemplate $template 关联模板
|
||||
* @property string $code 兑换码
|
||||
* @property string|null $batch_id 批次ID
|
||||
* @property int $status 状态
|
||||
* @property int|null $user_id 使用用户ID
|
||||
* @property int|null $used_at 使用时间
|
||||
* @property int|null $expires_at 过期时间
|
||||
* @property array|null $actual_rewards 实际奖励
|
||||
* @property int $usage_count 使用次数
|
||||
* @property int $max_usage 最大使用次数
|
||||
* @property array|null $metadata 额外数据
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
*/
|
||||
class GiftCardCode extends Model
|
||||
{
|
||||
protected $table = 'v2_gift_card_code';
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
// 状态常量
|
||||
const STATUS_UNUSED = 0; // 未使用
|
||||
const STATUS_USED = 1; // 已使用
|
||||
const STATUS_EXPIRED = 2; // 已过期
|
||||
const STATUS_DISABLED = 3; // 已禁用
|
||||
|
||||
protected $fillable = [
|
||||
'template_id',
|
||||
'code',
|
||||
'batch_id',
|
||||
'status',
|
||||
'user_id',
|
||||
'used_at',
|
||||
'expires_at',
|
||||
'actual_rewards',
|
||||
'usage_count',
|
||||
'max_usage',
|
||||
'metadata'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'used_at' => 'timestamp',
|
||||
'expires_at' => 'timestamp',
|
||||
'actual_rewards' => 'array',
|
||||
'metadata' => 'array'
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取状态映射
|
||||
*/
|
||||
public static function getStatusMap(): array
|
||||
{
|
||||
return [
|
||||
self::STATUS_UNUSED => '未使用',
|
||||
self::STATUS_USED => '已使用',
|
||||
self::STATUS_EXPIRED => '已过期',
|
||||
self::STATUS_DISABLED => '已禁用',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态名称
|
||||
*/
|
||||
public function getStatusNameAttribute(): string
|
||||
{
|
||||
return self::getStatusMap()[$this->status] ?? '未知状态';
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联礼品卡模板
|
||||
*/
|
||||
public function template(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GiftCardTemplate::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联使用用户
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联使用记录
|
||||
*/
|
||||
public function usages(): HasMany
|
||||
{
|
||||
return $this->hasMany(GiftCardUsage::class, 'code_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可用
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
// 检查状态
|
||||
if ($this->status !== self::STATUS_UNUSED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if ($this->expires_at && $this->expires_at < time()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查使用次数
|
||||
if ($this->usage_count >= $this->max_usage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已过期
|
||||
*/
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at && $this->expires_at < time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为已使用
|
||||
*/
|
||||
public function markAsUsed(User $user): bool
|
||||
{
|
||||
$this->status = self::STATUS_USED;
|
||||
$this->user_id = $user->id;
|
||||
$this->used_at = time();
|
||||
$this->usage_count += 1;
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为已过期
|
||||
*/
|
||||
public function markAsExpired(): bool
|
||||
{
|
||||
$this->status = self::STATUS_EXPIRED;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为已禁用
|
||||
*/
|
||||
public function markAsDisabled(): bool
|
||||
{
|
||||
$this->status = self::STATUS_DISABLED;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成兑换码
|
||||
*/
|
||||
public static function generateCode(string $prefix = 'GC'): string
|
||||
{
|
||||
do {
|
||||
$code = $prefix . strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 12));
|
||||
} while (self::where('code', $code)->exists());
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成兑换码
|
||||
*/
|
||||
public static function batchGenerate(int $templateId, int $count, array $options = []): string
|
||||
{
|
||||
$batchId = uniqid('batch_');
|
||||
$prefix = $options['prefix'] ?? 'GC';
|
||||
$expiresAt = $options['expires_at'] ?? null;
|
||||
$maxUsage = $options['max_usage'] ?? 1;
|
||||
|
||||
$codes = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$codes[] = [
|
||||
'template_id' => $templateId,
|
||||
'code' => self::generateCode($prefix),
|
||||
'batch_id' => $batchId,
|
||||
'status' => self::STATUS_UNUSED,
|
||||
'expires_at' => $expiresAt,
|
||||
'max_usage' => $maxUsage,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
self::insert($codes);
|
||||
|
||||
return $batchId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实际奖励(用于盲盒等)
|
||||
*/
|
||||
public function setActualRewards(array $rewards): bool
|
||||
{
|
||||
$this->actual_rewards = $rewards;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实际奖励
|
||||
*/
|
||||
public function getActualRewards(): array
|
||||
{
|
||||
return $this->actual_rewards ?? $this->template->rewards ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查兑换码格式
|
||||
*/
|
||||
public static function validateCodeFormat(string $code): bool
|
||||
{
|
||||
// 基本格式验证:字母数字组合,长度8-32
|
||||
return preg_match('/^[A-Z0-9]{8,32}$/', $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据批次ID获取兑换码
|
||||
*/
|
||||
public static function getByBatchId(string $batchId)
|
||||
{
|
||||
return self::where('batch_id', $batchId)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期兑换码
|
||||
*/
|
||||
public static function cleanupExpired(): int
|
||||
{
|
||||
$count = self::where('status', self::STATUS_UNUSED)
|
||||
->where('expires_at', '<', time())
|
||||
->count();
|
||||
|
||||
self::where('status', self::STATUS_UNUSED)
|
||||
->where('expires_at', '<', time())
|
||||
->update(['status' => self::STATUS_EXPIRED]);
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
253
app/Models/GiftCardTemplate.php
Normal file
253
app/Models/GiftCardTemplate.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* App\Models\GiftCardTemplate
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name 礼品卡名称
|
||||
* @property string|null $description 礼品卡描述
|
||||
* @property int $type 卡片类型
|
||||
* @property boolean $status 状态
|
||||
* @property array|null $conditions 使用条件配置
|
||||
* @property array $rewards 奖励配置
|
||||
* @property array|null $limits 限制条件
|
||||
* @property array|null $special_config 特殊配置
|
||||
* @property string|null $icon 卡片图标
|
||||
* @property string $theme_color 主题色
|
||||
* @property int $sort 排序
|
||||
* @property int $admin_id 创建管理员ID
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
*/
|
||||
class GiftCardTemplate extends Model
|
||||
{
|
||||
protected $table = 'v2_gift_card_template';
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
// 卡片类型常量
|
||||
const TYPE_GENERAL = 1; // 通用礼品卡
|
||||
const TYPE_PLAN = 2; // 套餐礼品卡
|
||||
const TYPE_MYSTERY = 3; // 盲盒礼品卡
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
'status',
|
||||
'conditions',
|
||||
'rewards',
|
||||
'limits',
|
||||
'special_config',
|
||||
'icon',
|
||||
'background_image',
|
||||
'theme_color',
|
||||
'sort',
|
||||
'admin_id'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'conditions' => 'array',
|
||||
'rewards' => 'array',
|
||||
'limits' => 'array',
|
||||
'special_config' => 'array',
|
||||
'status' => 'boolean'
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取卡片类型映射
|
||||
*/
|
||||
public static function getTypeMap(): array
|
||||
{
|
||||
return [
|
||||
self::TYPE_GENERAL => '通用礼品卡',
|
||||
self::TYPE_PLAN => '套餐礼品卡',
|
||||
self::TYPE_MYSTERY => '盲盒礼品卡',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型名称
|
||||
*/
|
||||
public function getTypeNameAttribute(): string
|
||||
{
|
||||
return self::getTypeMap()[$this->type] ?? '未知类型';
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联兑换码
|
||||
*/
|
||||
public function codes(): HasMany
|
||||
{
|
||||
return $this->hasMany(GiftCardCode::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联使用记录
|
||||
*/
|
||||
public function usages(): HasMany
|
||||
{
|
||||
return $this->hasMany(GiftCardUsage::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联统计数据
|
||||
*/
|
||||
public function stats(): HasMany
|
||||
{
|
||||
return $this->hasMany(GiftCardUsage::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可用
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否满足使用条件
|
||||
*/
|
||||
public function checkUserConditions(User $user): bool
|
||||
{
|
||||
switch ($this->type) {
|
||||
case self::TYPE_GENERAL:
|
||||
$rewards = $this->rewards ?? [];
|
||||
if (isset($rewards['transfer_enable']) || isset($rewards['expire_days']) || isset($rewards['reset_package'])) {
|
||||
if (!$user->plan_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case self::TYPE_PLAN:
|
||||
if ($user->isActive()) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$conditions = $this->conditions ?? [];
|
||||
|
||||
// 检查新用户条件
|
||||
if (isset($conditions['new_user_only']) && $conditions['new_user_only']) {
|
||||
$maxDays = $conditions['new_user_max_days'] ?? 7;
|
||||
if ($user->created_at < (time() - ($maxDays * 86400))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查付费用户条件
|
||||
if (isset($conditions['paid_user_only']) && $conditions['paid_user_only']) {
|
||||
$paidOrderExists = $user->orders()->where('status', Order::STATUS_COMPLETED)->exists();
|
||||
if (!$paidOrderExists) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查允许的套餐
|
||||
if (isset($conditions['allowed_plans']) && $user->plan_id) {
|
||||
if (!in_array($user->plan_id, $conditions['allowed_plans'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要邀请人
|
||||
if (isset($conditions['require_invite']) && $conditions['require_invite']) {
|
||||
if (!$user->invite_user_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算实际奖励
|
||||
*/
|
||||
public function calculateActualRewards(User $user): array
|
||||
{
|
||||
$baseRewards = $this->rewards;
|
||||
$actualRewards = $baseRewards;
|
||||
|
||||
// 处理盲盒随机奖励
|
||||
if ($this->type === self::TYPE_MYSTERY && isset($this->rewards['random_rewards'])) {
|
||||
$randomRewards = $this->rewards['random_rewards'];
|
||||
$totalWeight = array_sum(array_column($randomRewards, 'weight'));
|
||||
$random = mt_rand(1, $totalWeight);
|
||||
$currentWeight = 0;
|
||||
|
||||
foreach ($randomRewards as $reward) {
|
||||
$currentWeight += $reward['weight'];
|
||||
if ($random <= $currentWeight) {
|
||||
$actualRewards = array_merge($actualRewards, $reward);
|
||||
unset($actualRewards['weight']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理节日等特殊奖励(通用逻辑)
|
||||
if (isset($this->special_config['festival_bonus'])) {
|
||||
$now = time();
|
||||
$festivalConfig = $this->special_config;
|
||||
|
||||
if (isset($festivalConfig['start_time']) && isset($festivalConfig['end_time'])) {
|
||||
if ($now >= $festivalConfig['start_time'] && $now <= $festivalConfig['end_time']) {
|
||||
$bonus = (float) ($festivalConfig['festival_bonus'] ?? 1.0);
|
||||
if ($bonus > 1.0) {
|
||||
foreach ($actualRewards as $key => &$value) {
|
||||
if (is_numeric($value)) {
|
||||
$value = intval($value * $bonus);
|
||||
}
|
||||
}
|
||||
unset($value); // 解除引用
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $actualRewards;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查使用频率限制
|
||||
*/
|
||||
public function checkUsageLimit(User $user): bool
|
||||
{
|
||||
$conditions = $this->conditions ?? [];
|
||||
|
||||
// 检查每用户最大使用次数
|
||||
if (isset($conditions['max_use_per_user'])) {
|
||||
$usedCount = $this->usages()
|
||||
->where('user_id', $user->id)
|
||||
->count();
|
||||
if ($usedCount >= $conditions['max_use_per_user']) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查冷却时间
|
||||
if (isset($conditions['cooldown_hours'])) {
|
||||
$lastUsage = $this->usages()
|
||||
->where('user_id', $user->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->first();
|
||||
|
||||
if ($lastUsage) {
|
||||
$cooldownTime = $lastUsage->created_at + ($conditions['cooldown_hours'] * 3600);
|
||||
if (time() < $cooldownTime) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
112
app/Models/GiftCardUsage.php
Normal file
112
app/Models/GiftCardUsage.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* App\Models\GiftCardUsage
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $code_id 兑换码ID
|
||||
* @property int $template_id 模板ID
|
||||
* @property int $user_id 使用用户ID
|
||||
* @property int|null $invite_user_id 邀请人ID
|
||||
* @property array $rewards_given 实际发放的奖励
|
||||
* @property array|null $invite_rewards 邀请人获得的奖励
|
||||
* @property int|null $user_level_at_use 使用时用户等级
|
||||
* @property int|null $plan_id_at_use 使用时用户套餐ID
|
||||
* @property float $multiplier_applied 应用的倍率
|
||||
* @property string|null $ip_address 使用IP地址
|
||||
* @property string|null $user_agent 用户代理
|
||||
* @property string|null $notes 备注
|
||||
* @property int $created_at
|
||||
*/
|
||||
class GiftCardUsage extends Model
|
||||
{
|
||||
protected $table = 'v2_gift_card_usage';
|
||||
protected $dateFormat = 'U';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'code_id',
|
||||
'template_id',
|
||||
'user_id',
|
||||
'invite_user_id',
|
||||
'rewards_given',
|
||||
'invite_rewards',
|
||||
'user_level_at_use',
|
||||
'plan_id_at_use',
|
||||
'multiplier_applied',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'notes',
|
||||
'created_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'rewards_given' => 'array',
|
||||
'invite_rewards' => 'array',
|
||||
'multiplier_applied' => 'float'
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联兑换码
|
||||
*/
|
||||
public function code(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GiftCardCode::class, 'code_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联模板
|
||||
*/
|
||||
public function template(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GiftCardTemplate::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联使用用户
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联邀请人
|
||||
*/
|
||||
public function inviteUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'invite_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建使用记录
|
||||
*/
|
||||
public static function createRecord(
|
||||
GiftCardCode $code,
|
||||
User $user,
|
||||
array $rewards,
|
||||
array $options = []
|
||||
): self {
|
||||
return self::create([
|
||||
'code_id' => $code->id,
|
||||
'template_id' => $code->template_id,
|
||||
'user_id' => $user->id,
|
||||
'invite_user_id' => $user->invite_user_id,
|
||||
'rewards_given' => $rewards,
|
||||
'invite_rewards' => $options['invite_rewards'] ?? null,
|
||||
'user_level_at_use' => $user->plan ? $user->plan->sort : null,
|
||||
'plan_id_at_use' => $user->plan_id,
|
||||
'multiplier_applied' => $options['multiplier'] ?? 1.0,
|
||||
// 'ip_address' => $options['ip_address'] ?? null,
|
||||
'user_agent' => $options['user_agent'] ?? null,
|
||||
'notes' => $options['notes'] ?? null,
|
||||
'created_at' => time(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,11 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property int|null $actual_commission_balance
|
||||
* @property int|null $commission_rate
|
||||
* @property int|null $commission_auto_check
|
||||
*
|
||||
* @property int|null $commission_balance
|
||||
* @property int|null $discount_amount
|
||||
* @property int|null $paid_at
|
||||
* @property string|null $callback_no
|
||||
*
|
||||
* @property-read Plan $plan
|
||||
* @property-read Payment|null $payment
|
||||
* @property-read User $user
|
||||
|
||||
@@ -28,6 +28,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
* @property string|null $network 网络类型
|
||||
* @property int|null $parent_id 父节点ID
|
||||
* @property float|null $rate 倍率
|
||||
* @property array|null $rate_time_ranges 倍率时间范围
|
||||
* @property int|null $sort 排序
|
||||
* @property array|null $protocol_settings 协议设置
|
||||
* @property int $created_at
|
||||
@@ -114,7 +115,9 @@ class Server extends Model
|
||||
'last_push_at' => 'integer',
|
||||
'show' => 'boolean',
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
'updated_at' => 'timestamp',
|
||||
'rate_time_ranges' => 'array',
|
||||
'rate_time_enable' => 'boolean',
|
||||
];
|
||||
|
||||
private const PROTOCOL_CONFIGURATIONS = [
|
||||
@@ -449,4 +452,16 @@ class Server extends Model
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function getCurrentRate(): float
|
||||
{
|
||||
$now = date('H:i');
|
||||
$ranges = $this->rate_time_ranges ?? [];
|
||||
foreach ($ranges as $range) {
|
||||
if ($now >= $range['start'] && $now <= $range['end']) {
|
||||
return (float) $range['rate'];
|
||||
}
|
||||
}
|
||||
return (float) $this->rate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
class TrafficResetLog extends Model
|
||||
{
|
||||
protected $table = 'v2_traffic_reset_logs';
|
||||
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'reset_type',
|
||||
@@ -64,6 +64,8 @@ class TrafficResetLog extends Model
|
||||
public const SOURCE_API = 'api';
|
||||
public const SOURCE_CRON = 'cron';
|
||||
public const SOURCE_USER_ACCESS = 'user_access';
|
||||
public const SOURCE_ORDER = 'order';
|
||||
public const SOURCE_GIFT_CARD = 'gift_card';
|
||||
|
||||
/**
|
||||
* 获取重置类型的多语言名称
|
||||
@@ -139,9 +141,9 @@ class TrafficResetLog extends Model
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
|
||||
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property int|null $balance 余额
|
||||
* @property int|null $commission_balance 佣金余额
|
||||
* @property float $commission_rate 返佣比例
|
||||
* @property int|null $commission_type 返佣类型
|
||||
* @property int|null $device_limit 设备限制数量
|
||||
* @property int|null $discount 折扣
|
||||
* @property int|null $last_login_at 最后登录时间
|
||||
|
||||
334
app/Services/GiftCardService.php
Normal file
334
app/Services/GiftCardService.php
Normal file
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Resources\PlanResource;
|
||||
use App\Models\GiftCardCode;
|
||||
use App\Models\GiftCardTemplate;
|
||||
use App\Models\GiftCardUsage;
|
||||
use App\Models\Plan;
|
||||
use App\Models\TrafficResetLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GiftCardService
|
||||
{
|
||||
protected readonly GiftCardCode $code;
|
||||
protected readonly GiftCardTemplate $template;
|
||||
protected ?User $user = null;
|
||||
|
||||
public function __construct(string $code)
|
||||
{
|
||||
$this->code = GiftCardCode::where('code', $code)->first()
|
||||
?? throw new ApiException('兑换码不存在');
|
||||
|
||||
$this->template = $this->code->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置使用用户
|
||||
*/
|
||||
public function setUser(User $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证兑换码
|
||||
*/
|
||||
public function validate(): self
|
||||
{
|
||||
$this->validateIsActive();
|
||||
|
||||
$eligibility = $this->checkUserEligibility();
|
||||
if (!$eligibility['can_redeem']) {
|
||||
throw new ApiException($eligibility['reason']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证礼品卡本身是否可用 (不检查用户条件)
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function validateIsActive(): self
|
||||
{
|
||||
if (!$this->template->isAvailable()) {
|
||||
throw new ApiException('该礼品卡类型已停用');
|
||||
}
|
||||
|
||||
if (!$this->code->isAvailable()) {
|
||||
throw new ApiException('兑换码不可用:' . $this->code->status_name);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否满足兑换条件 (不抛出异常)
|
||||
*/
|
||||
public function checkUserEligibility(): array
|
||||
{
|
||||
if (!$this->user) {
|
||||
return [
|
||||
'can_redeem' => false,
|
||||
'reason' => '用户信息未提供'
|
||||
];
|
||||
}
|
||||
|
||||
if (!$this->template->checkUserConditions($this->user)) {
|
||||
return [
|
||||
'can_redeem' => false,
|
||||
'reason' => '您不满足此礼品卡的使用条件'
|
||||
];
|
||||
}
|
||||
|
||||
if (!$this->template->checkUsageLimit($this->user)) {
|
||||
return [
|
||||
'can_redeem' => false,
|
||||
'reason' => '您已达到此礼品卡的使用限制'
|
||||
];
|
||||
}
|
||||
|
||||
return ['can_redeem' => true, 'reason' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用礼品卡
|
||||
*/
|
||||
public function redeem(array $options = []): array
|
||||
{
|
||||
if (!$this->user) {
|
||||
throw new ApiException('未设置使用用户');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($options) {
|
||||
$actualRewards = $this->template->calculateActualRewards($this->user);
|
||||
|
||||
if ($this->template->type === GiftCardTemplate::TYPE_MYSTERY) {
|
||||
$this->code->setActualRewards($actualRewards);
|
||||
}
|
||||
|
||||
$this->giveRewards($actualRewards);
|
||||
|
||||
$inviteRewards = null;
|
||||
if ($this->user->invite_user_id && isset($actualRewards['invite_reward_rate'])) {
|
||||
$inviteRewards = $this->giveInviteRewards($actualRewards);
|
||||
}
|
||||
|
||||
$this->code->markAsUsed($this->user);
|
||||
|
||||
GiftCardUsage::createRecord(
|
||||
$this->code,
|
||||
$this->user,
|
||||
$actualRewards,
|
||||
array_merge($options, [
|
||||
'invite_rewards' => $inviteRewards,
|
||||
'multiplier' => $this->calculateMultiplier(),
|
||||
])
|
||||
);
|
||||
|
||||
return [
|
||||
'rewards' => $actualRewards,
|
||||
'invite_rewards' => $inviteRewards,
|
||||
'code' => $this->code->code,
|
||||
'template_name' => $this->template->name,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发放奖励
|
||||
*/
|
||||
protected function giveRewards(array $rewards): void
|
||||
{
|
||||
$userService = app(UserService::class);
|
||||
|
||||
if (isset($rewards['balance']) && $rewards['balance'] > 0) {
|
||||
if (!$userService->addBalance($this->user->id, $rewards['balance'])) {
|
||||
throw new ApiException('余额发放失败');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($rewards['transfer_enable']) && $rewards['transfer_enable'] > 0) {
|
||||
$this->user->transfer_enable = ($this->user->transfer_enable ?? 0) + $rewards['transfer_enable'];
|
||||
}
|
||||
|
||||
if (isset($rewards['device_limit']) && $rewards['device_limit'] > 0) {
|
||||
$this->user->device_limit = ($this->user->device_limit ?? 0) + $rewards['device_limit'];
|
||||
}
|
||||
|
||||
if (isset($rewards['reset_package']) && $rewards['reset_package']) {
|
||||
if ($this->user->plan_id) {
|
||||
app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_GIFT_CARD);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($rewards['plan_id'])) {
|
||||
$plan = Plan::find($rewards['plan_id']);
|
||||
if ($plan) {
|
||||
$userService->assignPlan(
|
||||
$this->user,
|
||||
$plan,
|
||||
$rewards['plan_validity_days'] ?? null
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 只有在不是套餐卡的情况下,才处理独立的有效期奖励
|
||||
if (isset($rewards['expire_days']) && $rewards['expire_days'] > 0) {
|
||||
$userService->extendSubscription($this->user, $rewards['expire_days']);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户更改
|
||||
if (!$this->user->save()) {
|
||||
throw new ApiException('用户信息更新失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发放邀请人奖励
|
||||
*/
|
||||
protected function giveInviteRewards(array $rewards): ?array
|
||||
{
|
||||
if (!$this->user->invite_user_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$inviteUser = User::find($this->user->invite_user_id);
|
||||
if (!$inviteUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rate = $rewards['invite_reward_rate'] ?? 0.2;
|
||||
$inviteRewards = [];
|
||||
|
||||
$userService = app(UserService::class);
|
||||
|
||||
// 邀请人余额奖励
|
||||
if (isset($rewards['balance']) && $rewards['balance'] > 0) {
|
||||
$inviteBalance = intval($rewards['balance'] * $rate);
|
||||
if ($inviteBalance > 0) {
|
||||
$userService->addBalance($inviteUser->id, $inviteBalance);
|
||||
$inviteRewards['balance'] = $inviteBalance;
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请人流量奖励
|
||||
if (isset($rewards['transfer_enable']) && $rewards['transfer_enable'] > 0) {
|
||||
$inviteTransfer = intval($rewards['transfer_enable'] * $rate);
|
||||
if ($inviteTransfer > 0) {
|
||||
$inviteUser->transfer_enable = ($inviteUser->transfer_enable ?? 0) + $inviteTransfer;
|
||||
$inviteUser->save();
|
||||
$inviteRewards['transfer_enable'] = $inviteTransfer;
|
||||
}
|
||||
}
|
||||
|
||||
return $inviteRewards;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算倍率
|
||||
*/
|
||||
protected function calculateMultiplier(): float
|
||||
{
|
||||
return $this->getFestivalBonus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节日加成倍率
|
||||
*/
|
||||
private function getFestivalBonus(): float
|
||||
{
|
||||
$festivalConfig = $this->template->special_config ?? [];
|
||||
$now = time();
|
||||
|
||||
if (
|
||||
isset($festivalConfig['start_time'], $festivalConfig['end_time']) &&
|
||||
$now >= $festivalConfig['start_time'] &&
|
||||
$now <= $festivalConfig['end_time']
|
||||
) {
|
||||
return $festivalConfig['festival_bonus'] ?? 1.0;
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兑换码信息(不包含敏感信息)
|
||||
*/
|
||||
public function getCodeInfo(): array
|
||||
{
|
||||
$info = [
|
||||
'code' => $this->code->code,
|
||||
'template' => [
|
||||
'name' => $this->template->name,
|
||||
'description' => $this->template->description,
|
||||
'type' => $this->template->type,
|
||||
'type_name' => $this->template->type_name,
|
||||
'icon' => $this->template->icon,
|
||||
'background_image' => $this->template->background_image,
|
||||
'theme_color' => $this->template->theme_color,
|
||||
],
|
||||
'status' => $this->code->status,
|
||||
'status_name' => $this->code->status_name,
|
||||
'expires_at' => $this->code->expires_at,
|
||||
'usage_count' => $this->code->usage_count,
|
||||
'max_usage' => $this->code->max_usage,
|
||||
];
|
||||
if ($this->template->type === GiftCardTemplate::TYPE_PLAN) {
|
||||
$plan = Plan::find($this->code->template->rewards['plan_id']);
|
||||
if ($plan) {
|
||||
$info['plan_info'] = PlanResource::make($plan)->toArray(request());
|
||||
}
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览奖励(不实际发放)
|
||||
*/
|
||||
public function previewRewards(): array
|
||||
{
|
||||
if (!$this->user) {
|
||||
throw new ApiException('未设置使用用户');
|
||||
}
|
||||
|
||||
return $this->template->calculateActualRewards($this->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兑换码
|
||||
*/
|
||||
public function getCode(): GiftCardCode
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板
|
||||
*/
|
||||
public function getTemplate(): GiftCardTemplate
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
*/
|
||||
protected function logUsage(string $action, array $data = []): void
|
||||
{
|
||||
Log::info('礼品卡使用记录', [
|
||||
'action' => $action,
|
||||
'code' => $this->code->code,
|
||||
'template_id' => $this->template->id,
|
||||
'user_id' => $this->user?->id,
|
||||
'data' => $data,
|
||||
'ip' => request()->ip(),
|
||||
'user_agent' => request()->userAgent(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Exceptions\ApiException;
|
||||
use App\Jobs\OrderHandleJob;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\TrafficResetLog;
|
||||
use App\Models\User;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Utils\Helper;
|
||||
@@ -37,6 +38,7 @@ class OrderService
|
||||
* @param Plan $plan
|
||||
* @param string $period
|
||||
* @param string|null $couponCode
|
||||
* @param array|null $telegramMessageIds
|
||||
* @return Order
|
||||
* @throws ApiException
|
||||
*/
|
||||
@@ -106,7 +108,7 @@ class OrderService
|
||||
$this->buyByOneTime($plan);
|
||||
break;
|
||||
case Plan::PERIOD_RESET_TRAFFIC:
|
||||
$this->buyByResetTraffic();
|
||||
app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
|
||||
break;
|
||||
default:
|
||||
$this->buyByPeriod($order, $plan);
|
||||
@@ -321,7 +323,7 @@ class OrderService
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -336,12 +338,6 @@ class OrderService
|
||||
$this->user->device_limit = $deviceLimit;
|
||||
}
|
||||
|
||||
private function buyByResetTraffic()
|
||||
{
|
||||
$this->user->u = 0;
|
||||
$this->user->d = 0;
|
||||
}
|
||||
|
||||
private function buyByPeriod(Order $order, Plan $plan)
|
||||
{
|
||||
// change plan process
|
||||
@@ -351,10 +347,10 @@ class OrderService
|
||||
$this->user->transfer_enable = $plan->transfer_enable * 1073741824;
|
||||
// 从一次性转换到循环
|
||||
if ($this->user->expired_at === NULL)
|
||||
$this->buyByResetTraffic();
|
||||
app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
|
||||
// 新购
|
||||
if ($order->type === Order::TYPE_NEW_PURCHASE)
|
||||
$this->buyByResetTraffic();
|
||||
app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
|
||||
$this->user->plan_id = $plan->id;
|
||||
$this->user->group_id = $plan->group_id;
|
||||
$this->user->expired_at = $this->getTime($order->period, $this->user->expired_at);
|
||||
@@ -362,7 +358,7 @@ class OrderService
|
||||
|
||||
private function buyByOneTime(Plan $plan)
|
||||
{
|
||||
$this->buyByResetTraffic();
|
||||
app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
|
||||
$this->user->transfer_enable = $plan->transfer_enable * 1073741824;
|
||||
$this->user->plan_id = $plan->id;
|
||||
$this->user->group_id = $plan->group_id;
|
||||
@@ -397,7 +393,7 @@ class OrderService
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
$this->buyByResetTraffic();
|
||||
app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +60,6 @@ class TrafficResetService
|
||||
]);
|
||||
|
||||
$this->clearUserCache($user);
|
||||
|
||||
Log::info(__('traffic_reset.reset_success'), [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'old_traffic' => $oldTotal,
|
||||
'trigger_source' => $triggerSource,
|
||||
]);
|
||||
|
||||
return true;
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
@@ -283,11 +275,6 @@ class TrafficResetService
|
||||
$errors = [];
|
||||
$lastProcessedId = 0;
|
||||
|
||||
Log::info('Starting batch traffic reset task.', [
|
||||
'batch_size' => $batchSize,
|
||||
'start_time' => now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
try {
|
||||
do {
|
||||
$users = User::where('next_reset_at', '<=', time())
|
||||
@@ -307,9 +294,7 @@ class TrafficResetService
|
||||
break;
|
||||
}
|
||||
|
||||
$batchStartTime = microtime(true);
|
||||
$batchResetCount = 0;
|
||||
$batchErrors = [];
|
||||
|
||||
if ($progressCallback) {
|
||||
$progressCallback([
|
||||
@@ -319,13 +304,6 @@ class TrafficResetService
|
||||
]);
|
||||
}
|
||||
|
||||
Log::info("Processing batch #{$batchNumber}", [
|
||||
'batch_number' => $batchNumber,
|
||||
'batch_size' => $users->count(),
|
||||
'total_processed' => $totalProcessedCount,
|
||||
'id_range' => $users->first()->id . '-' . $users->last()->id,
|
||||
]);
|
||||
|
||||
foreach ($users as $user) {
|
||||
try {
|
||||
if ($this->checkAndReset($user, TrafficResetLog::SOURCE_CRON)) {
|
||||
@@ -352,17 +330,6 @@ class TrafficResetService
|
||||
}
|
||||
}
|
||||
|
||||
$batchDuration = round(microtime(true) - $batchStartTime, 2);
|
||||
|
||||
Log::info("Batch #{$batchNumber} processing complete", [
|
||||
'batch_number' => $batchNumber,
|
||||
'processed_count' => $users->count(),
|
||||
'reset_count' => $batchResetCount,
|
||||
'error_count' => count($batchErrors),
|
||||
'duration' => $batchDuration,
|
||||
'last_processed_id' => $lastProcessedId,
|
||||
]);
|
||||
|
||||
$batchNumber++;
|
||||
|
||||
if ($batchNumber % 10 === 0) {
|
||||
@@ -407,8 +374,6 @@ class TrafficResetService
|
||||
'completed_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
Log::info('Batch traffic reset task completed', $result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Jobs\StatUserJob;
|
||||
use App\Jobs\TrafficFetchJob;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\TrafficResetService;
|
||||
@@ -113,8 +114,11 @@ class UserService
|
||||
return true;
|
||||
}
|
||||
|
||||
public function trafficFetch(array $server, string $protocol, array $data)
|
||||
public function trafficFetch(Server $server, string $protocol, array $data)
|
||||
{
|
||||
$server->rate = $server->getCurrentRate();
|
||||
$server = $server->toArray();
|
||||
|
||||
list($server, $protocol, $data) = HookManager::filter('traffic.before_process', [
|
||||
$server,
|
||||
$protocol,
|
||||
@@ -227,6 +231,44 @@ class UserService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户分配一个新套餐或续费现有套餐
|
||||
*
|
||||
* @param User $user 用户模型
|
||||
* @param Plan $plan 套餐模型
|
||||
* @param int $validityDays 购买天数
|
||||
* @return User 更新后的用户模型
|
||||
*/
|
||||
public function assignPlan(User $user, Plan $plan, int $validityDays): User
|
||||
{
|
||||
$user->plan_id = $plan->id;
|
||||
$user->group_id = $plan->group_id;
|
||||
$user->transfer_enable = $plan->transfer_enable * 1073741824;
|
||||
$user->speed_limit = $plan->speed_limit;
|
||||
|
||||
if ($validityDays > 0) {
|
||||
$user = $this->extendSubscription($user, $validityDays);
|
||||
}
|
||||
|
||||
$user->save();
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 延长用户的订阅有效期
|
||||
*
|
||||
* @param User $user 用户模型
|
||||
* @param int $days 延长天数
|
||||
* @return User 更新后的用户模型
|
||||
*/
|
||||
public function extendSubscription(User $user, int $days): User
|
||||
{
|
||||
$currentExpired = $user->expired_at ?? time();
|
||||
$user->expired_at = max($currentExpired, time()) + ($days * 86400);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置试用计划
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 礼品卡模板表
|
||||
Schema::create('v2_gift_card_template', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('礼品卡名称');
|
||||
$table->text('description')->nullable()->comment('礼品卡描述');
|
||||
$table->tinyInteger('type')->comment('卡片类型:1余额 2有效期 3流量 4重置包 5套餐 6组合 7盲盒 8任务 9等级 10节日');
|
||||
$table->tinyInteger('status')->default(1)->comment('状态:0禁用 1启用');
|
||||
$table->json('conditions')->nullable()->comment('使用条件配置');
|
||||
$table->json('rewards')->comment('奖励配置');
|
||||
$table->json('limits')->nullable()->comment('限制条件');
|
||||
$table->json('special_config')->nullable()->comment('特殊配置(节日时间、等级倍率等)');
|
||||
$table->string('icon')->nullable()->comment('卡片图标');
|
||||
$table->string('background_image')->nullable()->comment('背景图片URL');
|
||||
$table->string('theme_color', 7)->default('#1890ff')->comment('主题色');
|
||||
$table->integer('sort')->default(0)->comment('排序');
|
||||
$table->integer('admin_id')->comment('创建管理员ID');
|
||||
$table->integer('created_at');
|
||||
$table->integer('updated_at');
|
||||
|
||||
$table->index(['type', 'status'], 'idx_gift_template_type_status');
|
||||
$table->index('created_at', 'idx_gift_template_created_at');
|
||||
});
|
||||
|
||||
// 礼品卡兑换码表
|
||||
Schema::create('v2_gift_card_code', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('template_id')->comment('模板ID');
|
||||
$table->string('code', 32)->unique()->comment('兑换码');
|
||||
$table->string('batch_id', 32)->nullable()->comment('批次ID');
|
||||
$table->tinyInteger('status')->default(0)->comment('状态:0未使用 1已使用 2已过期 3已禁用');
|
||||
$table->integer('user_id')->nullable()->comment('使用用户ID');
|
||||
$table->integer('used_at')->nullable()->comment('使用时间');
|
||||
$table->integer('expires_at')->nullable()->comment('过期时间');
|
||||
$table->json('actual_rewards')->nullable()->comment('实际获得的奖励(用于盲盒等)');
|
||||
$table->integer('usage_count')->default(0)->comment('使用次数(分享卡)');
|
||||
$table->integer('max_usage')->default(1)->comment('最大使用次数');
|
||||
$table->json('metadata')->nullable()->comment('额外数据');
|
||||
$table->integer('created_at');
|
||||
$table->integer('updated_at');
|
||||
|
||||
$table->index('template_id', 'idx_gift_code_template_id');
|
||||
$table->index('status', 'idx_gift_code_status');
|
||||
$table->index('user_id', 'idx_gift_code_user_id');
|
||||
$table->index('batch_id', 'idx_gift_code_batch_id');
|
||||
$table->index('expires_at', 'idx_gift_code_expires_at');
|
||||
$table->index(['code', 'status', 'expires_at'], 'idx_gift_code_lookup');
|
||||
});
|
||||
|
||||
// 礼品卡使用记录表
|
||||
Schema::create('v2_gift_card_usage', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('code_id')->comment('兑换码ID');
|
||||
$table->integer('template_id')->comment('模板ID');
|
||||
$table->integer('user_id')->comment('使用用户ID');
|
||||
$table->integer('invite_user_id')->nullable()->comment('邀请人ID');
|
||||
$table->json('rewards_given')->comment('实际发放的奖励');
|
||||
$table->json('invite_rewards')->nullable()->comment('邀请人获得的奖励');
|
||||
$table->integer('user_level_at_use')->nullable()->comment('使用时用户等级');
|
||||
$table->integer('plan_id_at_use')->nullable()->comment('使用时用户套餐ID');
|
||||
$table->decimal('multiplier_applied', 3, 2)->default(1.00)->comment('应用的倍率');
|
||||
$table->string('ip_address', 45)->nullable()->comment('使用IP地址');
|
||||
$table->text('user_agent')->nullable()->comment('用户代理');
|
||||
$table->text('notes')->nullable()->comment('备注');
|
||||
$table->integer('created_at');
|
||||
|
||||
$table->index('code_id', 'idx_gift_usage_code_id');
|
||||
$table->index('template_id', 'idx_gift_usage_template_id');
|
||||
$table->index('user_id', 'idx_gift_usage_user_id');
|
||||
$table->index('invite_user_id', 'idx_gift_usage_invite_user_id');
|
||||
$table->index('created_at', 'idx_gift_usage_created_at');
|
||||
$table->index(['user_id', 'created_at'], 'idx_gift_usage_user_usage');
|
||||
$table->index(['template_id', 'created_at'], 'idx_gift_usage_template_stats');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('v2_gift_card_usage');
|
||||
Schema::dropIfExists('v2_gift_card_code');
|
||||
Schema::dropIfExists('v2_gift_card_template');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('v2_server', function (Blueprint $table) {
|
||||
$table->boolean('rate_time_enable')->default(false)->comment('是否启用动态倍率')->after('rate');
|
||||
$table->json('rate_time_ranges')->nullable()->comment('动态倍率规则')->after('rate_time_enable');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('v2_server', function (Blueprint $table) {
|
||||
$table->dropColumn('rate_time_enable');
|
||||
$table->dropColumn('rate_time_ranges');
|
||||
});
|
||||
}
|
||||
};
|
||||
2
public/assets/admin/assets/index.css
vendored
2
public/assets/admin/assets/index.css
vendored
File diff suppressed because one or more lines are too long
32
public/assets/admin/assets/index.js
vendored
32
public/assets/admin/assets/index.js
vendored
File diff suppressed because one or more lines are too long
445
public/assets/admin/assets/vendor.js
vendored
445
public/assets/admin/assets/vendor.js
vendored
File diff suppressed because one or more lines are too long
32
public/assets/admin/index.html
Normal file
32
public/assets/admin/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Shadcn Admin</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Admin Dashboard UI built with Shadcn and Vite."
|
||||
/>
|
||||
<script>
|
||||
window.settings = {
|
||||
base_url: 'http://127.0.0.1:8000',
|
||||
title: 'Xboard',
|
||||
version: '1.0.0',
|
||||
logo: 'https://xboard.io/i6mages/logo.png',
|
||||
secure_path: '/afbced4e',
|
||||
}
|
||||
</script>
|
||||
<script src="./locales/en-US.js"></script>
|
||||
<script src="./locales/zh-CN.js"></script>
|
||||
<script src="./locales/ko-KR.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
251
public/assets/admin/locales/en-US.js
vendored
251
public/assets/admin/locales/en-US.js
vendored
@@ -161,6 +161,7 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"planManagement": "Plan Management",
|
||||
"orderManagement": "Order Management",
|
||||
"couponManagement": "Coupon Management",
|
||||
"giftCardManagement": "Gift Card Management",
|
||||
"userManagement": "User Management",
|
||||
"trafficResetLogs": "Traffic Reset Logs",
|
||||
"ticketManagement": "Ticket Management"
|
||||
@@ -555,7 +556,8 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"subscribe_path": {
|
||||
"title": "Subscription Path",
|
||||
"description": "Subscription path, modifying will change the original subscribe path",
|
||||
"current_format": "Current subscription path format: {path}/xxxxxxxxxx"
|
||||
"current_format": "Current subscription path format: {path}/xxxxxxxxxx",
|
||||
"restart_tip": "You may need to restart the service for the new subscribe path to take effect."
|
||||
},
|
||||
"show_info_to_server": {
|
||||
"title": "Show Subscription Info in Nodes",
|
||||
@@ -932,6 +934,8 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"error": "Copy failed",
|
||||
"errorLog": "Error copying to clipboard"
|
||||
},
|
||||
"submit": "Submit",
|
||||
"saving": "Saving...",
|
||||
"table": {
|
||||
"noData": "No data available",
|
||||
"pagination": {
|
||||
@@ -955,6 +959,15 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"updating": "Updating...",
|
||||
"updateSuccess": "Update successful, system will restart shortly",
|
||||
"updateFailed": "Update failed, please try again later"
|
||||
},
|
||||
"time": {
|
||||
"day": "day",
|
||||
"hour": " hour(s)"
|
||||
},
|
||||
"reset": "Reset",
|
||||
"export": "Export",
|
||||
"currency": {
|
||||
"yuan": "Yuan"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -1164,6 +1177,241 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"loading": "Searching..."
|
||||
}
|
||||
},
|
||||
"giftCard": {
|
||||
"types": {
|
||||
"1": "General Gift Card",
|
||||
"2": "Plan-Specific Gift Card",
|
||||
"3": "Mystery Box Reward",
|
||||
"4": "Task Gift Card"
|
||||
},
|
||||
"status": {
|
||||
"0": "Unused",
|
||||
"1": "Used",
|
||||
"2": "Disabled",
|
||||
"3": "Expired"
|
||||
},
|
||||
"template": {
|
||||
"title": "Gift Card Templates",
|
||||
"add": "Add Template",
|
||||
"search": "Search template name...",
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"placeholder": "Enter template name"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sort",
|
||||
"placeholder": "Smaller numbers come first"
|
||||
},
|
||||
"type": {
|
||||
"label": "Type",
|
||||
"placeholder": "Select gift card type"
|
||||
},
|
||||
"description": {
|
||||
"label": "Description",
|
||||
"placeholder": "Enter gift card description"
|
||||
},
|
||||
"status": {
|
||||
"label": "Status",
|
||||
"description": "If disabled, this template cannot be used to generate or redeem new gift cards."
|
||||
},
|
||||
"display": {
|
||||
"title": "Display Settings"
|
||||
},
|
||||
"theme_color": {
|
||||
"label": "Theme Color"
|
||||
},
|
||||
"icon": {
|
||||
"label": "Icon",
|
||||
"placeholder": "Enter the URL of the icon"
|
||||
},
|
||||
"background_image": {
|
||||
"label": "Background Image",
|
||||
"placeholder": "Enter the URL of the background image"
|
||||
},
|
||||
"conditions": {
|
||||
"title": "Usage Conditions",
|
||||
"new_user_max_days": {
|
||||
"label": "New User Registration Day Limit",
|
||||
"placeholder": "e.g., 7 (only for users registered within 7 days)"
|
||||
},
|
||||
"new_user_only": {
|
||||
"label": "New Users Only"
|
||||
},
|
||||
"paid_user_only": {
|
||||
"label": "Paid Users Only"
|
||||
},
|
||||
"require_invite": {
|
||||
"label": "Requires Invitation"
|
||||
},
|
||||
"allowed_plans": {
|
||||
"label": "Allowed Plans",
|
||||
"placeholder": "Select allowed plans (leave empty for no restriction)"
|
||||
},
|
||||
"disallowed_plans": {
|
||||
"label": "Disallowed Plans",
|
||||
"placeholder": "Select disallowed plans (leave empty for no restriction)"
|
||||
}
|
||||
},
|
||||
"limits": {
|
||||
"title": "Usage Limits",
|
||||
"max_use_per_user": {
|
||||
"label": "Max Uses Per User",
|
||||
"placeholder": "Leave empty for no limit"
|
||||
},
|
||||
"cooldown_hours": {
|
||||
"label": "Cooldown for Same Type (Hours)",
|
||||
"placeholder": "Leave empty for no limit"
|
||||
},
|
||||
"invite_reward_rate": {
|
||||
"label": "Inviter Reward Rate",
|
||||
"placeholder": "e.g., 0.2 (for 20%)",
|
||||
"description": "If the user has an inviter, the inviter's reward = balance reward * this rate"
|
||||
}
|
||||
},
|
||||
"rewards": {
|
||||
"title": "Reward Content",
|
||||
"balance": {
|
||||
"label": "Reward Amount (in Yuan)",
|
||||
"short_label": "Balance",
|
||||
"placeholder": "Enter the reward amount in Yuan"
|
||||
},
|
||||
"transfer_enable": {
|
||||
"label": "Reward Traffic (in bytes)",
|
||||
"short_label": "Traffic",
|
||||
"placeholder": "Enter the reward traffic in bytes"
|
||||
},
|
||||
"expire_days": {
|
||||
"label": "Extend Validity (in days)",
|
||||
"short_label": "Validity",
|
||||
"placeholder": "Enter the number of days to extend"
|
||||
},
|
||||
"transfer": {
|
||||
"label": "Reward Traffic (in bytes)",
|
||||
"placeholder": "Enter the reward traffic in bytes"
|
||||
},
|
||||
"days": {
|
||||
"label": "Extend Validity (in days)",
|
||||
"placeholder": "Enter the number of days to extend"
|
||||
},
|
||||
"device_limit": {
|
||||
"label": "Increase Device Limit",
|
||||
"short_label": "Devices",
|
||||
"placeholder": "Enter the number of devices to increase"
|
||||
},
|
||||
"reset_package": {
|
||||
"label": "Reset Monthly Traffic",
|
||||
"description": "If enabled, the user's current plan traffic usage will be reset to zero upon redemption."
|
||||
},
|
||||
"reset_count": {
|
||||
"description": "This type of card will reset the user's traffic usage for the current month."
|
||||
},
|
||||
"task_card": {
|
||||
"description": "The specific rewards for task gift cards will be configured in the task system."
|
||||
},
|
||||
"plan_id": {
|
||||
"label": "Specify Plan",
|
||||
"short_label": "Plan",
|
||||
"placeholder": "Select a plan"
|
||||
},
|
||||
"plan_validity_days": {
|
||||
"label": "Plan Validity (in days)",
|
||||
"short_label": "Plan Validity",
|
||||
"placeholder": "Leave empty to use the plan's default validity"
|
||||
},
|
||||
"random_rewards": {
|
||||
"label": "Random Reward Pool",
|
||||
"add": "Add Random Reward Item",
|
||||
"weight": "Weight"
|
||||
}
|
||||
},
|
||||
"special_config": {
|
||||
"title": "Special Configuration",
|
||||
"start_time": {
|
||||
"label": "Event Start Time",
|
||||
"placeholder": "Pick a start date"
|
||||
},
|
||||
"end_time": {
|
||||
"label": "Event End Time",
|
||||
"placeholder": "Pick an end date"
|
||||
},
|
||||
"festival_bonus": {
|
||||
"label": "Festival Reward Multiplier",
|
||||
"placeholder": "e.g., 1.5 (for 1.5x)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"title": "Code Management",
|
||||
"add": "Generate Codes",
|
||||
"search": "Search codes...",
|
||||
"form": {
|
||||
"template_id": {
|
||||
"label": "Gift Card Template",
|
||||
"placeholder": "Select a template"
|
||||
},
|
||||
"count": {
|
||||
"label": "Quantity to Generate",
|
||||
"placeholder": "Enter the quantity to generate"
|
||||
},
|
||||
"prefix": {
|
||||
"label": "Code Prefix",
|
||||
"placeholder": "Leave empty for default prefix GC"
|
||||
},
|
||||
"expires_hours": {
|
||||
"label": "Validity (Hours)",
|
||||
"placeholder": "From generation time, leave empty for no expiration"
|
||||
},
|
||||
"max_usage": {
|
||||
"label": "Max Usage Count",
|
||||
"placeholder": "Total times each code can be used"
|
||||
},
|
||||
"download_csv": "Export as CSV"
|
||||
},
|
||||
"table": {
|
||||
"code": "Code",
|
||||
"template": "Template",
|
||||
"status": "Status",
|
||||
"expires_at": "Expires At",
|
||||
"used_at": "Used At",
|
||||
"used_by": "Used By",
|
||||
"max_usage": "Max Uses",
|
||||
"usage_count": "Usage Count"
|
||||
},
|
||||
"messages": {
|
||||
"generateSuccess": "Codes generated successfully",
|
||||
"generateFailed": "Failed to generate codes",
|
||||
"deleteConfirm": "Are you sure you want to delete this code? This action cannot be undone.",
|
||||
"deleteSuccess": "Code deleted successfully",
|
||||
"deleteFailed": "Failed to delete code"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"formInvalid": "Form validation failed, please check your input.",
|
||||
"templateCreated": "Template created successfully",
|
||||
"templateUpdated": "Template updated successfully",
|
||||
"createTemplateFailed": "Failed to create template",
|
||||
"updateTemplateFailed": "Failed to update template",
|
||||
"deleteConfirm": "Are you sure you want to delete this template? All codes under it will also be deleted.",
|
||||
"deleteSuccess": "Template deleted successfully",
|
||||
"deleteFailed": "Failed to delete template",
|
||||
"codesGenerated": "Codes generated successfully"
|
||||
},
|
||||
"table": {
|
||||
"columns": {
|
||||
"no_rewards": "No Rewards"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"currency": {
|
||||
"yuan": "Yuan"
|
||||
},
|
||||
"time": {
|
||||
"day": "day"
|
||||
}
|
||||
}
|
||||
},
|
||||
"route": {
|
||||
"title": "Route Management",
|
||||
"description": "Manage all route groups, including adding, deleting, and editing operations.",
|
||||
@@ -1705,6 +1953,7 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"error_gte_zero": "Base rate must be greater than or equal to 0"
|
||||
},
|
||||
"dynamic_rate": {
|
||||
"section_title": "Dynamic Rate Configuration",
|
||||
"enable_label": "Enable Dynamic Rate",
|
||||
"enable_description": "Set different rate multipliers based on time periods",
|
||||
"rules_label": "Time Period Rules",
|
||||
|
||||
358
public/assets/admin/locales/zh-CN.js
vendored
358
public/assets/admin/locales/zh-CN.js
vendored
@@ -161,6 +161,7 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"planManagement": "套餐管理",
|
||||
"orderManagement": "订单管理",
|
||||
"couponManagement": "优惠券管理",
|
||||
"giftCardManagement": "礼品卡管理",
|
||||
"userManagement": "用户管理",
|
||||
"ticketManagement": "工单管理",
|
||||
"trafficResetLogs": "流量重置日志"
|
||||
@@ -475,7 +476,8 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"subscribe_path": {
|
||||
"title": "订阅路径",
|
||||
"description": "订阅路径,修改后将会改变原有的subscribe路径",
|
||||
"current_format": "当前订阅路径格式:{path}/xxxxxxxxxx"
|
||||
"current_format": "当前订阅路径格式:{path}/xxxxxxxxxx",
|
||||
"restart_tip": "修改订阅路径后,可能需要重启服务才能生效。"
|
||||
},
|
||||
"show_info_to_server": {
|
||||
"title": "在订阅中展示订阅信息",
|
||||
@@ -937,6 +939,8 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"error": "复制失败",
|
||||
"errorLog": "复制到剪贴板时出错"
|
||||
},
|
||||
"submit": "提交",
|
||||
"saving": "保存中...",
|
||||
"table": {
|
||||
"noData": "暂无数据",
|
||||
"pagination": {
|
||||
@@ -960,6 +964,15 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"updating": "更新中...",
|
||||
"updateSuccess": "更新成功,系统将在稍后自动重启",
|
||||
"updateFailed": "更新失败,请稍后重试"
|
||||
},
|
||||
"time": {
|
||||
"day": "天",
|
||||
"hour": "小时"
|
||||
},
|
||||
"reset": "重置",
|
||||
"export": "导出",
|
||||
"currency": {
|
||||
"yuan": "元"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -1156,6 +1169,348 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"pagination": "第 {{current}}/{{total}} 页,共 {{count}} 条"
|
||||
}
|
||||
},
|
||||
"giftCard": {
|
||||
"title": "礼品卡管理",
|
||||
"description": "在这里可以管理礼品卡模板、兑换码和使用记录等功能。",
|
||||
"tabs": {
|
||||
"templates": "模板管理",
|
||||
"codes": "兑换码管理",
|
||||
"usages": "使用记录",
|
||||
"statistics": "统计数据"
|
||||
},
|
||||
"template": {
|
||||
"title": "模板管理",
|
||||
"description": "管理礼品卡模板,包括创建、编辑和删除模板。",
|
||||
"table": {
|
||||
"title": "模板列表",
|
||||
"columns": {
|
||||
"id": "ID",
|
||||
"name": "名称",
|
||||
"type": "类型",
|
||||
"status": "状态",
|
||||
"sort": "排序",
|
||||
"rewards": "奖励内容",
|
||||
"created_at": "创建时间",
|
||||
"actions": "操作",
|
||||
"no_rewards": "无奖励"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"add": "添加模板",
|
||||
"edit": "编辑模板",
|
||||
"name": {
|
||||
"label": "模板名称",
|
||||
"placeholder": "请输入模板名称",
|
||||
"required": "请输入模板名称"
|
||||
},
|
||||
"sort": {
|
||||
"label": "排序",
|
||||
"placeholder": "数字越小越靠前"
|
||||
},
|
||||
"type": {
|
||||
"label": "类型",
|
||||
"placeholder": "请选择礼品卡类型"
|
||||
},
|
||||
"description": {
|
||||
"label": "描述",
|
||||
"placeholder": "请输入礼品卡描述"
|
||||
},
|
||||
"status": {
|
||||
"label": "状态",
|
||||
"description": "禁用后,此模板将无法生成或兑换新的礼品卡。"
|
||||
},
|
||||
"display": {
|
||||
"title": "显示效果"
|
||||
},
|
||||
"theme_color": {
|
||||
"label": "主题颜色"
|
||||
},
|
||||
"icon": {
|
||||
"label": "图标",
|
||||
"placeholder": "请输入图标的URL"
|
||||
},
|
||||
"background_image": {
|
||||
"label": "背景图片",
|
||||
"placeholder": "请输入背景图片的URL"
|
||||
},
|
||||
"conditions": {
|
||||
"title": "使用条件",
|
||||
"new_user_max_days": {
|
||||
"label": "新用户注册天数限制",
|
||||
"placeholder": "例如: 7 (仅限注册7天内的用户)"
|
||||
},
|
||||
"new_user_only": {
|
||||
"label": "仅限新用户"
|
||||
},
|
||||
"paid_user_only": {
|
||||
"label": "仅限付费用户"
|
||||
},
|
||||
"require_invite": {
|
||||
"label": "需要邀请关系"
|
||||
},
|
||||
"allowed_plans": {
|
||||
"label": "允许的套餐",
|
||||
"placeholder": "选择允许兑换的套餐 (留空则不限制)"
|
||||
},
|
||||
"disallowed_plans": {
|
||||
"label": "禁止的套餐",
|
||||
"placeholder": "选择禁止兑换的套餐 (留空则不限制)"
|
||||
}
|
||||
},
|
||||
"limits": {
|
||||
"title": "使用限制",
|
||||
"max_use_per_user": {
|
||||
"label": "单用户最大使用次数",
|
||||
"placeholder": "留空则不限制"
|
||||
},
|
||||
"cooldown_hours": {
|
||||
"label": "同类卡冷却时间(小时)",
|
||||
"placeholder": "留空则不限制"
|
||||
},
|
||||
"invite_reward_rate": {
|
||||
"label": "邀请人奖励比例",
|
||||
"placeholder": "例如: 0.2 (代表20%)",
|
||||
"description": "使用者有邀请人时,给邀请人的奖励 = 余额奖励 * 此比例"
|
||||
}
|
||||
},
|
||||
"rewards": {
|
||||
"title": "奖励内容",
|
||||
"balance": {
|
||||
"label": "奖励余额 (元)",
|
||||
"short_label": "余额",
|
||||
"placeholder": "请输入奖励的金额(元)"
|
||||
},
|
||||
"transfer_enable": {
|
||||
"label": "奖励流量 (字节)",
|
||||
"short_label": "流量",
|
||||
"placeholder": "请输入奖励的流量(字节)"
|
||||
},
|
||||
"expire_days": {
|
||||
"label": "延长有效期 (天)",
|
||||
"short_label": "有效期",
|
||||
"placeholder": "请输入延长的天数"
|
||||
},
|
||||
"transfer": {
|
||||
"label": "奖励流量 (字节)",
|
||||
"placeholder": "请输入奖励的流量(字节)"
|
||||
},
|
||||
"days": {
|
||||
"label": "延长有效期 (天)",
|
||||
"placeholder": "请输入延长的天数"
|
||||
},
|
||||
"device_limit": {
|
||||
"label": "增加设备数",
|
||||
"short_label": "设备数",
|
||||
"placeholder": "请输入增加的设备数量"
|
||||
},
|
||||
"reset_package": {
|
||||
"label": "重置当月流量",
|
||||
"description": "开启后,兑换时会将用户当前套餐的已用流量清零。"
|
||||
},
|
||||
"reset_count": {
|
||||
"description": "该类型卡将重置用户当月的流量使用。"
|
||||
},
|
||||
"task_card": {
|
||||
"description": "任务礼品卡的具体奖励将在任务系统中配置。"
|
||||
},
|
||||
"plan_id": {
|
||||
"label": "指定套餐",
|
||||
"short_label": "套餐",
|
||||
"placeholder": "请选择一个套餐"
|
||||
},
|
||||
"plan_validity_days": {
|
||||
"label": "套餐有效期 (天)",
|
||||
"short_label": "套餐有效期",
|
||||
"placeholder": "留空则使用套餐默认有效期"
|
||||
},
|
||||
"random_rewards": {
|
||||
"label": "随机奖励池",
|
||||
"add": "添加随机奖励项",
|
||||
"weight": "权重"
|
||||
}
|
||||
},
|
||||
"special_config": {
|
||||
"title": "特殊配置",
|
||||
"start_time": {
|
||||
"label": "活动开始时间",
|
||||
"placeholder": "请选择开始日期"
|
||||
},
|
||||
"end_time": {
|
||||
"label": "活动结束时间",
|
||||
"placeholder": "请选择结束日期"
|
||||
},
|
||||
"festival_bonus": {
|
||||
"label": "节日奖励乘数",
|
||||
"placeholder": "例如: 1.5 (代表1.5倍)"
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"saving": "保存中...",
|
||||
"save": "保存"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"deleteConfirm": {
|
||||
"title": "确认删除",
|
||||
"description": "此操作将永久删除该模板,确定要继续吗?",
|
||||
"confirmText": "删除"
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"title": "兑换码管理",
|
||||
"form": {
|
||||
"generate": "生成兑换码",
|
||||
"template_id": {
|
||||
"label": "选择模板",
|
||||
"placeholder": "请选择一个模板来生成兑换码"
|
||||
},
|
||||
"count": {
|
||||
"label": "生成数量"
|
||||
},
|
||||
"prefix": {
|
||||
"label": "自定义前缀 (可选)"
|
||||
},
|
||||
"expires_hours": {
|
||||
"label": "有效期 (小时)"
|
||||
},
|
||||
"max_usage": {
|
||||
"label": "最大使用次数"
|
||||
},
|
||||
"download_csv": "导出CSV",
|
||||
"submit": {
|
||||
"generating": "生成中...",
|
||||
"generate": "立即生成"
|
||||
}
|
||||
},
|
||||
"description": "管理礼品卡兑换码,包括生成、查看和导出兑换码。",
|
||||
"generate": {
|
||||
"title": "生成兑换码",
|
||||
"template": "选择模板",
|
||||
"count": "生成数量",
|
||||
"prefix": "自定义前缀",
|
||||
"expires_hours": "有效期 (小时)",
|
||||
"max_usage": "最大使用次数",
|
||||
"submit": "生成"
|
||||
},
|
||||
"table": {
|
||||
"title": "兑换码列表",
|
||||
"columns": {
|
||||
"id": "ID",
|
||||
"code": "兑换码",
|
||||
"template_name": "模板名称",
|
||||
"status": "状态",
|
||||
"expires_at": "过期时间",
|
||||
"usage_count": "已用次数",
|
||||
"max_usage": "可用次数",
|
||||
"created_at": "创建时间"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"export": "导出",
|
||||
"exportConfirm": {
|
||||
"title": "确认导出",
|
||||
"description": "将导出选定批次的所有兑换码为文本文件。确定要继续吗?",
|
||||
"confirmText": "导出"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"0": "未使用",
|
||||
"1": "已使用",
|
||||
"2": "已禁用",
|
||||
"3": "已过期"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"title": "使用记录",
|
||||
"description": "查看礼品卡的使用记录和详细信息。",
|
||||
"table": {
|
||||
"columns": {
|
||||
"id": "ID",
|
||||
"code": "兑换码",
|
||||
"template_name": "模板名称",
|
||||
"user_email": "用户邮箱",
|
||||
"rewards_given": "获得奖励",
|
||||
"invite_rewards": "邀请奖励",
|
||||
"multiplier_applied": "倍数加成",
|
||||
"ip_address": "IP地址",
|
||||
"created_at": "使用时间",
|
||||
"actions": "操作"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"view": "查看详情"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
"title": "统计数据",
|
||||
"description": "查看礼品卡的统计数据和使用情况分析。",
|
||||
"total": {
|
||||
"title": "总体统计",
|
||||
"templates_count": "模板总数",
|
||||
"active_templates_count": "活跃模板数",
|
||||
"codes_count": "兑换码总数",
|
||||
"used_codes_count": "已使用兑换码",
|
||||
"usages_count": "使用记录数"
|
||||
},
|
||||
"daily": {
|
||||
"title": "每日使用量",
|
||||
"chart": "使用量趋势图"
|
||||
},
|
||||
"type": {
|
||||
"title": "类型统计",
|
||||
"chart": "类型分布图"
|
||||
},
|
||||
"dateRange": {
|
||||
"label": "日期范围",
|
||||
"start": "开始日期",
|
||||
"end": "结束日期"
|
||||
}
|
||||
},
|
||||
"types": {
|
||||
"1": "通用礼品卡",
|
||||
"2": "套餐礼品卡",
|
||||
"3": "盲盒礼品卡",
|
||||
"4": "任务礼品卡"
|
||||
},
|
||||
"common": {
|
||||
"search": "搜索礼品卡...",
|
||||
"reset": "重置",
|
||||
"filter": "筛选",
|
||||
"export": "导出",
|
||||
"refresh": "刷新",
|
||||
"back": "返回",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"loading": "加载中...",
|
||||
"noData": "暂无数据",
|
||||
"success": "操作成功",
|
||||
"error": "操作失败"
|
||||
},
|
||||
"messages": {
|
||||
"formInvalid": "请检查表单输入是否正确",
|
||||
"templateCreated": "模板创建成功",
|
||||
"templateUpdated": "模板更新成功",
|
||||
"templateDeleted": "模板删除成功",
|
||||
"codeGenerated": "兑换码生成成功",
|
||||
"generateCodeFailed": "兑换码生成失败",
|
||||
"codeStatusUpdated": "兑换码状态更新成功",
|
||||
"updateCodeStatusFailed": "兑换码状态更新失败",
|
||||
"codesExported": "兑换码导出成功",
|
||||
"createTemplateFailed": "创建模板失败",
|
||||
"updateTemplateFailed": "更新模板失败",
|
||||
"deleteTemplateFailed": "删除模板失败",
|
||||
"loadDataFailed": "加载数据失败",
|
||||
"codesGenerated": "兑换码生成成功"
|
||||
}
|
||||
},
|
||||
"route": {
|
||||
"title": "路由管理",
|
||||
"description": "管理所有路由组,包括添加、删除、编辑等操作。",
|
||||
@@ -1672,6 +2027,7 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"error_gte_zero": "基础倍率必须大于或等于0"
|
||||
},
|
||||
"dynamic_rate": {
|
||||
"section_title": "动态倍率配置",
|
||||
"enable_label": "启用动态倍率",
|
||||
"enable_description": "根据时间段设置不同的倍率乘数",
|
||||
"rules_label": "时间段规则",
|
||||
|
||||
Reference in New Issue
Block a user