Merge pull request 'fix(orders): enforce configured expiry' (#3) from worktree/fix-order-time-limit into main
Reviewed-on: #3
This commit is contained in:
@@ -216,7 +216,7 @@ trait PayOrders {
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => 'points',
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
|
||||
if ($order_number === '') {
|
||||
@@ -362,7 +362,7 @@ trait PayOrders {
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => 'free',
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
if ($order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
|
||||
@@ -448,7 +448,7 @@ trait PayOrders {
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => $payment_method,
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
|
||||
if ($order_number === '') {
|
||||
|
||||
@@ -369,7 +369,7 @@ trait VipServicePurchase {
|
||||
'discount_tag' => $discount_tag,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$expires_at = gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS);
|
||||
$expires_at = \AKSRT\Settings\PaymentRuntime::get_order_expiry_at();
|
||||
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
|
||||
'user_id' => $user_id,
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
|
||||
@@ -750,7 +750,7 @@ trait VipServicePurchase {
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => 'points',
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
if ($order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
|
||||
@@ -326,7 +326,7 @@ trait BalanceServiceCharge {
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => $payment_method,
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
if ($order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
|
||||
@@ -180,7 +180,7 @@ trait BalanceServicePoints {
|
||||
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
|
||||
'payment_method' => $payment_method,
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
if ($order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
|
||||
@@ -554,7 +554,7 @@ class ChargeCard {
|
||||
'status' => class_exists('AKSRT\\Payment\\Order') ? Order::STATUS_PENDING : 'pending',
|
||||
'payment_method' => 'card_pass',
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ trait OrderRepositoryAdmin {
|
||||
public function get_orders_admin(array $args = [], string $search = ''): array {
|
||||
global $wpdb;
|
||||
|
||||
$this->expire_pending_orders();
|
||||
[$where, $params] = $this->build_admin_order_filters($args, $search);
|
||||
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
@@ -147,6 +148,7 @@ trait OrderRepositoryAdmin {
|
||||
public function get_orders_count(array $args = [], string $search = ''): int {
|
||||
global $wpdb;
|
||||
|
||||
$this->expire_pending_orders();
|
||||
[$where, $params] = $this->build_admin_order_filters($args, $search);
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
@@ -57,10 +57,14 @@ trait OrderRepositoryCore {
|
||||
];
|
||||
$formats = ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s'];
|
||||
|
||||
// Honour configured expiry when callers pass expires_at (shop checkout etc.).
|
||||
// Pending orders always receive an expiry. Explicit caller values remain
|
||||
// supported for compatibility, while new flows use the shared runtime policy.
|
||||
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)) {
|
||||
$row['expires_at'] = \AKSRT\Settings\PaymentRuntime::get_order_expiry_at();
|
||||
$formats[] = '%s';
|
||||
}
|
||||
|
||||
$wpdb->insert($this->table(), $row, $formats);
|
||||
@@ -86,7 +90,7 @@ trait OrderRepositoryCore {
|
||||
), ARRAY_A);
|
||||
|
||||
if ($row) {
|
||||
return $this->normalize_row($row);
|
||||
return $this->expire_order_if_due($this->normalize_row($row));
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -100,6 +104,7 @@ trait OrderRepositoryCore {
|
||||
public function get_user_orders(int $user_id, array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$this->expire_pending_orders($user_id);
|
||||
$parts = $this->get_user_order_query_parts($user_id, $args);
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
@@ -143,6 +148,8 @@ trait OrderRepositoryCore {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->expire_pending_orders($user_id);
|
||||
|
||||
$parts = $this->get_user_order_query_parts($user_id, $args);
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} {$parts['where']}",
|
||||
@@ -169,6 +176,8 @@ trait OrderRepositoryCore {
|
||||
];
|
||||
}
|
||||
|
||||
$this->expire_pending_orders($user_id);
|
||||
|
||||
$parts = $this->get_user_order_query_parts($user_id, $args);
|
||||
$pending_statuses = [Order::STATUS_CREATED, Order::STATUS_PENDING];
|
||||
$paid_status = Order::STATUS_PAID;
|
||||
@@ -199,6 +208,59 @@ trait OrderRepositoryCore {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Close every expired, unpaid order for one user (or all users).
|
||||
*
|
||||
* The status condition makes this idempotent and prevents an expiry sweep
|
||||
* from overwriting a payment completed concurrently by a gateway callback.
|
||||
*/
|
||||
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 <> '' 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}";
|
||||
array_unshift($params, gmdate('Y-m-d H:i:s'));
|
||||
$result = $wpdb->query($wpdb->prepare($sql, $params));
|
||||
|
||||
return is_numeric($result) ? (int) $result : 0;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $order @return array<string,mixed> */
|
||||
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 === '') {
|
||||
return $order;
|
||||
}
|
||||
|
||||
$expires = strtotime($expires_at . ' UTC');
|
||||
if ($expires === false || $expires > time()) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$closed_at = gmdate('Y-m-d H:i:s');
|
||||
$updated = $wpdb->update(
|
||||
$this->table(),
|
||||
['status' => Order::STATUS_EXPIRED, 'cancelled_at' => $closed_at],
|
||||
['order_number' => (string) $order['order_number'], 'status' => $status],
|
||||
['%s', '%s'],
|
||||
['%s', '%s']
|
||||
);
|
||||
if ($updated) {
|
||||
$order['status'] = Order::STATUS_EXPIRED;
|
||||
$order['cancelled_at'] = $closed_at;
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function update_status(string $order_number, string $new_status, array $extra = []): bool {
|
||||
@@ -394,6 +456,8 @@ trait OrderRepositoryCore {
|
||||
public function get_stats(int $user_id = 0): array {
|
||||
global $wpdb;
|
||||
|
||||
$this->expire_pending_orders($user_id > 0 ? $user_id : null);
|
||||
|
||||
$where = '';
|
||||
$params = [];
|
||||
if ($user_id > 0) {
|
||||
@@ -703,7 +767,7 @@ trait OrderRepositoryCore {
|
||||
ARRAY_A
|
||||
);
|
||||
if (is_array($row)) {
|
||||
return $this->normalize_row($row);
|
||||
return $this->expire_order_if_due($this->normalize_row($row));
|
||||
}
|
||||
|
||||
// Superseded attempts stay in the history array.
|
||||
@@ -719,7 +783,7 @@ trait OrderRepositoryCore {
|
||||
ARRAY_A
|
||||
);
|
||||
if (is_array($row)) {
|
||||
return $this->normalize_row($row);
|
||||
return $this->expire_order_if_due($this->normalize_row($row));
|
||||
}
|
||||
|
||||
// Last resort: {order_number}P{n} prefix heuristic.
|
||||
|
||||
@@ -392,6 +392,20 @@ class PaymentRuntime {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the UTC expiry time for a newly created payable order.
|
||||
*
|
||||
* Keeping this in the payment runtime prevents individual checkout
|
||||
* flows from drifting away from the admin "payment_order_expiry" setting.
|
||||
*/
|
||||
public static function get_order_expiry_at(?int $now = null): string {
|
||||
$settings = self::get_order_settings();
|
||||
$timestamp = $now ?? time();
|
||||
$minutes = max(5, min(1440, (int) ($settings['expiry'] ?? 30)));
|
||||
|
||||
return gmdate('Y-m-d H:i:s', $timestamp + $minutes * MINUTE_IN_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user