注册码管理页面的功能完善

This commit is contained in:
DSQ
2026-03-12 17:35:02 +08:00
parent 462c744d06
commit 1163110810
4 changed files with 181 additions and 9 deletions

View File

@@ -489,9 +489,111 @@ def analytics_trend(gte: str = None, lte: str = None, interval: str = "day"):
print(f"分析趋势失败: {str(e)}")
return []
def analytics_types(gte: str = None, lte: str = None, size: int = 10):
def delete_key_globally(key_to_remove: str):
try:
filters = _type_filters_from_list(limit=size)
# 1. 从 GlobalDocument (id='keys') 中彻底移除
try:
doc = GlobalDocument.get(id='keys')
current_keys = list(doc.keys_list or [])
# 使用列表推导式进行彻底删除,处理可能的重复项
new_keys = [k.strip().strip(';') for k in current_keys if k.strip().strip(';') != key_to_remove]
if len(new_keys) != len(current_keys):
doc.keys_list = new_keys
doc.save()
print(f"已从全局列表移除 Key: {key_to_remove}")
except Exception as e:
print(f"从全局列表移除 Key 失败: {str(e)}")
# 2. 同步清理所有注册码中的该 key (无论是 keys 还是 manage_keys 字段)
from elasticsearch.helpers import scan
query = {
"query": {
"bool": {
"should": [
{"term": {"keys": key_to_remove}},
{"term": {"manage_keys": key_to_remove}}
],
"must": [
{"exists": {"field": "code"}} # 确保是注册码文档
]
}
}
}
updated_count = 0
for hit in scan(es, query=query, index=GLOBAL_INDEX_NAME):
try:
# 重新获取文档对象进行操作
doc = RegistrationCodeDocument.get(id=hit['_id'])
modified = False
if doc.keys:
old_keys = list(doc.keys)
new_ks = [k for k in old_keys if k != key_to_remove]
if len(new_ks) != len(old_keys):
doc.keys = new_ks
modified = True
if doc.manage_keys:
old_mks = list(doc.manage_keys)
new_mks = [k for k in old_mks if k != key_to_remove]
if len(new_mks) != len(old_mks):
doc.manage_keys = new_mks
modified = True
if modified:
doc.save()
updated_count += 1
except Exception as e:
print(f"同步清理注册码 {hit['_id']} 失败: {str(e)}")
# 3. 同步清理所有用户中的该 key (无论是 key 还是 manage_key 字段)
try:
user_query = {
"query": {
"bool": {
"should": [
{"term": {"key": key_to_remove}},
{"term": {"manage_key": key_to_remove}}
]
}
}
}
for user_hit in scan(es, query=user_query, index=USER_INDEX_NAME):
try:
user_doc = UserDocument.get(id=user_hit['_id'])
user_modified = False
if user_doc.key:
old_uk = list(user_doc.key)
new_uks = [k for k in old_uk if k != key_to_remove]
if len(new_uks) != len(old_uk):
user_doc.key = new_uks
user_modified = True
if user_doc.manage_key:
old_umk = list(user_doc.manage_key)
new_umks = [k for k in old_umk if k != key_to_remove]
if len(new_umks) != len(old_umk):
user_doc.manage_key = new_umks
user_modified = True
if user_modified:
user_doc.save()
except Exception as e:
print(f"同步清理用户 {user_hit['_id']} 失败: {str(e)}")
except Exception as e:
print(f"扫描用户失败: {str(e)}")
return True, updated_count
except Exception as e:
print(f"全局删除 Key 失败: {str(e)}")
return False, 0
def analytics_types(gte: str = None, lte: str = None, limit: int = 12):
try:
filters = _type_filters_from_list(limit=limit)
body = {
"size": 0,
"aggs": {

View File

@@ -19,6 +19,10 @@
.btn { padding:8px 12px; border:none; border-radius:8px; cursor:pointer; margin:0 4px; }
.btn-primary { background:#4f46e5; color:#fff; }
.btn-secondary { background:#64748b; color:#fff; }
.btn-danger { background:#ff4d4f; color:#fff; }
.btn-danger:hover { background:#ff7875 !important; }
.btn-primary:hover { background:#6366f1 !important; }
.btn-secondary:hover { background:#94a3b8 !important; }
.notice { padding:10px; border-radius:6px; margin-top:10px; display:none; }
.notice.success { background:#d4edda; color:#155724; border:1px solid #c3e6cb; }
.notice.error { background:#f8d7da; color:#721c24; border:1px solid #f5c6cb; }
@@ -57,6 +61,43 @@
if(resp.ok && data.status==='success'){msg.textContent='新增key成功'; msg.className='notice success'; msg.style.display='block'; document.getElementById('newKey').value=''; loadKeys();}
else{msg.textContent=data.message||'新增失败'; msg.className='notice error'; msg.style.display='block';}
}
async function deleteSelectedKey(){
const keySel = document.getElementById('keys');
const mkeySel = document.getElementById('manageKeys');
// 优先获取左侧选中的,如果没有则获取右侧选中的
const selectedKey = keySel.value || mkeySel.value;
if(!selectedKey){
alert('请先在下方列表中选择一个要删除的Key');
return;
}
if(!confirm(`确定要全局删除Key \"${selectedKey}\" 吗?\n该操作将:\n1. 从全局可选Key列表中移除\n2. 从所有包含此Key的注册码中同步清除\n此操作不可恢复!`)) return;
const ov=document.getElementById('overlay'); ov.style.display='flex';
const csrftoken=getCookie('csrftoken');
const resp=await fetch('/elastic/registration-codes/keys/remove/',{
method:'POST',
credentials:'same-origin',
headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},
body:JSON.stringify({key:selectedKey})
});
const data=await resp.json();
const msg=document.getElementById('msg');
if(resp.ok && data.status==='success'){
msg.textContent = data.message || '删除成功';
msg.className='notice success';
msg.style.display='block';
loadKeys(); // 重新加载keys列表
loadCodes(); // 重新加载注册码列表
} else {
msg.textContent=data.message||'删除失败';
msg.className='notice error';
msg.style.display='block';
}
ov.style.display='none';
}
function selectedValues(sel){return Array.from(sel.selectedOptions).map(o=>o.value);}
function enableToggleSelect(sel){ sel.addEventListener('mousedown',function(e){ if(e.target && e.target.tagName==='OPTION'){ e.preventDefault(); const op=e.target; op.selected=!op.selected; this.dispatchEvent(new Event('change',{bubbles:true})); } }); }
function clearSelection(id){ const sel=document.getElementById(id); Array.from(sel.options).forEach(o=>o.selected=false); }
@@ -116,21 +157,26 @@
<h2>管理注册码</h2>
<div class="row">
<div class="col">
<label>新增key</label>
<input id="newKey" type="text" placeholder="输入新的key" />
<button class="btn btn-secondary" onclick="addKey()">新增</button>
<label>管理 Key</label>
<div style="display:flex; gap:8px;">
<input id="newKey" type="text" placeholder="输入新的key进行新增或在下方选择后删除" style="flex: 1;" />
<button class="btn btn-secondary" onclick="addKey()">新增 Key</button>
<button class="btn btn-danger" onclick="deleteSelectedKey()">删除选中 Key</button>
</div>
</div>
</div>
<div class="row" style="margin-top:12px;">
<div class="col">
<label>选择keys</label>
<label>选择 keys</label>
<select id="keys" multiple size="10"></select>
<div style="margin-top:8px;"><button class="btn btn-secondary" onclick="clearSelection('keys')">清空选择</button></div>
<div style="margin-top:8px;"><button class="btn btn-secondary" style="width: 100%;" onclick="clearSelection('keys')">清空 keys 选择</button></div>
</div>
<div class="col">
<label>选择manage_keys</label>
<label>选择 manage_keys</label>
<select id="manageKeys" multiple size="10"></select>
<div style="margin-top:8px;"><button class="btn btn-secondary" onclick="clearSelection('manageKeys')">清空选择</button></div>
<div style="margin-top:8px;">
<button class="btn btn-secondary" style="width: 100%;" onclick="clearSelection('manageKeys')">清空 manage_keys 选择</button>
</div>
</div>
</div>
<div class="row" style="margin-top:12px;">

View File

@@ -35,6 +35,7 @@ urlpatterns = [
path('registration-codes/manage/', views.registration_code_manage_page, name='registration_code_manage_page'),
path('registration-codes/keys/', views.get_keys_list_view, name='get_keys_list'),
path('registration-codes/keys/add/', views.add_key_view, name='add_key'),
path('registration-codes/keys/remove/', views.remove_key_view, name='remove_key'),
path('registration-codes/generate/', views.generate_registration_code_view, name='generate_registration_code'),
path('registration-codes/list/', views.list_registration_codes_view, name='list_registration_codes'),
path('registration-codes/revoke/', views.revoke_registration_code_view, name='revoke_registration_code'),

View File

@@ -790,6 +790,29 @@ def analytics_recent_view(request):
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@require_http_methods(["POST"])
@csrf_protect
def remove_key_view(request):
try:
payload = json.loads(request.body.decode("utf-8"))
key_to_remove = payload.get("key")
if not key_to_remove:
return JsonResponse({"status": "error", "message": "缺少key参数"}, status=400)
from .es_connect import delete_key_globally
ok, count = delete_key_globally(key_to_remove)
if ok:
return JsonResponse({"status": "success", "message": f"已成功全局删除 Key '{key_to_remove}',并同步清理了 {count} 个注册码。"})
else:
return JsonResponse({"status": "error", "message": "删除失败"}, status=500)
except json.JSONDecodeError:
return HttpResponseBadRequest("Invalid JSON")
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@require_http_methods(["GET"])
@ensure_csrf_cookie
def user_manage(request):