Files
wordpress-theme-aksrt/src/Admin/Native/PayOrders.php
T
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

402 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 支付中心「订单明细」:列表 / 详情两个视图与订单运维动作。
*
* 列表与详情渲染分别在 PayOrdersList / PayOrdersDetail(flat sibling traits,
* PSR-4 要求与宿主类同目录);这里保留入口、admin-post 处理器与两个视图共用的展示助手。
* 页面样式在 assets/css/admin/orders.css,危险操作确认在 assets/js/admin-orders.js。
*
* @package Jio
*/
namespace Jio\Admin\Native;
use Jio\Core\DomainModel;
defined('ABSPATH') || exit;
class PayOrders {
use PayOrdersList;
use PayOrdersDetail;
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'jio_orders';
private static $instance = null;
/** @var array<string, string>|null */
private ?array $payment_labels = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('admin_post_jio_pay_update_order_status', [$this, 'handle_update_order_status']);
add_action('admin_post_jio_pay_update_order_note', [$this, 'handle_update_order_note']);
add_action('admin_post_jio_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']);
add_action('admin_post_jio_pay_delete_order', [$this, 'handle_delete_order']);
add_action('admin_post_jio_pay_refund_order', [$this, 'handle_refund_order']);
}
public function enqueue_assets(): void {
$page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
if ($page !== self::PAGE_SLUG) {
return;
}
$css = '/assets/css/admin/orders.css';
$js = '/assets/js/admin-orders.js';
wp_enqueue_style('jio-admin-orders', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css));
wp_enqueue_script('jio-admin-orders', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true);
}
public function render_orders_page(): void {
$this->ensure_cap();
NativeUi::page_open('jio-orders-page');
if (!class_exists('\Jio\Payment\OrderRepository')) {
NativeUi::header(__('订单明细', 'jio'), '', 'dashicons-list-view');
echo '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'jio') . '</p></div>';
NativeUi::page_close();
return;
}
$detail_order = isset($_GET['order']) ? sanitize_text_field((string) wp_unslash($_GET['order'])) : '';
if ($detail_order !== '') {
$this->render_order_detail_page($detail_order);
} else {
$this->render_order_list_page();
}
NativeUi::page_close();
}
/* ------------------------------------------------------------------
* admin-post 处理器
* ----------------------------------------------------------------*/
public function handle_update_order_status(): void {
$this->ensure_cap();
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
check_admin_referer('jio_pay_update_order_status_' . $order_number);
$ok = false;
if ($order_number !== '' && $new_status !== '' && class_exists('\Jio\Payment\OrderRepository')) {
$ok = \Jio\Payment\OrderRepository::get_instance()->update_status($order_number, $new_status);
}
$this->redirect_back(['jio_pay_msg' => $ok ? 'order_updated' : 'order_failed']);
}
/**
* 「标记退款」:把已支付订单推到已退款。只改订单状态,不调用渠道退款。
*
* 退款闸门只剩「订单当前为已支付」——工单系统与订单退款已解耦,
* 不再要求先有一条进行中的工单。
*/
public function handle_refund_order(): void {
$this->ensure_cap();
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
check_admin_referer('jio_pay_refund_order_' . $order_number);
$code = 'order_refund_failed';
if ($order_number !== '' && class_exists('\Jio\Payment\OrderRepository')) {
$repo = \Jio\Payment\OrderRepository::get_instance();
$order = $repo->get($order_number);
if (!is_array($order)) {
$code = 'order_refund_missing';
} elseif ((string) ($order['status'] ?? '') !== DomainModel::ORDER_STATUS_PAID) {
$code = 'order_refund_not_paid';
} else {
$ok = $repo->update_status($order_number, DomainModel::ORDER_STATUS_REFUNDED, [
'allow_refund' => true,
'note' => __('管理员在订单明细中标记退款', 'jio'),
]);
$code = $ok ? 'order_refunded' : 'order_refund_failed';
}
}
$this->redirect_back(['jio_pay_msg' => $code]);
}
public function handle_update_order_note(): void {
$this->ensure_cap();
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
$admin_note = isset($_POST['admin_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['admin_note'])) : '';
check_admin_referer('jio_pay_update_order_note_' . $order_number);
$ok = false;
if ($order_number !== '' && class_exists('\Jio\Payment\OrderRepository')) {
$ok = \Jio\Payment\OrderRepository::get_instance()->update_admin_note($order_number, $admin_note);
}
$this->redirect_back(['jio_pay_msg' => $ok ? 'order_note_updated' : 'order_note_failed']);
}
public function handle_clear_closed_orders(): void {
$this->ensure_cap();
check_admin_referer('jio_pay_clear_closed_orders');
$days = isset($_POST['days']) ? max(1, min(3650, (int) $_POST['days'])) : 14;
$deleted = 0;
if (class_exists('\Jio\Payment\OrderRepository')) {
$deleted = \Jio\Payment\OrderRepository::get_instance()->clear_closed_orders($days);
}
$this->redirect_back([
'jio_pay_msg' => $deleted > 0 ? 'orders_cleared' : 'orders_clear_empty',
'cleared' => $deleted,
], true);
}
public function handle_delete_order(): void {
$this->ensure_cap();
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
check_admin_referer('jio_pay_delete_order_' . $order_number);
$ok = false;
if ($order_number !== '' && class_exists('\Jio\Payment\OrderRepository')) {
$repo = \Jio\Payment\OrderRepository::get_instance();
$order = $repo->get($order_number);
// 只允许硬删已关闭订单。
if (is_array($order) && (string) ($order['status'] ?? '') === DomainModel::ORDER_STATUS_CLOSED) {
$ok = $repo->delete_order($order_number);
}
}
// 删除成功后详情已不存在,回到列表;失败时留在原处。
$this->redirect_back(['jio_pay_msg' => $ok ? 'order_deleted' : 'order_delete_failed'], $ok);
}
/**
* 回到发起操作的订单页(保留列表筛选与分页,或停在详情)。
* $to_list 时去掉 order 参数回到列表。来源不是本页时落到列表首页。
*/
private function redirect_back(array $args, bool $to_list = false): void {
$query = [];
$referer = wp_get_referer();
if (is_string($referer) && $referer !== '') {
parse_str((string) wp_parse_url($referer, PHP_URL_QUERY), $parsed);
if (($parsed['page'] ?? '') === self::PAGE_SLUG) {
unset($parsed['page'], $parsed['jio_pay_msg'], $parsed['cleared']);
if ($to_list) {
unset($parsed['order']);
}
$query = $parsed;
}
}
wp_safe_redirect($this->page_url(self::PAGE_SLUG, array_merge($query, $args)));
exit;
}
/* ------------------------------------------------------------------
* 订单动作表单(列表行与详情侧栏共用)
* ----------------------------------------------------------------*/
/** 当前状态可执行的动作按钮;没有可执行动作时返回空串。 */
private function order_actions_html(string $order_number, string $status, bool $small = true): string {
$labels = [
DomainModel::ORDER_STATUS_PAID => __('确认收款', 'jio'),
DomainModel::ORDER_STATUS_CLOSED => __('关闭订单', 'jio'),
];
$confirms = [
DomainModel::ORDER_STATUS_PAID => __('确认将该订单标记为已支付?系统会立即为用户发放对应权益。', 'jio'),
DomainModel::ORDER_STATUS_CLOSED => __('确认关闭该订单?关闭后用户无法继续支付。', 'jio'),
];
$html = '';
foreach ((array) (DomainModel::order_transitions()[$status] ?? []) as $target) {
// 已退款只走下方专用的退款动作(带 allow_refund 闸门),不进通用状态按钮。
if (!isset($labels[$target])) {
continue;
}
$html .= $this->action_form(
'jio_pay_update_order_status',
'jio_pay_update_order_status_' . $order_number,
['order_number' => $order_number, 'new_status' => $target],
$labels[$target],
$confirms[$target],
$target === DomainModel::ORDER_STATUS_CLOSED,
$small
);
}
// 已支付是已退款的唯一前置状态。
if ($status === DomainModel::ORDER_STATUS_PAID) {
$html .= $this->action_form(
'jio_pay_refund_order',
'jio_pay_refund_order_' . $order_number,
['order_number' => $order_number],
__('标记退款', 'jio'),
__('只会把订单标记为已退款,不会自动原路退回款项。确认继续?此操作不可撤销。', 'jio'),
true,
$small
);
}
if ($status === DomainModel::ORDER_STATUS_CLOSED) {
$html .= $this->action_form(
'jio_pay_delete_order',
'jio_pay_delete_order_' . $order_number,
['order_number' => $order_number],
__('删除', 'jio'),
__('确认永久删除此订单?不可恢复。', 'jio'),
true,
$small
);
}
return $html;
}
/**
* 单按钮 POST 表单。确认文案放在 data-jio-confirm,由 admin-orders.js 拦截提交。
*
* @param array<string, string> $fields
*/
private function action_form(string $action, string $nonce_action, array $fields, string $label, string $confirm = '', bool $danger = false, bool $small = true): string {
$html = '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="jio-orders-action"'
. ($confirm !== '' ? ' data-jio-confirm="' . esc_attr($confirm) . '"' : '') . '>';
$html .= '<input type="hidden" name="action" value="' . esc_attr($action) . '" />';
foreach ($fields as $name => $value) {
$html .= '<input type="hidden" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '" />';
}
$html .= wp_nonce_field($nonce_action, '_wpnonce', true, false);
$class = 'button' . ($small ? ' button-small' : '') . ($danger ? ' is-danger' : '');
return $html . '<button type="submit" class="' . esc_attr($class) . '">' . esc_html($label) . '</button></form>';
}
/* ------------------------------------------------------------------
* 展示助手
* ----------------------------------------------------------------*/
/** @return array<string, string> */
private function status_labels(): array {
return DomainModel::order_status_labels();
}
/**
* 订单状态键 → [文案, 色调] 映射,供 NativeUi::status_badge() 用。
*
* @return array<string, array{0:string,1:string}>
*/
private function status_badge_map(): array {
$tones = [
DomainModel::ORDER_STATUS_PENDING => 'warning',
DomainModel::ORDER_STATUS_PAID => 'success',
DomainModel::ORDER_STATUS_REFUNDED => 'info',
DomainModel::ORDER_STATUS_CLOSED => 'neutral',
];
$map = [];
foreach ($this->status_labels() as $key => $label) {
$map[$key] = [(string) $label, $tones[$key] ?? 'neutral'];
}
return $map;
}
private function product_type_label(string $type): string {
return DomainModel::order_product_type_labels()[$type] ?? ($type !== '' ? $type : '—');
}
/** 商品名:订单快照里的标题优先,付费内容回落到文章标题;都没有时返回空串。 */
private function product_title(array $order): string {
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
$title = (string) ($meta['product_title'] ?? $meta['title'] ?? $meta['package_title'] ?? '');
$id = (int) ($order['product_id'] ?? 0);
if ($title === '' && $id > 0 && DomainModel::is_resource_product_type((string) ($order['product_type'] ?? ''))) {
$title = (string) get_the_title($id);
}
return $title;
}
/** 支付方式键 → 显示名:已注册网关取网关名称,其余为站内固定方式。 */
private function payment_method_label(string $method): string {
if ($this->payment_labels === null) {
$this->payment_labels = [
'points' => __('积分', 'jio'),
'free' => __('免费', 'jio'),
];
if (class_exists('\Jio\Payment\Gateway')) {
foreach (\Jio\Payment\Gateway::get_instance()->get_all() as $id => $gateway) {
$this->payment_labels[(string) $id] = $gateway->get_name();
}
}
}
return $this->payment_labels[$method] ?? ($method !== '' ? $method : '—');
}
/** @return string[] */
private function third_party_gateway_ids(): array {
return array_values(array_diff(
array_keys(\Jio\Payment\Gateway::get_instance()->get_all()),
['balance', 'points']
));
}
private function is_points_order(array $order): bool {
return sanitize_key((string) ($order['currency'] ?? '')) === 'points'
|| sanitize_key((string) ($order['payment_method'] ?? '')) === 'points';
}
/** @param array<string,mixed> $order */
private function order_amount(array $order): string {
if ($this->is_points_order($order)) {
return max(0, (int) ($order['amount'] ?? 0)) . ' ' . __('积分', 'jio');
}
return $this->cash_amount((string) ($order['amount'] ?? '0'), (string) ($order['currency'] ?? ''));
}
private function cash_amount(string $value, string $currency): string {
return $this->cash_decimal($value) . ' ' . strtoupper($currency !== '' ? $currency : 'CNY');
}
private function cash_decimal(string $value): string {
try {
return \Jio\Ledger\AssetAmount::balance($value)->decimal();
} catch (\InvalidArgumentException $e) {
return '0.00';
}
}
private function render_flash(): void {
if (empty($_GET['jio_pay_msg'])) {
return;
}
$code = sanitize_key((string) wp_unslash($_GET['jio_pay_msg']));
// 码 => [文案, 是否失败]
$map = [
'order_updated' => [__('订单状态已更新。', 'jio'), false],
'order_failed' => [__('订单状态更新失败,订单可能已被其他操作改变,请刷新后重试。', 'jio'), true],
'order_refunded' => [__('订单已标记为已退款。款项请在支付渠道后台原路退回。', 'jio'), false],
'order_refund_failed' => [__('订单标记退款失败。', 'jio'), true],
'order_refund_missing' => [__('订单不存在。', 'jio'), true],
'order_refund_not_paid' => [__('仅已支付订单可以标记退款。', 'jio'), true],
'order_note_updated' => [__('运营备注已保存。', 'jio'), false],
'order_note_failed' => [__('运营备注保存失败。', 'jio'), true],
'orders_cleared' => [sprintf(
/* translators: %d deleted count */
__('已清理 %d 条关闭订单。', 'jio'),
max(0, (int) ($_GET['cleared'] ?? 0))
), false],
'orders_clear_empty' => [__('没有可清理的关闭订单。', 'jio'), false],
'order_deleted' => [__('订单已删除。', 'jio'), false],
'order_delete_failed' => [__('订单删除失败(仅允许删除已关闭订单)。', 'jio'), true],
];
if (!isset($map[$code])) {
return;
}
[$message, $failed] = $map[$code];
echo '<div class="notice ' . esc_attr($failed ? 'notice-error' : 'notice-success') . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
}
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'jio'));
}
}
/** 参数值统一 urlencode:add_query_arg 不编码值,搜索词里的 & / # 会截断链接。 */
private function page_url(string $slug, array $args = []): string {
return add_query_arg(urlencode_deep(array_merge(['page' => $slug], $args)), admin_url('admin.php'));
}
}