perf(modal): unify cancellable frontend modal flows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
25ce1d8f43
commit
5375ad6429
36 files changed
+2623
-1806
No files matched your search
+30
-9
@@ -243,6 +243,18 @@ class Gateway {
|
||||
return sanitize_text_field((string) ($order['order_number'] ?? $provider_ref));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a pending order can be reconciled against an external provider.
|
||||
*
|
||||
* This is the single gate used by the order view and the actual reconcile
|
||||
* operation, so ordinary detail reads never trigger provider I/O.
|
||||
*
|
||||
* @param array<string,mixed>|null $order
|
||||
*/
|
||||
public function needs_reconciliation(?array $order): bool {
|
||||
return null !== $this->reconciliation_gateway($order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile a pending external order against the configured provider.
|
||||
*
|
||||
@@ -254,18 +266,11 @@ class Gateway {
|
||||
}
|
||||
$repo = OrderRepository::get_instance();
|
||||
$order = is_array($order) ? $order : $repo->get($order_number);
|
||||
if (!$order || !in_array((string) ($order['status'] ?? ''), [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) {
|
||||
$gateway = $this->reconciliation_gateway($order);
|
||||
if (!$gateway) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
$gateway_id = sanitize_key((string) ($order['payment_method'] ?? ''));
|
||||
if ($gateway_id === '' || in_array($gateway_id, ['balance', 'points', 'manual_review'], true)) {
|
||||
return $order;
|
||||
}
|
||||
$gateway = $this->get_gateway($gateway_id);
|
||||
if (!$gateway || !$gateway->is_configured()) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
$throttle_key = 'aksrt_reconcile_' . hash('sha256', $order_number);
|
||||
if (get_transient($throttle_key)) {
|
||||
@@ -297,6 +302,22 @@ class Gateway {
|
||||
return $repo->get($order_number) ?: $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed>|null $order
|
||||
*/
|
||||
private function reconciliation_gateway(?array $order): ?GatewayInterface {
|
||||
if (!is_array($order) || !in_array((string) ($order['status'] ?? ''), [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$gateway_id = sanitize_key((string) ($order['payment_method'] ?? ''));
|
||||
if ($gateway_id === '' || in_array($gateway_id, ['balance', 'points', 'manual_review'], true)) {
|
||||
return null;
|
||||
}
|
||||
$gateway = $this->get_gateway($gateway_id);
|
||||
return $gateway && $gateway->is_configured() ? $gateway : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 幂等、带金额校验的订单完成。仅由验签通过的回调调用。
|
||||
*
|
||||
|
||||
@@ -61,6 +61,12 @@ class OrderCenterRest {
|
||||
'permission_callback' => [$this, 'require_logged_in'],
|
||||
'args' => $route_args,
|
||||
]);
|
||||
register_rest_route($namespace, '/orders/' . $order_number . '/refresh', [
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'refresh'],
|
||||
'permission_callback' => [$this, 'require_logged_in'],
|
||||
'args' => $route_args,
|
||||
]);
|
||||
register_rest_route($namespace, '/orders/' . $order_number, [
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_order'],
|
||||
@@ -108,6 +114,14 @@ class OrderCenterRest {
|
||||
: $this->error((string) ($result['message'] ?? __('关闭订单失败', 'aksrt')), 400);
|
||||
}
|
||||
|
||||
public function refresh(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$order = UserOrders::get_instance()->refresh_order(
|
||||
get_current_user_id(),
|
||||
sanitize_text_field((string) $request->get_param('order_number'))
|
||||
);
|
||||
return $order ? $this->success(['data' => $order]) : $this->error(__('订单不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
public function get_order(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$order = UserOrders::get_instance()->get_order_detail_view(
|
||||
get_current_user_id(),
|
||||
|
||||
@@ -100,46 +100,14 @@ trait OrderRepositoryCore {
|
||||
public function get_user_orders(int $user_id, array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($user_id > 0) {
|
||||
$where[] = 'user_id = %d';
|
||||
$params[] = $user_id;
|
||||
}
|
||||
|
||||
$statuses = array_values(array_filter(array_map('sanitize_key', (array) ($args['status_in'] ?? []))));
|
||||
if ($statuses !== []) {
|
||||
$where[] = 'status IN (' . implode(', ', array_fill(0, count($statuses), '%s')) . ')';
|
||||
array_push($params, ...$statuses);
|
||||
} else {
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
}
|
||||
|
||||
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
||||
if ($product_type !== '') {
|
||||
$where[] = 'product_type = %s';
|
||||
$params[] = $product_type;
|
||||
}
|
||||
|
||||
$order_number_prefix = strtoupper(preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($args['order_number_prefix'] ?? '')) ?? '');
|
||||
if ($order_number_prefix !== '') {
|
||||
$where[] = 'order_number LIKE %s';
|
||||
$params[] = $wpdb->esc_like($order_number_prefix) . '%';
|
||||
}
|
||||
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$parts = $this->get_user_order_query_parts($user_id, $args);
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$params = $parts['params'];
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
$sql = "SELECT * FROM {$this->table()} {$parts['where']} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
|
||||
return array_map([$this, 'normalize_row'], (array) $rows);
|
||||
@@ -175,37 +143,60 @@ trait OrderRepositoryCore {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$where = ['user_id = %d'];
|
||||
$params = [$user_id];
|
||||
$parts = $this->get_user_order_query_parts($user_id, $args);
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} {$parts['where']}",
|
||||
$parts['params']
|
||||
));
|
||||
}
|
||||
|
||||
$statuses = array_values(array_filter(array_map('sanitize_key', (array) ($args['status_in'] ?? []))));
|
||||
if ($statuses !== []) {
|
||||
$where[] = 'status IN (' . implode(', ', array_fill(0, count($statuses), '%s')) . ')';
|
||||
array_push($params, ...$statuses);
|
||||
} else {
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
/**
|
||||
* Get the filtered list total and the user's order-center counters in one query.
|
||||
*
|
||||
* The filtered total mirrors the list query while the counters intentionally keep
|
||||
* their existing user-wide semantics, so changing a filter does not change the
|
||||
* summary cards. The subquery avoids four independent COUNT statements.
|
||||
*
|
||||
* @return array{total:int,stats:array{total:int,pending:int,completed:int}}
|
||||
*/
|
||||
public function get_user_order_center_summary(int $user_id, array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
if ($user_id <= 0) {
|
||||
return [
|
||||
'total' => 0,
|
||||
'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0],
|
||||
];
|
||||
}
|
||||
|
||||
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
||||
if ($product_type !== '') {
|
||||
$where[] = 'product_type = %s';
|
||||
$params[] = $product_type;
|
||||
}
|
||||
$parts = $this->get_user_order_query_parts($user_id, $args);
|
||||
$pending_statuses = [Order::STATUS_CREATED, Order::STATUS_PENDING];
|
||||
$paid_status = Order::STATUS_PAID;
|
||||
$known_statuses = OrderCenterQuery::from_array([])->database_statuses();
|
||||
$known_placeholders = implode(', ', array_fill(0, count($known_statuses), '%s'));
|
||||
$sql = "SELECT
|
||||
(SELECT COUNT(*) FROM {$this->table()} {$parts['where']}) AS filtered_total,
|
||||
COUNT(*) AS known_total,
|
||||
SUM(CASE WHEN status IN (%s, %s) THEN 1 ELSE 0 END) AS pending,
|
||||
SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) AS completed
|
||||
FROM {$this->table()}
|
||||
WHERE user_id = %d AND status IN ({$known_placeholders})";
|
||||
$params = array_merge(
|
||||
$parts['params'],
|
||||
$pending_statuses,
|
||||
[$paid_status, $user_id],
|
||||
$known_statuses
|
||||
);
|
||||
$row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
|
||||
$order_number_prefix = strtoupper(preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($args['order_number_prefix'] ?? '')) ?? '');
|
||||
if ($order_number_prefix !== '') {
|
||||
$where[] = 'order_number LIKE %s';
|
||||
$params[] = $wpdb->esc_like($order_number_prefix) . '%';
|
||||
}
|
||||
|
||||
$where_sql = 'WHERE ' . implode(' AND ', $where);
|
||||
$sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
|
||||
|
||||
return (int) $wpdb->get_var($wpdb->prepare($sql, $params));
|
||||
return [
|
||||
'total' => (int) ($row['filtered_total'] ?? 0),
|
||||
'stats' => [
|
||||
'total' => (int) ($row['known_total'] ?? 0),
|
||||
'pending' => (int) ($row['pending'] ?? 0),
|
||||
'completed' => (int) ($row['completed'] ?? 0),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -515,6 +506,51 @@ trait OrderRepositoryCore {
|
||||
* Internal helpers
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Build the shared list/count predicate without changing caller semantics.
|
||||
*
|
||||
* @return array{where:string,params:array<int,mixed>}
|
||||
*/
|
||||
private function get_user_order_query_parts(int $user_id, array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
if ($user_id > 0) {
|
||||
$where[] = 'user_id = %d';
|
||||
$params[] = $user_id;
|
||||
}
|
||||
|
||||
$statuses = array_values(array_filter(array_map('sanitize_key', (array) ($args['status_in'] ?? []))));
|
||||
if ($statuses !== []) {
|
||||
$where[] = 'status IN (' . implode(', ', array_fill(0, count($statuses), '%s')) . ')';
|
||||
array_push($params, ...$statuses);
|
||||
} else {
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
}
|
||||
|
||||
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
||||
if ($product_type !== '') {
|
||||
$where[] = 'product_type = %s';
|
||||
$params[] = $product_type;
|
||||
}
|
||||
|
||||
$order_number_prefix = strtoupper(preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($args['order_number_prefix'] ?? '')) ?? '');
|
||||
if ($order_number_prefix !== '') {
|
||||
$where[] = 'order_number LIKE %s';
|
||||
$params[] = $wpdb->esc_like($order_number_prefix) . '%';
|
||||
}
|
||||
|
||||
return [
|
||||
'where' => $where ? 'WHERE ' . implode(' AND ', $where) : '',
|
||||
'params' => $params,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalize_row(array $row): array {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['user_id'] = (int) $row['user_id'];
|
||||
|
||||
@@ -23,7 +23,7 @@ class PaymentOrchestrator {
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $order Order row plus subject/amount overrides.
|
||||
* @param array<string,mixed> $opts paid_note: string
|
||||
* @param array<string,mixed> $opts paid_note: string, order_context: array
|
||||
*
|
||||
* @return array{
|
||||
* success:bool,
|
||||
@@ -58,7 +58,16 @@ class PaymentOrchestrator {
|
||||
// A pending order remains bound to the gateway selected at creation.
|
||||
// Switching it would make a late callback from the previous gateway look
|
||||
// invalid after that provider may already have collected the money.
|
||||
// The caller may pass request-local create context so metadata such as
|
||||
// subject is not reconstructed or read a second time. The persisted row
|
||||
// remains authoritative for status, ownership, and payment method.
|
||||
$stored = $repo->get($order_number);
|
||||
if (is_array($opts['order_context'] ?? null)) {
|
||||
$context = $opts['order_context'];
|
||||
if ((string) ($context['order_number'] ?? '') === $order_number && is_array($stored)) {
|
||||
$stored = array_merge($context, $stored);
|
||||
}
|
||||
}
|
||||
$stored_status = sanitize_key((string) ($stored['status'] ?? ''));
|
||||
if (!in_array($stored_status, [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) {
|
||||
return self::fail('aksrt_order_not_payable', __('当前订单状态不可支付。', 'aksrt'));
|
||||
|
||||
+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