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:
1 parent
3a883bc3d0
commit
1337664ed3
15 files changed
+999
-37
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user