From 32ff920921750d209e1ff20532e5d63d565b2eea Mon Sep 17 00:00:00 2001 From: spdis Date: Mon, 17 Nov 2025 18:03:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B3=A8=E5=86=8C=E7=A0=81=E7=94=9F=E6=88=90?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E7=94=A8=E6=88=B7=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/templates/accounts/login.html | 4 + accounts/templates/accounts/register.html | 62 +++++++++ accounts/urls.py | 2 + accounts/views.py | 53 +++++++- elastic/documents.py | 18 ++- elastic/es_connect.py | 91 +++++++++++++- elastic/indexes.py | 4 +- .../templates/elastic/registration_codes.html | 119 ++++++++++++++++++ elastic/urls.py | 4 + elastic/views.py | 64 ++++++++++ main/templates/main/home.html | 1 + 11 files changed, 413 insertions(+), 9 deletions(-) create mode 100644 accounts/templates/accounts/register.html create mode 100644 elastic/templates/elastic/registration_codes.html diff --git a/accounts/templates/accounts/login.html b/accounts/templates/accounts/login.html index 43aa079..7cbd268 100644 --- a/accounts/templates/accounts/login.html +++ b/accounts/templates/accounts/login.html @@ -42,6 +42,10 @@
+
+ 还没有账号? + 去注册 +
diff --git a/accounts/templates/accounts/register.html b/accounts/templates/accounts/register.html new file mode 100644 index 0000000..5f93502 --- /dev/null +++ b/accounts/templates/accounts/register.html @@ -0,0 +1,62 @@ + + + + + 用户注册 + + + +
+

注册新用户

+
+ {% csrf_token %} + + + + + + + + + + + +
+
+
仅允许持有管理员提供注册码的学生注册
+
+ + + \ No newline at end of file diff --git a/accounts/urls.py b/accounts/urls.py index 0325672..d28836f 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -9,4 +9,6 @@ urlpatterns = [ path("session-key/", views.set_session_key, name="set_session_key"), path("login/secure-submit/", views.secure_login_submit, name="secure_login_submit"), path("logout/", views.logout, name="logout"), + path("register/", views.register_page, name="register"), + path("register/submit/", views.register_submit, name="register_submit"), ] \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py index ca00532..5dbdfa3 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -13,6 +13,7 @@ from django.conf import settings from .es_client import get_user_by_username from .crypto import get_public_key_spki_b64, rsa_oaep_decrypt_b64, aes_gcm_decrypt_b64, verify_password +from elastic.es_connect import get_registration_code, get_user_by_username as es_get_user_by_username, get_all_users as es_get_all_users, write_user_data @require_http_methods(["GET"]) @@ -157,4 +158,54 @@ def logout(request): except Exception: pass - return resp \ No newline at end of file + return resp + +@require_http_methods(["GET"]) +@ensure_csrf_cookie +def register_page(request): + return render(request, "accounts/register.html") + +@require_http_methods(["POST"]) +@csrf_protect +def register_submit(request): + try: + payload = json.loads(request.body.decode("utf-8")) + except json.JSONDecodeError: + return HttpResponseBadRequest("Invalid JSON") + code = (payload.get("code") or "").strip() + email = (payload.get("email") or "").strip() + username = (payload.get("username") or "").strip() + password = (payload.get("password") or "") + if not code or not email or not username or not password: + return HttpResponseBadRequest("Missing fields") + rc = get_registration_code(code) + if not rc: + return JsonResponse({"ok": False, "message": "注册码无效"}, status=400) + try: + exp = rc.get("expires_at") + now = __import__("datetime").datetime.now(__import__("datetime").timezone.utc) + if hasattr(exp, 'isoformat'): + exp_dt = exp + else: + exp_dt = __import__("datetime").datetime.fromisoformat(str(exp)) + if exp_dt <= now: + return JsonResponse({"ok": False, "message": "注册码已过期"}, status=400) + except Exception: + pass + existing = es_get_user_by_username(username) + if existing: + return JsonResponse({"ok": False, "message": "用户名已存在"}, status=409) + users = es_get_all_users() + next_id = (max([int(u.get("user_id", 0)) for u in users]) + 1) if users else 1 + ok = write_user_data({ + "user_id": next_id, + "username": username, + "password": password, + "permission": 1, + "email": email, + "key": rc.get("keys") or [], + "manage_key": rc.get("manage_keys") or [], + }) + if not ok: + return JsonResponse({"ok": False, "message": "注册失败"}, status=500) + return JsonResponse({"ok": True, "redirect_url": "/accounts/login/"}) \ No newline at end of file diff --git a/elastic/documents.py b/elastic/documents.py index 7a96c5a..cf8eba1 100644 --- a/elastic/documents.py +++ b/elastic/documents.py @@ -34,13 +34,14 @@ class UserDocument(Document): """用户数据文档映射""" user_id = fields.LongField() username = fields.KeywordField() + email = fields.KeywordField() password_hash = fields.KeywordField() password_salt = fields.KeywordField() permission = fields.IntegerField() # 还是2种权限,0为管理员,1为用户(区别在于0有全部权限,1在数据管理页面有搜索框,但是索引到的录入信息要根据其用户id查询其key,若其中之一与用户的manage_key字段匹配就显示否则不显示) - key = fields.IntegerField() #表示该用户的关键字,举个例子:学生A的key为"2024届人工智能1班","2024届","计算机与人工智能学院" 班导师B的key为"计算机与人工智能学院" - manage_key = fields.IntegerField() #表示该用户管理的关键字(非管理员)班导师B的manage_key为"2024届人工智能1班" + key = fields.KeywordField(multi=True) #表示该用户的关键字,举个例子:学生A的key为"2024届人工智能1班","2024届","计算机与人工智能学院" 班导师B的key为"计算机与人工智能学院" + manage_key = fields.KeywordField(multi=True) #表示该用户管理的关键字(非管理员)班导师B的manage_key为"2024届人工智能1班" #那么学生A就可以在数据管理页面搜索到自己的获奖数据,而班导师B就可以在数据管理页面搜索到所有人工智能1班的获奖数据。也就是说学生A和班导师B都其实只有用户权限 - + class Django: model = User # fields列表应该只包含需要特殊处理的字段,或者可以完全省略 @@ -49,6 +50,17 @@ class UserDocument(Document): @GLOBAL_INDEX.doc_type class GlobalDocument(Document): type_list = fields.KeywordField() + keys_list = fields.KeywordField(multi=True) class Django: model = ElasticNews + +@GLOBAL_INDEX.doc_type +class RegistrationCodeDocument(Document): + code = fields.KeywordField() #具体值 + keys = fields.KeywordField(multi=True) #对应的key + manage_keys = fields.KeywordField(multi=True) #对应的manage_key + expires_at = fields.DateField() #过期时间 + created_by = fields.LongField() #创建者id + class Django: + model = ElasticNews diff --git a/elastic/es_connect.py b/elastic/es_connect.py index 01aca71..9811560 100644 --- a/elastic/es_connect.py +++ b/elastic/es_connect.py @@ -5,12 +5,13 @@ Django版本的ES连接和操作模块 from elasticsearch import Elasticsearch from elasticsearch_dsl import connections import os -from .documents import AchievementDocument, UserDocument, GlobalDocument +from .documents import AchievementDocument, UserDocument, GlobalDocument, RegistrationCodeDocument from accounts.crypto import hash_password_random_salt from .indexes import ACHIEVEMENT_INDEX_NAME, USER_INDEX_NAME, GLOBAL_INDEX_NAME import hashlib import time -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta +import uuid import json # 使用环境变量配置ES连接,默认为本机 @@ -115,6 +116,87 @@ def ensure_type_in_list(type_name: str): except Exception: return False +def get_keys_list(): + try: + try: + doc = GlobalDocument.get(id='keys') + cur = list(doc.keys_list or []) + except Exception: + cur = [] + doc = GlobalDocument(keys_list=cur) + doc.meta.id = 'keys' + doc.save() + return [str(t).strip().strip(';') for t in cur] + except Exception: + return [] + +def ensure_key_in_list(key_name: str): + if not key_name: + return False + norm = str(key_name).strip().strip(';') + try: + try: + doc = GlobalDocument.get(id='keys') + cur = list(doc.keys_list or []) + except Exception: + cur = [] + doc = GlobalDocument(keys_list=cur) + doc.meta.id = 'keys' + cur_sanitized = {str(t).strip().strip(';') for t in cur} + if norm not in cur_sanitized: + cur.append(norm) + doc.keys_list = cur + doc.save() + return True + return False + except Exception: + return False + +def generate_registration_code(keys=None, manage_keys=None, expires_in_days: int = 30, created_by: int = None): + try: + keys = list(keys or []) + manage_keys = list(manage_keys or []) + for k in list(keys): + ensure_key_in_list(k) + for mk in list(manage_keys): + ensure_key_in_list(mk) + code = uuid.uuid4().hex + str(int(time.time()))[-6:] + now = datetime.now(timezone.utc) + expires = now + timedelta(days=max(1, int(expires_in_days or 30))) + doc = RegistrationCodeDocument( + code=code, + keys=keys, + manage_keys=manage_keys, + created_at=now.isoformat(), + expires_at=expires.isoformat(), + created_by=created_by, + ) + doc.meta.id = code + doc.save() + return { + "code": code, + "keys": keys, + "manage_keys": manage_keys, + "created_at": now.isoformat(), + "expires_at": expires.isoformat(), + } + except Exception as e: + return None + +def get_registration_code(code: str): + try: + doc = RegistrationCodeDocument.get(id=str(code)) + return { + "code": getattr(doc, 'code', str(code)), + "keys": list(getattr(doc, 'keys', []) or []), + "manage_keys": list(getattr(doc, 'manage_keys', []) or []), + "created_at": getattr(doc, 'created_at', None), + "expires_at": getattr(doc, 'expires_at', None), + "created_by": getattr(doc, 'created_by', None), + } + except Exception: + return None + def get_doc_id(data): """ 根据数据内容生成唯一ID(用于去重) @@ -527,7 +609,10 @@ def write_user_data(user_data): username=user_data.get('username'), password_hash=pwd_hash_b64, password_salt=pwd_salt_b64, - permission=perm_val + permission=perm_val, + email=user_data.get('email'), + key=list(user_data.get('key') or []), + manage_key=list(user_data.get('manage_key') or []), ) user.save() print(f"用户数据写入成功: {user_data.get('username')}") diff --git a/elastic/indexes.py b/elastic/indexes.py index 1a37881..ce9f83a 100644 --- a/elastic/indexes.py +++ b/elastic/indexes.py @@ -1,5 +1,5 @@ INDEX_NAME = "wordsearch2666661" -USER_NAME = "users1111166" +USER_NAME = "users11111666789" ACHIEVEMENT_INDEX_NAME = INDEX_NAME USER_INDEX_NAME = USER_NAME -GLOBAL_INDEX_NAME = "global1111111121" +GLOBAL_INDEX_NAME = "global11111111" diff --git a/elastic/templates/elastic/registration_codes.html b/elastic/templates/elastic/registration_codes.html new file mode 100644 index 0000000..9869074 --- /dev/null +++ b/elastic/templates/elastic/registration_codes.html @@ -0,0 +1,119 @@ + + + + + 注册码管理 + + {% csrf_token %} + + + + +
+
+

管理注册码

+
+
+ + + +
+
+
+
+ + +
+
+
+ + +
+
+
+
+
+ + + +
+
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/elastic/urls.py b/elastic/urls.py index dc2690b..a885e77 100644 --- a/elastic/urls.py +++ b/elastic/urls.py @@ -32,6 +32,10 @@ urlpatterns = [ # 管理页面 path('manage/', views.manage_page, name='manage_page'), path('user_manage/', views.user_manage, name='user_manage'), + 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/generate/', views.generate_registration_code_view, name='generate_registration_code'), # 分析接口 path('analytics/trend/', views.analytics_trend_view, name='analytics_trend'), diff --git a/elastic/views.py b/elastic/views.py index 05a358d..4a076f7 100644 --- a/elastic/views.py +++ b/elastic/views.py @@ -604,3 +604,67 @@ def user_manage(request): user_id_qs = request.GET.get("user_id") context = {"user_id": user_id_qs or session_user_id} return render(request, "elastic/users.html", context) + +@require_http_methods(["GET"]) +@ensure_csrf_cookie +def registration_code_manage_page(request): + session_user_id = request.session.get("user_id") + if session_user_id is None: + from django.shortcuts import redirect + return redirect("/accounts/login/") + if int(request.session.get("permission", 1)) != 0: + from django.shortcuts import redirect + return redirect("/main/home/") + user_id_qs = request.GET.get("user_id") + context = {"user_id": user_id_qs or session_user_id} + return render(request, "elastic/registration_codes.html", context) + +@require_http_methods(["GET"]) +def get_keys_list_view(request): + if request.session.get("user_id") is None: + return JsonResponse({"status": "error", "message": "未登录"}, status=401) + if int(request.session.get("permission", 1)) != 0: + return JsonResponse({"status": "error", "message": "无权限"}, status=403) + lst = get_keys_list() + return JsonResponse({"status": "success", "data": lst}) + +@require_http_methods(["POST"]) +@csrf_protect +def add_key_view(request): + if request.session.get("user_id") is None: + return JsonResponse({"status": "error", "message": "未登录"}, status=401) + if int(request.session.get("permission", 1)) != 0: + return JsonResponse({"status": "error", "message": "无权限"}, status=403) + try: + payload = json.loads(request.body.decode("utf-8")) + except Exception: + return JsonResponse({"status": "error", "message": "JSON无效"}, status=400) + key_name = (payload.get("key") or "").strip() + if not key_name: + return JsonResponse({"status": "error", "message": "key不能为空"}, status=400) + ok = ensure_key_in_list(key_name) + if not ok: + return JsonResponse({"status": "error", "message": "key已存在或写入失败"}, status=409) + return JsonResponse({"status": "success"}) + +@require_http_methods(["POST"]) +@csrf_protect +def generate_registration_code_view(request): + if request.session.get("user_id") is None: + return JsonResponse({"status": "error", "message": "未登录"}, status=401) + if int(request.session.get("permission", 1)) != 0: + return JsonResponse({"status": "error", "message": "无权限"}, status=403) + try: + payload = json.loads(request.body.decode("utf-8")) + except Exception: + return JsonResponse({"status": "error", "message": "JSON无效"}, status=400) + keys = list(payload.get("keys") or []) + manage_keys = list(payload.get("manage_keys") or []) + try: + days = int(payload.get("expires_in_days", 30)) + except Exception: + days = 30 + result = generate_registration_code(keys=keys, manage_keys=manage_keys, expires_in_days=days, created_by=request.session.get("user_id")) + if not result: + return JsonResponse({"status": "error", "message": "生成失败"}, status=500) + return JsonResponse({"status": "success", "data": result}) diff --git a/main/templates/main/home.html b/main/templates/main/home.html index 79436f8..c062136 100644 --- a/main/templates/main/home.html +++ b/main/templates/main/home.html @@ -145,6 +145,7 @@ 数据管理 {% if is_admin %} 用户管理 + 注册码管理 {% endif %}