count_by_status( \AKSRT\Ticket\TicketStatusService::KEY_PENDING ); } /** 页面专属样式与脚本,只在本页入队。 */ public function enqueue_ticket_assets(string $hook): void { if ($hook !== 'users_page_' . self::TICKET_PAGE_SLUG) { return; } $css = '/assets/css/admin/tickets.css'; $js = '/assets/js/admin-tickets.js'; if (is_file(AKSRT_DIR . $css)) { wp_enqueue_style('aksrt-admin-tickets', AKSRT_URI . $css, [], (string) filemtime(AKSRT_DIR . $css)); } if (is_file(AKSRT_DIR . $js)) { wp_enqueue_script('aksrt-admin-tickets', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true); } } public function render_tickets_page(): void { if (!current_user_can(self::CAPABILITY)) { wp_die(esc_html__('权限不足', 'aksrt')); } echo '
'; echo '

' . esc_html__('工单管理', 'aksrt') . '

'; printf( '%s', esc_url(admin_url('admin.php?page=aksrt-settings#/users/ticket')), esc_html__('工单配置(部门 / 状态 / 预设回复)', 'aksrt') ); echo '
'; $this->render_ticket_notices(); $this->render_ticket_create_form(); $this->render_ticket_list_tab(); echo '
'; } /** 写操作后的 redirect 提示:成功绿色、失败红色。 */ private function render_ticket_notices(): void { if (empty($_GET['aksrt_ticket_msg'])) { return; } $code = sanitize_key((string) wp_unslash($_GET['aksrt_ticket_msg'])); $messages = [ 'updated' => [__('工单已更新。', 'aksrt'), true], 'failed' => [__('操作失败:当前状态不允许该操作,或提交内容无效。', 'aksrt'), false], 'replied' => [__('回复已提交,用户将收到通知。', 'aksrt'), true], 'reply_failed' => [__('回复提交失败:内容为空、超长或工单已关闭。', 'aksrt'), false], 'note_saved' => [__('备注已添加。', 'aksrt'), true], 'created' => [__('工单已创建,用户将收到通知。', 'aksrt'), true], 'create_failed' => [__('工单创建失败:请检查用户 ID、部门、标题与内容。', 'aksrt'), false], ]; if (!isset($messages[$code])) { return; } [$text, $ok] = $messages[$code]; printf( '

%s

', $ok ? 'notice-success' : 'notice-error', esc_html($text) ); } /** * 写操作:统一入口,按 $action 分发。nonce 以 action + id 为单位。 */ public function handle_ticket_update(): void { if (!current_user_can(self::CAPABILITY)) { wp_die(esc_html__('权限不足', 'aksrt')); } $action = isset($_POST['ticket_action']) ? sanitize_key((string) wp_unslash($_POST['ticket_action'])) : ''; $id = isset($_POST['ticket_id']) ? absint($_POST['ticket_id']) : 0; check_admin_referer('aksrt_ticket_' . $action . '_' . $id); $code = 'failed'; $open_id = $id; switch ($action) { case 'status': $code = $this->ticket_action_status($id); break; case 'reply': $code = $this->ticket_action_reply($id); break; case 'note': $code = $this->ticket_action_note($id); break; case 'create': $code = $this->ticket_action_create($open_id); break; } $this->ticket_redirect($code, $open_id, $action !== 'create'); } /** * 回到来源视图(保留筛选 / 分页),并保持处理中的工单展开。 * 新建工单回到默认视图:新单可能不满足来源筛选。 */ private function ticket_redirect(string $code, int $open_id, bool $keep_view): void { $target = $this->ticket_page_url(); $referer = $keep_view ? wp_get_referer() : false; if (is_string($referer) && str_contains($referer, 'page=' . self::TICKET_PAGE_SLUG)) { $target = remove_query_arg(['aksrt_ticket_msg', 'id'], $referer); } $args = ['aksrt_ticket_msg' => $code]; if ($open_id > 0) { $args['id'] = $open_id; } $url = add_query_arg($args, $target); wp_safe_redirect($open_id > 0 ? $url . '#aksrt-ticket-' . $open_id : $url); exit; } /** @return string 提示码 */ private function ticket_action_status(int $id): string { if ($id <= 0 || !class_exists('\AKSRT\Ticket\TicketRepository')) { return 'failed'; } $repo = \AKSRT\Ticket\TicketRepository::get_instance(); $ticket = $repo->find($id); if ($ticket === null) { return 'failed'; } $to = isset($_POST['status']) ? sanitize_key((string) wp_unslash($_POST['status'])) : ''; if (!\AKSRT\Ticket\TicketStatusService::can_transition((string) $ticket['status'], $to)) { return 'failed'; } $fields = ['status' => $to, 'updated_at' => current_time('mysql')]; $type_id = isset($_POST['ticket_type_id']) ? absint($_POST['ticket_type_id']) : 0; if ($type_id > 0 && \AKSRT\Ticket\TicketTypeService::exists($type_id)) { $fields['ticket_type_id'] = $type_id; } if (isset($_POST['admin_id'])) { // 0 = 取消指派;其余必须是存在的用户。 $admin_id = absint($_POST['admin_id']); if ($admin_id === 0 || get_userdata($admin_id)) { $fields['admin_id'] = $admin_id; } } if (!$repo->update_fields($id, $fields)) { return 'failed'; } $was_closed = \AKSRT\Ticket\TicketStatusService::is_closed((string) $ticket['status']); $ticket = array_merge($ticket, $fields); if ($to === \AKSRT\Ticket\TicketStatusService::KEY_CLOSED && !$was_closed) { \AKSRT\Ticket\TicketNotifier::closed($ticket, 'admin'); } return 'updated'; } /** @return string 提示码 */ private function ticket_action_reply(int $id): string { if ($id <= 0 || !class_exists('\AKSRT\Ticket\TicketRepository')) { return 'reply_failed'; } $repo = \AKSRT\Ticket\TicketRepository::get_instance(); $ticket = $repo->find($id); if ($ticket === null || !empty($ticket['is_closed'])) { return 'reply_failed'; } $content = trim($this->ticket_post_textarea('content')); if ($content === '' || mb_strlen($content) > self::TICKET_CONTENT_MAX) { return 'reply_failed'; } $reply_id = $repo->create_reply($id, 'admin', get_current_user_id(), $content, [], 0); if ($reply_id <= 0) { return 'reply_failed'; } $repo->touch_reply($id, 'admin', get_current_user_id()); $repo->apply_reply_status($id, $ticket, 'admin'); \AKSRT\Ticket\TicketNotifier::user_replied($repo->find($id) ?? $ticket); return 'replied'; } /** @return string 提示码 */ private function ticket_action_note(int $id): string { if ($id <= 0 || !class_exists('\AKSRT\Ticket\TicketNoteService')) { return 'failed'; } $content = trim($this->ticket_post_textarea('content')); if ($content === '' || mb_strlen($content) > self::TICKET_CONTENT_MAX) { return 'failed'; } return \AKSRT\Ticket\TicketNoteService::create($id, $content, get_current_user_id()) > 0 ? 'note_saved' : 'failed'; } /** * @param int $new_id 输出:新工单 ID(成功时) * @return string 提示码 */ private function ticket_action_create(int &$new_id): string { $new_id = 0; if (!class_exists('\AKSRT\Ticket\TicketRepository')) { return 'create_failed'; } $user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0; $title = isset($_POST['title']) ? trim(sanitize_text_field((string) wp_unslash($_POST['title']))) : ''; $content = trim($this->ticket_post_textarea('content')); $type_id = isset($_POST['ticket_type_id']) ? absint($_POST['ticket_type_id']) : 0; if ($user_id <= 0 || !get_userdata($user_id) || $title === '' || mb_strlen($title) > 200) { return 'create_failed'; } if ($content === '' || mb_strlen($content) > self::TICKET_CONTENT_MAX) { return 'create_failed'; } if (!\AKSRT\Ticket\TicketTypeService::exists($type_id)) { return 'create_failed'; } $order_number = \AKSRT\Ticket\TicketSettings::order_link_enabled() && isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; $repo = \AKSRT\Ticket\TicketRepository::get_instance(); $id = $repo->create([ 'ticket_num' => \AKSRT\Ticket\TicketNumber::generate(), 'user_id' => $user_id, 'title' => $title, 'ticket_type_id' => $type_id, 'content' => $content, 'order_number' => $order_number, 'status' => \AKSRT\Ticket\TicketStatusService::KEY_ADMIN_REPLIED, 'post_admin_id' => get_current_user_id(), 'admin_id' => get_current_user_id(), ]); if ($id <= 0) { return 'create_failed'; } $new_id = $id; $ticket = $repo->find($id); if (is_array($ticket)) { \AKSRT\Ticket\TicketNotifier::user_replied($ticket); } return 'created'; } private function ticket_post_textarea(string $key): string { return isset($_POST[$key]) ? sanitize_textarea_field((string) wp_unslash($_POST[$key])) : ''; } private function ticket_page_url(array $extra = []): string { return add_query_arg( array_merge(['page' => self::TICKET_PAGE_SLUG], $extra), admin_url('users.php') ); } /** admin_post 表单开头:action + ticket_action + ticket_id(nonce 由调用方各自输出)。 */ private function ticket_form_open(string $ticket_action, int $id, string $class = ''): string { return '
' . '' . '' . ''; } }