feat(orders): redesign user order center
This commit is contained in:
+85
-1675
File diff suppressed because it is too large
Load Diff
+225
-875
File diff suppressed because it is too large
Load Diff
@@ -17,13 +17,14 @@ function aksrt_get_order_status_labels(): array {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page = 20, string $status = '', string $product_type = ''): array {
|
function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page = 20, string $status = 'all', string $product_type = '', string $order_number = ''): array {
|
||||||
$empty = [
|
$empty = [
|
||||||
'items' => [],
|
'items' => [],
|
||||||
'total' => 0,
|
'total' => 0,
|
||||||
'total_pages' => 1,
|
'total_pages' => 1,
|
||||||
'page' => 1,
|
'page' => 1,
|
||||||
'stats' => ['total' => 0, 'pending' => 0, 'paid' => 0],
|
'query' => [],
|
||||||
|
'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0],
|
||||||
'product_types' => class_exists('\AKSRT\Core\DomainModel')
|
'product_types' => class_exists('\AKSRT\Core\DomainModel')
|
||||||
? array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels())
|
? array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels())
|
||||||
: [],
|
: [],
|
||||||
@@ -38,6 +39,7 @@ function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page
|
|||||||
'per_page' => $per_page,
|
'per_page' => $per_page,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'product_type' => $product_type,
|
'product_type' => $product_type,
|
||||||
|
'order_number' => $order_number,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (class_exists('\AKSRT\Payment\UserOrders')) {
|
if (class_exists('\AKSRT\Payment\UserOrders')) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* User center panel: My Orders (embedded; zibll-style tab content).
|
* User-center knowledge-payment order panel.
|
||||||
*
|
*
|
||||||
* @package AKSRT
|
* @package AKSRT
|
||||||
*/
|
*/
|
||||||
@@ -14,339 +14,128 @@ if ($user_id <= 0) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$per_page = 20;
|
$raw_query = [
|
||||||
$paged = max(1, (int) ($_GET['order_page'] ?? 1));
|
'status' => $_GET['order_status'] ?? '',
|
||||||
$tab = sanitize_key((string) ($_GET['order_tab'] ?? ($_GET['otab'] ?? '')));
|
'type' => $_GET['order_type'] ?? '',
|
||||||
$status = sanitize_key((string) ($_GET['order_status'] ?? ''));
|
'order_number' => $_GET['order_number'] ?? '',
|
||||||
$type_filter = sanitize_key((string) ($_GET['order_type'] ?? ''));
|
'page' => $_GET['order_page'] ?? 1,
|
||||||
$section = sanitize_key((string) ($_GET['section'] ?? ''));
|
'per_page' => 20,
|
||||||
|
|
||||||
// Accept legacy standalone-page tab keys.
|
|
||||||
if ($tab === '' && isset($_GET['tab']) && sanitize_key((string) $_GET['tab']) !== 'orders') {
|
|
||||||
// When rendered as standalone legacy include, outer tab may be status.
|
|
||||||
$maybe = sanitize_key((string) $_GET['tab']);
|
|
||||||
if (in_array($maybe, ['pending', 'after'], true)) {
|
|
||||||
$tab = $maybe;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$tab_aliases = [];
|
|
||||||
if (isset($tab_aliases[$tab])) {
|
|
||||||
$tab = $tab_aliases[$tab];
|
|
||||||
}
|
|
||||||
|
|
||||||
$tab_status_map = [
|
|
||||||
'pending' => \AKSRT\Payment\Order::STATUS_PENDING,
|
|
||||||
'after' => \AKSRT\Payment\Order::STATUS_REFUNDING,
|
|
||||||
];
|
];
|
||||||
if ($tab !== '' && isset($tab_status_map[$tab])) {
|
$query = \AKSRT\Payment\OrderCenterQuery::from_array($raw_query);
|
||||||
$status = $tab_status_map[$tab];
|
|
||||||
}
|
|
||||||
|
|
||||||
$orders_view = function_exists('aksrt_get_orders_page_view')
|
$orders_view = function_exists('aksrt_get_orders_page_view')
|
||||||
? aksrt_get_orders_page_view($user_id, $paged, $per_page, $status, $type_filter)
|
? aksrt_get_orders_page_view($user_id, $query->page(), $query->per_page(), $query->status(), $query->product_type(), $query->order_number())
|
||||||
: ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => [], 'product_types' => []];
|
: ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => [], 'product_types' => []];
|
||||||
|
|
||||||
$orders = (array) ($orders_view['items'] ?? []);
|
$orders = (array) ($orders_view['items'] ?? []);
|
||||||
$total_pages = max(1, (int) ($orders_view['total_pages'] ?? 1));
|
|
||||||
$stats = is_array($orders_view['stats'] ?? null) ? $orders_view['stats'] : [];
|
$stats = is_array($orders_view['stats'] ?? null) ? $orders_view['stats'] : [];
|
||||||
$type_options = is_array($orders_view['product_types'] ?? null) ? $orders_view['product_types'] : [];
|
$type_options = is_array($orders_view['product_types'] ?? null) ? $orders_view['product_types'] : [];
|
||||||
if ($type_options === [] && class_exists('\AKSRT\Core\DomainModel')) {
|
$page = max(1, (int) ($orders_view['page'] ?? 1));
|
||||||
$type_options = array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels());
|
$total_pages = max(1, (int) ($orders_view['total_pages'] ?? 1));
|
||||||
}
|
$status = $query->status();
|
||||||
|
$type = $query->product_type();
|
||||||
|
$order_number = $query->order_number();
|
||||||
|
|
||||||
$tabs = [
|
$orders_url = static function (array $override = []) use ($status, $type, $order_number, $page): string {
|
||||||
'' => __('全部', 'aksrt'),
|
$values = array_merge([
|
||||||
'pending' => __('待支付', 'aksrt'),
|
'order_status' => $status,
|
||||||
'after' => __('退款中', 'aksrt'),
|
'order_type' => $type,
|
||||||
];
|
'order_number' => $order_number,
|
||||||
|
'order_page' => $page,
|
||||||
$base_query = ['tab' => 'orders'];
|
], $override);
|
||||||
if ($type_filter !== '') {
|
$base = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('orders') : home_url('/profile/');
|
||||||
$base_query['order_type'] = $type_filter;
|
$values['tab'] = 'orders';
|
||||||
}
|
foreach ($values as $key => $value) {
|
||||||
if ($section !== '') {
|
if ($value === '' || $value === null || ($key === 'order_page' && (int) $value <= 1) || ($key === 'order_status' && $value === 'all')) {
|
||||||
$base_query['section'] = $section;
|
unset($values[$key]);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build URL staying inside user center orders tab.
|
|
||||||
*
|
|
||||||
* @param array<string, scalar|null> $args
|
|
||||||
*/
|
|
||||||
$orders_url = static function (array $args = []) use ($base_query): string {
|
|
||||||
$q = array_merge($base_query, $args);
|
|
||||||
$base = function_exists('aksrt_get_user_center_url')
|
|
||||||
? aksrt_get_user_center_url('orders')
|
|
||||||
: home_url('/profile/');
|
|
||||||
$url = remove_query_arg(
|
|
||||||
['order_tab', 'otab', 'order_status', 'order_page', 'order_type', 'section', 'tab'],
|
|
||||||
$base
|
|
||||||
);
|
|
||||||
// Always keep tab=orders.
|
|
||||||
$q['tab'] = 'orders';
|
|
||||||
// Map status filter to order_tab (avoid clashing with UC tab=).
|
|
||||||
if (isset($q['tab_status'])) {
|
|
||||||
$q['order_tab'] = $q['tab_status'];
|
|
||||||
unset($q['tab_status']);
|
|
||||||
}
|
|
||||||
foreach ($q as $k => $v) {
|
|
||||||
if ($v === null || $v === '') {
|
|
||||||
unset($q[$k]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return add_query_arg($q, $url);
|
return add_query_arg($values, remove_query_arg(['order_tab', 'otab', 'section'], $base));
|
||||||
};
|
};
|
||||||
|
|
||||||
$pending_count = (int) ($stats['pending'] ?? 0);
|
$tabs = [
|
||||||
|
'all' => __('全部', 'aksrt'),
|
||||||
|
'pending' => __('待支付', 'aksrt'),
|
||||||
|
'completed' => __('已完成', 'aksrt'),
|
||||||
|
'closed' => __('已关闭', 'aksrt'),
|
||||||
|
];
|
||||||
?>
|
?>
|
||||||
<div class="aksrt-orders-page uc-orders-panel" data-aksrt-order-center data-aksrt-uc-orders>
|
<div class="aksrt-orders-page uc-orders-panel" data-aksrt-order-center>
|
||||||
<div class="uc-orders-panel__head flex ac jsb mb10">
|
<header class="uc-orders-panel__head">
|
||||||
<div class="uc-orders-panel__title-wrap">
|
<div class="uc-orders-panel__title-wrap">
|
||||||
|
<p class="aksrt-orders-page__eyebrow"><?php esc_html_e('我的购买', 'aksrt'); ?></p>
|
||||||
<div class="uc-orders-panel__title font-bold em12">
|
<div class="uc-orders-panel__title font-bold em12">
|
||||||
<i class="fas fa-receipt uc-orders-panel__title-icon" aria-hidden="true"></i>
|
<i class="fas fa-receipt uc-orders-panel__title-icon" aria-hidden="true"></i>
|
||||||
<?php esc_html_e('我的订单', 'aksrt'); ?>
|
<?php esc_html_e('我的订单', 'aksrt'); ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted-2-color em09 mt3"><?php esc_html_e('查看付费内容、会员与充值等全部订单。', 'aksrt'); ?></div>
|
<p class="muted-2-color em09 mt3"><?php esc_html_e('管理已购内容、会员与账户权益。', 'aksrt'); ?></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (!empty($stats)) : ?>
|
|
||||||
<div class="aksrt-orders-page__stats" data-aksrt-order-stats>
|
<div class="aksrt-orders-page__stats" data-aksrt-order-stats>
|
||||||
<div class="aksrt-orders-page__stat">
|
<div class="aksrt-orders-page__stat"><span><?php esc_html_e('全部', 'aksrt'); ?></span><strong><?php echo esc_html((string) (int) ($stats['total'] ?? 0)); ?></strong></div>
|
||||||
<span class="aksrt-orders-page__stat-label"><?php esc_html_e('全部订单', 'aksrt'); ?></span>
|
<div class="aksrt-orders-page__stat"><span><?php esc_html_e('待支付', 'aksrt'); ?></span><strong class="is-pending"><?php echo esc_html((string) (int) ($stats['pending'] ?? 0)); ?></strong></div>
|
||||||
<strong class="aksrt-orders-page__stat-value"><?php echo esc_html((string) (int) ($stats['total'] ?? 0)); ?></strong>
|
<div class="aksrt-orders-page__stat"><span><?php esc_html_e('已生效', 'aksrt'); ?></span><strong class="is-completed"><?php echo esc_html((string) (int) ($stats['completed'] ?? 0)); ?></strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="aksrt-orders-page__stat">
|
</header>
|
||||||
<span class="aksrt-orders-page__stat-label"><?php esc_html_e('待支付', 'aksrt'); ?></span>
|
|
||||||
<strong class="aksrt-orders-page__stat-value c-red"><?php echo esc_html((string) $pending_count); ?></strong>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-orders-page__stat">
|
|
||||||
<span class="aksrt-orders-page__stat-label"><?php esc_html_e('已支付', 'aksrt'); ?></span>
|
|
||||||
<strong class="aksrt-orders-page__stat-value c-green"><?php echo esc_html((string) (int) ($stats['paid'] ?? 0)); ?></strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="aksrt-orders-page__extra-nav" data-aksrt-orders-extra-nav>
|
<form class="aksrt-orders-page__search" method="get" action="<?php echo esc_url($orders_url(['order_page' => null])); ?>">
|
||||||
<a href="<?php echo esc_url($orders_url(['section' => null])); ?>" class="aksrt-orders-page__extra-link<?php echo $section === '' ? ' is-active' : ''; ?>"><?php esc_html_e('订单列表', 'aksrt'); ?></a>
|
<input type="hidden" name="tab" value="orders">
|
||||||
<a href="<?php echo esc_url($orders_url(['section' => 'coupons'])); ?>" class="aksrt-orders-page__extra-link<?php echo $section === 'coupons' ? ' is-active' : ''; ?>"><?php esc_html_e('优惠券', 'aksrt'); ?></a>
|
<?php if ($status !== 'all') : ?><input type="hidden" name="order_status" value="<?php echo esc_attr($status); ?>"><?php endif; ?>
|
||||||
</div>
|
<?php if ($type !== '') : ?><input type="hidden" name="order_type" value="<?php echo esc_attr($type); ?>"><?php endif; ?>
|
||||||
|
<label class="screen-reader-text" for="aksrt-order-number"><?php esc_html_e('按订单号查找', 'aksrt'); ?></label>
|
||||||
|
<i class="fas fa-search" aria-hidden="true"></i>
|
||||||
|
<input id="aksrt-order-number" name="order_number" value="<?php echo esc_attr($order_number); ?>" maxlength="32" placeholder="<?php esc_attr_e('输入订单号开头快速查找', 'aksrt'); ?>">
|
||||||
|
<button class="btn-primary" type="submit"><?php esc_html_e('查找', 'aksrt'); ?></button>
|
||||||
|
<button class="aksrt-orders-page__filter-toggle" type="button" data-aksrt-orders-filter-toggle aria-expanded="false" aria-controls="aksrt-orders-filter-panel"><i class="fas fa-sliders-h" aria-hidden="true"></i> <?php esc_html_e('筛选', 'aksrt'); ?></button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<?php if ($type_options && $section === '') : ?>
|
<div id="aksrt-orders-filter-panel" class="aksrt-orders-page__filter-panel" data-aksrt-orders-filter-panel hidden>
|
||||||
<div class="aksrt-orders-page__type-filters" data-aksrt-order-type-filters>
|
<span class="aksrt-orders-page__filter-label"><?php esc_html_e('订单类型', 'aksrt'); ?></span>
|
||||||
<?php foreach ($type_options as $type_key => $type_label) :
|
<div class="aksrt-orders-page__type-filters">
|
||||||
$is_type_active = (string) $type_key === (string) $type_filter;
|
<?php foreach ($type_options as $key => $label) : ?>
|
||||||
$type_args = [
|
<a class="aksrt-orders-page__filter-chip<?php echo (string) $key === $type ? ' is-active' : ''; ?>" href="<?php echo esc_url($orders_url(['order_type' => $key, 'order_page' => null])); ?>"><?php echo esc_html((string) $label); ?></a>
|
||||||
'order_type' => $type_key !== '' ? $type_key : null,
|
|
||||||
'order_tab' => $tab !== '' ? $tab : null,
|
|
||||||
'order_page' => 1,
|
|
||||||
];
|
|
||||||
$type_url = $orders_url($type_args);
|
|
||||||
if ($type_key === '') {
|
|
||||||
$type_url = remove_query_arg('order_type', $type_url);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url($type_url); ?>" class="aksrt-orders-page__filter-chip<?php echo $is_type_active ? ' is-active' : ''; ?>">
|
|
||||||
<?php echo esc_html((string) $type_label); ?>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<a class="aksrt-orders-page__clear" href="<?php echo esc_url($orders_url(['order_status' => null, 'order_type' => null, 'order_number' => null, 'order_page' => null])); ?>"><?php esc_html_e('清除条件', 'aksrt'); ?></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php if ($section === '') : ?>
|
<nav class="aksrt-orders-page__tabs" aria-label="<?php esc_attr_e('订单状态', 'aksrt'); ?>">
|
||||||
<div class="aksrt-orders-page__tabs user-order-tabs">
|
<?php foreach ($tabs as $key => $label) : ?>
|
||||||
<?php foreach ($tabs as $tab_key => $tab_label) :
|
<a class="aksrt-orders-page__tab<?php echo $status === $key ? ' is-active' : ''; ?>" href="<?php echo esc_url($orders_url(['order_status' => $key, 'order_page' => null])); ?>">
|
||||||
$is_active = $tab === $tab_key;
|
<?php echo esc_html($label); ?>
|
||||||
$tab_url = $tab_key !== ''
|
<?php if ($key === 'pending' && !empty($stats['pending'])) : ?><span class="aksrt-orders-page__tab-badge"><?php echo esc_html((string) $stats['pending']); ?></span><?php endif; ?>
|
||||||
? $orders_url(['order_tab' => $tab_key, 'order_page' => 1])
|
|
||||||
: remove_query_arg(['order_tab', 'otab', 'order_status', 'order_page'], $orders_url());
|
|
||||||
?>
|
|
||||||
<a href="<?php echo esc_url($tab_url); ?>" class="aksrt-orders-page__tab<?php echo $is_active ? ' is-active' : ''; ?>" data-order-tab="<?php echo esc_attr((string) $tab_key); ?>">
|
|
||||||
<?php echo esc_html($tab_label); ?>
|
|
||||||
<?php if ($tab_key === 'pending' && $pending_count > 0) : ?>
|
|
||||||
<span class="aksrt-orders-page__tab-badge"><?php echo esc_html((string) $pending_count); ?></span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</a>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</nav>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($section === '' && !empty($orders)) : ?>
|
<?php if ($orders !== []) : ?>
|
||||||
<div class="aksrt-orders-page__list">
|
<div class="aksrt-orders-page__list" data-aksrt-order-list>
|
||||||
<?php foreach ($orders as $order) :
|
<?php foreach ($orders as $order) :
|
||||||
$on = (string) ($order['order_number'] ?? '');
|
$number = (string) ($order['order_number'] ?? '');
|
||||||
$os = (string) ($order['status'] ?? '');
|
$actions = (array) ($order['actions'] ?? []);
|
||||||
$items = $order['items'] ?? [];
|
$amount = !empty($order['is_points'])
|
||||||
$ptype = (string) ($order['product_type'] ?? '');
|
? esc_html((string) ($order['currency_symbol'] ?? __('积分', 'aksrt')) . ' ' . (string) (int) round((float) ($order['amount'] ?? 0)))
|
||||||
$is_points = !empty($order['is_points']);
|
|
||||||
$symbol = (string) ($order['currency_symbol'] ?? '¥');
|
|
||||||
$actions = is_array($order['actions'] ?? null) ? $order['actions'] : [];
|
|
||||||
$amount_display = $is_points
|
|
||||||
? esc_html($symbol) . ' ' . esc_html((string) (int) round((float) ($order['amount'] ?? 0)))
|
|
||||||
: '¥' . esc_html(number_format((float) ($order['amount'] ?? 0), 2));
|
: '¥' . esc_html(number_format((float) ($order['amount'] ?? 0), 2));
|
||||||
?>
|
?>
|
||||||
<article class="aksrt-orders-page__card" data-aksrt-order-card="<?php echo esc_attr($on); ?>" data-product-type="<?php echo esc_attr($ptype); ?>" data-order-status="<?php echo esc_attr($os); ?>">
|
<article class="aksrt-orders-page__card" data-aksrt-order-card="<?php echo esc_attr($number); ?>" data-order-status="<?php echo esc_attr((string) ($order['status_group'] ?? '')); ?>">
|
||||||
<div class="aksrt-orders-page__card-head">
|
<div class="aksrt-orders-page__card-rail"><span><?php echo esc_html((string) ($order['product_type_label'] ?? '')); ?></span><time><?php echo esc_html((string) ($order['created_at'] ?? '')); ?></time><span class="aksrt-orders-page__card-status is-<?php echo esc_attr((string) ($order['status_group'] ?? '')); ?>"><?php echo esc_html((string) ($order['status_label'] ?? '')); ?></span></div>
|
||||||
<?php if ((string) ($order['product_type_label'] ?? '') !== '') : ?>
|
<div class="aksrt-orders-page__card-main">
|
||||||
<span class="aksrt-orders-page__card-type"><?php echo esc_html((string) $order['product_type_label']); ?></span>
|
<?php if (!empty($order['image'])) : ?><img class="aksrt-orders-page__card-img" src="<?php echo esc_url((string) $order['image']); ?>" alt=""><?php else : ?><div class="aksrt-orders-page__card-art" aria-hidden="true"><i class="fas fa-book-open"></i></div><?php endif; ?>
|
||||||
<?php endif; ?>
|
<div class="aksrt-orders-page__card-copy"><h3><?php echo esc_html((string) ($order['product_name'] ?? __('订单', 'aksrt'))); ?></h3><p><?php printf(esc_html__('订单号 %s', 'aksrt'), esc_html($number)); ?></p></div>
|
||||||
<span class="aksrt-orders-page__card-number"><?php echo esc_html($on); ?></span>
|
<div class="aksrt-orders-page__card-value"><strong><?php echo $amount; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></strong><span><?php esc_html_e('实付', 'aksrt'); ?></span></div>
|
||||||
<time class="aksrt-orders-page__card-time"><?php echo esc_html((string) ($order['created_at'] ?? '')); ?></time>
|
|
||||||
<span class="aksrt-orders-page__card-status"><?php echo esc_html((string) ($order['status_label'] ?? '')); ?></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-orders-page__card-items">
|
|
||||||
<?php if (!empty($items) && is_array($items)) : ?>
|
|
||||||
<?php foreach ($items as $item) :
|
|
||||||
$item_image = (string) ($item['image'] ?? '');
|
|
||||||
if ($item_image === '') {
|
|
||||||
$item_image = (string) ($order['image'] ?? '');
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<div class="aksrt-orders-page__card-item<?php echo $item_image === '' ? ' aksrt-orders-page__card-item--no-image' : ''; ?>" data-product-id="<?php echo esc_attr((string) ($item['product_id'] ?? 0)); ?>">
|
|
||||||
<?php if ($item_image !== '') : ?>
|
|
||||||
<img src="<?php echo esc_url($item_image); ?>" alt="" class="aksrt-orders-page__card-img">
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="aksrt-orders-page__card-item-info">
|
|
||||||
<span class="aksrt-orders-page__card-item-title"><?php echo esc_html((string) ($item['product_title'] ?? $order['product_name'] ?? '')); ?></span>
|
|
||||||
<?php if (!empty($item['options_label'])) : ?>
|
|
||||||
<small class="aksrt-orders-page__card-item-opts"><?php echo esc_html((string) $item['options_label']); ?></small>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-orders-page__card-item-price">
|
|
||||||
<strong><?php
|
|
||||||
if ($is_points) {
|
|
||||||
echo esc_html($symbol . ' ' . (string) (int) round((float) ($item['pay_price'] ?? $item['unit_price'] ?? 0)));
|
|
||||||
} else {
|
|
||||||
echo '¥' . esc_html(number_format((float) ($item['pay_price'] ?? $item['unit_price'] ?? 0), 2));
|
|
||||||
}
|
|
||||||
?></strong>
|
|
||||||
<span>×<?php echo esc_html((string) ($item['quantity'] ?? 1)); ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php else : ?>
|
|
||||||
<?php $order_image = (string) ($order['image'] ?? ''); ?>
|
|
||||||
<div class="aksrt-orders-page__card-item<?php echo $order_image === '' ? ' aksrt-orders-page__card-item--no-image' : ''; ?>" data-product-id="<?php echo esc_attr((string) ($order['product_id'] ?? 0)); ?>">
|
|
||||||
<?php if ($order_image !== '') : ?>
|
|
||||||
<img src="<?php echo esc_url($order_image); ?>" alt="" class="aksrt-orders-page__card-img">
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="aksrt-orders-page__card-item-info">
|
|
||||||
<span class="aksrt-orders-page__card-item-title"><?php echo esc_html((string) ($order['product_name'] ?? __('订单', 'aksrt'))); ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-orders-page__card-item-price">
|
|
||||||
<strong><?php echo $amount_display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-orders-page__card-summary">
|
|
||||||
<span><?php esc_html_e('合计', 'aksrt'); ?></span>
|
|
||||||
<strong><?php echo $amount_display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></strong>
|
|
||||||
<?php if ((float) ($order['discount'] ?? 0) > 0 && !$is_points) : ?>
|
|
||||||
<small>(<?php
|
|
||||||
printf(
|
|
||||||
/* translators: %s: discount amount */
|
|
||||||
esc_html__('已优惠 ¥%s', 'aksrt'),
|
|
||||||
number_format((float) $order['discount'], 2)
|
|
||||||
);
|
|
||||||
?>)</small>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (!empty($order['shipping_records'])) : ?>
|
|
||||||
<div class="aksrt-orders-page__timeline" data-aksrt-timeline>
|
|
||||||
<h4 class="aksrt-orders-page__timeline-toggle" data-aksrt-toggle-timeline>
|
|
||||||
<i class="fas fa-truck"></i> <?php esc_html_e('物流信息', 'aksrt'); ?> <i class="fas fa-chevron-down"></i>
|
|
||||||
</h4>
|
|
||||||
<div class="aksrt-orders-page__timeline-body" hidden>
|
|
||||||
<?php foreach ((array) ($order['timeline'] ?? []) as $tl) : ?>
|
|
||||||
<div class="aksrt-orders-page__timeline-item">
|
|
||||||
<time><?php echo esc_html((string) ($tl['time'] ?? '')); ?></time>
|
|
||||||
<span><?php echo esc_html((string) ($tl['label'] ?? '')); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="aksrt-orders-page__card-actions">
|
|
||||||
<?php if (!empty($order['time_left'])) : ?>
|
|
||||||
<span class="aksrt-orders-page__countdown" data-aksrt-order-countdown="<?php echo esc_attr((string) $order['time_left']); ?>">
|
|
||||||
<?php
|
|
||||||
printf(
|
|
||||||
/* translators: %d: seconds */
|
|
||||||
esc_html__('%d秒后关闭', 'aksrt'),
|
|
||||||
(int) $order['time_left']
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<button type="button" class="btn-outline btn-sm" data-aksrt-order-detail="<?php echo esc_attr($on); ?>"><?php esc_html_e('查看', 'aksrt'); ?></button>
|
|
||||||
<?php if (!empty($actions['can_cancel'])) : ?>
|
|
||||||
<button type="button" class="btn-ghost btn-sm" data-aksrt-order-cancel="<?php echo esc_attr($on); ?>"><?php esc_html_e('取消', 'aksrt'); ?></button>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($actions['can_pay'])) : ?>
|
|
||||||
<button type="button" class="btn-primary btn-sm" data-aksrt-order-pay="<?php echo esc_attr($on); ?>" data-aksrt-order-method="<?php echo esc_attr((string) ($order['payment_method']['value'] ?? '')); ?>"><?php esc_html_e('去支付', 'aksrt'); ?></button>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($actions['can_view_product']) && !empty($order['permalink'])) : ?>
|
|
||||||
<a class="btn-ghost btn-sm" href="<?php echo esc_url((string) $order['permalink']); ?>"><?php
|
|
||||||
echo (class_exists('\AKSRT\Core\DomainModel') && $ptype === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE)
|
|
||||||
? esc_html__('去内容', 'aksrt')
|
|
||||||
: esc_html__('查看', 'aksrt');
|
|
||||||
?></a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
|
<footer class="aksrt-orders-page__card-actions">
|
||||||
|
<?php if (!empty($order['time_left'])) : ?><span class="aksrt-orders-page__countdown" data-aksrt-order-countdown="<?php echo esc_attr((string) $order['time_left']); ?>"></span><?php endif; ?>
|
||||||
|
<button class="btn-outline" type="button" data-aksrt-order-detail="<?php echo esc_attr($number); ?>"><?php esc_html_e('订单详情', 'aksrt'); ?></button>
|
||||||
|
<?php if (!empty($actions['can_cancel'])) : ?><button class="btn-ghost" type="button" data-aksrt-order-cancel="<?php echo esc_attr($number); ?>"><?php esc_html_e('关闭订单', 'aksrt'); ?></button><?php endif; ?>
|
||||||
|
<?php if (!empty($actions['can_pay'])) : ?><button class="btn-primary" type="button" data-aksrt-order-pay="<?php echo esc_attr($number); ?>" data-aksrt-order-method="<?php echo esc_attr((string) ($order['payment_method']['value'] ?? '')); ?>"><?php esc_html_e('继续支付', 'aksrt'); ?></button><?php endif; ?>
|
||||||
|
<?php if (!empty($actions['can_open'])) : ?><a class="btn-primary is-entitlement" href="<?php echo esc_url((string) ($order['permalink'] ?? '')); ?>"><?php echo esc_html((string) ($order['entitlement_label'] ?? __('打开已购内容', 'aksrt'))); ?></a><?php endif; ?>
|
||||||
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if ($total_pages > 1) : ?><nav class="aksrt-orders-page__pagination" aria-label="<?php esc_attr_e('订单分页', 'aksrt'); ?>"><?php for ($index = 1; $index <= $total_pages; $index++) : ?><a class="aksrt-orders-page__page-chip<?php echo $index === $page ? ' is-active' : ''; ?>" href="<?php echo esc_url($orders_url(['order_page' => $index])); ?>"><?php echo esc_html((string) $index); ?></a><?php endfor; ?></nav><?php endif; ?>
|
||||||
<?php if ($total_pages > 1) : ?>
|
|
||||||
<div class="aksrt-orders-page__pagination">
|
|
||||||
<?php for ($i = 1; $i <= $total_pages; $i++) :
|
|
||||||
$page_url = $orders_url(array_filter([
|
|
||||||
'order_tab' => $tab !== '' ? $tab : null,
|
|
||||||
'order_page' => $i,
|
|
||||||
], static fn($v) => $v !== null && $v !== ''));
|
|
||||||
?>
|
|
||||||
<?php if ($i === $paged) : ?>
|
|
||||||
<span class="aksrt-orders-page__page-chip is-active"><?php echo esc_html((string) $i); ?></span>
|
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<a href="<?php echo esc_url($page_url); ?>" class="aksrt-orders-page__page-chip"><?php echo esc_html((string) $i); ?></a>
|
<div class="aksrt-orders-page__empty"><i class="fas fa-receipt" aria-hidden="true"></i><h3><?php echo esc_html($order_number !== '' || $type !== '' || $status !== 'all' ? __('没有匹配的订单', 'aksrt') : __('还没有购买记录', 'aksrt')); ?></h3><p><?php echo esc_html($order_number !== '' || $type !== '' || $status !== 'all' ? __('换个条件试试,或清除当前筛选。', 'aksrt') : __('购买内容或开通权益后,订单会出现在这里。', 'aksrt')); ?></p><?php if ($order_number !== '' || $type !== '' || $status !== 'all') : ?><a class="btn-outline" href="<?php echo esc_url($orders_url(['order_status' => null, 'order_type' => null, 'order_number' => null, 'order_page' => null])); ?>"><?php esc_html_e('清除条件', 'aksrt'); ?></a><?php endif; ?></div>
|
||||||
<?php endif; ?>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php elseif ($section === '') : ?>
|
|
||||||
<div class="aksrt-orders-page__empty">
|
|
||||||
<i class="fas fa-receipt" aria-hidden="true"></i>
|
|
||||||
<p><?php esc_html_e('暂无订单记录', 'aksrt'); ?></p>
|
|
||||||
<p class="aksrt-orders-page__empty-hint muted-2-color em09"><?php esc_html_e('开通会员或购买付费内容,订单会出现在这里。', 'aksrt'); ?></p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Order detail modal -->
|
<div class="aksrt-modal" data-aksrt-order-detail-modal hidden><div class="aksrt-modal__overlay" data-aksrt-order-detail-close></div><section class="aksrt-modal__dialog aksrt-order-detail-dialog" role="dialog" aria-modal="true" aria-labelledby="aksrt-order-detail-title"><header class="aksrt-modal__header"><h3 id="aksrt-order-detail-title"><?php esc_html_e('订单详情', 'aksrt'); ?></h3><button class="aksrt-modal__close" type="button" data-aksrt-order-detail-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>">×</button></header><div class="aksrt-modal__body" data-aksrt-order-detail-body></div><footer class="aksrt-modal__footer" data-aksrt-order-detail-footer></footer></section></div>
|
||||||
<div class="aksrt-modal" data-aksrt-order-detail-modal hidden>
|
|
||||||
<div class="aksrt-modal__overlay" data-aksrt-order-detail-close></div>
|
|
||||||
<div class="aksrt-modal__dialog aksrt-order-detail-dialog" role="dialog" aria-modal="true" aria-labelledby="aksrt-order-detail-title">
|
|
||||||
<div class="aksrt-modal__header">
|
|
||||||
<h3 id="aksrt-order-detail-title"><?php esc_html_e('订单详情', 'aksrt'); ?></h3>
|
|
||||||
<button type="button" class="aksrt-modal__close" data-aksrt-order-detail-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-modal__body" data-aksrt-order-detail-body>
|
|
||||||
<p class="muted-2-color"><?php esc_html_e('加载中…', 'aksrt'); ?></p>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-modal__footer" data-aksrt-order-detail-footer></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($section === 'coupons') : ?>
|
|
||||||
<section class="aksrt-orders-page__coupons" data-aksrt-coupon-wallet>
|
|
||||||
<h2><?php esc_html_e('优惠券', 'aksrt'); ?></h2>
|
|
||||||
<div class="aksrt-coupon-wallet" data-aksrt-coupon-wallet-list>
|
|
||||||
<p class="muted-2-color"><?php esc_html_e('加载中…', 'aksrt'); ?></p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -668,21 +668,10 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
|
|||||||
['aksrt-tokens'],
|
['aksrt-tokens'],
|
||||||
$version
|
$version
|
||||||
);
|
);
|
||||||
// 微信扫码支付需要本地二维码生成器(AksrtShare.buildQrDataUrl)。
|
|
||||||
$weixin_share_orders = get_template_directory() . '/assets/js/weixin-share.js';
|
|
||||||
if (file_exists($weixin_share_orders)) {
|
|
||||||
wp_enqueue_script(
|
|
||||||
'aksrt-weixin-share',
|
|
||||||
$base_uri . '/assets/js/weixin-share.js',
|
|
||||||
['aksrt-script'],
|
|
||||||
(string) filemtime($weixin_share_orders),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'aksrt-order-center',
|
'aksrt-order-center',
|
||||||
$base_uri . '/assets/js/order-center.js',
|
$base_uri . '/assets/js/order-center.js',
|
||||||
['aksrt-script', 'aksrt-weixin-share', 'aksrt-core-remote-modal'],
|
['aksrt-script'],
|
||||||
file_exists(get_template_directory() . '/assets/js/order-center.js')
|
file_exists(get_template_directory() . '/assets/js/order-center.js')
|
||||||
? (string) filemtime(get_template_directory() . '/assets/js/order-center.js')
|
? (string) filemtime(get_template_directory() . '/assets/js/order-center.js')
|
||||||
: $version,
|
: $version,
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Canonical query state for the knowledge-payment order center.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Payment;
|
||||||
|
|
||||||
|
use AKSRT\Core\DomainModel;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
final class OrderCenterQuery {
|
||||||
|
|
||||||
|
public const STATUS_ALL = 'all';
|
||||||
|
public const STATUS_PENDING = 'pending';
|
||||||
|
public const STATUS_COMPLETED = 'completed';
|
||||||
|
public const STATUS_CLOSED = 'closed';
|
||||||
|
|
||||||
|
private const STATUS_GROUPS = [
|
||||||
|
self::STATUS_ALL,
|
||||||
|
self::STATUS_PENDING,
|
||||||
|
self::STATUS_COMPLETED,
|
||||||
|
self::STATUS_CLOSED,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
private array $values;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $values
|
||||||
|
*/
|
||||||
|
private function __construct(array $values) {
|
||||||
|
$this->values = $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $raw
|
||||||
|
*/
|
||||||
|
public static function from_array(array $raw, int $default_per_page = 20): self {
|
||||||
|
$status = sanitize_key((string) ($raw['status'] ?? self::STATUS_ALL));
|
||||||
|
if (!in_array($status, self::STATUS_GROUPS, true)) {
|
||||||
|
$status = self::STATUS_ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product_type = sanitize_key((string) ($raw['type'] ?? $raw['product_type'] ?? ''));
|
||||||
|
$types = DomainModel::order_product_type_labels();
|
||||||
|
if ($product_type !== '' && !isset($types[$product_type])) {
|
||||||
|
$product_type = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$order_number = strtoupper(preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($raw['order_number'] ?? '')) ?? '');
|
||||||
|
$order_number = substr($order_number, 0, 32);
|
||||||
|
|
||||||
|
$page = max(1, absint($raw['page'] ?? 1));
|
||||||
|
$per_page = max(1, min(50, absint($raw['per_page'] ?? $default_per_page)));
|
||||||
|
|
||||||
|
return new self([
|
||||||
|
'status' => $status,
|
||||||
|
'product_type' => $product_type,
|
||||||
|
'order_number' => $order_number,
|
||||||
|
'page' => $page,
|
||||||
|
'per_page' => $per_page,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status(): string {
|
||||||
|
return (string) $this->values['status'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function product_type(): string {
|
||||||
|
return (string) $this->values['product_type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function order_number(): string {
|
||||||
|
return (string) $this->values['order_number'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function page(): int {
|
||||||
|
return (int) $this->values['page'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function per_page(): int {
|
||||||
|
return (int) $this->values['per_page'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return string[] */
|
||||||
|
public function database_statuses(): array {
|
||||||
|
return match ($this->status()) {
|
||||||
|
self::STATUS_PENDING => [Order::STATUS_CREATED, Order::STATUS_PENDING],
|
||||||
|
self::STATUS_COMPLETED => [Order::STATUS_PAID],
|
||||||
|
self::STATUS_CLOSED => [Order::STATUS_CANCELLED, Order::STATUS_EXPIRED],
|
||||||
|
default => [
|
||||||
|
Order::STATUS_CREATED,
|
||||||
|
Order::STATUS_PENDING,
|
||||||
|
Order::STATUS_PAID,
|
||||||
|
Order::STATUS_CANCELLED,
|
||||||
|
Order::STATUS_EXPIRED,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, scalar> */
|
||||||
|
public function url_values(): array {
|
||||||
|
return array_filter([
|
||||||
|
'order_status' => $this->status() !== self::STATUS_ALL ? $this->status() : '',
|
||||||
|
'order_type' => $this->product_type(),
|
||||||
|
'order_number' => $this->order_number(),
|
||||||
|
'order_page' => $this->page() > 1 ? $this->page() : '',
|
||||||
|
], static fn($value): bool => $value !== '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function repository_args(): array {
|
||||||
|
return [
|
||||||
|
'status_in' => $this->database_statuses(),
|
||||||
|
'product_type' => $this->product_type(),
|
||||||
|
'order_number_prefix' => $this->order_number(),
|
||||||
|
'limit' => $this->per_page(),
|
||||||
|
'offset' => ($this->page() - 1) * $this->per_page(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Public REST for order-center actions (pay / cancel / detail).
|
* Public REST for the knowledge-payment order center.
|
||||||
*
|
|
||||||
* Registered outside the admin SPA bootstrap so front-end order center
|
|
||||||
* always has routes even if an admin controller fails to load.
|
|
||||||
*
|
*
|
||||||
* @package AKSRT
|
* @package AKSRT
|
||||||
*/
|
*/
|
||||||
@@ -28,100 +25,105 @@ class OrderCenterRest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function register_routes(): void {
|
public function register_routes(): void {
|
||||||
$ns = 'aksrt/v1';
|
$namespace = 'aksrt/v1';
|
||||||
// Allow common order_number charset (AKS… + optional separators).
|
$order_number = '(?P<order_number>[A-Za-z0-9_-]+)';
|
||||||
$on = '(?P<order_number>[A-Za-z0-9_-]+)';
|
$query_args = [
|
||||||
|
'order_status' => ['sanitize_callback' => 'sanitize_key'],
|
||||||
|
'order_type' => ['sanitize_callback' => 'sanitize_key'],
|
||||||
|
'order_number' => ['sanitize_callback' => 'sanitize_text_field'],
|
||||||
|
'order_page' => ['sanitize_callback' => 'absint'],
|
||||||
|
];
|
||||||
|
|
||||||
register_rest_route($ns, '/shop/frontend/orders/' . $on . '/pay', [
|
register_rest_route($namespace, '/orders/view', [
|
||||||
'methods' => 'POST',
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
|
'callback' => [$this, 'get_view'],
|
||||||
|
'permission_callback' => [$this, 'require_logged_in'],
|
||||||
|
'args' => $query_args,
|
||||||
|
]);
|
||||||
|
$route_args = [
|
||||||
|
'order_number' => [
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'validate_callback' => static function ($value): bool {
|
||||||
|
return is_string($value) && (bool) preg_match('/^[A-Za-z0-9_-]+$/', $value);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
register_rest_route($namespace, '/orders/' . $order_number . '/pay', [
|
||||||
|
'methods' => \WP_REST_Server::CREATABLE,
|
||||||
'callback' => [$this, 'pay'],
|
'callback' => [$this, 'pay'],
|
||||||
'permission_callback' => [$this, 'require_logged_in'],
|
'permission_callback' => [$this, 'require_logged_in'],
|
||||||
|
'args' => $route_args,
|
||||||
]);
|
]);
|
||||||
|
register_rest_route($namespace, '/orders/' . $order_number . '/cancel', [
|
||||||
register_rest_route($ns, '/shop/frontend/orders/' . $on . '/cancel', [
|
'methods' => \WP_REST_Server::CREATABLE,
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => [$this, 'cancel'],
|
'callback' => [$this, 'cancel'],
|
||||||
'permission_callback' => [$this, 'require_logged_in'],
|
'permission_callback' => [$this, 'require_logged_in'],
|
||||||
|
'args' => $route_args,
|
||||||
]);
|
]);
|
||||||
|
register_rest_route($namespace, '/orders/' . $order_number, [
|
||||||
register_rest_route($ns, '/shop/frontend/orders/' . $on, [
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
[
|
|
||||||
'methods' => 'GET',
|
|
||||||
'callback' => [$this, 'get_order'],
|
'callback' => [$this, 'get_order'],
|
||||||
'permission_callback' => [$this, 'require_logged_in'],
|
'permission_callback' => [$this, 'require_logged_in'],
|
||||||
],
|
'args' => $route_args,
|
||||||
[
|
|
||||||
// order-center.js historically POSTed for refresh — accept both.
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => [$this, 'get_order'],
|
|
||||||
'permission_callback' => [$this, 'require_logged_in'],
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @return true|\WP_Error */
|
||||||
* @return true|\WP_Error
|
|
||||||
*/
|
|
||||||
public function require_logged_in() {
|
public function require_logged_in() {
|
||||||
if (!is_user_logged_in()) {
|
if (!is_user_logged_in()) {
|
||||||
return new \WP_Error(
|
return new \WP_Error('aksrt_rest_forbidden', __('请先登录', 'aksrt'), ['status' => 401]);
|
||||||
'aksrt_rest_forbidden',
|
|
||||||
__('请先登录', 'aksrt'),
|
|
||||||
['status' => 401]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pay(\WP_REST_Request $request): \WP_REST_Response {
|
public function get_view(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
|
|
||||||
$params = $request->get_json_params();
|
|
||||||
if (!is_array($params)) {
|
|
||||||
$params = [];
|
|
||||||
}
|
|
||||||
$payment_method = sanitize_key((string) ($params['payment_method'] ?? ''));
|
|
||||||
|
|
||||||
if (!class_exists(UserOrders::class)) {
|
if (!class_exists(UserOrders::class)) {
|
||||||
return $this->error(__('订单服务不可用', 'aksrt'), 500);
|
return $this->error(__('订单服务不可用', 'aksrt'), 500);
|
||||||
}
|
}
|
||||||
$result = UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method);
|
$view = UserOrders::get_instance()->get_order_center_view(get_current_user_id(), $this->query_args($request));
|
||||||
|
return $this->success(['data' => $view]);
|
||||||
if (empty($result['success'])) {
|
|
||||||
return $this->error((string) ($result['message'] ?? __('支付失败', 'aksrt')), 400);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $result['data'] ?? []]);
|
public function pay(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
|
$params = $request->get_json_params();
|
||||||
|
$params = is_array($params) ? $params : [];
|
||||||
|
$result = UserOrders::get_instance()->pay_order(
|
||||||
|
get_current_user_id(),
|
||||||
|
sanitize_text_field((string) $request->get_param('order_number')),
|
||||||
|
sanitize_key((string) ($params['payment_method'] ?? ''))
|
||||||
|
);
|
||||||
|
return !empty($result['success'])
|
||||||
|
? $this->success(['data' => $result['data'] ?? []])
|
||||||
|
: $this->error((string) ($result['message'] ?? __('支付失败', 'aksrt')), 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cancel(\WP_REST_Request $request): \WP_REST_Response {
|
public function cancel(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
|
$result = UserOrders::get_instance()->cancel_order(
|
||||||
|
get_current_user_id(),
|
||||||
if (!class_exists(UserOrders::class)) {
|
sanitize_text_field((string) $request->get_param('order_number'))
|
||||||
return $this->error(__('订单服务不可用', 'aksrt'), 500);
|
);
|
||||||
}
|
return !empty($result['success'])
|
||||||
$result = UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number);
|
? $this->success(['data' => $result['data'] ?? []])
|
||||||
|
: $this->error((string) ($result['message'] ?? __('关闭订单失败', 'aksrt')), 400);
|
||||||
if (empty($result['success'])) {
|
|
||||||
return $this->error((string) ($result['message'] ?? __('取消订单失败', 'aksrt')), 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(['data' => $result['data'] ?? []]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_order(\WP_REST_Request $request): \WP_REST_Response {
|
public function get_order(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
|
$order = UserOrders::get_instance()->get_order_detail_view(
|
||||||
$order = null;
|
get_current_user_id(),
|
||||||
|
sanitize_text_field((string) $request->get_param('order_number'))
|
||||||
if (!class_exists(UserOrders::class)) {
|
);
|
||||||
return $this->error(__('订单服务不可用', 'aksrt'), 500);
|
return $order ? $this->success(['data' => $order]) : $this->error(__('订单不存在', 'aksrt'), 404);
|
||||||
}
|
|
||||||
$order = UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
|
|
||||||
|
|
||||||
if (!$order) {
|
|
||||||
return $this->error(__('订单不存在', 'aksrt'), 404);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $order]);
|
/** @return array<string, mixed> */
|
||||||
|
private function query_args(\WP_REST_Request $request): array {
|
||||||
|
return [
|
||||||
|
'status' => $request->get_param('order_status'),
|
||||||
|
'type' => $request->get_param('order_type'),
|
||||||
|
'order_number' => $request->get_param('order_number'),
|
||||||
|
'page' => $request->get_param('order_page'),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function success(array $data = [], int $status = 200): \WP_REST_Response {
|
private function success(array $data = [], int $status = 200): \WP_REST_Response {
|
||||||
@@ -129,9 +131,6 @@ class OrderCenterRest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function error(string $message, int $status = 400): \WP_REST_Response {
|
private function error(string $message, int $status = 400): \WP_REST_Response {
|
||||||
return new \WP_REST_Response([
|
return new \WP_REST_Response(['success' => false, 'message' => $message], $status);
|
||||||
'success' => false,
|
|
||||||
'message' => $message,
|
|
||||||
], $status);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,11 +108,17 @@ trait OrderRepositoryCore {
|
|||||||
$params[] = $user_id;
|
$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'] ?? ''));
|
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||||
if ($status !== '') {
|
if ($status !== '') {
|
||||||
$where[] = 'status = %s';
|
$where[] = 'status = %s';
|
||||||
$params[] = $status;
|
$params[] = $status;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
||||||
if ($product_type !== '') {
|
if ($product_type !== '') {
|
||||||
@@ -120,6 +126,12 @@ trait OrderRepositoryCore {
|
|||||||
$params[] = $product_type;
|
$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)));
|
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||||
|
|
||||||
@@ -166,11 +178,17 @@ trait OrderRepositoryCore {
|
|||||||
$where = ['user_id = %d'];
|
$where = ['user_id = %d'];
|
||||||
$params = [$user_id];
|
$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'] ?? ''));
|
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||||
if ($status !== '') {
|
if ($status !== '') {
|
||||||
$where[] = 'status = %s';
|
$where[] = 'status = %s';
|
||||||
$params[] = $status;
|
$params[] = $status;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
||||||
if ($product_type !== '') {
|
if ($product_type !== '') {
|
||||||
@@ -178,6 +196,12 @@ trait OrderRepositoryCore {
|
|||||||
$params[] = $product_type;
|
$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) . '%';
|
||||||
|
}
|
||||||
|
|
||||||
$where_sql = 'WHERE ' . implode(' AND ', $where);
|
$where_sql = 'WHERE ' . implode(' AND ', $where);
|
||||||
$sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
|
$sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
|
||||||
|
|
||||||
|
|||||||
+166
-565
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Front-end "My Orders" view-model (all product_types).
|
* Front-end knowledge-payment order view model.
|
||||||
*
|
|
||||||
* Shop logistics/review stay shop-only; content/VIP/recharge get type-aware cards.
|
|
||||||
*
|
*
|
||||||
* @package AKSRT
|
* @package AKSRT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace AKSRT\Payment;
|
namespace AKSRT\Payment;
|
||||||
|
|
||||||
|
use AKSRT\Core\DomainModel;
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
class UserOrders {
|
class UserOrders {
|
||||||
@@ -23,161 +23,109 @@ class UserOrders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paginated user order center payload.
|
* @param array<string, mixed> $args
|
||||||
*
|
* @return array<string, mixed>
|
||||||
* @param array{page?:int,per_page?:int,status?:string,product_type?:string} $args
|
|
||||||
* @return array{items:array,total:int,total_pages:int,page:int,stats:array,product_types:array}
|
|
||||||
*/
|
*/
|
||||||
public function get_order_center_view(int $user_id, array $args = []): array {
|
public function get_order_center_view(int $user_id, array $args = []): array {
|
||||||
|
$query = OrderCenterQuery::from_array($args);
|
||||||
|
$empty = $this->empty_view($query);
|
||||||
if ($user_id <= 0 || !class_exists(OrderRepository::class)) {
|
if ($user_id <= 0 || !class_exists(OrderRepository::class)) {
|
||||||
return [
|
return $empty;
|
||||||
'items' => [],
|
|
||||||
'total' => 0,
|
|
||||||
'total_pages' => 1,
|
|
||||||
'page' => 1,
|
|
||||||
'stats' => $this->empty_stats(),
|
|
||||||
'product_types' => $this->product_type_filter_options(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$page = max(1, (int) ($args['page'] ?? 1));
|
|
||||||
$per_page = max(1, min(50, (int) ($args['per_page'] ?? 20)));
|
|
||||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
|
||||||
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
|
||||||
|
|
||||||
$repo_args = [
|
|
||||||
'limit' => $per_page,
|
|
||||||
'offset' => ($page - 1) * $per_page,
|
|
||||||
];
|
|
||||||
if ($status !== '') {
|
|
||||||
$repo_args['status'] = $status;
|
|
||||||
}
|
|
||||||
if ($product_type !== '' && $product_type !== 'all') {
|
|
||||||
$repo_args['product_type'] = $product_type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$repo_args = $query->repository_args();
|
||||||
$repo = OrderRepository::get_instance();
|
$repo = OrderRepository::get_instance();
|
||||||
$orders = $repo->get_user_orders($user_id, $repo_args);
|
$orders = $repo->get_user_orders($user_id, $repo_args);
|
||||||
$total = $repo->count_user_orders($user_id, $repo_args);
|
$total = $repo->count_user_orders($user_id, $repo_args);
|
||||||
|
$total_pages = max(1, (int) ceil($total / $query->per_page()));
|
||||||
|
$page = min($query->page(), $total_pages);
|
||||||
|
|
||||||
$items = [];
|
if ($page !== $query->page() && $total > 0) {
|
||||||
foreach ($orders as $order) {
|
$repo_args['offset'] = ($page - 1) * $query->per_page();
|
||||||
$items[] = $this->decorate_order_view($order, true);
|
$orders = $repo->get_user_orders($user_id, $repo_args);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'items' => $items,
|
'items' => array_map([$this, 'decorate_order_view'], $orders),
|
||||||
'total' => $total,
|
'total' => $total,
|
||||||
'total_pages' => max(1, (int) ceil(max(0, $total) / $per_page)),
|
'total_pages' => $total_pages,
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
|
'query' => array_merge($query->url_values(), ['order_page' => $page]),
|
||||||
'stats' => $this->get_user_order_stats($user_id),
|
'stats' => $this->get_user_order_stats($user_id),
|
||||||
'product_types' => $this->product_type_filter_options(),
|
'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): ?array {
|
||||||
if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) {
|
if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$order = OrderRepository::get_instance()->get_user_order($user_id, $order_number);
|
$order = OrderRepository::get_instance()->get_user_order($user_id, $order_number);
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (class_exists(Gateway::class)) {
|
if (class_exists(Gateway::class)) {
|
||||||
$order = Gateway::get_instance()->reconcile_order($order_number, $order) ?: $order;
|
$order = Gateway::get_instance()->reconcile_order($order_number, $order) ?: $order;
|
||||||
}
|
}
|
||||||
return $this->decorate_order_view($order, true);
|
|
||||||
|
return $this->decorate_order_view($order);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @return array<string, int> */
|
||||||
* @return array{total:int,pending:int,paid:int}
|
|
||||||
*/
|
|
||||||
public function get_user_order_stats(int $user_id): array {
|
public function get_user_order_stats(int $user_id): array {
|
||||||
if ($user_id <= 0 || !class_exists(OrderRepository::class)) {
|
if ($user_id <= 0 || !class_exists(OrderRepository::class)) {
|
||||||
return $this->empty_stats();
|
return ['total' => 0, 'pending' => 0, 'completed' => 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
$repo = OrderRepository::get_instance();
|
$repo = OrderRepository::get_instance();
|
||||||
return [
|
return [
|
||||||
'total' => $repo->count_user_orders($user_id, []),
|
'total' => $repo->count_user_orders($user_id, [
|
||||||
'pending' => $repo->count_user_orders($user_id, ['status' => Order::STATUS_PENDING]),
|
'status_in' => OrderCenterQuery::from_array([])->database_statuses(),
|
||||||
'paid' => $repo->count_user_orders($user_id, ['status' => Order::STATUS_PAID]),
|
]),
|
||||||
|
'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],
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @return array<string, string> */
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function product_type_filter_options(): array {
|
public function product_type_filter_options(): array {
|
||||||
$labels = \AKSRT\Core\DomainModel::order_product_type_labels();
|
return array_merge(['' => __('全部类型', 'aksrt')], DomainModel::order_product_type_labels());
|
||||||
return array_merge(['' => __('全部类型', 'aksrt')], $labels);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $order Normalized order row.
|
* @param array<string, mixed> $order
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
public function decorate_order_view(array $order, bool $with_detail = false): array {
|
public function decorate_order_view(array $order): array {
|
||||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||||
$order_number = (string) ($order['order_number'] ?? '');
|
|
||||||
$product_type = sanitize_key((string) ($order['product_type'] ?? ''));
|
$product_type = sanitize_key((string) ($order['product_type'] ?? ''));
|
||||||
$status = (string) ($order['status'] ?? Order::STATUS_PENDING);
|
|
||||||
$product_id = (int) ($order['product_id'] ?? 0);
|
$product_id = (int) ($order['product_id'] ?? 0);
|
||||||
|
$status = sanitize_key((string) ($order['status'] ?? Order::STATUS_PENDING));
|
||||||
$currency = sanitize_key((string) ($order['currency'] ?? 'CNY'));
|
$currency = sanitize_key((string) ($order['currency'] ?? 'CNY'));
|
||||||
$is_points = $currency === 'points'
|
$is_points = $currency === 'points'
|
||||||
|| sanitize_key((string) ($order['payment_method'] ?? '')) === 'points'
|
|| sanitize_key((string) ($order['payment_method'] ?? '')) === 'points'
|
||||||
|| (!empty($meta['pay_modo']) && (string) $meta['pay_modo'] === 'points');
|
|| sanitize_key((string) ($meta['pay_modo'] ?? '')) === 'points';
|
||||||
|
$type_labels = DomainModel::order_product_type_labels();
|
||||||
|
$type_label = $type_labels[$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);
|
||||||
|
$group = $this->status_group($status);
|
||||||
|
$payment_method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
||||||
|
$can_pay = $group === OrderCenterQuery::STATUS_PENDING && $payment_method !== 'manual_review';
|
||||||
|
$can_cancel = $group === OrderCenterQuery::STATUS_PENDING;
|
||||||
|
$can_open = $group === OrderCenterQuery::STATUS_COMPLETED && $permalink !== '';
|
||||||
|
|
||||||
$type_labels = \AKSRT\Core\DomainModel::order_product_type_labels();
|
return [
|
||||||
$type_label = $type_labels[$product_type] ?? ($product_type !== '' ? $product_type : __('订单', 'aksrt'));
|
|
||||||
|
|
||||||
$title = $this->resolve_order_title($order, $meta, $product_type, $product_id, $type_label);
|
|
||||||
$image = $this->resolve_order_image($order, $meta, $product_type, $product_id);
|
|
||||||
$permalink = $this->resolve_order_permalink($product_type, $product_id, $meta);
|
|
||||||
|
|
||||||
$order_items = [];
|
|
||||||
$has_structured_items = !empty($order_items);
|
|
||||||
|
|
||||||
$product = [];
|
|
||||||
// Shop product cards were removed with the shop feature; content/VIP
|
|
||||||
// orders fall through to the shared meta-based fields below.
|
|
||||||
|
|
||||||
if (!empty($product['title'])) {
|
|
||||||
$title = (string) $product['title'];
|
|
||||||
}
|
|
||||||
if (!empty($product['image'])) {
|
|
||||||
$image = (string) $product['image'];
|
|
||||||
}
|
|
||||||
if (!empty($product['permalink'])) {
|
|
||||||
$permalink = (string) $product['permalink'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$shipping_records = [];
|
|
||||||
$after_sale_items = [];
|
|
||||||
$subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount'] ?? 0);
|
|
||||||
$shipping_fee = (float) ($meta['shipping_fee'] ?? 0);
|
|
||||||
$discount = (float) ($meta['discount'] ?? 0);
|
|
||||||
$expires_at = (string) ($order['expires_at'] ?? '');
|
|
||||||
$payment_value = sanitize_key((string) ($meta['payment_method']['value'] ?? $order['payment_method'] ?? 'balance'));
|
|
||||||
|
|
||||||
$can_pay = in_array($status, [Order::STATUS_PENDING, Order::STATUS_EXPIRED, Order::STATUS_CREATED], true)
|
|
||||||
&& $payment_value !== 'manual_review';
|
|
||||||
$can_cancel = in_array($status, [Order::STATUS_PENDING, Order::STATUS_CREATED, Order::STATUS_EXPIRED], true);
|
|
||||||
$can_after_sale = false;
|
|
||||||
$can_receipt = false;
|
|
||||||
$can_review = false;
|
|
||||||
$can_rebuy = false;
|
|
||||||
// Order detail modal is always available; product permalink is optional.
|
|
||||||
$can_view = true;
|
|
||||||
$can_view_product = $permalink !== '' && in_array($status, [
|
|
||||||
Order::STATUS_PAID,
|
|
||||||
Order::STATUS_DELIVERED,
|
|
||||||
Order::STATUS_COMPLETED,
|
|
||||||
Order::STATUS_PENDING_REVIEW,
|
|
||||||
], true);
|
|
||||||
|
|
||||||
$view = [
|
|
||||||
'id' => (int) ($order['id'] ?? 0),
|
'id' => (int) ($order['id'] ?? 0),
|
||||||
'order_number' => $order_number,
|
'order_number' => (string) ($order['order_number'] ?? ''),
|
||||||
'amount' => (float) ($order['amount'] ?? 0),
|
'amount' => (float) ($order['amount'] ?? 0),
|
||||||
'currency' => $currency,
|
'currency' => $currency,
|
||||||
'currency_symbol' => $is_points
|
'currency_symbol' => $is_points
|
||||||
@@ -185,536 +133,189 @@ class UserOrders {
|
|||||||
: '¥',
|
: '¥',
|
||||||
'is_points' => $is_points,
|
'is_points' => $is_points,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'status_label' => Order::get_valid_statuses()[$status] ?? $status,
|
'status_group' => $group,
|
||||||
|
'status_label' => $this->status_label($group),
|
||||||
'product_type' => $product_type,
|
'product_type' => $product_type,
|
||||||
'product_type_label' => $type_label,
|
'product_type_label' => $type_label,
|
||||||
'product_id' => $product_id,
|
'product_id' => $product_id,
|
||||||
'product_name' => $title,
|
'product_name' => $title,
|
||||||
'product' => $product ?: [
|
|
||||||
'title' => $title,
|
|
||||||
'image' => $image,
|
|
||||||
'permalink' => $permalink,
|
|
||||||
],
|
|
||||||
'image' => $image,
|
'image' => $image,
|
||||||
'permalink' => $permalink,
|
'permalink' => $permalink,
|
||||||
'created_at' => (string) ($order['created_at'] ?? ''),
|
'created_at' => (string) ($order['created_at'] ?? ''),
|
||||||
'paid_at' => (string) ($order['paid_at'] ?? ''),
|
'paid_at' => (string) ($order['paid_at'] ?? ''),
|
||||||
'expires_at' => $expires_at,
|
'expires_at' => (string) ($order['expires_at'] ?? ''),
|
||||||
'subtotal' => $subtotal,
|
'payment_method' => [
|
||||||
'shipping_fee' => $shipping_fee,
|
'value' => $payment_method,
|
||||||
'discount' => $discount,
|
'label' => $this->payment_method_label($payment_method),
|
||||||
'address' => $meta['address'] ?? [],
|
|
||||||
'payment_method' => $meta['payment_method'] ?? [
|
|
||||||
'value' => (string) ($order['payment_method'] ?? ''),
|
|
||||||
'label' => $this->payment_method_label((string) ($order['payment_method'] ?? '')),
|
|
||||||
],
|
],
|
||||||
'coupon' => $meta['coupon'] ?? [],
|
|
||||||
'items' => $has_structured_items ? $order_items : ($meta['items'] ?? []),
|
|
||||||
'shipping_method' => $meta['shipping_method'] ?? [],
|
|
||||||
'customer_note' => $meta['customer_note'] ?? ($order['note_text'] ?? ''),
|
|
||||||
'shipping_records' => $shipping_records,
|
|
||||||
'pay_url' => '#',
|
|
||||||
'actions' => [
|
'actions' => [
|
||||||
'can_pay' => $can_pay,
|
'can_pay' => $can_pay,
|
||||||
'can_cancel' => $can_cancel,
|
'can_cancel' => $can_cancel,
|
||||||
'can_after_sale' => $can_after_sale,
|
'can_open' => $can_open,
|
||||||
'can_receipt' => $can_receipt,
|
|
||||||
'can_review' => $can_review,
|
|
||||||
'can_rebuy' => $can_rebuy,
|
|
||||||
'can_view' => $can_view,
|
|
||||||
'can_view_product' => $can_view_product,
|
|
||||||
],
|
],
|
||||||
'quantity' => $this->resolve_order_quantity($order_items, $meta),
|
'time_left' => $this->get_order_countdown((string) ($order['expires_at'] ?? ''), $status),
|
||||||
'time_left' => $this->get_order_countdown($expires_at, $status),
|
'entitlement_label' => $this->entitlement_label($product_type),
|
||||||
'secondary_url' => $this->resolve_secondary_url($product_type),
|
|
||||||
'secondary_label' => $this->resolve_secondary_label($product_type),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($with_detail) {
|
|
||||||
$view['after_sale_records'] = $after_sale_items;
|
|
||||||
$view['timeline'] = $this->build_order_timeline($order, $shipping_records, $after_sale_items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $view;
|
/** @return array<string, mixed> */
|
||||||
}
|
private function empty_view(OrderCenterQuery $query): array {
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel pending order (any product_type).
|
|
||||||
*
|
|
||||||
* @return array{success:bool,message?:string,data?:array}
|
|
||||||
*/
|
|
||||||
public function cancel_order(int $user_id, string $order_number): array {
|
|
||||||
if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) {
|
|
||||||
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$repo = OrderRepository::get_instance();
|
|
||||||
$order = $repo->get_user_order($user_id, $order_number);
|
|
||||||
if (!$order) {
|
|
||||||
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = (string) ($order['status'] ?? '');
|
|
||||||
if (!in_array($status, [Order::STATUS_PENDING, Order::STATUS_CREATED, Order::STATUS_EXPIRED], true)) {
|
|
||||||
return ['success' => false, 'message' => __('当前订单不可取消。', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$repo->update_status($order_number, Order::STATUS_CANCELLED, [
|
|
||||||
'force' => true,
|
|
||||||
'note' => __('用户在前台取消订单。', 'aksrt'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'items' => [],
|
||||||
'data' => [
|
'total' => 0,
|
||||||
'order' => $this->get_order_detail_view($user_id, $order_number),
|
'total_pages' => 1,
|
||||||
],
|
'page' => 1,
|
||||||
|
'query' => $query->url_values(),
|
||||||
|
'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0],
|
||||||
|
'product_types' => $this->product_type_filter_options(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function status_group(string $status): string {
|
||||||
* Resume payment via configured gateway (any product_type).
|
return match ($status) {
|
||||||
*
|
Order::STATUS_CREATED, Order::STATUS_PENDING => OrderCenterQuery::STATUS_PENDING,
|
||||||
* @return array{success:bool,message?:string,data?:array}
|
Order::STATUS_PAID => OrderCenterQuery::STATUS_COMPLETED,
|
||||||
*/
|
Order::STATUS_CANCELLED, Order::STATUS_EXPIRED => OrderCenterQuery::STATUS_CLOSED,
|
||||||
public function pay_order(int $user_id, string $order_number, string $payment_method = ''): array {
|
default => OrderCenterQuery::STATUS_CLOSED,
|
||||||
if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) {
|
};
|
||||||
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume via shared gateway stack (any product_type; shop removed).
|
private function status_label(string $group): string {
|
||||||
$repo = OrderRepository::get_instance();
|
return match ($group) {
|
||||||
$order = $repo->get_user_order($user_id, $order_number);
|
OrderCenterQuery::STATUS_PENDING => __('待支付', 'aksrt'),
|
||||||
if (!$order) {
|
OrderCenterQuery::STATUS_COMPLETED => __('权益已生效', 'aksrt'),
|
||||||
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
default => __('已关闭', 'aksrt'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
$status = (string) ($order['status'] ?? '');
|
/** @param array<string, mixed> $meta */
|
||||||
if (!in_array($status, [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) {
|
private function resolve_title(array $meta, string $type, int $product_id, string $fallback): string {
|
||||||
return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$stored_method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
|
||||||
$payment_method = sanitize_key($payment_method ?: ($stored_method ?: 'balance'));
|
|
||||||
if ($stored_method !== '' && $payment_method !== $stored_method) {
|
|
||||||
return ['success' => false, 'message' => __('支付方式与订单不一致,请重新下单。', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$gateway_manager = class_exists(Gateway::class) ? Gateway::get_instance() : null;
|
|
||||||
$gateway = $gateway_manager ? $gateway_manager->get_gateway($payment_method) : null;
|
|
||||||
if (!$gateway) {
|
|
||||||
return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')];
|
|
||||||
}
|
|
||||||
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
|
|
||||||
return ['success' => false, 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
|
|
||||||
$order['payment_method'] = $payment_method;
|
|
||||||
|
|
||||||
$payment_result = PaymentOrchestrator::pay($gateway, $order, [
|
|
||||||
'paid_note' => __('用户在前台完成支付。', 'aksrt'),
|
|
||||||
]);
|
|
||||||
if (empty($payment_result['success'])) {
|
|
||||||
return ['success' => false, 'message' => $payment_result['message']];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($payment_result['redirect'])) {
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'data' => [
|
|
||||||
'redirect' => true,
|
|
||||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
|
||||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
|
||||||
'order_number' => $order_number,
|
|
||||||
'message' => $payment_result['message'],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'data' => [
|
|
||||||
'order' => $this->get_order_detail_view($user_id, $order_number),
|
|
||||||
'message' => $payment_result['message'],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function empty_stats(): array {
|
|
||||||
return ['total' => 0, 'pending' => 0, 'paid' => 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $order
|
|
||||||
* @param array<string, mixed> $meta
|
|
||||||
*/
|
|
||||||
private function resolve_order_title(array $order, array $meta, string $product_type, int $product_id, string $type_label): string {
|
|
||||||
foreach (['product_title', 'title', 'package_title'] as $key) {
|
foreach (['product_title', 'title', 'package_title'] as $key) {
|
||||||
if (!empty($meta[$key]) && is_string($meta[$key])) {
|
if (!empty($meta[$key]) && is_string($meta[$key])) {
|
||||||
return (string) $meta[$key];
|
return (string) $meta[$key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($type === DomainModel::ORDER_PRODUCT_VIP) {
|
||||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) {
|
|
||||||
$pkg = (string) ($meta['package_title'] ?? '');
|
|
||||||
if ($pkg !== '') {
|
|
||||||
return $pkg;
|
|
||||||
}
|
|
||||||
return __('VIP 会员', 'aksrt');
|
return __('VIP 会员', 'aksrt');
|
||||||
}
|
}
|
||||||
|
if ($type === DomainModel::ORDER_PRODUCT_RECHARGE) {
|
||||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) {
|
return __('余额充值', 'aksrt');
|
||||||
return (string) ($meta['title'] ?? __('余额充值', 'aksrt'));
|
|
||||||
}
|
}
|
||||||
|
if ($type === DomainModel::ORDER_PRODUCT_POINTS_BUY) {
|
||||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY) {
|
return __('购买积分', 'aksrt');
|
||||||
return (string) ($meta['title'] ?? __('购买积分', 'aksrt'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($product_id > 0) {
|
if ($product_id > 0) {
|
||||||
$post_title = get_the_title($product_id);
|
$title = get_the_title($product_id);
|
||||||
if (is_string($post_title) && $post_title !== '') {
|
if (is_string($title) && $title !== '') {
|
||||||
return $post_title;
|
return $title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
return $type_label;
|
/** @param array<string, mixed> $meta */
|
||||||
}
|
private function resolve_image(array $meta, int $product_id): string {
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $order
|
|
||||||
* @param array<string, mixed> $meta
|
|
||||||
*/
|
|
||||||
private function resolve_order_image(array $order, array $meta, string $product_type, int $product_id): string {
|
|
||||||
if (!empty($meta['thumb']) && is_string($meta['thumb'])) {
|
if (!empty($meta['thumb']) && is_string($meta['thumb'])) {
|
||||||
return esc_url_raw((string) $meta['thumb']);
|
return esc_url_raw($meta['thumb']);
|
||||||
}
|
}
|
||||||
if ($product_id > 0 && has_post_thumbnail($product_id)) {
|
if ($product_id > 0 && has_post_thumbnail($product_id)) {
|
||||||
$url = get_the_post_thumbnail_url($product_id, 'thumbnail');
|
return (string) get_the_post_thumbnail_url($product_id, 'thumbnail');
|
||||||
if (is_string($url) && $url !== '') {
|
|
||||||
return $url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @param array<string, mixed> $meta */
|
||||||
* @param array<string, mixed> $meta
|
private function resolve_entitlement_url(string $type, int $product_id, array $meta): string {
|
||||||
*/
|
if ($type === DomainModel::ORDER_PRODUCT_VIP) {
|
||||||
private function resolve_order_permalink(string $product_type, int $product_id, array $meta): string {
|
|
||||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) {
|
|
||||||
return function_exists('aksrt_get_user_center_tab_url')
|
|
||||||
? aksrt_get_user_center_tab_url('vip')
|
|
||||||
: '';
|
|
||||||
}
|
|
||||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE
|
|
||||||
|| $product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY) {
|
|
||||||
return function_exists('aksrt_get_user_center_tab_url')
|
|
||||||
? aksrt_get_user_center_tab_url('credits')
|
|
||||||
: '';
|
|
||||||
}
|
|
||||||
$post_id = $product_id > 0 ? $product_id : (int) ($meta['post_id'] ?? 0);
|
|
||||||
if ($post_id > 0) {
|
|
||||||
$link = get_permalink($post_id);
|
|
||||||
return is_string($link) ? $link : '';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolve_secondary_url(string $product_type): string {
|
|
||||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) {
|
|
||||||
return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('vip') : '';
|
return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('vip') : '';
|
||||||
}
|
}
|
||||||
if (in_array($product_type, [
|
if (in_array($type, [DomainModel::ORDER_PRODUCT_RECHARGE, DomainModel::ORDER_PRODUCT_POINTS_BUY], true)) {
|
||||||
\AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE,
|
|
||||||
\AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY,
|
|
||||||
], true)) {
|
|
||||||
return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('credits') : '';
|
return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('credits') : '';
|
||||||
}
|
}
|
||||||
return '';
|
$post_id = $product_id ?: (int) ($meta['post_id'] ?? 0);
|
||||||
|
return $post_id > 0 ? (string) get_permalink($post_id) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolve_secondary_label(string $product_type): string {
|
private function entitlement_label(string $type): string {
|
||||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) {
|
return match ($type) {
|
||||||
return __('我的会员', 'aksrt');
|
DomainModel::ORDER_PRODUCT_VIP => __('查看会员权益', 'aksrt'),
|
||||||
}
|
DomainModel::ORDER_PRODUCT_RECHARGE => __('查看余额', 'aksrt'),
|
||||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) {
|
DomainModel::ORDER_PRODUCT_POINTS_BUY => __('查看积分', 'aksrt'),
|
||||||
return __('我的余额', 'aksrt');
|
default => __('打开已购内容', 'aksrt'),
|
||||||
}
|
};
|
||||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY) {
|
|
||||||
return class_exists('\AKSRT\Settings\Settings')
|
|
||||||
? sprintf(__('我的%s', 'aksrt'), \AKSRT\Settings\Settings::get_currency_name())
|
|
||||||
: __('我的积分', 'aksrt');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function payment_method_label(string $method): string {
|
private function payment_method_label(string $method): string {
|
||||||
$method = sanitize_key($method);
|
$labels = [
|
||||||
$map = [
|
|
||||||
'balance' => __('站内余额', 'aksrt'),
|
'balance' => __('站内余额', 'aksrt'),
|
||||||
'points' => __('积分支付', 'aksrt'),
|
'points' => __('积分支付', 'aksrt'),
|
||||||
'alipay' => __('支付宝', 'aksrt'),
|
'alipay' => __('支付宝', 'aksrt'),
|
||||||
'wechatpay' => __('微信支付', 'aksrt'),
|
'wechatpay' => __('微信支付', 'aksrt'),
|
||||||
'wechat' => __('微信支付', 'aksrt'),
|
|
||||||
'paypal' => 'PayPal',
|
'paypal' => 'PayPal',
|
||||||
'stripe' => 'Stripe',
|
'stripe' => 'Stripe',
|
||||||
'epay_alipay' => __('易支付·支付宝', 'aksrt'),
|
|
||||||
'epay_wxpay' => __('易支付·微信', 'aksrt'),
|
|
||||||
];
|
];
|
||||||
return $map[$method] ?? ($method !== '' ? $method : __('—', 'aksrt'));
|
return $labels[$method] ?? ($method !== '' ? $method : __('—', 'aksrt'));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build chronological order progress for the detail modal.
|
|
||||||
*
|
|
||||||
* Driven by current status + available timestamps / related records so the
|
|
||||||
* chain advances even when historical rows lack paid_at/delivered_at.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $order
|
|
||||||
* @param array<int, array> $shipping_records
|
|
||||||
* @param array<int, array> $after_sale_records
|
|
||||||
* @return array<int, array{time:string,label:string,key?:string}>
|
|
||||||
*/
|
|
||||||
private function build_order_timeline(array $order, array $shipping_records, array $after_sale_records): array {
|
|
||||||
$status = sanitize_key((string) ($order['status'] ?? Order::STATUS_PENDING));
|
|
||||||
$created_at = $this->timeline_time((string) ($order['created_at'] ?? ''));
|
|
||||||
$paid_at = $this->timeline_time((string) ($order['paid_at'] ?? ''));
|
|
||||||
$delivered_at = $this->timeline_time((string) ($order['delivered_at'] ?? ''));
|
|
||||||
$completed_at = $this->timeline_time((string) ($order['completed_at'] ?? ''));
|
|
||||||
$cancelled_at = $this->timeline_time((string) ($order['cancelled_at'] ?? ''));
|
|
||||||
$refunded_at = $this->timeline_time((string) ($order['refunded_at'] ?? ''));
|
|
||||||
|
|
||||||
$shipping_created = '';
|
|
||||||
foreach ($shipping_records as $record) {
|
|
||||||
$t = $this->timeline_time((string) ($record['created_at'] ?? ''));
|
|
||||||
if ($t !== '' && ($shipping_created === '' || strcmp($t, $shipping_created) < 0)) {
|
|
||||||
$shipping_created = $t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($delivered_at === '' && $shipping_created !== '') {
|
|
||||||
$delivered_at = $shipping_created;
|
|
||||||
}
|
|
||||||
|
|
||||||
$paid_like = [
|
|
||||||
Order::STATUS_PAID,
|
|
||||||
Order::STATUS_DELIVERED,
|
|
||||||
Order::STATUS_COMPLETED,
|
|
||||||
Order::STATUS_PENDING_REVIEW,
|
|
||||||
Order::STATUS_REFUNDING,
|
|
||||||
Order::STATUS_REFUNDED,
|
|
||||||
];
|
|
||||||
$ship_like = [
|
|
||||||
Order::STATUS_DELIVERED,
|
|
||||||
Order::STATUS_COMPLETED,
|
|
||||||
Order::STATUS_PENDING_REVIEW,
|
|
||||||
];
|
|
||||||
$done_like = [
|
|
||||||
Order::STATUS_COMPLETED,
|
|
||||||
Order::STATUS_PENDING_REVIEW,
|
|
||||||
];
|
|
||||||
|
|
||||||
$timeline = [];
|
|
||||||
$push = static function (array &$out, string $key, string $label, string $time): void {
|
|
||||||
if ($label === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$out[] = [
|
|
||||||
'key' => $key,
|
|
||||||
'label' => $label,
|
|
||||||
'time' => $time,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1) Always: order created.
|
|
||||||
$push(
|
|
||||||
$timeline,
|
|
||||||
'created',
|
|
||||||
__('订单创建', 'aksrt'),
|
|
||||||
$created_at !== '' ? $created_at : ''
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2) Paid (status-driven; fall back to created_at when paid_at missing).
|
|
||||||
if ($paid_at !== '' || in_array($status, $paid_like, true)) {
|
|
||||||
$push(
|
|
||||||
$timeline,
|
|
||||||
'paid',
|
|
||||||
__('完成支付', 'aksrt'),
|
|
||||||
$paid_at !== '' ? $paid_at : $created_at
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Shipped / delivered — only when logistics exists or status reached it.
|
|
||||||
$has_shipping = $shipping_records !== [];
|
|
||||||
if (
|
|
||||||
$delivered_at !== ''
|
|
||||||
|| $has_shipping
|
|
||||||
|| in_array($status, $ship_like, true)
|
|
||||||
) {
|
|
||||||
// Virtual / content flows often jump paid → completed without logistics;
|
|
||||||
// only show the ship step when we have evidence of shipping.
|
|
||||||
if ($delivered_at !== '' || $has_shipping || $status === Order::STATUS_DELIVERED) {
|
|
||||||
$push(
|
|
||||||
$timeline,
|
|
||||||
'delivered',
|
|
||||||
__('订单发货', 'aksrt'),
|
|
||||||
$delivered_at !== '' ? $delivered_at : ($paid_at !== '' ? $paid_at : $created_at)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Completed / pending review.
|
|
||||||
if ($completed_at !== '' || in_array($status, $done_like, true)) {
|
|
||||||
$done_label = $status === Order::STATUS_PENDING_REVIEW
|
|
||||||
? __('待评价', 'aksrt')
|
|
||||||
: __('交易完成', 'aksrt');
|
|
||||||
$push(
|
|
||||||
$timeline,
|
|
||||||
'completed',
|
|
||||||
$done_label,
|
|
||||||
$completed_at !== ''
|
|
||||||
? $completed_at
|
|
||||||
: ($delivered_at !== '' ? $delivered_at : ($paid_at !== '' ? $paid_at : $created_at))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5) Terminal cancel / expire / refund.
|
|
||||||
if ($status === Order::STATUS_CANCELLED || $cancelled_at !== '') {
|
|
||||||
$push(
|
|
||||||
$timeline,
|
|
||||||
'cancelled',
|
|
||||||
__('订单取消', 'aksrt'),
|
|
||||||
$cancelled_at !== '' ? $cancelled_at : $created_at
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ($status === Order::STATUS_EXPIRED) {
|
|
||||||
$push(
|
|
||||||
$timeline,
|
|
||||||
'expired',
|
|
||||||
__('订单过期', 'aksrt'),
|
|
||||||
$cancelled_at !== '' ? $cancelled_at : $created_at
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ($status === Order::STATUS_REFUNDING) {
|
|
||||||
$push(
|
|
||||||
$timeline,
|
|
||||||
'refunding',
|
|
||||||
__('退款处理中', 'aksrt'),
|
|
||||||
$refunded_at !== '' ? $refunded_at : ($paid_at !== '' ? $paid_at : $created_at)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ($status === Order::STATUS_REFUNDED || $refunded_at !== '') {
|
|
||||||
$push(
|
|
||||||
$timeline,
|
|
||||||
'refunded',
|
|
||||||
__('完成退款', 'aksrt'),
|
|
||||||
$refunded_at !== '' ? $refunded_at : ($paid_at !== '' ? $paid_at : $created_at)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra logistics / after-sale events (detail lines).
|
|
||||||
foreach ($shipping_records as $record) {
|
|
||||||
$tracking = trim((string) ($record['tracking_no'] ?? ''));
|
|
||||||
$carrier = trim((string) ($record['carrier_label'] ?? $record['carrier'] ?? ''));
|
|
||||||
$status_l = trim((string) ($record['status_label'] ?? $record['status'] ?? ''));
|
|
||||||
$parts = array_filter([$carrier, $tracking, $status_l], static fn($v) => $v !== '');
|
|
||||||
$label = $parts !== []
|
|
||||||
? sprintf(__('物流更新:%s', 'aksrt'), implode(' / ', $parts))
|
|
||||||
: __('物流更新', 'aksrt');
|
|
||||||
$push(
|
|
||||||
$timeline,
|
|
||||||
'shipping',
|
|
||||||
$label,
|
|
||||||
$this->timeline_time((string) ($record['created_at'] ?? $record['updated_at'] ?? ''))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
foreach ($after_sale_records as $record) {
|
|
||||||
$reason = trim((string) ($record['reason'] ?? ''));
|
|
||||||
$type_l = trim((string) ($record['type_label'] ?? $record['type'] ?? ''));
|
|
||||||
$as_status = trim((string) ($record['status_label'] ?? $record['status'] ?? ''));
|
|
||||||
$parts = array_filter([$type_l, $as_status, $reason], static fn($v) => $v !== '');
|
|
||||||
$label = $parts !== []
|
|
||||||
? sprintf(__('售后:%s', 'aksrt'), implode(' — ', $parts))
|
|
||||||
: __('售后申请', 'aksrt');
|
|
||||||
$push(
|
|
||||||
$timeline,
|
|
||||||
'after_sale',
|
|
||||||
$label,
|
|
||||||
$this->timeline_time((string) ($record['created_at'] ?? $record['updated_at'] ?? ''))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($timeline, static function ($a, $b) {
|
|
||||||
$ta = (string) ($a['time'] ?? '');
|
|
||||||
$tb = (string) ($b['time'] ?? '');
|
|
||||||
if ($ta === $tb) {
|
|
||||||
// Keep canonical pipeline order when times collide / missing.
|
|
||||||
$rank = [
|
|
||||||
'created' => 10,
|
|
||||||
'paid' => 20,
|
|
||||||
'delivered' => 30,
|
|
||||||
'shipping' => 35,
|
|
||||||
'completed' => 40,
|
|
||||||
'refunding' => 50,
|
|
||||||
'refunded' => 60,
|
|
||||||
'cancelled' => 70,
|
|
||||||
'expired' => 80,
|
|
||||||
'after_sale'=> 90,
|
|
||||||
];
|
|
||||||
$ra = $rank[(string) ($a['key'] ?? '')] ?? 99;
|
|
||||||
$rb = $rank[(string) ($b['key'] ?? '')] ?? 99;
|
|
||||||
return $ra <=> $rb;
|
|
||||||
}
|
|
||||||
if ($ta === '') {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if ($tb === '') {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return strcmp($ta, $tb);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $timeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a mysql datetime for timeline display (empty when invalid/zero).
|
|
||||||
*/
|
|
||||||
private function timeline_time(string $value): string {
|
|
||||||
$value = trim($value);
|
|
||||||
if ($value === '' || str_starts_with($value, '0000-00-00')) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return $value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function get_order_countdown(string $expires_at, string $status): int {
|
private function get_order_countdown(string $expires_at, string $status): int {
|
||||||
if ($expires_at === '' || !in_array($status, [Order::STATUS_PENDING, Order::STATUS_EXPIRED, Order::STATUS_CREATED], true)) {
|
if (!in_array($status, [Order::STATUS_CREATED, Order::STATUS_PENDING], true) || $expires_at === '') {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
$left = strtotime($expires_at) - time();
|
$expires = strtotime($expires_at);
|
||||||
return max(0, (int) $left);
|
return $expires ? max(0, $expires - current_time('timestamp')) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @return array{success:bool,message?:string,data?:array} */
|
||||||
* Sum line quantities for display (shop multi-item or single meta item).
|
public function cancel_order(int $user_id, string $order_number): array {
|
||||||
*
|
if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) {
|
||||||
* @param array<int, array<string, mixed>> $order_items
|
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
||||||
* @param array<string, mixed> $meta
|
|
||||||
*/
|
|
||||||
private function resolve_order_quantity(array $order_items, array $meta): int {
|
|
||||||
$qty = 0;
|
|
||||||
$source = !empty($order_items) ? $order_items : (is_array($meta['items'] ?? null) ? $meta['items'] : []);
|
|
||||||
foreach ($source as $row) {
|
|
||||||
if (!is_array($row)) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
$qty += max(1, (int) ($row['quantity'] ?? $row['qty'] ?? 1));
|
$repo = OrderRepository::get_instance();
|
||||||
|
$order = $repo->get_user_order($user_id, $order_number);
|
||||||
|
if (!$order || $this->status_group((string) ($order['status'] ?? '')) !== OrderCenterQuery::STATUS_PENDING) {
|
||||||
|
return ['success' => false, 'message' => __('当前订单不可关闭。', 'aksrt')];
|
||||||
}
|
}
|
||||||
if ($qty <= 0) {
|
if (!$repo->update_status($order_number, Order::STATUS_CANCELLED, [
|
||||||
$qty = max(1, (int) ($meta['quantity'] ?? $meta['qty'] ?? 1));
|
'force' => true,
|
||||||
|
'note' => __('用户在前台关闭订单。', 'aksrt'),
|
||||||
|
])) {
|
||||||
|
return ['success' => false, 'message' => __('关闭订单失败,请稍后重试。', 'aksrt')];
|
||||||
}
|
}
|
||||||
return $qty;
|
return ['success' => true, 'data' => ['order' => $this->get_order_detail_view($user_id, $order_number)]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{success:bool,message?:string,data?:array} */
|
||||||
|
public function pay_order(int $user_id, string $order_number, string $payment_method = ''): array {
|
||||||
|
if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) {
|
||||||
|
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
||||||
|
}
|
||||||
|
$repo = OrderRepository::get_instance();
|
||||||
|
$order = $repo->get_user_order($user_id, $order_number);
|
||||||
|
if (!$order || $this->status_group((string) ($order['status'] ?? '')) !== OrderCenterQuery::STATUS_PENDING) {
|
||||||
|
return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')];
|
||||||
|
}
|
||||||
|
$stored_method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
||||||
|
$payment_method = sanitize_key($payment_method ?: ($stored_method ?: 'balance'));
|
||||||
|
if ($stored_method !== '' && $payment_method !== $stored_method) {
|
||||||
|
return ['success' => false, 'message' => __('支付方式与订单不一致,请重新下单。', 'aksrt')];
|
||||||
|
}
|
||||||
|
$manager = class_exists(Gateway::class) ? Gateway::get_instance() : null;
|
||||||
|
$gateway = $manager ? $manager->get_gateway($payment_method) : null;
|
||||||
|
if (!$gateway || (method_exists($gateway, 'is_configured') && !$gateway->is_configured())) {
|
||||||
|
return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')];
|
||||||
|
}
|
||||||
|
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
|
||||||
|
$order['payment_method'] = $payment_method;
|
||||||
|
$result = PaymentOrchestrator::pay($gateway, $order, ['paid_note' => __('用户在前台完成支付。', 'aksrt')]);
|
||||||
|
if (empty($result['success'])) {
|
||||||
|
return ['success' => false, 'message' => (string) ($result['message'] ?? __('支付失败。', 'aksrt'))];
|
||||||
|
}
|
||||||
|
return ['success' => true, 'data' => [
|
||||||
|
'redirect' => !empty($result['redirect']),
|
||||||
|
'payment_url' => (string) ($result['payment_url'] ?? ''),
|
||||||
|
'order_number' => $order_number,
|
||||||
|
'order' => empty($result['redirect']) ? $this->get_order_detail_view($user_id, $order_number) : null,
|
||||||
|
'message' => (string) ($result['message'] ?? ''),
|
||||||
|
]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,41 +3,29 @@
|
|||||||
* Template Name: AKSRT - Orders
|
* Template Name: AKSRT - Orders
|
||||||
* Template Post Type: page
|
* Template Post Type: page
|
||||||
*
|
*
|
||||||
* Legacy standalone orders URL — redirects into user center tab (zibll-style).
|
|
||||||
* Kept as template slug so old page assignments resolve; content lives in UC.
|
|
||||||
*
|
|
||||||
* @package AKSRT
|
* @package AKSRT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
if (!is_user_logged_in()) {
|
|
||||||
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(home_url('/profile/'));
|
|
||||||
wp_safe_redirect($login_url);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$target = function_exists('aksrt_get_user_center_url')
|
$target = function_exists('aksrt_get_user_center_url')
|
||||||
? aksrt_get_user_center_url('orders')
|
? aksrt_get_user_center_url('orders')
|
||||||
: home_url('/profile/?tab=orders');
|
: home_url('/profile/?tab=orders');
|
||||||
|
|
||||||
// Preserve filter/query from standalone page.
|
if (!is_user_logged_in()) {
|
||||||
$keep = [];
|
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url($target);
|
||||||
foreach (['order_tab', 'otab', 'order_status', 'order_page', 'order_type', 'section'] as $key) {
|
wp_safe_redirect($login_url);
|
||||||
if (isset($_GET[$key]) && (string) wp_unslash($_GET[$key]) !== '') {
|
exit;
|
||||||
$keep[$key] = sanitize_text_field(wp_unslash((string) $_GET[$key]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Legacy ?tab=pending on standalone page → order_tab.
|
|
||||||
if (isset($_GET['tab'])) {
|
|
||||||
$legacy_tab = sanitize_key((string) wp_unslash($_GET['tab']));
|
|
||||||
if ($legacy_tab !== '' && $legacy_tab !== 'orders' && !isset($keep['order_tab'])) {
|
|
||||||
$keep['order_tab'] = $legacy_tab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($keep !== []) {
|
|
||||||
$target = add_query_arg($keep, $target);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wp_safe_redirect($target, 302);
|
$raw = [
|
||||||
|
'status' => $_GET['order_status'] ?? '',
|
||||||
|
'type' => $_GET['order_type'] ?? '',
|
||||||
|
'order_number' => $_GET['order_number'] ?? '',
|
||||||
|
'page' => $_GET['order_page'] ?? 1,
|
||||||
|
];
|
||||||
|
$query = \AKSRT\Payment\OrderCenterQuery::from_array($raw);
|
||||||
|
$params = array_merge(['tab' => 'orders'], $query->url_values());
|
||||||
|
|
||||||
|
wp_safe_redirect(add_query_arg($params, $target), 302);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
Reference in New Issue
Block a user