Files
Sdf1_login/api/ticket.php
T

826 lines
29 KiB
PHP

<?php
/**
* 工单系统API
*
* 用户端:提交工单、查看我的工单、查看详情、回复(MD)、撤销
* 管理员端:查看所有工单、回复、驳回、分配服务商
* 服务商端:抢单大厅、我处理的工单、回复、标记完结
*/
while (ob_get_level() > 0) { @ob_end_clean(); }
ob_start();
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: no-store, no-cache, must-revalidate');
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
// 仅刷新当前文件的OPcache(不要opcache_reset——会重置整个服务器缓存导致竞态条件)
if (function_exists('opcache_invalidate')) { @opcache_invalidate(__FILE__); }
register_shutdown_function(function() {
while (ob_get_level() > 0) { @ob_end_flush(); }
});
require_once __DIR__ . '/../core.php';
if (session_status() === PHP_SESSION_NONE) { session_start(); }
$action = getParam('action', '');
try {
switch ($action) {
// ===== 用户端 =====
case 'create': ticketCreate(); break;
case 'my_list': ticketMyList(); break;
case 'detail': ticketDetail(); break;
case 'reply': ticketReply(); break;
case 'withdraw': ticketWithdraw(); break;
// ===== 管理员端 =====
case 'list_all': ticketListAll(); break;
case 'admin_reply': ticketAdminReply(); break;
case 'reject': ticketReject(); break;
case 'assign': ticketAssign(); break;
case 'admin_create': ticketAdminCreate(); break;
case 'admin_complete': ticketAdminComplete(); break;
// ===== 服务商端 =====
case 'provider_list': ticketProviderList(); break;
case 'available': ticketAvailable(); break;
case 'grab': ticketGrab(); break;
case 'provider_reply': ticketProviderReply(); break;
case 'complete': ticketProviderComplete(); break;
// ===== 通用 =====
case 'providers': ticketProviders(); break;
case 'provider_check': ticketProviderCheck(); break;
default:
error('未知操作: ' . $action);
}
} catch (\Throwable $e) {
error('服务器错误: ' . $e->getMessage());
}
// ============================================================
// 工具函数
// ============================================================
function requireWebToken() {
$token = getParam('token');
if (!$token) error('缺少token', 401);
$info = validateTokenSilent($token);
if (!$info) error('token无效或已过期', 401);
return $info;
}
function requireAdmin() {
if (!isAdminLoggedIn()) error('未登录管理后台', 401);
}
/**
* 检查是否为管理员或授权的服务商用户
* 管理员通过session认证,用户通过token认证
*/
function getAuthUser() {
if (isAdminLoggedIn()) return ['player' => 'admin', 'role' => 'admin'];
return requireWebToken();
}
function isServiceProviderInGame($playerName) {
// 优先从Web.db的web_service_providers表读取(Java同步数据)
$db = getDB();
try {
$stmt = $db->prepare("SELECT 1 FROM web_service_providers WHERE player_name = :p AND active = 1");
$stmt->bindValue(':p', $playerName, SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray();
if ($row !== false) return true;
} catch (\Throwable $e) { /* 表可能不存在 */ }
// 回退:尝试读取Java的login.db(本地开发环境)
$loginDbPath = GAME_LOGIN_DB;
if (!file_exists($loginDbPath)) return false;
try {
$loginDb = new SQLite3($loginDbPath);
$loginDb->enableExceptions(true);
$stmt = $loginDb->prepare("SELECT 1 FROM service_providers WHERE player_name = :p AND active = 1");
$stmt->bindValue(':p', $playerName, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray();
$loginDb->close();
return $row !== false;
} catch (Exception $e) {
return false;
}
}
function getServiceProviderRole($playerName) {
// 优先从Web.db读取
$db = getDB();
try {
$stmt = $db->prepare("SELECT role FROM web_service_providers WHERE player_name = :p AND active = 1");
$stmt->bindValue(':p', $playerName, SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if ($row) return $row['role'];
} catch (\Throwable $e) { /* 表可能不存在 */ }
// 回退:读取Java的login.db
$loginDbPath = GAME_LOGIN_DB;
if (!file_exists($loginDbPath)) return null;
try {
$loginDb = new SQLite3($loginDbPath);
$loginDb->enableExceptions(true);
$stmt = $loginDb->prepare("SELECT role FROM service_providers WHERE player_name = :p AND active = 1");
$stmt->bindValue(':p', $playerName, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$loginDb->close();
return $row ? $row['role'] : null;
} catch (Exception $e) {
return null;
}
}
function ticketCreate() {
$info = requireWebToken();
$player = $info['player'];
$type = trim(getParam('type', ''));
$title = trim(getParam('title', ''));
$description = trim(getParam('description', ''));
if (empty($type)) error('请选择工单类型');
if (empty($title)) error('请输入工单标题');
if (mb_strlen($title) > 100) error('标题不能超过100字');
if (mb_strlen($description) > 5000) error('描述不能超过5000字');
$validTypes = ['bug', 'help', 'report', 'apply', 'other'];
if (!in_array($type, $validTypes)) error('无效的工单类型');
$db = getDB();
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("INSERT INTO web_tickets (type, status, requester, title, description, created_at, updated_at) VALUES (:type, 'submitted', :player, :title, :desc, :now, :now)");
$stmt->bindValue(':type', $type, SQLITE3_TEXT);
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':title', $title, SQLITE3_TEXT);
$stmt->bindValue(':desc', $description, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
$ticketId = $db->lastInsertRowID();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('创建失败: ' . $e->getMessage());
}
success(['id' => $ticketId], '工单提交成功');
}
function ticketMyList() {
$info = requireWebToken();
$player = $info['player'];
$db = getDB();
$status = getParam('status', '');
$page = max(1, (int)getParam('page', 1));
$limit = 20;
$offset = ($page - 1) * $limit;
$where = "WHERE requester = :player";
$params = [':player' => $player];
if ($status && $status !== 'all') {
$where .= " AND status = :status";
$params[':status'] = $status;
}
$countStmt = $db->prepare("SELECT COUNT(*) as total FROM web_tickets $where");
foreach ($params as $k => $v) $countStmt->bindValue($k, $v, SQLITE3_TEXT);
$total = $countStmt->execute()->fetchArray(SQLITE3_ASSOC)['total'];
$stmt = $db->prepare("SELECT * FROM web_tickets $where ORDER BY created_at DESC LIMIT :limit OFFSET :offset");
foreach ($params as $k => $v) $stmt->bindValue($k, $v, SQLITE3_TEXT);
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
$stmt->bindValue(':offset', $offset, SQLITE3_INTEGER);
$result = $stmt->execute();
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
success([
'list' => $list,
'total' => (int)$total,
'page' => $page,
'pages' => ceil($total / $limit)
]);
}
function ticketDetail() {
$id = (int)getParam('id', 0);
if ($id <= 0) error('无效的工单ID');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$row) error('工单不存在');
// 权限检查:管理员可以直接查看,普通用户只能看自己的工单
if (isAdminLoggedIn()) {
// 管理员,直接放行
} else {
$info = requireWebToken();
$player = $info['player'];
if ($row['requester'] !== $player && $row['assigned_to'] !== $player) {
error('无权查看此工单');
}
}
// 获取回复列表
$replies = getTicketReplies($id);
$row['replies'] = $replies;
success($row);
}
function ticketReply() {
$info = requireWebToken();
$player = $info['player'];
$id = (int)getParam('id', 0);
$message = trim(getParam('message', ''));
if ($id <= 0) error('无效的工单ID');
if (empty($message)) error('回复内容不能为空');
if (mb_strlen($message) > 5000) error('回复内容不能超过5000字');
$db = getDB();
// 检查工单状态和权限
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['requester'] !== $player) error('只能回复自己的工单');
if (in_array($ticket['status'], ['withdrawn', 'rejected'])) error('该工单已关闭,无法回复');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
// 插入回复
$stmt = $db->prepare("INSERT INTO web_ticket_replies (ticket_id, sender, role, message, created_at) VALUES (:tid, :sender, 'user', :msg, :now)");
$stmt->bindValue(':tid', $id, SQLITE3_INTEGER);
$stmt->bindValue(':sender', $player, SQLITE3_TEXT);
$stmt->bindValue(':msg', $message, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
// 更新工单状态为已回复
$stmt = $db->prepare("UPDATE web_tickets SET status = 'replied', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('回复失败');
}
success(null, '回复成功');
}
function ticketWithdraw() {
$info = requireWebToken();
$player = $info['player'];
$id = (int)getParam('id', 0);
if ($id <= 0) error('无效的工单ID');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['requester'] !== $player) error('无权操作此工单');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("UPDATE web_tickets SET status = 'withdrawn', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', time(), SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('撤销失败');
}
success(null, '工单已撤销');
}
// ===== 管理员端 =====
function ticketListAll() {
requireAdmin();
$db = getDB();
// 兜底建表(服务器core.php可能版本旧)
try {
$db->exec("CREATE TABLE IF NOT EXISTS web_tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
status TEXT DEFAULT 'submitted',
requester TEXT NOT NULL,
assigned_to TEXT DEFAULT '',
title TEXT NOT NULL,
description TEXT DEFAULT '',
reject_reason TEXT DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)");
$db->exec("CREATE TABLE IF NOT EXISTS web_ticket_replies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticket_id INTEGER NOT NULL,
sender TEXT NOT NULL,
role TEXT DEFAULT 'user',
message TEXT NOT NULL,
created_at INTEGER NOT NULL
)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_wt_requester ON web_tickets(requester)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_wt_status ON web_tickets(status)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_wt_assigned ON web_tickets(assigned_to)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_wtr_ticket ON web_ticket_replies(ticket_id)");
} catch (\Throwable $e) {
error('工单表初始化失败: ' . $e->getMessage());
}
try {
$status = getParam('status', 'all');
$page = max(1, (int)getParam('page', 1));
$search = trim(getParam('search', ''));
$limit = 20;
$offset = ($page - 1) * $limit;
$where = "WHERE 1=1";
$params = [];
if ($status && $status !== 'all') {
$where .= " AND status = :status";
$params[':status'] = $status;
}
if ($search) {
$where .= " AND (requester LIKE :search OR title LIKE :search2)";
$params[':search'] = "%$search%";
$params[':search2'] = "%$search%";
}
$countStmt = $db->prepare("SELECT COUNT(*) as total FROM web_tickets $where");
foreach ($params as $k => $v) $countStmt->bindValue($k, $v, SQLITE3_TEXT);
$total = $countStmt->execute()->fetchArray(SQLITE3_ASSOC)['total'];
$stmt = $db->prepare("SELECT * FROM web_tickets $where ORDER BY created_at DESC LIMIT :limit OFFSET :offset");
foreach ($params as $k => $v) $stmt->bindValue($k, $v, SQLITE3_TEXT);
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
$stmt->bindValue(':offset', $offset, SQLITE3_INTEGER);
$result = $stmt->execute();
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
// 统计各状态数量
$stats = [];
foreach (['submitted', 'replied', 'completed', 'withdrawn', 'rejected'] as $s) {
$cnt = $db->querySingle("SELECT COUNT(*) FROM web_tickets WHERE status = '$s'");
$stats[$s] = (int)$cnt;
}
$stats['all'] = (int)$db->querySingle("SELECT COUNT(*) FROM web_tickets");
success([
'list' => $list,
'total' => (int)$total,
'page' => $page,
'pages' => ceil($total / $limit),
'stats' => $stats
]);
} catch (\Throwable $e) {
error('查询工单失败: ' . $e->getMessage());
}
}
function ticketAdminReply() {
requireAdmin();
$id = (int)getParam('id', 0);
$message = trim(getParam('message', ''));
if ($id <= 0) error('无效的工单ID');
if (empty($message)) error('回复内容不能为空');
if (mb_strlen($message) > 5000) error('回复内容不能超过5000字');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("INSERT INTO web_ticket_replies (ticket_id, sender, role, message, created_at) VALUES (:tid, 'admin', 'admin', :msg, :now)");
$stmt->bindValue(':tid', $id, SQLITE3_INTEGER);
$stmt->bindValue(':msg', $message, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
// 管理员回复 → 状态更新为已回复(不改变assigned_to,保留原服务商所有权)
$stmt = $db->prepare("UPDATE web_tickets SET status = 'replied', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('回复失败');
}
success(null, '回复成功');
}
function ticketReject() {
requireAdmin();
$id = (int)getParam('id', 0);
$reason = trim(getParam('reason', ''));
if ($id <= 0) error('无效的工单ID');
if (empty($reason)) error('请输入驳回原因');
if (mb_strlen($reason) > 500) error('驳回原因不能超过500字');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['status'] === 'withdrawn' || $ticket['status'] === 'rejected') error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("UPDATE web_tickets SET status = 'rejected', reject_reason = :reason, updated_at = :now WHERE id = :id");
$stmt->bindValue(':reason', $reason, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('驳回失败');
}
success(null, '工单已驳回');
}
function ticketAssign() {
requireAdmin();
$id = (int)getParam('id', 0);
$provider = trim(getParam('provider', ''));
if ($id <= 0) error('无效的工单ID');
if (empty($provider)) error('请输入服务商名称');
// 验证是否为服务商
if (!isServiceProviderInGame($provider)) error("$provider 不是服务商");
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("UPDATE web_tickets SET assigned_to = :provider, status = 'replied', updated_at = :now WHERE id = :id");
$stmt->bindValue(':provider', $provider, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('分配失败');
}
success(null, "已分配给 $provider");
}
function ticketAdminCreate() {
requireAdmin();
$type = trim(getParam('type', ''));
$title = trim(getParam('title', ''));
$description = trim(getParam('description', ''));
$provider = trim(getParam('provider', ''));
if (empty($type)) error('请选择工单类型');
if (empty($title)) error('请输入工单标题');
if (mb_strlen($title) > 100) error('标题不能超过100字');
if (mb_strlen($description) > 5000) error('描述不能超过5000字');
$validTypes = ['bug', 'help', 'report', 'apply', 'other'];
if (!in_array($type, $validTypes)) error('无效的工单类型');
// 如果指定了服务商,验证身份
if ($provider && !isServiceProviderInGame($provider)) {
error("$provider 不是服务商");
}
$db = getDB();
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("INSERT INTO web_tickets (type, status, requester, assigned_to, title, description, created_at, updated_at) VALUES (:type, 'replied', 'admin', :provider, :title, :desc, :now, :now)");
$stmt->bindValue(':type', $type, SQLITE3_TEXT);
$stmt->bindValue(':provider', $provider, SQLITE3_TEXT);
$stmt->bindValue(':title', $title, SQLITE3_TEXT);
$stmt->bindValue(':desc', $description, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
$ticketId = $db->lastInsertRowID();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('创建失败: ' . $e->getMessage());
}
$msg = '工单已创建';
if ($provider) $msg .= ",已派发给 $provider";
success(['id' => $ticketId], $msg);
}
function ticketAdminComplete() {
requireAdmin();
$id = (int)getParam('id', 0);
if ($id <= 0) error('无效的工单ID');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("INSERT INTO web_ticket_replies (ticket_id, sender, role, message, created_at) VALUES (:tid, 'admin', 'admin', '✅ 工单已由管理员标记完结', :now)");
$stmt->bindValue(':tid', $id, SQLITE3_INTEGER);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
$stmt = $db->prepare("UPDATE web_tickets SET status = 'completed', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('操作失败');
}
success(null, '已标记完结');
}
// ===== 服务商端 =====
function ticketProviderList() {
$info = requireWebToken();
$player = $info['player'];
// 验证服务商身份
$role = getServiceProviderRole($player);
if (!$role) error('你不是服务商');
$db = getDB();
$type = getParam('type', 'all');
// processing: 活跃(已提交/已回复)
// completed: 已完结/已撤销/已驳回
// all: 全部跟过的工单
$where = "WHERE assigned_to = :player";
$params = [':player' => $player];
if ($type === 'processing') {
$where .= " AND status IN ('submitted', 'replied')";
} elseif ($type === 'completed') {
$where .= " AND status IN ('completed', 'withdrawn', 'rejected')";
}
// 'all' 不加status过滤
$stmt = $db->prepare("SELECT * FROM web_tickets $where ORDER BY updated_at DESC LIMIT 100");
foreach ($params as $k => $v) $stmt->bindValue($k, $v, SQLITE3_TEXT);
$result = $stmt->execute();
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
success(['list' => $list, 'role' => $role]);
}
function ticketAvailable() {
$info = requireWebToken();
$player = $info['player'];
if (!isServiceProviderInGame($player)) error('你不是服务商');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE status = 'submitted' AND assigned_to = '' ORDER BY created_at DESC LIMIT 50");
$result = $stmt->execute();
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
success(['list' => $list]);
}
function ticketGrab() {
$info = requireWebToken();
$player = $info['player'];
$id = (int)getParam('id', 0);
if ($id <= 0) error('无效的工单ID');
if (!isServiceProviderInGame($player)) error('你不是服务商');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['status'] !== 'submitted') error('该工单已被接单');
if (!empty($ticket['assigned_to'])) error('该工单已被分配');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("UPDATE web_tickets SET assigned_to = :provider, updated_at = :now WHERE id = :id");
$stmt->bindValue(':provider', $player, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('抢单失败');
}
success(null, '抢单成功');
}
function ticketProviderReply() {
$info = requireWebToken();
$player = $info['player'];
$id = (int)getParam('id', 0);
$message = trim(getParam('message', ''));
if ($id <= 0) error('无效的工单ID');
if (empty($message)) error('回复内容不能为空');
if (mb_strlen($message) > 5000) error('回复内容不能超过5000字');
if (!isServiceProviderInGame($player)) error('你不是服务商');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['assigned_to'] !== $player) error('这不是分配给你的工单');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("INSERT INTO web_ticket_replies (ticket_id, sender, role, message, created_at) VALUES (:tid, :sender, 'provider', :msg, :now)");
$stmt->bindValue(':tid', $id, SQLITE3_INTEGER);
$stmt->bindValue(':sender', $player, SQLITE3_TEXT);
$stmt->bindValue(':msg', $message, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
$stmt = $db->prepare("UPDATE web_tickets SET status = 'replied', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('回复失败');
}
success(null, '回复成功');
}
function ticketProviderComplete() {
$info = requireWebToken();
$player = $info['player'];
$id = (int)getParam('id', 0);
if ($id <= 0) error('无效的工单ID');
if (!isServiceProviderInGame($player)) error('你不是服务商');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['assigned_to'] !== $player) error('这不是分配给你的工单');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
// 插入完结回复
$stmt = $db->prepare("INSERT INTO web_ticket_replies (ticket_id, sender, role, message, created_at) VALUES (:tid, :sender, 'provider', '✅ 工单已处理完毕', :now)");
$stmt->bindValue(':tid', $id, SQLITE3_INTEGER);
$stmt->bindValue(':sender', $player, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
// 状态改为completed
$stmt = $db->prepare("UPDATE web_tickets SET status = 'completed', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('操作失败');
}
success(null, '已标记完结');
}
// ===== 通用 =====
function ticketProviders() {
if (!isAdminLoggedIn()) error('未登录管理后台');
$loginDbPath = GAME_LOGIN_DB;
if (!file_exists($loginDbPath)) success(['list' => []]);
try {
$loginDb = new SQLite3($loginDbPath);
$result = $loginDb->query("SELECT player_name, role, join_time FROM service_providers WHERE active = 1");
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
$loginDb->close();
success(['list' => $list]);
} catch (Exception $e) {
success(['list' => []]);
}
}
function ticketProviderCheck() {
$info = requireWebToken();
$player = $info['player'];
$isProvider = isServiceProviderInGame($player);
$role = $isProvider ? getServiceProviderRole($player) : null;
success(['is_provider' => $isProvider, 'role' => $role]);
}
// ===== 辅助函数 =====
function getTicketReplies($ticketId) {
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_ticket_replies WHERE ticket_id = :tid ORDER BY created_at ASC");
$stmt->bindValue(':tid', $ticketId, SQLITE3_INTEGER);
$result = $stmt->execute();
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
return $list;
}