false, 'message' => __('售后系统不可用。', 'aksrt')]; } $order_number = sanitize_text_field((string) ($payload['order_number'] ?? '')); $reason = sanitize_textarea_field((string) ($payload['reason'] ?? '')); $type = sanitize_key((string) ($payload['type'] ?? AKSRT_After_Sale::TYPE_REFUND)); if ($order_number === '' || $reason === '') { return ['success' => false, 'message' => __('请填写售后原因。', 'aksrt')]; } $order = $this->get_order_detail_view($user_id, $order_number); if (!$order) { return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; } $ticket_id = AKSRT_After_Sale::get_instance()->create([ 'order_number' => $order_number, 'user_id' => $user_id, 'type' => $type, 'reason' => $reason, 'amount' => (float) ($order['amount'] ?? 0), 'note' => sanitize_textarea_field((string) ($payload['note'] ?? '')), ]); if ($ticket_id <= 0) { return ['success' => false, 'message' => __('售后申请提交失败。', 'aksrt')]; } return [ 'success' => true, 'data' => [ 'ticket_id' => $ticket_id, 'order' => $this->get_order_detail_view($user_id, $order_number), ], ]; } public function get_product_archive_view(array $args = []): array { $taxonomy = sanitize_key((string) ($args['taxonomy'] ?? '')); $term_id = (int) ($args['term_id'] ?? 0); $paged = max(1, (int) ($args['paged'] ?? $args['page'] ?? 1)); $shop_settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_shop_settings() : []; $list = is_array($shop_settings['list'] ?? null) ? $shop_settings['list'] : []; $per_page = max(4, min(100, (int) ($args['per_page'] ?? ($list['count'] ?? 12)))); $orderby = sanitize_key((string) ($args['orderby'] ?? ($list['orderby'] ?? 'date'))); if (!in_array($orderby, ['date', 'sales', 'price_asc', 'price_desc', 'comments', 'modified'], true)) { $orderby = 'date'; } $paging = sanitize_key((string) ($args['paging'] ?? ($list['paging'] ?? 'number'))); if (!in_array($paging, ['number', 'ajax'], true)) { $paging = 'number'; } $ias_s = array_key_exists('ias_s', $args) ? !empty($args['ias_s']) : !empty($list['ias_s']); $ias_max = max(1, min(50, (int) ($args['ias_max'] ?? ($list['ias_max'] ?? 5)))); $style = sanitize_key((string) ($list['style'] ?? 'default')); $empty = [ 'products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0, 'page' => $paged, 'orderby' => $orderby, 'paging' => $paging, 'ias_s' => $ias_s, 'ias_max' => $ias_max, 'per_page' => $per_page, 'style' => $style, 'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [], ]; if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true) && empty($args['allow_all'])) { return $empty; } $query_args = [ 'post_type' => 'aksrt_product', 'posts_per_page' => $per_page, 'paged' => $paged, 'post_status' => 'publish', 'order' => 'DESC', 'orderby' => 'date', ]; $term = null; if ($taxonomy !== '' && $term_id > 0) { $term = get_term($term_id, $taxonomy); if (is_wp_error($term) || !$term) { return $empty; } $query_args['tax_query'] = [[ 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $term_id, ]]; } if ($orderby === 'sales') { $query_args['meta_key'] = '_aksrt_sales'; $query_args['orderby'] = 'meta_value_num'; $query_args['order'] = 'DESC'; } elseif ($orderby === 'price_asc') { $query_args['meta_key'] = '_aksrt_price'; $query_args['orderby'] = 'meta_value_num'; $query_args['order'] = 'ASC'; } elseif ($orderby === 'price_desc') { $query_args['meta_key'] = '_aksrt_price'; $query_args['orderby'] = 'meta_value_num'; $query_args['order'] = 'DESC'; } elseif ($orderby === 'comments') { $query_args['orderby'] = 'comment_count'; $query_args['order'] = 'DESC'; } elseif ($orderby === 'modified') { $query_args['orderby'] = 'modified'; $query_args['order'] = 'DESC'; } $query = new WP_Query($query_args); $products = []; while ($query->have_posts()) { $query->the_post(); $products[] = $this->build_product_card_view(get_the_ID()); } wp_reset_postdata(); return [ 'products' => $products, 'term' => $term ? [ 'id' => (int) $term->term_id, 'name' => (string) $term->name, 'description' => (string) $term->description, 'count' => (int) $term->count, 'taxonomy' => $taxonomy, ] : null, 'total' => (int) $query->found_posts, 'max_pages' => (int) $query->max_num_pages, 'page' => $paged, 'orderby' => $orderby, 'paging' => $paging, 'ias_s' => $ias_s, 'ias_max' => $ias_max, 'per_page' => $per_page, 'style' => $style, 'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [], ]; } public function get_product_categories(): array { $terms = get_terms([ 'taxonomy' => 'aksrt_product_cat', 'hide_empty' => true, 'orderby' => 'count', 'order' => 'DESC', ]); if (is_wp_error($terms)) { return []; } return array_map(static function ($t) { return [ 'id' => $t->term_id, 'name' => $t->name, 'count' => $t->count, 'url' => get_term_link($t), ]; }, $terms); } /* ============================================================ * Merchant / Store Mode (§6.10) * ============================================================ */ public function get_store_profile(int $author_id): array { if ($author_id <= 0) return []; $user = get_userdata($author_id); if (!$user) return []; $product_count = $this->count_author_products($author_id); $shop_settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_shop_settings() : []; $author_show = !isset($shop_settings['general']['shop_author_show']) || !empty($shop_settings['general']['shop_author_show']); if (!$author_show) return []; $contact = $this->get_store_contact($author_id); return [ 'author_id' => $author_id, 'name' => (string) $user->display_name, 'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($author_id) : get_avatar_url($author_id), 'description' => (string) get_the_author_meta('description', $author_id), 'url' => get_author_posts_url($author_id), 'product_count' => $product_count, 'contact' => $contact, 'addresses' => $this->get_user_address_book($author_id), ]; } public function get_store_products(int $author_id, int $page = 1, int $per_page = 12): array { $query = new WP_Query([ 'post_type' => 'aksrt_product', 'author' => $author_id, 'posts_per_page' => $per_page, 'paged' => $page, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', ]); $products = []; while ($query->have_posts()) { $query->the_post(); $products[] = $this->build_product_card_view(get_the_ID()); } wp_reset_postdata(); return [ 'items' => $products, 'total' => (int) $query->found_posts, 'max_pages' => (int) $query->max_num_pages, ]; } public function count_author_products(int $author_id): int { global $wpdb; return (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'aksrt_product' AND post_status = 'publish' AND post_author = %d", $author_id )); } private function get_store_contact(int $author_id): array { $raw = get_user_meta($author_id, 'aksrt_shop_contact', true); if (!is_array($raw)) return []; $contacts = []; foreach ($raw as $c) { $name = sanitize_text_field((string) ($c['name'] ?? '')); if ($name === '') continue; $contacts[] = [ 'name' => $name, 'icon' => sanitize_text_field((string) ($c['icon'] ?? '')), 'link' => esc_url_raw((string) ($c['link'] ?? '')), 'qr' => esc_url_raw((string) ($c['qr'] ?? '')), ]; } return $contacts; } public function get_stats(): array { $product_count = 0; $post_type_obj = get_post_type_object('aksrt_product'); if ($post_type_obj) { $counts = wp_count_posts('aksrt_product'); $product_count = (int) ($counts->publish ?? 0); } return [ 'product_count' => $product_count, 'order_count' => class_exists('AKSRT_Order_Repository') ? AKSRT_Order_Repository::get_instance()->get_stats()['total'] : 0, 'revenue_total' => class_exists('AKSRT_Order_Repository') ? AKSRT_Order_Repository::get_instance()->get_stats()['revenue'] : 0, ]; } public function resolve_main_image(int $product_id): string { return $this->product_service()->resolve_main_image($product_id); } public function build_product_card_view(int $product_id): array { return $this->product_service()->build_product_card_view($product_id); } private function decorate_order_view(array $order, bool $with_detail = false): array { $meta = $order['meta_payload'] ?? []; $order_number = (string) ($order['order_number'] ?? ''); $order_items = $this->get_order_items($order_number); $has_structured_items = !empty($order_items); $product = $has_structured_items ? $this->build_product_card_view((int) ($order_items[0]['product_id'] ?? 0)) : $this->build_product_card_view((int) ($order['product_id'] ?? 0)); $shipping_records = class_exists('AKSRT_Shipping') ? AKSRT_Shipping::get_instance()->get_by_order($order_number) : []; $after_sale_records = class_exists('AKSRT_After_Sale') ? AKSRT_After_Sale::get_instance()->get_list([ 'limit' => 20, 'offset' => 0, 'order_number' => $order_number, ]) : ['items' => []]; $status = (string) ($order['status'] ?? AKSRT_Order::STATUS_PENDING); $subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount']); $shipping_fee = (float) ($meta['shipping_fee'] ?? 0); $discount = (float) ($meta['discount'] ?? 0); $expires_at = (string) ($order['expires_at'] ?? ''); $payment_value = (string) ($meta['payment_method']['value'] ?? $order['payment_method'] ?? 'balance'); $view = [ 'id' => (int) ($order['id'] ?? 0), 'order_number' => (string) ($order['order_number'] ?? ''), 'amount' => (float) ($order['amount'] ?? 0), 'status' => $status, 'status_label' => AKSRT_Order::get_valid_statuses()[$status] ?? $status, 'product_name' => $product['title'] ?? __('商品订单', 'aksrt'), 'product' => $product, 'created_at' => (string) ($order['created_at'] ?? ''), 'paid_at' => (string) ($order['paid_at'] ?? ''), 'expires_at' => $expires_at, 'currency_symbol' => '¥', 'subtotal' => $subtotal, 'shipping_fee' => $shipping_fee, 'discount' => $discount, 'address' => $meta['address'] ?? [], 'payment_method' => $meta['payment_method'] ?? ['value' => $order['payment_method'] ?? '', 'label' => __('站内余额', 'aksrt')], 'coupon' => $meta['coupon'] ?? [], 'items' => $has_structured_items ? $order_items : ($meta['items'] ?? []), 'shipping_method' => $meta['shipping_method'] ?? [], 'customer_note' => $meta['customer_note'] ?? '', 'pay_url' => '#', 'actions' => [ 'can_pay' => in_array($status, [AKSRT_Order::STATUS_PENDING, AKSRT_Order::STATUS_EXPIRED], true) && $payment_value !== 'manual_review', 'can_cancel' => in_array($status, [AKSRT_Order::STATUS_PENDING, AKSRT_Order::STATUS_CREATED, AKSRT_Order::STATUS_EXPIRED], true), 'can_after_sale' => in_array($status, [AKSRT_Order::STATUS_PAID, AKSRT_Order::STATUS_DELIVERED, AKSRT_Order::STATUS_COMPLETED], true), 'can_receipt' => $status === AKSRT_Order::STATUS_DELIVERED, ], 'time_left' => $this->get_order_countdown($expires_at, $status), ]; if ($with_detail) { $view['shipping_records'] = $shipping_records; $view['after_sale_records'] = $after_sale_records['items'] ?? []; $view['timeline'] = $this->build_order_timeline($order, $shipping_records, $after_sale_records['items'] ?? []); } return $view; } 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, [AKSRT_Order::STATUS_PENDING, AKSRT_Order::STATUS_EXPIRED], true)) { return 0; } $left = strtotime($expires_at) - time(); return max(0, $left); } public function get_limit_buy_config(int $product_id): array { return $this->product_service()->get_limit_buy_config($product_id); } public function get_product_services(int $product_id): array { return $this->product_service()->get_product_services($product_id); } public function get_product_params(int $product_id): array { return $this->product_service()->get_product_params($product_id); } public function get_address_book(int $user_id): array { return $this->address_service()->get_address_book($user_id); } public function add_address(int $user_id, array $data): array { return $this->address_service()->add_address($user_id, $data); } public function update_address(int $user_id, int $address_id, array $data): array { return $this->address_service()->update_address($user_id, $address_id, $data); } public function delete_address(int $user_id, int $address_id): array { return $this->address_service()->delete_address($user_id, $address_id); } public function set_default_address(int $user_id, int $address_id): array { return $this->address_service()->set_default_address($user_id, $address_id); } public function get_payment_method_options(): array { return $this->product_service()->get_payment_method_options(); } public function get_shipping_method_options(): array { return $this->product_service()->get_shipping_method_options(); } /* ============================================================ * Product Variants / SKU * ============================================================ */ public function get_variants(int $product_id): array { return $this->product_service()->get_variants($product_id); } public function get_variant(int $variant_id): ?array { return $this->product_service()->get_variant($variant_id); } }