Move ~94 root class shells and root *-traits into domain directories (core, auth, user, credits, content, payment/gateways, forum, shop, sms, ai, messages, extensions, widgets, settings, admin). Update bootstrap require paths, co-locate traits with owners, fix forum/user relative requires, drop duplicate shop controller requires, and wire home-modules load. Path-only; class names and init order unchanged.
374 lines
12 KiB
PHP
374 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* AKSRT Shipping — 物流管理
|
|
*
|
|
* 管理订单发货、物流单号、发货状态。
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class AKSRT_Shipping {
|
|
use AKSRT_Shipping_Part_1;
|
|
use AKSRT_Shipping_Part_2;
|
|
|
|
|
|
private static $instance = null;
|
|
|
|
const STATUS_SHIPPED = 'shipped';
|
|
const STATUS_IN_TRANSIT = 'in_transit';
|
|
const STATUS_DELIVERED = 'delivered';
|
|
const STATUS_RETURNED = 'returned';
|
|
|
|
public static function get_instance(): self {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
private function table(): string {
|
|
return AKSRT_DB_Migrator::table('order_shipping');
|
|
}
|
|
|
|
/**
|
|
* 创建发货记录
|
|
*/
|
|
public function create(array $data): int {
|
|
global $wpdb;
|
|
|
|
$order_number = sanitize_text_field((string) ($data['order_number'] ?? ''));
|
|
$carrier = sanitize_text_field((string) ($data['carrier'] ?? ''));
|
|
$tracking_no = sanitize_text_field((string) ($data['tracking_no'] ?? ''));
|
|
$note = sanitize_textarea_field((string) ($data['note'] ?? ''));
|
|
$operator_id = (int) ($data['operator_id'] ?? 0);
|
|
|
|
if ($order_number === '' || $carrier === '' || $tracking_no === '') {
|
|
return 0;
|
|
}
|
|
|
|
$result = $wpdb->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<int, string> $order_numbers
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
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.
|
|
*/
|
|
}
|