feat(admin): 移除支付中心的“下载记录”子菜单页面

下载不做记录,支付中心下的“下载记录”页面及相关链路整体删除。

删除的内容:
- src/Admin/Native/PayDownloadLogs.php(页面渲染、批量删除、
  令牌查询)整文件移除。
- PayMenus 中的 download_logs 子菜单注册(概览页快捷入口由同一
  份 pages() 数组生成,随之消失)。
- functions.php 中的 PayDownloadLogs::init() 接线。
- 各入口跳转链接:付费文章列表的操作按钮、用户列表“积分分成”
  列的操作链接、用户列表行操作。
- “保存用户下载记录(条) / 保存文章下载记录(条)”两个设置项及其
  消费链:SchemaPayment.php、assets/admin 侧 knowledge.js 的字段
  与默认值、PaymentRuntime.php 的 user_log / post_log。
- 前台下载中心登录提示里的“管理下载记录”文案(该处本就没有
  可跳转的管理页面)。

未改动内容:
- aksrt_dl_token 数据表仍由 DBMigrator 创建、uninstall.php 仍会
  删除:存量站点可能留有历史令牌行,属数据而非功能,迁移风险
  高于收益。
- 独立下载页面开关(down_alone_page)与远程文件转本地等下载
  行为配置保留。
- 资源下载量(_aksrt_res_download_count)仍是只读口径:下载走
  普通外链,主题内没有写入方,本次不新增下载统计。

download-direct-contract.test.cjs 中原有“下载记录页面展示历史
令牌”用例反向改写为三项断言:页面与接线已删除、记录数设置项
已注销、下载仍不做记录。

验证:node --test "tests/*/*.test.cjs" 517 项全部通过;
php -l 全部改动文件无语法错误;重新构建 admin SPA,确认产物
中不含被移除的字段与菜单。
This commit is contained in:
2026-10-06 14:35:14 +08:00
parent 50fac36169
commit 194280af72
10 changed files with 27 additions and 321 deletions
@@ -29,8 +29,6 @@ export const knowledgeSchema = {
payment_order_status_closed: '已关闭',
pay_service: [],
down_alone_page: false,
down_user_log: 50,
down_post_log: 50,
down_remote_to_local_s: true,
down_remote_to_local_size: 2048,
down_remote_to_local_ext: 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf',
@@ -172,8 +170,6 @@ export const knowledgeSchema = {
title: '付费下载',
fields: [
{ key: 'down_alone_page', label: '独立下载页面', type: 'switch', default: false, help: '开启后,付费资源的下载链接会跳转独立页面显示' },
{ key: 'down_user_log', label: '保存用户下载记录(条)', type: 'number', default: 50, min: 0, help: '填0则不保存,注意记录量太大可能会影响网站性能' },
{ key: 'down_post_log', label: '保存文章下载记录(条)', type: 'number', default: 50, min: 0, help: '填0则不保存,注意记录量太大可能会影响网站性能' },
{ key: 'down_remote_to_local_s', label: '远程文件转本地文件下载', type: 'switch', default: true, help: '当下载地址为外链文件时,先将远程文件下载到本地服务器,再提供给用户下载,可完全隐藏远程下载地址' },
{ key: 'down_remote_to_local_size', label: '文件大小限制(Kb)', type: 'number', default: 2048, min: 0 },
{ key: 'down_remote_to_local_ext', label: '文件格式限制', type: 'input', default: 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf', help: '限制使用此功能的文件格式,多个用英文逗号隔开' },
-3
View File
@@ -406,9 +406,6 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayOrders')) {
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayProducts')) {
\AKSRT\Admin\Native\PayProducts::get_instance()->init();
}
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayDownloadLogs')) {
\AKSRT\Admin\Native\PayDownloadLogs::get_instance()->init();
}
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayWithdrawals')) {
\AKSRT\Admin\Native\PayWithdrawals::get_instance()->init();
}
-284
View File
@@ -1,284 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
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;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
// 页面动作(批量删除)在 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;
}
$table = \AKSRT\Core\DBMigrator::table('dl_token');
if ($table === '') {
echo '<div class="notice notice-error"><p>' . esc_html__('下载令牌表未初始化。', 'aksrt') . '</p></div></div>';
return;
}
$post_id = isset($_GET['post_id']) ? max(0, (int) $_GET['post_id']) : 0;
$user_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : 0;
$only_used = !isset($_GET['used']) || (string) $_GET['used'] !== '0';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 30;
$result = $this->query_logs($table, [
'post_id' => $post_id,
'user_id' => $user_id,
'only_used' => $only_used,
'limit' => $per_page,
'offset' => ($page_num - 1) * $per_page,
]);
$items = $result['items'];
$total = $result['total'];
$pages = max(1, (int) ceil($total / $per_page));
echo '<p class="description">' . esc_html__('基于下载令牌表(aksrt_dl_token)。下载已改为直连模式,不再签发新令牌,以下为历史记录。', 'aksrt') . '</p>';
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
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> ';
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>';
// 批量操作(多选 / 单选)包裹列表;删除成功后重定向回当前筛选视图。
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 '<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=&quot;dl_token[]&quot;]\').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__('IP', 'aksrt') . '</th>';
echo '<th>' . esc_html__('时间', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="7">' . esc_html__('暂无下载记录。', 'aksrt') . '</td></tr>';
}
$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') : '';
echo '<tr>';
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>';
} else {
echo esc_html($title);
}
if ($pid > 0) {
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>';
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><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">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
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}
*/
private function query_logs(string $table, array $args): array {
global $wpdb;
$where = ['1=1'];
$params = [];
$post_id = (int) ($args['post_id'] ?? 0);
if ($post_id > 0) {
$where[] = 'post_id = %d';
$params[] = $post_id;
}
$user_id = (int) ($args['user_id'] ?? 0);
if ($user_id > 0) {
$where[] = 'user_id = %d';
$params[] = $user_id;
}
if (!empty($args['only_used'])) {
$where[] = 'used_count > 0';
}
$where_sql = implode(' AND ', $where);
$limit = max(1, min(100, (int) ($args['limit'] ?? 30)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
$total = $params
? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params))
: (int) $wpdb->get_var($count_sql);
$sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
$list_params = $params;
$list_params[] = $limit;
$list_params[] = $offset;
$rows = $wpdb->get_results($wpdb->prepare($sql, $list_params), ARRAY_A);
return [
'items' => is_array($rows) ? $rows : [],
'total' => $total,
];
}
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
}
}
}
-11
View File
@@ -63,17 +63,6 @@ class PayMenus {
wp_die(esc_html__('付费文章模块未初始化。', 'aksrt'));
},
],
'download_logs' => [
'slug' => 'aksrt_download_logs',
'title' => __('下载记录', 'aksrt'),
'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayDownloadLogs')) {
PayDownloadLogs::get_instance()->render_page();
return;
}
wp_die(esc_html__('下载记录模块未初始化。', 'aksrt'));
},
],
'coupons' => [
'slug' => 'aksrt_coupons',
'title' => __('优惠码', 'aksrt'),
-4
View File
@@ -138,10 +138,6 @@ class PayProducts {
echo '<a class="button button-small" href="' . esc_url($edit_url) . '">' . esc_html__('编辑', 'aksrt') . '</a>';
}
echo '<a class="button button-small" href="' . esc_url($orders_url) . '">' . esc_html__('订单', 'aksrt') . '</a>';
if ($pt === \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD) {
$dl_url = $this->page_url('aksrt_download_logs', ['post_id' => $post_id]);
echo '<a class="button button-small" href="' . esc_url($dl_url) . '">' . esc_html__('下载记录', 'aksrt') . '</a>';
}
$permalink = get_permalink($post_id);
if ($permalink) {
echo '<a class="button button-small" target="_blank" rel="noopener noreferrer" href="' . esc_url($permalink) . '">' . esc_html__('查看', 'aksrt') . '</a>';
-2
View File
@@ -81,8 +81,6 @@ class SchemaPayment {
['key' => 'pay_type_option', 'label' => '商品功能配置', 'type' => 'tabbed', 'tabs' => [
['key' => 'download', 'title' => '付费下载', 'fields' => [
['key' => 'down_alone_page', 'label' => '独立下载页面', 'type' => 'switch', 'default' => false, 'help' => '开启后,付费资源的下载链接会跳转独立页面显示'],
['key' => 'down_user_log', 'label' => '保存用户下载记录(条)', 'type' => 'number', 'default' => 50, 'min' => 0, 'help' => '填0则不保存,注意记录量太大可能会影响网站性能'],
['key' => 'down_post_log', 'label' => '保存文章下载记录(条)', 'type' => 'number', 'default' => 50, 'min' => 0, 'help' => '填0则不保存,注意记录量太大可能会影响网站性能'],
['key' => 'down_remote_to_local_s', 'label' => '远程文件转本地文件下载', 'type' => 'switch', 'default' => true, 'help' => '当下载地址为外链文件时,先将远程文件下载到本地服务器,再提供给用户下载,可完全隐藏远程下载地址'],
['key' => 'down_remote_to_local_size', 'label' => '文件大小限制(Kb)', 'type' => 'number', 'default' => 2048, 'min' => 0],
['key' => 'down_remote_to_local_ext', 'label' => '文件格式限制', 'type' => 'input', 'default' => 'zip,rar,7z,text,png,jpg,jpeg,gif,mp3,mp4,avi,wmv,flv,swf,doc,docx,xls,xlsx,ppt,pptx,pdf', 'help' => '限制使用此功能的文件格式,多个用英文逗号隔开'],
-2
View File
@@ -48,8 +48,6 @@ class PaymentRuntime {
'enabled' => true,
'remote' => !isset($settings['down_remote_to_local_s']) || !empty($settings['down_remote_to_local_s']),
'verify_nonce' => !isset($settings['down_verify_nonce']) || !empty($settings['down_verify_nonce']),
'user_log' => max(0, (int) ($settings['down_user_log'] ?? 50)),
'post_log' => max(0, (int) ($settings['down_post_log'] ?? 50)),
'remote_size' => max(0, (int) ($settings['down_remote_to_local_size'] ?? 2048)),
'remote_ext' => sanitize_text_field((string) ($settings['down_remote_to_local_ext'] ?? 'zip,rar,7z')),
'alone_page' => !empty($settings['down_alone_page']),
-6
View File
@@ -88,7 +88,6 @@ class CreditsAdmin {
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_income&user_id=' . $user_id)) . '">' . esc_html__('分成明细', 'aksrt') . '</a>';
}
}
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_download_logs&user_id=' . $user_id)) . '">' . esc_html__('下载记录', 'aksrt') . '</a>';
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_orders&user_id=' . $user_id)) . '">' . esc_html__('订单', 'aksrt') . '</a>';
if (!$bits) {
@@ -129,11 +128,6 @@ class CreditsAdmin {
esc_html(sprintf(__('调整%s', 'aksrt'), $currency_name))
);
}
$actions['aksrt_user_dl_logs'] = sprintf(
'<a href="%s">%s</a>',
esc_url(admin_url('admin.php?page=aksrt_download_logs&user_id=' . (int) $user->ID)),
esc_html__('下载记录', 'aksrt')
);
return $actions;
}
+1 -1
View File
@@ -92,7 +92,7 @@ $dl_requires_login = function_exists('aksrt_user_can') && !aksrt_user_can('resou
<?php if ($dl_requires_login) : ?>
<?php esc_html_e('登录后可查看下载状态、积分余额并解锁可下载内容', 'aksrt'); ?>
<?php else : ?>
<?php esc_html_e('登录后可管理下载记录和积分余额', 'aksrt'); ?>
<?php esc_html_e('登录后可查看下载状态和积分余额,并解锁可下载内容', 'aksrt'); ?>
<?php endif; ?>
</span>
</div>
@@ -114,8 +114,30 @@ test('cms-download.js only serves the cashier now', () => {
assert.match(read('src/Core/ScriptsPart1b.php'), /'aksrt-cms-download'/);
});
test('the download logs page reports historical token records', () => {
const page = read('src/Admin/Native/PayDownloadLogs.php');
assert.match(page, /aksrt_dl_token/);
assert.match(page, /不再签发新令牌/);
test('the admin download-logs page and its wiring are gone', () => {
// 下载记录菜单页面:类文件、菜单注册、init 接线、各入口链接全部移除。
assert.equal(exists('src/Admin/Native/PayDownloadLogs.php'), false);
assert.doesNotMatch(read('src/Admin/Native/PayMenus.php'), /download_logs|aksrt_download_logs|PayDownloadLogs/)
assert.doesNotMatch(read('functions.php'), /PayDownloadLogs/)
assert.doesNotMatch(read('src/Admin/Native/PayProducts.php'), /aksrt_download_logs/)
assert.doesNotMatch(read('src/User/CreditsAdmin.php'), /aksrt_download_logs|下载记录/)
})
test('download log retention settings no longer exist', () => {
// 「保存用户下载记录/保存文章下载记录」两个设置项及其消费链一并注销。
assert.doesNotMatch(read('src/Admin/Schema/SchemaPayment.php'), /down_user_log|down_post_log/)
assert.doesNotMatch(read('assets/admin/src/schema/sections/payment/knowledge.js'), /down_user_log|down_post_log/)
assert.doesNotMatch(read('src/Settings/PaymentRuntime.php'), /down_user_log|down_post_log|'user_log'|'post_log'/)
})
test('downloads stay unrecorded', () => {
// 下载不做记录:没有任何 PHP 写入下载计数或下载令牌。
const php = [
'inc/helpers/aksrt-pay.php',
'inc/helpers/aksrt-resource.php',
'src/Content/PayRest.php',
'src/Content/Visibility.php',
].map(read).join('\n')
assert.doesNotMatch(php, /update_post_meta\([^)]*download_count/)
assert.doesNotMatch(php, /aksrt_update_post_counter\([^)]*download/)
});