fix: optimize batch email performance and fix gift card issues

- Add chunking and batching for admin email sending
- Fix gift card pagination and add per_page limits
- Update frontend prompts and complete language packs
This commit is contained in:
xboard
2025-08-23 00:11:45 +08:00
parent 53ca5d062c
commit 8f3cdf0dde
6 changed files with 266 additions and 157 deletions

View File

@@ -65,15 +65,7 @@ class GiftCardController extends Controller
]; ];
})->values(); })->values();
return response()->json([ return $this->paginate( $templates);
'data' => $data,
'pagination' => [
'current_page' => $templates->currentPage(),
'last_page' => $templates->lastPage(),
'per_page' => $templates->perPage(),
'total' => $templates->total(),
],
]);
} }
/** /**
@@ -352,7 +344,7 @@ class GiftCardController extends Controller
'batch_id' => 'string', 'batch_id' => 'string',
'status' => 'integer|in:0,1,2,3', 'status' => 'integer|in:0,1,2,3',
'page' => 'integer|min:1', 'page' => 'integer|min:1',
'per_page' => 'integer|min:1|max:100', 'per_page' => 'integer|min:1|max:500',
]); ]);
$query = GiftCardCode::with(['template', 'user']); $query = GiftCardCode::with(['template', 'user']);
@@ -391,15 +383,7 @@ class GiftCardController extends Controller
]; ];
})->values(); })->values();
return response()->json([ return $this->paginate($codes);
'data' => $data,
'pagination' => [
'current_page' => $codes->currentPage(),
'last_page' => $codes->lastPage(),
'per_page' => $codes->perPage(),
'total' => $codes->total(),
],
]);
} }
/** /**
@@ -464,7 +448,7 @@ class GiftCardController extends Controller
'template_id' => 'integer|exists:v2_gift_card_template,id', 'template_id' => 'integer|exists:v2_gift_card_template,id',
'user_id' => 'integer|exists:v2_user,id', 'user_id' => 'integer|exists:v2_user,id',
'page' => 'integer|min:1', 'page' => 'integer|min:1',
'per_page' => 'integer|min:1|max:100', 'per_page' => 'integer|min:1|max:500',
]); ]);
$query = GiftCardUsage::with(['template', 'code', 'user', 'inviteUser']); $query = GiftCardUsage::with(['template', 'code', 'user', 'inviteUser']);
@@ -480,7 +464,7 @@ class GiftCardController extends Controller
$perPage = $request->input('per_page', 15); $perPage = $request->input('per_page', 15);
$usages = $query->orderBy('created_at', 'desc')->paginate($perPage); $usages = $query->orderBy('created_at', 'desc')->paginate($perPage);
$data = $usages->getCollection()->map(function ($usage) { $usages->transform(function ($usage) {
return [ return [
'id' => $usage->id, 'id' => $usage->id,
'code' => $usage->code->code ?? '', 'code' => $usage->code->code ?? '',
@@ -493,15 +477,7 @@ class GiftCardController extends Controller
'created_at' => $usage->created_at, 'created_at' => $usage->created_at,
]; ];
})->values(); })->values();
return response()->json([ return $this->paginate($usages);
'data' => $data,
'pagination' => [
'current_page' => $usages->currentPage(),
'last_page' => $usages->lastPage(),
'per_page' => $usages->perPage(),
'total' => $usages->total(),
],
]);
} }
/** /**

View File

@@ -16,6 +16,7 @@ use App\Utils\Helper;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@@ -442,22 +443,38 @@ class UserController extends Controller
$sort = $request->input('sort') ? $request->input('sort') : 'created_at'; $sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType); $builder = User::orderBy($sort, $sortType);
$this->applyFiltersAndSorts($request, $builder); $this->applyFiltersAndSorts($request, $builder);
$users = $builder->get();
foreach ($users as $user) { $subject = $request->input('subject');
SendEmailJob::dispatch( $content = $request->input('content');
[ $templateValue = [
'name' => admin_setting('app_name', 'XBoard'),
'url' => admin_setting('app_url'),
'content' => $content
];
$chunkSize = 1000;
$totalProcessed = 0;
$builder->chunk($chunkSize, function ($users) use ($subject, $templateValue, &$totalProcessed) {
$jobs = [];
foreach ($users as $user) {
$jobs[] = new SendEmailJob([
'email' => $user->email, 'email' => $user->email,
'subject' => $request->input('subject'), 'subject' => $subject,
'template_name' => 'notify', 'template_name' => 'notify',
'template_value' => [ 'template_value' => $templateValue
'name' => admin_setting('app_name', 'XBoard'), ], 'send_email_mass');
'url' => admin_setting('app_url'), }
'content' => $request->input('content')
] if (!empty($jobs)) {
], Bus::batch($jobs)
'send_email_mass' ->allowFailures()
); ->dispatch();
} }
$totalProcessed += $users->count();
});
return $this->success(true); return $this->success(true);
} }

View File

@@ -3,6 +3,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Services\MailService; use App\Services\MailService;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@@ -11,7 +12,7 @@ use Illuminate\Queue\SerializesModels;
class SendEmailJob implements ShouldQueue class SendEmailJob implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Batchable;
protected $params; protected $params;
public $tries = 3; public $tries = 3;

File diff suppressed because one or more lines are too long

View File

@@ -967,6 +967,10 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"previousPage": "Previous page", "previousPage": "Previous page",
"nextPage": "Next page", "nextPage": "Next page",
"lastPage": "Go to last page" "lastPage": "Go to last page"
},
"viewOptions": {
"button": "Columns",
"label": "Toggle columns"
} }
}, },
"update": { "update": {
@@ -1198,62 +1202,74 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
} }
}, },
"giftCard": { "giftCard": {
"types": { "title": "Gift Card Management",
"1": "General Gift Card", "description": "Manage gift card templates, redemption codes, and usage records.",
"2": "Plan-Specific Gift Card", "tabs": {
"3": "Mystery Box Reward", "templates": "Template Management",
"4": "Task Gift Card" "codes": "Redemption Code Management",
}, "usages": "Usage Records",
"status": { "statistics": "Statistics"
"0": "Unused",
"1": "Used",
"2": "Disabled",
"3": "Expired"
}, },
"template": { "template": {
"title": "Gift Card Templates", "title": "Template Management",
"add": "Add Template", "description": "Manage gift card templates, including creating, editing, and deleting templates.",
"search": "Search template name...", "table": {
"title": "Template List",
"columns": {
"id": "ID",
"name": "Name",
"type": "Type",
"status": "Status",
"sort": "Sort",
"rewards": "Rewards",
"created_at": "Created At",
"actions": "Actions",
"no_rewards": "No Rewards"
}
},
"form": { "form": {
"add": "Add Template",
"edit": "Edit Template",
"name": { "name": {
"label": "Name", "label": "Template Name",
"placeholder": "Enter template name" "placeholder": "Please enter template name",
"required": "Please enter template name"
}, },
"sort": { "sort": {
"label": "Sort", "label": "Sort",
"placeholder": "Smaller numbers come first" "placeholder": "Smaller numbers appear first"
}, },
"type": { "type": {
"label": "Type", "label": "Type",
"placeholder": "Select gift card type" "placeholder": "Please select gift card type"
}, },
"description": { "description": {
"label": "Description", "label": "Description",
"placeholder": "Enter gift card description" "placeholder": "Please enter gift card description"
}, },
"status": { "status": {
"label": "Status", "label": "Status",
"description": "If disabled, this template cannot be used to generate or redeem new gift cards." "description": "When disabled, this template cannot generate or redeem new gift cards."
}, },
"display": { "display": {
"title": "Display Settings" "title": "Display Effect"
}, },
"theme_color": { "theme_color": {
"label": "Theme Color" "label": "Theme Color"
}, },
"icon": { "icon": {
"label": "Icon", "label": "Icon",
"placeholder": "Enter the URL of the icon" "placeholder": "Please enter icon URL"
}, },
"background_image": { "background_image": {
"label": "Background Image", "label": "Background Image",
"placeholder": "Enter the URL of the background image" "placeholder": "Please enter background image URL"
}, },
"conditions": { "conditions": {
"title": "Usage Conditions", "title": "Usage Conditions",
"new_user_max_days": { "new_user_max_days": {
"label": "New User Registration Day Limit", "label": "New User Registration Days Limit",
"placeholder": "e.g., 7 (only for users registered within 7 days)" "placeholder": "Example: 7 (Only for users registered within 7 days)"
}, },
"new_user_only": { "new_user_only": {
"label": "New Users Only" "label": "New Users Only"
@@ -1262,15 +1278,15 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"label": "Paid Users Only" "label": "Paid Users Only"
}, },
"require_invite": { "require_invite": {
"label": "Requires Invitation" "label": "Require Invitation Relationship"
}, },
"allowed_plans": { "allowed_plans": {
"label": "Allowed Plans", "label": "Allowed Plans",
"placeholder": "Select allowed plans (leave empty for no restriction)" "placeholder": "Select plans allowed for redemption (leave empty for no restriction)"
}, },
"disallowed_plans": { "disallowed_plans": {
"label": "Disallowed Plans", "label": "Disallowed Plans",
"placeholder": "Select disallowed plans (leave empty for no restriction)" "placeholder": "Select plans forbidden for redemption (leave empty for no restriction)"
} }
}, },
"limits": { "limits": {
@@ -1280,64 +1296,64 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"placeholder": "Leave empty for no limit" "placeholder": "Leave empty for no limit"
}, },
"cooldown_hours": { "cooldown_hours": {
"label": "Cooldown for Same Type (Hours)", "label": "Cooldown Hours for Same Type",
"placeholder": "Leave empty for no limit" "placeholder": "Leave empty for no limit"
}, },
"invite_reward_rate": { "invite_reward_rate": {
"label": "Inviter Reward Rate", "label": "Inviter Reward Rate",
"placeholder": "e.g., 0.2 (for 20%)", "placeholder": "Example: 0.2 (represents 20%)",
"description": "If the user has an inviter, the inviter's reward = balance reward * this rate" "description": "When user has an inviter, inviter reward = balance reward × this rate"
} }
}, },
"rewards": { "rewards": {
"title": "Reward Content", "title": "Rewards",
"balance": { "balance": {
"label": "Reward Amount (in Yuan)", "label": "Reward Balance (Yuan)",
"short_label": "Balance", "short_label": "Balance",
"placeholder": "Enter the reward amount in Yuan" "placeholder": "Please enter reward amount (Yuan)"
}, },
"transfer_enable": { "transfer_enable": {
"label": "Reward Traffic (in GB)", "label": "Reward Traffic (GB)",
"short_label": "Traffic", "short_label": "Traffic",
"placeholder": "Enter the reward traffic in GB" "placeholder": "Please enter reward traffic (GB)"
}, },
"expire_days": { "expire_days": {
"label": "Extend Validity (in days)", "label": "Extend Validity (Days)",
"short_label": "Validity", "short_label": "Validity",
"placeholder": "Enter the number of days to extend" "placeholder": "Please enter extension days"
}, },
"transfer": { "transfer": {
"label": "Reward Traffic (in bytes)", "label": "Reward Traffic (Bytes)",
"placeholder": "Enter the reward traffic in bytes" "placeholder": "Please enter reward traffic (bytes)"
}, },
"days": { "days": {
"label": "Extend Validity (in days)", "label": "Extend Validity (Days)",
"placeholder": "Enter the number of days to extend" "placeholder": "Please enter extension days"
}, },
"device_limit": { "device_limit": {
"label": "Increase Device Limit", "label": "Increase Device Count",
"short_label": "Devices", "short_label": "Devices",
"placeholder": "Enter the number of devices to increase" "placeholder": "Please enter increased device count"
}, },
"reset_package": { "reset_package": {
"label": "Reset Monthly Traffic", "label": "Reset Monthly Traffic",
"description": "If enabled, the user's current plan traffic usage will be reset to zero upon redemption." "description": "When enabled, redemption will clear the user's current plan's used traffic."
}, },
"reset_count": { "reset_count": {
"description": "This type of card will reset the user's traffic usage for the current month." "description": "This type of card will reset the user's monthly traffic usage."
}, },
"task_card": { "task_card": {
"description": "The specific rewards for task gift cards will be configured in the task system." "description": "Specific rewards for task gift cards will be configured in the task system."
}, },
"plan_id": { "plan_id": {
"label": "Specify Plan", "label": "Specified Plan",
"short_label": "Plan", "short_label": "Plan",
"placeholder": "Select a plan" "placeholder": "Please select a plan"
}, },
"plan_validity_days": { "plan_validity_days": {
"label": "Plan Validity (in days)", "label": "Plan Validity (Days)",
"short_label": "Plan Validity", "short_label": "Plan Validity",
"placeholder": "Leave empty to use the plan's default validity" "placeholder": "Leave empty to use plan default validity"
}, },
"random_rewards": { "random_rewards": {
"label": "Random Reward Pool", "label": "Random Reward Pool",
@@ -1349,87 +1365,182 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"title": "Special Configuration", "title": "Special Configuration",
"start_time": { "start_time": {
"label": "Event Start Time", "label": "Event Start Time",
"placeholder": "Pick a start date" "placeholder": "Please select start date"
}, },
"end_time": { "end_time": {
"label": "Event End Time", "label": "Event End Time",
"placeholder": "Pick an end date" "placeholder": "Please select end date"
}, },
"festival_bonus": { "festival_bonus": {
"label": "Festival Reward Multiplier", "label": "Festival Reward Multiplier",
"placeholder": "e.g., 1.5 (for 1.5x)" "placeholder": "Example: 1.5 (represents 1.5x)"
} }
},
"submit": {
"saving": "Saving...",
"save": "Save"
}
},
"actions": {
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": {
"title": "Confirm Delete",
"description": "This action will permanently delete this template. Are you sure you want to continue?",
"confirmText": "Delete"
} }
} }
}, },
"code": { "code": {
"title": "Code Management", "title": "Redemption Code Management",
"add": "Generate Codes",
"search": "Search codes...",
"form": { "form": {
"generate": "Generate Redemption Codes",
"template_id": { "template_id": {
"label": "Gift Card Template", "label": "Select Template",
"placeholder": "Select a template" "placeholder": "Please select a template to generate redemption codes"
}, },
"count": { "count": {
"label": "Quantity to Generate", "label": "Generation Count"
"placeholder": "Enter the quantity to generate"
}, },
"prefix": { "prefix": {
"label": "Code Prefix", "label": "Custom Prefix (Optional)"
"placeholder": "Leave empty for default prefix GC"
}, },
"expires_hours": { "expires_hours": {
"label": "Validity (Hours)", "label": "Validity (Hours)"
"placeholder": "From generation time, leave empty for no expiration"
}, },
"max_usage": { "max_usage": {
"label": "Max Usage Count", "label": "Max Usage Count"
"placeholder": "Total times each code can be used"
}, },
"download_csv": "Export as CSV" "download_csv": "Export CSV",
"submit": {
"generating": "Generating...",
"generate": "Generate Now"
}
},
"description": "Manage gift card redemption codes, including generation, viewing, and exporting codes.",
"generate": {
"title": "Generate Redemption Codes",
"template": "Select Template",
"count": "Generation Count",
"prefix": "Custom Prefix",
"expires_hours": "Validity (Hours)",
"max_usage": "Max Usage Count",
"submit": "Generate"
}, },
"table": { "table": {
"code": "Code", "title": "Redemption Code List",
"template": "Template", "columns": {
"status": "Status", "id": "ID",
"expires_at": "Expires At", "code": "Redemption Code",
"used_at": "Used At", "template_name": "Template Name",
"used_by": "Used By", "status": "Status",
"max_usage": "Max Uses", "expires_at": "Expires At",
"usage_count": "Usage Count" "usage_count": "Used Count",
"max_usage": "Available Count",
"created_at": "Created At"
}
}, },
"messages": { "actions": {
"generateSuccess": "Codes generated successfully", "enable": "Enable",
"generateFailed": "Failed to generate codes", "disable": "Disable",
"deleteConfirm": "Are you sure you want to delete this code? This action cannot be undone.", "export": "Export",
"deleteSuccess": "Code deleted successfully", "exportConfirm": {
"deleteFailed": "Failed to delete code" "title": "Confirm Export",
"description": "This will export all redemption codes from the selected batch as a text file. Are you sure you want to continue?",
"confirmText": "Export"
}
},
"status": {
"0": "Unused",
"1": "Used",
"2": "Disabled",
"3": "Expired"
} }
}, },
"messages": { "usage": {
"formInvalid": "Form validation failed, please check your input.", "title": "Usage Records",
"templateCreated": "Template created successfully", "description": "View gift card usage records and detailed information.",
"templateUpdated": "Template updated successfully", "table": {
"createTemplateFailed": "Failed to create template", "columns": {
"updateTemplateFailed": "Failed to update template", "id": "ID",
"deleteConfirm": "Are you sure you want to delete this template? All codes under it will also be deleted.", "code": "Redemption Code",
"deleteSuccess": "Template deleted successfully", "template_name": "Template Name",
"deleteFailed": "Failed to delete template", "user_email": "User Email",
"codesGenerated": "Codes generated successfully" "rewards_given": "Rewards Given",
}, "invite_rewards": "Invitation Rewards",
"table": { "multiplier_applied": "Multiplier Applied",
"columns": { "ip_address": "IP Address",
"no_rewards": "No Rewards" "created_at": "Usage Time",
"actions": "Actions"
}
},
"actions": {
"view": "View Details"
} }
}, },
"statistics": {
"title": "Statistics",
"description": "View gift card statistics and usage analysis.",
"total": {
"title": "Overall Statistics",
"templates_count": "Total Templates",
"active_templates_count": "Active Templates",
"codes_count": "Total Redemption Codes",
"used_codes_count": "Used Redemption Codes",
"usages_count": "Usage Records"
},
"daily": {
"title": "Daily Usage",
"chart": "Usage Trend Chart"
},
"type": {
"title": "Type Statistics",
"chart": "Type Distribution Chart"
},
"dateRange": {
"label": "Date Range",
"start": "Start Date",
"end": "End Date"
}
},
"types": {
"1": "General Gift Card",
"2": "Plan Gift Card",
"3": "Mystery Gift Card",
"4": "Task Gift Card"
},
"common": { "common": {
"currency": { "search": "Search gift cards...",
"yuan": "Yuan" "reset": "Reset",
}, "filter": "Filter",
"time": { "export": "Export",
"day": "day" "refresh": "Refresh",
} "back": "Back",
"close": "Close",
"confirm": "Confirm",
"cancel": "Cancel",
"enabled": "Enabled",
"disabled": "Disabled",
"loading": "Loading...",
"noData": "No Data",
"success": "Operation Successful",
"error": "Operation Failed"
},
"messages": {
"formInvalid": "Please check if the form input is correct",
"templateCreated": "Template created successfully",
"templateUpdated": "Template updated successfully",
"templateDeleted": "Template deleted successfully",
"codeGenerated": "Redemption codes generated successfully",
"generateCodeFailed": "Failed to generate redemption codes",
"codeStatusUpdated": "Redemption code status updated successfully",
"updateCodeStatusFailed": "Failed to update redemption code status",
"codesExported": "Redemption codes exported successfully",
"createTemplateFailed": "Failed to create template",
"updateTemplateFailed": "Failed to update template",
"deleteTemplateFailed": "Failed to delete template",
"loadDataFailed": "Failed to load data",
"codesGenerated": "Redemption codes generated successfully"
} }
}, },
"route": { "route": {

View File

@@ -972,6 +972,10 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"previousPage": "上一页", "previousPage": "上一页",
"nextPage": "下一页", "nextPage": "下一页",
"lastPage": "跳转到最后一页" "lastPage": "跳转到最后一页"
},
"viewOptions": {
"button": "显示列",
"label": "切换显示列"
} }
}, },
"update": { "update": {