The old 售后 flow was an order-refund appendage: tickets were hard-bound to an order number, users could only file them from 「我的订单」against a paid order, and the admin action bottomed out at 「标记订单已退款」. It could not carry account questions or feature requests that have nothing to do with an order. Replace it with a general ticket system modelled on 魔方财务云's idcsmart_ticket: title + department + description are required, linking an order is optional (user's own paid orders only). Add departments, custom statuses, preset replies and internal admin notes as four configuration surfaces. Conversation history moves off the ticket row's JSON log into dedicated ticket_reply rows, which unlocks quoted replies. Front end gets a 工单中心 panel under 我的服务; back end moves from 支付中心 → 售后服务 to users.php → 工单管理. Per CLAUDE.md's rewrite-over-compatibility rule: drop the old table, build new ones, delete the old code. No dual paths, no data migration. The upstream plugin's upstream/downstream delivery and internal-ticket conversion are deliberately left out — both are multi-provider platform concerns with no meaning on a single WordPress site. Data: order_aftersale becomes six aksrt_ticket* tables (ticket, ticket_reply, ticket_status, ticket_type, ticket_prereply, ticket_note). SCHEMA_VERSION 2.6.0 -> 3.0.0. Status and department seeds are idempotent; the default status 「待接单」is flagged as non-editable. Domain: new src/Ticket/ (13 files, all under 460 lines). Attachments reuse UserController::rest_upload_comment_image with a ticket-specific permission callback, mirroring the existing /report/upload-image route rather than growing a second upload path. Notifications go through AKSRT\Messages\Message::send() and deep-link to the right surface; email/SMS stay out of scope since the theme's mail and SMS stacks are separate implementations. Refund gate opens up: marking an order refunded no longer requires an in-flight ticket, only that the order is paid. Also generalise a hardcoded special case in tools/audit-admin-contracts.php — schema domain inference now reads the parent directory instead of matching "PostList" by name, so new leaf schema files need no per-name exception. Verification: php tools/audit-admin-contracts.php exit 0, errors=0 node --test (widget-system + admin) 664 pass / 0 fail php -l over all new/changed files clean node --check over all JS clean npm run build + build:public clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
134 lines
10 KiB
PHP
134 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* User-center knowledge-payment order panel.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
$context = is_array($args['context'] ?? null) ? $args['context'] : [];
|
|
$user_id = (int) ($context['user_id'] ?? get_current_user_id());
|
|
if ($user_id <= 0) {
|
|
echo '<div class="uc-panel-empty muted-2-color">' . esc_html__('请先登录', 'aksrt') . '</div>';
|
|
return;
|
|
}
|
|
|
|
$raw_query = [
|
|
'status' => 'all',
|
|
'type' => '',
|
|
'order_number' => '',
|
|
'page' => 1,
|
|
'per_page' => 20,
|
|
];
|
|
$query = \AKSRT\Payment\OrderCenterQuery::from_array($raw_query);
|
|
$orders_view = function_exists('aksrt_get_orders_page_view')
|
|
? aksrt_get_orders_page_view($user_id, $query->page(), $query->per_page(), $query->status(), $query->product_type(), $query->order_number())
|
|
: ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => [], 'product_types' => []];
|
|
|
|
$orders = (array) ($orders_view['items'] ?? []);
|
|
$stats = is_array($orders_view['stats'] ?? null) ? $orders_view['stats'] : [];
|
|
$type_options = is_array($orders_view['product_types'] ?? null) ? $orders_view['product_types'] : [];
|
|
$page = max(1, (int) ($orders_view['page'] ?? 1));
|
|
$total_pages = max(1, (int) ($orders_view['total_pages'] ?? 1));
|
|
$status = $query->status();
|
|
$type = $query->product_type();
|
|
$order_number = $query->order_number();
|
|
|
|
$status_options = [
|
|
'all' => __('全部', 'aksrt'),
|
|
'pending' => __('待支付', 'aksrt'),
|
|
'paid' => __('已支付', 'aksrt'),
|
|
'refunded' => __('已退款', 'aksrt'),
|
|
'closed' => __('已关闭', 'aksrt'),
|
|
];
|
|
?>
|
|
<div class="aksrt-orders-page uc-orders-panel" data-aksrt-order-center>
|
|
<header class="uc-orders-panel__head">
|
|
<div class="uc-orders-panel__title-wrap">
|
|
<p class="aksrt-orders-page__eyebrow"><?php esc_html_e('我的购买', 'aksrt'); ?></p>
|
|
<div class="uc-orders-panel__title font-bold em12">
|
|
<i class="fa fas fa-file-alt uc-orders-panel__title-icon" aria-hidden="true"></i>
|
|
<?php esc_html_e('我的订单', 'aksrt'); ?>
|
|
</div>
|
|
<p class="muted-2-color em09 mt3"><?php esc_html_e('管理已购内容、会员与账户权益。', 'aksrt'); ?></p>
|
|
</div>
|
|
<div class="aksrt-orders-page__stats" data-aksrt-order-stats>
|
|
<div class="aksrt-orders-page__stat"><span><?php esc_html_e('全部', 'aksrt'); ?></span><strong><?php echo esc_html((string) (int) ($stats['total'] ?? 0)); ?></strong></div>
|
|
<div class="aksrt-orders-page__stat"><span><?php esc_html_e('待支付', 'aksrt'); ?></span><strong class="is-pending"><?php echo esc_html((string) (int) ($stats['pending'] ?? 0)); ?></strong></div>
|
|
<div class="aksrt-orders-page__stat"><span><?php esc_html_e('已支付', 'aksrt'); ?></span><strong class="is-paid"><?php echo esc_html((string) (int) ($stats['paid'] ?? 0)); ?></strong></div>
|
|
<div class="aksrt-orders-page__stat"><span><?php esc_html_e('已关闭', 'aksrt'); ?></span><strong><?php echo esc_html((string) (int) ($stats['closed'] ?? 0)); ?></strong></div>
|
|
</div>
|
|
</header>
|
|
|
|
<form class="aksrt-orders-page__search" data-aksrt-orders-search>
|
|
<label class="screen-reader-text" for="aksrt-order-number"><?php esc_html_e('按订单号查找', 'aksrt'); ?></label>
|
|
<i class="fa fas fa-search" aria-hidden="true"></i>
|
|
<input id="aksrt-order-number" value="<?php echo esc_attr($order_number); ?>" maxlength="22" placeholder="<?php esc_attr_e('输入订单号查找', 'aksrt'); ?>">
|
|
<button class="btn-primary" type="submit"><?php esc_html_e('查找', 'aksrt'); ?></button>
|
|
<button class="aksrt-orders-page__filter-toggle" type="button" data-aksrt-orders-filter-toggle aria-expanded="false" aria-controls="aksrt-orders-filter-panel"><i class="fa fas fa-sliders-h" aria-hidden="true"></i> <?php esc_html_e('筛选', 'aksrt'); ?></button>
|
|
</form>
|
|
|
|
<div id="aksrt-orders-filter-panel" class="aksrt-orders-page__filter-panel" data-aksrt-orders-filter-panel hidden>
|
|
<div class="aksrt-orders-page__filter-group">
|
|
<span class="aksrt-orders-page__filter-label"><?php esc_html_e('订单状态', 'aksrt'); ?></span>
|
|
<nav class="aksrt-orders-page__status-filters" aria-label="<?php esc_attr_e('订单状态', 'aksrt'); ?>">
|
|
<?php foreach ($status_options as $key => $label) : ?>
|
|
<button class="aksrt-orders-page__filter-chip<?php echo $status === $key ? ' is-active' : ''; ?>" type="button" data-aksrt-order-filter="status" data-aksrt-order-filter-value="<?php echo esc_attr($key); ?>" aria-pressed="<?php echo $status === $key ? 'true' : 'false'; ?>">
|
|
<?php echo esc_html($label); ?>
|
|
<?php if ($key === 'pending') : ?><span class="aksrt-orders-page__filter-count"<?php echo empty($stats['pending']) ? ' hidden' : ''; ?>><?php echo esc_html((string) (int) ($stats['pending'] ?? 0)); ?></span><?php endif; ?>
|
|
</button>
|
|
<?php endforeach; ?>
|
|
</nav>
|
|
</div>
|
|
<div class="aksrt-orders-page__filter-group">
|
|
<span class="aksrt-orders-page__filter-label"><?php esc_html_e('订单类型', 'aksrt'); ?></span>
|
|
<div class="aksrt-orders-page__type-filters">
|
|
<?php foreach ($type_options as $key => $label) : ?>
|
|
<button class="aksrt-orders-page__filter-chip<?php echo (string) $key === $type ? ' is-active' : ''; ?>" type="button" data-aksrt-order-filter="type" data-aksrt-order-filter-value="<?php echo esc_attr((string) $key); ?>" aria-pressed="<?php echo (string) $key === $type ? 'true' : 'false'; ?>"><?php echo esc_html((string) $label); ?></button>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<button class="aksrt-orders-page__clear" type="button" data-aksrt-orders-clear><?php esc_html_e('清除条件', 'aksrt'); ?></button>
|
|
</div>
|
|
|
|
<?php if ($orders !== []) : ?>
|
|
<div class="aksrt-orders-page__list" data-aksrt-order-list>
|
|
<?php foreach ($orders as $order) :
|
|
$number = (string) ($order['order_number'] ?? '');
|
|
$actions = (array) ($order['allowed_actions'] ?? []);
|
|
if (!empty($order['is_points'])) {
|
|
$amount = esc_html((string) ($order['currency_symbol'] ?? __('积分', 'aksrt')) . ' ' . (string) max(0, (int) ($order['amount'] ?? 0)));
|
|
} else {
|
|
try {
|
|
$order_amount = \AKSRT\Ledger\AssetAmount::balance(is_string($order['amount'] ?? null) ? $order['amount'] : '')->formatted();
|
|
} catch (\InvalidArgumentException $e) {
|
|
$order_amount = '0.00';
|
|
}
|
|
$amount = '¥' . esc_html($order_amount);
|
|
}
|
|
?>
|
|
<article class="aksrt-orders-page__card" data-aksrt-order-card="<?php echo esc_attr($number); ?>" data-order-status="<?php echo esc_attr((string) ($order['status_group'] ?? '')); ?>">
|
|
<div class="aksrt-orders-page__card-rail"><span><?php echo esc_html((string) ($order['product_type_label'] ?? '')); ?></span><time><?php echo esc_html((string) ($order['created_at'] ?? '')); ?></time><span class="aksrt-orders-page__card-status is-<?php echo esc_attr((string) ($order['status_group'] ?? '')); ?>"><?php echo esc_html((string) ($order['status_label'] ?? '')); ?></span></div>
|
|
<div class="aksrt-orders-page__card-main">
|
|
<?php if (!empty($order['image'])) : ?><img class="aksrt-orders-page__card-img" src="<?php echo esc_url((string) $order['image']); ?>" alt=""><?php else : ?><div class="aksrt-orders-page__card-art" aria-hidden="true"><i class="fa fas fa-book"></i></div><?php endif; ?>
|
|
<div class="aksrt-orders-page__card-copy"><h3><?php echo esc_html((string) ($order['product_name'] ?? __('订单', 'aksrt'))); ?></h3><p><?php printf(esc_html__('订单号 %s', 'aksrt'), esc_html($number)); ?></p></div>
|
|
<div class="aksrt-orders-page__card-value"><strong><?php echo $amount; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></strong><span><?php esc_html_e('实付', 'aksrt'); ?></span></div>
|
|
</div>
|
|
<footer class="aksrt-orders-page__card-actions">
|
|
<?php if (!empty($order['time_left'])) : ?><span class="aksrt-orders-page__countdown" data-aksrt-order-countdown="<?php echo esc_attr((string) $order['time_left']); ?>"></span><?php endif; ?>
|
|
<button class="btn-outline" type="button" data-aksrt-order-detail="<?php echo esc_attr($number); ?>"><?php esc_html_e('订单详情', 'aksrt'); ?></button>
|
|
<?php if (!empty($actions['can_cancel'])) : ?><button class="btn-ghost" type="button" data-aksrt-order-cancel="<?php echo esc_attr($number); ?>"><?php esc_html_e('关闭订单', 'aksrt'); ?></button><?php endif; ?>
|
|
<?php if (!empty($actions['can_pay'])) : ?><button class="btn-primary" type="button" data-aksrt-order-pay="<?php echo esc_attr($number); ?>" data-aksrt-order-method="<?php echo esc_attr((string) ($order['payment_method']['value'] ?? '')); ?>"><?php esc_html_e('继续支付', 'aksrt'); ?></button><?php endif; ?>
|
|
</footer>
|
|
</article>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php if ($total_pages > 1) : ?><nav class="aksrt-orders-page__pagination" aria-label="<?php esc_attr_e('订单分页', 'aksrt'); ?>"><?php for ($index = 1; $index <= $total_pages; $index++) : ?><button class="aksrt-orders-page__page-chip<?php echo $index === $page ? ' is-active' : ''; ?>" type="button" data-aksrt-order-page="<?php echo esc_attr((string) $index); ?>"<?php echo $index === $page ? ' aria-current="page"' : ''; ?>><?php echo esc_html((string) $index); ?></button><?php endfor; ?></nav><?php endif; ?>
|
|
<?php else : ?>
|
|
<div class="aksrt-orders-page__empty" data-aksrt-order-list><i class="fa fas fa-file-alt" aria-hidden="true"></i><h3><?php esc_html_e('还没有购买记录', 'aksrt'); ?></h3><p><?php esc_html_e('购买内容或开通权益后,订单会出现在这里。', 'aksrt'); ?></p></div>
|
|
<?php endif; ?>
|
|
|
|
|
|
</div>
|