perf(modal): unify cancellable frontend modal flows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+170
-39
@@ -15,6 +15,18 @@ class UserOrders {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/** @var array<string,string> */
|
||||
private array $post_title_cache = [];
|
||||
|
||||
/** @var array<string,string> */
|
||||
private array $post_image_cache = [];
|
||||
|
||||
/** @var array<string,string> */
|
||||
private array $post_permalink_cache = [];
|
||||
|
||||
/** @var array<string,string> */
|
||||
private array $type_labels_cache = [];
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
@@ -36,7 +48,8 @@ class UserOrders {
|
||||
$repo_args = $query->repository_args();
|
||||
$repo = OrderRepository::get_instance();
|
||||
$orders = $repo->get_user_orders($user_id, $repo_args);
|
||||
$total = $repo->count_user_orders($user_id, $repo_args);
|
||||
$summary = $repo->get_user_order_center_summary($user_id, $repo_args);
|
||||
$total = (int) ($summary['total'] ?? 0);
|
||||
$total_pages = max(1, (int) ceil($total / $query->per_page()));
|
||||
$page = min($query->page(), $total_pages);
|
||||
|
||||
@@ -46,18 +59,19 @@ class UserOrders {
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => array_map([$this, 'decorate_order_view'], $orders),
|
||||
'items' => $this->decorate_order_views($orders),
|
||||
'total' => $total,
|
||||
'total_pages' => $total_pages,
|
||||
'page' => $page,
|
||||
'query' => array_merge($query->url_values(), ['order_page' => $page]),
|
||||
'stats' => $this->get_user_order_stats($user_id),
|
||||
'stats' => is_array($summary['stats'] ?? null) ? $summary['stats'] : ['total' => 0, 'pending' => 0, 'completed' => 0],
|
||||
'invalidation_tags' => ['orders'],
|
||||
'product_types' => $this->product_type_filter_options(),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
public function get_order_detail_view(int $user_id, string $order_number): ?array {
|
||||
public function get_order_detail_view(int $user_id, string $order_number, bool $reconcile = false): ?array {
|
||||
if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) {
|
||||
return null;
|
||||
}
|
||||
@@ -67,11 +81,29 @@ class UserOrders {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reconciliation = [
|
||||
'state' => 'not_needed',
|
||||
'required' => false,
|
||||
];
|
||||
if (class_exists(Gateway::class)) {
|
||||
$order = Gateway::get_instance()->reconcile_order($order_number, $order) ?: $order;
|
||||
$gateway = Gateway::get_instance();
|
||||
$required = $gateway->needs_reconciliation($order);
|
||||
if ($required && $reconcile) {
|
||||
$order = $gateway->reconcile_order($order_number, $order) ?: $order;
|
||||
$required = $gateway->needs_reconciliation($order);
|
||||
$reconciliation['state'] = $required ? 'refresh_available' : 'checked';
|
||||
} elseif ($required) {
|
||||
$reconciliation['state'] = 'refresh_available';
|
||||
}
|
||||
$reconciliation['required'] = $required;
|
||||
}
|
||||
|
||||
return $this->decorate_order_view($order);
|
||||
return $this->decorate_order_view($order, true, $reconciliation);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
public function refresh_order(int $user_id, string $order_number): ?array {
|
||||
return $this->get_order_detail_view($user_id, $order_number, true);
|
||||
}
|
||||
|
||||
/** @return array<string, int> */
|
||||
@@ -80,30 +112,67 @@ class UserOrders {
|
||||
return ['total' => 0, 'pending' => 0, 'completed' => 0];
|
||||
}
|
||||
|
||||
$repo = OrderRepository::get_instance();
|
||||
return [
|
||||
'total' => $repo->count_user_orders($user_id, [
|
||||
'status_in' => OrderCenterQuery::from_array([])->database_statuses(),
|
||||
]),
|
||||
'pending' => $repo->count_user_orders($user_id, [
|
||||
'status_in' => [Order::STATUS_CREATED, Order::STATUS_PENDING],
|
||||
]),
|
||||
'completed' => $repo->count_user_orders($user_id, [
|
||||
'status_in' => [Order::STATUS_PAID],
|
||||
]),
|
||||
];
|
||||
$summary = OrderRepository::get_instance()->get_user_order_center_summary($user_id);
|
||||
return is_array($summary['stats'] ?? null)
|
||||
? $summary['stats']
|
||||
: ['total' => 0, 'pending' => 0, 'completed' => 0];
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
public function product_type_filter_options(): array {
|
||||
return array_merge(['' => __('全部类型', 'aksrt')], DomainModel::order_product_type_labels());
|
||||
if ($this->type_labels_cache === []) {
|
||||
$this->type_labels_cache = DomainModel::order_product_type_labels();
|
||||
}
|
||||
return array_merge(['' => __('全部类型', 'aksrt')], $this->type_labels_cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prime post and post-meta caches for all resource rows before decoration.
|
||||
*
|
||||
* @param array<int,array<string,mixed>> $orders
|
||||
*/
|
||||
private function prime_resource_caches(array $orders): void {
|
||||
$post_ids = [];
|
||||
foreach ($orders as $order) {
|
||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||
foreach ([(int) ($order['product_id'] ?? 0), (int) ($meta['post_id'] ?? 0)] as $post_id) {
|
||||
if ($post_id > 0) {
|
||||
$post_ids[$post_id] = $post_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($post_ids === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
_prime_post_caches(array_values($post_ids), false, true);
|
||||
$thumbnail_ids = [];
|
||||
foreach ($post_ids as $post_id) {
|
||||
$thumbnail_id = (int) get_post_thumbnail_id($post_id);
|
||||
if ($thumbnail_id > 0) {
|
||||
$thumbnail_ids[$thumbnail_id] = $thumbnail_id;
|
||||
}
|
||||
}
|
||||
if ($thumbnail_ids !== []) {
|
||||
_prime_post_caches(array_values($thumbnail_ids), false, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<string,mixed>> $orders
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private function decorate_order_views(array $orders): array {
|
||||
$this->prime_resource_caches($orders);
|
||||
return array_map(fn(array $order): array => $this->decorate_order_view($order, false), $orders);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $order
|
||||
* @param array<string, mixed>|null $reconciliation
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function decorate_order_view(array $order): array {
|
||||
public function decorate_order_view(array $order, bool $detail = true, ?array $reconciliation = null): array {
|
||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||
$product_type = sanitize_key((string) ($order['product_type'] ?? ''));
|
||||
$product_id = (int) ($order['product_id'] ?? 0);
|
||||
@@ -112,8 +181,10 @@ class UserOrders {
|
||||
$is_points = $currency === 'points'
|
||||
|| sanitize_key((string) ($order['payment_method'] ?? '')) === 'points'
|
||||
|| sanitize_key((string) ($meta['pay_modo'] ?? '')) === 'points';
|
||||
$type_labels = DomainModel::order_product_type_labels();
|
||||
$type_label = $type_labels[$product_type] ?? __('订单', 'aksrt');
|
||||
if ($this->type_labels_cache === []) {
|
||||
$this->type_labels_cache = DomainModel::order_product_type_labels();
|
||||
}
|
||||
$type_label = $this->type_labels_cache[$product_type] ?? __('订单', 'aksrt');
|
||||
$title = $this->resolve_title($meta, $product_type, $product_id, $type_label);
|
||||
$image = $this->resolve_image($meta, $product_id);
|
||||
$permalink = $this->resolve_entitlement_url($product_type, $product_id, $meta);
|
||||
@@ -122,10 +193,12 @@ class UserOrders {
|
||||
$can_pay = $group === OrderCenterQuery::STATUS_PENDING && $payment_method !== 'manual_review';
|
||||
$can_cancel = $group === OrderCenterQuery::STATUS_PENDING;
|
||||
$can_open = $group === OrderCenterQuery::STATUS_COMPLETED && $permalink !== '';
|
||||
$order_number = (string) ($order['order_number'] ?? '');
|
||||
$updated_at = $this->resolve_updated_at($order);
|
||||
|
||||
return [
|
||||
$view = [
|
||||
'id' => (int) ($order['id'] ?? 0),
|
||||
'order_number' => (string) ($order['order_number'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'amount' => (float) ($order['amount'] ?? 0),
|
||||
'currency' => $currency,
|
||||
'currency_symbol' => $is_points
|
||||
@@ -142,20 +215,30 @@ class UserOrders {
|
||||
'image' => $image,
|
||||
'permalink' => $permalink,
|
||||
'created_at' => (string) ($order['created_at'] ?? ''),
|
||||
'paid_at' => (string) ($order['paid_at'] ?? ''),
|
||||
'expires_at' => (string) ($order['expires_at'] ?? ''),
|
||||
'payment_method' => [
|
||||
'value' => $payment_method,
|
||||
'label' => $this->payment_method_label($payment_method),
|
||||
],
|
||||
'actions' => [
|
||||
'allowed_actions' => [
|
||||
'can_pay' => $can_pay,
|
||||
'can_cancel' => $can_cancel,
|
||||
'can_open' => $can_open,
|
||||
],
|
||||
'time_left' => $this->get_order_countdown((string) ($order['expires_at'] ?? ''), $status),
|
||||
'entitlement_label' => $this->entitlement_label($product_type),
|
||||
'updated_at' => $updated_at,
|
||||
'status_version' => $this->resolve_status_version($order, $updated_at),
|
||||
'invalidation_tags' => array_values(array_filter([
|
||||
'orders',
|
||||
$order_number !== '' ? 'order:' . sanitize_key($order_number) : '',
|
||||
])),
|
||||
];
|
||||
if ($detail) {
|
||||
$view['paid_at'] = (string) ($order['paid_at'] ?? '');
|
||||
$view['expires_at'] = (string) ($order['expires_at'] ?? '');
|
||||
$view['reconciliation'] = $reconciliation ?: ['state' => 'not_needed', 'required' => false];
|
||||
}
|
||||
return $view;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
@@ -167,10 +250,39 @@ class UserOrders {
|
||||
'page' => 1,
|
||||
'query' => $query->url_values(),
|
||||
'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0],
|
||||
'invalidation_tags' => ['orders'],
|
||||
'product_types' => $this->product_type_filter_options(),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $order */
|
||||
private function resolve_updated_at(array $order): string {
|
||||
$latest = '';
|
||||
$latest_timestamp = 0;
|
||||
foreach (['created_at', 'paid_at', 'delivered_at', 'completed_at', 'cancelled_at', 'refunded_at'] as $field) {
|
||||
$value = trim((string) ($order[$field] ?? ''));
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
$timestamp = strtotime($value);
|
||||
if ($timestamp !== false && $timestamp >= $latest_timestamp) {
|
||||
$latest_timestamp = $timestamp;
|
||||
$latest = $value;
|
||||
}
|
||||
}
|
||||
return $latest;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $order */
|
||||
private function resolve_status_version(array $order, string $updated_at): string {
|
||||
return substr(hash('sha256', implode('|', [
|
||||
(int) ($order['id'] ?? 0),
|
||||
(string) ($order['order_number'] ?? ''),
|
||||
sanitize_key((string) ($order['status'] ?? '')),
|
||||
$updated_at,
|
||||
])), 0, 16);
|
||||
}
|
||||
|
||||
private function status_group(string $status): string {
|
||||
return match ($status) {
|
||||
Order::STATUS_CREATED, Order::STATUS_PENDING => OrderCenterQuery::STATUS_PENDING,
|
||||
@@ -205,9 +317,13 @@ class UserOrders {
|
||||
return __('购买积分', 'aksrt');
|
||||
}
|
||||
if ($product_id > 0) {
|
||||
$title = get_the_title($product_id);
|
||||
if (is_string($title) && $title !== '') {
|
||||
return $title;
|
||||
$cache_key = (string) $product_id;
|
||||
if (!array_key_exists($cache_key, $this->post_title_cache)) {
|
||||
$title = get_the_title($product_id);
|
||||
$this->post_title_cache[$cache_key] = is_string($title) ? $title : '';
|
||||
}
|
||||
if ($this->post_title_cache[$cache_key] !== '') {
|
||||
return $this->post_title_cache[$cache_key];
|
||||
}
|
||||
}
|
||||
return $fallback;
|
||||
@@ -218,22 +334,37 @@ class UserOrders {
|
||||
if (!empty($meta['thumb']) && is_string($meta['thumb'])) {
|
||||
return esc_url_raw($meta['thumb']);
|
||||
}
|
||||
if ($product_id > 0 && has_post_thumbnail($product_id)) {
|
||||
return (string) get_the_post_thumbnail_url($product_id, 'thumbnail');
|
||||
if ($product_id > 0) {
|
||||
$cache_key = (string) $product_id;
|
||||
if (!array_key_exists($cache_key, $this->post_image_cache)) {
|
||||
$image = has_post_thumbnail($product_id)
|
||||
? get_the_post_thumbnail_url($product_id, 'thumbnail')
|
||||
: '';
|
||||
$this->post_image_cache[$cache_key] = is_string($image) ? $image : '';
|
||||
}
|
||||
return $this->post_image_cache[$cache_key];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $meta */
|
||||
private function resolve_entitlement_url(string $type, int $product_id, array $meta): string {
|
||||
if ($type === DomainModel::ORDER_PRODUCT_VIP) {
|
||||
return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('vip') : '';
|
||||
}
|
||||
if (in_array($type, [DomainModel::ORDER_PRODUCT_RECHARGE, DomainModel::ORDER_PRODUCT_POINTS_BUY], true)) {
|
||||
return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('credits') : '';
|
||||
}
|
||||
$post_id = $product_id ?: (int) ($meta['post_id'] ?? 0);
|
||||
return $post_id > 0 ? (string) get_permalink($post_id) : '';
|
||||
$cache_key = $type . ':' . $post_id;
|
||||
if (array_key_exists($cache_key, $this->post_permalink_cache)) {
|
||||
return $this->post_permalink_cache[$cache_key];
|
||||
}
|
||||
|
||||
if ($type === DomainModel::ORDER_PRODUCT_VIP) {
|
||||
$url = function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('vip') : '';
|
||||
} elseif (in_array($type, [DomainModel::ORDER_PRODUCT_RECHARGE, DomainModel::ORDER_PRODUCT_POINTS_BUY], true)) {
|
||||
$url = function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('credits') : '';
|
||||
} else {
|
||||
$url = $post_id > 0 ? get_permalink($post_id) : '';
|
||||
}
|
||||
|
||||
$this->post_permalink_cache[$cache_key] = is_string($url) ? $url : '';
|
||||
return $this->post_permalink_cache[$cache_key];
|
||||
}
|
||||
|
||||
private function entitlement_label(string $type): string {
|
||||
|
||||
Reference in New Issue
Block a user