diff --git a/assets/css/routes/orders.css b/assets/css/routes/orders.css index e625b1bb..70449223 100644 --- a/assets/css/routes/orders.css +++ b/assets/css/routes/orders.css @@ -436,3 +436,65 @@ html.dark .aksrt-orders-page__filter-chip.is-active, html.dark .aksrt-orders-page__filter-chip.is-active { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); } + +/* ===== Stats + type filters (My Orders) ===== */ +.aksrt-orders-page__stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + margin: 0 0 16px; +} + +.aksrt-orders-page__stat { + padding: 14px 16px; + border-radius: var(--radius-md, 12px); + border: 1px solid var(--color-line); + background: var(--surface-4, #fff); + display: flex; + flex-direction: column; + gap: 6px; +} + +.aksrt-orders-page__stat-label { + font-size: 12px; + color: var(--color-ink-3, #8b8f97); +} + +.aksrt-orders-page__stat-value { + font-size: 22px; + font-weight: 700; + color: var(--color-ink, #1f2329); + line-height: 1.1; +} + +.aksrt-orders-page__type-filters { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin: 0 0 14px; +} + +.aksrt-orders-page__card-type { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + background: color-mix(in srgb, var(--color-primary, #3b82f6) 12%, transparent); + color: var(--color-primary, #3b82f6); + font-size: 12px; + font-weight: 600; + margin-right: 8px; +} + +.aksrt-orders-page__card-head { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +@media (max-width: 640px) { + .aksrt-orders-page__stats { + grid-template-columns: 1fr; + } +} diff --git a/inc/helpers/aksrt-transactions.php b/inc/helpers/aksrt-transactions.php index de90c34a..d190592c 100644 --- a/inc/helpers/aksrt-transactions.php +++ b/inc/helpers/aksrt-transactions.php @@ -52,21 +52,38 @@ function aksrt_get_cart_page_view(int $user_id): array { return \AKSRT\Shop\Shop::get_instance()->get_cart_page_view($user_id); } -function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page = 20, string $status = ''): array { - if ($user_id <= 0 || !class_exists('\AKSRT\Shop\Shop')) { - return [ - 'items' => [], - 'total' => 0, - 'total_pages' => 1, - 'page' => 1, - ]; +function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page = 20, string $status = '', string $product_type = ''): array { + $empty = [ + 'items' => [], + 'total' => 0, + 'total_pages' => 1, + 'page' => 1, + 'stats' => ['total' => 0, 'pending' => 0, 'paid' => 0], + 'product_types' => class_exists('\AKSRT\Core\DomainModel') + ? array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels()) + : [], + ]; + + if ($user_id <= 0) { + return $empty; } - return \AKSRT\Shop\Shop::get_instance()->get_order_center_view($user_id, [ - 'page' => $paged, - 'per_page' => $per_page, - 'status' => $status, - ]); + $args = [ + 'page' => $paged, + 'per_page' => $per_page, + 'status' => $status, + 'product_type' => $product_type, + ]; + + if (class_exists('\AKSRT\Payment\UserOrders')) { + return \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view($user_id, $args); + } + + if (class_exists('\AKSRT\Shop\Shop')) { + return \AKSRT\Shop\Shop::get_instance()->get_order_center_view($user_id, $args); + } + + return $empty; } function aksrt_get_revenue_page_view(int $user_id): array { diff --git a/inc/modules/user-parts/urls.php b/inc/modules/user-parts/urls.php index e565c899..6c054355 100644 --- a/inc/modules/user-parts/urls.php +++ b/inc/modules/user-parts/urls.php @@ -74,12 +74,16 @@ endif; if (!function_exists('aksrt_get_orders_page_url')) : /** * 订单中心页面 URL(模板 templates/pages/page-orders.php)。 - * 未指派页面时返回空串,调用方应隐藏入口。 + * 未指派页面时回退 /orders/(与 Scripts 路由一致),保证「权益与消费」入口可用。 */ function aksrt_get_orders_page_url(): string { - return function_exists('aksrt_get_page_url_by_template') + $url = function_exists('aksrt_get_page_url_by_template') ? aksrt_get_page_url_by_template('templates/pages/page-orders.php') : ''; + if ($url !== '') { + return $url; + } + return home_url('/orders/'); } endif; diff --git a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php b/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php index 4451f987..4e50cac9 100644 --- a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php +++ b/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php @@ -43,30 +43,37 @@ trait ShopControllerFrontendFlowOperations { } public function get_frontend_orders(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->success(['data' => ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1]]); - } - $page = max(1, (int) $request->get_param('page')); $per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20; $status = sanitize_key((string) $request->get_param('status')); + $product_type = sanitize_key((string) $request->get_param('product_type')); - $view = \AKSRT\Shop\Shop::get_instance()->get_order_center_view(get_current_user_id(), [ - 'page' => $page, - 'per_page' => $per_page, - 'status' => $status, - ]); + $args = [ + 'page' => $page, + 'per_page' => $per_page, + 'status' => $status, + 'product_type' => $product_type, + ]; + + if (class_exists('\AKSRT\Payment\UserOrders')) { + $view = \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view(get_current_user_id(), $args); + } elseif (class_exists('\AKSRT\Shop\Shop')) { + $view = \AKSRT\Shop\Shop::get_instance()->get_order_center_view(get_current_user_id(), $args); + } else { + $view = ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1]; + } return $this->success(['data' => $view]); } public function get_frontend_order(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $order = \AKSRT\Shop\Shop::get_instance()->get_order_detail_view(get_current_user_id(), $order_number); + $order = null; + if (class_exists('\AKSRT\Payment\UserOrders')) { + $order = \AKSRT\Payment\UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number); + } elseif (class_exists('\AKSRT\Shop\Shop')) { + $order = \AKSRT\Shop\Shop::get_instance()->get_order_detail_view(get_current_user_id(), $order_number); + } if (!$order) { return $this->error('订单不存在', 404); } @@ -75,15 +82,17 @@ trait ShopControllerFrontendFlowOperations { } public function pay_frontend_order(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - $params = $this->get_json_params($request); - $payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance')); + $payment_method = sanitize_key((string) ($params['payment_method'] ?? '')); $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $result = \AKSRT\Shop\Shop::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method); + if (class_exists('\AKSRT\Payment\UserOrders')) { + $result = \AKSRT\Payment\UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method); + } elseif (class_exists('\AKSRT\Shop\Shop')) { + $result = \AKSRT\Shop\Shop::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method ?: 'balance'); + } else { + return $this->error('订单服务不可用', 500); + } if (empty($result['success'])) { return $this->error($result['message'] ?? '支付失败'); } @@ -92,12 +101,14 @@ trait ShopControllerFrontendFlowOperations { } public function cancel_frontend_order(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $result = \AKSRT\Shop\Shop::get_instance()->cancel_order(get_current_user_id(), $order_number); + if (class_exists('\AKSRT\Payment\UserOrders')) { + $result = \AKSRT\Payment\UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number); + } elseif (class_exists('\AKSRT\Shop\Shop')) { + $result = \AKSRT\Shop\Shop::get_instance()->cancel_order(get_current_user_id(), $order_number); + } else { + return $this->error('订单服务不可用', 500); + } if (empty($result['success'])) { return $this->error($result['message'] ?? '取消订单失败'); } diff --git a/src/AKSRT/Core/DomainModel.php b/src/AKSRT/Core/DomainModel.php index 33185acc..09c1a9c0 100644 --- a/src/AKSRT/Core/DomainModel.php +++ b/src/AKSRT/Core/DomainModel.php @@ -34,6 +34,22 @@ class DomainModel { /** Paid plate follow (zibll order_type=11). product_id = plate post ID. */ public const ORDER_PRODUCT_PLATE_FOLLOW = 'plate_follow'; + /** + * Human labels for aksrt_order.product_type (front-end order center). + * + * @return array + */ + public static function order_product_type_labels(): array { + return [ + self::ORDER_PRODUCT_SHOP => __('商城商品', 'aksrt'), + self::ORDER_PRODUCT_RESOURCE => __('付费内容', 'aksrt'), + self::ORDER_PRODUCT_VIP => __('VIP 会员', 'aksrt'), + self::ORDER_PRODUCT_RECHARGE => __('余额充值', 'aksrt'), + self::ORDER_PRODUCT_POINTS_BUY => __('购买积分', 'aksrt'), + self::ORDER_PRODUCT_PLATE_FOLLOW => __('关注版块', 'aksrt'), + ]; + } + public const PAY_TYPE_NO = 'no'; public const PAY_TYPE_READING = 'reading'; public const PAY_TYPE_DOWNLOAD = 'download'; diff --git a/src/AKSRT/Payment/UserOrders.php b/src/AKSRT/Payment/UserOrders.php new file mode 100644 index 00000000..f439781b --- /dev/null +++ b/src/AKSRT/Payment/UserOrders.php @@ -0,0 +1,528 @@ + [], + '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')]; + } + + $payment_method = sanitize_key($payment_method ?: (string) ($order['payment_method'] ?? 'balance')); + $gateway_manager = class_exists(Gateway::class) ? Gateway::get_instance() : null; + $gateway = $gateway_manager ? $gateway_manager->get_gateway($payment_method) : null; + if (!$gateway) { + return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')]; + } + if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) { + return ['success' => false, 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')]; + } + + $order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number); + $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'), + ], + ]; + } + + $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'), + ]); + + return [ + 'success' => true, + 'data' => [ + 'order' => $this->get_order_detail_view($user_id, $order_number), + 'message' => $payment_result['message'] ?? __('支付成功。', 'aksrt'), + ], + ]; + } + + 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', + ]; + 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); + } +} diff --git a/src/AKSRT/Shop/Shop.php b/src/AKSRT/Shop/Shop.php index c0907c11..b84e48a6 100644 --- a/src/AKSRT/Shop/Shop.php +++ b/src/AKSRT/Shop/Shop.php @@ -266,51 +266,19 @@ class Shop { } public function get_order_center_view(int $user_id, array $args = []): array { - if ($user_id <= 0 || !class_exists('\AKSRT\Payment\OrderRepository')) { - return ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1]; + if (class_exists('\AKSRT\Payment\UserOrders')) { + return \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view($user_id, $args); } - $page = max(1, (int) ($args['page'] ?? 1)); - $per_page = max(1, min(50, (int) ($args['per_page'] ?? 20))); - $status = sanitize_key((string) ($args['status'] ?? '')); - - $repo_args = [ - 'limit' => $per_page, - 'offset' => ($page - 1) * $per_page, - ]; - - if ($status !== '') { - $repo_args['status'] = $status; - } - - $repo = \AKSRT\Payment\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); - } - - return [ - 'items' => $items, - 'total' => $total, - 'total_pages' => max(1, (int) ceil($total / $per_page)), - 'page' => $page, - ]; + return ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => [], 'product_types' => []]; } public function get_order_detail_view(int $user_id, string $order_number): ?array { - if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) { - return null; + if (class_exists('\AKSRT\Payment\UserOrders')) { + return \AKSRT\Payment\UserOrders::get_instance()->get_order_detail_view($user_id, $order_number); } - $order = \AKSRT\Payment\OrderRepository::get_instance()->get_user_order($user_id, $order_number); - if (!$order) { - return null; - } - - return $this->decorate_order_view($order, true); + return null; } public function preview_checkout(int $user_id, array $payload): array { diff --git a/templates/pages/page-orders.php b/templates/pages/page-orders.php index b762f978..9be4d6c9 100644 --- a/templates/pages/page-orders.php +++ b/templates/pages/page-orders.php @@ -3,8 +3,7 @@ * Template Name: AKSRT - Orders * Template Post Type: page * - * Enhanced: 5 tabs, logistics timeline, receipt confirm, - * review button, re-purchase, gift/discount details (§6.10). + * My Orders (权益与消费): multi product_type list + shop fulfillment actions. * * @package AKSRT */ @@ -24,6 +23,8 @@ $per_page = 20; $paged = max(1, (int) ($_GET['order_page'] ?? 1)); $tab = sanitize_key((string) ($_GET['tab'] ?? '')); $status = sanitize_key((string) ($_GET['order_status'] ?? '')); +$type_filter = sanitize_key((string) ($_GET['order_type'] ?? '')); +$section = sanitize_key((string) ($_GET['section'] ?? '')); $tab_status_map = [ 'pending' => \AKSRT\Payment\Order::STATUS_PENDING, @@ -37,14 +38,18 @@ if ($tab !== '' && isset($tab_status_map[$tab])) { } $orders_view = function_exists('aksrt_get_orders_page_view') - ? aksrt_get_orders_page_view($user_id, $paged, $per_page, $status) - : ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1]; + ? aksrt_get_orders_page_view($user_id, $paged, $per_page, $status, $type_filter) + : ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => [], 'product_types' => []]; -$orders = (array) ($orders_view['items'] ?? []); -$total_pages = max(1, (int) ($orders_view['total_pages'] ?? 1)); +$orders = (array) ($orders_view['items'] ?? []); +$total_pages = max(1, (int) ($orders_view['total_pages'] ?? 1)); +$stats = is_array($orders_view['stats'] ?? null) ? $orders_view['stats'] : []; +$type_options = is_array($orders_view['product_types'] ?? null) ? $orders_view['product_types'] : []; +if ($type_options === [] && class_exists('\AKSRT\Core\DomainModel')) { + $type_options = array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels()); +} $shop = class_exists('\AKSRT\Shop\Shop') ? \AKSRT\Shop\Shop::get_instance() : null; -$shipping = class_exists('\AKSRT\Payment\Shipping') ? \AKSRT\Payment\Shipping::get_instance() : null; $tabs = [ '' => __('全部', 'aksrt'), @@ -53,6 +58,23 @@ $tabs = [ 'review' => __('待评价', 'aksrt'), 'after' => __('售后', 'aksrt'), ]; + +$base_query = []; +if ($type_filter !== '') { + $base_query['order_type'] = $type_filter; +} +if ($section !== '') { + $base_query['section'] = $section; +} + +/** + * @param array $args + */ +$orders_url = static function (array $args = []) use ($base_query): string { + $q = array_merge($base_query, $args); + $url = remove_query_arg(['tab', 'order_status', 'order_page', 'order_type', 'section']); + return add_query_arg($q, $url); +}; ?> @@ -65,7 +87,7 @@ $tabs = [ if (function_exists('aksrt_render_commerce_page_header')) { aksrt_render_commerce_page_header([ 'title' => __('我的订单', 'aksrt'), - 'subtitle' => __('查看订单状态、物流进度与售后处理。', 'aksrt'), + 'subtitle' => __('查看商城、付费内容、会员与充值等全部订单。', 'aksrt'), 'active' => 'orders', ]); } else { @@ -73,77 +95,140 @@ $tabs = [ } ?> - + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
- - - + + +
+ +
+ $type_label) : + $is_type_active = (string) $type_key === (string) $type_filter; + $type_url = $orders_url([ + 'order_type' => $type_key !== '' ? $type_key : null, + 'tab' => $tab !== '' ? $tab : null, + 'order_page' => 1, + ]); + if ($type_key === '') { + $type_url = remove_query_arg('order_type', $type_url); + } + ?> + + + + +
+ +
$tab_label) : $is_active = $tab === $tab_key; - $tab_url = $tab_key !== '' ? add_query_arg(['tab' => $tab_key, 'order_page' => 1]) : remove_query_arg(['tab', 'order_status', 'order_page']); - ?> + $tab_url = $tab_key !== '' + ? $orders_url(['tab' => $tab_key, 'order_page' => 1]) + : remove_query_arg(['tab', 'order_status', 'order_page'], $orders_url()); + ?>
- +
-
- + $ptype = (string) ($order['product_type'] ?? ''); + $is_points = !empty($order['is_points']); + $symbol = (string) ($order['currency_symbol'] ?? '¥'); + $actions = is_array($order['actions'] ?? null) ? $order['actions'] : []; + $amount_display = $is_points + ? esc_html($symbol) . ' ' . esc_html((string) (int) round((float) ($order['amount'] ?? 0))) + : '¥' . esc_html(number_format((float) ($order['amount'] ?? 0), 2)); + ?> +
+
-
- +
+ +
- +
- ¥ + ×
-
- - ¥ +
+ + + +
+ +
+
+ +
-
- ¥ - 0) : ?> - () + + 0 && !$is_points) : ?> + ()
-

@@ -160,29 +245,44 @@ $tabs = [

-
- - + + - + - + - + - + - + + + + + + + - +
@@ -193,8 +293,11 @@ $tabs = [ 1) : ?>
$i]); - ?> + $page_url = $orders_url(array_filter([ + 'tab' => $tab !== '' ? $tab : null, + 'order_page' => $i, + ])); + ?> @@ -203,7 +306,7 @@ $tabs = [
- +

@@ -223,9 +326,11 @@ $tabs = [
@@ -266,10 +371,7 @@ $tabs = [
- - get_user_tickets($user_id, 50); ?>
@@ -279,7 +381,7 @@ $tabs = [
@@ -290,23 +392,8 @@ $tabs = [
· - - · -
- -
- -
-
    @@ -318,10 +405,10 @@ $tabs = [
- + - +
@@ -334,8 +421,7 @@ $tabs = [
- - +

@@ -344,12 +430,9 @@ $tabs = [
- - get_user_favorites($user_id); - ?> + ?>