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

@@ -8,6 +8,19 @@ const TicketCreate = {
const isAppeal = type === 'appeal';
const typeLabel = isReport ? '举报' : isAppeal ? '申诉' : '建议';
this.files = [];
this.identities = [];
// 登录用户: 拉取多来源身份(网易端+皮肤站可同时绑定)
if (user) {
API.get('/auth/identities').then(list => {
this.identities = list || [];
const sel = container.querySelector('#identity-select');
if (sel && this.identities.length > 1) {
sel.innerHTML = this.identities.map((it, i) => `<option value="${i}">${it.source === 'netease' ? '网易端' : '皮肤站'}${U.esc(it.game_name)}${it.game_uid ? ' (UID: ' + U.esc(it.game_uid) + ')' : ''}</option>`).join('');
sel.closest('.form-group').classList.remove('hidden');
}
}).catch(() => {});
}
container.innerHTML = `
${hideTabs ? '' : `<div class="tab-nav" id="type-tabs">
@@ -33,6 +46,12 @@ const TicketCreate = {
</select>
</div>
${user ? `<div class="form-group hidden" id="identity-group">
<label>使用身份</label>
<select id="identity-select"></select>
<span class="help">同一账号可绑定网易端 + 皮肤站身份</span>
</div>` : ''}
<div class="grid-2">
<div class="form-group">
<label>您的游戏名称 *</label>
@@ -145,6 +164,16 @@ const TicketCreate = {
e.preventDefault();
const fd = new FormData(form);
// 身份选择: 选中多来源身份时覆盖游戏名/UID
const sel = container.querySelector('#identity-select');
if (sel && sel.value !== '') {
const it = this.identities[parseInt(sel.value)];
if (it) {
fd.set('reporter_game_name', it.game_name);
fd.set('reporter_game_uid', it.game_uid || '');
}
}
if (type === 'report') {
const tgn = fd.get('target_game_name');
const tgu = fd.get('target_game_uid');