refactor(autoload): migrate theme classes to Composer PSR-4
Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
This commit is contained in:
1 parent
8f349b40fb
commit
aaf458af1c
404 files changed
+13602
-7705
No files matched your search
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Shop;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class ProductReview {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const COMMENT_TYPE = 'aksrt_product_review';
|
||||
|
||||
const RATING_LEVELS = [
|
||||
5 => '超赞',
|
||||
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\Payment\OrderRepository')) {
|
||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
||||
$status = (string) ($order['status'] ?? '');
|
||||
if ($status === \AKSRT\Payment\Order::STATUS_PENDING_REVIEW) {
|
||||
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
|
||||
$order_number,
|
||||
\AKSRT\Payment\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\Payment\OrderRepository')) return false;
|
||||
|
||||
$order = \AKSRT\Payment\OrderRepository::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\Payment\Order::STATUS_COMPLETED,
|
||||
\AKSRT\Payment\Order::STATUS_PENDING_REVIEW,
|
||||
\AKSRT\Payment\Order::STATUS_DELIVERED,
|
||||
], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (class_exists('\AKSRT\Payment\AfterSale')) {
|
||||
$tickets = \AKSRT\Payment\AfterSale::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\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shop = \AKSRT\Settings\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\Payment\OrderRepository::get_instance();
|
||||
$orders = $repo->get_orders_admin([
|
||||
'status' => \AKSRT\Payment\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('Shop')
|
||||
? 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\Payment\Order::STATUS_COMPLETED, [
|
||||
'note' => sprintf(__('超过 %d 天未评价,系统自动好评。', 'aksrt'), $max_day),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function get_max_images(): int {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) return 9;
|
||||
$shop = \AKSRT\Settings\Settings::get_shop_settings();
|
||||
return max(1, min(20, (int) ($shop['timing']['comment_img_num'] ?? 9)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user