修复支付状态问题
This commit is contained in:
@@ -42,7 +42,7 @@
|
|||||||
.aksrt-orders-page__stat span { color: var(--orders-muted); font-size: 11px; font-weight: 600; }
|
.aksrt-orders-page__stat span { color: var(--orders-muted); font-size: 11px; font-weight: 600; }
|
||||||
.aksrt-orders-page__stat strong { font-size: 21px; line-height: 1; font-variant-numeric: tabular-nums; }
|
.aksrt-orders-page__stat strong { font-size: 21px; line-height: 1; font-variant-numeric: tabular-nums; }
|
||||||
.aksrt-orders-page__stat strong.is-pending { color: var(--color-warning, #c87819); }
|
.aksrt-orders-page__stat strong.is-pending { color: var(--color-warning, #c87819); }
|
||||||
.aksrt-orders-page__stat strong.is-completed { color: var(--color-success, #168560); }
|
.aksrt-orders-page__stat strong.is-paid { color: var(--color-success, #168560); }
|
||||||
|
|
||||||
.aksrt-orders-page__search { display: flex; gap: 8px; align-items: center; margin: 20px 0 12px; padding: 7px; border: 1px solid var(--orders-line); border-radius: 12px; background: var(--surface-1, #fff); }
|
.aksrt-orders-page__search { display: flex; gap: 8px; align-items: center; margin: 20px 0 12px; padding: 7px; border: 1px solid var(--orders-line); border-radius: 12px; background: var(--surface-1, #fff); }
|
||||||
.aksrt-orders-page__search > i { margin-left: 7px; color: var(--orders-muted); font-size: 13px; }
|
.aksrt-orders-page__search > i { margin-left: 7px; color: var(--orders-muted); font-size: 13px; }
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
.aksrt-orders-page__card-rail time { margin-left: auto; font-variant-numeric: tabular-nums; }
|
.aksrt-orders-page__card-rail time { margin-left: auto; font-variant-numeric: tabular-nums; }
|
||||||
.aksrt-orders-page__card-status { font-weight: 700; }
|
.aksrt-orders-page__card-status { font-weight: 700; }
|
||||||
.aksrt-orders-page__card-status.is-pending { color: var(--color-warning, #c87819); }
|
.aksrt-orders-page__card-status.is-pending { color: var(--color-warning, #c87819); }
|
||||||
.aksrt-orders-page__card-status.is-completed { color: var(--color-success, #168560); }
|
.aksrt-orders-page__card-status.is-paid { color: var(--color-success, #168560); }
|
||||||
.aksrt-orders-page__card-main { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 13px; align-items: center; padding: 14px; }
|
.aksrt-orders-page__card-main { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 13px; align-items: center; padding: 14px; }
|
||||||
.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 52px; height: 52px; border-radius: 10px; object-fit: cover; }
|
.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 52px; height: 52px; border-radius: 10px; object-fit: cover; }
|
||||||
.aksrt-orders-page__card-art { display: grid; place-items: center; color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-2, #f4f6f9)); }
|
.aksrt-orders-page__card-art { display: grid; place-items: center; color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-2, #f4f6f9)); }
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
.aksrt-orders-page__empty h3 { margin: 0 0 7px; font-size: 15px; }.aksrt-orders-page__empty p { margin: 0 0 16px; color: var(--orders-muted); font-size: 13px; }
|
.aksrt-orders-page__empty h3 { margin: 0 0 7px; font-size: 15px; }.aksrt-orders-page__empty p { margin: 0 0 16px; color: var(--orders-muted); font-size: 13px; }
|
||||||
|
|
||||||
.aksrt-order-detail__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 7px; padding: 12px 16px; border-top: 1px solid var(--orders-line); background: var(--surface-2, #f7f8fb); }
|
.aksrt-order-detail__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 7px; padding: 12px 16px; border-top: 1px solid var(--orders-line); background: var(--surface-2, #f7f8fb); }
|
||||||
.aksrt-order-detail__hero { display: flex; align-items: center; justify-content: space-between; padding: 15px; border-radius: 11px; background: color-mix(in srgb, var(--color-primary, #3b82f6) 9%, var(--surface-2, #f7f8fb)); }.aksrt-order-detail__hero.is-pending { background: color-mix(in srgb, var(--color-warning, #c87819) 11%, var(--surface-2, #f7f8fb)); }.aksrt-order-detail__hero.is-completed { background: color-mix(in srgb, var(--color-success, #168560) 10%, var(--surface-2, #f7f8fb)); }.aksrt-order-detail__status-pill { font-size: 12px; font-weight: 700; }.aksrt-order-detail__hero-amount { font-size: 23px; font-variant-numeric: tabular-nums; }.aksrt-order-detail__product { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 11px; align-items: center; }.aksrt-order-detail__thumb { width: 56px; height: 56px; border-radius: 10px; object-fit: cover; }.aksrt-order-detail__thumb--empty { display: grid; place-items: center; background: var(--surface-2, #f4f6f9); color: var(--color-primary, #3b82f6); }.aksrt-order-detail__product-title { display: block; line-height: 1.4; }.aksrt-order-detail__product-meta { display: block; margin-top: 5px; color: var(--orders-muted); font-size: 12px; }.aksrt-order-detail__list { margin: 0; border: 1px solid var(--orders-line); border-radius: 10px; overflow: hidden; }.aksrt-order-detail__list > div { display: grid; grid-template-columns: 86px minmax(0, 1fr); padding: 10px 12px; border-bottom: 1px solid var(--orders-line); font-size: 12px; }.aksrt-order-detail__list > div:last-child { border-bottom: 0; }.aksrt-order-detail__list dt { color: var(--orders-muted); }.aksrt-order-detail__list dd { margin: 0; overflow-wrap: anywhere; text-align: right; font-variant-numeric: tabular-nums; }
|
.aksrt-order-detail__hero { display: flex; align-items: center; justify-content: space-between; padding: 15px; border-radius: 11px; background: color-mix(in srgb, var(--color-primary, #3b82f6) 9%, var(--surface-2, #f7f8fb)); }.aksrt-order-detail__hero.is-pending { background: color-mix(in srgb, var(--color-warning, #c87819) 11%, var(--surface-2, #f7f8fb)); }.aksrt-order-detail__hero.is-paid { background: color-mix(in srgb, var(--color-success, #168560) 10%, var(--surface-2, #f7f8fb)); }.aksrt-order-detail__status-pill { font-size: 12px; font-weight: 700; }.aksrt-order-detail__hero-amount { font-size: 23px; font-variant-numeric: tabular-nums; }.aksrt-order-detail__product { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 11px; align-items: center; }.aksrt-order-detail__thumb { width: 56px; height: 56px; border-radius: 10px; object-fit: cover; }.aksrt-order-detail__thumb--empty { display: grid; place-items: center; background: var(--surface-2, #f4f6f9); color: var(--color-primary, #3b82f6); }.aksrt-order-detail__product-title { display: block; line-height: 1.4; }.aksrt-order-detail__product-meta { display: block; margin-top: 5px; color: var(--orders-muted); font-size: 12px; }.aksrt-order-detail__list { margin: 0; border: 1px solid var(--orders-line); border-radius: 10px; overflow: hidden; }.aksrt-order-detail__list > div { display: grid; grid-template-columns: 86px minmax(0, 1fr); padding: 10px 12px; border-bottom: 1px solid var(--orders-line); font-size: 12px; }.aksrt-order-detail__list > div:last-child { border-bottom: 0; }.aksrt-order-detail__list dt { color: var(--orders-muted); }.aksrt-order-detail__list dd { margin: 0; overflow-wrap: anywhere; text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
|
||||||
/* Orders extensions */
|
/* Orders extensions */
|
||||||
.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
.aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
||||||
|
|||||||
@@ -196,7 +196,7 @@
|
|||||||
if (!root) return;
|
if (!root) return;
|
||||||
root.innerHTML = '<div class="aksrt-orders-page__stat"><span>全部</span><strong>' + Number(stats.total || 0) + "</strong></div>" +
|
root.innerHTML = '<div class="aksrt-orders-page__stat"><span>全部</span><strong>' + Number(stats.total || 0) + "</strong></div>" +
|
||||||
'<div class="aksrt-orders-page__stat"><span>待支付</span><strong class="is-pending">' + Number(stats.pending || 0) + "</strong></div>" +
|
'<div class="aksrt-orders-page__stat"><span>待支付</span><strong class="is-pending">' + Number(stats.pending || 0) + "</strong></div>" +
|
||||||
'<div class="aksrt-orders-page__stat"><span>已生效</span><strong class="is-completed">' + Number(stats.completed || 0) + "</strong></div>";
|
'<div class="aksrt-orders-page__stat"><span>已支付</span><strong class="is-paid">' + Number(stats.paid || 0) + "</strong></div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPagination(state, view) {
|
function renderPagination(state, view) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page
|
|||||||
'total_pages' => 1,
|
'total_pages' => 1,
|
||||||
'page' => 1,
|
'page' => 1,
|
||||||
'query' => [],
|
'query' => [],
|
||||||
'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0],
|
'stats' => ['total' => 0, 'pending' => 0, 'paid' => 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())
|
||||||
: [],
|
: [],
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ $orders_url = static function (array $override = []) use ($status, $type, $order
|
|||||||
$tabs = [
|
$tabs = [
|
||||||
'all' => __('全部', 'aksrt'),
|
'all' => __('全部', 'aksrt'),
|
||||||
'pending' => __('待支付', 'aksrt'),
|
'pending' => __('待支付', 'aksrt'),
|
||||||
'completed' => __('已完成', 'aksrt'),
|
'paid' => __('已支付', 'aksrt'),
|
||||||
|
'refunding' => __('退款中', 'aksrt'),
|
||||||
|
'refunded' => __('已退款', 'aksrt'),
|
||||||
'closed' => __('已关闭', 'aksrt'),
|
'closed' => __('已关闭', 'aksrt'),
|
||||||
];
|
];
|
||||||
?>
|
?>
|
||||||
@@ -72,7 +74,7 @@ $tabs = [
|
|||||||
<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"><span><?php esc_html_e('全部', 'aksrt'); ?></span><strong><?php echo esc_html((string) (int) ($stats['total'] ?? 0)); ?></strong></div>
|
<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>
|
||||||
<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>
|
<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>
|
||||||
<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 class="aksrt-orders-page__stat"><span><?php esc_html_e('已支付', 'aksrt'); ?></span><strong class="is-paid"><?php echo esc_html((string) (int) ($stats['paid'] ?? 0)); ?></strong></div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class PayOrders {
|
|||||||
? \AKSRT\Core\DomainModel::order_status_labels()
|
? \AKSRT\Core\DomainModel::order_status_labels()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Type tabs (legacy order subsubsub parity).
|
// Type tabs.
|
||||||
$type_tabs = [
|
$type_tabs = [
|
||||||
'' => __('全部类型', 'aksrt'),
|
'' => __('全部类型', 'aksrt'),
|
||||||
'resource' => __('内容付费', 'aksrt'),
|
'resource' => __('内容付费', 'aksrt'),
|
||||||
@@ -177,9 +177,9 @@ class PayOrders {
|
|||||||
}
|
}
|
||||||
echo '</form>';
|
echo '</form>';
|
||||||
|
|
||||||
// Hygiene actions (legacy clear_order / delete).
|
// Closed-order cleanup.
|
||||||
echo '<p style="margin:8px 0 12px;">';
|
echo '<p style="margin:8px 0 12px;">';
|
||||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('将删除 14 天前所有已关闭/已过期订单,不可恢复。确认清理?', 'aksrt')) . '\');">';
|
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('将删除 14 天前所有已关闭订单,不可恢复。确认清理?', 'aksrt')) . '\');">';
|
||||||
echo '<input type="hidden" name="action" value="aksrt_pay_clear_closed_orders" />';
|
echo '<input type="hidden" name="action" value="aksrt_pay_clear_closed_orders" />';
|
||||||
echo '<input type="hidden" name="days" value="14" />';
|
echo '<input type="hidden" name="days" value="14" />';
|
||||||
wp_nonce_field('aksrt_pay_clear_closed_orders');
|
wp_nonce_field('aksrt_pay_clear_closed_orders');
|
||||||
@@ -219,23 +219,7 @@ class PayOrders {
|
|||||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
||||||
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '">' . esc_html__('详情', 'aksrt') . '</a>';
|
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '">' . esc_html__('详情', 'aksrt') . '</a>';
|
||||||
$this->render_order_quick_status_buttons($order_number, $status_key);
|
$this->render_order_quick_status_buttons($order_number, $status_key);
|
||||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;align-items:center;">';
|
if ($status_key === 'closed') {
|
||||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_order_status" />';
|
|
||||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
|
||||||
wp_nonce_field('aksrt_pay_update_order_status_' . $order_number);
|
|
||||||
echo '<select name="new_status">';
|
|
||||||
foreach ($labels as $key => $label) {
|
|
||||||
printf(
|
|
||||||
'<option value="%s"%s>%s</option>',
|
|
||||||
esc_attr($key),
|
|
||||||
selected($status_key, $key, false),
|
|
||||||
esc_html($label)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
echo '</select>';
|
|
||||||
submit_button(__('更新', 'aksrt'), 'small', '', false);
|
|
||||||
echo '</form>';
|
|
||||||
if (in_array($status_key, ['cancelled', 'expired'], true)) {
|
|
||||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
|
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
|
||||||
echo '<input type="hidden" name="action" value="aksrt_pay_delete_order" />';
|
echo '<input type="hidden" name="action" value="aksrt_pay_delete_order" />';
|
||||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||||
@@ -325,18 +309,6 @@ class PayOrders {
|
|||||||
echo '<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px;">';
|
echo '<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px;">';
|
||||||
$this->render_order_quick_status_buttons($order_number, $status, true);
|
$this->render_order_quick_status_buttons($order_number, $status, true);
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:8px;align-items:center;">';
|
|
||||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_order_status" />';
|
|
||||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
|
||||||
echo '<input type="hidden" name="redirect_order" value="' . esc_attr($order_number) . '" />';
|
|
||||||
wp_nonce_field('aksrt_pay_update_order_status_' . $order_number);
|
|
||||||
echo '<select name="new_status">';
|
|
||||||
foreach ($labels as $key => $label) {
|
|
||||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
|
||||||
}
|
|
||||||
echo '</select> ';
|
|
||||||
submit_button(__('保存状态', 'aksrt'), 'primary', '', false);
|
|
||||||
echo '</form>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle_update_order_status(): void {
|
public function handle_update_order_status(): void {
|
||||||
@@ -350,7 +322,7 @@ class PayOrders {
|
|||||||
|
|
||||||
$ok = false;
|
$ok = false;
|
||||||
if ($order_number !== '' && $new_status !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
if ($order_number !== '' && $new_status !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||||
$ok = \AKSRT\Payment\OrderRepository::get_instance()->update_status($order_number, $new_status, ['force' => true]);
|
$ok = \AKSRT\Payment\OrderRepository::get_instance()->update_status($order_number, $new_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
$args = ['aksrt_pay_msg' => $ok ? 'order_updated' : 'order_failed'];
|
$args = ['aksrt_pay_msg' => $ok ? 'order_updated' : 'order_failed'];
|
||||||
@@ -411,8 +383,8 @@ class PayOrders {
|
|||||||
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
||||||
$status = is_array($order) ? (string) ($order['status'] ?? '') : '';
|
$status = is_array($order) ? (string) ($order['status'] ?? '') : '';
|
||||||
// Only allow hard-delete of closed/expired rows (safety).
|
// Only allow hard-delete of closed rows (safety).
|
||||||
if (in_array($status, ['cancelled', 'expired'], true)) {
|
if ($status === 'closed') {
|
||||||
$ok = \AKSRT\Payment\OrderRepository::get_instance()->delete_order($order_number);
|
$ok = \AKSRT\Payment\OrderRepository::get_instance()->delete_order($order_number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,23 +395,18 @@ class PayOrders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function render_order_quick_status_buttons(string $order_number, string $current_status, bool $redirect_detail = false): void {
|
private function render_order_quick_status_buttons(string $order_number, string $current_status, bool $redirect_detail = false): void {
|
||||||
$actions = [
|
$labels = [
|
||||||
'completed' => __('完成', 'aksrt'),
|
'paid' => __('确认收款', 'aksrt'),
|
||||||
'refunded' => __('退款', 'aksrt'),
|
'refunding' => __('发起退款', 'aksrt'),
|
||||||
'cancelled' => __('关闭', 'aksrt'),
|
'refunded' => __('确认退款', 'aksrt'),
|
||||||
|
'closed' => __('关闭订单', 'aksrt'),
|
||||||
];
|
];
|
||||||
foreach ($actions as $status => $label) {
|
$transitions = \AKSRT\Core\DomainModel::order_transitions();
|
||||||
if ($status === $current_status) {
|
foreach ((array) ($transitions[$current_status] ?? []) as $status) {
|
||||||
continue;
|
$label = $status === 'paid' && $current_status === 'refunding'
|
||||||
}
|
? __('取消退款', 'aksrt')
|
||||||
$confirm = '';
|
: ($labels[$status] ?? $status);
|
||||||
if ($status === 'refunded') {
|
$confirm = sprintf(__('确认将订单状态改为“%s”?', 'aksrt'), $label);
|
||||||
$confirm = __('确认将该订单标记为已退款?', 'aksrt');
|
|
||||||
} elseif ($status === 'cancelled') {
|
|
||||||
$confirm = __('确认关闭该订单?', 'aksrt');
|
|
||||||
} elseif ($status === 'completed') {
|
|
||||||
$confirm = __('确认完成该订单?', 'aksrt');
|
|
||||||
}
|
|
||||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;">';
|
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;">';
|
||||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_order_status" />';
|
echo '<input type="hidden" name="action" value="aksrt_pay_update_order_status" />';
|
||||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||||
@@ -500,7 +467,7 @@ class PayOrders {
|
|||||||
),
|
),
|
||||||
'orders_clear_empty' => __('没有可清理的关闭订单。', 'aksrt'),
|
'orders_clear_empty' => __('没有可清理的关闭订单。', 'aksrt'),
|
||||||
'order_deleted' => __('订单已删除。', 'aksrt'),
|
'order_deleted' => __('订单已删除。', 'aksrt'),
|
||||||
'order_delete_failed' => __('订单删除失败(仅允许删除已关闭/过期订单)。', 'aksrt'),
|
'order_delete_failed' => __('订单删除失败(仅允许删除已关闭订单)。', 'aksrt'),
|
||||||
'shipping_updated' => __('物流状态已更新。', 'aksrt'),
|
'shipping_updated' => __('物流状态已更新。', 'aksrt'),
|
||||||
'shipping_failed' => __('物流状态更新失败。', 'aksrt'),
|
'shipping_failed' => __('物流状态更新失败。', 'aksrt'),
|
||||||
'shipping_created' => __('发货记录已创建。', 'aksrt'),
|
'shipping_created' => __('发货记录已创建。', 'aksrt'),
|
||||||
|
|||||||
@@ -57,26 +57,16 @@ trait PayOrders {
|
|||||||
if (is_wp_error($side_effects)) {
|
if (is_wp_error($side_effects)) {
|
||||||
return $side_effects;
|
return $side_effects;
|
||||||
}
|
}
|
||||||
if (class_exists('\AKSRT\Payment\OrderRepository')) {
|
|
||||||
$completed = \AKSRT\Payment\OrderRepository::get_instance()->update_status(
|
|
||||||
$order_number,
|
|
||||||
\AKSRT\Payment\Order::STATUS_COMPLETED,
|
|
||||||
['note' => __('内容权益已发放。', 'aksrt')]
|
|
||||||
);
|
|
||||||
if (!$completed) {
|
|
||||||
return new \WP_Error('aksrt_content_fulfillment_failed', __('内容权益发放失败。', 'aksrt'), ['status' => 500]);
|
|
||||||
}
|
|
||||||
if (class_exists('\AKSRT\Payment\CreatorRevenue')) {
|
if (class_exists('\AKSRT\Payment\CreatorRevenue')) {
|
||||||
$completed_order = array_merge($paid_order, [
|
$revenue_order = array_merge($paid_order, [
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
'status' => \AKSRT\Payment\Order::STATUS_COMPLETED,
|
'status' => \AKSRT\Payment\Order::STATUS_PAID,
|
||||||
]);
|
]);
|
||||||
$revenue_id = \AKSRT\Payment\CreatorRevenue::get_instance()->create_from_paid_order($completed_order);
|
$revenue_id = \AKSRT\Payment\CreatorRevenue::get_instance()->create_from_paid_order($revenue_order);
|
||||||
if ($revenue_id < 0) {
|
if ($revenue_id < 0) {
|
||||||
return new \WP_Error('aksrt_creator_revenue_conflict', __('同一订单的收益载荷不一致。', 'aksrt'), ['status' => 409]);
|
return new \WP_Error('aksrt_creator_revenue_conflict', __('同一订单的收益载荷不一致。', 'aksrt'), ['status' => 409]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +207,7 @@ trait PayOrders {
|
|||||||
|| (int) ($current['product_id'] ?? 0) !== $post_id
|
|| (int) ($current['product_id'] ?? 0) !== $post_id
|
||||||
|| (int) ($current['amount'] ?? 0) !== $amount
|
|| (int) ($current['amount'] ?? 0) !== $amount
|
||||||
|| (string) ($current['currency'] ?? '') !== 'points'
|
|| (string) ($current['currency'] ?? '') !== 'points'
|
||||||
|| !in_array((string) ($current['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_PENDING], true)
|
|| (string) ($current['status'] ?? '') !== \AKSRT\Payment\Order::STATUS_PENDING
|
||||||
) {
|
) {
|
||||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态或金额已变化。', 'aksrt'), ['status' => 409]);
|
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态或金额已变化。', 'aksrt'), ['status' => 409]);
|
||||||
}
|
}
|
||||||
@@ -390,7 +380,7 @@ trait PayOrders {
|
|||||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
|
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
|
||||||
}
|
}
|
||||||
$status = is_array($existing) ? (string) ($existing['status'] ?? '') : '';
|
$status = is_array($existing) ? (string) ($existing['status'] ?? '') : '';
|
||||||
if (!in_array($status, [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true)) {
|
if ($status !== \AKSRT\Payment\Order::STATUS_PAID) {
|
||||||
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
|
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
|
||||||
'payment_method' => 'free',
|
'payment_method' => 'free',
|
||||||
'payment_transaction_id' => 'free_' . $order_number,
|
'payment_transaction_id' => 'free_' . $order_number,
|
||||||
@@ -503,7 +493,7 @@ trait PayOrders {
|
|||||||
if ($payment_method === '') {
|
if ($payment_method === '') {
|
||||||
$payment_method = $user_id > 0 ? 'balance' : 'alipay';
|
$payment_method = $user_id > 0 ? 'balance' : 'alipay';
|
||||||
}
|
}
|
||||||
if ($can_resume && in_array((string) ($order['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true)) {
|
if ($can_resume && (string) ($order['status'] ?? '') === \AKSRT\Payment\Order::STATUS_PAID) {
|
||||||
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('order-payment', [$order_number]);
|
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('order-payment', [$order_number]);
|
||||||
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ trait VipServicePurchase {
|
|||||||
if ($order_number === '') {
|
if ($order_number === '') {
|
||||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||||
}
|
}
|
||||||
if (is_array($existing) && in_array((string) ($existing['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true)) {
|
if (is_array($existing) && (string) ($existing['status'] ?? '') === \AKSRT\Payment\Order::STATUS_PAID) {
|
||||||
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('order-payment', [$order_number]);
|
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('order-payment', [$order_number]);
|
||||||
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
||||||
return [
|
return [
|
||||||
@@ -610,16 +610,6 @@ trait VipServicePurchase {
|
|||||||
}
|
}
|
||||||
if (!$fulfilled) {
|
if (!$fulfilled) {
|
||||||
return new \WP_Error('aksrt_vip_fulfillment_failed', __('会员权益发放失败。', 'aksrt'), ['status' => 500]);
|
return new \WP_Error('aksrt_vip_fulfillment_failed', __('会员权益发放失败。', 'aksrt'), ['status' => 500]);
|
||||||
}
|
|
||||||
if (class_exists('\AKSRT\Payment\OrderRepository')) {
|
|
||||||
$completed = \AKSRT\Payment\OrderRepository::get_instance()->update_status(
|
|
||||||
$order_number,
|
|
||||||
\AKSRT\Payment\Order::STATUS_COMPLETED,
|
|
||||||
['note' => __('会员权益已发放。', 'aksrt')]
|
|
||||||
);
|
|
||||||
if (!$completed) {
|
|
||||||
return new \WP_Error('aksrt_vip_fulfillment_failed', __('会员订单完成失败。', 'aksrt'), ['status' => 500]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -828,7 +818,7 @@ trait VipServicePurchase {
|
|||||||
|| (int) ($current['product_id'] ?? 0) !== $package_id
|
|| (int) ($current['product_id'] ?? 0) !== $package_id
|
||||||
|| (int) ($current['amount'] ?? 0) !== $cost
|
|| (int) ($current['amount'] ?? 0) !== $cost
|
||||||
|| (string) ($current['currency'] ?? '') !== 'points'
|
|| (string) ($current['currency'] ?? '') !== 'points'
|
||||||
|| !in_array((string) ($current['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_PENDING], true)
|
|| (string) ($current['status'] ?? '') !== \AKSRT\Payment\Order::STATUS_PENDING
|
||||||
) {
|
) {
|
||||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态或金额已变化', 'aksrt'), ['status' => 409]);
|
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态或金额已变化', 'aksrt'), ['status' => 409]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,16 +50,11 @@ class DomainModel {
|
|||||||
public const PAY_TYPE_VIDEO = 'video';
|
public const PAY_TYPE_VIDEO = 'video';
|
||||||
public const PAY_TYPE_VIP = 'vip';
|
public const PAY_TYPE_VIP = 'vip';
|
||||||
|
|
||||||
public const ORDER_STATUS_CREATED = 'created';
|
|
||||||
public const ORDER_STATUS_PENDING = 'pending';
|
public const ORDER_STATUS_PENDING = 'pending';
|
||||||
public const ORDER_STATUS_PAID = 'paid';
|
public const ORDER_STATUS_PAID = 'paid';
|
||||||
public const ORDER_STATUS_DELIVERED = 'delivered';
|
|
||||||
public const ORDER_STATUS_COMPLETED = 'completed';
|
|
||||||
public const ORDER_STATUS_CANCELLED = 'cancelled';
|
|
||||||
public const ORDER_STATUS_EXPIRED = 'expired';
|
|
||||||
public const ORDER_STATUS_REFUNDING = 'refunding';
|
public const ORDER_STATUS_REFUNDING = 'refunding';
|
||||||
public const ORDER_STATUS_REFUNDED = 'refunded';
|
public const ORDER_STATUS_REFUNDED = 'refunded';
|
||||||
public const ORDER_STATUS_PENDING_REVIEW = 'pending_review';
|
public const ORDER_STATUS_CLOSED = 'closed';
|
||||||
|
|
||||||
public const SHIPPING_STATUS_PENDING = 0;
|
public const SHIPPING_STATUS_PENDING = 0;
|
||||||
public const SHIPPING_STATUS_SHIPPED = 1;
|
public const SHIPPING_STATUS_SHIPPED = 1;
|
||||||
@@ -390,16 +385,11 @@ class DomainModel {
|
|||||||
*/
|
*/
|
||||||
public static function order_status_labels(): array {
|
public static function order_status_labels(): array {
|
||||||
return [
|
return [
|
||||||
self::ORDER_STATUS_CREATED => __('已创建', 'aksrt'),
|
|
||||||
self::ORDER_STATUS_PENDING => __('待支付', 'aksrt'),
|
self::ORDER_STATUS_PENDING => __('待支付', 'aksrt'),
|
||||||
self::ORDER_STATUS_PAID => __('已支付', 'aksrt'),
|
self::ORDER_STATUS_PAID => __('已支付', 'aksrt'),
|
||||||
self::ORDER_STATUS_DELIVERED => __('已发货', 'aksrt'),
|
|
||||||
self::ORDER_STATUS_COMPLETED => __('已完成', 'aksrt'),
|
|
||||||
self::ORDER_STATUS_CANCELLED => __('已取消', 'aksrt'),
|
|
||||||
self::ORDER_STATUS_EXPIRED => __('已过期', 'aksrt'),
|
|
||||||
self::ORDER_STATUS_REFUNDING => __('退款中', 'aksrt'),
|
self::ORDER_STATUS_REFUNDING => __('退款中', 'aksrt'),
|
||||||
self::ORDER_STATUS_REFUNDED => __('已退款', 'aksrt'),
|
self::ORDER_STATUS_REFUNDED => __('已退款', 'aksrt'),
|
||||||
self::ORDER_STATUS_PENDING_REVIEW => __('待评价', 'aksrt'),
|
self::ORDER_STATUS_CLOSED => __('已关闭', 'aksrt'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,53 +398,30 @@ class DomainModel {
|
|||||||
*/
|
*/
|
||||||
public static function order_transitions(): array {
|
public static function order_transitions(): array {
|
||||||
return [
|
return [
|
||||||
self::ORDER_STATUS_CREATED => [
|
|
||||||
self::ORDER_STATUS_PENDING,
|
|
||||||
self::ORDER_STATUS_CANCELLED,
|
|
||||||
],
|
|
||||||
self::ORDER_STATUS_PENDING => [
|
self::ORDER_STATUS_PENDING => [
|
||||||
self::ORDER_STATUS_PAID,
|
self::ORDER_STATUS_PAID,
|
||||||
self::ORDER_STATUS_CANCELLED,
|
self::ORDER_STATUS_CLOSED,
|
||||||
self::ORDER_STATUS_EXPIRED,
|
|
||||||
],
|
],
|
||||||
self::ORDER_STATUS_PAID => [
|
self::ORDER_STATUS_PAID => [
|
||||||
self::ORDER_STATUS_DELIVERED,
|
|
||||||
self::ORDER_STATUS_COMPLETED,
|
|
||||||
self::ORDER_STATUS_REFUNDING,
|
self::ORDER_STATUS_REFUNDING,
|
||||||
],
|
],
|
||||||
self::ORDER_STATUS_DELIVERED => [
|
|
||||||
self::ORDER_STATUS_COMPLETED,
|
|
||||||
self::ORDER_STATUS_PENDING_REVIEW,
|
|
||||||
self::ORDER_STATUS_REFUNDING,
|
|
||||||
],
|
|
||||||
self::ORDER_STATUS_COMPLETED => [
|
|
||||||
self::ORDER_STATUS_PENDING_REVIEW,
|
|
||||||
self::ORDER_STATUS_REFUNDING,
|
|
||||||
],
|
|
||||||
self::ORDER_STATUS_CANCELLED => [],
|
|
||||||
self::ORDER_STATUS_EXPIRED => [
|
|
||||||
self::ORDER_STATUS_PENDING,
|
|
||||||
],
|
|
||||||
self::ORDER_STATUS_REFUNDING => [
|
self::ORDER_STATUS_REFUNDING => [
|
||||||
self::ORDER_STATUS_REFUNDED,
|
self::ORDER_STATUS_REFUNDED,
|
||||||
self::ORDER_STATUS_PAID,
|
self::ORDER_STATUS_PAID,
|
||||||
],
|
],
|
||||||
self::ORDER_STATUS_REFUNDED => [],
|
self::ORDER_STATUS_REFUNDED => [],
|
||||||
self::ORDER_STATUS_PENDING_REVIEW => [
|
self::ORDER_STATUS_CLOSED => [],
|
||||||
self::ORDER_STATUS_COMPLETED,
|
|
||||||
self::ORDER_STATUS_REFUNDING,
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function normalize_order_status(string $status, string $default = self::ORDER_STATUS_PENDING): string {
|
public static function normalize_order_status(string $status): string {
|
||||||
$status = sanitize_key($status);
|
$status = sanitize_key($status);
|
||||||
return array_key_exists($status, self::order_status_labels()) ? $status : $default;
|
return array_key_exists($status, self::order_status_labels()) ? $status : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function can_transition_order_status(string $from, string $to): bool {
|
public static function can_transition_order_status(string $from, string $to): bool {
|
||||||
$from = self::normalize_order_status($from, self::ORDER_STATUS_CREATED);
|
$from = sanitize_key($from);
|
||||||
$to = self::normalize_order_status($to);
|
$to = sanitize_key($to);
|
||||||
$transitions = self::order_transitions();
|
$transitions = self::order_transitions();
|
||||||
|
|
||||||
return isset($transitions[$from]) && in_array($to, $transitions[$from], true);
|
return isset($transitions[$from]) && in_array($to, $transitions[$from], true);
|
||||||
@@ -466,14 +433,11 @@ class DomainModel {
|
|||||||
public static function paid_order_statuses(): array {
|
public static function paid_order_statuses(): array {
|
||||||
return [
|
return [
|
||||||
self::ORDER_STATUS_PAID,
|
self::ORDER_STATUS_PAID,
|
||||||
self::ORDER_STATUS_DELIVERED,
|
|
||||||
self::ORDER_STATUS_COMPLETED,
|
|
||||||
self::ORDER_STATUS_PENDING_REVIEW,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function is_paid_order_status(string $status): bool {
|
public static function is_paid_order_status(string $status): bool {
|
||||||
return in_array(self::normalize_order_status($status), self::paid_order_statuses(), true);
|
return in_array(sanitize_key($status), self::paid_order_statuses(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -43,8 +43,7 @@ trait BalanceServiceCharge {
|
|||||||
$locked_payment_method = '';
|
$locked_payment_method = '';
|
||||||
$locked_transaction_id = '';
|
$locked_transaction_id = '';
|
||||||
$locked_paid_at = '';
|
$locked_paid_at = '';
|
||||||
$locked_completed_at = '';
|
$guard = function () use ($order_number, $user_id, $credit, $provider_transaction_id, &$locked_status, &$locked_note, &$locked_payment_method, &$locked_transaction_id, &$locked_paid_at) {
|
||||||
$guard = function () use ($order_number, $user_id, $credit, $provider_transaction_id, &$locked_status, &$locked_note, &$locked_payment_method, &$locked_transaction_id, &$locked_paid_at, &$locked_completed_at) {
|
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table = \AKSRT\Core\DBMigrator::table('order');
|
$table = \AKSRT\Core\DBMigrator::table('order');
|
||||||
$current = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE order_number = %s FOR UPDATE", $order_number), ARRAY_A);
|
$current = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE order_number = %s FOR UPDATE", $order_number), ARRAY_A);
|
||||||
@@ -52,10 +51,8 @@ trait BalanceServiceCharge {
|
|||||||
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
||||||
|| sanitize_key((string) ($current['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE
|
|| sanitize_key((string) ($current['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE
|
||||||
|| !in_array((string) ($current['status'] ?? ''), [
|
|| !in_array((string) ($current['status'] ?? ''), [
|
||||||
\AKSRT\Payment\Order::STATUS_CREATED,
|
|
||||||
\AKSRT\Payment\Order::STATUS_PENDING,
|
\AKSRT\Payment\Order::STATUS_PENDING,
|
||||||
\AKSRT\Payment\Order::STATUS_PAID,
|
\AKSRT\Payment\Order::STATUS_PAID,
|
||||||
\AKSRT\Payment\Order::STATUS_COMPLETED,
|
|
||||||
], true)
|
], true)
|
||||||
) {
|
) {
|
||||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态确认失败', 'aksrt'), ['status' => 409]);
|
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态确认失败', 'aksrt'), ['status' => 409]);
|
||||||
@@ -77,13 +74,12 @@ trait BalanceServiceCharge {
|
|||||||
$locked_payment_method = sanitize_key((string) ($current['payment_method'] ?? ''));
|
$locked_payment_method = sanitize_key((string) ($current['payment_method'] ?? ''));
|
||||||
$locked_transaction_id = sanitize_text_field((string) ($current['payment_transaction_id'] ?? ''));
|
$locked_transaction_id = sanitize_text_field((string) ($current['payment_transaction_id'] ?? ''));
|
||||||
$locked_paid_at = trim((string) ($current['paid_at'] ?? ''));
|
$locked_paid_at = trim((string) ($current['paid_at'] ?? ''));
|
||||||
$locked_completed_at = trim((string) ($current['completed_at'] ?? ''));
|
|
||||||
if ($provider_transaction_id !== '' && $locked_transaction_id !== '' && !hash_equals($locked_transaction_id, $provider_transaction_id)) {
|
if ($provider_transaction_id !== '' && $locked_transaction_id !== '' && !hash_equals($locked_transaction_id, $provider_transaction_id)) {
|
||||||
return new \WP_Error('aksrt_recharge_transaction_conflict', __('充值交易号不一致', 'aksrt'), ['status' => 409]);
|
return new \WP_Error('aksrt_recharge_transaction_conflict', __('充值交易号不一致', 'aksrt'), ['status' => 409]);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
$mutation = function () use ($order_number, $payment_method, $provider_transaction_id, $status_note, $credit, &$locked_status, &$locked_note, &$locked_payment_method, &$locked_transaction_id, &$locked_paid_at, &$locked_completed_at) {
|
$mutation = function () use ($order_number, $payment_method, $provider_transaction_id, $status_note, $credit, &$locked_status, &$locked_note, &$locked_payment_method, &$locked_transaction_id, &$locked_paid_at) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table = \AKSRT\Core\DBMigrator::table('order');
|
$table = \AKSRT\Core\DBMigrator::table('order');
|
||||||
$locked_note['ledger_credited'] = 1;
|
$locked_note['ledger_credited'] = 1;
|
||||||
@@ -93,13 +89,12 @@ trait BalanceServiceCharge {
|
|||||||
}
|
}
|
||||||
$now = current_time('mysql');
|
$now = current_time('mysql');
|
||||||
$updated = $wpdb->update($table, [
|
$updated = $wpdb->update($table, [
|
||||||
'status' => \AKSRT\Payment\Order::STATUS_COMPLETED,
|
'status' => \AKSRT\Payment\Order::STATUS_PAID,
|
||||||
'payment_method' => $payment_method !== '' ? $payment_method : $locked_payment_method,
|
'payment_method' => $payment_method !== '' ? $payment_method : $locked_payment_method,
|
||||||
'payment_transaction_id' => $provider_transaction_id !== '' ? $provider_transaction_id : $locked_transaction_id,
|
'payment_transaction_id' => $provider_transaction_id !== '' ? $provider_transaction_id : $locked_transaction_id,
|
||||||
'paid_at' => $locked_paid_at !== '' ? $locked_paid_at : $now,
|
'paid_at' => $locked_paid_at !== '' ? $locked_paid_at : $now,
|
||||||
'completed_at' => $locked_completed_at !== '' ? $locked_completed_at : $now,
|
|
||||||
'note' => wp_json_encode($locked_note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
'note' => wp_json_encode($locked_note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
], ['order_number' => $order_number, 'status' => $locked_status], ['%s', '%s', '%s', '%s', '%s', '%s'], ['%s', '%s']);
|
], ['order_number' => $order_number, 'status' => $locked_status], ['%s', '%s', '%s', '%s', '%s'], ['%s', '%s']);
|
||||||
return $updated !== false ? true : new \WP_Error('aksrt_order_confirm_failed', __('订单状态确认失败', 'aksrt'), ['status' => 409]);
|
return $updated !== false ? true : new \WP_Error('aksrt_order_confirm_failed', __('订单状态确认失败', 'aksrt'), ['status' => 409]);
|
||||||
};
|
};
|
||||||
$result = \AKSRT\Ledger\LedgerService::get_instance()->credit(
|
$result = \AKSRT\Ledger\LedgerService::get_instance()->credit(
|
||||||
@@ -126,7 +121,6 @@ trait BalanceServiceCharge {
|
|||||||
]);
|
]);
|
||||||
do_action('aksrt_order_status_changed', $order_number, \AKSRT\Payment\Order::STATUS_PAID, $paid_order);
|
do_action('aksrt_order_status_changed', $order_number, \AKSRT\Payment\Order::STATUS_PAID, $paid_order);
|
||||||
do_action('aksrt_order_paid', $order_number, $paid_order);
|
do_action('aksrt_order_paid', $order_number, $paid_order);
|
||||||
do_action('aksrt_order_status_changed', $order_number, \AKSRT\Payment\Order::STATUS_COMPLETED, array_merge($paid_order, ['status' => \AKSRT\Payment\Order::STATUS_COMPLETED]));
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -332,10 +326,7 @@ trait BalanceServiceCharge {
|
|||||||
if (!$order) {
|
if (!$order) {
|
||||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||||
}
|
}
|
||||||
if (is_array($existing) && in_array((string) ($order['status'] ?? ''), [
|
if (is_array($existing) && (string) ($order['status'] ?? '') === \AKSRT\Payment\Order::STATUS_PAID) {
|
||||||
\AKSRT\Payment\Order::STATUS_PAID,
|
|
||||||
\AKSRT\Payment\Order::STATUS_COMPLETED,
|
|
||||||
], true)) {
|
|
||||||
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('recharge', [$order_number]);
|
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('recharge', [$order_number]);
|
||||||
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
||||||
$transaction_id = (int) ($history['items'][0]['id'] ?? 0);
|
$transaction_id = (int) ($history['items'][0]['id'] ?? 0);
|
||||||
|
|||||||
@@ -390,11 +390,10 @@ trait BalanceServiceRest {
|
|||||||
'product_id' => 0,
|
'product_id' => 0,
|
||||||
'amount' => $amount->decimal(),
|
'amount' => $amount->decimal(),
|
||||||
'currency' => 'CNY',
|
'currency' => 'CNY',
|
||||||
'status' => \AKSRT\Payment\Order::STATUS_COMPLETED,
|
'status' => \AKSRT\Payment\Order::STATUS_PAID,
|
||||||
'payment_method' => 'balance',
|
'payment_method' => 'balance',
|
||||||
'payment_transaction_id' => 'ledger_' . $transaction_id,
|
'payment_transaction_id' => 'ledger_' . $transaction_id,
|
||||||
'paid_at' => current_time('mysql'),
|
'paid_at' => current_time('mysql'),
|
||||||
'completed_at' => current_time('mysql'),
|
|
||||||
'note' => is_string($note) ? $note : '',
|
'note' => is_string($note) ? $note : '',
|
||||||
'request_key' => $operation_key,
|
'request_key' => $operation_key,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class BalanceGateway implements GatewayInterface {
|
|||||||
if (!is_array($current)
|
if (!is_array($current)
|
||||||
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
||||||
|| $current_minor !== $amount_minor
|
|| $current_minor !== $amount_minor
|
||||||
|| !in_array((string) ($current['status'] ?? ''), [Order::STATUS_CREATED, Order::STATUS_PENDING], true)
|
|| (string) ($current['status'] ?? '') !== Order::STATUS_PENDING
|
||||||
) {
|
) {
|
||||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态或金额已变化', 'aksrt'), ['status' => 409]);
|
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态或金额已变化', 'aksrt'), ['status' => 409]);
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@ class BalanceGateway implements GatewayInterface {
|
|||||||
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
||||||
|| sanitize_key((string) ($current['payment_method'] ?? '')) !== 'balance'
|
|| sanitize_key((string) ($current['payment_method'] ?? '')) !== 'balance'
|
||||||
|| $order_minor !== $amount_minor
|
|| $order_minor !== $amount_minor
|
||||||
|| !in_array((string) ($current['status'] ?? ''), [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED, Order::STATUS_REFUNDING], true)
|
|| !in_array((string) ($current['status'] ?? ''), [Order::STATUS_PAID, Order::STATUS_REFUNDING], true)
|
||||||
) {
|
) {
|
||||||
return new \WP_Error('aksrt_order_refund_conflict', __('退款订单状态或金额已变化', 'aksrt'), ['status' => 409]);
|
return new \WP_Error('aksrt_order_refund_conflict', __('退款订单状态或金额已变化', 'aksrt'), ['status' => 409]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -652,7 +652,7 @@ class ChargeCard {
|
|||||||
&& sanitize_key((string) ($order['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP
|
&& sanitize_key((string) ($order['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP
|
||||||
&& (int) ($order['product_id'] ?? 0) === $card_id
|
&& (int) ($order['product_id'] ?? 0) === $card_id
|
||||||
&& sanitize_key((string) ($order['payment_method'] ?? '')) === 'card_pass'
|
&& sanitize_key((string) ($order['payment_method'] ?? '')) === 'card_pass'
|
||||||
&& in_array((string) ($order['status'] ?? ''), [Order::STATUS_PAID, Order::STATUS_COMPLETED], true)
|
&& (string) ($order['status'] ?? '') === Order::STATUS_PAID
|
||||||
&& (int) ($note['card_id'] ?? 0) === $card_id
|
&& (int) ($note['card_id'] ?? 0) === $card_id
|
||||||
&& sanitize_key((string) ($note['card_type'] ?? '')) === self::TYPE_VIP
|
&& sanitize_key((string) ($note['card_type'] ?? '')) === self::TYPE_VIP
|
||||||
&& (int) ($stored_meta['level'] ?? 0) === $level
|
&& (int) ($stored_meta['level'] ?? 0) === $level
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class Gateway {
|
|||||||
$provider_ref = sanitize_text_field((string) ($request->get_param('order')
|
$provider_ref = sanitize_text_field((string) ($request->get_param('order')
|
||||||
?: $request->get_param('out_trade_no')
|
?: $request->get_param('out_trade_no')
|
||||||
?: ''));
|
?: ''));
|
||||||
$this->redirect_to_orders($this->local_order_number($provider_ref), 'cancelled');
|
$this->redirect_to_orders($this->local_order_number($provider_ref), Order::STATUS_CLOSED);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ class Gateway {
|
|||||||
* @param array<string,mixed>|null $order
|
* @param array<string,mixed>|null $order
|
||||||
*/
|
*/
|
||||||
private function reconciliation_gateway(?array $order): ?GatewayInterface {
|
private function reconciliation_gateway(?array $order): ?GatewayInterface {
|
||||||
if (!is_array($order) || !in_array((string) ($order['status'] ?? ''), [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) {
|
if (!is_array($order) || (string) ($order['status'] ?? '') !== Order::STATUS_PENDING) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ class Gateway {
|
|||||||
|
|
||||||
// 幂等:仅同一交易号的重复回调可视为成功。不同交易号意味着
|
// 幂等:仅同一交易号的重复回调可视为成功。不同交易号意味着
|
||||||
// 用户可能重复付款,不能静默 ACK 为已正确同步。
|
// 用户可能重复付款,不能静默 ACK 为已正确同步。
|
||||||
if (in_array($current, ['paid', 'delivered', 'completed'], true)) {
|
if ($current === Order::STATUS_PAID) {
|
||||||
$stored_transaction = sanitize_text_field((string) ($order['payment_transaction_id'] ?? ''));
|
$stored_transaction = sanitize_text_field((string) ($order['payment_transaction_id'] ?? ''));
|
||||||
if (sanitize_key((string) ($order['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) {
|
if (sanitize_key((string) ($order['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) {
|
||||||
if ($stored_transaction !== '' && !hash_equals($stored_transaction, $transaction_id)) {
|
if ($stored_transaction !== '' && !hash_equals($stored_transaction, $transaction_id)) {
|
||||||
@@ -399,7 +399,7 @@ class Gateway {
|
|||||||
return $stored_transaction !== '' && hash_equals($stored_transaction, $transaction_id);
|
return $stored_transaction !== '' && hash_equals($stored_transaction, $transaction_id);
|
||||||
}
|
}
|
||||||
// 只允许从未支付态完成
|
// 只允许从未支付态完成
|
||||||
if (!in_array($current, ['created', 'pending'], true)) {
|
if ($current !== Order::STATUS_PENDING) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,16 +9,11 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
class Order {
|
class Order {
|
||||||
|
|
||||||
public const STATUS_CREATED = \AKSRT\Core\DomainModel::ORDER_STATUS_CREATED;
|
|
||||||
public const STATUS_PENDING = \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING;
|
public const STATUS_PENDING = \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING;
|
||||||
public const STATUS_PAID = \AKSRT\Core\DomainModel::ORDER_STATUS_PAID;
|
public const STATUS_PAID = \AKSRT\Core\DomainModel::ORDER_STATUS_PAID;
|
||||||
public const STATUS_DELIVERED = \AKSRT\Core\DomainModel::ORDER_STATUS_DELIVERED;
|
|
||||||
public const STATUS_COMPLETED = \AKSRT\Core\DomainModel::ORDER_STATUS_COMPLETED;
|
|
||||||
public const STATUS_CANCELLED = \AKSRT\Core\DomainModel::ORDER_STATUS_CANCELLED;
|
|
||||||
public const STATUS_EXPIRED = \AKSRT\Core\DomainModel::ORDER_STATUS_EXPIRED;
|
|
||||||
public const STATUS_REFUNDING = \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDING;
|
public const STATUS_REFUNDING = \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDING;
|
||||||
public const STATUS_REFUNDED = \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED;
|
public const STATUS_REFUNDED = \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED;
|
||||||
public const STATUS_PENDING_REVIEW = \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING_REVIEW;
|
public const STATUS_CLOSED = \AKSRT\Core\DomainModel::ORDER_STATUS_CLOSED;
|
||||||
|
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
@@ -122,7 +117,7 @@ class Order {
|
|||||||
wp_send_json_error(['message' => __('参数无效', 'aksrt')]);
|
wp_send_json_error(['message' => __('参数无效', 'aksrt')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->update_status($order_number, $new_status, ['note' => $note, 'force' => true])) {
|
if ($this->update_status($order_number, $new_status, ['note' => $note])) {
|
||||||
wp_send_json_success(['message' => __('订单状态已更新', 'aksrt')]);
|
wp_send_json_success(['message' => __('订单状态已更新', 'aksrt')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,17 @@ final class OrderCenterQuery {
|
|||||||
|
|
||||||
public const STATUS_ALL = 'all';
|
public const STATUS_ALL = 'all';
|
||||||
public const STATUS_PENDING = 'pending';
|
public const STATUS_PENDING = 'pending';
|
||||||
public const STATUS_COMPLETED = 'completed';
|
public const STATUS_PAID = 'paid';
|
||||||
|
public const STATUS_REFUNDING = 'refunding';
|
||||||
|
public const STATUS_REFUNDED = 'refunded';
|
||||||
public const STATUS_CLOSED = 'closed';
|
public const STATUS_CLOSED = 'closed';
|
||||||
|
|
||||||
private const STATUS_GROUPS = [
|
private const STATUS_GROUPS = [
|
||||||
self::STATUS_ALL,
|
self::STATUS_ALL,
|
||||||
self::STATUS_PENDING,
|
self::STATUS_PENDING,
|
||||||
self::STATUS_COMPLETED,
|
self::STATUS_PAID,
|
||||||
|
self::STATUS_REFUNDING,
|
||||||
|
self::STATUS_REFUNDED,
|
||||||
self::STATUS_CLOSED,
|
self::STATUS_CLOSED,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -87,33 +91,10 @@ final class OrderCenterQuery {
|
|||||||
|
|
||||||
/** @return string[] */
|
/** @return string[] */
|
||||||
public function database_statuses(): array {
|
public function database_statuses(): array {
|
||||||
return match ($this->status()) {
|
if ($this->status() === self::STATUS_ALL) {
|
||||||
self::STATUS_PENDING => [Order::STATUS_CREATED, Order::STATUS_PENDING],
|
return array_keys(DomainModel::order_status_labels());
|
||||||
self::STATUS_COMPLETED => [
|
}
|
||||||
Order::STATUS_PAID,
|
return [$this->status()];
|
||||||
Order::STATUS_DELIVERED,
|
|
||||||
Order::STATUS_COMPLETED,
|
|
||||||
Order::STATUS_PENDING_REVIEW,
|
|
||||||
],
|
|
||||||
self::STATUS_CLOSED => [
|
|
||||||
Order::STATUS_CANCELLED,
|
|
||||||
Order::STATUS_EXPIRED,
|
|
||||||
Order::STATUS_REFUNDING,
|
|
||||||
Order::STATUS_REFUNDED,
|
|
||||||
],
|
|
||||||
default => [
|
|
||||||
Order::STATUS_CREATED,
|
|
||||||
Order::STATUS_PENDING,
|
|
||||||
Order::STATUS_PAID,
|
|
||||||
Order::STATUS_DELIVERED,
|
|
||||||
Order::STATUS_COMPLETED,
|
|
||||||
Order::STATUS_PENDING_REVIEW,
|
|
||||||
Order::STATUS_CANCELLED,
|
|
||||||
Order::STATUS_EXPIRED,
|
|
||||||
Order::STATUS_REFUNDING,
|
|
||||||
Order::STATUS_REFUNDED,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, scalar> */
|
/** @return array<string, scalar> */
|
||||||
|
|||||||
@@ -342,9 +342,13 @@ trait OrderRepositoryAdmin {
|
|||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||||
if ($status !== '' && $status !== 'all') {
|
$valid_statuses = array_keys(\AKSRT\Core\DomainModel::order_status_labels());
|
||||||
|
if ($status !== '' && $status !== 'all' && in_array($status, $valid_statuses, true)) {
|
||||||
$where[] = 'status = %s';
|
$where[] = 'status = %s';
|
||||||
$params[] = $status;
|
$params[] = $status;
|
||||||
|
} else {
|
||||||
|
$where[] = 'status IN (' . implode(', ', array_fill(0, count($valid_statuses), '%s')) . ')';
|
||||||
|
array_push($params, ...$valid_statuses);
|
||||||
}
|
}
|
||||||
|
|
||||||
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ trait OrderRepositoryCore {
|
|||||||
if ($amount === null) {
|
if ($amount === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
$status = array_key_exists('status', $data)
|
||||||
|
? \AKSRT\Core\DomainModel::normalize_order_status((string) $data['status'])
|
||||||
|
: \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING;
|
||||||
|
if ($status === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$row = [
|
$row = [
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
@@ -54,22 +60,21 @@ trait OrderRepositoryCore {
|
|||||||
'product_id' => (int) ($data['product_id'] ?? 0),
|
'product_id' => (int) ($data['product_id'] ?? 0),
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
'currency' => $currency,
|
'currency' => $currency,
|
||||||
'status' => \AKSRT\Core\DomainModel::normalize_order_status((string) ($data['status'] ?? \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING)),
|
'status' => $status,
|
||||||
'payment_method' => sanitize_key((string) ($data['payment_method'] ?? '')),
|
'payment_method' => sanitize_key((string) ($data['payment_method'] ?? '')),
|
||||||
'payment_transaction_id' => sanitize_text_field((string) ($data['payment_transaction_id'] ?? '')),
|
'payment_transaction_id' => sanitize_text_field((string) ($data['payment_transaction_id'] ?? '')),
|
||||||
'created_at' => $data['created_at'] ?? current_time('mysql'),
|
'created_at' => $data['created_at'] ?? current_time('mysql'),
|
||||||
'paid_at' => $data['paid_at'] ?? null,
|
'paid_at' => $data['paid_at'] ?? null,
|
||||||
'completed_at' => $data['completed_at'] ?? null,
|
|
||||||
'note' => $note,
|
'note' => $note,
|
||||||
];
|
];
|
||||||
$formats = ['%s', '%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s'];
|
$formats = ['%s', '%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s'];
|
||||||
|
|
||||||
// Pending orders always receive an expiry. Explicit caller values remain
|
// Pending orders always receive an expiry. Callers may provide a shorter
|
||||||
// supported for compatibility, while new flows use the shared runtime policy.
|
// expiry; otherwise the shared runtime policy supplies it.
|
||||||
if (array_key_exists('expires_at', $data) && $data['expires_at'] !== null && $data['expires_at'] !== '') {
|
if (array_key_exists('expires_at', $data) && $data['expires_at'] !== null && $data['expires_at'] !== '') {
|
||||||
$row['expires_at'] = (string) $data['expires_at'];
|
$row['expires_at'] = (string) $data['expires_at'];
|
||||||
$formats[] = '%s';
|
$formats[] = '%s';
|
||||||
} elseif (in_array($row['status'], [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) {
|
} elseif ($row['status'] === Order::STATUS_PENDING) {
|
||||||
$row['expires_at'] = \AKSRT\Settings\PaymentRuntime::get_order_expiry_at();
|
$row['expires_at'] = \AKSRT\Settings\PaymentRuntime::get_order_expiry_at();
|
||||||
$formats[] = '%s';
|
$formats[] = '%s';
|
||||||
}
|
}
|
||||||
@@ -189,7 +194,7 @@ trait OrderRepositoryCore {
|
|||||||
* their existing user-wide semantics, so changing a filter does not change the
|
* their existing user-wide semantics, so changing a filter does not change the
|
||||||
* summary cards. The subquery avoids four independent COUNT statements.
|
* summary cards. The subquery avoids four independent COUNT statements.
|
||||||
*
|
*
|
||||||
* @return array{total:int,stats:array{total:int,pending:int,completed:int}}
|
* @return array{total:int,stats:array{total:int,pending:int,paid:int}}
|
||||||
*/
|
*/
|
||||||
public function get_user_order_center_summary(int $user_id, array $args = []): array {
|
public function get_user_order_center_summary(int $user_id, array $args = []): array {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
@@ -197,29 +202,25 @@ trait OrderRepositoryCore {
|
|||||||
if ($user_id <= 0) {
|
if ($user_id <= 0) {
|
||||||
return [
|
return [
|
||||||
'total' => 0,
|
'total' => 0,
|
||||||
'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0],
|
'stats' => ['total' => 0, 'pending' => 0, 'paid' => 0],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->expire_pending_orders($user_id);
|
$this->expire_pending_orders($user_id);
|
||||||
|
|
||||||
$parts = $this->get_user_order_query_parts($user_id, $args);
|
$parts = $this->get_user_order_query_parts($user_id, $args);
|
||||||
$pending_statuses = [Order::STATUS_CREATED, Order::STATUS_PENDING];
|
|
||||||
$completed_statuses = [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW];
|
|
||||||
$completed_placeholders = implode(', ', array_fill(0, count($completed_statuses), '%s'));
|
|
||||||
$known_statuses = OrderCenterQuery::from_array([])->database_statuses();
|
$known_statuses = OrderCenterQuery::from_array([])->database_statuses();
|
||||||
$known_placeholders = implode(', ', array_fill(0, count($known_statuses), '%s'));
|
$known_placeholders = implode(', ', array_fill(0, count($known_statuses), '%s'));
|
||||||
$sql = "SELECT
|
$sql = "SELECT
|
||||||
(SELECT COUNT(*) FROM {$this->table()} {$parts['where']}) AS filtered_total,
|
(SELECT COUNT(*) FROM {$this->table()} {$parts['where']}) AS filtered_total,
|
||||||
COUNT(*) AS known_total,
|
COUNT(*) AS known_total,
|
||||||
SUM(CASE WHEN status IN (%s, %s) THEN 1 ELSE 0 END) AS pending,
|
SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) AS pending,
|
||||||
SUM(CASE WHEN status IN ({$completed_placeholders}) THEN 1 ELSE 0 END) AS completed
|
SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) AS paid
|
||||||
FROM {$this->table()}
|
FROM {$this->table()}
|
||||||
WHERE user_id = %d AND status IN ({$known_placeholders})";
|
WHERE user_id = %d AND status IN ({$known_placeholders})";
|
||||||
$params = array_merge(
|
$params = array_merge(
|
||||||
$parts['params'],
|
$parts['params'],
|
||||||
$pending_statuses,
|
[Order::STATUS_PENDING, Order::STATUS_PAID],
|
||||||
$completed_statuses,
|
|
||||||
[$user_id],
|
[$user_id],
|
||||||
$known_statuses
|
$known_statuses
|
||||||
);
|
);
|
||||||
@@ -230,7 +231,7 @@ trait OrderRepositoryCore {
|
|||||||
'stats' => [
|
'stats' => [
|
||||||
'total' => (int) ($row['known_total'] ?? 0),
|
'total' => (int) ($row['known_total'] ?? 0),
|
||||||
'pending' => (int) ($row['pending'] ?? 0),
|
'pending' => (int) ($row['pending'] ?? 0),
|
||||||
'completed' => (int) ($row['completed'] ?? 0),
|
'paid' => (int) ($row['paid'] ?? 0),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -244,14 +245,14 @@ trait OrderRepositoryCore {
|
|||||||
public function expire_pending_orders(?int $user_id = null): int {
|
public function expire_pending_orders(?int $user_id = null): int {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$where = "status IN ('created', 'pending') AND expires_at IS NOT NULL AND expires_at <= %s";
|
$where = "status = 'pending' AND expires_at IS NOT NULL AND expires_at <= %s";
|
||||||
$params = [gmdate('Y-m-d H:i:s')];
|
$params = [gmdate('Y-m-d H:i:s')];
|
||||||
if ($user_id !== null && $user_id > 0) {
|
if ($user_id !== null && $user_id > 0) {
|
||||||
$where .= ' AND user_id = %d';
|
$where .= ' AND user_id = %d';
|
||||||
$params[] = $user_id;
|
$params[] = $user_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql = "UPDATE {$this->table()} SET status = 'expired', cancelled_at = %s WHERE {$where}";
|
$sql = "UPDATE {$this->table()} SET status = 'closed', cancelled_at = %s WHERE {$where}";
|
||||||
array_unshift($params, gmdate('Y-m-d H:i:s'));
|
array_unshift($params, gmdate('Y-m-d H:i:s'));
|
||||||
$result = $wpdb->query($wpdb->prepare($sql, $params));
|
$result = $wpdb->query($wpdb->prepare($sql, $params));
|
||||||
|
|
||||||
@@ -262,7 +263,7 @@ trait OrderRepositoryCore {
|
|||||||
private function expire_order_if_due(array $order): array {
|
private function expire_order_if_due(array $order): array {
|
||||||
$status = sanitize_key((string) ($order['status'] ?? ''));
|
$status = sanitize_key((string) ($order['status'] ?? ''));
|
||||||
$expires_at = trim((string) ($order['expires_at'] ?? ''));
|
$expires_at = trim((string) ($order['expires_at'] ?? ''));
|
||||||
if (!in_array($status, [Order::STATUS_CREATED, Order::STATUS_PENDING], true) || $expires_at === '') {
|
if ($status !== Order::STATUS_PENDING || $expires_at === '') {
|
||||||
return $order;
|
return $order;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,13 +276,13 @@ trait OrderRepositoryCore {
|
|||||||
$closed_at = gmdate('Y-m-d H:i:s');
|
$closed_at = gmdate('Y-m-d H:i:s');
|
||||||
$updated = $wpdb->update(
|
$updated = $wpdb->update(
|
||||||
$this->table(),
|
$this->table(),
|
||||||
['status' => Order::STATUS_EXPIRED, 'cancelled_at' => $closed_at],
|
['status' => Order::STATUS_CLOSED, 'cancelled_at' => $closed_at],
|
||||||
['order_number' => (string) $order['order_number'], 'status' => $status],
|
['order_number' => (string) $order['order_number'], 'status' => $status],
|
||||||
['%s', '%s'],
|
['%s', '%s'],
|
||||||
['%s', '%s']
|
['%s', '%s']
|
||||||
);
|
);
|
||||||
if ($updated) {
|
if ($updated) {
|
||||||
$order['status'] = Order::STATUS_EXPIRED;
|
$order['status'] = Order::STATUS_CLOSED;
|
||||||
$order['cancelled_at'] = $closed_at;
|
$order['cancelled_at'] = $closed_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,10 +299,8 @@ trait OrderRepositoryCore {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
in_array($new_status, [
|
$new_status === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_PAID,
|
&& (string) ($preflight['status'] ?? '') !== \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDING
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_COMPLETED,
|
|
||||||
], true)
|
|
||||||
&& sanitize_key((string) ($preflight['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE
|
&& sanitize_key((string) ($preflight['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE
|
||||||
&& class_exists('\AKSRT\Credits\BalanceService')
|
&& class_exists('\AKSRT\Credits\BalanceService')
|
||||||
) {
|
) {
|
||||||
@@ -382,30 +381,24 @@ trait OrderRepositoryCore {
|
|||||||
$order = is_array($order_row) ? $this->normalize_row($order_row) : null;
|
$order = is_array($order_row) ? $this->normalize_row($order_row) : null;
|
||||||
if (!$order) return false;
|
if (!$order) return false;
|
||||||
|
|
||||||
$current_status = $order['status'] ?? 'created';
|
$current_status = (string) ($order['status'] ?? '');
|
||||||
|
|
||||||
if (empty($extra['force']) && !\AKSRT\Core\DomainModel::can_transition_order_status($current_status, $new_status)) {
|
if (!\AKSRT\Core\DomainModel::can_transition_order_status($current_status, $new_status)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$update_data = ['status' => $new_status];
|
$update_data = ['status' => $new_status];
|
||||||
$format = ['%s'];
|
$format = ['%s'];
|
||||||
|
|
||||||
// Stamp lifecycle timestamps. Some statuses share a column (e.g. expired → cancelled_at;
|
|
||||||
// pending_review still counts as a completion-like milestone for the timeline).
|
|
||||||
$time_fields = [
|
$time_fields = [
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_PAID => 'paid_at',
|
\AKSRT\Core\DomainModel::ORDER_STATUS_PAID => 'paid_at',
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_DELIVERED => 'delivered_at',
|
\AKSRT\Core\DomainModel::ORDER_STATUS_CLOSED => 'cancelled_at',
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_COMPLETED => 'completed_at',
|
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_PENDING_REVIEW => 'completed_at',
|
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_CANCELLED => 'cancelled_at',
|
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_EXPIRED => 'cancelled_at',
|
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED => 'refunded_at',
|
\AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED => 'refunded_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isset($time_fields[$new_status])) {
|
if (isset($time_fields[$new_status])) {
|
||||||
$field = $time_fields[$new_status];
|
$field = $time_fields[$new_status];
|
||||||
// Do not overwrite an existing timestamp (idempotent callbacks / force updates).
|
// Do not overwrite an existing timestamp on repeated callbacks.
|
||||||
$existing = is_string($order[$field] ?? null) ? trim((string) $order[$field]) : '';
|
$existing = is_string($order[$field] ?? null) ? trim((string) $order[$field]) : '';
|
||||||
if ($existing === '' || str_starts_with($existing, '0000-00-00')) {
|
if ($existing === '' || str_starts_with($existing, '0000-00-00')) {
|
||||||
$update_data[$field] = current_time('mysql');
|
$update_data[$field] = current_time('mysql');
|
||||||
@@ -413,13 +406,8 @@ trait OrderRepositoryCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When jumping past paid (force admin transitions), backfill paid_at so the
|
|
||||||
// progress chain is never stuck on "订单创建" alone.
|
|
||||||
if (
|
if (
|
||||||
in_array($new_status, [
|
in_array($new_status, [
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_DELIVERED,
|
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_COMPLETED,
|
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_PENDING_REVIEW,
|
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDING,
|
\AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDING,
|
||||||
\AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED,
|
\AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED,
|
||||||
], true)
|
], true)
|
||||||
@@ -487,7 +475,7 @@ trait OrderRepositoryCore {
|
|||||||
];
|
];
|
||||||
// 到账事件:让充值/VIP/商品履约等挂接(单一履约入口)。
|
// 到账事件:让充值/VIP/商品履约等挂接(单一履约入口)。
|
||||||
// 仅在「首次」转为 paid 时触发,避免回调多次到达重复履约。
|
// 仅在「首次」转为 paid 时触发,避免回调多次到达重复履约。
|
||||||
if ($new_status === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID && $current_status !== \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
|
if ($new_status === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID && $current_status === \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING) {
|
||||||
$paid_order = array_merge($order, [
|
$paid_order = array_merge($order, [
|
||||||
'status' => $new_status,
|
'status' => $new_status,
|
||||||
'payment_method' => $update_data['payment_method'] ?? ($order['payment_method'] ?? ''),
|
'payment_method' => $update_data['payment_method'] ?? ($order['payment_method'] ?? ''),
|
||||||
@@ -568,8 +556,7 @@ trait OrderRepositoryCore {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete closed (cancelled/expired) orders older than $days days.
|
* Delete closed orders older than $days days.
|
||||||
* Mirrors the legacy pay module clear_order(14).
|
|
||||||
*
|
*
|
||||||
* @return int Number of rows deleted.
|
* @return int Number of rows deleted.
|
||||||
*/
|
*/
|
||||||
@@ -579,10 +566,10 @@ trait OrderRepositoryCore {
|
|||||||
$days = max(1, min(3650, $days));
|
$days = max(1, min(3650, $days));
|
||||||
$table = $this->table();
|
$table = $this->table();
|
||||||
$cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
|
$cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
|
||||||
// Prefer cancelled_at when set; fall back to created_at for older rows.
|
|
||||||
$sql = "DELETE FROM {$table}
|
$sql = "DELETE FROM {$table}
|
||||||
WHERE status IN ('cancelled', 'expired')
|
WHERE status = 'closed'
|
||||||
AND COALESCE(cancelled_at, created_at) < %s";
|
AND cancelled_at IS NOT NULL
|
||||||
|
AND cancelled_at < %s";
|
||||||
$deleted = $wpdb->query($wpdb->prepare($sql, $cutoff));
|
$deleted = $wpdb->query($wpdb->prepare($sql, $cutoff));
|
||||||
return $deleted === false ? 0 : (int) $deleted;
|
return $deleted === false ? 0 : (int) $deleted;
|
||||||
}
|
}
|
||||||
@@ -612,10 +599,10 @@ trait OrderRepositoryCore {
|
|||||||
|
|
||||||
$this->expire_pending_orders($user_id > 0 ? $user_id : null);
|
$this->expire_pending_orders($user_id > 0 ? $user_id : null);
|
||||||
|
|
||||||
$where = '';
|
$where = "WHERE status IN ('pending', 'paid', 'refunding', 'refunded', 'closed')";
|
||||||
$params = [];
|
$params = [];
|
||||||
if ($user_id > 0) {
|
if ($user_id > 0) {
|
||||||
$where = 'WHERE user_id = %d';
|
$where .= ' AND user_id = %d';
|
||||||
$params[] = $user_id;
|
$params[] = $user_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,13 +610,10 @@ trait OrderRepositoryCore {
|
|||||||
COUNT(*) as total,
|
COUNT(*) as total,
|
||||||
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
||||||
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid,
|
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid,
|
||||||
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) as delivered,
|
|
||||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
|
||||||
SUM(CASE WHEN status = 'pending_review' THEN 1 ELSE 0 END) as pending_review,
|
|
||||||
SUM(CASE WHEN status = 'refunding' THEN 1 ELSE 0 END) as refunding,
|
SUM(CASE WHEN status = 'refunding' THEN 1 ELSE 0 END) as refunding,
|
||||||
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled,
|
|
||||||
SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) as refunded,
|
SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) as refunded,
|
||||||
SUM(CASE WHEN status IN ('paid','delivered','completed','pending_review') THEN amount ELSE 0 END) as revenue
|
SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) as closed,
|
||||||
|
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) as revenue
|
||||||
FROM {$this->table()} {$where}";
|
FROM {$this->table()} {$where}";
|
||||||
|
|
||||||
if (!empty($params)) {
|
if (!empty($params)) {
|
||||||
@@ -642,12 +626,9 @@ trait OrderRepositoryCore {
|
|||||||
'total' => (int) ($row['total'] ?? 0),
|
'total' => (int) ($row['total'] ?? 0),
|
||||||
'pending' => (int) ($row['pending'] ?? 0),
|
'pending' => (int) ($row['pending'] ?? 0),
|
||||||
'paid' => (int) ($row['paid'] ?? 0),
|
'paid' => (int) ($row['paid'] ?? 0),
|
||||||
'delivered' => (int) ($row['delivered'] ?? 0),
|
|
||||||
'completed' => (int) ($row['completed'] ?? 0),
|
|
||||||
'pending_review' => (int) ($row['pending_review'] ?? 0),
|
|
||||||
'refunding' => (int) ($row['refunding'] ?? 0),
|
'refunding' => (int) ($row['refunding'] ?? 0),
|
||||||
'cancelled' => (int) ($row['cancelled'] ?? 0),
|
|
||||||
'refunded' => (int) ($row['refunded'] ?? 0),
|
'refunded' => (int) ($row['refunded'] ?? 0),
|
||||||
|
'closed' => (int) ($row['closed'] ?? 0),
|
||||||
'revenue' => $this->cash_decimal((string) ($row['revenue'] ?? '0')),
|
'revenue' => $this->cash_decimal((string) ($row['revenue'] ?? '0')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -727,7 +708,7 @@ trait OrderRepositoryCore {
|
|||||||
if ($row['amount'] === null) {
|
if ($row['amount'] === null) {
|
||||||
$row['amount'] = (string) ($row['currency'] ?? '') === 'points' ? 0 : '0.00';
|
$row['amount'] = (string) ($row['currency'] ?? '') === 'points' ? 0 : '0.00';
|
||||||
}
|
}
|
||||||
$row['status'] = \AKSRT\Core\DomainModel::normalize_order_status((string) ($row['status'] ?? ''));
|
$row['status'] = sanitize_key((string) ($row['status'] ?? ''));
|
||||||
|
|
||||||
$row['meta_payload'] = [];
|
$row['meta_payload'] = [];
|
||||||
if (!empty($row['note']) && is_string($row['note'])) {
|
if (!empty($row['note']) && is_string($row['note'])) {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class PaymentOrchestrator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$stored_status = sanitize_key((string) ($stored['status'] ?? ''));
|
$stored_status = sanitize_key((string) ($stored['status'] ?? ''));
|
||||||
if (!in_array($stored_status, [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) {
|
if ($stored_status !== Order::STATUS_PENDING) {
|
||||||
return self::fail('aksrt_order_not_payable', __('当前订单状态不可支付。', 'aksrt'));
|
return self::fail('aksrt_order_not_payable', __('当前订单状态不可支付。', 'aksrt'));
|
||||||
}
|
}
|
||||||
$stored_method = sanitize_key((string) ($stored['payment_method'] ?? ''));
|
$stored_method = sanitize_key((string) ($stored['payment_method'] ?? ''));
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Revenue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create one pending creator-income record from a completed paid order.
|
* Create one pending creator-income record from a paid order.
|
||||||
*
|
*
|
||||||
* @param array<string,mixed> $order
|
* @param array<string,mixed> $order
|
||||||
*/
|
*/
|
||||||
@@ -35,7 +35,7 @@ class Revenue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$order_number = sanitize_text_field((string) ($order['order_number'] ?? ''));
|
$order_number = sanitize_text_field((string) ($order['order_number'] ?? ''));
|
||||||
if ($order_number === '' || (string) ($order['status'] ?? '') !== 'completed') {
|
if ($order_number === '' || (string) ($order['status'] ?? '') !== \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
$product_type = sanitize_key((string) ($order['product_type'] ?? ''));
|
$product_type = sanitize_key((string) ($order['product_type'] ?? ''));
|
||||||
|
|||||||
+11
-21
@@ -64,7 +64,7 @@ class UserOrders {
|
|||||||
'total_pages' => $total_pages,
|
'total_pages' => $total_pages,
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'query' => array_merge($query->url_values(), ['order_page' => $page]),
|
'query' => array_merge($query->url_values(), ['order_page' => $page]),
|
||||||
'stats' => is_array($summary['stats'] ?? null) ? $summary['stats'] : ['total' => 0, 'pending' => 0, 'completed' => 0],
|
'stats' => is_array($summary['stats'] ?? null) ? $summary['stats'] : ['total' => 0, 'pending' => 0, 'paid' => 0],
|
||||||
'invalidation_tags' => ['orders'],
|
'invalidation_tags' => ['orders'],
|
||||||
'product_types' => $this->product_type_filter_options(),
|
'product_types' => $this->product_type_filter_options(),
|
||||||
];
|
];
|
||||||
@@ -109,13 +109,13 @@ class UserOrders {
|
|||||||
/** @return array<string, int> */
|
/** @return array<string, 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 ['total' => 0, 'pending' => 0, 'completed' => 0];
|
return ['total' => 0, 'pending' => 0, 'paid' => 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
$summary = OrderRepository::get_instance()->get_user_order_center_summary($user_id);
|
$summary = OrderRepository::get_instance()->get_user_order_center_summary($user_id);
|
||||||
return is_array($summary['stats'] ?? null)
|
return is_array($summary['stats'] ?? null)
|
||||||
? $summary['stats']
|
? $summary['stats']
|
||||||
: ['total' => 0, 'pending' => 0, 'completed' => 0];
|
: ['total' => 0, 'pending' => 0, 'paid' => 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, string> */
|
/** @return array<string, string> */
|
||||||
@@ -192,7 +192,7 @@ class UserOrders {
|
|||||||
$payment_method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
$payment_method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
||||||
$can_pay = $group === OrderCenterQuery::STATUS_PENDING && $payment_method !== 'manual_review';
|
$can_pay = $group === OrderCenterQuery::STATUS_PENDING && $payment_method !== 'manual_review';
|
||||||
$can_cancel = $group === OrderCenterQuery::STATUS_PENDING;
|
$can_cancel = $group === OrderCenterQuery::STATUS_PENDING;
|
||||||
$can_open = $group === OrderCenterQuery::STATUS_COMPLETED && $permalink !== '';
|
$can_open = $group === OrderCenterQuery::STATUS_PAID && $permalink !== '';
|
||||||
$order_number = (string) ($order['order_number'] ?? '');
|
$order_number = (string) ($order['order_number'] ?? '');
|
||||||
$updated_at = $this->resolve_updated_at($order);
|
$updated_at = $this->resolve_updated_at($order);
|
||||||
|
|
||||||
@@ -249,7 +249,7 @@ class UserOrders {
|
|||||||
'total_pages' => 1,
|
'total_pages' => 1,
|
||||||
'page' => 1,
|
'page' => 1,
|
||||||
'query' => $query->url_values(),
|
'query' => $query->url_values(),
|
||||||
'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0],
|
'stats' => ['total' => 0, 'pending' => 0, 'paid' => 0],
|
||||||
'invalidation_tags' => ['orders'],
|
'invalidation_tags' => ['orders'],
|
||||||
'product_types' => $this->product_type_filter_options(),
|
'product_types' => $this->product_type_filter_options(),
|
||||||
];
|
];
|
||||||
@@ -259,7 +259,7 @@ class UserOrders {
|
|||||||
private function resolve_updated_at(array $order): string {
|
private function resolve_updated_at(array $order): string {
|
||||||
$latest = '';
|
$latest = '';
|
||||||
$latest_timestamp = 0;
|
$latest_timestamp = 0;
|
||||||
foreach (['created_at', 'paid_at', 'delivered_at', 'completed_at', 'cancelled_at', 'refunded_at'] as $field) {
|
foreach (['created_at', 'paid_at', 'cancelled_at', 'refunded_at'] as $field) {
|
||||||
$value = trim((string) ($order[$field] ?? ''));
|
$value = trim((string) ($order[$field] ?? ''));
|
||||||
if ($value === '') {
|
if ($value === '') {
|
||||||
continue;
|
continue;
|
||||||
@@ -284,20 +284,11 @@ class UserOrders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function status_group(string $status): string {
|
private function status_group(string $status): string {
|
||||||
return match ($status) {
|
return isset(DomainModel::order_status_labels()[$status]) ? $status : '';
|
||||||
Order::STATUS_CREATED, Order::STATUS_PENDING => OrderCenterQuery::STATUS_PENDING,
|
|
||||||
Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW => OrderCenterQuery::STATUS_COMPLETED,
|
|
||||||
Order::STATUS_CANCELLED, Order::STATUS_EXPIRED, Order::STATUS_REFUNDING, Order::STATUS_REFUNDED => OrderCenterQuery::STATUS_CLOSED,
|
|
||||||
default => OrderCenterQuery::STATUS_CLOSED,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function status_label(string $group): string {
|
private function status_label(string $group): string {
|
||||||
return match ($group) {
|
return DomainModel::order_status_labels()[$group] ?? __('未知状态', 'aksrt');
|
||||||
OrderCenterQuery::STATUS_PENDING => __('待支付', 'aksrt'),
|
|
||||||
OrderCenterQuery::STATUS_COMPLETED => __('权益已生效', 'aksrt'),
|
|
||||||
default => __('已关闭', 'aksrt'),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $meta */
|
/** @param array<string, mixed> $meta */
|
||||||
@@ -389,7 +380,7 @@ class UserOrders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function get_order_countdown(string $expires_at, string $status): int {
|
private function get_order_countdown(string $expires_at, string $status): int {
|
||||||
if (!in_array($status, [Order::STATUS_CREATED, Order::STATUS_PENDING], true) || $expires_at === '') {
|
if ($status !== Order::STATUS_PENDING || $expires_at === '') {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
$expires = strtotime($expires_at);
|
$expires = strtotime($expires_at);
|
||||||
@@ -406,8 +397,7 @@ class UserOrders {
|
|||||||
if (!$order || $this->status_group((string) ($order['status'] ?? '')) !== OrderCenterQuery::STATUS_PENDING) {
|
if (!$order || $this->status_group((string) ($order['status'] ?? '')) !== OrderCenterQuery::STATUS_PENDING) {
|
||||||
return ['success' => false, 'message' => __('当前订单不可关闭。', 'aksrt')];
|
return ['success' => false, 'message' => __('当前订单不可关闭。', 'aksrt')];
|
||||||
}
|
}
|
||||||
if (!$repo->update_status($order_number, Order::STATUS_CANCELLED, [
|
if (!$repo->update_status($order_number, Order::STATUS_CLOSED, [
|
||||||
'force' => true,
|
|
||||||
'note' => __('用户在前台关闭订单。', 'aksrt'),
|
'note' => __('用户在前台关闭订单。', 'aksrt'),
|
||||||
])) {
|
])) {
|
||||||
return ['success' => false, 'message' => __('关闭订单失败,请稍后重试。', 'aksrt')];
|
return ['success' => false, 'message' => __('关闭订单失败,请稍后重试。', 'aksrt')];
|
||||||
@@ -425,7 +415,7 @@ class UserOrders {
|
|||||||
if (!$order) {
|
if (!$order) {
|
||||||
return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')];
|
return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')];
|
||||||
}
|
}
|
||||||
if ($this->status_group((string) ($order['status'] ?? '')) === OrderCenterQuery::STATUS_COMPLETED) {
|
if ($this->status_group((string) ($order['status'] ?? '')) === OrderCenterQuery::STATUS_PAID) {
|
||||||
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('order-payment', [$order_number]);
|
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('order-payment', [$order_number]);
|
||||||
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
|
||||||
if (!empty($history['items'][0])) {
|
if (!empty($history['items'][0])) {
|
||||||
|
|||||||
@@ -171,9 +171,12 @@ test('admin ledger keeps cash minor values as decimal strings', () => {
|
|||||||
|
|
||||||
test('asset purchases share the order center and refresh every wallet surface', () => {
|
test('asset purchases share the order center and refresh every wallet surface', () => {
|
||||||
const domain = fs.readFileSync(path.join(root, 'src', 'Core', 'DomainModel.php'), 'utf8')
|
const domain = fs.readFileSync(path.join(root, 'src', 'Core', 'DomainModel.php'), 'utf8')
|
||||||
|
const order = fs.readFileSync(path.join(root, 'src', 'Payment', 'Order.php'), 'utf8')
|
||||||
const query = fs.readFileSync(path.join(root, 'src', 'Payment', 'OrderCenterQuery.php'), 'utf8')
|
const query = fs.readFileSync(path.join(root, 'src', 'Payment', 'OrderCenterQuery.php'), 'utf8')
|
||||||
const userOrders = fs.readFileSync(path.join(root, 'src', 'Payment', 'UserOrders.php'), 'utf8')
|
const userOrders = fs.readFileSync(path.join(root, 'src', 'Payment', 'UserOrders.php'), 'utf8')
|
||||||
const repository = fs.readFileSync(path.join(root, 'src', 'Payment', 'OrderRepositoryCore.php'), 'utf8')
|
const repository = fs.readFileSync(path.join(root, 'src', 'Payment', 'OrderRepositoryCore.php'), 'utf8')
|
||||||
|
const adminRepository = fs.readFileSync(path.join(root, 'src', 'Payment', 'OrderRepositoryAdmin.php'), 'utf8')
|
||||||
|
const payOrders = fs.readFileSync(path.join(root, 'src', 'Admin', 'Native', 'PayOrders.php'), 'utf8')
|
||||||
const balanceRest = fs.readFileSync(path.join(root, 'src', 'Credits', 'BalanceServiceRest.php'), 'utf8')
|
const balanceRest = fs.readFileSync(path.join(root, 'src', 'Credits', 'BalanceServiceRest.php'), 'utf8')
|
||||||
const chargeCard = fs.readFileSync(path.join(root, 'src', 'Payment', 'ChargeCard.php'), 'utf8')
|
const chargeCard = fs.readFileSync(path.join(root, 'src', 'Payment', 'ChargeCard.php'), 'utf8')
|
||||||
const creditsJs = fs.readFileSync(path.join(root, 'assets', 'js', 'credits.js'), 'utf8')
|
const creditsJs = fs.readFileSync(path.join(root, 'assets', 'js', 'credits.js'), 'utf8')
|
||||||
@@ -182,13 +185,44 @@ test('asset purchases share the order center and refresh every wallet surface',
|
|||||||
|
|
||||||
assert.match(domain, /ORDER_PRODUCT_POINTS\s*=\s*'points'/)
|
assert.match(domain, /ORDER_PRODUCT_POINTS\s*=\s*'points'/)
|
||||||
assert.match(domain, /self::ORDER_PRODUCT_POINTS\s*=>\s*__\('积分购买'/)
|
assert.match(domain, /self::ORDER_PRODUCT_POINTS\s*=>\s*__\('积分购买'/)
|
||||||
for (const status of ['STATUS_PAID', 'STATUS_DELIVERED', 'STATUS_COMPLETED', 'STATUS_PENDING_REVIEW']) {
|
const statuses = ['PENDING', 'PAID', 'REFUNDING', 'REFUNDED', 'CLOSED']
|
||||||
assert.match(query, new RegExp(`Order::${status}`))
|
assert.deepEqual(
|
||||||
|
[...domain.matchAll(/public const ORDER_STATUS_([A-Z_]+)\s*=\s*'([^']+)'/g)].map((match) => [match[1], match[2]]),
|
||||||
|
statuses.map((status) => [status, status.toLowerCase()]),
|
||||||
|
)
|
||||||
|
assert.deepEqual(
|
||||||
|
[...order.matchAll(/public const STATUS_([A-Z_]+)\s*=\s*\\AKSRT\\Core\\DomainModel::ORDER_STATUS_([A-Z_]+);/g)].map((match) => [match[1], match[2]]),
|
||||||
|
statuses.map((status) => [status, status]),
|
||||||
|
)
|
||||||
|
for (const status of statuses) {
|
||||||
|
assert.match(domain, new RegExp(`ORDER_STATUS_${status}\\s*=\\s*'${status.toLowerCase()}'`))
|
||||||
|
assert.match(order, new RegExp(`STATUS_${status}\\s*=\\s*\\\\AKSRT\\\\Core\\\\DomainModel::ORDER_STATUS_${status}`))
|
||||||
|
assert.match(query, new RegExp(`STATUS_${status}\\s*=\\s*'${status.toLowerCase()}'`))
|
||||||
}
|
}
|
||||||
assert.match(userOrders, /Order::STATUS_DELIVERED, Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW/)
|
for (const status of ['CREATED', 'DELIVERED', 'COMPLETED', 'PENDING_REVIEW', 'CANCELLED', 'EXPIRED']) {
|
||||||
assert.doesNotMatch(userOrders, /OrderCenterQuery::STATUS_PAID/)
|
assert.doesNotMatch(domain, new RegExp(`ORDER_STATUS_${status}`))
|
||||||
assert.match(repository, /\$completed_statuses = \[Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW\]/)
|
assert.doesNotMatch(order, new RegExp(`STATUS_${status}`))
|
||||||
assert.match(repository, /SUM\(CASE WHEN status IN \(\{\$completed_placeholders\}\) THEN 1 ELSE 0 END\) AS completed/)
|
}
|
||||||
|
const transitionSource = domain.slice(domain.indexOf('public static function order_transitions'), domain.indexOf('public static function normalize_order_status'))
|
||||||
|
const transitions = Object.fromEntries(
|
||||||
|
[...transitionSource.matchAll(/self::ORDER_STATUS_([A-Z_]+)\s*=>\s*\[([\s\S]*?)\],/g)].map((match) => [
|
||||||
|
match[1],
|
||||||
|
[...match[2].matchAll(/self::ORDER_STATUS_([A-Z_]+)/g)].map((target) => target[1]),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
assert.deepEqual(transitions, {
|
||||||
|
PENDING: ['PAID', 'CLOSED'],
|
||||||
|
PAID: ['REFUNDING'],
|
||||||
|
REFUNDING: ['REFUNDED', 'PAID'],
|
||||||
|
REFUNDED: [],
|
||||||
|
CLOSED: [],
|
||||||
|
})
|
||||||
|
assert.match(userOrders, /return isset\(DomainModel::order_status_labels\(\)\[\$status\]\) \? \$status : ''/)
|
||||||
|
assert.match(repository, /SUM\(CASE WHEN status = %s THEN 1 ELSE 0 END\) AS paid/)
|
||||||
|
assert.doesNotMatch(repository, /\$completed_statuses|\$completed_placeholders/)
|
||||||
|
assert.match(adminRepository, /\$valid_statuses = array_keys\(\\AKSRT\\Core\\DomainModel::order_status_labels\(\)\)/)
|
||||||
|
assert.doesNotMatch(payOrders, /<select name="new_status">/)
|
||||||
|
assert.doesNotMatch(`${order}\n${repository}\n${userOrders}\n${payOrders}`, /['"]force['"]\s*=>\s*true/)
|
||||||
|
|
||||||
assert.match(balanceRest, /register_rest_route\('aksrt\/v1', '\/pay\/assets'/)
|
assert.match(balanceRest, /register_rest_route\('aksrt\/v1', '\/pay\/assets'/)
|
||||||
assert.match(balanceRest, /'product_type'\s*=>\s*\\AKSRT\\Core\\DomainModel::ORDER_PRODUCT_POINTS/)
|
assert.match(balanceRest, /'product_type'\s*=>\s*\\AKSRT\\Core\\DomainModel::ORDER_PRODUCT_POINTS/)
|
||||||
@@ -210,8 +244,10 @@ test('recharge completion always credits the ledger and expiry SQL is strict-mod
|
|||||||
const charge = fs.readFileSync(path.join(root, 'src', 'Credits', 'BalanceServiceCharge.php'), 'utf8')
|
const charge = fs.readFileSync(path.join(root, 'src', 'Credits', 'BalanceServiceCharge.php'), 'utf8')
|
||||||
const gateway = fs.readFileSync(path.join(root, 'src', 'Payment', 'Gateway.php'), 'utf8')
|
const gateway = fs.readFileSync(path.join(root, 'src', 'Payment', 'Gateway.php'), 'utf8')
|
||||||
|
|
||||||
assert.match(repository, /in_array\(\$new_status,\s*\[\s*\\AKSRT\\Core\\DomainModel::ORDER_STATUS_PAID,\s*\\AKSRT\\Core\\DomainModel::ORDER_STATUS_COMPLETED,\s*\],\s*true\)/)
|
assert.match(repository, /\$new_status === \\AKSRT\\Core\\DomainModel::ORDER_STATUS_PAID/)
|
||||||
assert.match(charge, /Order::STATUS_CREATED,\s*\\AKSRT\\Payment\\Order::STATUS_PENDING,\s*\\AKSRT\\Payment\\Order::STATUS_PAID,\s*\\AKSRT\\Payment\\Order::STATUS_COMPLETED/)
|
assert.match(charge, /'status'\s*=>\s*\\AKSRT\\Payment\\Order::STATUS_PAID/)
|
||||||
|
assert.match(charge, /\\AKSRT\\Payment\\Order::STATUS_PENDING,\s*\\AKSRT\\Payment\\Order::STATUS_PAID/)
|
||||||
|
assert.doesNotMatch(charge, /Order::STATUS_(?:CREATED|COMPLETED)/)
|
||||||
assert.match(gateway, /ORDER_PRODUCT_RECHARGE[\s\S]*?update_status\(\$order_number, Order::STATUS_PAID/)
|
assert.match(gateway, /ORDER_PRODUCT_RECHARGE[\s\S]*?update_status\(\$order_number, Order::STATUS_PAID/)
|
||||||
assert.doesNotMatch(repository, /expires_at\s*<>\s*''/)
|
assert.doesNotMatch(repository, /expires_at\s*<>\s*''/)
|
||||||
assert.doesNotMatch(repository, /NULLIF\(cancelled_at,\s*''\)/)
|
assert.doesNotMatch(repository, /NULLIF\(cancelled_at,\s*''\)/)
|
||||||
|
|||||||
Reference in New Issue
Block a user