feat: ticket create - sub-server selector
- ticket-create.js: added 子服务器 dropdown (group/server from /polls/server-list, supports anonymous users), sends 'server' field - tickets.js POST: accept server param, resolve 'group/server' or alias/server_name from server_groups, store into tickets.server_name - polls.js: new public GET /server-list (group_name+server_name only, no auth required) for ticket create page - verified: 13 checks (frontend/backend/db/column/syntax)
This commit is contained in:
@@ -33,6 +33,13 @@ router.get('/groups', authenticate, async (req, res) => {
|
|||||||
res.json(await query('SELECT * FROM server_groups ORDER BY group_name, server_name'));
|
res.json(await query('SELECT * FROM server_groups ORDER BY group_name, server_name'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 公开: 子服务器列表(工单提交页匿名/登录共用, 不含别名等内部字段)
|
||||||
|
router.get('/server-list', async (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json(await query('SELECT group_name, server_name FROM server_groups ORDER BY group_name, server_name'));
|
||||||
|
} catch { res.json([]); }
|
||||||
|
});
|
||||||
|
|
||||||
router.post('/groups', authenticate, requireRole('owner'), async (req, res) => {
|
router.post('/groups', authenticate, requireRole('owner'), async (req, res) => {
|
||||||
const { group_name, server_name, alias } = req.body;
|
const { group_name, server_name, alias } = req.body;
|
||||||
if (!group_name || !server_name) return res.status(400).json({ error: '分组名和子服名为必填' });
|
if (!group_name || !server_name) return res.status(400).json({ error: '分组名和子服名为必填' });
|
||||||
|
|||||||
@@ -129,9 +129,23 @@ router.get('/:id', authenticate, async (req, res) => {
|
|||||||
router.post('/', ticketAnonLimiter, optionalAuth, upload.array('files', 5), finalizeUpload, validateLengths({
|
router.post('/', ticketAnonLimiter, optionalAuth, upload.array('files', 5), finalizeUpload, validateLengths({
|
||||||
title:100, reporter_game_name:50, reporter_game_uid:50, target_game_name:50, target_game_uid:50, reason:300, description:5000,
|
title:100, reporter_game_name:50, reporter_game_uid:50, target_game_name:50, target_game_uid:50, reason:300, description:5000,
|
||||||
}), async (req, res) => {
|
}), async (req, res) => {
|
||||||
const { type, title, reporter_game_name, reporter_game_uid, target_game_name, target_game_uid, reason, description, is_admin_complaint, parent_ticket_id } = req.body;
|
const { type, title, reporter_game_name, reporter_game_uid, target_game_name, target_game_uid, reason, description, is_admin_complaint, parent_ticket_id, server } = req.body;
|
||||||
if (!type || !['report','suggestion','appeal','result_appeal'].includes(type)) return res.status(400).json({ error: '类型不正确' });
|
if (!type || !['report','suggestion','appeal','result_appeal'].includes(type)) return res.status(400).json({ error: '类型不正确' });
|
||||||
if (!title) return res.status(400).json({ error: '标题不能为空' });
|
if (!title) return res.status(400).json({ error: '标题不能为空' });
|
||||||
|
// 子服务器解析: '分组/子服' 或 alias 或 server_name
|
||||||
|
let serverName = '';
|
||||||
|
if (server) {
|
||||||
|
let g = '', s = '';
|
||||||
|
if (String(server).includes('/')) {
|
||||||
|
const parts = String(server).split('/');
|
||||||
|
g = parts[0].trim(); s = parts[1].trim();
|
||||||
|
serverName = s;
|
||||||
|
} else {
|
||||||
|
const row = await getRow('SELECT group_name, server_name FROM server_groups WHERE alias = ? OR server_name = ? LIMIT 1', [server, server]);
|
||||||
|
if (row) { g = row.group_name; s = row.server_name; serverName = s; }
|
||||||
|
else serverName = String(server).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
// 身份解析: 登录用户可选用自己绑定的任一身份(网易端/皮肤站); 匿名用表单值
|
// 身份解析: 登录用户可选用自己绑定的任一身份(网易端/皮肤站); 匿名用表单值
|
||||||
let rgn = reporter_game_name, rgu = reporter_game_uid;
|
let rgn = reporter_game_name, rgu = reporter_game_uid;
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
@@ -167,14 +181,15 @@ router.post('/', ticketAnonLimiter, optionalAuth, upload.array('files', 5), fina
|
|||||||
try {
|
try {
|
||||||
await conn.beginTransaction();
|
await conn.beginTransaction();
|
||||||
const [r] = await conn.execute(`INSERT INTO tickets(type,title,user_id,reporter_game_name,reporter_game_uid,
|
const [r] = await conn.execute(`INSERT INTO tickets(type,title,user_id,reporter_game_name,reporter_game_uid,
|
||||||
target_game_name,target_game_uid,reason,description,tracking_token,is_admin_complaint,parent_ticket_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
target_game_name,target_game_uid,reason,description,tracking_token,is_admin_complaint,parent_ticket_id,server_name) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||||
[type, title, req.user?.id||null, rgn, rgu||'',
|
[type, title, req.user?.id||null, rgn, rgu||'',
|
||||||
type==='report'?(target_game_name||''):null, type==='report'?(target_game_uid||''):null,
|
type==='report'?(target_game_name||''):null, type==='report'?(target_game_uid||''):null,
|
||||||
(type==='report'||type==='appeal'||type==='result_appeal')?reason:null,
|
(type==='report'||type==='appeal'||type==='result_appeal')?reason:null,
|
||||||
(type==='suggestion'||type==='appeal'||type==='result_appeal')?description:'',
|
(type==='suggestion'||type==='appeal'||type==='result_appeal')?description:'',
|
||||||
trackingToken,
|
trackingToken,
|
||||||
is_admin_complaint ? 1 : 0,
|
is_admin_complaint ? 1 : 0,
|
||||||
parent_ticket_id || null]);
|
parent_ticket_id || null,
|
||||||
|
serverName]);
|
||||||
ticketId = r.insertId;
|
ticketId = r.insertId;
|
||||||
|
|
||||||
if (req.files?.length) {
|
if (req.files?.length) {
|
||||||
|
|||||||
@@ -41,6 +41,17 @@ const TicketCreate = {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 子服务器列表(server_groups: 分组/子服)
|
||||||
|
this.servers = [];
|
||||||
|
API.get('/polls/server-list').then(list => {
|
||||||
|
this.servers = list || [];
|
||||||
|
const sel = container.querySelector('#server-select');
|
||||||
|
if (sel) {
|
||||||
|
sel.innerHTML = '<option value="">不指定</option>' + this.servers.map(g =>
|
||||||
|
`<option value="${U.escJs(g.group_name)}/${U.escJs(g.server_name)}">${U.esc(g.group_name)} / ${U.esc(g.server_name)}</option>`).join('');
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
${hideTabs ? '' : `<div class="tab-nav" id="type-tabs">
|
${hideTabs ? '' : `<div class="tab-nav" id="type-tabs">
|
||||||
<button class="${isReport?'active':''}" data-type="report">举报</button>
|
<button class="${isReport?'active':''}" data-type="report">举报</button>
|
||||||
@@ -58,6 +69,11 @@ const TicketCreate = {
|
|||||||
<input name="title" required placeholder="${isReport ? '如:玩家破坏建筑' : isAppeal ? '如:申请解除封禁' : '如:增加领地保护功能'}">
|
<input name="title" required placeholder="${isReport ? '如:玩家破坏建筑' : isAppeal ? '如:申请解除封禁' : '如:增加领地保护功能'}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>子服务器</label>
|
||||||
|
<select name="server" id="server-select"><option value="">不指定</option></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>优先级</label>
|
<label>优先级</label>
|
||||||
<select name="priority">
|
<select name="priority">
|
||||||
|
|||||||
Reference in New Issue
Block a user