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:
xboard
2025-07-14 00:33:04 +08:00
parent a01b94f131
commit a838a43ae5
38 changed files with 3056 additions and 325 deletions
+3 -4
View File
@@ -2,6 +2,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\User;
use App\Services\TrafficResetService; use App\Services\TrafficResetService;
use App\Utils\Helper; use App\Utils\Helper;
use Carbon\Carbon; use Carbon\Carbon;
@@ -54,8 +55,6 @@ class ResetTraffic extends Command
// 设置最大执行时间 // 设置最大执行时间
set_time_limit($maxTime); set_time_limit($maxTime);
$startTime = microtime(true);
try { try {
if ($dryRun) { if ($dryRun) {
$result = $this->performDryRun($batchSize); $result = $this->performDryRun($batchSize);
@@ -129,7 +128,7 @@ class ResetTraffic extends Command
{ {
$this->info("🔍 扫描需要重置的用户..."); $this->info("🔍 扫描需要重置的用户...");
$totalUsers = \App\Models\User::where('next_reset_at', '<=', time()) $totalUsers = User::where('next_reset_at', '<=', time())
->whereNotNull('next_reset_at') ->whereNotNull('next_reset_at')
->where(function ($query) { ->where(function ($query) {
$query->where('expired_at', '>', time()) $query->where('expired_at', '>', time())
@@ -160,7 +159,7 @@ class ResetTraffic extends Command
// 显示前几个用户的详情作为示例 // 显示前几个用户的详情作为示例
if ($this->option('verbose') || $totalUsers <= 20) { 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') ->whereNotNull('next_reset_at')
->where(function ($query) { ->where(function ($query) {
$query->where('expired_at', '>', time()) $query->where('expired_at', '>', time())
+7 -3
View File
@@ -2,7 +2,9 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Services\UpdateService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class XboardUpdate extends Command class XboardUpdate extends Command
@@ -39,9 +41,11 @@ class XboardUpdate extends Command
public function handle() public function handle()
{ {
$this->info('正在导入数据库请稍等...'); $this->info('正在导入数据库请稍等...');
\Artisan::call("migrate"); Artisan::call("migrate");
$this->info(\Artisan::output()); $this->info(Artisan::output());
\Artisan::call('horizon:terminate'); Artisan::call('horizon:terminate');
$updateService = new UpdateService();
$updateService->updateVersionCache();
$this->info('更新完毕,队列服务已重启,你无需进行任何操作。'); $this->info('更新完毕,队列服务已重启,你无需进行任何操作。');
} }
} }
+17 -42
View File
@@ -16,7 +16,7 @@ trait ApiResponse
* @param array $codeResponse * @param array $codeResponse
* @return JsonResponse * @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); return $this->jsonResponse('success', $codeResponse, $data, null);
} }
@@ -28,7 +28,7 @@ trait ApiResponse
* @param mixed $error * @param mixed $error
* @return JsonResponse * @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); return $this->jsonResponse('fail', $codeResponse, $data, $error);
} }
@@ -46,49 +46,24 @@ trait ApiResponse
list($code, $message) = $codeResponse; list($code, $message) = $codeResponse;
return response() return response()
->json([ ->json([
'status' => $status, 'status' => $status,
// 'code' => $code, // 'code' => $code,
'message' => $message, 'message' => $message,
'data' => $data ?? null, 'data' => $data ?? null,
'error' => $error, 'error' => $error,
],(int)substr(((string) $code),0,3)); ], (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 response()->json([
return [ 'total' => $page->total(),
'total' => $page->total(), 'current_page' => $page->currentPage(),
'page' => $page->currentPage(), 'per_page' => $page->perPage(),
'limit' => $page->perPage(), 'last_page' => $page->lastPage(),
'pages' => $page->lastPage(), 'data' => $page->items()
'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 // 数据
];
} }
/** /**
@@ -97,7 +72,7 @@ trait ApiResponse
* @param string $info * @param string $info
* @throws BusinessException * @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); throw new BusinessException($codeResponse, $info);
} }
@@ -55,7 +55,7 @@ class ShadowsocksTidalabController extends Controller
foreach ($data as $item) { foreach ($data as $item) {
$formatData[$item['user_id']] = [$item['u'], $item['d']]; $formatData[$item['user_id']] = [$item['u'], $item['d']];
} }
$userService->trafficFetch($server->toArray(), 'shadowsocks', $formatData); $userService->trafficFetch($server, 'shadowsocks', $formatData);
return response([ return response([
'ret' => 1, 'ret' => 1,
@@ -62,7 +62,7 @@ class TrojanTidalabController extends Controller
foreach ($data as $item) { foreach ($data as $item) {
$formatData[$item['user_id']] = [$item['u'], $item['d']]; $formatData[$item['user_id']] = [$item['u'], $item['d']];
} }
$userService->trafficFetch($server->toArray(), 'trojan', $formatData); $userService->trafficFetch($server, 'trojan', $formatData);
return response([ return response([
'ret' => 1, 'ret' => 1,
@@ -79,7 +79,7 @@ class UniProxyController extends Controller
); );
$userService = new UserService(); $userService = new UserService();
$userService->trafficFetch($node->toArray(), $nodeType, $data); $userService->trafficFetch($node, $nodeType, $data);
return $this->success(true); return $this->success(true);
} }
@@ -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\TelegramService;
use App\Services\ThemeService; use App\Services\ThemeService;
use App\Utils\Dict; use App\Utils\Dict;
use Illuminate\Console\Command;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
class ConfigController extends Controller class ConfigController extends Controller
@@ -46,10 +46,7 @@ class CouponController extends Controller
$coupons = $builder $coupons = $builder
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->paginate($pageSize, ["*"], 'page', $current); ->paginate($pageSize, ["*"], 'page', $current);
return response([ return $this->paginate($coupons);
'data' => $coupons->items(),
'total' => $coupons->total()
]);
} }
public function update(Request $request) public function update(Request $request)
@@ -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 page: $current
); );
$paginatedResults->getCollection()->transform(function($order) { $paginatedResults->getCollection()->transform(function ($order) {
$orderArray = $order->toArray(); $orderArray = $order->toArray();
$orderArray['period'] = PlanService::getLegacyPeriod((string) $order->period); $orderArray['period'] = PlanService::getLegacyPeriod((string) $order->period);
return $orderArray; return $orderArray;
}); });
return response()->json($paginatedResults); return $this->paginate($paginatedResults);
} }
private function applyFiltersAndSorts(Request $request, Builder $builder): void private function applyFiltersAndSorts(Request $request, Builder $builder): void
@@ -8,6 +8,7 @@ use App\Services\Plugin\PluginManager;
use App\Services\Plugin\PluginConfigService; use App\Services\Plugin\PluginConfigService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class PluginController extends Controller class PluginController extends Controller
{ {
@@ -44,6 +45,11 @@ class PluginController extends Controller
$installed = isset($installedPlugins[$code]); $installed = isset($installedPlugins[$code]);
// 使用配置服务获取配置 // 使用配置服务获取配置
$pluginConfig = $installed ? $this->configService->getConfig($code) : ($config['config'] ?? []); $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[] = [ $plugins[] = [
'code' => $config['code'], 'code' => $config['code'],
'name' => $config['name'], 'name' => $config['name'],
@@ -53,6 +59,7 @@ class PluginController extends Controller
'is_installed' => $installed, 'is_installed' => $installed,
'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false, 'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false,
'config' => $pluginConfig, 'config' => $pluginConfig,
'readme' => $readmeContent,
]; ];
} }
} }
@@ -169,10 +169,7 @@ class UserController extends Controller
return self::transformUserData($user); return self::transformUserData($user);
}); });
return response([ return $this->paginate($users);
'data' => $users->items(),
'total' => $users->total()
]);
} }
/** /**
+5
View File
@@ -95,6 +95,11 @@ class ServerSave extends FormRequest
'excludes' => 'nullable|array', 'excludes' => 'nullable|array',
'ips' => 'nullable|array', 'ips' => 'nullable|array',
'rate' => 'required|numeric', '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', 'protocol_settings' => 'array',
]; ];
} }
@@ -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 [
//
];
}
}
@@ -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位',
];
}
}
+1
View File
@@ -29,6 +29,7 @@ class PlanResource extends JsonResource
'capacity_limit' => $this->getFormattedCapacityLimit(), 'capacity_limit' => $this->getFormattedCapacityLimit(),
'transfer_enable' => $this->resource['transfer_enable'], 'transfer_enable' => $this->resource['transfer_enable'],
'speed_limit' => $this->resource['speed_limit'], 'speed_limit' => $this->resource['speed_limit'],
'device_limit' => $this->resource['device_limit'],
'show' => (bool) $this->resource['show'], 'show' => (bool) $this->resource['show'],
'sell' => (bool) $this->resource['sell'], 'sell' => (bool) $this->resource['sell'],
'renew' => (bool) $this->resource['renew'], 'renew' => (bool) $this->resource['renew'],
+7
View File
@@ -3,6 +3,7 @@ namespace App\Http\Routes\V1;
use App\Http\Controllers\V1\User\CommController; use App\Http\Controllers\V1\User\CommController;
use App\Http\Controllers\V1\User\CouponController; 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\InviteController;
use App\Http\Controllers\V1\User\KnowledgeController; use App\Http\Controllers\V1\User\KnowledgeController;
use App\Http\Controllers\V1\User\NoticeController; use App\Http\Controllers\V1\User\NoticeController;
@@ -61,6 +62,12 @@ class UserRoute
$router->get('/server/fetch', [ServerController::class, 'fetch']); $router->get('/server/fetch', [ServerController::class, 'fetch']);
// Coupon // Coupon
$router->post('/coupon/check', [CouponController::class, 'check']); $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 // Telegram
$router->get('/telegram/getBotInfo', [TelegramController::class, 'getBotInfo']); $router->get('/telegram/getBotInfo', [TelegramController::class, 'getBotInfo']);
// Comm // Comm
+27
View File
@@ -12,6 +12,7 @@ use App\Http\Controllers\V2\Admin\StatController;
use App\Http\Controllers\V2\Admin\NoticeController; use App\Http\Controllers\V2\Admin\NoticeController;
use App\Http\Controllers\V2\Admin\TicketController; use App\Http\Controllers\V2\Admin\TicketController;
use App\Http\Controllers\V2\Admin\CouponController; 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\KnowledgeController;
use App\Http\Controllers\V2\Admin\PaymentController; use App\Http\Controllers\V2\Admin\PaymentController;
use App\Http\Controllers\V2\Admin\SystemController; use App\Http\Controllers\V2\Admin\SystemController;
@@ -158,6 +159,32 @@ class AdminRoute
$router->post('/update', [CouponController::class, 'update']); $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 // Knowledge
$router->group([ $router->group([
'prefix' => 'knowledge' 'prefix' => 'knowledge'
+259
View 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
View 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
View 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(),
]);
}
}
+5 -1
View File
@@ -31,7 +31,11 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property int|null $actual_commission_balance * @property int|null $actual_commission_balance
* @property int|null $commission_rate * @property int|null $commission_rate
* @property int|null $commission_auto_check * @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 Plan $plan
* @property-read Payment|null $payment * @property-read Payment|null $payment
* @property-read User $user * @property-read User $user
+16 -1
View File
@@ -28,6 +28,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
* @property string|null $network 网络类型 * @property string|null $network 网络类型
* @property int|null $parent_id 父节点ID * @property int|null $parent_id 父节点ID
* @property float|null $rate 倍率 * @property float|null $rate 倍率
* @property array|null $rate_time_ranges 倍率时间范围
* @property int|null $sort 排序 * @property int|null $sort 排序
* @property array|null $protocol_settings 协议设置 * @property array|null $protocol_settings 协议设置
* @property int $created_at * @property int $created_at
@@ -114,7 +115,9 @@ class Server extends Model
'last_push_at' => 'integer', 'last_push_at' => 'integer',
'show' => 'boolean', 'show' => 'boolean',
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'updated_at' => 'timestamp' 'updated_at' => 'timestamp',
'rate_time_ranges' => 'array',
'rate_time_enable' => 'boolean',
]; ];
private const PROTOCOL_CONFIGURATIONS = [ 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;
}
} }
+6 -4
View File
@@ -28,7 +28,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TrafficResetLog extends Model class TrafficResetLog extends Model
{ {
protected $table = 'v2_traffic_reset_logs'; protected $table = 'v2_traffic_reset_logs';
protected $fillable = [ protected $fillable = [
'user_id', 'user_id',
'reset_type', 'reset_type',
@@ -64,6 +64,8 @@ class TrafficResetLog extends Model
public const SOURCE_API = 'api'; public const SOURCE_API = 'api';
public const SOURCE_CRON = 'cron'; public const SOURCE_CRON = 'cron';
public const SOURCE_USER_ACCESS = 'user_access'; 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); $bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1); $pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow)); $bytes /= (1 << (10 * $pow));
return round($bytes, 2) . ' ' . $units[$pow]; return round($bytes, 2) . ' ' . $units[$pow];
} }
} }
+1
View File
@@ -32,6 +32,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property int|null $balance 余额 * @property int|null $balance 余额
* @property int|null $commission_balance 佣金余额 * @property int|null $commission_balance 佣金余额
* @property float $commission_rate 返佣比例 * @property float $commission_rate 返佣比例
* @property int|null $commission_type 返佣类型
* @property int|null $device_limit 设备限制数量 * @property int|null $device_limit 设备限制数量
* @property int|null $discount 折扣 * @property int|null $discount 折扣
* @property int|null $last_login_at 最后登录时间 * @property int|null $last_login_at 最后登录时间
+334
View 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(),
]);
}
}
+8 -12
View File
@@ -6,6 +6,7 @@ use App\Exceptions\ApiException;
use App\Jobs\OrderHandleJob; use App\Jobs\OrderHandleJob;
use App\Models\Order; use App\Models\Order;
use App\Models\Plan; use App\Models\Plan;
use App\Models\TrafficResetLog;
use App\Models\User; use App\Models\User;
use App\Services\Plugin\HookManager; use App\Services\Plugin\HookManager;
use App\Utils\Helper; use App\Utils\Helper;
@@ -37,6 +38,7 @@ class OrderService
* @param Plan $plan * @param Plan $plan
* @param string $period * @param string $period
* @param string|null $couponCode * @param string|null $couponCode
* @param array|null $telegramMessageIds
* @return Order * @return Order
* @throws ApiException * @throws ApiException
*/ */
@@ -106,7 +108,7 @@ class OrderService
$this->buyByOneTime($plan); $this->buyByOneTime($plan);
break; break;
case Plan::PERIOD_RESET_TRAFFIC: case Plan::PERIOD_RESET_TRAFFIC:
$this->buyByResetTraffic(); app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
break; break;
default: default:
$this->buyByPeriod($order, $plan); $this->buyByPeriod($order, $plan);
@@ -321,7 +323,7 @@ class OrderService
return true; return true;
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
\Log::error($e); Log::error($e);
return false; return false;
} }
} }
@@ -336,12 +338,6 @@ class OrderService
$this->user->device_limit = $deviceLimit; $this->user->device_limit = $deviceLimit;
} }
private function buyByResetTraffic()
{
$this->user->u = 0;
$this->user->d = 0;
}
private function buyByPeriod(Order $order, Plan $plan) private function buyByPeriod(Order $order, Plan $plan)
{ {
// change plan process // change plan process
@@ -351,10 +347,10 @@ class OrderService
$this->user->transfer_enable = $plan->transfer_enable * 1073741824; $this->user->transfer_enable = $plan->transfer_enable * 1073741824;
// 从一次性转换到循环 // 从一次性转换到循环
if ($this->user->expired_at === NULL) if ($this->user->expired_at === NULL)
$this->buyByResetTraffic(); app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
// 新购 // 新购
if ($order->type === Order::TYPE_NEW_PURCHASE) 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->plan_id = $plan->id;
$this->user->group_id = $plan->group_id; $this->user->group_id = $plan->group_id;
$this->user->expired_at = $this->getTime($order->period, $this->user->expired_at); $this->user->expired_at = $this->getTime($order->period, $this->user->expired_at);
@@ -362,7 +358,7 @@ class OrderService
private function buyByOneTime(Plan $plan) 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->transfer_enable = $plan->transfer_enable * 1073741824;
$this->user->plan_id = $plan->id; $this->user->plan_id = $plan->id;
$this->user->group_id = $plan->group_id; $this->user->group_id = $plan->group_id;
@@ -397,7 +393,7 @@ class OrderService
case 0: case 0:
break; break;
case 1: case 1:
$this->buyByResetTraffic(); app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
break; break;
} }
} }
-35
View File
@@ -60,14 +60,6 @@ class TrafficResetService
]); ]);
$this->clearUserCache($user); $this->clearUserCache($user);
Log::info(__('traffic_reset.reset_success'), [
'user_id' => $user->id,
'email' => $user->email,
'old_traffic' => $oldTotal,
'trigger_source' => $triggerSource,
]);
return true; return true;
}); });
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -283,11 +275,6 @@ class TrafficResetService
$errors = []; $errors = [];
$lastProcessedId = 0; $lastProcessedId = 0;
Log::info('Starting batch traffic reset task.', [
'batch_size' => $batchSize,
'start_time' => now()->toDateTimeString(),
]);
try { try {
do { do {
$users = User::where('next_reset_at', '<=', time()) $users = User::where('next_reset_at', '<=', time())
@@ -307,9 +294,7 @@ class TrafficResetService
break; break;
} }
$batchStartTime = microtime(true);
$batchResetCount = 0; $batchResetCount = 0;
$batchErrors = [];
if ($progressCallback) { if ($progressCallback) {
$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) { foreach ($users as $user) {
try { try {
if ($this->checkAndReset($user, TrafficResetLog::SOURCE_CRON)) { 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++; $batchNumber++;
if ($batchNumber % 10 === 0) { if ($batchNumber % 10 === 0) {
@@ -407,8 +374,6 @@ class TrafficResetService
'completed_at' => now()->toDateTimeString(), 'completed_at' => now()->toDateTimeString(),
]; ];
Log::info('Batch traffic reset task completed', $result);
return $result; return $result;
} }
+43 -1
View File
@@ -7,6 +7,7 @@ use App\Jobs\StatUserJob;
use App\Jobs\TrafficFetchJob; use App\Jobs\TrafficFetchJob;
use App\Models\Order; use App\Models\Order;
use App\Models\Plan; use App\Models\Plan;
use App\Models\Server;
use App\Models\User; use App\Models\User;
use App\Services\Plugin\HookManager; use App\Services\Plugin\HookManager;
use App\Services\TrafficResetService; use App\Services\TrafficResetService;
@@ -113,8 +114,11 @@ class UserService
return true; 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', [ list($server, $protocol, $data) = HookManager::filter('traffic.before_process', [
$server, $server,
$protocol, $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');
});
}
};
File diff suppressed because one or more lines are too long
+16 -16
View File
File diff suppressed because one or more lines are too long
+255 -190
View File
File diff suppressed because one or more lines are too long
+32
View 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>
+250 -1
View File
@@ -161,6 +161,7 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"planManagement": "Plan Management", "planManagement": "Plan Management",
"orderManagement": "Order Management", "orderManagement": "Order Management",
"couponManagement": "Coupon Management", "couponManagement": "Coupon Management",
"giftCardManagement": "Gift Card Management",
"userManagement": "User Management", "userManagement": "User Management",
"trafficResetLogs": "Traffic Reset Logs", "trafficResetLogs": "Traffic Reset Logs",
"ticketManagement": "Ticket Management" "ticketManagement": "Ticket Management"
@@ -555,7 +556,8 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"subscribe_path": { "subscribe_path": {
"title": "Subscription Path", "title": "Subscription Path",
"description": "Subscription path, modifying will change the original subscribe 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": { "show_info_to_server": {
"title": "Show Subscription Info in Nodes", "title": "Show Subscription Info in Nodes",
@@ -932,6 +934,8 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"error": "Copy failed", "error": "Copy failed",
"errorLog": "Error copying to clipboard" "errorLog": "Error copying to clipboard"
}, },
"submit": "Submit",
"saving": "Saving...",
"table": { "table": {
"noData": "No data available", "noData": "No data available",
"pagination": { "pagination": {
@@ -955,6 +959,15 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"updating": "Updating...", "updating": "Updating...",
"updateSuccess": "Update successful, system will restart shortly", "updateSuccess": "Update successful, system will restart shortly",
"updateFailed": "Update failed, please try again later" "updateFailed": "Update failed, please try again later"
},
"time": {
"day": "day",
"hour": " hour(s)"
},
"reset": "Reset",
"export": "Export",
"currency": {
"yuan": "Yuan"
} }
}, },
"dashboard": { "dashboard": {
@@ -1164,6 +1177,241 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"loading": "Searching..." "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": { "route": {
"title": "Route Management", "title": "Route Management",
"description": "Manage all route groups, including adding, deleting, and editing operations.", "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" "error_gte_zero": "Base rate must be greater than or equal to 0"
}, },
"dynamic_rate": { "dynamic_rate": {
"section_title": "Dynamic Rate Configuration",
"enable_label": "Enable Dynamic Rate", "enable_label": "Enable Dynamic Rate",
"enable_description": "Set different rate multipliers based on time periods", "enable_description": "Set different rate multipliers based on time periods",
"rules_label": "Time Period Rules", "rules_label": "Time Period Rules",
+357 -1
View File
@@ -161,6 +161,7 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"planManagement": "套餐管理", "planManagement": "套餐管理",
"orderManagement": "订单管理", "orderManagement": "订单管理",
"couponManagement": "优惠券管理", "couponManagement": "优惠券管理",
"giftCardManagement": "礼品卡管理",
"userManagement": "用户管理", "userManagement": "用户管理",
"ticketManagement": "工单管理", "ticketManagement": "工单管理",
"trafficResetLogs": "流量重置日志" "trafficResetLogs": "流量重置日志"
@@ -475,7 +476,8 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"subscribe_path": { "subscribe_path": {
"title": "订阅路径", "title": "订阅路径",
"description": "订阅路径,修改后将会改变原有的subscribe路径", "description": "订阅路径,修改后将会改变原有的subscribe路径",
"current_format": "当前订阅路径格式:{path}/xxxxxxxxxx" "current_format": "当前订阅路径格式:{path}/xxxxxxxxxx",
"restart_tip": "修改订阅路径后,可能需要重启服务才能生效。"
}, },
"show_info_to_server": { "show_info_to_server": {
"title": "在订阅中展示订阅信息", "title": "在订阅中展示订阅信息",
@@ -937,6 +939,8 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"error": "复制失败", "error": "复制失败",
"errorLog": "复制到剪贴板时出错" "errorLog": "复制到剪贴板时出错"
}, },
"submit": "提交",
"saving": "保存中...",
"table": { "table": {
"noData": "暂无数据", "noData": "暂无数据",
"pagination": { "pagination": {
@@ -960,6 +964,15 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"updating": "更新中...", "updating": "更新中...",
"updateSuccess": "更新成功,系统将在稍后自动重启", "updateSuccess": "更新成功,系统将在稍后自动重启",
"updateFailed": "更新失败,请稍后重试" "updateFailed": "更新失败,请稍后重试"
},
"time": {
"day": "天",
"hour": "小时"
},
"reset": "重置",
"export": "导出",
"currency": {
"yuan": "元"
} }
}, },
"dashboard": { "dashboard": {
@@ -1156,6 +1169,348 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"pagination": "第 {{current}}/{{total}} 页,共 {{count}} 条" "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": { "route": {
"title": "路由管理", "title": "路由管理",
"description": "管理所有路由组,包括添加、删除、编辑等操作。", "description": "管理所有路由组,包括添加、删除、编辑等操作。",
@@ -1672,6 +2027,7 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"error_gte_zero": "基础倍率必须大于或等于0" "error_gte_zero": "基础倍率必须大于或等于0"
}, },
"dynamic_rate": { "dynamic_rate": {
"section_title": "动态倍率配置",
"enable_label": "启用动态倍率", "enable_label": "启用动态倍率",
"enable_description": "根据时间段设置不同的倍率乘数", "enable_description": "根据时间段设置不同的倍率乘数",
"rules_label": "时间段规则", "rules_label": "时间段规则",