diff --git a/functions.php b/functions.php
index e12f4b2f..4c793d43 100644
--- a/functions.php
+++ b/functions.php
@@ -371,6 +371,9 @@ if (is_admin() && class_exists('AKSRT_Admin_Pay_After_Sale')) {
if (is_admin() && class_exists('AKSRT_Admin_Pay_Orders')) {
AKSRT_Admin_Pay_Orders::get_instance()->init();
}
+if (is_admin() && class_exists('AKSRT_Admin_Pay_Products')) {
+ AKSRT_Admin_Pay_Products::get_instance()->init();
+}
if (is_admin() && class_exists('AKSRT_Admin_Pay_Withdrawals')) {
AKSRT_Admin_Pay_Withdrawals::get_instance()->init();
}
diff --git a/inc/bootstrap.php b/inc/bootstrap.php
index 675d3751..db167446 100644
--- a/inc/bootstrap.php
+++ b/inc/bootstrap.php
@@ -178,6 +178,7 @@ require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-charge
require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-shipping.php';
require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-after-sale.php';
require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-orders.php';
+require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-products.php';
require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-withdrawals.php';
require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-income.php';
require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-rebate.php';
diff --git a/inc/classes/admin/native/class-aksrt-admin-pay-menus.php b/inc/classes/admin/native/class-aksrt-admin-pay-menus.php
index f9a076d7..f3189845 100644
--- a/inc/classes/admin/native/class-aksrt-admin-pay-menus.php
+++ b/inc/classes/admin/native/class-aksrt-admin-pay-menus.php
@@ -54,6 +54,17 @@ class AKSRT_Admin_Pay_Menus {
wp_die(esc_html__('订单模块未初始化。', 'aksrt'));
},
],
+ 'products' => [
+ 'slug' => 'aksrt_pay_products',
+ 'title' => __('付费文章', 'aksrt'),
+ 'callback' => static function (): void {
+ if (class_exists('AKSRT_Admin_Pay_Products')) {
+ AKSRT_Admin_Pay_Products::get_instance()->render_products_page();
+ return;
+ }
+ wp_die(esc_html__('付费文章模块未初始化。', 'aksrt'));
+ },
+ ],
'shipping' => [
'slug' => 'aksrt_shipping',
'title' => __('发货及物流', 'aksrt'),
diff --git a/inc/classes/admin/native/class-aksrt-admin-pay-orders.php b/inc/classes/admin/native/class-aksrt-admin-pay-orders.php
index 6311e75b..615fff0b 100644
--- a/inc/classes/admin/native/class-aksrt-admin-pay-orders.php
+++ b/inc/classes/admin/native/class-aksrt-admin-pay-orders.php
@@ -48,10 +48,12 @@ class AKSRT_Admin_Pay_Orders {
return;
}
- $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
- $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
- $page_num = max(1, (int) ($_GET['paged'] ?? 1));
- $per_page = 20;
+ $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
+ $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
+ $product_type = isset($_GET['product_type']) ? sanitize_key((string) wp_unslash($_GET['product_type'])) : '';
+ $product_id = isset($_GET['product_id']) ? max(0, (int) $_GET['product_id']) : 0;
+ $page_num = max(1, (int) ($_GET['paged'] ?? 1));
+ $per_page = 20;
$repo = AKSRT_Order_Repository::get_instance();
$args = [
@@ -61,6 +63,12 @@ class AKSRT_Admin_Pay_Orders {
if ($status !== '' && $status !== 'all') {
$args['status'] = $status;
}
+ if ($product_type !== '') {
+ $args['product_type'] = $product_type;
+ }
+ if ($product_id > 0) {
+ $args['product_id'] = $product_id;
+ }
$items = $repo->get_orders_admin($args, $search);
$total = $repo->get_orders_count($args, $search);
@@ -71,6 +79,12 @@ class AKSRT_Admin_Pay_Orders {
echo '
';
echo '';
diff --git a/inc/classes/admin/native/class-aksrt-admin-pay-products.php b/inc/classes/admin/native/class-aksrt-admin-pay-products.php
new file mode 100644
index 00000000..b3c4420a
--- /dev/null
+++ b/inc/classes/admin/native/class-aksrt-admin-pay-products.php
@@ -0,0 +1,366 @@
+ensure_cap();
+ $this->render_header(__('付费文章', 'aksrt'));
+
+ if (!class_exists('AKSRT_Order_Repository') || !class_exists('AKSRT_Domain_Model')) {
+ echo '' . esc_html__('订单服务不可用。', 'aksrt') . '
';
+ $this->render_footer();
+ return;
+ }
+
+ $pay_type = isset($_GET['pay_type']) ? sanitize_key((string) wp_unslash($_GET['pay_type'])) : '';
+ $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
+ $page_num = max(1, (int) ($_GET['paged'] ?? 1));
+ $per_page = 20;
+
+ $type_labels = AKSRT_Domain_Model::pay_type_labels();
+ unset($type_labels[AKSRT_Domain_Model::PAY_TYPE_NO]);
+
+ $query = $this->query_paid_posts([
+ 'pay_type' => $pay_type,
+ 'search' => $search,
+ 'page' => $page_num,
+ 'per_page' => $per_page,
+ ]);
+
+ $posts = $query['posts'];
+ $total = $query['total'];
+ $pages = max(1, (int) ceil($total / $per_page));
+
+ $sales_map = [];
+ if ($posts && class_exists('AKSRT_Order_Repository')) {
+ $ids = array_map(static function ($p) {
+ return (int) $p->ID;
+ }, $posts);
+ $sales_map = AKSRT_Order_Repository::get_instance()->get_content_sales_map(
+ $ids,
+ AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE
+ );
+ }
+
+ $this->render_filters($pay_type, $search, $type_labels);
+
+ echo '';
+ echo '';
+ echo '| ' . esc_html__('商品', 'aksrt') . ' | ';
+ echo '' . esc_html__('ID', 'aksrt') . ' | ';
+ echo '' . esc_html__('付费类型', 'aksrt') . ' | ';
+ echo '' . esc_html__('模式', 'aksrt') . ' | ';
+ echo '' . esc_html__('标价', 'aksrt') . ' | ';
+ echo '' . esc_html__('销售均价', 'aksrt') . ' | ';
+ echo '' . esc_html__('订单销量', 'aksrt') . ' | ';
+ echo '' . esc_html__('Meta 销量', 'aksrt') . ' | ';
+ echo '' . esc_html__('销售总额', 'aksrt') . ' | ';
+ echo '' . esc_html__('操作', 'aksrt') . ' | ';
+ echo '
';
+
+ if (!$posts) {
+ echo '| ' . esc_html__('暂无付费文章。', 'aksrt') . ' |
';
+ }
+
+ foreach ($posts as $post) {
+ $post_id = (int) $post->ID;
+ $meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post) : [];
+ $pt = AKSRT_Domain_Model::normalize_pay_type((string) ($meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
+ $pt_label = $type_labels[$pt] ?? $pt;
+ $modo = (($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0';
+ $modo_label = $modo === 'points' ? __('积分', 'aksrt') : __('现金', 'aksrt');
+
+ if ($modo === 'points') {
+ $list_price = (int) ceil((float) ($meta['points_price'] ?? $meta['price'] ?? 0));
+ $price_text = sprintf(__('%d 积分', 'aksrt'), $list_price);
+ } else {
+ $list_price = round((float) ($meta['price'] ?? 0), 2);
+ $price_text = number_format($list_price, 2);
+ }
+
+ $sales = $sales_map[$post_id] ?? ['sales_count' => 0, 'avg_amount' => 0.0, 'revenue' => 0.0];
+ $db_sales = (int) ($sales['sales_count'] ?? 0);
+ $avg = (float) ($sales['avg_amount'] ?? 0);
+ $revenue = (float) ($sales['revenue'] ?? 0);
+ $meta_sales = (int) get_post_meta($post_id, AKSRT_Domain_Model::RESOURCE_SALES_VOLUME_KEY, true);
+
+ $edit_url = get_edit_post_link($post_id, 'raw');
+ $title = get_the_title($post_id);
+ if ($title === '') {
+ $title = sprintf(__('(无标题 #%d)', 'aksrt'), $post_id);
+ }
+
+ $orders_url = $this->page_url('aksrt_orders', [
+ 'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
+ 'product_id' => $post_id,
+ ]);
+
+ echo '';
+ echo '| ';
+ if ($edit_url) {
+ echo '' . esc_html($title) . '';
+ } else {
+ echo '' . esc_html($title) . '';
+ }
+ echo ' | ';
+ echo '' . esc_html((string) $post_id) . ' | ';
+ echo '' . esc_html((string) $pt_label) . ' | ';
+ echo '' . esc_html($modo_label) . ' | ';
+ echo '' . esc_html($price_text) . ' | ';
+ echo '' . esc_html(number_format($avg, 2)) . ' | ';
+ echo '' . esc_html((string) $db_sales) . ' | ';
+ echo '' . esc_html((string) max(0, $meta_sales)) . ' | ';
+ echo '' . esc_html(number_format($revenue, 2)) . ' | ';
+ echo '';
+ if ($edit_url) {
+ echo '' . esc_html__('编辑', 'aksrt') . '';
+ }
+ echo '' . esc_html__('订单', 'aksrt') . '';
+ $permalink = get_permalink($post_id);
+ if ($permalink) {
+ echo '' . esc_html__('查看', 'aksrt') . '';
+ }
+ echo ' | ';
+ echo '
';
+ }
+
+ echo '
';
+ $this->render_pagination($pages, $page_num);
+ $this->render_footer();
+ }
+
+ /**
+ * @param array{pay_type?:string,search?:string,page?:int,per_page?:int} $args
+ * @return array{posts: WP_Post[], total: int}
+ */
+ private function query_paid_posts(array $args): array {
+ $pay_type = sanitize_key((string) ($args['pay_type'] ?? ''));
+ $search = (string) ($args['search'] ?? '');
+ $page = max(1, (int) ($args['page'] ?? 1));
+ $per_page = max(1, min(100, (int) ($args['per_page'] ?? 20)));
+
+ $meta_query = [
+ 'relation' => 'AND',
+ [
+ 'key' => AKSRT_Domain_Model::PAY_META_KEY,
+ 'compare' => 'EXISTS',
+ ],
+ ];
+
+ // Prefer denormalized type key when present; still exclude "no".
+ if ($pay_type !== '' && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_NO) {
+ $meta_query[] = [
+ 'relation' => 'OR',
+ [
+ 'key' => AKSRT_Domain_Model::PAY_TYPE_KEY,
+ 'value' => $pay_type,
+ 'compare' => '=',
+ ],
+ [
+ 'key' => AKSRT_Domain_Model::PAY_META_KEY,
+ 'value' => '"pay_type";s:' . strlen($pay_type) . ':"' . $pay_type . '"',
+ 'compare' => 'LIKE',
+ ],
+ ];
+ } else {
+ $meta_query[] = [
+ 'relation' => 'OR',
+ [
+ 'key' => AKSRT_Domain_Model::PAY_TYPE_KEY,
+ 'value' => AKSRT_Domain_Model::PAY_TYPE_NO,
+ 'compare' => '!=',
+ ],
+ [
+ 'key' => AKSRT_Domain_Model::PAY_TYPE_KEY,
+ 'compare' => 'NOT EXISTS',
+ ],
+ ];
+ }
+
+ $q_args = [
+ 'post_type' => 'post',
+ 'post_status' => ['publish', 'draft', 'pending', 'private', 'future'],
+ 'posts_per_page' => $per_page,
+ 'paged' => $page,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ 'meta_query' => $meta_query,
+ 'fields' => 'all',
+ 'no_found_rows' => false,
+ 'update_post_meta_cache' => true,
+ 'update_post_term_cache' => false,
+ ];
+
+ if ($search !== '') {
+ if (ctype_digit($search)) {
+ $q_args['post__in'] = [(int) $search];
+ } else {
+ $q_args['s'] = $search;
+ }
+ }
+
+ $q = new WP_Query($q_args);
+ $posts = [];
+ foreach ($q->posts as $post) {
+ if (!($post instanceof WP_Post)) {
+ continue;
+ }
+ // Drop posts whose pay bag is effectively free/no.
+ if (function_exists('aksrt_is_paid_post') && !aksrt_is_paid_post($post)) {
+ continue;
+ }
+ if ($pay_type !== '' && function_exists('aksrt_get_pay_meta')) {
+ $meta = aksrt_get_pay_meta($post);
+ $pt = AKSRT_Domain_Model::normalize_pay_type((string) ($meta['pay_type'] ?? ''));
+ if ($pt !== $pay_type) {
+ continue;
+ }
+ }
+ $posts[] = $post;
+ }
+
+ // When post-filtering drops rows, fall back to order-driven list if empty page
+ // so operators still see historical sold posts without pay meta.
+ if (empty($posts) && $search === '' && $pay_type === '' && $page === 1) {
+ $order_posts = $this->posts_from_order_summary($per_page, 0);
+ if (!empty($order_posts['posts'])) {
+ return $order_posts;
+ }
+ }
+
+ return [
+ 'posts' => $posts,
+ 'total' => (int) $q->found_posts,
+ ];
+ }
+
+ /**
+ * Fallback: posts that already have paid resource orders.
+ *
+ * @return array{posts: WP_Post[], total: int}
+ */
+ private function posts_from_order_summary(int $limit, int $offset): array {
+ if (!class_exists('AKSRT_Order_Repository')) {
+ return ['posts' => [], 'total' => 0];
+ }
+
+ $summary = AKSRT_Order_Repository::get_instance()->get_content_sales_summary([
+ 'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
+ 'limit' => $limit,
+ 'offset' => $offset,
+ 'orderby' => 'sales_count',
+ 'order' => 'DESC',
+ ]);
+
+ $posts = [];
+ foreach ($summary['items'] as $item) {
+ $pid = (int) ($item['product_id'] ?? 0);
+ $post = $pid > 0 ? get_post($pid) : null;
+ if ($post instanceof WP_Post && $post->post_type === 'post') {
+ $posts[] = $post;
+ }
+ }
+
+ return [
+ 'posts' => $posts,
+ 'total' => (int) ($summary['total'] ?? count($posts)),
+ ];
+ }
+
+ /**
+ * @param array $type_labels
+ */
+ private function render_filters(string $pay_type, string $search, array $type_labels): void {
+ echo '';
+
+ echo '';
+ $base = $this->page_url(self::PAGE_SLUG);
+ $all_class = $pay_type === '' ? 'current' : '';
+ echo '- ' . esc_html__('全部', 'aksrt') . '
';
+ foreach ($type_labels as $key => $label) {
+ $url = $this->page_url(self::PAGE_SLUG, array_filter([
+ 'pay_type' => $key,
+ 's' => $search !== '' ? $search : null,
+ ]));
+ $cls = $pay_type === $key ? 'current' : '';
+ echo ' | - ' . esc_html($label) . '
';
+ }
+ echo '
';
+ echo '' . esc_html__('本页统计内容付费文章(product_type=resource),不含商城实体商品。订单销量来自已支付/已发货/已完成订单;Meta 销量为缓存计数。', 'aksrt') . '
';
+ }
+
+ private function ensure_cap(): void {
+ if (!current_user_can(self::CAPABILITY)) {
+ wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
+ }
+ }
+
+ private function render_header(string $title): void {
+ echo '';
+ echo '
' . esc_html($title) . '
';
+ }
+
+ private function render_footer(): void {
+ echo '';
+ }
+
+ private function page_url(string $slug, array $args = []): string {
+ return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
+ }
+
+ private function render_pagination(int $pages, int $page_num): void {
+ if ($pages <= 1) {
+ return;
+ }
+ echo '';
+ echo paginate_links([
+ 'base' => add_query_arg('paged', '%#%'),
+ 'format' => '',
+ 'prev_text' => '«',
+ 'next_text' => '»',
+ 'total' => $pages,
+ 'current' => $page_num,
+ ]);
+ echo '
';
+ }
+}
diff --git a/inc/classes/class-aksrt-order-repository.php b/inc/classes/class-aksrt-order-repository.php
index f6ee9f99..40f4cbb5 100644
--- a/inc/classes/class-aksrt-order-repository.php
+++ b/inc/classes/class-aksrt-order-repository.php
@@ -408,21 +408,7 @@ class AKSRT_Order_Repository {
public function get_orders_admin(array $args = [], string $search = ''): array {
global $wpdb;
- $where = [];
- $params = [];
-
- $status = sanitize_key((string) ($args['status'] ?? ''));
- if ($status !== '' && $status !== 'all') {
- $where[] = 'status = %s';
- $params[] = $status;
- }
-
- if ($search !== '') {
- $where[] = '(order_number LIKE %s OR product_type LIKE %s)';
- $like = '%' . $wpdb->esc_like($search) . '%';
- $params[] = $like;
- $params[] = $like;
- }
+ [$where, $params] = $this->build_admin_order_filters($args, $search);
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
@@ -442,21 +428,7 @@ class AKSRT_Order_Repository {
public function get_orders_count(array $args = [], string $search = ''): int {
global $wpdb;
- $where = [];
- $params = [];
-
- $status = sanitize_key((string) ($args['status'] ?? ''));
- if ($status !== '' && $status !== 'all') {
- $where[] = 'status = %s';
- $params[] = $status;
- }
-
- if ($search !== '') {
- $where[] = '(order_number LIKE %s OR product_type LIKE %s)';
- $like = '%' . $wpdb->esc_like($search) . '%';
- $params[] = $like;
- $params[] = $like;
- }
+ [$where, $params] = $this->build_admin_order_filters($args, $search);
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
@@ -467,6 +439,192 @@ class AKSRT_Order_Repository {
return (int) $wpdb->get_var($sql);
}
+ /**
+ * Aggregate paid content (resource) sales by product_id (post ID).
+ *
+ * @param array{
+ * product_type?: string,
+ * product_ids?: int[],
+ * status_in?: string[],
+ * limit?: int,
+ * offset?: int,
+ * orderby?: string,
+ * order?: string
+ * } $args
+ * @return array{items: list, total: int}
+ */
+ public function get_content_sales_summary(array $args = []): array {
+ global $wpdb;
+
+ $product_type = sanitize_key((string) ($args['product_type'] ?? AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE));
+ if ($product_type === '') {
+ $product_type = AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE;
+ }
+
+ $status_in = $args['status_in'] ?? AKSRT_Domain_Model::paid_order_statuses();
+ if (!is_array($status_in) || empty($status_in)) {
+ $status_in = AKSRT_Domain_Model::paid_order_statuses();
+ }
+ $status_in = array_values(array_filter(array_map('sanitize_key', $status_in)));
+ if (empty($status_in)) {
+ return ['items' => [], 'total' => 0];
+ }
+
+ $where = ['product_type = %s', 'product_id > 0'];
+ $params = [$product_type];
+
+ $status_placeholders = implode(', ', array_fill(0, count($status_in), '%s'));
+ $where[] = "status IN ({$status_placeholders})";
+ foreach ($status_in as $st) {
+ $params[] = $st;
+ }
+
+ $product_ids = $args['product_ids'] ?? [];
+ if (is_array($product_ids) && !empty($product_ids)) {
+ $ids = array_values(array_unique(array_filter(array_map('intval', $product_ids), static function ($id) {
+ return $id > 0;
+ })));
+ if (!empty($ids)) {
+ $id_ph = implode(', ', array_fill(0, count($ids), '%d'));
+ $where[] = "product_id IN ({$id_ph})";
+ foreach ($ids as $id) {
+ $params[] = $id;
+ }
+ }
+ }
+
+ $where_sql = 'WHERE ' . implode(' AND ', $where);
+ $table = $this->table();
+
+ $total_sql = "SELECT COUNT(DISTINCT product_id) FROM {$table} {$where_sql}";
+ $total = (int) $wpdb->get_var($wpdb->prepare($total_sql, $params));
+
+ $orderby_map = [
+ 'sales_count' => 'sales_count',
+ 'revenue' => 'revenue',
+ 'avg_amount' => 'avg_amount',
+ 'product_id' => 'product_id',
+ ];
+ $orderby_key = sanitize_key((string) ($args['orderby'] ?? 'sales_count'));
+ $orderby_col = $orderby_map[$orderby_key] ?? 'sales_count';
+ $order_dir = strtoupper((string) ($args['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
+
+ $limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
+ $offset = max(0, (int) ($args['offset'] ?? 0));
+
+ $list_sql = "SELECT product_id,
+ product_type,
+ COUNT(*) AS sales_count,
+ AVG(amount) AS avg_amount,
+ SUM(amount) AS revenue
+ FROM {$table}
+ {$where_sql}
+ GROUP BY product_id, product_type
+ ORDER BY {$orderby_col} {$order_dir}
+ LIMIT %d OFFSET %d";
+ $list_params = $params;
+ $list_params[] = $limit;
+ $list_params[] = $offset;
+
+ $rows = $wpdb->get_results($wpdb->prepare($list_sql, $list_params), ARRAY_A);
+ $items = [];
+ foreach ((array) $rows as $row) {
+ $items[] = [
+ 'product_id' => (int) ($row['product_id'] ?? 0),
+ 'product_type' => (string) ($row['product_type'] ?? $product_type),
+ 'sales_count' => (int) ($row['sales_count'] ?? 0),
+ 'avg_amount' => round((float) ($row['avg_amount'] ?? 0), 2),
+ 'revenue' => round((float) ($row['revenue'] ?? 0), 2),
+ ];
+ }
+
+ return [
+ 'items' => $items,
+ 'total' => $total,
+ ];
+ }
+
+ /**
+ * Paid order totals for a set of product IDs (resource content).
+ *
+ * @param int[] $product_ids
+ * @return array
+ */
+ public function get_content_sales_map(array $product_ids, string $product_type = ''): array {
+ $ids = array_values(array_unique(array_filter(array_map('intval', $product_ids), static function ($id) {
+ return $id > 0;
+ })));
+ if (empty($ids)) {
+ return [];
+ }
+
+ if ($product_type === '') {
+ $product_type = AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE;
+ }
+
+ $summary = $this->get_content_sales_summary([
+ 'product_type' => $product_type,
+ 'product_ids' => $ids,
+ 'limit' => max(count($ids), 1),
+ 'offset' => 0,
+ 'orderby' => 'product_id',
+ 'order' => 'ASC',
+ ]);
+
+ $map = [];
+ foreach ($summary['items'] as $item) {
+ $pid = (int) ($item['product_id'] ?? 0);
+ if ($pid <= 0) {
+ continue;
+ }
+ $map[$pid] = [
+ 'sales_count' => (int) ($item['sales_count'] ?? 0),
+ 'avg_amount' => (float) ($item['avg_amount'] ?? 0),
+ 'revenue' => (float) ($item['revenue'] ?? 0),
+ ];
+ }
+ return $map;
+ }
+
+ /**
+ * Shared WHERE builder for admin order list/count.
+ *
+ * @return array{0: string[], 1: array}
+ */
+ private function build_admin_order_filters(array $args, string $search = ''): array {
+ global $wpdb;
+
+ $where = [];
+ $params = [];
+
+ $status = sanitize_key((string) ($args['status'] ?? ''));
+ if ($status !== '' && $status !== 'all') {
+ $where[] = 'status = %s';
+ $params[] = $status;
+ }
+
+ $product_type = sanitize_key((string) ($args['product_type'] ?? ''));
+ if ($product_type !== '') {
+ $where[] = 'product_type = %s';
+ $params[] = $product_type;
+ }
+
+ $product_id = (int) ($args['product_id'] ?? 0);
+ if ($product_id > 0) {
+ $where[] = 'product_id = %d';
+ $params[] = $product_id;
+ }
+
+ if ($search !== '') {
+ $where[] = '(order_number LIKE %s OR product_type LIKE %s)';
+ $like = '%' . $wpdb->esc_like($search) . '%';
+ $params[] = $like;
+ $params[] = $like;
+ }
+
+ return [$where, $params];
+ }
+
/**
* 获取订单统计
*/