fix(admin): 让后台「销量浮动」设置真正生效
资源 metabox 和快捷编辑都能设置「销量浮动」(pay_count),但设了完全 不起作用——所有读取方用的是另一个键 virtual_sales(§3.8 电商的虚拟 销量加成),pay_count 只被写、从未被读。 新增统一读取口径 aksrt_get_content_sales_count($post, $pay_meta): 真实订单销量 + pay_count,并把四处各自拼装销量的地方全部接到这个 口径上: - aksrt_get_download_box_meta() 的 sales_count(内容帖) - aksrt_get_resource_meta() 的 sales_count(资源帖) - 付费墙 aksrt_get_paywall_html() 的已售徽标 - 付费盒 aksrt_get_paid_box_html() 的已售徽标 - 后台「所有文章」销售数据列的销量 此前资源帖的 sales_count 走的是「虚拟销量 + 下载量」这条与内容帖 不一致的口径,现统一。 快捷编辑的 pay_count 运算本是整数加减 + max(0,...) 归零,与读取口径 一致,无需改动。 _aksrt_sales_volume 保持不变,它仍是订单流水累加的副本;列表列取 订单表统计与 meta 副本的较大值再加浮动量,避免两条链路数字打架。 新增 sales-volume-float-contract.test.cjs 4 项,锁住「pay_count 必须 被销量读取方消费」这一契约,防止以后又被改成摆设。 验证:node --test tests/widget-system/*.test.cjs 449 项全部通过 (原有 445 项 + 新增 4 项);php -l 三个 PHP 文件无语法错误。
This commit is contained in:
@@ -296,8 +296,8 @@ if (!function_exists('aksrt_get_download_box_meta')) {
|
|||||||
'view_count' => max(0, $view_count),
|
'view_count' => max(0, $view_count),
|
||||||
'virtual_sales' => $virtual_sales,
|
'virtual_sales' => $virtual_sales,
|
||||||
'sales_volume' => max(0, $sales_volume),
|
'sales_volume' => max(0, $sales_volume),
|
||||||
// legacy: virtual (pay_cuont) + real sales_volume; fall back to downloads when no sales yet.
|
// 展示销量 = 真实订单 + 后台「销量浮动」(pay_count)。
|
||||||
'sales_count' => $show_sales ? ($virtual_sales + max(0, $sales_volume, $download_count)) : 0,
|
'sales_count' => $show_sales ? aksrt_get_content_sales_count($p, $pay_meta) : 0,
|
||||||
'show_sales' => $show_sales,
|
'show_sales' => $show_sales,
|
||||||
'download_limit_over_price' => $over_price,
|
'download_limit_over_price' => $over_price,
|
||||||
'require_login' => !empty($pay_meta['require_login']),
|
'require_login' => !empty($pay_meta['require_login']),
|
||||||
@@ -352,6 +352,30 @@ if (!function_exists('aksrt_bump_content_sales_volume')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total sales shown to visitors: real paid orders plus the configured 销量浮动.
|
||||||
|
*
|
||||||
|
* pay_count is the only field behind the 后台「销量浮动」输入框(资源 metabox 与
|
||||||
|
* 快捷编辑都写这个键),此前没有任何读取方用它,导致设了不生效。所有展示销量
|
||||||
|
* 的地方都必须走这个口径,不要再各自拼 virtual_sales / sales_volume。
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $pay_meta 已取得的付费 meta,传空则按 $post_id 现取。
|
||||||
|
*/
|
||||||
|
if (!function_exists('aksrt_get_content_sales_count')) {
|
||||||
|
function aksrt_get_content_sales_count($post = null, array $pay_meta = []): int {
|
||||||
|
$post_id = $post instanceof \WP_Post ? (int) $post->ID : (int) $post;
|
||||||
|
if ($post_id <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ($pay_meta === []) {
|
||||||
|
$pay_meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post) : [];
|
||||||
|
}
|
||||||
|
$real = max(0, (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
|
||||||
|
$float = max(0, (int) ($pay_meta['pay_count'] ?? 0));
|
||||||
|
return max(0, $real + $float);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether guest cash purchase is allowed (legacy pay_no_logged_in + pay_limit=0).
|
* Whether guest cash purchase is allowed (legacy pay_no_logged_in + pay_limit=0).
|
||||||
*/
|
*/
|
||||||
@@ -1348,8 +1372,7 @@ if (!function_exists('aksrt_get_paywall_html')) {
|
|||||||
if ($doc === '') {
|
if ($doc === '') {
|
||||||
$doc = sprintf(__('此内容为%s,请付费后查看', 'aksrt'), $type_label);
|
$doc = sprintf(__('此内容为%s,请付费后查看', 'aksrt'), $type_label);
|
||||||
}
|
}
|
||||||
$sales = max(0, (int) ($meta['virtual_sales'] ?? 0));
|
$sales = aksrt_get_content_sales_count($post, $meta);
|
||||||
$sales += max(0, (int) get_post_meta($post->ID, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
|
|
||||||
$tag = $is_widget
|
$tag = $is_widget
|
||||||
? '<badge class="pay-tag abs-center">' . esc_html($type_label) . '</badge>'
|
? '<badge class="pay-tag abs-center">' . esc_html($type_label) . '</badge>'
|
||||||
: '<div class="pay-tag abs-center">' . esc_html($type_label) . '</div>';
|
: '<div class="pay-tag abs-center">' . esc_html($type_label) . '</div>';
|
||||||
@@ -1537,8 +1560,7 @@ if (!function_exists('aksrt_get_paid_box_html')) {
|
|||||||
$order_type = (int) ($order_types[$pay_type] ?? 1);
|
$order_type = (int) ($order_types[$pay_type] ?? 1);
|
||||||
$type_label = (string) ($type_labels[$pay_type] ?? __('付费内容', 'aksrt'));
|
$type_label = (string) ($type_labels[$pay_type] ?? __('付费内容', 'aksrt'));
|
||||||
$title = get_the_title($post);
|
$title = get_the_title($post);
|
||||||
$sales = max(0, (int) ($meta['virtual_sales'] ?? 0));
|
$sales = aksrt_get_content_sales_count($post, $meta);
|
||||||
$sales += max(0, (int) get_post_meta($post->ID, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
|
|
||||||
$sales_label = function_exists('aksrt_format_number') ? aksrt_format_number($sales) : number_format_i18n($sales);
|
$sales_label = function_exists('aksrt_format_number') ? aksrt_format_number($sales) : number_format_i18n($sales);
|
||||||
$count = $sales > 0 ? '<badge class="img-badge hot jb-blue px12">' . esc_html(sprintf(__('已售 %s', 'aksrt'), $sales_label)) . '</badge>' : '';
|
$count = $sales > 0 ? '<badge class="img-badge hot jb-blue px12">' . esc_html(sprintf(__('已售 %s', 'aksrt'), $sales_label)) . '</badge>' : '';
|
||||||
$order = aksrt_get_current_content_paid_order($post);
|
$order = aksrt_get_current_content_paid_order($post);
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ if (!function_exists('aksrt_get_resource_meta')) {
|
|||||||
'download_count' => max(0, max($download_count, $legacy_download_count)),
|
'download_count' => max(0, max($download_count, $legacy_download_count)),
|
||||||
'view_count' => max(0, max($resource_view_count, $common_view_count)),
|
'view_count' => max(0, max($resource_view_count, $common_view_count)),
|
||||||
'virtual_sales' => $virtual_sales,
|
'virtual_sales' => $virtual_sales,
|
||||||
'sales_count' => $show_sales ? ($virtual_sales + max(0, max($download_count, $legacy_download_count))) : 0,
|
// 展示销量 = 真实订单 + 后台「销量浮动」(pay_count),与内容帖同一口径。
|
||||||
|
'sales_count' => $show_sales ? aksrt_get_content_sales_count($p, $pay_meta) : 0,
|
||||||
'show_sales' => $show_sales,
|
'show_sales' => $show_sales,
|
||||||
'require_login' => !empty($pay_meta['require_login']),
|
'require_login' => !empty($pay_meta['require_login']),
|
||||||
'require_captcha' => !empty($pay_meta['require_captcha']),
|
'require_captcha' => !empty($pay_meta['require_captcha']),
|
||||||
|
|||||||
@@ -166,8 +166,11 @@ class PostListEnhance {
|
|||||||
$price_text = $this->currency_symbol() . ':' . (string) ($pay_meta['price'] ?? '0.00');
|
$price_text = $this->currency_symbol() . ':' . (string) ($pay_meta['price'] ?? '0.00');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sales_volume = max(0, (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
|
// 列表列与前台同一口径:真实订单 + 后台「销量浮动」(pay_count)。
|
||||||
|
// 真实销量取订单表统计,_aksrt_sales_volume 只是订单流水累加的副本。
|
||||||
$real_sales = $this->real_sales_count($post_id);
|
$real_sales = $this->real_sales_count($post_id);
|
||||||
|
$meta_sales = max(0, (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
|
||||||
|
$sales_volume = max($real_sales, $meta_sales) + max(0, (int) ($pay_meta['pay_count'] ?? 0));
|
||||||
$real_html = $real_sales > 0
|
$real_html = $real_sales > 0
|
||||||
? ' <a title="' . esc_attr__('点击查看销售明细', 'aksrt') . '" href="' . esc_url($this->orders_url($post_id)) . '">[' . esc_html(sprintf(__('真实销量%s', 'aksrt'), $real_sales)) . ']</a>'
|
? ' <a title="' . esc_attr__('点击查看销售明细', 'aksrt') . '" href="' . esc_url($this->orders_url($post_id)) . '">[' . esc_html(sprintf(__('真实销量%s', 'aksrt'), $real_sales)) . ']</a>'
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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 pay = read('inc/helpers/aksrt-pay.php')
|
||||||
|
const resource = read('inc/helpers/aksrt-resource.php')
|
||||||
|
const list = read('src/Admin/Native/PostListEnhance.php')
|
||||||
|
const domain = read('src/Core/DomainModel.php')
|
||||||
|
|
||||||
|
test('sales count has a single read path that adds the 销量浮动', () => {
|
||||||
|
// 展示销量只有一个口径:真实订单 + 后台「销量浮动」(pay_count)。
|
||||||
|
// 此前各调用点各自拼 virtual_sales / sales_volume,pay_count 写进去却没人读。
|
||||||
|
assert.match(pay, /function aksrt_get_content_sales_count\(/)
|
||||||
|
assert.match(pay, /RESOURCE_SALES_VOLUME_KEY/)
|
||||||
|
assert.match(pay, /\$pay_meta\['pay_count'\]/)
|
||||||
|
// 两部分都必须下限为 0,避免负的浮动把销量拉成负数。
|
||||||
|
assert.match(pay, /max\(0, \$real \+ \$float\)/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('every sales_count producer goes through that read path', () => {
|
||||||
|
// 内容帖、资源帖、付费墙、付费盒四处不得再各自拼装销量。
|
||||||
|
assert.match(pay, /'sales_count'\s*=>\s*\$show_sales \? aksrt_get_content_sales_count\(\$p, \$pay_meta\) : 0/)
|
||||||
|
assert.match(resource, /'sales_count'\s*=>\s*\$show_sales \? aksrt_get_content_sales_count\(\$p, \$pay_meta\) : 0/)
|
||||||
|
assert.match(pay, /\$sales = aksrt_get_content_sales_count\(\$post, \$meta\)/)
|
||||||
|
|
||||||
|
// 不得再出现按 virtual_sales 现拼销量的旧写法。
|
||||||
|
assert.doesNotMatch(pay, /\$sales\s*=\s*max\(0, \(int\) \(\$meta\['virtual_sales'\] \?\? 0\)\);/)
|
||||||
|
assert.doesNotMatch(pay, /\$virtual_sales \+ max\(0, \$sales_volume, \$download_count\)/)
|
||||||
|
assert.doesNotMatch(resource, /\$virtual_sales \+ max\(0, max\(\$download_count, \$legacy_download_count\)\)/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pay_count is declared, sanitized and consumed end to end', () => {
|
||||||
|
// 删掉任何一段,这个字段就又变成「设了不生效」的摆设。
|
||||||
|
assert.match(domain, /'pay_count'\s*=>\s*0/, 'default value exists')
|
||||||
|
assert.match(domain, /\$meta\['pay_count'\]\s*=\s*\(int\)/, 'sanitize exists')
|
||||||
|
|
||||||
|
// 后台两个入口都写这个键:资源 metabox 与文章列表快捷/批量编辑。
|
||||||
|
assert.match(read('src/Core/ResourceMetaBox.php'), /'pay_count'/)
|
||||||
|
assert.match(list, /case 'pay_count':/)
|
||||||
|
|
||||||
|
// 快捷编辑按整数运算,与读取口径的 max(0,(int)) 一致。
|
||||||
|
const branch = list.slice(list.indexOf("case 'pay_count':"), list.indexOf('case \'price\':'))
|
||||||
|
assert.match(branch, /apply_operation/)
|
||||||
|
assert.match(branch, /max\(0, \(int\) \$new\)/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the admin sales column shows 真实 + 浮动, not the raw meta', () => {
|
||||||
|
// 列表列与前台同一口径:真实订单 + pay_count,否则后台与前台数字不一致。
|
||||||
|
assert.match(list, /\$sales_volume = max\(\$real_sales, \$meta_sales\) \+ max\(0, \(int\) \(\$pay_meta\['pay_count'\] \?\? 0\)\)/)
|
||||||
|
// 真实销量取订单表统计,不能只有 meta 副本。
|
||||||
|
assert.match(list, /\$real_sales = \$this->real_sales_count\(\$post_id\)/)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user