Files
wordpress-theme-aksrt/inc/helpers/user-center-renderers/profile-tickets.php
T
kate522andClaude Opus 5 dbb7de41e9 feat(ticket): replace the after-sale flow with a general ticket system
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>
2026-10-25 23:29:51 +08:00

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>