优化了一些问题

This commit is contained in:
2026-08-29 17:36:21 +08:00
parent 88eb29c645
commit af30159a03
6 changed files with 151 additions and 92 deletions
@@ -0,0 +1,51 @@
## 目标
在后台「AKSRT → 功能&权限 → 下载配置」中新增总开关 `enabled`(switch,**默认开启**)。开启 = 现有本站 token 中转下载(`/download/{token}`,别名/次数/有效期生效);关闭 = 直连模式,下载按钮直接渲染原始下载地址,不再生成本站中转链接。**权限门控保留**(未登录/未购买/非会员仍显示解锁提示,已确认)。不写迁移、不留死代码、不做真实视觉效果验收。
## 现状(调研结论)
- 「下载配置」是 SPA 通用 schema 页(`SchemaPage.vue`),PHP schema 在 `src/Admin/Schema/SchemaPermissions.php:73-85`(option `aksrt_download_config`),前端镜像在 `assets/admin/src/schema/sections/permissions/index.js:246-269`
- 下载链路:按钮 `data-action="aksrt-download"` → `POST /aksrt/v1/download/request`(`src/Content/Download.php:215`)签发 token → 返回 `home_url('/别名/{token}')` → rewrite + `maybe_serve_download()` 中转(本地流式/302 外链)
- 发现死代码:`rest_policy()` + `/download/policy` 路由(全库无调用方)、`aksrt_is_download_alone_page_enabled()`(`inc/helpers/aksrt-pay.php:921`,无调用方)
## 变更清单
**1. `src/Admin/Schema/SchemaPermissions.php`(PHP 权威 schema)**
- download section version 1.0.0 → 1.1.0
- groups 重构(仿 login/moderation 模式):新 `basic` 组「下载总开关」,字段 `enabled`(switch,default true,help 说明「关闭后下载采用直连,下方链接规则不生效」);原 `url_alias`/`default_max_uses`/`ttl_minutes` 移入 `rules` 组「链接规则」,各加 `dependsOn: {key: 'enabled', value: true}`
**2. `assets/admin/src/schema/sections/permissions/index.js`(前端镜像)**
- `download.defaults` 增加 `enabled: true`;groups 同步 PHP 双组结构(switch + dependsOn);更新 description
**3. `src/Content/Download.php`**
- 新增 `public function is_enabled(): bool`:读 `aksrt_download_config`,`enabled` 键缺失视为开启(默认 true),否则 `!empty()` 判定(与 PostDisplay 等现有默认 true 惯用法一致)
- 关闭时提前 return:`register_rewrite()`(不注册路由)、`register_routes()`(不注册 `/download/request`)、`maybe_serve_download()`(旧 token 链接失效,含 `?aksrt_dl_token=` 直查)
- **删除死代码**:`rest_policy()` 方法与 `/download/policy` 路由注册
- token 签发/流式输出等开启路径逻辑不动(现有契约测试正则不受影响)
**4. `templates/parts/resource-download-box.php`**
- 顶部计算 `$direct_download`(`class_exists` 守卫 + `is_enabled()` 取反)
- 下载源循环:直连模式且非 need_login 时,渲染 `<a href="{原始url}" target="_blank" rel="nofollow noopener">`(保留网盘识别徽章/着色),提取码旁复用全局 clipboard 委托渲染复制按钮(`data-clipboard-text`,main.min.js 已全局处理);need_login 分支不变
- 直连模式下 `$captcha_active` 置 false(验证码容器成死 UI)
- 独立下载页入口(`down_alone_page`)是独立功能,不与本开关耦合
**5. `inc/helpers/aksrt-pay.php`**
- `aksrt_get_paid_download_actions_html()`:直连模式渲染 `<a class="but ...">` 直链替代 token 按钮;提取码/复制按钮逻辑不变
- **删除死代码** `aksrt_is_download_alone_page_enabled()`
**6. `tests/widget-system/download-config-contract.test.cjs`(扩展契约测试)**
- 字段对齐测试加入 `enabled => true`(PHP)/ `enabled: true`(JS)
- 新增断言:Download.php 含 `is_enabled` 守卫;`/download/policy` 已移除;直连模式下盒模板与付费卡函数含直链 `<a>` 渲染分支
## 行为语义(写入代码注释与测试预期)
- 默认开启,存量站点零感知(不迁移 option,键缺失即 true)
- 关闭后:无下载计次、无免费额度扣减、无下载就绪通知、无验证码——直连语义的自然结果
- 关闭后旧 `/download/{token}` 链接全部失效
## 验证
- `php -l` 检查所有改动的 PHP 文件
- `node --test tests/widget-system/download-config-contract.test.cjs`,并跑 pay-card-contract、post-visual-contract 确认无回归
- `cd assets/admin && npm run build` 重建 SPA(前端 schema 变更需构建生效);若 `assets/admin/build/` 被 git 跟踪则一并纳入变更
- 按要求跳过真实视觉效果验收
@@ -247,8 +247,9 @@ export const permissionsSchemas = {
key: 'permissions.download',
title: '下载配置',
icon: 'fa fas fa-download',
description: '下载链接别名、默认使用次数与有效期限规则。',
description: '下载总开关与链接别名、默认使用次数、有效期限规则。',
defaults: {
enabled: true,
url_alias: 'download',
default_max_uses: 1,
ttl_minutes: 10,
@@ -256,13 +257,21 @@ export const permissionsSchemas = {
groups: [
{
key: 'basic',
title: '下载总开关',
icon: 'fa fas fa-power-off',
fields: [
{ key: 'enabled', label: '启用本站下载链接', type: 'switch', help: '总开关:开启后下载通过本站中转链接;关闭后下载按钮直接使用原始下载地址(直连),下方链接规则全部不生效,旧的中转链接同步失效。' },
],
},
{
key: 'rules',
title: '链接规则',
icon: 'fa fas fa-link',
description: '文件完整送达才计次(HEAD 探测、传输中断不计次);到期或耗尽后访问旧地址返回 410,需重新获取,不会自动续签。',
fields: [
{ key: 'url_alias', label: '下载链接 URL 别名', type: 'input', placeholder: 'download', required: true, help: '仅允许英文字母、数字、连字符和下划线;禁止中文、空格和 / \\ ? # & 等特殊符号。修改后旧别名路由立即失效。' },
{ key: 'default_max_uses', label: '下载链接默认使用次数', type: 'number', min: 1, max: 100, help: '单个链接可完成下载的次数;耗尽后链接返回 410,需重新获取。' },
{ key: 'ttl_minutes', label: '下载链接默认有效期限(分钟)', type: 'number', min: 1, max: 1440, help: '链接签发后超过该时长即失效并返回 410,不自动续期。' },
{ key: 'url_alias', label: '下载链接 URL 别名', type: 'input', placeholder: 'download', required: true, dependsOn: { key: 'enabled', value: true }, help: '仅允许英文字母、数字、连字符和下划线;禁止中文、空格和 / \\ ? # & 等特殊符号。修改后旧别名路由立即失效。' },
{ key: 'default_max_uses', label: '下载链接默认使用次数', type: 'number', min: 1, max: 100, dependsOn: { key: 'enabled', value: true }, help: '单个链接可完成下载的次数;耗尽后链接返回 410,需重新获取。' },
{ key: 'ttl_minutes', label: '下载链接默认有效期限(分钟)', type: 'number', min: 1, max: 1440, dependsOn: { key: 'enabled', value: true }, help: '链接签发后超过该时长即失效并返回 410,不自动续期。' },
],
},
],
+15 -15
View File
@@ -914,19 +914,6 @@ if (!function_exists('aksrt_get_single_download_page_url')) {
}
}
/**
* Whether independent download page mode is enabled (legacy down_alone_page).
*/
if (!function_exists('aksrt_is_download_alone_page_enabled')) {
function aksrt_is_download_alone_page_enabled(): bool {
if (!class_exists('\AKSRT\Settings\Settings')) {
return false;
}
$paid = \AKSRT\Settings\Settings::get_paid_content_settings();
return !empty($paid['down_alone_page']) || !empty($paid['download']['alone_page']);
}
}
// ============================================================
// 价格显示
// ============================================================
@@ -1450,12 +1437,20 @@ if (!function_exists('aksrt_get_paid_download_actions_html')) {
return '<div class="muted-2-color text-center">' . esc_html__('暂无可下载资源', 'aksrt') . '</div>';
}
// 下载总开关关闭 = 直连模式:按钮直接输出原始地址,不走本站中转链接。
$direct_download = class_exists('\AKSRT\Content\Download')
&& !\AKSRT\Content\Download::get_instance()->is_enabled();
$buttons = '';
foreach ($downloads as $index => $download) {
if (!is_array($download)) {
continue;
}
$url = (string) ($download['url'] ?? '');
if ($direct_download && $url === '') {
// 直连模式没有可用的原始地址,空源不渲染。
continue;
}
$label = trim((string) ($download['label'] ?? ''));
$service = function_exists('aksrt_detect_cloud_service')
? aksrt_detect_cloud_service($url)
@@ -1474,8 +1469,13 @@ if (!function_exists('aksrt_get_paid_download_actions_html')) {
$copy_label = $extract_code !== '' ? $extract_code : ($copy_key !== '' ? $copy_key : __('更多内容', 'aksrt'));
$buttons .= '<div class="but-download flex ac">';
$buttons .= '<button type="button" class="mr10 but ' . esc_attr($button_class) . '" data-action="aksrt-download" data-source-index="' . esc_attr((string) $index) . '" data-source-type="' . esc_attr($type) . '" data-has-extract="' . ($has_extract ? '1' : '0') . '">';
$buttons .= '<i class="fa fa-download" aria-hidden="true"></i>' . esc_html($label) . '</button>';
if ($direct_download) {
$buttons .= '<a class="mr10 but ' . esc_attr($button_class) . '" href="' . esc_url($url) . '" target="_blank" rel="nofollow noopener">';
$buttons .= '<i class="fa fa-download" aria-hidden="true"></i>' . esc_html($label) . '</a>';
} else {
$buttons .= '<button type="button" class="mr10 but ' . esc_attr($button_class) . '" data-action="aksrt-download" data-source-index="' . esc_attr((string) $index) . '" data-source-type="' . esc_attr($type) . '" data-has-extract="' . ($has_extract ? '1' : '0') . '">';
$buttons .= '<i class="fa fa-download" aria-hidden="true"></i>' . esc_html($label) . '</button>';
}
if ($copy_value !== '') {
$buttons .= '<button type="button" class="but" data-clipboard-tag="' . esc_attr($copy_key !== '' ? $copy_key : __('内容', 'aksrt')) . '" data-clipboard-text="' . esc_attr($copy_value) . '">' . esc_html($copy_label) . '</button>';
} elseif ($extract_code !== '') {
+8 -5
View File
@@ -73,13 +73,16 @@ class SchemaPermissions {
'download' => [
'key' => 'download',
'title' => '下载配置',
'version' => '1.0.0',
'version' => '1.1.0',
'option_key' => 'aksrt_download_config',
'groups' => [
['key' => 'basic', 'title' => '链接规则', 'fields' => [
['key' => 'url_alias', 'label' => '下载链接 URL 别名', 'type' => 'input', 'default' => 'download', 'required' => true, 'help' => '仅允许英文字母、数字、连字符和下划线;禁止中文、空格和 / \\ ? # & 等特殊符号。修改后旧别名路由立即失效,需重新获取下载地址。'],
['key' => 'default_max_uses', 'label' => '下载链接默认使用次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 100, 'help' => '单个链接可完成下载的次数;文件完整送达后才计次(HEAD 探测、传输中断不计次)。耗尽后链接返回 410,需重新获取。'],
['key' => 'ttl_minutes', 'label' => '下载链接默认有效期限(分钟)', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 1440, 'help' => '链接签发后超过该时长即失效并返回 410,不自动续期,需重新获取。'],
['key' => 'basic', 'title' => '下载总开关', 'fields' => [
['key' => 'enabled', 'label' => '启用本站下载链接', 'type' => 'switch', 'default' => true, 'help' => '总开关:开启后下载通过本站中转链接(别名、次数、有效期生效);关闭后下载按钮直接使用原始下载地址(直连),下方链接规则全部不生效,旧的中转链接同步失效。'],
]],
['key' => 'rules', 'title' => '链接规则', 'fields' => [
['key' => 'url_alias', 'label' => '下载链接 URL 别名', 'type' => 'input', 'default' => 'download', 'required' => true, 'dependsOn' => ['key' => 'enabled', 'value' => true], 'help' => '仅允许英文字母、数字、连字符和下划线;禁止中文、空格和 / \\ ? # & 等特殊符号。修改后旧别名路由立即失效,需重新获取下载地址。'],
['key' => 'default_max_uses', 'label' => '下载链接默认使用次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'enabled', 'value' => true], 'help' => '单个链接可完成下载的次数;文件完整送达后才计次(HEAD 探测、传输中断不计次)。耗尽后链接返回 410,需重新获取。'],
['key' => 'ttl_minutes', 'label' => '下载链接默认有效期限(分钟)', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 1440, 'dependsOn' => ['key' => 'enabled', 'value' => true], 'help' => '链接签发后超过该时长即失效并返回 410,不自动续期,需重新获取。'],
]],
],
],
+30 -66
View File
@@ -68,6 +68,19 @@ class Download {
));
}
/**
* Master switch for the site-routed download system (aksrt_download_config
* "enabled", default true). When off, download buttons render the raw
* source URLs directly and every token route/endpoint below stays dormant.
*/
public function is_enabled(): bool {
$cfg = get_option('aksrt_download_config', []);
if (!is_array($cfg)) {
return true;
}
return !isset($cfg['enabled']) || !empty($cfg['enabled']);
}
/**
* Effective download settings (aksrt_download_config), clamped so a
* corrupted option can never break routing or accounting.
@@ -97,6 +110,10 @@ class Download {
}
public function register_rewrite(): void {
if (!$this->is_enabled()) {
return;
}
add_rewrite_rule(
'^' . preg_quote($this->config()['endpoint'], '/') . '/([A-Za-z0-9]{1,64})/?$',
'index.php?' . self::QUERY_VAR . '=$matches[1]',
@@ -105,11 +122,12 @@ class Download {
}
/**
* Re-flush rewrite rules whenever the configured alias changes, using
* the same version-marker approach as Auth\Router.
* Re-flush rewrite rules whenever the configured alias or the master
* switch changes, using the same version-marker approach as Auth\Router.
*/
public function maybe_flush_rewrite(): void {
$version = AKSRT_VERSION . ':download-endpoint:' . $this->config()['endpoint'];
$endpoint = $this->is_enabled() ? $this->config()['endpoint'] : 'off';
$version = AKSRT_VERSION . ':download-endpoint:' . $endpoint;
if ((string) get_option('aksrt_dl_rewrite_version') !== $version) {
flush_rewrite_rules(false);
update_option('aksrt_dl_rewrite_version', $version);
@@ -122,17 +140,15 @@ class Download {
}
public function register_routes(): void {
if (!$this->is_enabled()) {
return;
}
register_rest_route('aksrt/v1', '/download/request', [
'methods' => 'POST',
'callback' => [$this, 'rest_request'],
'permission_callback' => [$this, 'check_rest_nonce'],
]);
register_rest_route('aksrt/v1', '/download/policy/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'rest_policy'],
'permission_callback' => '__return_true',
]);
}
public function check_rest_nonce(\WP_REST_Request $request) {
@@ -155,63 +171,6 @@ class Download {
return true;
}
public function rest_policy(\WP_REST_Request $request) {
$post_id = (int) $request['id'];
$post = get_post($post_id);
if (!$post || !function_exists('aksrt_is_downloadable_content') || !aksrt_is_downloadable_content($post)) {
return new \WP_Error('aksrt_invalid', __('内容不存在或未启用下载能力', 'aksrt'), ['status' => 404]);
}
$meta = function_exists('aksrt_get_download_box_meta') ? aksrt_get_download_box_meta($post) : [];
$cost = aksrt_resource_download_cost($post);
$uid = get_current_user_id();
$priced = function_exists('aksrt_get_user_pay_price') ? aksrt_get_user_pay_price($post, $uid) : null;
$pay_modo = is_array($priced)
? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0')
: ((($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0');
$display_price = is_array($priced)
? ($priced['amount'] ?? ($pay_modo === 'points' ? 0 : '0.00'))
: (($pay_modo === 'points') ? (int) ($meta['points_price'] ?? 0) : (string) ($meta['price'] ?? '0.00'));
$access = class_exists('AKSRT\\Content\\Visibility')
? Visibility::get_post_access_state($post, $uid)
: [];
$need_captcha = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
return new \WP_REST_Response([
'post_id' => $post_id,
'price' => $display_price,
'base_price' => is_array($priced) ? ($priced['base'] ?? $display_price) : $display_price,
'pay_modo' => $pay_modo,
'pay_limit' => (int) ($meta['pay_limit'] ?? 0),
'cost' => $cost,
'require_login' => !empty($access['need_login']),
'require_captcha' => $need_captcha,
'logged_in' => $uid > 0,
'balance' => $uid > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($uid, \AKSRT\Ledger\AssetType::POINTS) : 0,
'has_paid_access' => !empty($access['has_paid_access']),
'need_purchase' => !empty($access['need_purchase']),
'need_vip' => !empty($access['need_vip']),
'need_login' => !empty($access['need_login']) && $uid <= 0,
'can_download' => !empty($access['can_download']),
'login_url' => wp_login_url(get_permalink($post)),
'vip_level' => (int) ($access['user_vip_level'] ?? 0),
'free_limit' => $uid > 0 && function_exists('aksrt_get_user_free_download_limit') ? aksrt_get_user_free_download_limit($uid) : 0,
'free_used' => $uid > 0 && function_exists('aksrt_get_user_free_downloaded_number') ? aksrt_get_user_free_downloaded_number($uid) : 0,
'free_surplus' => $uid > 0 && function_exists('aksrt_get_free_download_surplus') ? aksrt_get_free_download_surplus($uid) : null,
'sources' => array_map(function ($row, $idx) {
$extract_code = trim((string) ($row['extract_code'] ?? ''));
return [
'index' => $idx,
'label' => $row['label'] ?? '',
'type' => $row['type'] ?? 'direct',
'has_extract' => $extract_code !== '',
];
}, $meta['downloads'] ?? [], array_keys($meta['downloads'] ?? [])),
], 200);
}
public function rest_request(\WP_REST_Request $request) {
$params = $request->get_json_params();
if (!is_array($params)) $params = $request->get_params();
@@ -303,6 +262,11 @@ class Download {
$this->fail_410();
}
// Master switch off: the token system is retired, links are dead ends.
if (!$this->is_enabled()) {
$this->fail_410();
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('dl_token');
if (!$table) $this->fail_410();
+34 -2
View File
@@ -26,6 +26,10 @@ if ($pay_type !== 'download' || empty($downloads)) {
$is_resource_context = function_exists('aksrt_is_resource_post') && aksrt_is_resource_post($post_id);
$context_variant = $is_resource_context ? 'resource' : 'content';
// 下载总开关关闭 = 直连模式:下载按钮直接输出原始地址,不走本站中转链接。
$direct_download = class_exists('\AKSRT\Content\Download')
&& !\AKSRT\Content\Download::get_instance()->is_enabled();
$subject_label = trim((string) ($meta['subject_label'] ?? ''));
if ($subject_label === '') {
$subject_label = $is_resource_context ? __('资源', 'aksrt') : __('内容', 'aksrt');
@@ -56,7 +60,10 @@ $pay_modo = is_array($priced)
$user_amount = is_array($priced) ? (float) ($priced['amount'] ?? 0) : (float) ($meta['price'] ?? 0);
$cost = function_exists('aksrt_resource_download_cost') ? (int) aksrt_resource_download_cost($post_id) : 0;
$captcha_active = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
// 直连模式下没有“获取链接”请求,验证码容器不会被打开,属死 UI 不渲染。
$captcha_active = !$direct_download
&& function_exists('aksrt_is_captcha_enabled_for')
&& aksrt_is_captcha_enabled_for('download');
$balance = (int) ($gate_data['balance'] ?? (is_user_logged_in() ? \AKSRT\Ledger\LedgerService::get_instance()->balance(get_current_user_id(), \AKSRT\Ledger\AssetType::POINTS) : 0));
$login_url = (string) ($gate_data['login_url'] ?? wp_login_url(get_permalink($post_id)));
$vip_url = function_exists('aksrt_get_user_center_tab_url')
@@ -343,6 +350,10 @@ if ($is_need_purchase && $user_amount > 0) {
}
$type = isset($row['type']) ? (string) $row['type'] : 'direct';
$url = isset($row['url']) ? (string) $row['url'] : '';
if ($direct_download && $url === '') {
// 直连模式没有可用的原始地址,空源不渲染。
continue;
}
$extract_code = isset($row['extract_code']) ? trim((string) $row['extract_code']) : '';
$has_extract = $extract_code !== '';
@@ -353,23 +364,38 @@ if ($is_need_purchase && $user_amount > 0) {
if ($is_need_login) {
$btn_tag = 'a';
$btn_href = $login_url;
$btn_text = __('登录后下载', 'aksrt');
$btn_icon = 'fa-sign-in-alt';
$btn_modifier = ' resource-download-box__btn--login';
$btn_action = '';
$btn_target = '';
$btn_rel = '';
} elseif ($direct_download) {
$btn_tag = 'a';
$btn_href = $url;
$btn_text = __('立即下载', 'aksrt');
$btn_icon = 'fa-download';
$btn_modifier = $has_paid_access ? ' resource-download-box__btn--owned' : '';
$btn_action = '';
$btn_target = ' target="_blank"';
$btn_rel = ' rel="nofollow noopener"';
} else {
$btn_tag = 'button';
$btn_href = '';
$btn_text = __('立即下载', 'aksrt');
$btn_icon = 'fa-download';
$btn_modifier = $has_paid_access ? ' resource-download-box__btn--owned' : '';
$btn_action = 'aksrt-download';
$btn_target = '';
$btn_rel = '';
}
?>
<li class="resource-download-box__item">
<?php if ($btn_tag === 'a') : ?>
<a
class="resource-download-box__btn btn-primary resource-download-box__btn--service-<?php echo esc_attr($service['key']); ?><?php echo esc_attr($btn_modifier); ?>"
href="<?php echo esc_url($login_url); ?>"
href="<?php echo esc_url($btn_href); ?>"<?php echo $btn_target; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo $btn_rel; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
data-source-index="<?php echo esc_attr((string) $idx); ?>"
data-source-type="<?php echo esc_attr($type); ?>"
style="--service-color: <?php echo esc_attr($service['color']); ?>"
@@ -380,6 +406,12 @@ if ($is_need_purchase && $user_amount > 0) {
<?php echo esc_html($service['name']); ?>
</span>
</a>
<?php if ($direct_download && $has_extract) : ?>
<button type="button" class="resource-download-box__btn resource-download-box__btn--extract"
data-clipboard-tag="<?php esc_attr_e('提取码', 'aksrt'); ?>"
data-clipboard-text="<?php echo esc_attr($extract_code); ?>"
><?php echo esc_html($extract_code); ?></button>
<?php endif; ?>
<?php else : ?>
<button
type="button"