From 8e1fd0a30fd54080b7f3a8d697af673b1d041bbb Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 6 Oct 2026 19:52:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E8=AE=A9=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E3=80=8C=E9=94=80=E9=87=8F=E6=B5=AE=E5=8A=A8=E3=80=8D=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E7=9C=9F=E6=AD=A3=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 资源 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 文件无语法错误。 --- inc/helpers/aksrt-pay.php | 34 +++++++++-- inc/helpers/aksrt-resource.php | 3 +- src/Admin/Native/PostListEnhance.php | 5 +- .../sales-volume-float-contract.test.cjs | 56 +++++++++++++++++++ 4 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 tests/widget-system/sales-volume-float-contract.test.cjs diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index f7515e80..5c90404e 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -296,8 +296,8 @@ if (!function_exists('aksrt_get_download_box_meta')) { 'view_count' => max(0, $view_count), 'virtual_sales' => $virtual_sales, 'sales_volume' => max(0, $sales_volume), - // legacy: virtual (pay_cuont) + real sales_volume; fall back to downloads when no sales yet. - 'sales_count' => $show_sales ? ($virtual_sales + max(0, $sales_volume, $download_count)) : 0, + // 展示销量 = 真实订单 + 后台「销量浮动」(pay_count)。 + 'sales_count' => $show_sales ? aksrt_get_content_sales_count($p, $pay_meta) : 0, 'show_sales' => $show_sales, 'download_limit_over_price' => $over_price, '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 $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). */ @@ -1348,8 +1372,7 @@ if (!function_exists('aksrt_get_paywall_html')) { if ($doc === '') { $doc = sprintf(__('此内容为%s,请付费后查看', 'aksrt'), $type_label); } - $sales = max(0, (int) ($meta['virtual_sales'] ?? 0)); - $sales += max(0, (int) get_post_meta($post->ID, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true)); + $sales = aksrt_get_content_sales_count($post, $meta); $tag = $is_widget ? '' . esc_html($type_label) . '' : '
' . esc_html($type_label) . '
'; @@ -1537,8 +1560,7 @@ if (!function_exists('aksrt_get_paid_box_html')) { $order_type = (int) ($order_types[$pay_type] ?? 1); $type_label = (string) ($type_labels[$pay_type] ?? __('付费内容', 'aksrt')); $title = get_the_title($post); - $sales = max(0, (int) ($meta['virtual_sales'] ?? 0)); - $sales += max(0, (int) get_post_meta($post->ID, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true)); + $sales = aksrt_get_content_sales_count($post, $meta); $sales_label = function_exists('aksrt_format_number') ? aksrt_format_number($sales) : number_format_i18n($sales); $count = $sales > 0 ? '' . esc_html(sprintf(__('已售 %s', 'aksrt'), $sales_label)) . '' : ''; $order = aksrt_get_current_content_paid_order($post); diff --git a/inc/helpers/aksrt-resource.php b/inc/helpers/aksrt-resource.php index b720760d..13866ce3 100644 --- a/inc/helpers/aksrt-resource.php +++ b/inc/helpers/aksrt-resource.php @@ -84,7 +84,8 @@ if (!function_exists('aksrt_get_resource_meta')) { 'download_count' => max(0, max($download_count, $legacy_download_count)), 'view_count' => max(0, max($resource_view_count, $common_view_count)), '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, 'require_login' => !empty($pay_meta['require_login']), 'require_captcha' => !empty($pay_meta['require_captcha']), diff --git a/src/Admin/Native/PostListEnhance.php b/src/Admin/Native/PostListEnhance.php index dc1fc9e9..d927f091 100644 --- a/src/Admin/Native/PostListEnhance.php +++ b/src/Admin/Native/PostListEnhance.php @@ -166,8 +166,11 @@ class PostListEnhance { $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); + $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 ? ' [' . esc_html(sprintf(__('真实销量%s', 'aksrt'), $real_sales)) . ']' : ''; diff --git a/tests/widget-system/sales-volume-float-contract.test.cjs b/tests/widget-system/sales-volume-float-contract.test.cjs new file mode 100644 index 00000000..64085c28 --- /dev/null +++ b/tests/widget-system/sales-volume-float-contract.test.cjs @@ -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\)/) +})