feat(admin): 订单明细页已支付订单新增「退款」按钮

操作栏「详情」右侧为已支付订单渲染「退款」按钮:点击经 aksrt_pay_refund_order 把订单推到已退款。

退款沿既有的受闸路径执行:OrderRepository::update_status 要求 allow_refund,且 can_finalize_order_refund 要求该售后工单仍为待审核/处理中。因此工单号由服务端按订单号反查 find_active 得到,客户端只提交订单号;没有进行中的工单时整次操作中止并给出可操作提示,不会出现订单已退款但无工单依据的状态。

按钮仅在已支付订单上渲染——已退款是已支付的唯一后继状态,其他状态点击也只会在服务端被拒。附带清理 AfterSaleService::find_active 上方一处早已错配的 @param 文档注释(描述的是 \,与该函数签名无关)。
This commit is contained in:
2026-10-08 21:14:56 +08:00
parent 5fd8c6fc31
commit 83c164a427
4 changed files with 188 additions and 9 deletions
+56
View File
@@ -30,6 +30,7 @@ class PayOrders {
add_action('admin_post_aksrt_pay_update_order_note', [$this, 'handle_update_order_note']);
add_action('admin_post_aksrt_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']);
add_action('admin_post_aksrt_pay_delete_order', [$this, 'handle_delete_order']);
add_action('admin_post_aksrt_pay_refund_order', [$this, 'handle_refund_order']);
add_action('admin_post_aksrt_after_sale_update', [$this, 'handle_after_sale_status']);
}
@@ -254,6 +255,15 @@ class PayOrders {
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '<br><span class="description">' . esc_html((string) ($row['paid_at'] ?? '') ?: '—') . '</span></td>';
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '">' . esc_html__('详情', 'aksrt') . '</a>';
// 已支付是已退款的唯一前置状态;退款仍需落到一条进行中的售后工单上。
if ($status_key === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认将该订单标记为已退款?不可恢复。', 'aksrt')) . '\');">';
echo '<input type="hidden" name="action" value="aksrt_pay_refund_order" />';
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
wp_nonce_field('aksrt_pay_refund_order_' . $order_number);
submit_button(__('退款', 'aksrt'), 'small', '', false);
echo '</form>';
}
$this->render_order_quick_status_buttons($order_number, $status_key);
if ($status_key === 'closed') {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
@@ -631,6 +641,47 @@ class PayOrders {
exit;
}
/**
* 订单列表「退款」:把已支付订单推到已退款。退款必须落到一条进行中的售后工单上,
* 工单号由服务端按订单号反查,不经客户端传递。
*/
public function handle_refund_order(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
check_admin_referer('aksrt_pay_refund_order_' . $order_number);
$code = 'order_refund_failed';
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get($order_number);
if (!is_array($order)) {
$code = 'order_refund_missing';
} elseif ((string) ($order['status'] ?? '') !== \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
$code = 'order_refund_not_paid';
} else {
$ticket = class_exists('\AKSRT\Payment\AfterSaleService')
? \AKSRT\Payment\AfterSaleService::get_instance()->find_active($order_number)
: null;
if (!is_array($ticket)) {
$code = 'order_refund_no_ticket';
} else {
$ok = $repo->update_status($order_number, \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED, [
'allow_refund' => true,
'after_sale_id' => (int) ($ticket['id'] ?? 0),
'note' => __('管理员在订单列表中标记退款', 'aksrt'),
]);
$code = $ok ? 'order_refunded' : 'order_refund_failed';
}
}
}
wp_safe_redirect($this->page_url('aksrt_orders', ['aksrt_pay_msg' => $code]));
exit;
}
public function handle_update_order_note(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
@@ -769,6 +820,11 @@ class PayOrders {
$map = [
'order_updated' => __('订单状态已更新。', 'aksrt'),
'order_failed' => __('订单状态更新失败。', 'aksrt'),
'order_refunded' => __('订单已标记为已退款。', 'aksrt'),
'order_refund_failed' => __('订单标记退款失败。', 'aksrt'),
'order_refund_missing' => __('订单不存在。', 'aksrt'),
'order_refund_not_paid' => __('仅已支付订单可以标记退款。', 'aksrt'),
'order_refund_no_ticket' => __('该订单没有进行中的售后工单,无法标记退款;请先在「售后服务」中处理。', 'aksrt'),
'order_note_updated' => __('订单备注已保存。', 'aksrt'),
'order_note_failed' => __('订单备注保存失败。', 'aksrt'),
'orders_cleared' => sprintf(
+2 -2
View File
@@ -313,8 +313,8 @@ final class AfterSaleService {
return $this->get($id);
}
/** @param array<int,array<string,mixed>> $entries */
private function find_active(string $number): ?array {
/** 该订单当前仍在处理中的售后工单(待审核/处理中),退款闸门只认它。 */
public function find_active(string $number): ?array {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table()} WHERE order_number = %s AND status IN ('pending','processing') ORDER BY id DESC LIMIT 1", $number), ARRAY_A);
return is_array($row) ? $this->normalize($row) : null;