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>
402 lines
18 KiB
PHP
402 lines
18 KiB
PHP
<?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'));
|
||
}
|
||
}
|