after-sales page -> after-sales-rendered.html // php render-after-sales-harness.php orders -> orders list page -> orders-rendered.html // php render-after-sales-harness.php -> run one handler scenario, print JSON log namespace { define('ABSPATH', __DIR__ . '/'); final class HarnessLog { /** @var list> */ public static array $events = []; public static function add(string $what, array $data = []): void { self::$events[] = ['what' => $what] + $data; } } final class WP_Error { public function __construct(private string $code, private string $message, private array $data = []) {} public function get_error_code(): string { return $this->code; } public function get_error_message(): string { return $this->message; } public function get_error_data(): array { return $this->data; } } // ---------- WP function stubs ---------- function esc_html($s) { return htmlspecialchars((string) $s, ENT_QUOTES); } function esc_attr($s) { return htmlspecialchars((string) $s, ENT_QUOTES); } function esc_url($s) { return (string) $s; } function esc_url_raw($s) { return (string) $s; } function esc_js($s) { return addslashes((string) $s); } function esc_textarea($s) { return htmlspecialchars((string) $s, ENT_QUOTES); } function esc_html__($s, $d = null) { return esc_html($s); } function esc_attr__($s, $d = null) { return esc_attr($s); } function esc_html_e($s, $d = null) { echo esc_html($s); } function __($s, $d = null) { return $s; } function wp_unslash($v) { return $v; } function sanitize_key($s) { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $s)); } function sanitize_text_field($s) { return trim(strip_tags((string) $s)); } function sanitize_textarea_field($s) { return trim(strip_tags((string) $s)); } function selected($a, $b, $echo = true) { $r = ((string) $a === (string) $b) ? " selected='selected'" : ''; if ($echo) echo $r; return $r; } function submit_button($text = null, $type = 'primary', $name = 'submit', $wrap = true, $other = []) { echo ''; } 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 current_time($type = 'mysql') { return '2026-10-08 12:00:00'; } function is_wp_error($t) { return $t instanceof WP_Error; } function absint($v) { return abs((int) $v); } function check_admin_referer($a = -1, $n = '_wpnonce') { \HarnessLog::add('check_admin_referer', ['action' => (string) $a]); } function wp_safe_redirect($url) { \HarnessLog::add('wp_safe_redirect', ['url' => (string) $url]); } 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\Ledger { final class AssetAmount { public static function balance($value): self { return new self(); } public function decimal(): string { return '0.00'; } } } namespace AKSRT\Payment { class Gateway { private static $i; public static function get_instance(): self { return self::$i ?: self::$i = new self(); } public function get_all(): array { return ['balance' => (object) [], 'points' => (object) []]; } } class AfterSaleService { public const STATUS_PENDING = 'pending'; public const STATUS_PROCESSING = 'processing'; public const STATUS_REJECTED = 'rejected'; public const STATUS_CANCELLED = 'cancelled'; public const STATUS_COMPLETED = 'completed'; public const SOURCE_ADMIN = 'admin'; public const SOURCE_USER = 'user'; private static $i; /** @var array> keyed by after-sale id */ public static array $rows = []; public static function get_instance(): self { return self::$i ?: self::$i = new self(); } public function admin_list(array $args = []): array { return array_values(self::$rows); } public function list_for_order(string $order_number): array { return array_values(array_filter(self::$rows, static fn($row): bool => (string) ($row['order_number'] ?? '') === $order_number)); } public function count(array $args = []): int { return count(self::$rows); } public function get(int $id): ?array { return self::$rows[$id] ?? null; } public function can_finalize_order_refund(int $after_sale_id, string $order_number): bool { $row = self::$rows[$after_sale_id] ?? null; $ok = is_array($row) && hash_equals((string) ($row['order_number'] ?? ''), $order_number) && in_array((string) ($row['status'] ?? ''), [self::STATUS_PENDING, self::STATUS_PROCESSING], true); \HarnessLog::add('can_finalize_order_refund', ['id' => $after_sale_id, 'order' => $order_number, 'ticket_status' => is_array($row) ? (string) ($row['status'] ?? '') : null, 'ok' => $ok]); return $ok; } public function find_active(string $order_number): ?array { $found = null; foreach (self::$rows as $row) { if ((string) ($row['order_number'] ?? '') !== $order_number) continue; if (!in_array((string) ($row['status'] ?? ''), [self::STATUS_PENDING, self::STATUS_PROCESSING], true)) continue; if ($found === null || (int) ($row['id'] ?? 0) > (int) ($found['id'] ?? 0)) $found = $row; } \HarnessLog::add('find_active', ['order' => $order_number, 'ticket' => $found === null ? null : (int) ($found['id'] ?? 0)]); return $found; } public function set_status(int $id, string $status, string $note = '') { \HarnessLog::add('set_status', ['id' => $id, 'status' => $status, 'note' => $note]); if (!in_array($status, [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_COMPLETED, self::STATUS_REJECTED, self::STATUS_CANCELLED], true)) { return new \WP_Error('aksrt_after_sale_status', '售后状态无效', ['status' => 400]); } if (!isset(self::$rows[$id])) return new \WP_Error('aksrt_after_sale_state', '售后申请不存在', ['status' => 409]); self::$rows[$id]['status'] = $status; self::$rows[$id]['handling_log'][] = ['status' => $status, 'note' => $note, 'source' => 'admin', 'created_at' => current_time('mysql')]; return self::$rows[$id]; } } class OrderRepository { private static $i; /** @var array> keyed by order number */ public static array $orders = []; public static function get_instance(): self { return self::$i ?: self::$i = new self(); } public function get($n): ?array { return self::$orders[(string) $n] ?? null; } public function get_admin_order_context(string $order_number): array { return ['customer_note' => '买家备注示例']; } public function get_orders_admin(array $args = [], string $search = ''): array { return array_values(self::$orders); } public function get_orders_count(array $args = [], string $search = ''): int { return count(self::$orders); } public function update_status(string $order_number, string $new_status, array $extra = []): bool { \HarnessLog::add('update_status', ['order' => $order_number, 'status' => $new_status, 'extra' => $extra]); // Mirrors OrderRepository::update_status(): refund needs allow_refund, an active // after-sale id, and a valid paid -> refunded transition. if ($new_status !== 'refunded') return true; if (empty($extra['allow_refund'])) return false; $order = self::$orders[$order_number] ?? null; if (!is_array($order) || (string) ($order['status'] ?? '') !== 'paid') return false; self::$orders[$order_number]['status'] = $new_status; return true; } } 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 const ORDER_STATUS_PENDING = 'pending'; public const ORDER_STATUS_PAID = 'paid'; public const ORDER_STATUS_REFUNDED = 'refunded'; public const ORDER_STATUS_CLOSED = 'closed'; public const ORDER_PRODUCT_RESOURCE = 'resource'; public static function resolve_order_product_type(string $t, array $m): string { return $t; } public const ORDER_PRODUCT_READING = 'reading'; public const ORDER_PRODUCT_DOWNLOAD = 'download'; public const ORDER_PRODUCT_IMAGE = 'image'; public const ORDER_PRODUCT_VIDEO = 'video'; public const ORDER_PRODUCT_VIP = 'vip'; public const ORDER_PRODUCT_RECHARGE = 'recharge'; public const ORDER_PRODUCT_POINTS = 'points'; public static function order_product_type_labels(): array { return [ self::ORDER_PRODUCT_READING => '付费阅读', self::ORDER_PRODUCT_DOWNLOAD => '付费下载', self::ORDER_PRODUCT_IMAGE => '付费图片', self::ORDER_PRODUCT_VIDEO => '付费视频', self::ORDER_PRODUCT_VIP => '购买会员', self::ORDER_PRODUCT_RECHARGE => '余额充值', self::ORDER_PRODUCT_POINTS => '购买积分', ]; } public static function order_status_labels(): array { return [ self::ORDER_STATUS_PENDING => '待支付', self::ORDER_STATUS_PAID => '已支付', self::ORDER_STATUS_REFUNDED => '已退款', self::ORDER_STATUS_CLOSED => '已关闭', ]; } public static function order_transitions(): array { return [ self::ORDER_STATUS_PENDING => [self::ORDER_STATUS_PAID, self::ORDER_STATUS_CLOSED], self::ORDER_STATUS_PAID => [self::ORDER_STATUS_REFUNDED], self::ORDER_STATUS_REFUNDED => [], self::ORDER_STATUS_CLOSED => [], ]; } } } 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 __DIR__ . '/../../src/Admin/Native/AdminListPagination.php'; require 'C:/Users/Xing/Desktop/AKSRT/AKSRT-theme/src/Admin/Native/PayOrders.php'; $scenario = $argv[1] ?? ''; // Seed data shared by the render pass and every handler scenario. \AKSRT\Payment\AfterSaleService::$rows = [ 5 => ['id' => 5, 'order_number' => 'ORD100', 'user_id' => 3, 'reason' => 'legacy-reason', 'apply_form' => ['reason_label' => '商品与描述不符', 'materials' => [['label' => '联系方式', 'value' => 'wx-abc']], 'description' => '说明文字'], 'images' => ['https://example.test/img1.png'], 'refund_amount' => '12.50', 'status' => 'pending', 'handling_log' => [ ['status' => 'pending', 'note' => '', 'source' => 'user', 'created_at' => '2026-10-01 10:00:00'], ['status' => 'processing', 'note' => '已联系仓库核实库存', 'source' => 'admin', 'created_at' => '2026-10-01 14:20:00'], ], 'created_at' => '2026-10-01 10:00:00'], 6 => ['id' => 6, 'order_number' => 'ORD101', 'user_id' => 4, 'reason' => 'r2', 'apply_form' => null, 'images' => [], 'refund_amount' => '8.00', 'status' => 'processing', 'handling_log' => [ ['status' => 'pending', 'note' => '', 'source' => 'user', 'created_at' => '2026-10-02 11:00:00'], ['status' => 'processing', 'note' => '资料已补齐,进入处理', 'source' => 'admin', 'created_at' => '2026-10-02 15:40:00'], ['status' => 'rejected', 'note' => '超出七天无理由范围,已驳回', 'source' => 'admin', 'created_at' => '2026-10-03 09:00:00'], ], 'created_at' => '2026-10-02 11:00:00'], ]; \AKSRT\Payment\OrderRepository::$orders = [ 'ORD100' => ['order_number' => 'ORD100', 'user_id' => 3, 'product_id' => 9, 'product_type' => 'reading', 'meta_payload' => ['product_title' => '测试商品'], 'currency' => 'CNY', 'amount' => '12.50', 'payment_method' => 'balance', 'status' => 'paid', 'created_at' => '2026-10-01 09:00:00', 'paid_at' => '2026-10-01 09:05:00'], 'ORD101' => ['order_number' => 'ORD101', 'user_id' => 4, 'product_id' => 10, 'product_type' => 'reading', 'meta_payload' => [], 'currency' => 'CNY', 'amount' => '8.00', 'payment_method' => 'balance', 'status' => 'paid', 'created_at' => '2026-10-02 09:00:00', 'paid_at' => '2026-10-02 09:05:00'], 'ORD102' => ['order_number' => 'ORD102', 'user_id' => 5, 'product_id' => 11, 'product_type' => 'reading', 'meta_payload' => [], 'currency' => 'CNY', 'amount' => '20.00', 'payment_method' => 'balance', 'status' => 'refunded', 'created_at' => '2026-10-03 09:00:00', 'paid_at' => '2026-10-03 09:05:00'], 'ORD103' => ['order_number' => 'ORD103', 'user_id' => 6, 'product_id' => 12, 'product_type' => 'reading', 'meta_payload' => [], 'currency' => 'CNY', 'amount' => '5.00', 'payment_method' => 'balance', 'status' => 'pending', 'created_at' => '2026-10-04 09:00:00', 'paid_at' => ''], 'ORD104' => ['order_number' => 'ORD104', 'user_id' => 7, 'product_id' => 13, 'product_type' => 'reading', 'meta_payload' => [], 'currency' => 'CNY', 'amount' => '7.00', 'payment_method' => 'balance', 'status' => 'closed', 'created_at' => '2026-10-05 09:00:00', 'paid_at' => '2026-10-05 09:05:00'], ]; if ($scenario === 'orders') { $_GET = ['page' => 'aksrt_orders']; $pay = AKSRT\Admin\Native\PayOrders::get_instance(); ob_start(); $pay->render_orders_page(); file_put_contents(__DIR__ . '/orders-rendered.html', ob_get_clean()); exit(0); } // 订单明细页:售后记录单元格按工单渲染完整处理记录。 if ($scenario === 'order-detail') { $_GET = ['page' => 'aksrt_orders', 'order' => 'ORD100']; $pay = AKSRT\Admin\Native\PayOrders::get_instance(); ob_start(); $pay->render_orders_page(); file_put_contents(__DIR__ . '/order-detail-rendered.html', ob_get_clean()); exit(0); } if ($scenario !== '') { // The handler ends in exit(), so the call log is flushed on shutdown. register_shutdown_function(static function (): void { echo json_encode(\HarnessLog::$events, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n"; echo 'rows=' . json_encode(array_map(static fn($r): string => (string) ($r['status'] ?? ''), \AKSRT\Payment\AfterSaleService::$rows), JSON_UNESCAPED_UNICODE) . "\n"; echo 'orders=' . json_encode(array_map(static fn($o): string => (string) ($o['status'] ?? ''), \AKSRT\Payment\OrderRepository::$orders), JSON_UNESCAPED_UNICODE) . "\n"; }); switch ($scenario) { case 'unchecked': $_POST = ['after_sale_id' => 5, 'after_sale_status' => 'completed', 'admin_note' => '已退款']; break; case 'checked_paid': $_POST = ['after_sale_id' => 5, 'after_sale_status' => 'completed', 'mark_order_refunded' => '1', 'admin_note' => '']; break; case 'checked_already_refunded': \AKSRT\Payment\OrderRepository::$orders['ORD100']['status'] = 'refunded'; $_POST = ['after_sale_id' => 5, 'after_sale_status' => 'completed', 'mark_order_refunded' => '1', 'admin_note' => '']; break; case 'checked_unpaid_order': \AKSRT\Payment\OrderRepository::$orders['ORD100']['status'] = 'pending'; $_POST = ['after_sale_id' => 5, 'after_sale_status' => 'completed', 'mark_order_refunded' => '1', 'admin_note' => '']; break; case 'checked_closed_ticket': \AKSRT\Payment\AfterSaleService::$rows[5]['status'] = 'completed'; $_POST = ['after_sale_id' => 5, 'after_sale_status' => 'cancelled', 'mark_order_refunded' => '1', 'admin_note' => '']; break; case 'bad_status': $_POST = ['after_sale_id' => 5, 'after_sale_status' => 'bogus', 'admin_note' => '']; break; case 'refund_paid': $_POST = ['order_number' => 'ORD100']; break; case 'refund_no_ticket': unset(\AKSRT\Payment\AfterSaleService::$rows[5]); $_POST = ['order_number' => 'ORD100']; break; case 'refund_inactive_ticket': \AKSRT\Payment\AfterSaleService::$rows[5]['status'] = 'completed'; $_POST = ['order_number' => 'ORD100']; break; case 'refund_not_paid': \AKSRT\Payment\OrderRepository::$orders['ORD100']['status'] = 'pending'; $_POST = ['order_number' => 'ORD100']; break; case 'refund_already_refunded': \AKSRT\Payment\OrderRepository::$orders['ORD100']['status'] = 'refunded'; $_POST = ['order_number' => 'ORD100']; break; case 'refund_missing': $_POST = ['order_number' => 'ORDNOPE']; break; default: fwrite(STDERR, "unknown scenario: {$scenario}\n"); exit(2); } $pay = AKSRT\Admin\Native\PayOrders::get_instance(); if (str_starts_with($scenario, 'refund')) { $pay->handle_refund_order(); } else { $pay->handle_after_sale_status(); } // Unreachable: the handler ends in exit(). } $_GET = ['page' => 'aksrt_after_sales']; $pay = AKSRT\Admin\Native\PayOrders::get_instance(); $ref = new ReflectionMethod($pay, 'render_after_sales_page'); ob_start(); $ref->invoke($pay); $html = ob_get_clean(); file_put_contents(__DIR__ . '/after-sales-rendered.html', $html); echo 'bytes=' . strlen($html) . "\n"; }