mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-24 20:17:32 +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('更新完毕,队列服务已重启,你无需进行任何操作。');
|
||||
}
|
||||
}
|
||||
|
||||
+17
-42
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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位',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+16
-1
@@ -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 最后登录时间
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置试用计划
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user