|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 '
' . esc_html__('订单服务不可用。', 'jio') . '
';
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 $fields
*/
private function action_form(string $action, string $nonce_action, array $fields, string $label, string $confirm = '', bool $danger = false, bool $small = true): string {
$html = '';
}
/* ------------------------------------------------------------------
* 展示助手
* ----------------------------------------------------------------*/
/** @return array */
private function status_labels(): array {
return DomainModel::order_status_labels();
}
/**
* 订单状态键 → [文案, 色调] 映射,供 NativeUi::status_badge() 用。
*
* @return array
*/
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 $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 '' . esc_html($message) . '
';
}
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'));
}
}