generate_order_number(); $wpdb->insert($this->table(), [ 'order_number' => $order_number, 'user_id' => (int) ($data['user_id'] ?? 0), 'product_type' => sanitize_key((string) ($data['product_type'] ?? '')), 'product_id' => (int) ($data['product_id'] ?? 0), 'amount' => (float) ($data['amount'] ?? 0), 'currency' => sanitize_key((string) ($data['currency'] ?? 'CNY')), 'status' => AKSRT_Domain_Model::normalize_order_status((string) ($data['status'] ?? AKSRT_Domain_Model::ORDER_STATUS_PENDING)), '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, 'expires_at' => $data['expires_at'] ?? null, 'note' => sanitize_textarea_field((string) ($data['note'] ?? '')), ], ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']); if ($wpdb->insert_id) { return $order_number; } return ''; } /** * 获取单个订单 */ public function get(string $order_number): ?array { global $wpdb; $row = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$this->table()} WHERE order_number = %s", $order_number ), ARRAY_A); if ($row) { return $this->normalize_row($row); } return null; } /** * 获取用户订单列表 */ public function get_user_orders(int $user_id, array $args = []): array { global $wpdb; $where = []; $params = []; if ($user_id > 0) { $where[] = 'user_id = %d'; $params[] = $user_id; } $status = sanitize_key((string) ($args['status'] ?? '')); if ($status !== '') { $where[] = 'status = %s'; $params[] = $status; } $product_type = sanitize_key((string) ($args['product_type'] ?? '')); if ($product_type !== '') { $where[] = 'product_type = %s'; $params[] = $product_type; } $limit = max(1, min(100, (int) ($args['limit'] ?? 20))); $offset = max(0, (int) ($args['offset'] ?? 0)); $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d"; $params[] = $limit; $params[] = $offset; $rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A); return array_map([$this, 'normalize_row'], (array) $rows); } /** * 获取单个用户订单。 */ public function get_user_order(int $user_id, string $order_number): ?array { $order = $this->get($order_number); if (!$order) { return null; } if ($user_id > 0 && (int) ($order['user_id'] ?? 0) !== $user_id) { return null; } return $order; } /** * 获取用户订单总数。 */ public function count_user_orders(int $user_id, array $args = []): int { global $wpdb; if ($user_id <= 0) { return 0; } $where = ['user_id = %d']; $params = [$user_id]; $status = sanitize_key((string) ($args['status'] ?? '')); if ($status !== '') { $where[] = 'status = %s'; $params[] = $status; } $product_type = sanitize_key((string) ($args['product_type'] ?? '')); if ($product_type !== '') { $where[] = 'product_type = %s'; $params[] = $product_type; } $where_sql = 'WHERE ' . implode(' AND ', $where); $sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}"; return (int) $wpdb->get_var($wpdb->prepare($sql, $params)); } /** * Build a stable admin-facing order context payload for workbench pages. */ public function get_admin_order_context(string $order_number): array { $order_number = sanitize_text_field($order_number); $contexts = $this->get_admin_order_context_map([$order_number]); return $contexts[$order_number] ?? $this->get_empty_admin_order_context($order_number); } /** * Build admin-facing order context payloads in bulk to avoid per-row order queries. * * @param array $order_numbers * @return array> */ public function get_admin_order_context_map(array $order_numbers): array { global $wpdb; $order_numbers = array_values(array_unique(array_filter(array_map( static function ($order_number): string { return sanitize_text_field((string) $order_number); }, $order_numbers )))); if (empty($order_numbers)) { return []; } $contexts = []; foreach ($order_numbers as $order_number) { $contexts[$order_number] = $this->get_empty_admin_order_context($order_number); } $placeholders = implode(', ', array_fill(0, count($order_numbers), '%s')); $sql = "SELECT * FROM {$this->table()} WHERE order_number IN ({$placeholders})"; $rows = $wpdb->get_results($wpdb->prepare($sql, $order_numbers), ARRAY_A); if (empty($rows)) { return $contexts; } $orders = []; foreach ((array) $rows as $row) { $order = $this->normalize_row($row); $orders[(string) ($order['order_number'] ?? '')] = $order; } $resolved_order_numbers = array_keys($orders); $shipping_map = class_exists('AKSRT_Shipping') ? AKSRT_Shipping::get_instance()->get_summary_map($resolved_order_numbers) : []; $after_sale_map = class_exists('AKSRT_After_Sale') ? AKSRT_After_Sale::get_instance()->get_summary_map($resolved_order_numbers) : []; foreach ($orders as $order_number => $order) { $contexts[$order_number] = $this->build_admin_order_context($order, [ 'shipping_summary' => $shipping_map[$order_number] ?? [], 'after_sale_summary' => $after_sale_map[$order_number] ?? [], ]); } return $contexts; } /** * 更新订单状态(带状态机验证) */ /** * Merge operator admin note into order note meta without destroying JSON payload. */ public function update_admin_note(string $order_number, string $admin_note): bool { global $wpdb; $order = $this->get($order_number); if (!$order) { return false; } $raw = isset($order['note']) && is_string($order['note']) ? $order['note'] : ''; $decoded = $raw !== '' ? json_decode($raw, true) : null; $admin_note = sanitize_textarea_field($admin_note); if (is_array($decoded)) { $decoded['admin_note'] = $admin_note; $new_note = wp_json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } elseif ($raw !== '') { $new_note = wp_json_encode([ 'customer_note' => $raw, 'admin_note' => $admin_note, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $new_note = wp_json_encode([ 'admin_note' => $admin_note, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } $result = $wpdb->update( $this->table(), ['note' => $new_note], ['order_number' => $order_number], ['%s'], ['%s'] ); return $result !== false; } public function update_status(string $order_number, string $new_status, array $extra = []): bool { global $wpdb; $valid = AKSRT_Domain_Model::order_status_labels(); if (!isset($valid[$new_status])) return false; $order = $this->get($order_number); if (!$order) return false; $current_status = $order['status'] ?? 'created'; if (empty($extra['force']) && !AKSRT_Domain_Model::can_transition_order_status($current_status, $new_status)) { return false; } $update_data = ['status' => $new_status]; $format = ['%s']; $time_fields = [ 'paid' => 'paid_at', 'delivered' => 'delivered_at', 'completed' => 'completed_at', 'cancelled' => 'cancelled_at', 'refunded' => 'refunded_at', ]; foreach ($time_fields as $status_key => $field) { if ($new_status === $status_key) { $update_data[$field] = current_time('mysql'); $format[] = '%s'; } } if (!empty($extra['payment_method'])) { $update_data['payment_method'] = sanitize_key($extra['payment_method']); $format[] = '%s'; } if (!empty($extra['payment_transaction_id'])) { $update_data['payment_transaction_id'] = sanitize_text_field($extra['payment_transaction_id']); $format[] = '%s'; } if (!empty($extra['note'])) { // Preserve JSON pay meta (coupon/topic_pay/content_pay/…); stash human text as status_note. $status_note = sanitize_textarea_field((string) $extra['note']); $raw_note = isset($order['note']) && is_string($order['note']) ? $order['note'] : ''; $decoded_note = $raw_note !== '' ? json_decode($raw_note, true) : null; if (is_array($decoded_note)) { $decoded_note['status_note'] = $status_note; $merged = wp_json_encode($decoded_note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $update_data['note'] = is_string($merged) ? $merged : $status_note; } else { $update_data['note'] = $status_note; } $format[] = '%s'; } $result = $wpdb->update($this->table(), $update_data, ['order_number' => $order_number], $format, ['%s']); if ($result !== false) { // 到账事件:让充值/VIP/商品履约等挂接(单一履约入口)。 // 仅在「首次」转为 paid 时触发,避免回调多次到达重复履约。 if ($new_status === AKSRT_Domain_Model::ORDER_STATUS_PAID && $current_status !== AKSRT_Domain_Model::ORDER_STATUS_PAID) { $paid_order = array_merge($order, [ 'status' => $new_status, 'payment_method' => $update_data['payment_method'] ?? ($order['payment_method'] ?? ''), 'payment_transaction_id' => $update_data['payment_transaction_id'] ?? ($order['payment_transaction_id'] ?? ''), ]); if (isset($update_data['note'])) { // Keep pre-update JSON payload for listeners (coupon burn etc.); // only attach status_note if callers mutated note. $pre_meta = []; $pre_raw = isset($order['note']) && is_string($order['note']) ? $order['note'] : ''; if ($pre_raw !== '') { $pre_decoded = json_decode($pre_raw, true); if (is_array($pre_decoded)) { $pre_meta = $pre_decoded; } } if ($pre_meta !== []) { $paid_order['meta_payload'] = $pre_meta; // Prefer merged DB note (JSON + status_note) for downstream reads. $paid_order['note'] = $update_data['note']; } } /** * 订单到账。 * * @param string $order_number 订单号 * @param array $paid_order 订单数据(status=paid) */ do_action('aksrt_order_paid', $order_number, $paid_order); } } return $result !== false; } /** * 管理后台订单列表 — 支持 search(order_number / product_type 模糊匹配) */ /** * Paid orders that still need shipping (no shipping row yet). * * @return array{items: array>, total: int, page: int, max_pages: int} */ public function get_pending_shipment_orders(array $args = []): array { global $wpdb; $limit = max(1, min(100, (int) ($args['limit'] ?? 20))); $offset = max(0, (int) ($args['offset'] ?? 0)); $search = sanitize_text_field((string) ($args['search'] ?? $args['s'] ?? '')); $order_table = $this->table(); // Must match AKSRT_Shipping::table() key. $shipping_table = class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('order_shipping') : $wpdb->prefix . 'aksrt_order_shipping'; $where = ["o.status = %s"]; $params = ['paid']; if ($search !== '') { $where[] = '(o.order_number LIKE %s OR o.product_type LIKE %s)'; $like = '%' . $wpdb->esc_like($search) . '%'; $params[] = $like; $params[] = $like; } // NOT EXISTS avoids ONLY_FULL_GROUP_BY issues from LEFT JOIN + GROUP BY. $where[] = "NOT EXISTS ( SELECT 1 FROM {$shipping_table} s WHERE s.order_number = o.order_number )"; $where_sql = 'WHERE ' . implode(' AND ', $where); $count_sql = "SELECT COUNT(*) FROM {$order_table} o {$where_sql}"; $total = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)); $sql = "SELECT o.* FROM {$order_table} o {$where_sql} ORDER BY COALESCE(o.paid_at, o.created_at) DESC, o.id DESC LIMIT %d OFFSET %d"; $query_params = $params; $query_params[] = $limit; $query_params[] = $offset; $rows = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A); $items = array_map([$this, 'normalize_row'], (array) $rows); return [ 'items' => $items, 'total' => $total, 'page' => (int) floor($offset / $limit) + 1, 'max_pages' => (int) ceil(max(1, $total) / $limit), ]; } public function get_orders_admin(array $args = [], string $search = ''): array { global $wpdb; [$where, $params] = $this->build_admin_order_filters($args, $search); $limit = max(1, min(100, (int) ($args['limit'] ?? 20))); $offset = max(0, (int) ($args['offset'] ?? 0)); $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d"; $params[] = $limit; $params[] = $offset; $rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A); return array_map([$this, 'normalize_row'], (array) $rows); } /** * 管理后台订单计数 — 与 get_orders_admin 共用过滤条件 */ public function get_orders_count(array $args = [], string $search = ''): int { global $wpdb; [$where, $params] = $this->build_admin_order_filters($args, $search); $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}"; if (!empty($params)) { return (int) $wpdb->get_var($wpdb->prepare($sql, $params)); } return (int) $wpdb->get_var($sql); } /** * Aggregate paid content (resource) sales by product_id (post ID). * * @param array{ * product_type?: string, * product_ids?: int[], * status_in?: string[], * limit?: int, * offset?: int, * orderby?: string, * order?: string * } $args * @return array{items: list, total: int} */ public function get_content_sales_summary(array $args = []): array { global $wpdb; $product_type = sanitize_key((string) ($args['product_type'] ?? AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE)); if ($product_type === '') { $product_type = AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE; } $status_in = $args['status_in'] ?? AKSRT_Domain_Model::paid_order_statuses(); if (!is_array($status_in) || empty($status_in)) { $status_in = AKSRT_Domain_Model::paid_order_statuses(); } $status_in = array_values(array_filter(array_map('sanitize_key', $status_in))); if (empty($status_in)) { return ['items' => [], 'total' => 0]; } $where = ['product_type = %s', 'product_id > 0']; $params = [$product_type]; $status_placeholders = implode(', ', array_fill(0, count($status_in), '%s')); $where[] = "status IN ({$status_placeholders})"; foreach ($status_in as $st) { $params[] = $st; } $product_ids = $args['product_ids'] ?? []; if (is_array($product_ids) && !empty($product_ids)) { $ids = array_values(array_unique(array_filter(array_map('intval', $product_ids), static function ($id) { return $id > 0; }))); if (!empty($ids)) { $id_ph = implode(', ', array_fill(0, count($ids), '%d')); $where[] = "product_id IN ({$id_ph})"; foreach ($ids as $id) { $params[] = $id; } } } $where_sql = 'WHERE ' . implode(' AND ', $where); $table = $this->table(); $total_sql = "SELECT COUNT(DISTINCT product_id) FROM {$table} {$where_sql}"; $total = (int) $wpdb->get_var($wpdb->prepare($total_sql, $params)); $orderby_map = [ 'sales_count' => 'sales_count', 'revenue' => 'revenue', 'avg_amount' => 'avg_amount', 'product_id' => 'product_id', ]; $orderby_key = sanitize_key((string) ($args['orderby'] ?? 'sales_count')); $orderby_col = $orderby_map[$orderby_key] ?? 'sales_count'; $order_dir = strtoupper((string) ($args['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; $limit = max(1, min(100, (int) ($args['limit'] ?? 20))); $offset = max(0, (int) ($args['offset'] ?? 0)); $list_sql = "SELECT product_id, product_type, COUNT(*) AS sales_count, AVG(amount) AS avg_amount, SUM(amount) AS revenue FROM {$table} {$where_sql} GROUP BY product_id, product_type ORDER BY {$orderby_col} {$order_dir} LIMIT %d OFFSET %d"; $list_params = $params; $list_params[] = $limit; $list_params[] = $offset; $rows = $wpdb->get_results($wpdb->prepare($list_sql, $list_params), ARRAY_A); $items = []; foreach ((array) $rows as $row) { $items[] = [ 'product_id' => (int) ($row['product_id'] ?? 0), 'product_type' => (string) ($row['product_type'] ?? $product_type), 'sales_count' => (int) ($row['sales_count'] ?? 0), 'avg_amount' => round((float) ($row['avg_amount'] ?? 0), 2), 'revenue' => round((float) ($row['revenue'] ?? 0), 2), ]; } return [ 'items' => $items, 'total' => $total, ]; } /** * Paid order totals for a set of product IDs (resource content). * * @param int[] $product_ids * @return array */ public function get_content_sales_map(array $product_ids, string $product_type = ''): array { $ids = array_values(array_unique(array_filter(array_map('intval', $product_ids), static function ($id) { return $id > 0; }))); if (empty($ids)) { return []; } if ($product_type === '') { $product_type = AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE; } $summary = $this->get_content_sales_summary([ 'product_type' => $product_type, 'product_ids' => $ids, 'limit' => max(count($ids), 1), 'offset' => 0, 'orderby' => 'product_id', 'order' => 'ASC', ]); $map = []; foreach ($summary['items'] as $item) { $pid = (int) ($item['product_id'] ?? 0); if ($pid <= 0) { continue; } $map[$pid] = [ 'sales_count' => (int) ($item['sales_count'] ?? 0), 'avg_amount' => (float) ($item['avg_amount'] ?? 0), 'revenue' => (float) ($item['revenue'] ?? 0), ]; } return $map; } /** * Shared WHERE builder for admin order list/count. * * @return array{0: string[], 1: array} */ private function build_admin_order_filters(array $args, string $search = ''): array { global $wpdb; $where = []; $params = []; $status = sanitize_key((string) ($args['status'] ?? '')); if ($status !== '' && $status !== 'all') { $where[] = 'status = %s'; $params[] = $status; } $product_type = sanitize_key((string) ($args['product_type'] ?? '')); if ($product_type !== '') { $where[] = 'product_type = %s'; $params[] = $product_type; } $product_id = (int) ($args['product_id'] ?? 0); if ($product_id > 0) { $where[] = 'product_id = %d'; $params[] = $product_id; } $user_id = (int) ($args['user_id'] ?? 0); if ($user_id > 0) { $where[] = 'user_id = %d'; $params[] = $user_id; } $payment_method = sanitize_key((string) ($args['payment_method'] ?? '')); if ($payment_method !== '') { $where[] = 'payment_method = %s'; $params[] = $payment_method; } if ($search !== '') { // Numeric search may mean user_id or product_id; also match order number / type. if (ctype_digit($search)) { $where[] = '(order_number LIKE %s OR product_type LIKE %s OR user_id = %d OR product_id = %d)'; $like = '%' . $wpdb->esc_like($search) . '%'; $params[] = $like; $params[] = $like; $params[] = (int) $search; $params[] = (int) $search; } else { $where[] = '(order_number LIKE %s OR product_type LIKE %s OR payment_method LIKE %s)'; $like = '%' . $wpdb->esc_like($search) . '%'; $params[] = $like; $params[] = $like; $params[] = $like; } } return [$where, $params]; } /** * Delete closed (cancelled/expired) orders older than $days days. * Mirrors zibll ZibPay::clear_order(14). * * @return int Number of rows deleted. */ public function clear_closed_orders(int $days = 14): int { global $wpdb; $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(NULLIF(cancelled_at, ''), created_at) < %s"; $deleted = $wpdb->query($wpdb->prepare($sql, $cutoff)); return $deleted === false ? 0 : (int) $deleted; } /** * Hard-delete a single order by order_number (admin hygiene). */ public function delete_order(string $order_number): bool { global $wpdb; $order_number = sanitize_text_field($order_number); if ($order_number === '') { return false; } $ok = $wpdb->delete($this->table(), ['order_number' => $order_number], ['%s']); return $ok !== false && $ok > 0; } /** * 获取订单统计 */ public function get_stats(int $user_id = 0): array { global $wpdb; $where = ''; $params = []; if ($user_id > 0) { $where = 'WHERE 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 = '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') THEN amount ELSE 0 END) as revenue FROM {$this->table()} {$where}"; if (!empty($params)) { $row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A); } else { $row = $wpdb->get_row($sql, ARRAY_A); } return [ '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), 'cancelled' => (int) ($row['cancelled'] ?? 0), 'refunded' => (int) ($row['refunded'] ?? 0), 'revenue' => (float) ($row['revenue'] ?? 0), ]; } public function has_paid_access(int $user_id, string $product_type, int $product_id): bool { if ($user_id <= 0 || $product_id <= 0) { return false; } return null !== $this->find_latest_success_order($user_id, $product_type, $product_id); } public function get_valid_vip_order(int $user_id): ?array { return $this->find_latest_success_order($user_id, AKSRT_Domain_Model::ORDER_PRODUCT_VIP, 0, true); } /* ------------------------------------------------------------------ * Migration bridge: option → table * ----------------------------------------------------------------*/ /** * 一次性迁移:把 option 中的订单搬到表中。 * 幂等 — 已存在的 order_number 不会重复插入。 */ public function migrate_from_option(): int { global $wpdb; $orders = get_option('aksrt_orders', []); if (!is_array($orders) || empty($orders)) return 0; $migrated = 0; foreach ($orders as $order_number => $data) { $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$this->table()} WHERE order_number = %s", $order_number )); if ($existing) continue; $wpdb->insert($this->table(), [ 'order_number' => $order_number, 'user_id' => (int) ($data['user_id'] ?? 0), 'product_type' => sanitize_key((string) ($data['product_type'] ?? '')), 'product_id' => (int) ($data['product_id'] ?? 0), 'amount' => (float) ($data['amount'] ?? 0), 'currency' => sanitize_key((string) ($data['currency'] ?? 'CNY')), 'status' => AKSRT_Domain_Model::normalize_order_status((string) ($data['status'] ?? AKSRT_Domain_Model::ORDER_STATUS_PENDING)), 'payment_method' => sanitize_key((string) ($data['payment_method'] ?? '')), 'created_at' => $data['created_at'] ?? current_time('mysql'), 'paid_at' => $data['paid_at'] ?? null, 'delivered_at' => $data['delivered_at'] ?? null, 'completed_at' => $data['completed_at'] ?? null, 'cancelled_at' => $data['cancelled_at'] ?? null, 'refunded_at' => $data['refunded_at'] ?? null, 'expires_at' => $data['expires_at'] ?? null, 'note' => '', ], ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']); if ($wpdb->insert_id) $migrated++; } update_option(AKSRT_Domain_Model::ORDER_MIGRATED_OPTION, time()); return $migrated; } /* ------------------------------------------------------------------ * Internal helpers * ----------------------------------------------------------------*/ private function normalize_row(array $row): array { $row['id'] = (int) $row['id']; $row['user_id'] = (int) $row['user_id']; $row['product_id'] = (int) $row['product_id']; $row['amount'] = (float) $row['amount']; $row['status'] = AKSRT_Domain_Model::normalize_order_status((string) ($row['status'] ?? '')); $row['meta_payload'] = []; if (!empty($row['note']) && is_string($row['note'])) { $decoded = json_decode($row['note'], true); if (is_array($decoded)) { $row['meta_payload'] = $decoded; if (isset($decoded['customer_note']) && is_string($decoded['customer_note'])) { $row['note_text'] = $decoded['customer_note']; } } else { $row['note_text'] = $row['note']; } } return $row; } private function find_latest_success_order(int $user_id, string $product_type, int $product_id = 0, bool $ignore_product_id = false): ?array { global $wpdb; if ($user_id <= 0 || $product_type === '') { return null; } $status_placeholders = implode(', ', array_fill(0, count(AKSRT_Domain_Model::paid_order_statuses()), '%s')); $params = array_merge( [$user_id, sanitize_key($product_type)], $ignore_product_id ? [] : [$product_id], AKSRT_Domain_Model::paid_order_statuses() ); $product_sql = $ignore_product_id ? '' : ' AND product_id = %d'; $sql = "SELECT * FROM {$this->table()} WHERE user_id = %d AND product_type = %s{$product_sql} AND status IN ({$status_placeholders}) ORDER BY COALESCE(paid_at, created_at) DESC, id DESC LIMIT 1"; $row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A); return is_array($row) ? $this->normalize_row($row) : null; } private function get_empty_admin_order_context(string $order_number): array { return [ 'exists' => false, 'summary' => [ 'order_number' => $order_number, 'status' => '', 'status_label' => '', 'amount' => 0.0, 'currency_symbol' => '¥', 'created_at' => '', 'paid_at' => '', ], 'buyer' => [], 'address' => [], 'payment_method' => [], 'shipping_method' => [], 'shipping' => [ 'count' => 0, 'latest' => [], ], 'after_sale' => [ 'count' => 0, 'latest' => [], ], 'coupon' => [], 'pricing' => [ 'subtotal' => 0.0, 'shipping_fee' => 0.0, 'discount' => 0.0, 'amount' => 0.0, ], 'items' => [], 'customer_note' => '', 'raw_order' => null, ]; } /** * @param array $order * @param array $related * @return array */ private function build_admin_order_context(array $order, array $related = []): array { $meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : []; $user = get_userdata((int) ($order['user_id'] ?? 0)); $status = (string) ($order['status'] ?? ''); $address = is_array($meta['address'] ?? null) ? $meta['address'] : []; $payment_method = is_array($meta['payment_method'] ?? null) ? $meta['payment_method'] : []; $shipping_method = is_array($meta['shipping_method'] ?? null) ? $meta['shipping_method'] : []; $coupon = is_array($meta['coupon'] ?? null) ? $meta['coupon'] : []; $items = is_array($meta['items'] ?? null) ? $meta['items'] : []; $shipping_fee = (float) ($meta['shipping_fee'] ?? 0); $discount = (float) ($meta['discount'] ?? 0); $amount = (float) ($order['amount'] ?? 0); $subtotal = $this->calculate_order_subtotal($items, max(0, $amount + $discount - $shipping_fee)); $shipping_summary = is_array($related['shipping_summary'] ?? null) ? $related['shipping_summary'] : []; $after_sale_summary = is_array($related['after_sale_summary'] ?? null) ? $related['after_sale_summary'] : []; $address['full_text'] = $this->build_address_text($address); return [ 'exists' => true, 'summary' => [ 'order_number' => (string) ($order['order_number'] ?? ''), 'status' => $status, 'status_label' => class_exists('AKSRT_Order') ? (AKSRT_Order::get_valid_statuses()[$status] ?? $status) : $status, 'amount' => $amount, 'currency_symbol' => '¥', 'created_at' => (string) ($order['created_at'] ?? ''), 'paid_at' => (string) ($order['paid_at'] ?? ''), 'product_type' => (string) ($order['product_type'] ?? ''), 'product_id' => (int) ($order['product_id'] ?? 0), ], 'buyer' => [ 'id' => (int) ($order['user_id'] ?? 0), 'display_name' => $user ? (string) $user->display_name : '', 'user_login' => $user ? (string) $user->user_login : '', 'user_email' => $user ? (string) $user->user_email : '', ], 'address' => $address, 'payment_method' => [ 'value' => (string) ($payment_method['value'] ?? $order['payment_method'] ?? ''), 'label' => (string) ($payment_method['label'] ?? $order['payment_method'] ?? ''), ], 'shipping_method' => [ 'value' => (string) ($shipping_method['value'] ?? ''), 'label' => (string) ($shipping_method['label'] ?? ''), 'fee' => $shipping_fee, ], 'shipping' => [ 'count' => (int) ($shipping_summary['count'] ?? 0), 'latest' => is_array($shipping_summary['latest'] ?? null) ? $shipping_summary['latest'] : [], ], 'after_sale' => [ 'count' => (int) ($after_sale_summary['count'] ?? 0), 'latest' => is_array($after_sale_summary['latest'] ?? null) ? $after_sale_summary['latest'] : [], ], 'coupon' => $coupon, 'pricing' => [ 'subtotal' => $subtotal, 'shipping_fee' => $shipping_fee, 'discount' => $discount, 'amount' => $amount, ], 'items' => $items, 'customer_note' => (string) ($meta['customer_note'] ?? $order['note_text'] ?? ''), 'raw_order' => $order, ]; } /** * @param array> $items */ private function calculate_order_subtotal(array $items, float $fallback): float { if (empty($items)) { return round($fallback, 2); } $subtotal = 0.0; foreach ($items as $item) { if (isset($item['line_total'])) { $subtotal += (float) $item['line_total']; continue; } $subtotal += (float) ($item['price'] ?? 0) * max(1, (int) ($item['quantity'] ?? 1)); } if ($subtotal <= 0) { return round($fallback, 2); } return round($subtotal, 2); } private function build_address_text(array $address): string { $segments = []; foreach (['name', 'phone', 'province', 'city', 'district', 'address'] as $key) { $value = trim((string) ($address[$key] ?? '')); if ($value !== '') { $segments[] = $value; } } if (!empty($address['full_text']) && is_string($address['full_text'])) { return (string) $address['full_text']; } return implode(' / ', $segments); } private function generate_order_number(): string { return 'AKS' . date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT); } }