From 80373746f858dfea67b28e809566aa44c5d11cbc Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 7 Oct 2026 23:38:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=94=AE=E5=90=8E=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=94=B9=E4=B8=BA=E6=89=8B=E9=A3=8E=E7=90=B4=E5=B1=95?= =?UTF-8?q?=E5=BC=80=E8=AF=A6=E6=83=85=E5=B9=B6=E5=86=85=E8=81=94=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Admin/Native/PayOrders.php | 169 ++++++++++++------ tests/dev/render-after-sales-harness.php | 97 ++++++++++ .../financial-flow-contract.test.cjs | 16 +- 3 files changed, 221 insertions(+), 61 deletions(-) create mode 100644 tests/dev/render-after-sales-harness.php diff --git a/src/Admin/Native/PayOrders.php b/src/Admin/Native/PayOrders.php index f77d0e79..da06daa9 100644 --- a/src/Admin/Native/PayOrders.php +++ b/src/Admin/Native/PayOrders.php @@ -403,6 +403,20 @@ class PayOrders { echo ' '; submit_button(__('筛选', 'aksrt'), 'secondary', '', false); echo ''; + echo ''; echo '
'; echo ''; wp_nonce_field('aksrt_after_sale_bulk'); echo ''; @@ -411,41 +425,48 @@ class PayOrders { echo '
'; echo ''; if (!$items) echo ''; + $status_labels = [\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_APPROVED => '已通过', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成']; foreach ($items as $item) { $id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id']; $user = $uid > 0 ? get_userdata($uid) : null; $order = \AKSRT\Payment\OrderRepository::get_instance()->get($number); $type = $order ? \AKSRT\Core\DomainModel::resolve_order_product_type((string) ($order['product_type'] ?? ''), (array) ($order['meta_payload'] ?? [])) : ''; $type_label = \AKSRT\Core\DomainModel::order_product_type_labels()[$type] ?? $type; - $status_labels = [\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_APPROVED => '已通过', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成']; - $refund_amount = (string) ($item['refund_amount'] ?? '0'); - if ($order && (string) ($order['currency'] ?? '') === 'points') $refund_amount .= ' ' . __('积分', 'aksrt'); else $refund_amount = '¥' . $refund_amount; - $apply_form = is_array($item['apply_form'] ?? null) ? $item['apply_form'] : null; - if ($apply_form) { - $reason_cell = esc_html((string) ($apply_form['reason_label'] ?? '')); - $material_lines = []; - foreach ((array) ($apply_form['materials'] ?? []) as $m) { - $ml = trim((string) ($m['label'] ?? '')); $mv = trim((string) ($m['value'] ?? '')); - if ($ml === '' && $mv === '') continue; - $material_lines[] = esc_html($ml !== '' ? $ml . ':' . $mv : $mv); + $product_name = $order ? (string) (\AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, false)['product_name'] ?? '') : ''; + $refund_amount = (string) ($item['refund_amount'] ?? '0'); + if ($order && (string) ($order['currency'] ?? '') === 'points') $refund_amount .= ' ' . __('积分', 'aksrt'); else $refund_amount = '¥' . $refund_amount; + $apply_form = is_array($item['apply_form'] ?? null) ? $item['apply_form'] : null; + if ($apply_form) { + $reason_cell = esc_html((string) ($apply_form['reason_label'] ?? '')); + $material_lines = []; + foreach ((array) ($apply_form['materials'] ?? []) as $m) { + $ml = trim((string) ($m['label'] ?? '')); $mv = trim((string) ($m['value'] ?? '')); + if ($ml === '' && $mv === '') continue; + $material_lines[] = esc_html($ml !== '' ? $ml . ':' . $mv : $mv); + } + $materials_cell = $material_lines ? implode('
', $material_lines) : '—'; + $desc_text = trim((string) ($apply_form['description'] ?? '')); + $desc_cell = $desc_text !== '' ? nl2br(esc_html($desc_text)) : '—'; + } else { + // 老数据:融合串原样展示在原因列 + $reason_cell = nl2br(esc_html((string) $item['reason'])); + $materials_cell = '—'; + $desc_cell = '—'; } - $materials_cell = $material_lines ? implode('
', $material_lines) : '—'; - $desc_text = trim((string) ($apply_form['description'] ?? '')); - $desc_cell = $desc_text !== '' ? nl2br(esc_html($desc_text)) : '—'; - } else { - // 老数据:融合串原样展示在原因列 - $reason_cell = nl2br(esc_html((string) $item['reason'])); - $materials_cell = '—'; - $desc_cell = '—'; - } $image_urls = array_values(array_filter((array) ($item['images'] ?? []), 'is_string')); $thumbs = []; foreach ($image_urls as $url) { - $thumbs[] = ''; + $thumbs[] = ''; } $images_cell = $thumbs ? implode('', $thumbs) : '—'; - echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; } echo '
ID订单号用户原因申请资料申请说明举证图片退款金额状态操作 / 处理记录
' . esc_html__('暂无售后申请', 'aksrt') . '
' . esc_html((string) $id) . '
' . esc_html((string) ($item['created_at'] ?? '')) . '
' . esc_html($number) . '
' . esc_html($type_label) . ($order ? ' · ' . esc_html(\AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, false)['product_name'] ?? '') : '') . '
' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : '#' . $uid) . '' . $reason_cell . '' . $materials_cell . '' . $desc_cell . '' . $images_cell . '' . esc_html($refund_amount) . '' . esc_html($status_labels[(string) $item['status']] ?? (string) $item['status']) . ''; - // 快捷操作:点击「操作」展开面板,点击链接经 JS 以 POST 提交 + $status_label = $status_labels[(string) $item['status']] ?? (string) $item['status']; + $admin_note = (string) ($item['admin_note'] ?? ''); + $gateway_text = ''; + if (!empty($item['gateway_result'])) { + $result_data = json_decode((string) $item['gateway_result'], true); + $gateway_text = is_array($result_data) ? implode(' · ', array_filter([(string) ($result_data['status'] ?? ''), (string) ($result_data['message'] ?? ''), !empty($result_data['refund_id']) ? '退款单号 ' . (string) $result_data['refund_id'] : ''])) : (string) $item['gateway_result']; + } + // 按状态决定该记录可执行的审核动作(经 JS 以 POST 提交) $quick_actions = []; if ((string) $item['status'] === 'pending') { $quick_actions = ['approve' => __('通过', 'aksrt'), 'reject' => __('拒绝', 'aksrt')]; @@ -456,62 +477,104 @@ class PayOrders { ? ['complete' => __('确认线下退款完成', 'aksrt'), 'reject' => __('确认未退款并拒绝', 'aksrt')] : ['approve' => __('同步退款状态', 'aksrt')]; } + // 摘要行 + 手风琴详情行:点击「操作」展开整条记录的完整信息与审核表单 + echo '
' . esc_html((string) $id) . '
' . esc_html((string) ($item['created_at'] ?? '')) . '
' . esc_html($number) . '
' . esc_html($type_label) . ($product_name !== '' ? ' · ' . esc_html($product_name) : '') . '
' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : '#' . $uid) . '' . $reason_cell . '' . $materials_cell . '' . $desc_cell . '' . $images_cell . '' . esc_html($refund_amount) . '' . esc_html($status_label) . '
'; echo '
'; + // 事件委托:点击「操作」切换手风琴详情;点击审核按钮经 JS 以 POST 提交(详情行在批量表单内,禁止嵌套 form) echo ''; } diff --git a/tests/dev/render-after-sales-harness.php b/tests/dev/render-after-sales-harness.php new file mode 100644 index 00000000..fc68fef0 --- /dev/null +++ b/tests/dev/render-after-sales-harness.php @@ -0,0 +1,97 @@ +' . esc_html($text ?: 'Submit') . ''; } +function wp_nonce_field($a = -1, $n = '_wpnonce', $r = true, $e = true) { if ($e) echo ''; } +function wp_create_nonce($a = -1) { return 'stubnonce' . md5((string) $a); } +function admin_url($p = '') { return 'https://example.test/wp-admin/' . $p; } +function add_query_arg($k, $v = null, $u = null) { if (is_array($k)) { $u = $v ?: ''; $q = http_build_query($k); return $u . (str_contains($u, '?') ? '&' : '?') . $q; } return ($u ?: '') . (str_contains(($u ?: ''), '?') ? '&' : '?') . $k . '=' . $v; } +function wp_json_encode($v, $f = 0) { return json_encode($v, $f); } +function current_user_can($c) { return true; } +function wp_die($m) { throw new Exception('wp_die: ' . $m); } +function get_userdata($id) { return $id > 0 ? (object) ['display_name' => 'User' . $id] : null; } +function get_the_title($id) { return 'Product' . $id; } +function is_admin() { return true; } +} + +namespace AKSRT\Payment { + class AfterSaleService { + public const STATUS_PENDING = 'pending'; + public const STATUS_PROCESSING = 'processing'; + public const STATUS_APPROVED = 'approved'; + public const STATUS_REJECTED = 'rejected'; + public const STATUS_CANCELLED = 'cancelled'; + public const STATUS_COMPLETED = 'completed'; + private static $i; + public static function get_instance(): self { return self::$i ?: self::$i = new self(); } + public function admin_list(array $args = []): array { + return [ + ['id' => 5, 'order_number' => 'ORD100', 'user_id' => 3, 'reason' => 'legacy-reason', + 'apply_form' => ['reason_label' => '商品与描述不符', 'materials' => [['label' => '联系方式', 'value' => 'wx-abc']], 'description' => '说明文字'], + 'images' => [], 'refund_amount' => '12.50', 'status' => 'pending', 'admin_note' => '', 'gateway_result' => '', 'created_at' => '2026-10-01 10:00:00'], + ['id' => 6, 'order_number' => 'ORD101', 'user_id' => 4, 'reason' => 'r2', + 'apply_form' => null, 'images' => [], 'refund_amount' => '8.00', 'status' => 'processing', + 'admin_note' => '已有备注', 'gateway_result' => json_encode(['status' => 'manual', 'message' => '线下退款']), 'created_at' => '2026-10-02 11:00:00'], + ]; + } + } + class OrderRepository { + private static $i; + public static function get_instance(): self { return self::$i ?: self::$i = new self(); } + public function get($n): array { return ['order_number' => $n, 'user_id' => 3, 'product_id' => 9, 'product_type' => 'reading', 'meta_payload' => [], 'currency' => 'CNY', 'status' => 'paid']; } + } + class UserOrders { + private static $i; + public static function get_instance(): self { return self::$i ?: self::$i = new self(); } + public function decorate_order_view(array $o, bool $x): array { return ['product_name' => '测试商品']; } + } +} + +namespace AKSRT\Core { + class DomainModel { + public static function resolve_order_product_type(string $t, array $m): string { return $t; } + public static function order_product_type_labels(): array { return ['reading' => '付费阅读']; } + } +} + +namespace AKSRT\Settings { + class PaymentRuntime { + public static function get_after_sale_settings(): array { + return ['enabled' => true, 'time_limit' => 7, 'reasons' => [['name' => '原因A']], 'materials' => ['截图']]; + } + } +} + +namespace { + require 'C:/Users/Xing/Desktop/AKSRT/AKSRT-theme/src/Admin/Native/PayOrders.php'; + + $_GET = ['page' => 'aksrt_after_sales']; + $pay = AKSRT\Admin\Native\PayOrders::get_instance(); + + $ref = new ReflectionMethod($pay, 'render_after_sales_page'); + $ref->setAccessible(true); + ob_start(); + $ref->invoke($pay); + $html = ob_get_clean(); + file_put_contents(__DIR__ . '/after-sales-rendered.html', $html); + echo 'bytes=' . strlen($html) . "\n"; +} diff --git a/tests/widget-system/financial-flow-contract.test.cjs b/tests/widget-system/financial-flow-contract.test.cjs index 3df1fc04..aa570fec 100644 --- a/tests/widget-system/financial-flow-contract.test.cjs +++ b/tests/widget-system/financial-flow-contract.test.cjs @@ -145,14 +145,14 @@ test('order detail page drops tabs; after-sales becomes a standalone submenu', ( assert.ok(page.includes('申请说明'), 'description column') assert.ok(page.includes('举证图片'), 'evidence images column') assert.ok(page.includes('$images_cell'), 'evidence images cell rendered from item images') - // 操作列仿 WP 文章列表 row-actions:悬停显示的快捷操作链接,经 JS 以 POST 提交 - assert.ok(page.includes('row-actions'), 'row-actions container') - assert.ok(page.includes('aksrt-after-sale-quick-action'), 'quick action links') - assert.ok(!page.includes('aksrt_after_sale_review" />