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 @@
+
+
+
+
+ 用户注册
+
+
+
+
+
注册新用户
+
+
仅允许持有管理员提供注册码的学生注册
+
+
+
+
\ 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 %}