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>
101 lines
4.3 KiB
PHP
101 lines
4.3 KiB
PHP
<?php
|
|
/**
|
|
* User center ticket (工单) panel.
|
|
*
|
|
* 数据全部来自 /aksrt/v1/tickets* REST;本文件只输出容器与挂载点,
|
|
* 交互在 assets/js/ticket-center.js。
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
$enabled = class_exists('\AKSRT\Ticket\TicketSettings') && \AKSRT\Ticket\TicketSettings::enabled();
|
|
if (!$enabled) {
|
|
echo '<div class="uc-panel-empty muted-2-color">' . esc_html__('工单功能暂未开启', 'aksrt') . '</div>';
|
|
return;
|
|
}
|
|
|
|
$statuses = class_exists('\AKSRT\Ticket\TicketStatusService')
|
|
? \AKSRT\Ticket\TicketStatusService::all()
|
|
: [];
|
|
$types = class_exists('\AKSRT\Ticket\TicketTypeService')
|
|
? \AKSRT\Ticket\TicketTypeService::public_list()
|
|
: [];
|
|
$attachment_max = class_exists('\AKSRT\Ticket\TicketSettings')
|
|
? \AKSRT\Ticket\TicketSettings::attachment_max()
|
|
: 5;
|
|
?>
|
|
<div class="aksrt-tickets-page uc-tickets-panel" data-aksrt-ticket-center>
|
|
<header class="uc-tickets-panel__head">
|
|
<div class="uc-tickets-panel__title-wrap">
|
|
<p class="aksrt-tickets-page__eyebrow"><?php esc_html_e('我的服务', 'aksrt'); ?></p>
|
|
<div class="uc-tickets-panel__title font-bold em12">
|
|
<i class="fa fas fa-headset uc-tickets-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>
|
|
<button class="btn-primary" type="button" data-aksrt-ticket-create><?php esc_html_e('提交工单', 'aksrt'); ?></button>
|
|
</header>
|
|
|
|
<div class="aksrt-tickets-page__stats" data-aksrt-ticket-stats>
|
|
<?php foreach ($statuses as $status) : ?>
|
|
<div class="aksrt-tickets-page__stat" data-aksrt-ticket-stat="<?php echo esc_attr((string) $status['status_key']); ?>">
|
|
<span><?php echo esc_html((string) $status['name']); ?></span><strong>0</strong>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<form class="aksrt-tickets-page__search" data-aksrt-tickets-search>
|
|
<label class="screen-reader-text" for="aksrt-ticket-keywords"><?php esc_html_e('按工单号或标题查找', 'aksrt'); ?></label>
|
|
<i class="fa fas fa-search" aria-hidden="true"></i>
|
|
<input id="aksrt-ticket-keywords" maxlength="60" placeholder="<?php esc_attr_e('输入工单号或标题查找', 'aksrt'); ?>">
|
|
<button class="btn-primary" type="submit"><?php esc_html_e('查找', 'aksrt'); ?></button>
|
|
<select data-aksrt-ticket-filter="status" aria-label="<?php esc_attr_e('工单状态', 'aksrt'); ?>">
|
|
<option value=""><?php esc_html_e('全部状态', 'aksrt'); ?></option>
|
|
<?php foreach ($statuses as $status) : ?>
|
|
<option value="<?php echo esc_attr((string) $status['status_key']); ?>"><?php echo esc_html((string) $status['name']); ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<?php if ($types !== []) : ?>
|
|
<select data-aksrt-ticket-filter="ticket_type_id" aria-label="<?php esc_attr_e('工单部门', 'aksrt'); ?>">
|
|
<option value=""><?php esc_html_e('全部部门', 'aksrt'); ?></option>
|
|
<?php foreach ($types as $type) : ?>
|
|
<option value="<?php echo esc_attr((string) $type['id']); ?>"><?php echo esc_html((string) $type['name']); ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<?php endif; ?>
|
|
</form>
|
|
|
|
<div class="aksrt-tickets-page__list" data-aksrt-ticket-list>
|
|
<div class="aksrt-tickets-page__empty">
|
|
<i class="fa fas fa-headset" aria-hidden="true"></i>
|
|
<h3><?php esc_html_e('还没有工单', 'aksrt'); ?></h3>
|
|
<p><?php esc_html_e('遇到问题时提交工单,我们会尽快处理。', 'aksrt'); ?></p>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="aksrt-tickets-page__pagination" data-aksrt-ticket-pagination hidden></nav>
|
|
|
|
<?php
|
|
// 人机验证挂载点(提交工单 / 回复):场景未启用时 aksrt_render_captcha 无输出,
|
|
// ticket-center.js 以挂载点存在与否决定是否先过验证。
|
|
if (function_exists('aksrt_render_captcha')) {
|
|
aksrt_render_captcha('ticket_create');
|
|
aksrt_render_captcha('ticket_reply');
|
|
}
|
|
?>
|
|
<span class="hidden" data-aksrt-ticket-config="<?php echo esc_attr(wp_json_encode([
|
|
'attachmentMax' => $attachment_max,
|
|
])); ?>"></span>
|
|
</div>
|