diff --git a/elastic/es_connect.py b/elastic/es_connect.py index 2d8368e..7868ca7 100644 --- a/elastic/es_connect.py +++ b/elastic/es_connect.py @@ -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": { diff --git a/elastic/templates/elastic/registration_codes.html b/elastic/templates/elastic/registration_codes.html index 2327119..f68fe7a 100644 --- a/elastic/templates/elastic/registration_codes.html +++ b/elastic/templates/elastic/registration_codes.html @@ -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 @@

管理注册码

- - - + +
+ + + +
- + -
+
- + -
+
+ +
diff --git a/elastic/urls.py b/elastic/urls.py index b2a09d1..d89b439 100644 --- a/elastic/urls.py +++ b/elastic/urls.py @@ -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'), diff --git a/elastic/views.py b/elastic/views.py index 3728316..937f41a 100644 --- a/elastic/views.py +++ b/elastic/views.py @@ -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):