feat(admin): rework download logs page columns and selection
- Headers are now checkbox / index / post / user / credits cost / IP / time; drop the source index, usage and expiry columns - Add a check-column before the index: header select-all for multi-select plus per-row checkboxes, tokens as row values - Bulk delete selected tokens behind nonce and capability checks with prepared statements, redirect-after-post back to the filtered view - Index numbers continue across pagination; dead comments removed
This commit is contained in:
@@ -11,6 +11,10 @@ class PayDownloadLogs {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_download_logs';
|
||||
public const NONCE = 'aksrt_download_logs_bulk';
|
||||
|
||||
/** 单次批量删除的令牌上限,与列表每页条数量级一致,防止超大 IN 查询。 */
|
||||
private const BULK_DELETE_LIMIT = 100;
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
@@ -22,13 +26,28 @@ class PayDownloadLogs {
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
// Read-only.
|
||||
// 页面动作(批量删除)在 render_page() 渲染前就地处理,无需额外钩子。
|
||||
}
|
||||
|
||||
public function render_page(): void {
|
||||
$this->ensure_cap();
|
||||
|
||||
if ($this->is_bulk_delete_request()) {
|
||||
$this->handle_bulk_delete();
|
||||
}
|
||||
|
||||
echo '<div class="wrap"><h1>' . esc_html__('下载记录', 'aksrt') . '</h1>';
|
||||
|
||||
if (isset($_GET['dl_deleted'])) {
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html(sprintf(
|
||||
/* translators: %d: deleted record count */
|
||||
__('已删除 %d 条下载记录。', 'aksrt'),
|
||||
max(0, (int) $_GET['dl_deleted'])
|
||||
)) . '</p></div>';
|
||||
} elseif (isset($_GET['dl_empty'])) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('未选择需要删除的记录。', 'aksrt') . '</p></div>';
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Core\DBMigrator')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('数据库服务不可用。', 'aksrt') . '</p></div></div>';
|
||||
return;
|
||||
@@ -64,55 +83,52 @@ class PayDownloadLogs {
|
||||
echo '<label>' . esc_html__('文章 ID', 'aksrt') . ' <input type="number" min="0" name="post_id" value="' . esc_attr($post_id > 0 ? (string) $post_id : '') . '" class="small-text" /></label> ';
|
||||
echo '<label>' . esc_html__('用户 ID', 'aksrt') . ' <input type="number" min="0" name="user_id" value="' . esc_attr($user_id > 0 ? (string) $user_id : '') . '" class="small-text" /></label> ';
|
||||
echo '<label><input type="checkbox" name="used" value="1" ' . checked($only_used, true, false) . ' /> ' . esc_html__('仅已使用', 'aksrt') . '</label> ';
|
||||
// When unchecked, pass used=0 explicitly.
|
||||
if (!$only_used) {
|
||||
// no-op; checkbox not checked
|
||||
}
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo ' <a class="button" href="' . esc_url(admin_url('admin.php?page=' . self::PAGE_SLUG)) . '">' . esc_html__('重置', 'aksrt') . '</a>';
|
||||
echo '</form>';
|
||||
|
||||
// Fix: unchecked checkbox means used filter off — re-read without default if form submitted with no used.
|
||||
// Already handled via isset($_GET['used']).
|
||||
// 批量操作(多选 / 单选)包裹列表;删除成功后重定向回当前筛选视图。
|
||||
echo '<form method="post" action="' . esc_url($this->current_url()) . '">';
|
||||
wp_nonce_field(self::NONCE);
|
||||
echo '<div class="tablenav top"><div class="alignleft actions bulkactions">';
|
||||
echo '<label for="bulk-action-selector-top" class="screen-reader-text">' . esc_html__('选择批量操作', 'aksrt') . '</label>';
|
||||
echo '<select name="bulk_action" id="bulk-action-selector-top">';
|
||||
echo '<option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option>';
|
||||
echo '<option value="delete">' . esc_html__('删除', 'aksrt') . '</option>';
|
||||
echo '</select>';
|
||||
echo '<input type="submit" class="button action" value="' . esc_attr__('应用', 'aksrt') . '">';
|
||||
echo '</div><br class="clear"></div>';
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th>' . esc_html__('时间', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('用户', 'aksrt') . '</th>';
|
||||
echo '<td id="cb" class="manage-column column-cb check-column" style="width:2%;"><label class="screen-reader-text" for="cb-select-all-1">' . esc_html__('全选', 'aksrt') . '</label><input id="cb-select-all-1" type="checkbox" onclick="document.querySelectorAll(\'tbody [name="dl_token[]"]\').forEach(c=>c.checked=this.checked)"></td>';
|
||||
echo '<th>' . esc_html__('序号', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('文章', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('源序号', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('用户', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('积分消耗', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('使用', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('过期', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('IP', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('时间', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="8">' . esc_html__('暂无下载记录。', 'aksrt') . '</td></tr>';
|
||||
echo '<tr><td colspan="7">' . esc_html__('暂无下载记录。', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
|
||||
foreach ($items as $row) {
|
||||
$uid = (int) ($row['user_id'] ?? 0);
|
||||
$pid = (int) ($row['post_id'] ?? 0);
|
||||
$user = $uid > 0 ? get_userdata($uid) : null;
|
||||
$seq_offset = ($page_num - 1) * $per_page;
|
||||
foreach ($items as $i => $row) {
|
||||
$uid = (int) ($row['user_id'] ?? 0);
|
||||
$pid = (int) ($row['post_id'] ?? 0);
|
||||
$token = (string) ($row['token'] ?? '');
|
||||
$user = $uid > 0 ? get_userdata($uid) : null;
|
||||
$title = $pid > 0 ? get_the_title($pid) : '';
|
||||
if ($title === '') {
|
||||
$title = $pid > 0 ? sprintf(__('(已删除 #%d)', 'aksrt'), $pid) : '—';
|
||||
}
|
||||
$edit = $pid > 0 ? get_edit_post_link($pid, 'raw') : '';
|
||||
$used = (int) ($row['used_count'] ?? 0);
|
||||
$max = (int) ($row['max_uses'] ?? 1);
|
||||
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td>';
|
||||
if ($user) {
|
||||
echo esc_html($user->display_name . ' (#' . $uid . ')');
|
||||
echo '<br><a href="' . esc_url(admin_url('admin.php?page=' . self::PAGE_SLUG . '&user_id=' . $uid)) . '">' . esc_html__('仅此用户', 'aksrt') . '</a>';
|
||||
} else {
|
||||
echo esc_html((string) $uid);
|
||||
}
|
||||
echo '</td>';
|
||||
echo '<th scope="row" class="check-column"><label class="screen-reader-text" for="cb-select-' . esc_attr($token) . '">' . esc_html__('选择', 'aksrt') . '</label><input id="cb-select-' . esc_attr($token) . '" type="checkbox" name="dl_token[]" value="' . esc_attr($token) . '"></th>';
|
||||
echo '<td>' . esc_html((string) ($seq_offset + $i + 1)) . '</td>';
|
||||
echo '<td>';
|
||||
if ($edit) {
|
||||
echo '<a href="' . esc_url($edit) . '">' . esc_html($title) . '</a>';
|
||||
@@ -123,14 +139,21 @@ class PayDownloadLogs {
|
||||
echo '<br><a href="' . esc_url(admin_url('admin.php?page=' . self::PAGE_SLUG . '&post_id=' . $pid)) . '">' . esc_html__('仅此文章', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</td>';
|
||||
echo '<td>' . esc_html((string) (int) ($row['source_index'] ?? 0)) . '</td>';
|
||||
echo '<td>';
|
||||
if ($user) {
|
||||
echo esc_html($user->display_name . ' (#' . $uid . ')');
|
||||
echo '<br><a href="' . esc_url(admin_url('admin.php?page=' . self::PAGE_SLUG . '&user_id=' . $uid)) . '">' . esc_html__('仅此用户', 'aksrt') . '</a>';
|
||||
} else {
|
||||
echo esc_html((string) $uid);
|
||||
}
|
||||
echo '</td>';
|
||||
echo '<td>' . esc_html((string) (int) ($row['cost'] ?? 0)) . '</td>';
|
||||
echo '<td>' . esc_html($used . ' / ' . $max) . ($used > 0 ? ' <span class="dashicons dashicons-yes" style="color:green;"></span>' : '') . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['expires_at'] ?? '')) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['ip_bind'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
echo '</form>';
|
||||
|
||||
if ($pages > 1) {
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
@@ -147,6 +170,67 @@ class PayDownloadLogs {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前页面 URL(含筛选参数),用于批量表单回帖与删除后重定向。
|
||||
*/
|
||||
private function current_url(): string {
|
||||
$args = ['page' => self::PAGE_SLUG];
|
||||
foreach (['post_id', 'user_id', 'used', 'paged'] as $key) {
|
||||
if (isset($_GET[$key])) {
|
||||
$args[$key] = wp_unslash($_GET[$key]);
|
||||
}
|
||||
}
|
||||
return add_query_arg($args, admin_url('admin.php'));
|
||||
}
|
||||
|
||||
private function is_bulk_delete_request(): bool {
|
||||
return (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST')
|
||||
&& isset($_POST['bulk_action'])
|
||||
&& ((string) wp_unslash($_POST['bulk_action']) === 'delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除选中的下载令牌记录;完成后重定向回当前筛选视图,避免刷新重复提交。
|
||||
*/
|
||||
private function handle_bulk_delete(): void {
|
||||
check_admin_referer(self::NONCE);
|
||||
|
||||
$raw = isset($_POST['dl_token']) ? (array) wp_unslash($_POST['dl_token']) : [];
|
||||
$tokens = array_values(array_unique(array_filter($raw, static function ($token): bool {
|
||||
return is_string($token) && preg_match('/^[A-Za-z0-9]{1,64}$/', $token) === 1;
|
||||
})));
|
||||
$tokens = array_slice($tokens, 0, self::BULK_DELETE_LIMIT);
|
||||
|
||||
if (!$tokens) {
|
||||
wp_safe_redirect(add_query_arg('dl_empty', '1', $this->current_url()));
|
||||
exit;
|
||||
}
|
||||
|
||||
$deleted = $this->delete_tokens($tokens);
|
||||
wp_safe_redirect(add_query_arg('dl_deleted', $deleted, $this->current_url()));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $tokens
|
||||
*/
|
||||
private function delete_tokens(array $tokens): int {
|
||||
global $wpdb;
|
||||
|
||||
if (!class_exists('\AKSRT\Core\DBMigrator')) {
|
||||
return 0;
|
||||
}
|
||||
$table = \AKSRT\Core\DBMigrator::table('dl_token');
|
||||
if ($table === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($tokens), '%s'));
|
||||
$sql = "DELETE FROM {$table} WHERE token IN ({$placeholders})";
|
||||
|
||||
return (int) $wpdb->query($wpdb->prepare($sql, $tokens));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{post_id?:int,user_id?:int,only_used?:bool,limit?:int,offset?:int} $args
|
||||
* @return array{items: array<int,array<string,mixed>>, total:int}
|
||||
|
||||
Reference in New Issue
Block a user