fix(admin): 修复后台文章列表互动数据列无数据
后台「文章 → 所有文章」列表的「互动数据」列四项计数(浏览/点赞/ 收藏/下载)恒为 0。原因是列回调读取的 meta key 与主题实际写入的 键不一致:读取用的是 'views' / 'like' / 'favorite' / 'download_count',而全仓库的写入方只写 aksrt_post_views / _aksrt_like_count / _aksrt_favorite_count / _aksrt_res_download_count, 'views' / 'like' / 'favorite' 从未被任何代码写过。 改为复用既有的 aksrt_get_post_views() / aksrt_get_post_likes() / aksrt_get_post_favorites() 读取(helpers 在 bootstrap 中先于 core-hooks.php 加载,仍保留 function_exists 兜底与原始键回退), 下载量走 DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY,并对历史 download_count 键做一次回退读取。 同时修正两个相邻问题: - 缩写口径与前台不一致:列内用的是前台从未使用的「万」后缀, 改为统一的 aksrt_format_number()(≥10000 → 1.2W+, ≥1000 → 1.5K),与前台列表、文章详情页保持同一套。 - pre_get_posts 的按计数排序同样拿用户可见的排序键当 meta_key 使用,现映射到真实存储键。 资源下载走普通外链,主题内没有下载计数的写入方,下载项仍只在 >0 时出现,此为现状而非本次回归。 新增 tests/widget-system/admin-stats-column-contract.test.cjs (7 项)锁定列注册、读取键与写入键一致、缩写口径、排序键映射, 以及下载键的写入方状态,避免再次改名时静默失效。 验证:node --test "tests/*/*.test.cjs" 515 项全部通过 (原有 508 项 + 新增 7 项);php -l inc/core-hooks.php 无语法错误。
This commit is contained in:
+23
-8
@@ -482,13 +482,23 @@ add_action('admin_enqueue_scripts', function ($hook) {
|
||||
add_action('manage_post_posts_custom_column', function ($column, $post_id) {
|
||||
if ($column !== 'aksrt_stats') return;
|
||||
|
||||
$views = (int) get_post_meta($post_id, 'views', true);
|
||||
$likes = (int) get_post_meta($post_id, 'like', true);
|
||||
$favorites = (int) get_post_meta($post_id, 'favorite', true);
|
||||
$downloads = (int) get_post_meta($post_id, 'download_count', true);
|
||||
$views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, true);
|
||||
$likes = function_exists('aksrt_get_post_likes') ? (int) aksrt_get_post_likes($post_id) : (int) get_post_meta($post_id, '_aksrt_like_count', true);
|
||||
$favorites = function_exists('aksrt_get_post_favorites') ? (int) aksrt_get_post_favorites($post_id) : (int) get_post_meta($post_id, '_aksrt_favorite_count', true);
|
||||
$downloads = (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY, true);
|
||||
|
||||
if ($downloads <= 0) {
|
||||
$legacy_downloads = (int) get_post_meta($post_id, 'download_count', true);
|
||||
if ($legacy_downloads > 0) {
|
||||
$downloads = $legacy_downloads;
|
||||
}
|
||||
}
|
||||
|
||||
$format = function (int $n): string {
|
||||
if ($n >= 10000) return round($n / 10000, 1) . '万';
|
||||
if (function_exists('aksrt_format_number')) {
|
||||
return (string) aksrt_format_number($n);
|
||||
}
|
||||
if ($n >= 10000) return round($n / 10000, 1) . 'W+';
|
||||
if ($n >= 1000) return round($n / 1000, 1) . 'K';
|
||||
return (string) $n;
|
||||
};
|
||||
@@ -510,10 +520,15 @@ add_action('pre_get_posts', function ($query) {
|
||||
if (!is_admin() || !$query->is_main_query()) return;
|
||||
|
||||
$orderby = $query->get('orderby');
|
||||
$allowed = ['views', 'like', 'favorite', 'download_count'];
|
||||
$allowed = [
|
||||
'views' => \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY,
|
||||
'like' => '_aksrt_like_count',
|
||||
'favorite' => '_aksrt_favorite_count',
|
||||
'download_count' => \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY,
|
||||
];
|
||||
|
||||
if (in_array($orderby, $allowed, true)) {
|
||||
$query->set('meta_key', $orderby);
|
||||
if (isset($allowed[$orderby])) {
|
||||
$query->set('meta_key', $allowed[$orderby]);
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const root = path.resolve(__dirname, '../..')
|
||||
const read = file => fs.readFileSync(path.join(root, file), 'utf8')
|
||||
|
||||
const hooks = read('inc/core-hooks.php')
|
||||
const domain = read('src/Core/DomainModel.php')
|
||||
const tags = read('inc/helpers/aksrt-frontend-tags.php')
|
||||
|
||||
test('admin post list has the interaction stats column', () => {
|
||||
assert.match(hooks, /manage_post_posts_columns/)
|
||||
assert.match(hooks, /互动数据/)
|
||||
assert.match(hooks, /manage_post_posts_custom_column/)
|
||||
assert.match(hooks, /\$column !== 'aksrt_stats'/)
|
||||
})
|
||||
|
||||
test('interaction counts are read from the keys the theme actually writes', () => {
|
||||
// 计数一律落在这几个 meta key 上:aksrt_post_views / _aksrt_like_count /
|
||||
// _aksrt_favorite_count / _aksrt_res_download_count。列回调只能用同一套键,
|
||||
// 否则全列恒为 0。旧实现读 'views' / 'like' / 'favorite',仓库里没有任何写入方。
|
||||
assert.match(tags, /'_aksrt_like_count'/)
|
||||
assert.match(tags, /'_aksrt_favorite_count'/)
|
||||
assert.match(tags, /'aksrt_post_views'/)
|
||||
|
||||
assert.match(hooks, /aksrt_get_post_views\(\$post_id\)/)
|
||||
assert.match(hooks, /aksrt_get_post_likes\(\$post_id\)/)
|
||||
assert.match(hooks, /aksrt_get_post_favorites\(\$post_id\)/)
|
||||
|
||||
// 资源下载量只有 _aksrt_res_download_count 一个写入键。
|
||||
assert.match(hooks, /DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY/)
|
||||
assert.match(domain, /RESOURCE_DOWNLOAD_COUNT_KEY\s*=\s*'_aksrt_res_download_count'/)
|
||||
|
||||
// 不得再直接读这些不存在的键('download_count' 只作为历史遗留键回退读取)。
|
||||
const column = hooks.slice(hooks.indexOf('manage_post_posts_custom_column'), hooks.indexOf('Support sorting by'))
|
||||
assert.doesNotMatch(column, /get_post_meta\(\$post_id, 'views'/)
|
||||
assert.doesNotMatch(column, /get_post_meta\(\$post_id, 'like'/)
|
||||
assert.doesNotMatch(column, /get_post_meta\(\$post_id, 'favorite'/)
|
||||
})
|
||||
|
||||
test('admin stats column shares the aksrt_format_number abbreviation', () => {
|
||||
// 后台列与前台必须同一套缩写口径(≥10000 → 1.2W+,≥1000 → 1.5K)。
|
||||
// 旧实现用了前台从未用过的「万」后缀。
|
||||
assert.match(hooks, /aksrt_format_number\(\$n\)/)
|
||||
const column = hooks.slice(hooks.indexOf('manage_post_posts_custom_column'), hooks.indexOf('Support sorting by'))
|
||||
assert.doesNotMatch(column, /'万'/)
|
||||
})
|
||||
|
||||
test('sortable meta maps the public orderby keys to the stored keys', () => {
|
||||
assert.match(hooks, /'views'\s*=>\s*\\AKSRT\\Core\\DomainModel::POST_VIEW_COUNT_KEY/)
|
||||
assert.match(hooks, /'like'\s*=>\s*'_aksrt_like_count'/)
|
||||
assert.match(hooks, /'favorite'\s*=>\s*'_aksrt_favorite_count'/)
|
||||
assert.match(hooks, /'download_count'\s*=>\s*\\AKSRT\\Core\\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY/)
|
||||
// meta_key 必须是真实存储键,而不是用户可见的排序键。
|
||||
assert.match(hooks, /\$query->set\('meta_key', \$allowed\[\$orderby\]\)/)
|
||||
})
|
||||
|
||||
test('every counter meta key the admin column renders has a writer', () => {
|
||||
// 逐键核对:列里展示的每项计数都必须在某个 helper 里真正被写入过。
|
||||
const sources = [
|
||||
'inc/core-hooks.php',
|
||||
'inc/helpers/aksrt-frontend-tags.php',
|
||||
'src/Core/PostMetaBox.php',
|
||||
'src/Frontend/PageAdminSet.php',
|
||||
].map(read).join('\n')
|
||||
|
||||
for (const key of ['aksrt_post_views', '_aksrt_like_count', '_aksrt_favorite_count']) {
|
||||
assert.ok(sources.includes(`'${key}'`), `${key} is written somewhere`)
|
||||
}
|
||||
// 资源帖的浏览有独立键,aksrt_get_post_views() 取两者较大值。
|
||||
assert.match(tags, /_aksrt_res_view_count/)
|
||||
assert.match(read('inc/helpers/aksrt-resource.php'), /RESOURCE_VIEW_COUNT_KEY|_aksrt_res_view_count/)
|
||||
|
||||
// 资源下载走普通外链,主题内没有任何写入方,所以下载项只在 >0 时出现。
|
||||
// 万一将来接了下载统计,写入方必须落在同一个键上。
|
||||
assert.match(hooks, /\$downloads <= 0/)
|
||||
assert.match(hooks, /\$legacy_downloads/)
|
||||
})
|
||||
|
||||
test('download count stays read-only until a writer exists', () => {
|
||||
// 目前没有任何 PHP 写入 _aksrt_res_download_count;若哪天新增了写入方,
|
||||
// 这里会失败,提示把该键也纳入后台列的统计口径。
|
||||
const php = ['inc', 'src', 'templates'].flatMap(dir =>
|
||||
fs.readdirSync(path.join(root, dir), { withFileTypes: true })
|
||||
.filter(e => e.isFile() && e.name.endsWith('.php'))
|
||||
.map(e => `${dir}/${e.name}`)
|
||||
).filter(f => !f.includes('core-hooks') && !f.includes('DomainModel')).map(read).join('\n')
|
||||
const writers = php.match(/(?:update|add)_post_meta\([^)]*_aksrt_res_download_count/g) || []
|
||||
assert.equal(writers.length, 0, 'download count still has no writer')
|
||||
})
|
||||
|
||||
test('admin column icons stay behind the Font Awesome enqueue on edit.php', () => {
|
||||
assert.match(hooks, /admin_enqueue_scripts[\s\S]*?edit\.php[\s\S]*?aksrt_enqueue_fontawesome\(\)/)
|
||||
for (const klass of ['fa-eye', 'fa-thumbs-up', 'fa-star', 'fa-download']) {
|
||||
assert.match(hooks, new RegExp(klass))
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user