[], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => $this->empty_stats(), 'product_types' => $this->product_type_filter_options(), ]; } $page = max(1, (int) ($args['page'] ?? 1)); $per_page = max(1, min(50, (int) ($args['per_page'] ?? 20))); $status = sanitize_key((string) ($args['status'] ?? '')); $product_type = sanitize_key((string) ($args['product_type'] ?? '')); $repo_args = [ 'limit' => $per_page, 'offset' => ($page - 1) * $per_page, ]; if ($status !== '') { $repo_args['status'] = $status; } if ($product_type !== '' && $product_type !== 'all') { $repo_args['product_type'] = $product_type; } $repo = OrderRepository::get_instance(); $orders = $repo->get_user_orders($user_id, $repo_args); $total = $repo->count_user_orders($user_id, $repo_args); $items = []; foreach ($orders as $order) { $items[] = $this->decorate_order_view($order, true); } return [ 'items' => $items, 'total' => $total, 'total_pages' => max(1, (int) ceil(max(0, $total) / $per_page)), 'page' => $page, 'stats' => $this->get_user_order_stats($user_id), 'product_types' => $this->product_type_filter_options(), ]; } public function get_order_detail_view(int $user_id, string $order_number): ?array { if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) { return null; } $order = OrderRepository::get_instance()->get_user_order($user_id, $order_number); if (!$order) { return null; } return $this->decorate_order_view($order, true); } /** * @return array{total:int,pending:int,paid:int} */ public function get_user_order_stats(int $user_id): array { if ($user_id <= 0 || !class_exists(OrderRepository::class)) { return $this->empty_stats(); } $repo = OrderRepository::get_instance(); return [ 'total' => $repo->count_user_orders($user_id, []), 'pending' => $repo->count_user_orders($user_id, ['status' => Order::STATUS_PENDING]), 'paid' => $repo->count_user_orders($user_id, ['status' => Order::STATUS_PAID]), ]; } /** * @return array */ public function product_type_filter_options(): array { $labels = \AKSRT\Core\DomainModel::order_product_type_labels(); return array_merge(['' => __('全部类型', 'aksrt')], $labels); } /** * @param array $order Normalized order row. * @return array */ public function decorate_order_view(array $order, bool $with_detail = false): array { $meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : []; $order_number = (string) ($order['order_number'] ?? ''); $product_type = sanitize_key((string) ($order['product_type'] ?? '')); $status = (string) ($order['status'] ?? Order::STATUS_PENDING); $product_id = (int) ($order['product_id'] ?? 0); $is_shop = $product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP || $product_type === 'shop'; $currency = sanitize_key((string) ($order['currency'] ?? 'CNY')); $is_points = $currency === 'points' || sanitize_key((string) ($order['payment_method'] ?? '')) === 'points' || (!empty($meta['pay_modo']) && (string) $meta['pay_modo'] === 'points'); $type_labels = \AKSRT\Core\DomainModel::order_product_type_labels(); $type_label = $type_labels[$product_type] ?? ($product_type !== '' ? $product_type : __('订单', 'aksrt')); $title = $this->resolve_order_title($order, $meta, $product_type, $product_id, $type_label); $image = $this->resolve_order_image($order, $meta, $product_type, $product_id); $permalink = $this->resolve_order_permalink($product_type, $product_id, $meta); $order_items = []; if ($is_shop && class_exists('\AKSRT\Shop\Shop')) { $order_items = \AKSRT\Shop\Shop::get_instance()->get_order_items($order_number); } $has_structured_items = !empty($order_items); $product = []; if ($has_structured_items && class_exists('\AKSRT\Shop\Shop')) { $product = \AKSRT\Shop\Shop::get_instance()->build_product_card_view((int) ($order_items[0]['product_id'] ?? 0)); } elseif ($is_shop && $product_id > 0 && class_exists('\AKSRT\Shop\Shop')) { $product = \AKSRT\Shop\Shop::get_instance()->build_product_card_view($product_id); } if (!empty($product['title'])) { $title = (string) $product['title']; } if (!empty($product['image'])) { $image = (string) $product['image']; } if (!empty($product['permalink'])) { $permalink = (string) $product['permalink']; } $shipping_records = []; $after_sale_items = []; if ($is_shop) { if (class_exists(Shipping::class)) { $shipping_records = Shipping::get_instance()->get_by_order($order_number); } if (class_exists(AfterSale::class)) { $as = AfterSale::get_instance()->get_list([ 'limit' => 20, 'offset' => 0, 'order_number' => $order_number, ]); $after_sale_items = $as['items'] ?? []; } } $subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount'] ?? 0); $shipping_fee = (float) ($meta['shipping_fee'] ?? 0); $discount = (float) ($meta['discount'] ?? 0); $expires_at = (string) ($order['expires_at'] ?? ''); $payment_value = sanitize_key((string) ($meta['payment_method']['value'] ?? $order['payment_method'] ?? 'balance')); $can_pay = in_array($status, [Order::STATUS_PENDING, Order::STATUS_EXPIRED, Order::STATUS_CREATED], true) && $payment_value !== 'manual_review'; $can_cancel = in_array($status, [Order::STATUS_PENDING, Order::STATUS_CREATED, Order::STATUS_EXPIRED], true); $can_after_sale = $is_shop && in_array($status, [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED], true); $can_receipt = $is_shop && $status === Order::STATUS_DELIVERED; $can_review = $is_shop && $status === Order::STATUS_PENDING_REVIEW; $can_rebuy = $is_shop && in_array($status, [Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW], true); $can_view = $permalink !== '' && in_array($status, [ Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW, ], true); $view = [ 'id' => (int) ($order['id'] ?? 0), 'order_number' => $order_number, 'amount' => (float) ($order['amount'] ?? 0), 'currency' => $currency, 'currency_symbol' => $is_points ? (class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt')) : '¥', 'is_points' => $is_points, 'status' => $status, 'status_label' => Order::get_valid_statuses()[$status] ?? $status, 'product_type' => $product_type, 'product_type_label' => $type_label, 'product_id' => $product_id, 'product_name' => $title, 'product' => $product ?: [ 'title' => $title, 'image' => $image, 'permalink' => $permalink, ], 'image' => $image, 'permalink' => $permalink, 'created_at' => (string) ($order['created_at'] ?? ''), 'paid_at' => (string) ($order['paid_at'] ?? ''), 'expires_at' => $expires_at, 'subtotal' => $subtotal, 'shipping_fee' => $shipping_fee, 'discount' => $discount, 'address' => $meta['address'] ?? [], 'payment_method' => $meta['payment_method'] ?? [ 'value' => (string) ($order['payment_method'] ?? ''), 'label' => $this->payment_method_label((string) ($order['payment_method'] ?? '')), ], 'coupon' => $meta['coupon'] ?? [], 'items' => $has_structured_items ? $order_items : ($meta['items'] ?? []), 'shipping_method' => $meta['shipping_method'] ?? [], 'customer_note' => $meta['customer_note'] ?? ($order['note_text'] ?? ''), 'shipping_records' => $shipping_records, 'pay_url' => '#', 'actions' => [ 'can_pay' => $can_pay, 'can_cancel' => $can_cancel, 'can_after_sale' => $can_after_sale, 'can_receipt' => $can_receipt, 'can_review' => $can_review, 'can_rebuy' => $can_rebuy, 'can_view' => $can_view, ], 'time_left' => $this->get_order_countdown($expires_at, $status), 'secondary_url' => $this->resolve_secondary_url($product_type), 'secondary_label' => $this->resolve_secondary_label($product_type), ]; if ($with_detail) { $view['after_sale_records'] = $after_sale_items; $view['timeline'] = $this->build_order_timeline($order, $shipping_records, $after_sale_items); } return $view; } /** * Cancel pending order (any product_type). * * @return array{success:bool,message?:string,data?:array} */ public function cancel_order(int $user_id, string $order_number): array { if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) { return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; } $repo = OrderRepository::get_instance(); $order = $repo->get_user_order($user_id, $order_number); if (!$order) { return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; } $status = (string) ($order['status'] ?? ''); if (!in_array($status, [Order::STATUS_PENDING, Order::STATUS_CREATED, Order::STATUS_EXPIRED], true)) { return ['success' => false, 'message' => __('当前订单不可取消。', 'aksrt')]; } $repo->update_status($order_number, Order::STATUS_CANCELLED, [ 'force' => true, 'note' => __('用户在前台取消订单。', 'aksrt'), ]); return [ 'success' => true, 'data' => [ 'order' => $this->get_order_detail_view($user_id, $order_number), ], ]; } /** * Resume payment via configured gateway (any product_type). * * @return array{success:bool,message?:string,data?:array} */ public function pay_order(int $user_id, string $order_number, string $payment_method = ''): array { if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) { return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; } // Prefer shop checkout when available (handles points + shop meta). if (class_exists('\AKSRT\Shop\Shop')) { return \AKSRT\Shop\Shop::get_instance()->pay_order($user_id, $order_number, $payment_method); } $repo = OrderRepository::get_instance(); $order = $repo->get_user_order($user_id, $order_number); if (!$order) { return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; } $status = (string) ($order['status'] ?? ''); if (!in_array($status, [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) { return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')]; } $stored_method = sanitize_key((string) ($order['payment_method'] ?? '')); $payment_method = sanitize_key($payment_method ?: ($stored_method ?: 'balance')); if ($stored_method !== '' && $payment_method !== $stored_method) { return ['success' => false, 'message' => __('支付方式与订单不一致,请重新下单。', 'aksrt')]; } $gateway_manager = class_exists(Gateway::class) ? Gateway::get_instance() : null; $gateway = $gateway_manager ? $gateway_manager->get_gateway($payment_method) : null; if (!$gateway) { return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')]; } if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) { return ['success' => false, 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')]; } $lock = class_exists(PaymentAttemptLock::class) ? PaymentAttemptLock::acquire($order_number) : false; if ($lock === false) { return ['success' => false, 'message' => __('该订单正在发起支付,请稍后重试。', 'aksrt')]; } try { $order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number); $payment_result = $gateway->create_payment($order); if (empty($payment_result['success'])) { return ['success' => false, 'message' => $payment_result['message'] ?? __('支付失败。', 'aksrt')]; } $code_url = $payment_result['payment_data']['code_url'] ?? ''; if (!empty($payment_result['payment_url']) || $code_url !== '') { return [ 'success' => true, 'data' => [ 'redirect' => true, 'payment_url' => (string) ($payment_result['payment_url'] ?? ''), 'qr_content' => (string) $code_url, 'order_number' => $order_number, 'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'), ], ]; } $paid = $repo->update_status($order_number, Order::STATUS_PAID, [ 'payment_method' => $payment_method, 'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')), 'note' => __('用户在前台完成支付。', 'aksrt'), 'require_transition' => true, ]); if (!$paid) { $compensation = $gateway->refund( $order_number, (float) ($order['amount'] ?? 0), 'PAY_ROLLBACK_' . substr(hash('sha256', $order_number), 0, 24) ); if (empty($compensation['success'])) { error_log(sprintf( '[AKSRT Payment] rollback failed order=%s gateway=%s: %s', $order_number, $payment_method, (string) ($compensation['message'] ?? 'unknown error') )); return ['success' => false, 'message' => __('订单状态已变化,扣款补偿失败,请联系管理员处理。', 'aksrt')]; } return ['success' => false, 'message' => __('订单状态已变化,扣款已退回。', 'aksrt')]; } return [ 'success' => true, 'data' => [ 'order' => $this->get_order_detail_view($user_id, $order_number), 'message' => $payment_result['message'] ?? __('支付成功。', 'aksrt'), ], ]; } finally { PaymentAttemptLock::release($order_number, $lock); } } private function empty_stats(): array { return ['total' => 0, 'pending' => 0, 'paid' => 0]; } /** * @param array $order * @param array $meta */ private function resolve_order_title(array $order, array $meta, string $product_type, int $product_id, string $type_label): string { foreach (['product_title', 'title', 'package_title'] as $key) { if (!empty($meta[$key]) && is_string($meta[$key])) { return (string) $meta[$key]; } } if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) { $pkg = (string) ($meta['package_title'] ?? ''); if ($pkg !== '') { return $pkg; } return __('VIP 会员', 'aksrt'); } if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) { return (string) ($meta['title'] ?? __('余额充值', 'aksrt')); } if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY) { return (string) ($meta['title'] ?? __('购买积分', 'aksrt')); } if ($product_id > 0) { $post_title = get_the_title($product_id); if (is_string($post_title) && $post_title !== '') { return $post_title; } } return $type_label; } /** * @param array $order * @param array $meta */ private function resolve_order_image(array $order, array $meta, string $product_type, int $product_id): string { if (!empty($meta['thumb']) && is_string($meta['thumb'])) { return esc_url_raw((string) $meta['thumb']); } if ($product_id > 0 && has_post_thumbnail($product_id)) { $url = get_the_post_thumbnail_url($product_id, 'thumbnail'); if (is_string($url) && $url !== '') { return $url; } } return ''; } /** * @param array $meta */ private function resolve_order_permalink(string $product_type, int $product_id, array $meta): string { if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) { return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('vip') : ''; } if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE || $product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY) { return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('credits') : ''; } $post_id = $product_id > 0 ? $product_id : (int) ($meta['post_id'] ?? 0); if ($post_id > 0) { $link = get_permalink($post_id); return is_string($link) ? $link : ''; } return ''; } private function resolve_secondary_url(string $product_type): string { if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) { return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('vip') : ''; } if (in_array($product_type, [ \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE, \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY, ], true)) { return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('credits') : ''; } return ''; } private function resolve_secondary_label(string $product_type): string { if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) { return __('我的会员', 'aksrt'); } if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) { return __('我的余额', 'aksrt'); } if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY) { return class_exists('\AKSRT\Settings\Settings') ? sprintf(__('我的%s', 'aksrt'), \AKSRT\Settings\Settings::get_currency_name()) : __('我的积分', 'aksrt'); } return ''; } private function payment_method_label(string $method): string { $method = sanitize_key($method); $map = [ 'balance' => __('站内余额', 'aksrt'), 'points' => __('积分支付', 'aksrt'), 'alipay' => __('支付宝', 'aksrt'), 'wechatpay' => __('微信支付', 'aksrt'), 'wechat' => __('微信支付', 'aksrt'), 'paypal' => 'PayPal', 'stripe' => 'Stripe', 'epay_alipay' => __('易支付·支付宝', 'aksrt'), 'epay_wxpay' => __('易支付·微信', 'aksrt'), ]; return $map[$method] ?? ($method !== '' ? $method : __('—', 'aksrt')); } /** * @param array $order * @param array $shipping_records * @param array $after_sale_records * @return array */ private function build_order_timeline(array $order, array $shipping_records, array $after_sale_records): array { $timeline = []; $date_fields = [ 'created_at' => __('订单创建', 'aksrt'), 'paid_at' => __('完成支付', 'aksrt'), 'delivered_at' => __('订单发货', 'aksrt'), 'completed_at' => __('交易完成', 'aksrt'), 'cancelled_at' => __('订单取消', 'aksrt'), 'refunded_at' => __('完成退款', 'aksrt'), ]; foreach ($date_fields as $key => $label) { if (!empty($order[$key])) { $timeline[] = [ 'time' => (string) $order[$key], 'label' => $label, ]; } } foreach ($shipping_records as $record) { $timeline[] = [ 'time' => (string) ($record['created_at'] ?? ''), 'label' => sprintf(__('物流更新:%s', 'aksrt'), $record['tracking_no'] ?? ''), ]; } foreach ($after_sale_records as $record) { $timeline[] = [ 'time' => (string) ($record['created_at'] ?? ''), 'label' => sprintf(__('售后申请:%s', 'aksrt'), $record['reason'] ?? ''), ]; } usort($timeline, static function ($a, $b) { return strcmp($a['time'] ?? '', $b['time'] ?? ''); }); return $timeline; } private function get_order_countdown(string $expires_at, string $status): int { if ($expires_at === '' || !in_array($status, [Order::STATUS_PENDING, Order::STATUS_EXPIRED, Order::STATUS_CREATED], true)) { return 0; } $left = strtotime($expires_at) - time(); return max(0, (int) $left); } }