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'])) { $update_data['note'] = sanitize_textarea_field($extra['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'] ?? ''), ]); /** * 订单到账。 * * @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 模糊匹配) */ public function get_orders_admin(array $args = [], string $search = ''): array { global $wpdb; $where = []; $params = []; $status = sanitize_key((string) ($args['status'] ?? '')); if ($status !== '' && $status !== 'all') { $where[] = 'status = %s'; $params[] = $status; } if ($search !== '') { $where[] = '(order_number LIKE %s OR product_type LIKE %s)'; $like = '%' . $wpdb->esc_like($search) . '%'; $params[] = $like; $params[] = $like; } $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 = []; $status = sanitize_key((string) ($args['status'] ?? '')); if ($status !== '' && $status !== 'all') { $where[] = 'status = %s'; $params[] = $status; } if ($search !== '') { $where[] = '(order_number LIKE %s OR product_type LIKE %s)'; $like = '%' . $wpdb->esc_like($search) . '%'; $params[] = $like; $params[] = $like; } $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); } /** * 获取订单统计 */ 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); } }