feat: one account can bind both netease + skin identities

- db: user_identities table (UNIQUE user_id+source), migrate backfill from users
- auth: GET/POST/DELETE /api/auth/identities - bind/unbind/list identities
  (netease requires UID, one per source, name uniqueness, keep >=1)
- register/external/plugin/admin-created users auto-write primary identity
- tickets: submit uses selected identity (validated belongs to user)
- dashboard: 我的身份 card with bind/unbind UI
- ticket-create: identity selector when >1 identity
This commit is contained in:
2026-08-17 01:17:09 +08:00
parent 42d4cafedd
commit 559970c5b7
8 changed files with 196 additions and 7 deletions

View File

@@ -107,8 +107,15 @@ router.post('/', ticketAnonLimiter, optionalAuth, upload.array('files', 5), fina
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;
if (!type || !['report','suggestion','appeal','result_appeal'].includes(type)) return res.status(400).json({ error: '类型不正确' });
if (!title) return res.status(400).json({ error: '标题不能为空' });
const rgn = req.user?.game_name || reporter_game_name;
const rgu = req.user?.game_uid || reporter_game_uid;
// 身份解析: 登录用户可选用自己绑定的任一身份(网易端/皮肤站); 匿名用表单值
let rgn = reporter_game_name, rgu = reporter_game_uid;
if (req.user) {
if (reporter_game_name && reporter_game_name !== req.user.game_name) {
const ident = await getRow('SELECT id FROM user_identities WHERE user_id = ? AND game_name = ?', [req.user.id, reporter_game_name]);
if (!ident) return res.status(403).json({ error: '所选身份不属于当前账号' });
}
if (!rgn) { rgn = req.user.game_name; rgu = req.user.game_uid; }
}
if (!rgn || !rgu) return res.status(400).json({ error: '请填写游戏名称和UID' });
if (type === 'report') { if (!target_game_name && !target_game_uid) return res.status(400).json({ error: '举报需至少填写对方游戏名或UID之一' }); if (!reason) return res.status(400).json({ error: '请填写举报原因' }); }
if (type === 'suggestion' && !description) return res.status(400).json({ error: '建议内容不能为空' });