'超赞', 4 => '很棒', 3 => '不错', 2 => '一般', 1 => '较差', 0 => '很差', ]; public static function get_instance(): self { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } public function init(): void { add_action('aksrt_daily_cron', [$this, 'run_auto_positive_cron']); } public function submit_review(int $user_id, string $order_number, int $product_id, array $data): array { if ($user_id <= 0 || $order_number === '' || $product_id <= 0) { return ['success' => false, 'message' => __('参数错误', 'aksrt')]; } if (!$this->can_review($user_id, $order_number, $product_id)) { return ['success' => false, 'message' => __('当前订单不可评价', 'aksrt')]; } if ($this->has_reviewed($user_id, $order_number, $product_id)) { return ['success' => false, 'message' => __('已评价过该商品', 'aksrt')]; } $rating_product = max(0, min(5, (int) ($data['rating_product'] ?? 5))); $rating_service = max(0, min(5, (int) ($data['rating_service'] ?? 5))); $rating_shipping = max(0, min(5, (int) ($data['rating_shipping'] ?? 5))); $average = round(($rating_product + $rating_service + $rating_shipping) / 3, 1); $content = sanitize_textarea_field((string) ($data['content'] ?? '')); $images = []; $max_images = $this->get_max_images(); foreach ((array) ($data['images'] ?? []) as $img) { if (count($images) >= $max_images) break; $url = esc_url_raw((string) $img); if ($url !== '') { $images[] = $url; } } $comment_id = wp_insert_comment([ 'comment_post_ID' => $product_id, 'user_id' => $user_id, 'comment_content' => $content ?: __('好评!', 'aksrt'), 'comment_type' => self::COMMENT_TYPE, 'comment_approved' => 1, ]); if (!$comment_id) { return ['success' => false, 'message' => __('评价提交失败', 'aksrt')]; } update_comment_meta($comment_id, '_aksrt_order_number', $order_number); update_comment_meta($comment_id, '_aksrt_rating_product', $rating_product); update_comment_meta($comment_id, '_aksrt_rating_service', $rating_service); update_comment_meta($comment_id, '_aksrt_rating_shipping', $rating_shipping); update_comment_meta($comment_id, '_aksrt_rating_average', $average); if (!empty($images)) { update_comment_meta($comment_id, '_aksrt_review_images', $images); } $this->refresh_product_scores($product_id); if (class_exists('AKSRT_Order_Repository')) { $order = AKSRT_Order_Repository::get_instance()->get($order_number); $status = (string) ($order['status'] ?? ''); if ($status === AKSRT_Order::STATUS_PENDING_REVIEW) { AKSRT_Order_Repository::get_instance()->update_status( $order_number, AKSRT_Order::STATUS_COMPLETED, ['note' => __('用户已评价', 'aksrt')] ); } } return ['success' => true, 'comment_id' => $comment_id]; } public function can_review(int $user_id, string $order_number, int $product_id): bool { if (!class_exists('AKSRT_Order_Repository')) return false; $order = AKSRT_Order_Repository::get_instance()->get($order_number); if (!$order || (int) ($order['user_id'] ?? 0) !== $user_id) return false; $status = (string) ($order['status'] ?? ''); if (!in_array($status, [ AKSRT_Order::STATUS_COMPLETED, AKSRT_Order::STATUS_PENDING_REVIEW, AKSRT_Order::STATUS_DELIVERED, ], true)) { return false; } if (class_exists('AKSRT_After_Sale')) { $tickets = AKSRT_After_Sale::get_instance()->get_list([ 'order_number' => $order_number, 'limit' => 1, ]); foreach (($tickets['items'] ?? []) as $ticket) { if (in_array(($ticket['status'] ?? ''), ['pending', 'processing'], true)) { return false; } } } return true; } public function has_reviewed(int $user_id, string $order_number, int $product_id): bool { $comments = get_comments([ 'post_id' => $product_id, 'user_id' => $user_id, 'type' => self::COMMENT_TYPE, 'number' => 1, 'meta_query' => [ ['key' => '_aksrt_order_number', 'value' => $order_number], ], ]); return !empty($comments); } public function get_reviews(int $product_id, array $args = []): array { $page = max(1, (int) ($args['page'] ?? 1)); $per_page = max(1, min(50, (int) ($args['per_page'] ?? 10))); $filter = sanitize_key((string) ($args['filter'] ?? '')); $query_args = [ 'post_id' => $product_id, 'type' => self::COMMENT_TYPE, 'status' => 'approve', 'number' => $per_page, 'offset' => ($page - 1) * $per_page, 'orderby' => 'comment_date', 'order' => 'DESC', ]; if ($filter === 'good') { $query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 4, 'compare' => '>=', 'type' => 'NUMERIC']]; } elseif ($filter === 'bad') { $query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 3, 'compare' => '<', 'type' => 'NUMERIC']]; } elseif ($filter === 'has_image') { $query_args['meta_query'] = [['key' => '_aksrt_review_images', 'compare' => 'EXISTS']]; } $comments = get_comments($query_args); $total = (int) get_comments(array_merge($query_args, ['count' => true, 'number' => 0, 'offset' => 0])); $items = []; foreach ($comments as $comment) { $cid = (int) $comment->comment_ID; $items[] = [ 'id' => $cid, 'user_id' => (int) $comment->user_id, 'user_name' => (string) $comment->comment_author, 'user_avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url((int) $comment->user_id) : get_avatar_url((int) $comment->user_id), 'content' => (string) $comment->comment_content, 'date' => (string) $comment->comment_date, 'rating_product' => (int) get_comment_meta($cid, '_aksrt_rating_product', true), 'rating_service' => (int) get_comment_meta($cid, '_aksrt_rating_service', true), 'rating_shipping' => (int) get_comment_meta($cid, '_aksrt_rating_shipping', true), 'rating_average' => (float) get_comment_meta($cid, '_aksrt_rating_average', true), 'images' => (array) (get_comment_meta($cid, '_aksrt_review_images', true) ?: []), 'order_number' => (string) get_comment_meta($cid, '_aksrt_order_number', true), ]; } return ['items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $per_page]; } public function get_product_scores(int $product_id): array { $cached = get_post_meta($product_id, '_aksrt_review_scores', true); if (is_array($cached)) { return $cached; } return $this->refresh_product_scores($product_id); } public function refresh_product_scores(int $product_id): array { global $wpdb; $results = $wpdb->get_row($wpdb->prepare( "SELECT COUNT(*) as total, AVG(CAST(m.meta_value AS DECIMAL(3,1))) as average FROM {$wpdb->comments} c JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average' WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'", $product_id, self::COMMENT_TYPE ), ARRAY_A); $total = (int) ($results['total'] ?? 0); $average = $total > 0 ? round((float) ($results['average'] ?? 0), 1) : 0; $good_count = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} c JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average' WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1' AND CAST(m.meta_value AS DECIMAL(3,1)) >= 4", $product_id, self::COMMENT_TYPE )); $bad_count = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} c JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average' WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1' AND CAST(m.meta_value AS DECIMAL(3,1)) < 3", $product_id, self::COMMENT_TYPE )); $has_image = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} c JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_review_images' WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'", $product_id, self::COMMENT_TYPE )); $scores = [ 'total' => $total, 'average' => $average, 'good' => $good_count, 'bad' => $bad_count, 'has_image' => $has_image, 'level' => self::RATING_LEVELS[min(5, max(0, (int) round($average)))] ?? '', ]; update_post_meta($product_id, '_aksrt_review_scores', $scores); return $scores; } public function run_auto_positive_cron(): void { if (!class_exists('AKSRT_Order_Repository') || !class_exists('AKSRT_Settings')) { return; } $shop = AKSRT_Settings::get_shop_settings(); $max_day = max(1, (int) ($shop['timing']['comment_max_day'] ?? 15)); $cutoff = date('Y-m-d H:i:s', strtotime("-{$max_day} days")); $repo = AKSRT_Order_Repository::get_instance(); $orders = $repo->get_orders_admin([ 'status' => AKSRT_Order::STATUS_PENDING_REVIEW, 'limit' => 200, ]); foreach ($orders as $order) { $completed_at = (string) ($order['completed_at'] ?? $order['delivered_at'] ?? ''); if ($completed_at === '' || $completed_at > $cutoff) { continue; } $user_id = (int) ($order['user_id'] ?? 0); $order_number = (string) ($order['order_number'] ?? ''); if ($user_id <= 0 || $order_number === '') continue; $items = class_exists('AKSRT_Shop') ? AKSRT_Shop::get_instance()->get_order_items($order_number) : []; $product_ids = array_column($items, 'product_id'); if (empty($product_ids) && !empty($order['product_id'])) { $product_ids = [(int) $order['product_id']]; } foreach ($product_ids as $pid) { if (!$this->has_reviewed($user_id, $order_number, (int) $pid)) { $this->submit_review($user_id, $order_number, (int) $pid, [ 'rating_product' => 5, 'rating_service' => 5, 'rating_shipping' => 5, 'content' => __('系统默认好评', 'aksrt'), ]); } } $repo->update_status($order_number, AKSRT_Order::STATUS_COMPLETED, [ 'note' => sprintf(__('超过 %d 天未评价,系统自动好评。', 'aksrt'), $max_day), ]); } } private function get_max_images(): int { if (!class_exists('AKSRT_Settings')) return 9; $shop = AKSRT_Settings::get_shop_settings(); return max(1, min(20, (int) ($shop['timing']['comment_img_num'] ?? 9))); } }