perf(modal): unify cancellable frontend modal flows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 02:04:55 +08:00
co-authored by Claude Fable 5
parent 25ce1d8f43
commit 5375ad6429
36 changed files with 2623 additions and 1806 deletions
+170 -39
View File
@@ -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 {