feat(ux): after-sale multi-step, ban appeal, message types, coupon wallet

Orders center gains after-sale tickets (return ship/cancel/timeline),
coupon wallet list, ban-appeal page, and expanded message center tabs.
This commit is contained in:
kate522 committed 2026-07-18 22:48:08 +08:00
1 parent 3a883bc3d0
commit 1337664ed3
15 files changed
+999 -37

No files matched your search

+123 -6
View File
@@ -75,7 +75,12 @@ class AfterSale {
'created_at' => current_time('mysql'),
]);
return $result ? (int) $wpdb->insert_id : 0;
if (!$result) {
return 0;
}
$ticket_id = (int) $wpdb->insert_id;
$this->append_event($ticket_id, 'created', __('用户提交售后申请', 'aksrt'), $user_id);
return $ticket_id;
}
/**
@@ -657,21 +662,133 @@ class AfterSale {
return false;
}
$return_data = wp_json_encode([
'carrier' => sanitize_text_field($carrier),
'tracking_no' => sanitize_text_field($tracking_no),
$return_data = [
'carrier' => sanitize_text_field($carrier),
'tracking_no' => sanitize_text_field($tracking_no),
'submitted_at' => current_time('mysql'),
], JSON_UNESCAPED_UNICODE);
];
// Keep human-readable note and structured shipping meta.
$note = sprintf(
/* translators: 1: carrier 2: tracking */
__('用户已寄回:%1$s %2$s', 'aksrt'),
$return_data['carrier'],
$return_data['tracking_no']
);
$wpdb->update($this->table(), [
'note' => $return_data,
'note' => $note,
'updated_at' => current_time('mysql'),
], ['id' => $ticket_id]);
update_post_meta($ticket_id, '_aksrt_as_progress', self::PROGRESS_WAITING_MERCHANT);
update_post_meta($ticket_id, '_aksrt_as_return_shipping', $return_data);
$this->append_event($ticket_id, 'return_shipped', $note, $user_id);
return true;
}
/**
* User cancels a pending after-sale ticket.
*/
public function user_cancel(int $ticket_id, int $user_id, string $reason = ''): bool {
global $wpdb;
$record = $this->get_by_id($ticket_id);
if (!$record || (int) ($record['user_id'] ?? 0) !== $user_id) {
return false;
}
$status = (string) ($record['status'] ?? '');
if (!in_array($status, [self::STATUS_PENDING, self::STATUS_PROCESSING], true)) {
return false;
}
// Only allow cancel before merchant confirmed return completion.
$progress = (int) get_post_meta($ticket_id, '_aksrt_as_progress', true);
if ($status === self::STATUS_PROCESSING && $progress === self::PROGRESS_DONE) {
return false;
}
$wpdb->update($this->table(), [
'status' => self::STATUS_CANCELLED,
'updated_at' => current_time('mysql'),
], ['id' => $ticket_id]);
$msg = $reason !== ''
? sprintf(__('用户取消售后:%s', 'aksrt'), sanitize_text_field($reason))
: __('用户取消售后', 'aksrt');
$this->append_event($ticket_id, 'user_cancelled', $msg, $user_id);
return true;
}
/**
* @return list<array{time:string,event:string,label:string,user_id:int}>
*/
public function get_events(int $ticket_id): array {
$events = get_post_meta($ticket_id, '_aksrt_as_events', true);
return is_array($events) ? array_values($events) : [];
}
public function append_event(int $ticket_id, string $event, string $label, int $user_id = 0): void {
$events = $this->get_events($ticket_id);
$events[] = [
'time' => current_time('mysql'),
'event' => sanitize_key($event),
'label' => sanitize_text_field($label),
'user_id' => $user_id,
];
// Keep last 50 events.
if (count($events) > 50) {
$events = array_slice($events, -50);
}
update_post_meta($ticket_id, '_aksrt_as_events', $events);
}
/**
* List tickets for a frontend user.
*/
public function get_user_tickets(int $user_id, int $limit = 50): array {
if ($user_id <= 0) {
return [];
}
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$this->table()} WHERE user_id = %d ORDER BY created_at DESC LIMIT %d",
$user_id,
max(1, min(100, $limit))
), ARRAY_A);
$items = [];
foreach ((array) $rows as $row) {
$items[] = $this->decorate_user_ticket($row);
}
return $items;
}
/**
* @param array<string,mixed> $row
* @return array<string,mixed>
*/
public function decorate_user_ticket(array $row): array {
$id = (int) ($row['id'] ?? 0);
$decorated = method_exists($this, 'decorate_record') ? $this->decorate_record($row) : $row;
$progress = (int) get_post_meta($id, '_aksrt_as_progress', true);
$labels = self::get_progress_labels();
$return = get_post_meta($id, '_aksrt_as_return_shipping', true);
$status = (string) ($row['status'] ?? '');
$type = (string) ($row['type'] ?? '');
$needs_ship = in_array($type, [self::TYPE_REFUND_RETURN, self::TYPE_EXCHANGE, self::TYPE_REPLACEMENT], true)
&& $status === self::STATUS_PROCESSING
&& $progress === self::PROGRESS_WAITING_USER_SHIP;
$decorated['id'] = $id;
$decorated['progress'] = $progress;
$decorated['progress_label'] = $labels[$progress] ?? '';
$decorated['return_shipping'] = is_array($return) ? $return : null;
$decorated['events'] = $this->get_events($id);
$decorated['actions'] = [
'can_cancel' => in_array($status, [self::STATUS_PENDING, self::STATUS_PROCESSING], true),
'can_return_ship' => $needs_ship,
];
return $decorated;
}
public function merchant_confirm_return(int $ticket_id, int $operator_id, string $note = ''): bool {
global $wpdb;
$record = $this->get_by_id($ticket_id);