From 85a124b8863695474d50857db7719f9aa8bedc84 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 24 Aug 2026 17:21:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=94=AF=E4=BB=98=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/routes/orders.css | 6 +- assets/js/order-center.js | 2 +- inc/helpers/aksrt-transactions.php | 2 +- .../user-center-renderers/profile-orders.php | 6 +- src/Admin/Native/PayOrders.php | 71 +++--------- src/Content/PayOrders.php | 48 +++----- src/Content/VipServicePurchase.php | 28 ++--- src/Core/DomainModel.php | 54 ++------- src/Credits/BalanceServiceCharge.php | 19 +-- src/Credits/BalanceServiceRest.php | 3 +- src/Payment/BalanceGateway.php | 4 +- src/Payment/ChargeCard.php | 2 +- src/Payment/Gateway.php | 8 +- src/Payment/Order.php | 9 +- src/Payment/OrderCenterQuery.php | 39 ++----- src/Payment/OrderRepositoryAdmin.php | 6 +- src/Payment/OrderRepositoryCore.php | 109 ++++++++---------- src/Payment/PaymentOrchestrator.php | 2 +- src/Payment/Revenue.php | 4 +- src/Payment/UserOrders.php | 32 ++--- .../asset-ledger-contract.test.cjs | 52 +++++++-- 21 files changed, 198 insertions(+), 308 deletions(-) diff --git a/assets/css/routes/orders.css b/assets/css/routes/orders.css index 1d37cbcc..920236e5 100644 --- a/assets/css/routes/orders.css +++ b/assets/css/routes/orders.css @@ -42,7 +42,7 @@ .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.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 > 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-status { font-weight: 700; } .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-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)); } @@ -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-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 */ .aksrt-orders-page__extra-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; } diff --git a/assets/js/order-center.js b/assets/js/order-center.js index 9a3cdf48..53bf2eb2 100644 --- a/assets/js/order-center.js +++ b/assets/js/order-center.js @@ -196,7 +196,7 @@ if (!root) return; root.innerHTML = '
全部' + Number(stats.total || 0) + "
" + '
待支付' + Number(stats.pending || 0) + "
" + - '
已生效' + Number(stats.completed || 0) + "
"; + '
已支付' + Number(stats.paid || 0) + "
"; } function renderPagination(state, view) { diff --git a/inc/helpers/aksrt-transactions.php b/inc/helpers/aksrt-transactions.php index e5682cca..abd5faa9 100644 --- a/inc/helpers/aksrt-transactions.php +++ b/inc/helpers/aksrt-transactions.php @@ -24,7 +24,7 @@ function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page 'total_pages' => 1, 'page' => 1, 'query' => [], - 'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0], + 'stats' => ['total' => 0, 'pending' => 0, 'paid' => 0], 'product_types' => class_exists('\AKSRT\Core\DomainModel') ? array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels()) : [], diff --git a/inc/helpers/user-center-renderers/profile-orders.php b/inc/helpers/user-center-renderers/profile-orders.php index 301c1a56..44709a02 100644 --- a/inc/helpers/user-center-renderers/profile-orders.php +++ b/inc/helpers/user-center-renderers/profile-orders.php @@ -55,7 +55,9 @@ $orders_url = static function (array $override = []) use ($status, $type, $order $tabs = [ 'all' => __('全部', 'aksrt'), 'pending' => __('待支付', 'aksrt'), - 'completed' => __('已完成', 'aksrt'), + 'paid' => __('已支付', 'aksrt'), + 'refunding' => __('退款中', 'aksrt'), + 'refunded' => __('已退款', 'aksrt'), 'closed' => __('已关闭', 'aksrt'), ]; ?> @@ -72,7 +74,7 @@ $tabs = [
-
+
diff --git a/src/Admin/Native/PayOrders.php b/src/Admin/Native/PayOrders.php index 61648061..b17b173b 100644 --- a/src/Admin/Native/PayOrders.php +++ b/src/Admin/Native/PayOrders.php @@ -85,7 +85,7 @@ class PayOrders { ? \AKSRT\Core\DomainModel::order_status_labels() : []; - // Type tabs (legacy order subsubsub parity). + // Type tabs. $type_tabs = [ '' => __('全部类型', 'aksrt'), 'resource' => __('内容付费', 'aksrt'), @@ -177,9 +177,9 @@ class PayOrders { } echo ''; - // Hygiene actions (legacy clear_order / delete). + // Closed-order cleanup. echo '

'; - echo '

'; + echo ''; echo ''; echo ''; wp_nonce_field('aksrt_pay_clear_closed_orders'); @@ -219,23 +219,7 @@ class PayOrders { echo ''; echo '' . esc_html__('详情', 'aksrt') . ''; $this->render_order_quick_status_buttons($order_number, $status_key); - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_update_order_status_' . $order_number); - echo ''; - submit_button(__('更新', 'aksrt'), 'small', '', false); - echo '
'; - if (in_array($status_key, ['cancelled', 'expired'], true)) { + if ($status_key === 'closed') { echo '
'; echo ''; echo ''; @@ -325,18 +309,6 @@ class PayOrders { echo '
'; $this->render_order_quick_status_buttons($order_number, $status, true); echo '
'; - echo ''; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_update_order_status_' . $order_number); - echo ' '; - submit_button(__('保存状态', 'aksrt'), 'primary', '', false); - echo '
'; } public function handle_update_order_status(): void { @@ -350,7 +322,7 @@ class PayOrders { $ok = false; 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']; @@ -411,8 +383,8 @@ class PayOrders { if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) { $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); $status = is_array($order) ? (string) ($order['status'] ?? '') : ''; - // Only allow hard-delete of closed/expired rows (safety). - if (in_array($status, ['cancelled', 'expired'], true)) { + // Only allow hard-delete of closed rows (safety). + if ($status === 'closed') { $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 { - $actions = [ - 'completed' => __('完成', 'aksrt'), - 'refunded' => __('退款', 'aksrt'), - 'cancelled' => __('关闭', 'aksrt'), + $labels = [ + 'paid' => __('确认收款', 'aksrt'), + 'refunding' => __('发起退款', 'aksrt'), + 'refunded' => __('确认退款', 'aksrt'), + 'closed' => __('关闭订单', 'aksrt'), ]; - foreach ($actions as $status => $label) { - if ($status === $current_status) { - continue; - } - $confirm = ''; - if ($status === 'refunded') { - $confirm = __('确认将该订单标记为已退款?', 'aksrt'); - } elseif ($status === 'cancelled') { - $confirm = __('确认关闭该订单?', 'aksrt'); - } elseif ($status === 'completed') { - $confirm = __('确认完成该订单?', 'aksrt'); - } + $transitions = \AKSRT\Core\DomainModel::order_transitions(); + foreach ((array) ($transitions[$current_status] ?? []) as $status) { + $label = $status === 'paid' && $current_status === 'refunding' + ? __('取消退款', 'aksrt') + : ($labels[$status] ?? $status); + $confirm = sprintf(__('确认将订单状态改为“%s”?', 'aksrt'), $label); echo '
'; echo ''; echo ''; @@ -500,7 +467,7 @@ class PayOrders { ), 'orders_clear_empty' => __('没有可清理的关闭订单。', 'aksrt'), 'order_deleted' => __('订单已删除。', 'aksrt'), - 'order_delete_failed' => __('订单删除失败(仅允许删除已关闭/过期订单)。', 'aksrt'), + 'order_delete_failed' => __('订单删除失败(仅允许删除已关闭订单)。', 'aksrt'), 'shipping_updated' => __('物流状态已更新。', 'aksrt'), 'shipping_failed' => __('物流状态更新失败。', 'aksrt'), 'shipping_created' => __('发货记录已创建。', 'aksrt'), diff --git a/src/Content/PayOrders.php b/src/Content/PayOrders.php index 693bbda1..ccbf2430 100644 --- a/src/Content/PayOrders.php +++ b/src/Content/PayOrders.php @@ -57,26 +57,16 @@ trait PayOrders { if (is_wp_error($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')) { + $revenue_order = array_merge($paid_order, [ + 'order_number' => $order_number, + 'status' => \AKSRT\Payment\Order::STATUS_PAID, + ]); + $revenue_id = \AKSRT\Payment\CreatorRevenue::get_instance()->create_from_paid_order($revenue_order); + if ($revenue_id < 0) { + return new \WP_Error('aksrt_creator_revenue_conflict', __('同一订单的收益载荷不一致。', 'aksrt'), ['status' => 409]); } - if (class_exists('\AKSRT\Payment\CreatorRevenue')) { - $completed_order = array_merge($paid_order, [ - 'order_number' => $order_number, - 'status' => \AKSRT\Payment\Order::STATUS_COMPLETED, - ]); - $revenue_id = \AKSRT\Payment\CreatorRevenue::get_instance()->create_from_paid_order($completed_order); - if ($revenue_id < 0) { - return new \WP_Error('aksrt_creator_revenue_conflict', __('同一订单的收益载荷不一致。', 'aksrt'), ['status' => 409]); - } - } - } + } return true; } @@ -217,7 +207,7 @@ trait PayOrders { || (int) ($current['product_id'] ?? 0) !== $post_id || (int) ($current['amount'] ?? 0) !== $amount || (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]); } @@ -253,13 +243,13 @@ trait PayOrders { return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]); } } - $paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [ - 'payment_method' => 'points', - 'payment_transaction_id' => 'points_' . $order_number, - 'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount), - 'require_transition' => true, - ]); - return $paid ? true : new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]); + $paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [ + 'payment_method' => 'points', + 'payment_transaction_id' => 'points_' . $order_number, + 'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount), + 'require_transition' => true, + ]); + return $paid ? true : new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]); }, $guard ); @@ -390,7 +380,7 @@ trait PayOrders { return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]); } $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, [ 'payment_method' => 'free', 'payment_transaction_id' => 'free_' . $order_number, @@ -503,7 +493,7 @@ trait PayOrders { if ($payment_method === '') { $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]); $history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]); return [ diff --git a/src/Content/VipServicePurchase.php b/src/Content/VipServicePurchase.php index c04370c7..ec5ee5e1 100644 --- a/src/Content/VipServicePurchase.php +++ b/src/Content/VipServicePurchase.php @@ -408,7 +408,7 @@ trait VipServicePurchase { if ($order_number === '') { 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]); $history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]); return [ @@ -611,16 +611,6 @@ trait VipServicePurchase { if (!$fulfilled) { 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; } @@ -828,7 +818,7 @@ trait VipServicePurchase { || (int) ($current['product_id'] ?? 0) !== $package_id || (int) ($current['amount'] ?? 0) !== $cost || (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]); } @@ -865,13 +855,13 @@ trait VipServicePurchase { return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]); } } - $paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [ - 'payment_method' => 'points', - 'payment_transaction_id' => 'points_' . $order_number, - 'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost), - 'require_transition' => true, - ]); - return $paid ? true : new \WP_Error('aksrt_order_confirm_failed', __('兑换确认失败', 'aksrt'), ['status' => 500]); + $paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [ + 'payment_method' => 'points', + 'payment_transaction_id' => 'points_' . $order_number, + 'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost), + 'require_transition' => true, + ]); + return $paid ? true : new \WP_Error('aksrt_order_confirm_failed', __('兑换确认失败', 'aksrt'), ['status' => 500]); }, $guard ); diff --git a/src/Core/DomainModel.php b/src/Core/DomainModel.php index af0b5533..516677e4 100644 --- a/src/Core/DomainModel.php +++ b/src/Core/DomainModel.php @@ -50,16 +50,11 @@ class DomainModel { public const PAY_TYPE_VIDEO = 'video'; public const PAY_TYPE_VIP = 'vip'; - public const ORDER_STATUS_CREATED = 'created'; public const ORDER_STATUS_PENDING = 'pending'; 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_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_SHIPPED = 1; @@ -390,16 +385,11 @@ class DomainModel { */ public static function order_status_labels(): array { return [ - self::ORDER_STATUS_CREATED => __('已创建', 'aksrt'), self::ORDER_STATUS_PENDING => __('待支付', '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_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 { return [ - self::ORDER_STATUS_CREATED => [ - self::ORDER_STATUS_PENDING, - self::ORDER_STATUS_CANCELLED, - ], self::ORDER_STATUS_PENDING => [ self::ORDER_STATUS_PAID, - self::ORDER_STATUS_CANCELLED, - self::ORDER_STATUS_EXPIRED, + self::ORDER_STATUS_CLOSED, ], self::ORDER_STATUS_PAID => [ - self::ORDER_STATUS_DELIVERED, - self::ORDER_STATUS_COMPLETED, 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_REFUNDED, self::ORDER_STATUS_PAID, ], self::ORDER_STATUS_REFUNDED => [], - self::ORDER_STATUS_PENDING_REVIEW => [ - self::ORDER_STATUS_COMPLETED, - self::ORDER_STATUS_REFUNDING, - ], + self::ORDER_STATUS_CLOSED => [], ]; } - 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); - 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 { - $from = self::normalize_order_status($from, self::ORDER_STATUS_CREATED); - $to = self::normalize_order_status($to); + $from = sanitize_key($from); + $to = sanitize_key($to); $transitions = self::order_transitions(); return isset($transitions[$from]) && in_array($to, $transitions[$from], true); @@ -466,14 +433,11 @@ class DomainModel { public static function paid_order_statuses(): array { return [ 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 { - return in_array(self::normalize_order_status($status), self::paid_order_statuses(), true); + return in_array(sanitize_key($status), self::paid_order_statuses(), true); } /** diff --git a/src/Credits/BalanceServiceCharge.php b/src/Credits/BalanceServiceCharge.php index 8bdc66f4..70d55f16 100644 --- a/src/Credits/BalanceServiceCharge.php +++ b/src/Credits/BalanceServiceCharge.php @@ -43,8 +43,7 @@ trait BalanceServiceCharge { $locked_payment_method = ''; $locked_transaction_id = ''; $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, &$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) { global $wpdb; $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); @@ -52,10 +51,8 @@ trait BalanceServiceCharge { || (int) ($current['user_id'] ?? 0) !== $user_id || sanitize_key((string) ($current['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE || !in_array((string) ($current['status'] ?? ''), [ - \AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_PAID, - \AKSRT\Payment\Order::STATUS_COMPLETED, ], true) ) { 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_transaction_id = sanitize_text_field((string) ($current['payment_transaction_id'] ?? '')); $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)) { return new \WP_Error('aksrt_recharge_transaction_conflict', __('充值交易号不一致', 'aksrt'), ['status' => 409]); } 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; $table = \AKSRT\Core\DBMigrator::table('order'); $locked_note['ledger_credited'] = 1; @@ -93,13 +89,12 @@ trait BalanceServiceCharge { } $now = current_time('mysql'); $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_transaction_id' => $provider_transaction_id !== '' ? $provider_transaction_id : $locked_transaction_id, '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), - ], ['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]); }; $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_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; } @@ -332,10 +326,7 @@ trait BalanceServiceCharge { if (!$order) { return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]); } - if (is_array($existing) && in_array((string) ($order['status'] ?? ''), [ - \AKSRT\Payment\Order::STATUS_PAID, - \AKSRT\Payment\Order::STATUS_COMPLETED, - ], true)) { + if (is_array($existing) && (string) ($order['status'] ?? '') === \AKSRT\Payment\Order::STATUS_PAID) { $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]); $transaction_id = (int) ($history['items'][0]['id'] ?? 0); diff --git a/src/Credits/BalanceServiceRest.php b/src/Credits/BalanceServiceRest.php index 93d7953f..dc356b6f 100644 --- a/src/Credits/BalanceServiceRest.php +++ b/src/Credits/BalanceServiceRest.php @@ -390,11 +390,10 @@ trait BalanceServiceRest { 'product_id' => 0, 'amount' => $amount->decimal(), 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_COMPLETED, + 'status' => \AKSRT\Payment\Order::STATUS_PAID, 'payment_method' => 'balance', 'payment_transaction_id' => 'ledger_' . $transaction_id, 'paid_at' => current_time('mysql'), - 'completed_at' => current_time('mysql'), 'note' => is_string($note) ? $note : '', 'request_key' => $operation_key, ]); diff --git a/src/Payment/BalanceGateway.php b/src/Payment/BalanceGateway.php index b05019ab..f7479f60 100644 --- a/src/Payment/BalanceGateway.php +++ b/src/Payment/BalanceGateway.php @@ -72,7 +72,7 @@ class BalanceGateway implements GatewayInterface { if (!is_array($current) || (int) ($current['user_id'] ?? 0) !== $user_id || $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]); } @@ -212,7 +212,7 @@ class BalanceGateway implements GatewayInterface { || (int) ($current['user_id'] ?? 0) !== $user_id || sanitize_key((string) ($current['payment_method'] ?? '')) !== 'balance' || $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]); } diff --git a/src/Payment/ChargeCard.php b/src/Payment/ChargeCard.php index 62b748db..9417cd23 100644 --- a/src/Payment/ChargeCard.php +++ b/src/Payment/ChargeCard.php @@ -652,7 +652,7 @@ class ChargeCard { && sanitize_key((string) ($order['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP && (int) ($order['product_id'] ?? 0) === $card_id && 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 && sanitize_key((string) ($note['card_type'] ?? '')) === self::TYPE_VIP && (int) ($stored_meta['level'] ?? 0) === $level diff --git a/src/Payment/Gateway.php b/src/Payment/Gateway.php index c5a6e3bf..4abf0609 100644 --- a/src/Payment/Gateway.php +++ b/src/Payment/Gateway.php @@ -225,7 +225,7 @@ class Gateway { $provider_ref = sanitize_text_field((string) ($request->get_param('order') ?: $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; } @@ -306,7 +306,7 @@ class Gateway { * @param array|null $order */ private function reconciliation_gateway(?array $order): ?GatewayInterface { - if (!is_array($order) || !in_array((string) ($order['status'] ?? ''), [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) { + if (!is_array($order) || (string) ($order['status'] ?? '') !== Order::STATUS_PENDING) { return null; } @@ -384,7 +384,7 @@ class Gateway { // 幂等:仅同一交易号的重复回调可视为成功。不同交易号意味着 // 用户可能重复付款,不能静默 ACK 为已正确同步。 - if (in_array($current, ['paid', 'delivered', 'completed'], true)) { + if ($current === Order::STATUS_PAID) { $stored_transaction = sanitize_text_field((string) ($order['payment_transaction_id'] ?? '')); if (sanitize_key((string) ($order['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) { if ($stored_transaction !== '' && !hash_equals($stored_transaction, $transaction_id)) { @@ -399,7 +399,7 @@ class Gateway { return $stored_transaction !== '' && hash_equals($stored_transaction, $transaction_id); } // 只允许从未支付态完成 - if (!in_array($current, ['created', 'pending'], true)) { + if ($current !== Order::STATUS_PENDING) { return false; } diff --git a/src/Payment/Order.php b/src/Payment/Order.php index db707990..c7db2baf 100644 --- a/src/Payment/Order.php +++ b/src/Payment/Order.php @@ -9,16 +9,11 @@ defined('ABSPATH') || exit; 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_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_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; @@ -122,7 +117,7 @@ class Order { 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')]); } diff --git a/src/Payment/OrderCenterQuery.php b/src/Payment/OrderCenterQuery.php index 95ce708d..6e80c438 100644 --- a/src/Payment/OrderCenterQuery.php +++ b/src/Payment/OrderCenterQuery.php @@ -15,13 +15,17 @@ final class OrderCenterQuery { public const STATUS_ALL = 'all'; 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'; private const STATUS_GROUPS = [ self::STATUS_ALL, self::STATUS_PENDING, - self::STATUS_COMPLETED, + self::STATUS_PAID, + self::STATUS_REFUNDING, + self::STATUS_REFUNDED, self::STATUS_CLOSED, ]; @@ -87,33 +91,10 @@ final class OrderCenterQuery { /** @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, - 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, - ], - }; + if ($this->status() === self::STATUS_ALL) { + return array_keys(DomainModel::order_status_labels()); + } + return [$this->status()]; } /** @return array */ diff --git a/src/Payment/OrderRepositoryAdmin.php b/src/Payment/OrderRepositoryAdmin.php index 07f275bd..5b6f8d47 100644 --- a/src/Payment/OrderRepositoryAdmin.php +++ b/src/Payment/OrderRepositoryAdmin.php @@ -342,9 +342,13 @@ trait OrderRepositoryAdmin { $params = []; $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'; $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'] ?? '')); diff --git a/src/Payment/OrderRepositoryCore.php b/src/Payment/OrderRepositoryCore.php index a94ad38a..c217eca7 100644 --- a/src/Payment/OrderRepositoryCore.php +++ b/src/Payment/OrderRepositoryCore.php @@ -46,6 +46,12 @@ trait OrderRepositoryCore { if ($amount === null) { 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 = [ 'order_number' => $order_number, @@ -54,22 +60,21 @@ trait OrderRepositoryCore { 'product_id' => (int) ($data['product_id'] ?? 0), 'amount' => $amount, '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_transaction_id' => sanitize_text_field((string) ($data['payment_transaction_id'] ?? '')), 'created_at' => $data['created_at'] ?? current_time('mysql'), 'paid_at' => $data['paid_at'] ?? null, - 'completed_at' => $data['completed_at'] ?? null, '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 - // supported for compatibility, while new flows use the shared runtime policy. + // Pending orders always receive an expiry. Callers may provide a shorter + // expiry; otherwise the shared runtime policy supplies it. if (array_key_exists('expires_at', $data) && $data['expires_at'] !== null && $data['expires_at'] !== '') { $row['expires_at'] = (string) $data['expires_at']; $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(); $formats[] = '%s'; } @@ -189,7 +194,7 @@ trait OrderRepositoryCore { * their existing user-wide semantics, so changing a filter does not change the * summary cards. The subquery avoids four independent COUNT statements. * - * @return array{total:int,stats:array{total:int,pending:int,completed:int}} + * @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 { global $wpdb; @@ -197,29 +202,25 @@ trait OrderRepositoryCore { if ($user_id <= 0) { return [ 'total' => 0, - 'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0], + 'stats' => ['total' => 0, 'pending' => 0, 'paid' => 0], ]; } $this->expire_pending_orders($user_id); $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_placeholders = implode(', ', array_fill(0, count($known_statuses), '%s')); $sql = "SELECT (SELECT COUNT(*) FROM {$this->table()} {$parts['where']}) AS filtered_total, COUNT(*) AS known_total, - SUM(CASE WHEN status IN (%s, %s) THEN 1 ELSE 0 END) AS pending, - SUM(CASE WHEN status IN ({$completed_placeholders}) THEN 1 ELSE 0 END) AS completed + SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) AS pending, + SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) AS paid FROM {$this->table()} WHERE user_id = %d AND status IN ({$known_placeholders})"; $params = array_merge( $parts['params'], - $pending_statuses, - $completed_statuses, + [Order::STATUS_PENDING, Order::STATUS_PAID], [$user_id], $known_statuses ); @@ -230,7 +231,7 @@ trait OrderRepositoryCore { 'stats' => [ 'total' => (int) ($row['known_total'] ?? 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 { 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')]; if ($user_id !== null && $user_id > 0) { $where .= ' AND user_id = %d'; $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')); $result = $wpdb->query($wpdb->prepare($sql, $params)); @@ -262,7 +263,7 @@ trait OrderRepositoryCore { private function expire_order_if_due(array $order): array { $status = sanitize_key((string) ($order['status'] ?? '')); $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; } @@ -275,13 +276,13 @@ trait OrderRepositoryCore { $closed_at = gmdate('Y-m-d H:i:s'); $updated = $wpdb->update( $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], ['%s', '%s'], ['%s', '%s'] ); if ($updated) { - $order['status'] = Order::STATUS_EXPIRED; + $order['status'] = Order::STATUS_CLOSED; $order['cancelled_at'] = $closed_at; } @@ -298,10 +299,8 @@ trait OrderRepositoryCore { return false; } if ( - in_array($new_status, [ - \AKSRT\Core\DomainModel::ORDER_STATUS_PAID, - \AKSRT\Core\DomainModel::ORDER_STATUS_COMPLETED, - ], true) + $new_status === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID + && (string) ($preflight['status'] ?? '') !== \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDING && sanitize_key((string) ($preflight['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE && class_exists('\AKSRT\Credits\BalanceService') ) { @@ -382,30 +381,24 @@ trait OrderRepositoryCore { $order = is_array($order_row) ? $this->normalize_row($order_row) : null; 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; } $update_data = ['status' => $new_status]; $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 = [ - \AKSRT\Core\DomainModel::ORDER_STATUS_PAID => 'paid_at', - \AKSRT\Core\DomainModel::ORDER_STATUS_DELIVERED => 'delivered_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_PAID => 'paid_at', + \AKSRT\Core\DomainModel::ORDER_STATUS_CLOSED => 'cancelled_at', + \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED => 'refunded_at', ]; if (isset($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]) : ''; if ($existing === '' || str_starts_with($existing, '0000-00-00')) { $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 ( 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_REFUNDED, ], true) @@ -487,7 +475,7 @@ trait OrderRepositoryCore { ]; // 到账事件:让充值/VIP/商品履约等挂接(单一履约入口)。 // 仅在「首次」转为 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, [ 'status' => $new_status, 'payment_method' => $update_data['payment_method'] ?? ($order['payment_method'] ?? ''), @@ -568,8 +556,7 @@ trait OrderRepositoryCore { /** - * Delete closed (cancelled/expired) orders older than $days days. - * Mirrors the legacy pay module clear_order(14). + * Delete closed orders older than $days days. * * @return int Number of rows deleted. */ @@ -579,10 +566,10 @@ trait OrderRepositoryCore { $days = max(1, min(3650, $days)); $table = $this->table(); $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} - WHERE status IN ('cancelled', 'expired') - AND COALESCE(cancelled_at, created_at) < %s"; + WHERE status = 'closed' + AND cancelled_at IS NOT NULL + AND cancelled_at < %s"; $deleted = $wpdb->query($wpdb->prepare($sql, $cutoff)); return $deleted === false ? 0 : (int) $deleted; } @@ -612,24 +599,21 @@ trait OrderRepositoryCore { $this->expire_pending_orders($user_id > 0 ? $user_id : null); - $where = ''; - $params = []; - if ($user_id > 0) { - $where = 'WHERE user_id = %d'; - $params[] = $user_id; - } + $where = "WHERE status IN ('pending', 'paid', 'refunding', 'refunded', 'closed')"; + $params = []; + if ($user_id > 0) { + $where .= ' AND user_id = %d'; + $params[] = $user_id; + } $sql = "SELECT COUNT(*) as total, 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 = '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 = 'cancelled' THEN 1 ELSE 0 END) as cancelled, 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}"; if (!empty($params)) { @@ -642,12 +626,9 @@ trait OrderRepositoryCore { 'total' => (int) ($row['total'] ?? 0), 'pending' => (int) ($row['pending'] ?? 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), - 'cancelled' => (int) ($row['cancelled'] ?? 0), 'refunded' => (int) ($row['refunded'] ?? 0), + 'closed' => (int) ($row['closed'] ?? 0), 'revenue' => $this->cash_decimal((string) ($row['revenue'] ?? '0')), ]; } @@ -727,7 +708,7 @@ trait OrderRepositoryCore { if ($row['amount'] === null) { $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'] = []; if (!empty($row['note']) && is_string($row['note'])) { diff --git a/src/Payment/PaymentOrchestrator.php b/src/Payment/PaymentOrchestrator.php index 02bbba9d..c0aec1f0 100644 --- a/src/Payment/PaymentOrchestrator.php +++ b/src/Payment/PaymentOrchestrator.php @@ -69,7 +69,7 @@ class PaymentOrchestrator { } } $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')); } $stored_method = sanitize_key((string) ($stored['payment_method'] ?? '')); diff --git a/src/Payment/Revenue.php b/src/Payment/Revenue.php index 38f89578..1066d78e 100644 --- a/src/Payment/Revenue.php +++ b/src/Payment/Revenue.php @@ -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 $order */ @@ -35,7 +35,7 @@ class Revenue { } $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; } $product_type = sanitize_key((string) ($order['product_type'] ?? '')); diff --git a/src/Payment/UserOrders.php b/src/Payment/UserOrders.php index c4108e9f..f0f4b738 100644 --- a/src/Payment/UserOrders.php +++ b/src/Payment/UserOrders.php @@ -64,7 +64,7 @@ class UserOrders { 'total_pages' => $total_pages, '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'], 'product_types' => $this->product_type_filter_options(), ]; @@ -109,13 +109,13 @@ class UserOrders { /** @return array */ public function get_user_order_stats(int $user_id): array { 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); return is_array($summary['stats'] ?? null) ? $summary['stats'] - : ['total' => 0, 'pending' => 0, 'completed' => 0]; + : ['total' => 0, 'pending' => 0, 'paid' => 0]; } /** @return array */ @@ -192,7 +192,7 @@ class UserOrders { $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 !== ''; + $can_open = $group === OrderCenterQuery::STATUS_PAID && $permalink !== ''; $order_number = (string) ($order['order_number'] ?? ''); $updated_at = $this->resolve_updated_at($order); @@ -249,7 +249,7 @@ class UserOrders { 'total_pages' => 1, 'page' => 1, 'query' => $query->url_values(), - 'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0], + 'stats' => ['total' => 0, 'pending' => 0, 'paid' => 0], 'invalidation_tags' => ['orders'], 'product_types' => $this->product_type_filter_options(), ]; @@ -259,7 +259,7 @@ class UserOrders { private function resolve_updated_at(array $order): string { $latest = ''; $latest_timestamp = 0; - foreach (['created_at', 'paid_at', 'delivered_at', 'completed_at', 'cancelled_at', 'refunded_at'] as $field) { + foreach (['created_at', 'paid_at', 'cancelled_at', 'refunded_at'] as $field) { $value = trim((string) ($order[$field] ?? '')); if ($value === '') { continue; @@ -284,20 +284,11 @@ class UserOrders { } private function status_group(string $status): string { - return match ($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, - }; + return isset(DomainModel::order_status_labels()[$status]) ? $status : ''; } private function status_label(string $group): string { - return match ($group) { - OrderCenterQuery::STATUS_PENDING => __('待支付', 'aksrt'), - OrderCenterQuery::STATUS_COMPLETED => __('权益已生效', 'aksrt'), - default => __('已关闭', 'aksrt'), - }; + return DomainModel::order_status_labels()[$group] ?? __('未知状态', 'aksrt'); } /** @param array $meta */ @@ -389,7 +380,7 @@ class UserOrders { } 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; } $expires = strtotime($expires_at); @@ -406,8 +397,7 @@ class UserOrders { if (!$order || $this->status_group((string) ($order['status'] ?? '')) !== OrderCenterQuery::STATUS_PENDING) { return ['success' => false, 'message' => __('当前订单不可关闭。', 'aksrt')]; } - if (!$repo->update_status($order_number, Order::STATUS_CANCELLED, [ - 'force' => true, + if (!$repo->update_status($order_number, Order::STATUS_CLOSED, [ 'note' => __('用户在前台关闭订单。', 'aksrt'), ])) { return ['success' => false, 'message' => __('关闭订单失败,请稍后重试。', 'aksrt')]; @@ -425,7 +415,7 @@ class UserOrders { if (!$order) { 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]); $history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]); if (!empty($history['items'][0])) { diff --git a/tests/widget-system/asset-ledger-contract.test.cjs b/tests/widget-system/asset-ledger-contract.test.cjs index 99d2b5dd..7359c96b 100644 --- a/tests/widget-system/asset-ledger-contract.test.cjs +++ b/tests/widget-system/asset-ledger-contract.test.cjs @@ -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', () => { 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 userOrders = fs.readFileSync(path.join(root, 'src', 'Payment', 'UserOrders.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 chargeCard = fs.readFileSync(path.join(root, 'src', 'Payment', 'ChargeCard.php'), '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, /self::ORDER_PRODUCT_POINTS\s*=>\s*__\('积分购买'/) - for (const status of ['STATUS_PAID', 'STATUS_DELIVERED', 'STATUS_COMPLETED', 'STATUS_PENDING_REVIEW']) { - assert.match(query, new RegExp(`Order::${status}`)) + const statuses = ['PENDING', 'PAID', 'REFUNDING', 'REFUNDED', 'CLOSED'] + 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/) - assert.doesNotMatch(userOrders, /OrderCenterQuery::STATUS_PAID/) - assert.match(repository, /\$completed_statuses = \[Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW\]/) - assert.match(repository, /SUM\(CASE WHEN status IN \(\{\$completed_placeholders\}\) THEN 1 ELSE 0 END\) AS completed/) + for (const status of ['CREATED', 'DELIVERED', 'COMPLETED', 'PENDING_REVIEW', 'CANCELLED', 'EXPIRED']) { + assert.doesNotMatch(domain, new RegExp(`ORDER_STATUS_${status}`)) + assert.doesNotMatch(order, new RegExp(`STATUS_${status}`)) + } + 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, /