get($order_number); if (!$order) { return; } $user_id = (int) ($order['user_id'] ?? 0); if ($user_id <= 0) { return; } $user = get_userdata($user_id); if (!$user || empty($user->user_email)) { return; } $carrier_label = $this->get_carrier_label($carrier); $site_name = get_bloginfo('name'); $subject = sprintf(__('[%s] 您的订单已发货', 'aksrt'), $site_name); $body = sprintf( __('您好 %1$s,\n\n您的订单 %2$s 已发货。\n\n物流公司:%3$s\n运单号:%4$s\n%5$s\n\n如有疑问请联系客服。\n\n%s', 'aksrt'), $user->display_name, $order_number, $carrier_label, $tracking_no, $note !== '' ? sprintf(__('备注:%s', 'aksrt'), $note) : '', $site_name ); wp_mail($user->user_email, $subject, $body); } private function decorate_record(array $record): array { $record['id'] = (int) ($record['id'] ?? 0); $record['operator_id'] = isset($record['operator_id']) ? (int) $record['operator_id'] : 0; $record['status_label'] = $this->get_status_label((string) ($record['status'] ?? '')); $record['carrier_label'] = $this->get_carrier_label((string) ($record['carrier'] ?? '')); $record['operator'] = $this->get_user_summary((int) ($record['operator_id'] ?? 0)); return $record; } private function get_carrier_label(string $carrier): string { foreach (self::get_carrier_options() as $item) { if (($item['value'] ?? '') === $carrier) { return (string) ($item['label'] ?? $carrier); } } return $carrier; } private function get_status_label(string $status): string { foreach (self::get_status_options() as $item) { if (($item['value'] ?? '') === $status) { return (string) ($item['label'] ?? $status); } } return $status; } private function get_user_summary(int $user_id): array { if ($user_id <= 0) { return []; } $user = get_userdata($user_id); if (!$user) { return []; } return [ 'id' => $user_id, 'display_name' => (string) $user->display_name, 'user_login' => (string) $user->user_login, ]; } private function build_summary_item(array $record): array { return [ 'id' => (int) ($record['id'] ?? 0), 'status' => (string) ($record['status'] ?? ''), 'status_label' => (string) ($record['status_label'] ?? ''), 'carrier' => (string) ($record['carrier'] ?? ''), 'carrier_label' => (string) ($record['carrier_label'] ?? ''), 'tracking_no' => (string) ($record['tracking_no'] ?? ''), 'created_at' => (string) ($record['created_at'] ?? ''), 'updated_at' => (string) ($record['updated_at'] ?? ''), 'delivered_at' => (string) ($record['delivered_at'] ?? ''), ]; } private function can_transition_status(string $current_status, string $new_status): bool { $map = [ self::STATUS_SHIPPED => [self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED, self::STATUS_RETURNED], self::STATUS_IN_TRANSIT => [self::STATUS_DELIVERED, self::STATUS_RETURNED], self::STATUS_DELIVERED => [], self::STATUS_RETURNED => [], ]; return in_array($new_status, $map[$current_status] ?? [], true); } private function sync_order_status(string $order_number, string $shipping_status, string $note = ''): void { if ($order_number === '' || !class_exists('AKSRT_Order_Repository') || !class_exists('AKSRT_Order')) { return; } if (!in_array($shipping_status, [self::STATUS_SHIPPED, self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED], true)) { return; } AKSRT_Order_Repository::get_instance()->update_status($order_number, AKSRT_Order::STATUS_DELIVERED, [ 'note' => $note, ]); } /* ============================================================ * Express Query API (§6.6 快递查询) * * Supports kuaidi100, aliyun, kdniao. * Traces cached in order_shipping.express_data JSON. * Refresh interval from shop settings. * ============================================================ */ public function query_express(int $shipping_id, bool $force = false): array { global $wpdb; $record = $this->get($shipping_id); if (!$record) { return ['success' => false, 'message' => __('发货记录不存在', 'aksrt')]; } $tracking_no = (string) ($record['tracking_no'] ?? ''); $carrier = (string) ($record['carrier'] ?? ''); if ($tracking_no === '' || $carrier === '') { return ['success' => false, 'message' => __('缺少运单号或物流公司', 'aksrt')]; } $cached = $this->get_express_cache($shipping_id); if (!$force && !empty($cached) && !$this->should_refresh_cache($cached)) { return ['success' => true, 'data' => $cached]; } if (!empty($cached['signed'])) { return ['success' => true, 'data' => $cached]; } $shop_settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_shop_settings() : []; $api_sdk = sanitize_key((string) ($shop_settings['express']['api_sdk'] ?? 'kuaidi100')); $result = $this->call_express_api($api_sdk, $carrier, $tracking_no, $shop_settings); if ($result['success']) { $express_data = [ 'query_time' => current_time('mysql'), 'traces' => $result['traces'] ?? [], 'state' => $result['state'] ?? 'unknown', 'signed' => !empty($result['signed']), 'api' => $api_sdk, ]; $wpdb->update( $this->table(), ['note' => wp_json_encode($express_data, JSON_UNESCAPED_UNICODE)], ['id' => $shipping_id], ['%s'], ['%d'] ); if (!empty($result['signed']) && ($record['status'] ?? '') !== self::STATUS_DELIVERED) { $this->update_status($shipping_id, self::STATUS_DELIVERED, __('快递已签收(API检测)', 'aksrt')); } return ['success' => true, 'data' => $express_data]; } return $result; } private function get_express_cache(int $shipping_id): array { global $wpdb; $note = $wpdb->get_var($wpdb->prepare( "SELECT note FROM {$this->table()} WHERE id = %d", $shipping_id )); if (!$note) return []; $data = json_decode($note, true); return is_array($data) && isset($data['traces']) ? $data : []; } private function should_refresh_cache(array $cached): bool { if (empty($cached['query_time'])) return true; $shop = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_shop_settings() : []; $interval = max(30, min(240, (int) ($shop['express']['query_interval'] ?? 60))); $last = strtotime($cached['query_time']); return (time() - $last) > ($interval * 60); } private function call_express_api(string $sdk, string $carrier, string $tracking_no, array $settings): array { $carrier_code = $this->map_carrier_code($sdk, $carrier); if ($sdk === 'kuaidi100') { return $this->query_kuaidi100($carrier_code, $tracking_no, $settings); } if ($sdk === 'kdniao') { return $this->query_kdniao($carrier_code, $tracking_no, $settings); } if ($sdk === 'aliyun') { return $this->query_aliyun($carrier_code, $tracking_no, $settings); } return ['success' => false, 'message' => __('未配置快递查询接口', 'aksrt')]; } private function query_kuaidi100(string $com, string $num, array $settings): array { $key = sanitize_text_field((string) ($settings['express']['kuaidi100_key'] ?? '')); $customer = sanitize_text_field((string) ($settings['express']['kuaidi100_customer'] ?? '')); if ($key === '' || $customer === '') { return ['success' => false, 'message' => __('快递100 API 未配置', 'aksrt')]; } $param = wp_json_encode(['com' => $com, 'num' => $num]); $sign = strtoupper(md5($param . $key . $customer)); $response = wp_remote_post('https://poll.kuaidi100.com/poll/query.do', [ 'body' => ['customer' => $customer, 'sign' => $sign, 'param' => $param], 'timeout' => 10, ]); if (is_wp_error($response)) { return ['success' => false, 'message' => $response->get_error_message()]; } $body = json_decode(wp_remote_retrieve_body($response), true); if (!is_array($body) || (int) ($body['status'] ?? 0) !== 200) { return ['success' => false, 'message' => (string) ($body['message'] ?? __('查询失败', 'aksrt'))]; } $state = (string) ($body['state'] ?? '0'); $signed = $state === '3'; $traces = []; foreach ((array) ($body['data'] ?? []) as $item) { $traces[] = [ 'time' => (string) ($item['ftime'] ?? $item['time'] ?? ''), 'context' => (string) ($item['context'] ?? ''), ]; } return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed]; } private function query_kdniao(string $code, string $num, array $settings): array { $app_id = sanitize_text_field((string) ($settings['express']['kdniao_app_id'] ?? '')); $app_key = sanitize_text_field((string) ($settings['express']['kdniao_app_key'] ?? '')); if ($app_id === '' || $app_key === '') { return ['success' => false, 'message' => __('快递鸟 API 未配置', 'aksrt')]; } $request_data = wp_json_encode(['ShipperCode' => $code, 'LogisticCode' => $num]); $data_sign = urlencode(base64_encode(md5($request_data . $app_key))); $response = wp_remote_post('https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx', [ 'body' => [ 'RequestData' => urlencode($request_data), 'EBusinessID' => $app_id, 'RequestType' => '1002', 'DataSign' => $data_sign, 'DataType' => '2', ], 'timeout' => 10, ]); if (is_wp_error($response)) { return ['success' => false, 'message' => $response->get_error_message()]; } $body = json_decode(wp_remote_retrieve_body($response), true); if (!is_array($body) || !($body['Success'] ?? false)) { return ['success' => false, 'message' => (string) ($body['Reason'] ?? __('查询失败', 'aksrt'))]; } $state = (string) ($body['State'] ?? '0'); $signed = $state === '3'; $traces = []; foreach ((array) ($body['Traces'] ?? []) as $item) { $traces[] = [ 'time' => (string) ($item['AcceptTime'] ?? ''), 'context' => (string) ($item['AcceptStation'] ?? ''), ]; } return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed]; } private function query_aliyun(string $code, string $num, array $settings): array { $app_code = sanitize_text_field((string) ($settings['express']['aliyun_app_code'] ?? '')); if ($app_code === '') { return ['success' => false, 'message' => __('阿里云快递 API 未配置', 'aksrt')]; } $url = add_query_arg(['no' => $num, 'type' => $code], 'https://wuliu.market.alicloudapi.com/kdi'); $response = wp_remote_get($url, [ 'headers' => ['Authorization' => 'APPCODE ' . $app_code], 'timeout' => 10, ]); if (is_wp_error($response)) { return ['success' => false, 'message' => $response->get_error_message()]; } $body = json_decode(wp_remote_retrieve_body($response), true); if (!is_array($body) || (int) ($body['status'] ?? -1) !== 0) { return ['success' => false, 'message' => (string) ($body['msg'] ?? __('查询失败', 'aksrt'))]; } $result_data = $body['result'] ?? []; $state = (string) ($result_data['deliverystatus'] ?? '0'); $signed = $state === '3'; $traces = []; foreach ((array) ($result_data['list'] ?? []) as $item) { $traces[] = [ 'time' => (string) ($item['time'] ?? ''), 'context' => (string) ($item['status'] ?? ''), ]; } return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed]; } private function map_carrier_code(string $sdk, string $carrier): string { $kuaidi100_map = [ 'sf' => 'shunfeng', 'yd' => 'yunda', 'zt' => 'zhongtong', 'yt' => 'yuantong', 'sto' => 'shentong', 'jd' => 'jd', 'ems' => 'ems', 'db' => 'debangkuaidi', 'yunda' => 'yunda', ]; $kdniao_map = [ 'sf' => 'SF', 'yd' => 'YD', 'zt' => 'ZTO', 'yt' => 'YTO', 'sto' => 'STO', 'jd' => 'JD', 'ems' => 'EMS', 'db' => 'DBL', 'yunda' => 'YD', ]; if ($sdk === 'kuaidi100') { return $kuaidi100_map[$carrier] ?? $carrier; } if ($sdk === 'kdniao') { return $kdniao_map[$carrier] ?? strtoupper($carrier); } return $carrier; } }