insert($this->table(), [ 'order_number' => $order_number, 'carrier' => $carrier, 'tracking_no' => $tracking_no, 'status' => self::STATUS_SHIPPED, 'note' => $note, 'operator_id' => $operator_id ?: null, 'created_at' => current_time('mysql'), ]); $insert_id = $result ? (int) $wpdb->insert_id : 0; if ($insert_id > 0) { $this->sync_order_status($order_number, self::STATUS_SHIPPED, sprintf( __('发货记录已创建:%1$s %2$s', 'aksrt'), $this->get_carrier_label($carrier), $tracking_no )); // R68: Email notification on shipping creation (§6.6) $this->send_shipping_notification($order_number, $carrier, $tracking_no, $note); } return $insert_id; } /** * 获取发货记录列表 */ public function get_list(array $args = []): array { global $wpdb; $limit = max(1, min(100, (int) ($args['limit'] ?? 20))); $offset = max(0, (int) ($args['offset'] ?? 0)); $include_order_context = !empty($args['include_order_context']); $status = sanitize_key((string) ($args['status'] ?? '')); $order_number = sanitize_text_field((string) ($args['order_number'] ?? '')); $date_from = sanitize_text_field((string) ($args['date_from'] ?? '')); $date_to = sanitize_text_field((string) ($args['date_to'] ?? '')); $tracking_no = sanitize_text_field((string) ($args['tracking_no'] ?? '')); $where = []; $params = []; if ($status !== '') { $where[] = 'status = %s'; $params[] = $status; } if ($order_number !== '') { $where[] = 'order_number = %s'; $params[] = $order_number; } if ($tracking_no !== '') { $where[] = 'tracking_no LIKE %s'; $params[] = '%' . $wpdb->esc_like($tracking_no) . '%'; } if ($date_from !== '') { $where[] = 'created_at >= %s'; $params[] = $date_from . ' 00:00:00'; } if ($date_to !== '') { $where[] = 'created_at <= %s'; $params[] = $date_to . ' 23:59:59'; } $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}"; $total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql); $sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d"; $params[] = $limit; $params[] = $offset; $items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A); $items = array_map([$this, 'decorate_record'], (array) $items); if ($include_order_context && !empty($items) && class_exists('AKSRT_Order_Repository')) { $contexts = AKSRT_Order_Repository::get_instance()->get_admin_order_context_map(array_column($items, 'order_number')); foreach ($items as &$item) { $item['order_context'] = $contexts[(string) ($item['order_number'] ?? '')] ?? []; } unset($item); } return [ 'items' => $items ?: [], 'total' => $total, 'page' => floor($offset / $limit) + 1, 'max_pages' => ceil($total / $limit), ]; } /** * 获取单个发货记录 */ public function get(int $id): ?array { global $wpdb; $row = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$this->table()} WHERE id = %d", $id ), ARRAY_A); return $row ? $this->decorate_record($row) : null; } /** * 获取订单的发货记录 */ public function get_by_order(string $order_number): array { global $wpdb; $items = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$this->table()} WHERE order_number = %s ORDER BY created_at DESC", $order_number ), ARRAY_A); return array_map([$this, 'decorate_record'], (array) $items); } /** * 获取单个发货记录并附带订单上下文。 */ public function get_with_order_context(int $id): ?array { $record = $this->get($id); if (!$record) { return null; } $record['order_context'] = class_exists('AKSRT_Order_Repository') ? AKSRT_Order_Repository::get_instance()->get_admin_order_context((string) ($record['order_number'] ?? '')) : []; return $record; } /** * @param array $order_numbers * @return array> */ public function get_summary_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 []; } $map = []; foreach ($order_numbers as $order_number) { $map[$order_number] = [ 'count' => 0, 'latest' => [], ]; } $placeholders = implode(', ', array_fill(0, count($order_numbers), '%s')); $sql = "SELECT * FROM {$this->table()} WHERE order_number IN ({$placeholders}) ORDER BY created_at DESC, id DESC"; $rows = $wpdb->get_results($wpdb->prepare($sql, $order_numbers), ARRAY_A); foreach ((array) $rows as $row) { $record = $this->decorate_record($row); $order_number = (string) ($record['order_number'] ?? ''); if (!isset($map[$order_number])) { continue; } $map[$order_number]['count']++; if (empty($map[$order_number]['latest'])) { $map[$order_number]['latest'] = $this->build_summary_item($record); } } return $map; } public function batch_update_status(array $ids, string $new_status, string $note = '', int $operator_id = 0): array { $ids = array_values(array_unique(array_filter(array_map('intval', $ids)))); $result = [ 'updated_ids' => [], 'failed' => [], ]; foreach ($ids as $id) { $record = $this->get($id); if (!$record) { $result['failed'][] = [ 'id' => $id, 'reason' => __('发货记录不存在', 'aksrt'), ]; continue; } if (!$this->can_transition_status((string) ($record['status'] ?? ''), $new_status)) { $result['failed'][] = [ 'id' => $id, 'reason' => __('当前状态不允许批量更新到目标状态', 'aksrt'), ]; continue; } if ($this->update_status($id, $new_status, $note, $operator_id)) { $result['updated_ids'][] = $id; } else { $result['failed'][] = [ 'id' => $id, 'reason' => __('状态更新失败', 'aksrt'), ]; } } return $result; } /** * 更新发货状态 */ public function update_status(int $id, string $new_status, string $note = '', int $operator_id = 0): bool { global $wpdb; $record = $this->get($id); if (!$record) { return false; } $valid = [self::STATUS_SHIPPED, self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED, self::STATUS_RETURNED]; if (!in_array($new_status, $valid, true)) return false; if (!$this->can_transition_status((string) ($record['status'] ?? ''), $new_status)) { return false; } $update = [ 'status' => $new_status, 'updated_at' => current_time('mysql'), ]; if ($note !== '') { $update['note'] = sanitize_textarea_field($note); } if ($operator_id > 0) { $update['operator_id'] = $operator_id; } if ($new_status === self::STATUS_DELIVERED) { $update['delivered_at'] = current_time('mysql'); } $updated = (bool) $wpdb->update($this->table(), $update, ['id' => $id]); if ($updated) { $this->sync_order_status( (string) ($record['order_number'] ?? ''), $new_status, $note !== '' ? $note : sprintf(__('物流状态更新为:%s', 'aksrt'), $this->get_status_label($new_status)) ); } return $updated; } /** * 获取统计 */ public function get_stats(): array { global $wpdb; $table = $this->table(); $counts = $wpdb->get_results( "SELECT status, COUNT(*) as cnt FROM {$table} GROUP BY status", ARRAY_A ); $counts = array_column($counts, 'cnt', 'status'); $stats = [ 'total' => 0, 'shipped' => 0, 'in_transit' => 0, 'delivered' => 0, 'returned' => 0, ]; foreach ($counts as $status => $cnt) { if (isset($stats[$status])) { $stats[$status] = (int) $cnt; } $stats['total'] += (int) $cnt; } return $stats; } /** * 获取常用快递公司列表 */ public static function get_carrier_options(): array { return [ ['value' => 'sf', 'label' => '顺丰速运'], ['value' => 'yd', 'label' => '韵达快递'], ['value' => 'zt', 'label' => '中通快递'], ['value' => 'yt', 'label' => '圆通速递'], ['value' => 'sto', 'label' => '申通快递'], ['value' => 'jd', 'label' => '京东物流'], ['value' => 'ems', 'label' => 'EMS'], ['value' => 'db', 'label' => '德邦快递'], ['value' => 'yunda', 'label' => '韵达快递'], ['value' => 'other', 'label' => '其他'], ]; } public static function get_status_options(): array { return [ ['value' => self::STATUS_SHIPPED, 'label' => __('已发货', 'aksrt')], ['value' => self::STATUS_IN_TRANSIT, 'label' => __('运输中', 'aksrt')], ['value' => self::STATUS_DELIVERED, 'label' => __('已签收', 'aksrt')], ['value' => self::STATUS_RETURNED, 'label' => __('已退回', 'aksrt')], ]; } /** * Send email notification to order customer when shipping record is created. */ }