注册码管理页面的功能完善
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
<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;">
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user