From aba94c074ad3b832f164ad2b01545f7050bd27b3 Mon Sep 17 00:00:00 2001 From: spdis Date: Sun, 9 Nov 2025 20:31:37 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BB=E5=BD=95=E4=B8=8A=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Achievement_Inputing/__init__.py | 0 Achievement_Inputing/asgi.py | 16 + Achievement_Inputing/settings.py | 134 +++++ Achievement_Inputing/urls.py | 26 + Achievement_Inputing/wsgi.py | 16 + ESConnect.py | 780 ------------------------ ESTest.py | 10 - accounts/__init__.py | 1 + accounts/apps.py | 6 + accounts/es_client.py | 38 ++ accounts/static/accounts/login.js | 123 ++++ accounts/templates/accounts/login.html | 40 ++ accounts/urls.py | 12 + accounts/views.py | 146 +++++ app.py | 784 ------------------------- db.sqlite3 | Bin 0 -> 131072 bytes json_converter.py | 100 ---- main/__init__.py | 1 + main/apps.py | 6 + main/templates/main/home.html | 62 ++ main/urls.py | 9 + main/views.py | 17 + manage.py | 22 + requirements.txt | 6 - templates/all.html | 414 ------------- templates/base.html | 275 --------- templates/edit.html | 316 ---------- templates/edited.html | 256 -------- templates/index.html | 617 ------------------- templates/login.html | 162 ----- templates/my_data.html | 503 ---------------- templates/profile.html | 538 ----------------- templates/register.html | 440 -------------- templates/results.html | 362 ------------ templates/user_management.html | 356 ----------- 35 files changed, 675 insertions(+), 5919 deletions(-) create mode 100644 Achievement_Inputing/__init__.py create mode 100644 Achievement_Inputing/asgi.py create mode 100644 Achievement_Inputing/settings.py create mode 100644 Achievement_Inputing/urls.py create mode 100644 Achievement_Inputing/wsgi.py delete mode 100644 ESConnect.py delete mode 100644 ESTest.py create mode 100644 accounts/__init__.py create mode 100644 accounts/apps.py create mode 100644 accounts/es_client.py create mode 100644 accounts/static/accounts/login.js create mode 100644 accounts/templates/accounts/login.html create mode 100644 accounts/urls.py create mode 100644 accounts/views.py delete mode 100644 app.py create mode 100644 db.sqlite3 delete mode 100644 json_converter.py create mode 100644 main/__init__.py create mode 100644 main/apps.py create mode 100644 main/templates/main/home.html create mode 100644 main/urls.py create mode 100644 main/views.py create mode 100644 manage.py delete mode 100644 requirements.txt delete mode 100644 templates/all.html delete mode 100644 templates/base.html delete mode 100644 templates/edit.html delete mode 100644 templates/edited.html delete mode 100644 templates/index.html delete mode 100644 templates/login.html delete mode 100644 templates/my_data.html delete mode 100644 templates/profile.html delete mode 100644 templates/register.html delete mode 100644 templates/results.html delete mode 100644 templates/user_management.html diff --git a/Achievement_Inputing/__init__.py b/Achievement_Inputing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Achievement_Inputing/asgi.py b/Achievement_Inputing/asgi.py new file mode 100644 index 0000000..64c80ca --- /dev/null +++ b/Achievement_Inputing/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for Achievement_Inputing project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Achievement_Inputing.settings') + +application = get_asgi_application() diff --git a/Achievement_Inputing/settings.py b/Achievement_Inputing/settings.py new file mode 100644 index 0000000..f034975 --- /dev/null +++ b/Achievement_Inputing/settings.py @@ -0,0 +1,134 @@ +""" +Django settings for Achievement_Inputing project. + +Generated by 'django-admin startproject' using Django 5.2.8. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-p^*6tak7wy1z#bw__#o^s5hsydearm=(-s(km!-61j2(#)*+-t' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["127.0.0.1", "localhost"] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'accounts', + 'main', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'Achievement_Inputing.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'Achievement_Inputing.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Security settings for cookies and headers (dev-friendly defaults) +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SAMESITE = 'Lax' +SESSION_COOKIE_SECURE = False if DEBUG else True + +CSRF_COOKIE_SECURE = False if DEBUG else True +CSRF_COOKIE_SAMESITE = 'Lax' + +X_FRAME_OPTIONS = 'DENY' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/Achievement_Inputing/urls.py b/Achievement_Inputing/urls.py new file mode 100644 index 0000000..9d29e93 --- /dev/null +++ b/Achievement_Inputing/urls.py @@ -0,0 +1,26 @@ +""" +URL configuration for Achievement_Inputing project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from main.views import home as main_home + +urlpatterns = [ + path('admin/', admin.site.urls), + path('accounts/', include('accounts.urls', namespace='accounts')), + path('main/', include('main.urls', namespace='main')), + path('', main_home, name='root_home'), +] diff --git a/Achievement_Inputing/wsgi.py b/Achievement_Inputing/wsgi.py new file mode 100644 index 0000000..97894ed --- /dev/null +++ b/Achievement_Inputing/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for Achievement_Inputing project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Achievement_Inputing.settings') + +application = get_wsgi_application() diff --git a/ESConnect.py b/ESConnect.py deleted file mode 100644 index 38303cd..0000000 --- a/ESConnect.py +++ /dev/null @@ -1,780 +0,0 @@ -from elasticsearch import Elasticsearch -# import os -# import json -import hashlib -import requests -import json - -# Elasticsearch连接配置 -ES_URL = "http://localhost:9200" -AUTH = None # 如需认证则改为("用户名","密码") - -# document=os.open('results/output.json', os.O_RDONLY) - -# 创建Elasticsearch客户端实例,连接到本地Elasticsearch服务 -es = Elasticsearch(["http://localhost:9200"]) - -# 定义索引名称和类型名称 -data_index_name = "wordsearch266666" -users_index_name = "users" - -def create_index_with_mapping(): - """修正后的索引映射配置""" - # 新增一个用户mapping - data_mapping = { - "mappings": { - "properties": { - "writer_id":{"type": "text"}, - - "data": { - "type": "text", # 存储转换后的字符串,支持分词搜索 - "analyzer": "ik_max_word", - "search_analyzer": "ik_smart" - }, - "image": {"type": "keyword"}, # 存储图片路径或标识 - } - } - } - - users_mapping = { - "mappings": { - "properties": { - "user_id":{"type":"long"}, #由系统分配的用户唯一id - "username":{"type":"keyword"}, #可修改的用户名 - "password":{"type":"keyword"}, #密码 - "premission":{"type":"integer"},#权限组分配(比方说0就是管理员,1是普通用户,以此类推) - } - } - } - - # 检查数据索引是否存在,不存在则创建 - if not es.indices.exists(index=data_index_name): - es.indices.create(index=data_index_name, body=data_mapping) - print(f"创建索引 {data_index_name} 并设置映射") - else: - print(f"索引 {data_index_name} 已存在") - - # 检查用户索引是否存在,不存在则创建 - if not es.indices.exists(index=users_index_name): - es.indices.create(index=users_index_name, body=users_mapping) - print(f"创建索引 {users_index_name} 并设置映射") - admin={"user_id":0000000000,"username": "admin", "password": "admin", "premission": 0} - write_user_data(admin) - else: - print(f"索引 {users_index_name} 已存在") -def update_document(es, index_name, doc_id=None, updated_doc=None): - """更新指定ID的文档""" - es.update(index=index_name, id=doc_id, body={"doc": updated_doc}) - - -def get_doc_id(data): - """ - 根据数据内容生成唯一ID(用于去重) - - 参数: - data (dict): 包含文档数据的字典 - - 返回: - str: 基于数据内容生成的MD5哈希值作为唯一ID - """ - # 使用data字段的内容生成唯一字符串 - data_str = data.get('data', '') - image_str = data.get('image', '') - unique_str = f"{data_str}{image_str}" - # 使用MD5哈希生成唯一ID - return hashlib.md5(unique_str.encode('utf-8')).hexdigest() - - -def insert_data(data): - """ - 向Elasticsearch插入数据 - - 参数: - data (dict): 要插入的数据 - - 返回: - bool: 插入成功返回True,失败返回False - """ - # 生成文档唯一ID - return batch_write_data(data) - - -def search_data(query): - """ - 在Elasticsearch中搜索数据 - - 参数: - query (str): 搜索关键词 - - 返回: - list: 包含搜索结果的列表,每个元素是一个文档的源数据 - """ - # 执行多字段匹配搜索 - result = es.search(index=data_index_name, body={"query": {"multi_match": {"query": query, "fields": ["*"]}}}) - # 返回搜索结果的源数据部分 - return [hit["_source"] for hit in result['hits']['hits']] - -def search_all(): - """ - 获取所有文档 - - 返回: - list: 包含所有文档的列表,每个元素包含文档ID和源数据 - """ - # 执行匹配所有文档的查询 - result = es.search(index=data_index_name, body={"query": {"match_all": {}}}) - # 返回包含文档ID和源数据的列表 - return [{ - "_id": hit["_id"], - **hit["_source"] - } for hit in result['hits']['hits']] - -def delete_by_id(doc_id): - """ - 根据 doc_id 删除文档 - - 参数: - doc_id (str): 要删除的文档ID - - 返回: - bool: 删除成功返回True,失败返回False - """ - try: - # 执行删除操作 - es.delete(index=data_index_name, id=doc_id) - return True - except Exception as e: - print("删除失败:", str(e)) - return False - -def update_by_id(doc_id, updated_data): - """ - 根据文档ID更新数据 - - 参数: - doc_id (str): 要更新的文档ID - updated_data (dict): 更新的数据内容 - - 返回: - bool: 更新成功返回True,失败返回False - """ - try: - # 执行更新操作 - es.update(index=data_index_name, id=doc_id, body={"doc": updated_data}) - print(f"文档 {doc_id} 更新成功") - return True - except Exception as e: - print(f"更新失败: {str(e)}") - return False - -def get_by_id(doc_id): - """ - 根据文档ID获取单个文档 - - 参数: - doc_id (str): 要获取的文档ID - - 返回: - dict or None: 成功返回文档数据,失败返回None - """ - try: - # 执行获取操作 - result = es.get(index=data_index_name, id=doc_id) - if result['found']: - return { - "_id": result['_id'], - **result['_source'] - } - return None - except Exception as e: - print(f"获取文档失败: {str(e)}") - return None - -def search_by_any_field(keyword): - """全字段模糊搜索(支持拼写错误)""" - try: - # update_data_mapping() - response = requests.post( - f"{ES_URL}/{data_index_name}/_search", - auth=AUTH, - json={ - "query": { - "multi_match": { - "query": keyword, - "fields": ["*"], # 匹配所有字段 - "fuzziness": "AUTO", # 启用模糊匹配 - } - } - } - ) - response.raise_for_status() - results = response.json()["hits"]["hits"] - print(f"\n模糊搜索 '{keyword}' 找到 {len(results)} 条结果:") - - for doc in results: - print(f"\n文档ID: {doc['_id']}") - if '_source' in doc: - max_key_len = max(len(k) for k in doc['_source'].keys()) - for key, value in doc['_source'].items(): - # 提取高亮部分 - highlight = doc.get('highlight', {}).get(key, [value])[0] - print(f"{key:>{max_key_len + 2}} : {highlight}") - else: - print("无_source数据") - - return results - except requests.exceptions.HTTPError as e: - print(f"搜索失败: {e.response.text}") - return [] - -def batch_write_data(data): - """批量写入获奖数据""" - try: - response = requests.post( - f"{ES_URL}/{data_index_name}/_doc", - json=data, - auth=AUTH, - headers={"Content-Type": "application/json"} - ) - response.raise_for_status() - doc_id = response.json()["_id"] - print(f"文档写入成功,ID: {doc_id}, 内容: {data}") - return True - except requests.exceptions.HTTPError as e: - print(f"文档写入失败: {e.response.text}, 数据: {data}") - return False - -def write_user_data(data): - """写入用户数据""" - try: - response = requests.post( - f"{ES_URL}/{users_index_name}/_doc", - json=data, - auth=AUTH, - headers={"Content-Type": "application/json"} - ) - response.raise_for_status() - doc_id = response.json()["_id"] - print(f"文档写入成功,ID: {doc_id}, 内容: {data}") - return True - except requests.exceptions.HTTPError as e: - print(f"文档写入失败: {e.response.text}, 数据: {data}") - return False - -def verify_user(username, password): - """ - 验证用户登录信息 - - 参数: - username (str): 用户名 - password (str): 密码 - - 返回: - dict or None: 验证成功返回用户信息,失败返回None - """ - try: - # 搜索用户名匹配的用户 - response = requests.post( - f"{ES_URL}/{users_index_name}/_search", - auth=AUTH, - json={ - "query": { - "term": { - "username": username - } - } - } - ) - response.raise_for_status() - results = response.json()["hits"]["hits"] - - if results: - user_data = results[0]["_source"] - # 验证密码 - if user_data.get("password") == password: - print(f"用户 {username} 登录成功") - return user_data - else: - print(f"用户 {username} 密码错误") - return None - else: - print(f"用户 {username} 不存在") - return None - - except requests.exceptions.HTTPError as e: - print(f"用户验证失败: {e.response.text}") - return None - -def get_user_by_username(username): - """ - 根据用户名查询用户信息 - - 参数: - username (str): 用户名 - - 返回: - dict or None: 查询成功返回用户信息,失败返回None - """ - try: - response = requests.post( - f"{ES_URL}/{users_index_name}/_search", - auth=AUTH, - json={ - "query": { - "term": { - "username": username - } - } - } - ) - response.raise_for_status() - results = response.json()["hits"]["hits"] - - if results: - return results[0]["_source"] - else: - return None - - except requests.exceptions.HTTPError as e: - print(f"用户查询失败: {e.response.text}") - return None - -def create_user(username, password, permission=1): - """ - 创建新用户 - - 参数: - username (str): 用户名 - password (str): 密码 - permission (int): 权限级别,默认为1(普通用户) - - 返回: - bool: 创建成功返回True,失败返回False - """ - # 检查用户名是否已存在 - if get_user_by_username(username): - print(f"用户名 {username} 已存在") - return False - - # 生成新的用户ID - import time - user_id = int(time.time() * 1000) # 使用时间戳作为用户ID - - user_data = { - "user_id": user_id, - "username": username, - "password": password, - "premission": permission - } - - return write_user_data(user_data) - -def get_all_users(): - """ - 获取所有用户信息 - - 返回: - list: 包含所有用户信息的列表 - """ - try: - response = requests.post( - f"{ES_URL}/{users_index_name}/_search", - auth=AUTH, - json={ - "query": { - "match_all": {} - }, - "size": 1000 # 限制返回数量,可根据需要调整 - } - ) - response.raise_for_status() - results = response.json()["hits"]["hits"] - - users = [] - for hit in results: - user_data = hit["_source"] - user_data["_id"] = hit["_id"] # 添加文档ID用于后续操作 - users.append(user_data) - - return users - - except requests.exceptions.HTTPError as e: - print(f"获取用户列表失败: {e.response.text}") - return [] - -def update_user_password(username, new_password): - """ - 更新用户密码 - - 参数: - username (str): 用户名 - new_password (str): 新密码 - - 返回: - bool: 更新成功返回True,失败返回False - """ - try: - # 先查找用户 - response = requests.post( - f"{ES_URL}/{users_index_name}/_search", - auth=AUTH, - json={ - "query": { - "term": { - "username": username - } - } - } - ) - response.raise_for_status() - results = response.json()["hits"]["hits"] - - if not results: - print(f"用户 {username} 不存在") - return False - - # 获取用户文档ID - doc_id = results[0]["_id"] - user_data = results[0]["_source"] - - # 更新密码 - user_data["password"] = new_password - - # 更新文档 - update_response = requests.post( - f"{ES_URL}/{users_index_name}/_doc/{doc_id}", - auth=AUTH, - json=user_data, - headers={"Content-Type": "application/json"} - ) - update_response.raise_for_status() - - print(f"用户 {username} 密码更新成功") - return True - - except requests.exceptions.HTTPError as e: - print(f"更新用户密码失败: {e.response.text}") - return False - -def delete_user(username): - """ - 删除用户 - - 参数: - username (str): 要删除的用户名 - - 返回: - bool: 删除成功返回True,失败返回False - """ - try: - # 防止删除管理员账户 - if username == "admin": - print("不能删除管理员账户") - return False - - # 先查找用户 - response = requests.post( - f"{ES_URL}/{users_index_name}/_search", - auth=AUTH, - json={ - "query": { - "term": { - "username": username - } - } - } - ) - response.raise_for_status() - results = response.json()["hits"]["hits"] - - if not results: - print(f"用户 {username} 不存在") - return False - - # 获取用户文档ID - doc_id = results[0]["_id"] - - # 删除用户 - delete_response = requests.delete( - f"{ES_URL}/{users_index_name}/_doc/{doc_id}", - auth=AUTH - ) - delete_response.raise_for_status() - - print(f"用户 {username} 删除成功") - return True - - except requests.exceptions.HTTPError as e: - print(f"删除用户失败: {e.response.text}") - return False - -def update_user_permission(username, new_permission): - """ - 更新用户权限 - - 参数: - username (str): 用户名 - new_permission (int): 新权限级别 - - 返回: - bool: 更新成功返回True,失败返回False - """ - try: - # 防止修改管理员权限 - if username == "admin": - print("不能修改管理员权限") - return False - - # 先查找用户 - response = requests.post( - f"{ES_URL}/{users_index_name}/_search", - auth=AUTH, - json={ - "query": { - "term": { - "username": username - } - } - } - ) - response.raise_for_status() - results = response.json()["hits"]["hits"] - - if not results: - print(f"用户 {username} 不存在") - return False - - # 获取用户文档ID - doc_id = results[0]["_id"] - user_data = results[0]["_source"] - - # 更新权限 - user_data["premission"] = new_permission - - # 更新文档 - update_response = requests.post( - f"{ES_URL}/{users_index_name}/_doc/{doc_id}", - auth=AUTH, - json=user_data, - headers={"Content-Type": "application/json"} - ) - update_response.raise_for_status() - - print(f"用户 {username} 权限更新成功") - return True - - except requests.exceptions.HTTPError as e: - print(f"更新用户权限失败: {e.response.text}") - return False - -def search_data_by_user(user_id, keyword=None): - """ - 根据用户ID查询该用户的数据,支持关键词搜索 - - 参数: - user_id (str): 用户ID - keyword (str, optional): 搜索关键词 - - 返回: - list: 包含文档ID和源数据的列表 - """ - try: - if keyword: - # 带关键词的搜索 - query = { - "bool": { - "must": [ - {"term": {"user_id": user_id}}, - { - "multi_match": { - "query": keyword, - "fields": ["*"], - "fuzziness": "AUTO" - } - } - ] - } - } - else: - # 只按用户ID搜索 - query = { - "term": {"user_id": user_id} - } - - response = requests.post( - f"{ES_URL}/{data_index_name}/_search", - auth=AUTH, - json={ - "query": query, - "size": 1000 # 限制返回数量 - } - ) - response.raise_for_status() - results = response.json()["hits"]["hits"] - - # 返回包含文档ID和源数据的列表 - return [{ - "_id": hit["_id"], - **hit["_source"] - } for hit in results] - - except requests.exceptions.HTTPError as e: - print(f"查询用户数据失败: {e.response.text}") - return [] - -def update_data_by_id(doc_id, updated_data, user_id): - """ - 根据文档ID更新数据(仅允许数据所有者修改) - - 参数: - doc_id (str): 文档ID - updated_data (dict): 更新的数据 - user_id (str): 当前用户ID - - 返回: - bool: 更新成功返回True,失败返回False - """ - try: - # 先查询文档,验证所有权 - response = requests.get( - f"{ES_URL}/{data_index_name}/_doc/{doc_id}", - auth=AUTH - ) - response.raise_for_status() - doc = response.json() - - # 检查文档是否存在 - if not doc.get("found"): - print(f"文档 {doc_id} 不存在") - return False - - # 检查用户权限(只能修改自己的数据) - if doc["_source"].get("user_id") != user_id: - print(f"用户 {user_id} 无权修改文档 {doc_id}") - return False - - # 保持用户ID不变 - updated_data["user_id"] = user_id - - # 更新文档 - update_response = requests.post( - f"{ES_URL}/{data_index_name}/_doc/{doc_id}", - auth=AUTH, - json=updated_data, - headers={"Content-Type": "application/json"} - ) - update_response.raise_for_status() - - print(f"文档 {doc_id} 更新成功") - return True - - except requests.exceptions.HTTPError as e: - print(f"更新文档失败: {e.response.text}") - return False - -def delete_data_by_id(doc_id, user_id): - """ - 根据文档ID删除数据(仅允许数据所有者或管理员删除) - - 参数: - doc_id (str): 文档ID - user_id (str): 当前用户ID - - 返回: - bool: 删除成功返回True,失败返回False - """ - try: - # 先查询文档,验证所有权 - response = requests.get( - f"{ES_URL}/{data_index_name}/_doc/{doc_id}", - auth=AUTH - ) - response.raise_for_status() - doc = response.json() - - # 检查文档是否存在 - if not doc.get("found"): - print(f"文档 {doc_id} 不存在") - return False - - # 检查用户权限(只能删除自己的数据,管理员可以删除所有数据) - doc_user_id = doc["_source"].get("user_id") - if doc_user_id != user_id: - # 检查是否为管理员 - user_info = get_user_by_username(user_id) # 这里需要用户名,稍后会修改 - if not user_info or user_info.get("premission") != 0: - print(f"用户 {user_id} 无权删除文档 {doc_id}") - return False - - # 删除文档 - delete_response = requests.delete( - f"{ES_URL}/{data_index_name}/_doc/{doc_id}", - auth=AUTH - ) - delete_response.raise_for_status() - - print(f"文档 {doc_id} 删除成功") - return True - - except requests.exceptions.HTTPError as e: - print(f"删除文档失败: {e.response.text}") - return False - -def update_user_own_password(user_id, old_password, new_password): - """ - 用户修改自己的密码 - - 参数: - user_id (str): 用户ID - old_password (str): 旧密码 - new_password (str): 新密码 - - 返回: - bool: 修改成功返回True,失败返回False - """ - try: - # 先查找用户 - response = requests.post( - f"{ES_URL}/{users_index_name}/_search", - auth=AUTH, - json={ - "query": { - "term": { - "user_id": user_id - } - } - } - ) - response.raise_for_status() - results = response.json()["hits"]["hits"] - - if not results: - print(f"用户 {user_id} 不存在") - return False - - user_data = results[0]["_source"] - doc_id = results[0]["_id"] - - # 验证旧密码 - if user_data.get("password") != old_password: - print("旧密码错误") - return False - - # 更新密码 - user_data["password"] = new_password - - # 更新文档 - update_response = requests.post( - f"{ES_URL}/{users_index_name}/_doc/{doc_id}", - auth=AUTH, - json=user_data, - headers={"Content-Type": "application/json"} - ) - update_response.raise_for_status() - - print(f"用户 {user_id} 密码修改成功") - return True - - except requests.exceptions.HTTPError as e: - print(f"修改密码失败: {e.response.text}") - return False diff --git a/ESTest.py b/ESTest.py deleted file mode 100644 index 4c6f9bc..0000000 --- a/ESTest.py +++ /dev/null @@ -1,10 +0,0 @@ -from elasticsearch import Elasticsearch - -# 连接本地的 Elasticsearch 实例 -es = Elasticsearch(["http://localhost:9200"]) - -# 检查连接是否成功 -if es.ping(): - print("连接成功!") -else: - print("连接失败!") diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..a0e90ea --- /dev/null +++ b/accounts/__init__.py @@ -0,0 +1 @@ +"""Accounts app for secure login flow.""" \ No newline at end of file diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..122e64e --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' \ No newline at end of file diff --git a/accounts/es_client.py b/accounts/es_client.py new file mode 100644 index 0000000..f3296b1 --- /dev/null +++ b/accounts/es_client.py @@ -0,0 +1,38 @@ +import base64 +import hashlib + + +def _salt_for_username(username: str) -> bytes: + return hashlib.sha256(username.encode('utf-8')).digest() + + +def _derive_password(password_plain: str, salt: bytes) -> bytes: + return hashlib.pbkdf2_hmac('sha256', password_plain.encode('utf-8'), salt, 100_000, dklen=32) + + +def get_user_by_username(username: str): + """ + Placeholder for ES lookup. Returns fixed JSON for a demo user. + In production this should query ES with the given mapping. + + Demo user: + - username: admin + - password: Password123! (stored as PBKDF2-derived secret only) + - user_id: 1 + - premission: 0 (admin) + """ + if username != 'admin': + return None + + salt = _salt_for_username(username) + # Demo: derive and store secret from a known password for the placeholder + derived = _derive_password('Password123!', salt) + return { + 'user_id': 1, + 'username': 'admin', + # Store only the derived secret, not the plaintext password + 'password': base64.b64encode(derived).decode('ascii'), + 'premission': 0, + # Expose salt to the client during challenge so both sides derive consistently + 'salt': base64.b64encode(salt).decode('ascii'), + } \ No newline at end of file diff --git a/accounts/static/accounts/login.js b/accounts/static/accounts/login.js new file mode 100644 index 0000000..8afb0e4 --- /dev/null +++ b/accounts/static/accounts/login.js @@ -0,0 +1,123 @@ +// Utility: read cookie value +function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); +} + +// Convert base64 string to ArrayBuffer +function base64ToArrayBuffer(b64) { + const binary = atob(b64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes.buffer; +} + +// ArrayBuffer to base64 +function arrayBufferToBase64(buffer) { + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} + +async function deriveKey(password, saltBytes, iterations = 100000, length = 32) { + const encoder = new TextEncoder(); + const keyMaterial = await window.crypto.subtle.importKey( + 'raw', + encoder.encode(password), + { name: 'PBKDF2' }, + false, + ['deriveBits'] + ); + + const derivedBits = await window.crypto.subtle.deriveBits( + { + name: 'PBKDF2', + salt: saltBytes, + iterations, + hash: 'SHA-256' + }, + keyMaterial, + length * 8 + ); + + return new Uint8Array(derivedBits); +} + +async function hmacSha256(keyBytes, messageBytes) { + const key = await window.crypto.subtle.importKey( + 'raw', + keyBytes, + { name: 'HMAC', hash: { name: 'SHA-256' } }, + false, + ['sign'] + ); + const signature = await window.crypto.subtle.sign('HMAC', key, messageBytes); + return new Uint8Array(signature); +} + +document.getElementById('loginForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const errorEl = document.getElementById('error'); + errorEl.textContent = ''; + + const username = document.getElementById('username').value.trim(); + const password = document.getElementById('password').value; + if (!username || !password) { + errorEl.textContent = '请输入账户与密码'; + return; + } + + const btn = document.getElementById('loginBtn'); + btn.disabled = true; + + try { + // Step 1: get challenge (nonce + salt) + const csrftoken = getCookie('csrftoken'); + const chalResp = await fetch('/accounts/challenge/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrftoken || '' + }, + body: JSON.stringify({ username }) + }); + if (!chalResp.ok) { + throw new Error('获取挑战失败'); + } + const chal = await chalResp.json(); + const nonceBytes = new Uint8Array(base64ToArrayBuffer(chal.nonce)); + const saltBytes = new Uint8Array(base64ToArrayBuffer(chal.salt)); + + // Step 2: derive secret and compute HMAC + const derived = await deriveKey(password, saltBytes, 100000, 32); + const hmac = await hmacSha256(derived, nonceBytes); + const hmacB64 = arrayBufferToBase64(hmac); + + // Step 3: submit login with username and hmac + const submitResp = await fetch('/accounts/login/submit/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrftoken || '' + }, + body: JSON.stringify({ username, hmac: hmacB64 }) + }); + const submitJson = await submitResp.json(); + if (!submitResp.ok || !submitJson.ok) { + throw new Error(submitJson.message || '登录失败'); + } + // Redirect to home with user_id + window.location.href = submitJson.redirect_url; + } catch (err) { + console.error(err); + errorEl.textContent = err.message || '发生错误'; + } finally { + btn.disabled = false; + } +}); \ No newline at end of file diff --git a/accounts/templates/accounts/login.html b/accounts/templates/accounts/login.html new file mode 100644 index 0000000..de921b8 --- /dev/null +++ b/accounts/templates/accounts/login.html @@ -0,0 +1,40 @@ +{% load static %} + + + + + + + 安全登录 + + + + +
+

登录到系统

+
+ {% csrf_token %} + + + + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..3d0b481 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + + +app_name = "accounts" + +urlpatterns = [ + path("login/", views.login_page, name="login"), + path("challenge/", views.challenge, name="challenge"), + path("login/submit/", views.login_submit, name="login_submit"), + path("logout/", views.logout, name="logout"), +] \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..3d3d6c6 --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,146 @@ +import base64 +import json +import os + +from django.http import JsonResponse, HttpResponseBadRequest +from django.shortcuts import render, redirect +from django.views.decorators.http import require_http_methods +from django.views.decorators.csrf import csrf_protect +from django.conf import settings + +from .es_client import get_user_by_username, _salt_for_username + + +@require_http_methods(["GET"]) +def login_page(request): + return render(request, "accounts/login.html") + + +@require_http_methods(["POST"]) +@csrf_protect +def challenge(request): + try: + payload = json.loads(request.body.decode("utf-8")) + except json.JSONDecodeError: + return HttpResponseBadRequest("Invalid JSON") + + username = payload.get("username", "").strip() + if not username: + return HttpResponseBadRequest("Username required") + + # Generate nonce and compute per-username salt + nonce = os.urandom(16) + salt = _salt_for_username(username) + + # Persist challenge in session to prevent replay with mismatched user + request.session["challenge_nonce"] = base64.b64encode(nonce).decode("ascii") + request.session["challenge_username"] = username + + return JsonResponse({ + "nonce": base64.b64encode(nonce).decode("ascii"), + "salt": base64.b64encode(salt).decode("ascii"), + }) + + +@require_http_methods(["POST"]) +@csrf_protect +def login_submit(request): + try: + payload = json.loads(request.body.decode("utf-8")) + except json.JSONDecodeError: + return HttpResponseBadRequest("Invalid JSON") + + username = payload.get("username", "").strip() + client_hmac_b64 = payload.get("hmac", "") + if not username or not client_hmac_b64: + return HttpResponseBadRequest("Missing fields") + + # Validate challenge stored in session + session_username = request.session.get("challenge_username") + nonce_b64 = request.session.get("challenge_nonce") + if not session_username or not nonce_b64 or session_username != username: + return HttpResponseBadRequest("Challenge not found or mismatched user") + + # Lookup user in ES (placeholder) + user = get_user_by_username(username) + if not user: + return JsonResponse({"ok": False, "message": "User not found"}, status=401) + + # Server-side HMAC verification + try: + nonce = base64.b64decode(nonce_b64) + stored_derived_b64 = user.get("password", "") + stored_derived = base64.b64decode(stored_derived_b64) + # HMAC-SHA256: server computes with stored derived secret + import hmac, hashlib + server_hmac = hmac.new(stored_derived, nonce, hashlib.sha256).digest() + server_hmac_b64 = base64.b64encode(server_hmac).decode("ascii") + except Exception: + return HttpResponseBadRequest("Verification error") + + if not hmac.compare_digest(server_hmac_b64, client_hmac_b64): + return JsonResponse({"ok": False, "message": "Invalid credentials"}, status=401) + + # Successful login: rotate session key and set user session + try: + request.session.cycle_key() + except Exception: + pass + + request.session["user_id"] = user["user_id"] + request.session["username"] = user["username"] + request.session["premission"] = user["premission"] + + # Clear challenge to prevent reuse + for k in ("challenge_username", "challenge_nonce"): + if k in request.session: + del request.session[k] + + return JsonResponse({ + "ok": True, + "redirect_url": f"/main/home/?user_id={user['user_id']}", + }) + + +@require_http_methods(["GET"]) +def home(request): + # Minimal placeholder page per requirement + # Ensure user_id is passed via query and session contains id + user_id = request.GET.get("user_id") + session_user_id = request.session.get("user_id") + context = { + "user_id": user_id or session_user_id, + } + return render(request, "accounts/home.html", context) + + +@require_http_methods(["POST"]) +@csrf_protect +def logout(request): + # Flush the session to clear all data and rotate the key + try: + request.session.flush() + except Exception: + pass + + # Return a response that also deletes cookies client-side + resp = JsonResponse({"ok": True, "redirect_url": "/accounts/login/"}) + try: + # Delete session cookie + resp.delete_cookie( + settings.SESSION_COOKIE_NAME, + path='/', + samesite=settings.SESSION_COOKIE_SAMESITE, + secure=settings.SESSION_COOKIE_SECURE, + ) + # Optionally delete CSRF cookie to satisfy "清除cookie" 的要求 + resp.delete_cookie( + settings.CSRF_COOKIE_NAME, + path='/', + samesite=settings.CSRF_COOKIE_SAMESITE, + secure=settings.CSRF_COOKIE_SECURE, + ) + except Exception: + pass + + return resp \ No newline at end of file diff --git a/app.py b/app.py deleted file mode 100644 index 6dc8c76..0000000 --- a/app.py +++ /dev/null @@ -1,784 +0,0 @@ -import base64 -from flask import Flask, request, render_template, redirect, url_for, jsonify, session, flash, send_from_directory -from werkzeug.utils import secure_filename -import os -import uuid -from PIL import Image -import re -import json -import requests -from functools import wraps -from ESConnect import * -from json_converter import json_to_string, string_to_json -from openai import OpenAI -# import config - -# 创建Flask应用实例 -app = Flask(__name__) -# app.config.from_object(config.Config) - -# 设置会话密钥,用于加密会话数据 -app.secret_key = 'your-secret-key-change-this-in-production' -# OCR和信息提取函数,使用大模型API处理图片并提取结构化信息 -# 权限装饰器 -def login_required(f): - """要求用户登录的装饰器""" - @wraps(f) - def decorated_function(*args, **kwargs): - if 'user_id' not in session: - flash('请先登录', 'error') - return redirect(url_for('login')) - return f(*args, **kwargs) - return decorated_function -def admin_required(f): - """要求管理员权限的装饰器""" - @wraps(f) - def decorated_function(*args, **kwargs): - if 'user_id' not in session: - flash('请先登录', 'error') - return redirect(url_for('login')) - if session.get('permission', 1) != 0: - flash('权限不足,需要管理员权限', 'error') - return redirect(url_for('index')) - return f(*args, **kwargs) - return decorated_function -def user_or_admin_required(f): - """要求普通用户或管理员权限的装饰器""" - @wraps(f) - def decorated_function(*args, **kwargs): - if 'user_id' not in session: - flash('请先登录', 'error') - return redirect(url_for('login')) - permission = session.get('permission', 1) - if permission not in [0, 1]: - flash('权限不足', 'error') - return redirect(url_for('index')) - return f(*args, **kwargs) - return decorated_function - -def ocr_and_extract_info(image_path): - """ - 使用大模型API进行OCR识别并提取图片中的结构化信息 - - 参数: - image_path (str): 图片文件路径 - - 返回: - dict: 包含提取信息的字典,格式为 {'id': '', 'name': '', 'students': '', 'teacher': ''} - """ - def encode_image(image_path): - """ - 将图片编码为base64格式 - - 参数: - image_path (str): 图片文件路径 - - 返回: - str: base64编码的图片字符串 - """ - with open(image_path, "rb") as image_file: - return base64.b64encode(image_file.read()).decode('utf-8') - - # 将图片转换为base64编码 - base64_image = encode_image(image_path) - - # 初始化OpenAI客户端,使用百度AI Studio的API - client = OpenAI( - api_key="188f57db3766e02ed2c7e18373996d84f4112272", - # 含有 AI Studio 访问令牌的环境变量,https://aistudio.baidu.com/account/accessToken, - base_url="https://aistudio.baidu.com/llm/lmapi/v3", # aistudio 大模型 api 服务域名 - ) - - # 调用大模型API进行图片识别和信息提取 - chat_completion = client.chat.completions.create( - messages=[ - {'role': 'system', 'content': '你是一个能理解图片和文本的助手,请根据用户提供的信息进行回答。'}, - {'role': 'user', "content": [ - {"type": "text", "text": "请识别这张图片中的信息,将你认为重要的数据转换为不包含嵌套的json,不要显示其它信息以便于解析" - "直接输出json结果即可" - "你可以自行决定使用哪些json字段"}, - { - "type": "image_url", - "image_url": { - "url": f"data:image/png;base64,{base64_image}" - } - } - ]} - ], - model="ernie-4.5-turbo-vl-32k", # 使用百度文心大模型 - ) - - # 获取API返回的文本内容 - response_text = chat_completion.choices[0].message.content - - # 添加调试信息:输出模型返回的原始字符串 - print("=" * 50) - print("模型返回的原始字符串:") - print(response_text) - print("=" * 50) - - def parse_respound(text): - """ - 解析API返回的文本,提取JSON数据 - - 参数: - text (str): API返回的文本 - - 返回: - dict or None: 解析成功返回字典,失败返回None - """ - # 尝试直接解析标准JSON - try: - result=json.loads(text) - if result: - print("✓ 成功解析标准JSON格式") - return result - except json.JSONDecodeError: - print("✗ 无法解析标准JSON格式") - pass - - # 提取markdown代码块中的内容 - code_block = re.search(r'```json\n(.*?)```', text, re.DOTALL) - if code_block: - try: - result=json.loads(code_block.group(1)) - if result: - print("✓ 成功解析markdown代码块中的JSON") - return result - except json.JSONDecodeError: - print("✗ 无法解析markdown代码块中的JSON") - pass - - # 尝试替换单引号并解析 - try: - fixed_json = text.replace("'", "\"") - result=json.loads(fixed_json) - if(result): - print("✓ 成功解析替换单引号后的JSON") - return result - except json.JSONDecodeError: - print("✗ 无法解析替换单引号后的JSON") - pass - - # 解析API返回的文本 - result_data = parse_respound(response_text) - - # 添加调试信息:输出解析结果 - print("解析结果:") - if result_data: - print(f"✓ 解析成功: {result_data}") - else: - print("✗ 解析失败,返回None") - print("=" * 50) - - return result_data - - """ - 模拟大模型识别图像并返回结构化JSON。 - 实际应调用Qwen-VL或其他OCR+解析服务。 - """ - - -# 登录页面路由 -@app.route('/login', methods=['GET', 'POST']) -def login(): - """ - 处理用户登录 - - GET: 显示登录页面 - POST: 处理登录表单提交 - """ - if request.method == 'POST': - username = request.form.get('username') - password = request.form.get('password') - - if not username or not password: - flash('请输入用户名和密码', 'error') - return render_template('login.html') - - # 验证用户 - user_data = verify_user(username, password) - if user_data: - # 登录成功,设置会话 - session['user_id'] = user_data['user_id'] - session['username'] = user_data['username'] - session['permission'] = user_data['premission'] - flash(f'欢迎回来,{username}!', 'success') - return redirect(url_for('index')) - else: - flash('用户名或密码错误', 'error') - return render_template('login.html') - - return render_template('login.html') - - -# 登出路由 -@app.route('/logout') -def logout(): - """ - 处理用户登出 - """ - session.clear() - flash('已成功登出', 'info') - return redirect(url_for('login')) - - -# 用户管理页面路由 -@app.route('/user_management') -@admin_required -def user_management(): - """ - 显示用户管理页面(仅管理员可访问) - """ - users = get_all_users() - return render_template('user_management.html', users=users) - - -# 注册新用户路由 -@app.route('/register', methods=['GET', 'POST']) -@admin_required -def register(): - """ - 注册新用户(仅管理员可访问) - - GET: 显示注册页面 - POST: 处理注册表单提交 - """ - if request.method == 'POST': - username = request.form.get('username') - password = request.form.get('password') - confirm_password = request.form.get('confirm_password') - permission = int(request.form.get('permission', 1)) - - # 验证输入 - if not username or not password: - flash('请输入用户名和密码', 'error') - return render_template('register.html') - - if password != confirm_password: - flash('两次输入的密码不一致', 'error') - return render_template('register.html') - - if len(password) < 6: - flash('密码长度至少6位', 'error') - return render_template('register.html') - - # 检查用户名是否已存在 - existing_user = get_user_by_username(username) - if existing_user: - flash('用户名已存在', 'error') - return render_template('register.html') - - # 创建新用户 - success = create_user(username, password, permission) - if success: - flash(f'用户 {username} 创建成功', 'success') - return redirect(url_for('user_management')) - else: - flash('创建用户失败', 'error') - return render_template('register.html') - - return render_template('register.html') - - -# 修改用户密码路由 -@app.route('/change_password/', methods=['POST']) -@admin_required -def change_password(username): - """ - 修改用户密码(仅管理员可访问) - """ - new_password = request.form.get('new_password') - confirm_password = request.form.get('confirm_password') - - if not new_password or not confirm_password: - flash('请输入新密码', 'error') - return redirect(url_for('user_management')) - - if new_password != confirm_password: - flash('两次输入的密码不一致', 'error') - return redirect(url_for('user_management')) - - if len(new_password) < 6: - flash('密码长度至少6位', 'error') - return redirect(url_for('user_management')) - - success = update_user_password(username, new_password) - if success: - flash(f'用户 {username} 密码修改成功', 'success') - else: - flash(f'修改用户 {username} 密码失败', 'error') - - return redirect(url_for('user_management')) - - -# 修改用户权限路由 -@app.route('/change_permission/', methods=['POST']) -@admin_required -def change_permission(username): - """ - 修改用户权限(仅管理员可访问) - """ - new_permission = int(request.form.get('permission', 1)) - - success = update_user_permission(username, new_permission) - if success: - flash(f'用户 {username} 权限修改成功', 'success') - else: - flash(f'修改用户 {username} 权限失败', 'error') - - return redirect(url_for('user_management')) - - -# 删除用户路由 -@app.route('/delete_user/', methods=['POST']) -@admin_required -def delete_user_route(username): - """ - 删除用户(仅管理员可访问) - """ - success = delete_user(username) - if success: - flash(f'用户 {username} 删除成功', 'success') - else: - flash(f'删除用户 {username} 失败', 'error') - - return redirect(url_for('user_management')) - - -# 个人设置页面路由 -@app.route('/profile') -@login_required -def profile(): - """ - 显示个人设置页面 - """ - return render_template('profile.html') - - -# 修改个人密码路由 -@app.route('/change_own_password', methods=['POST']) -@login_required -def change_own_password(): - """ - 用户修改自己的密码 - """ - old_password = request.form.get('old_password') - new_password = request.form.get('new_password') - confirm_password = request.form.get('confirm_password') - - # 验证输入 - if not old_password or not new_password or not confirm_password: - flash('请填写所有密码字段', 'error') - return redirect(url_for('profile')) - - if new_password != confirm_password: - flash('两次输入的新密码不一致', 'error') - return redirect(url_for('profile')) - - if len(new_password) < 6: - flash('新密码长度至少6位', 'error') - return redirect(url_for('profile')) - - # 调用修改密码函数 - success = update_user_own_password(session['user_id'], old_password, new_password) - if success: - flash('密码修改成功', 'success') - else: - flash('密码修改失败,请检查旧密码是否正确', 'error') - - return redirect(url_for('profile')) - - -# 个人数据页面路由 -@app.route('/my_data') -@login_required -def my_data(): - """ - 显示用户自己的数据 - """ - user_id = session['user_id'] - keyword = request.args.get('keyword', '') - - # 查询用户自己的数据 - if keyword: - data = search_data_by_user(user_id, keyword) - else: - data = search_data_by_user(user_id) - - # 将data字段从字符串转换回JSON格式以便显示 - processed_data = [] - for item in data: - if 'data' in item and item['data']: - try: - # 将data字段的字符串转换回JSON - original_data = string_to_json(item['data']) - # 合并原始数据和其他字段 - display_item = { - '_id': item['_id'], - 'image': item.get('image', ''), - **original_data # 展开原始数据字段 - } - processed_data.append(display_item) - except Exception as e: - # 如果转换失败,保持原始格式 - processed_data.append(item) - else: - processed_data.append(item) - - return render_template('my_data.html', data=processed_data, keyword=keyword) - -# 首页路由 -@app.route('/') -@login_required -def index(): - """ - 渲染首页模板 - - 返回: - str: 渲染后的HTML页面 - """ - return render_template('index.html') - -# 图片上传路由 -@app.route('/upload', methods=['POST']) -@user_or_admin_required -def upload_image(): - """ - 处理图片上传请求,调用OCR识别但不存储结果 - - 返回: - JSON: 识别结果,供用户编辑确认 - """ - # 获取上传的文件 - file = request.files.get('file') - if not file: - return jsonify({"error": "No file uploaded"}), 400 - - # 保存上传的图片 - filename = f"{uuid.uuid4()}_{file.filename}" - image_path = os.path.join("image", filename) - file.save(image_path) - - # 调用大模型进行识别 - try: - print(f"开始处理图片: {image_path}") - original_data = ocr_and_extract_info(image_path) # 获取原始JSON数据 - if original_data: - print(f"识别成功: {original_data}") - # 返回识别结果和图片文件名,供用户编辑确认 - return jsonify({ - "message": "识别成功,请确认数据后点击录入", - "data": original_data, - "image": filename - }) - else: - print("✗ 无法识别图片内容") - return jsonify({"error": "无法识别图片内容"}), 400 - except Exception as e: - print(f"✗ 处理过程中发生错误: {str(e)}") - return jsonify({"error": str(e)}), 500 - -# 确认录入路由 -@app.route('/confirm', methods=['POST']) -@user_or_admin_required -def confirm_data(): - """ - 确认并录入用户编辑后的数据 - - 返回: - JSON: 录入成功或失败的响应 - """ - try: - # 获取前端提交的数据 - request_data = request.get_json() - if not request_data: - return jsonify({"error": "没有接收到数据"}), 400 - - # 获取编辑后的数据和图片文件名 - edited_data = request_data.get('data', {}) - image_filename = request_data.get('image', '') - - if not edited_data: - return jsonify({"error": "数据不能为空"}), 400 - - # 使用json_converter将JSON数据转换为字符串 - data_string = json_to_string(edited_data) - print(f"转换后的数据字符串: {data_string}") - - # 构造新的数据结构,只包含data和image字段,并添加用户ID - processed_data = { - "data": data_string, - "image": image_filename, # 存储图片文件名 - "user_id": session['user_id'] # 添加用户ID关联 - } - print(f"准备存储的数据: {processed_data}") - - # 存入ES - insert_data(processed_data) - print("✓ 数据成功存储到Elasticsearch") - - return jsonify({"message": "数据录入成功", "data": edited_data}) - - except Exception as e: - print(f"✗ 录入过程中发生错误: {str(e)}") - return jsonify({"error": str(e)}), 500 - -# 搜索路由 -@app.route('/search') -@user_or_admin_required -def search(): - """ - 处理搜索请求,从Elasticsearch中检索匹配的数据 - - 返回: - JSON: 搜索结果列表 - """ - keyword = request.args.get('q') - if not keyword: - return jsonify([]) - results = search_by_any_field(keyword) - - # 处理搜索结果,将data字段转换回JSON格式 - processed_results = [] - for result in results: - if '_source' in result and 'data' in result['_source']: - try: - # 将data字段的字符串转换回JSON - original_data = string_to_json(result['_source']['data']) - # 构造新的结果格式 - processed_result = { - '_id': result.get('_id', ''), - '_source': { - 'image': result['_source'].get('image', ''), - **original_data # 展开原始数据字段 - } - } - processed_results.append(processed_result) - except Exception as e: - # 如果转换失败,保持原始格式 - processed_results.append(result) - else: - processed_results.append(result) - - print(processed_results) - return jsonify(processed_results) - -# 结果页面路由 -@app.route('/results') -@user_or_admin_required -def results_page(): - """ - 渲染搜索结果页面 - - 返回: - str: 渲染后的HTML页面 - """ - return render_template('results.html') - -# 显示所有数据路由 -@app.route('/all') -@admin_required -def show_all(): - """ - 获取所有数据并渲染到页面 - - 返回: - str: 渲染后的HTML页面,包含所有数据 - """ - all_data = search_all() - # 将data字段从字符串转换回JSON格式以便显示 - processed_data = [] - for item in all_data: - if 'data' in item and item['data']: - try: - # 将data字段的字符串转换回JSON - original_data = string_to_json(item['data']) - # 合并原始数据和其他字段 - display_item = { - '_id': item['_id'], - 'image': item.get('image', ''), - **original_data # 展开原始数据字段 - } - processed_data.append(display_item) - except Exception as e: - # 如果转换失败,保持原始格式 - processed_data.append(item) - else: - processed_data.append(item) - - return render_template('all.html', data=processed_data) - -# 添加图片路由 -@app.route('/image/') -def serve_image(filename): - """ - 提供图片文件服务 - - 参数: - filename (str): 图片文件名 - - 返回: - Response: 图片文件响应 - """ - from flask import send_from_directory - return send_from_directory('image', filename) - -# 删除数据路由 -@app.route('/delete/', methods=['POST']) -@login_required -def delete_entry(doc_id): - """ - 根据文档ID删除数据 - - 参数: - doc_id (str): 要删除的文档ID - - 返回: - 重定向到所有数据页面或错误信息 - """ - user_id = session['user_id'] - user_permission = session.get('permission', 1) - - # 管理员可以删除所有数据,普通用户只能删除自己的数据 - if user_permission == 0: # 管理员 - success = delete_by_id(doc_id) - redirect_url = 'show_all' - else: # 普通用户 - success = delete_data_by_id(doc_id, user_id) - redirect_url = 'my_data' - - if success: - return redirect(url_for(redirect_url)) - else: - return "删除失败", 500 - - -# 批量删除数据路由 -@app.route('/batch_delete', methods=['POST']) -@admin_required -def batch_delete(): - """ - 批量删除选中的数据(仅管理员可访问) - - 返回: - 重定向到所有数据页面或错误信息 - """ - try: - # 获取选中的文档ID列表 - doc_ids = request.form.getlist('doc_ids') - - if not doc_ids: - flash('请选择要删除的记录', 'error') - return redirect(url_for('show_all')) - - # 批量删除选中的文档 - success_count = 0 - for doc_id in doc_ids: - if delete_by_id(doc_id): - success_count += 1 - - if success_count > 0: - flash(f'成功删除 {success_count} 条记录', 'success') - else: - flash('删除失败,请重试', 'error') - - return redirect(url_for('show_all')) - - except Exception as e: - print(f"批量删除失败: {str(e)}") - flash('批量删除失败,请重试', 'error') - return redirect(url_for('show_all')) - - -@app.route('/edit/', methods=['GET', 'POST']) -@login_required -def edit_entry(doc_id): - """ - 编辑数据条目(用户只能编辑自己的数据) - """ - if request.method == 'GET': - # 获取要编辑的数据 - try: - # 先获取文档检查权限 - response = requests.get( - f"{ES_URL}/{data_index_name}/_doc/{doc_id}", - auth=AUTH - ) - response.raise_for_status() - doc = response.json() - - if not doc.get("found"): - flash('数据不存在', 'error') - return redirect(url_for('my_data')) - - # 检查权限 - user_id = session['user_id'] - user_permission = session.get('permission', 1) - doc_user_id = doc["_source"].get("user_id") - - # 管理员可以编辑所有数据,普通用户只能编辑自己的数据 - if user_permission != 0 and doc_user_id != user_id: - flash('您无权编辑此数据', 'error') - return redirect(url_for('my_data')) - - # 解析数据 - data_str = doc["_source"].get("data", "{}") - original_data = string_to_json(data_str) - - edit_data = { - '_id': doc_id, - 'image': doc["_source"].get('image', ''), - **original_data - } - - return render_template('edit.html', data=edit_data) - - except Exception as e: - flash('获取数据失败', 'error') - return redirect(url_for('my_data')) - - else: # POST 请求 - 保存编辑 - try: - # 获取编辑后的数据 - edited_data = {} - for key, value in request.form.items(): - if key != '_id' and key != 'image': - edited_data[key] = value - - # 转换为字符串格式 - data_string = json_to_string(edited_data) - - # 构造更新数据 - updated_data = { - "data": data_string, - "image": request.form.get('image', ''), - "user_id": session['user_id'] - } - - # 更新数据 - success = update_data_by_id(doc_id, updated_data, session['user_id']) - - if success: - flash('数据更新成功', 'success') - else: - flash('数据更新失败', 'error') - - # 根据用户权限重定向 - if session.get('permission', 1) == 0: - return redirect(url_for('show_all')) - else: - return redirect(url_for('my_data')) - - except Exception as e: - flash('保存数据失败', 'error') - return redirect(url_for('my_data')) - -# 主程序入口 -if __name__ == '__main__': - # 创建Elasticsearch索引 - create_index_with_mapping() - # 创建图片存储目录 - os.makedirs("image", exist_ok=True) - # 启动Flask应用 - app.run(use_reloader=False) \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..0f0d574ce136c3515770405b7bfbee2d5d314243 GIT binary patch literal 131072 zcmeI5Yit|Yb;mg(MM{>)ktJ)#&92uJZSR_qWz7uV-lhpx=GyY=VN0^TT68fTk|Sv> zK4g-5Ip~90ZxU?U0!@(sEzqVYP@qNop-%)w8lXr}6m8KaEwTmrNYQ*s8z4>5BFTp| zK<~XAavmH#Rw=nXBrQmvKMMmXjf@^GBzW8tvJ z<4KbL>A&rtD`dsj-XQ;Smi7Hslb*}(4);=H{)f~?;C~oAGw}U^mj?c{|GmKX`hU~^ z<=$1_FL}S+^P`@5?lYdR?dRyI`sn1&tyMQS>aBIX z*xFRcN+uplhy}5rY*zI9PVk!bVE$$+q$;hL%$LF?#-Rx9iEx?Zi+6?2^ik&YJWEF{Om#ETb_vSyd;Z-{7S}M_sJva?s9gRGA;n5MlCVxHbXBW`=jNBfbGK(^!Z#PD zXR`~-;n#D^;q2|D`RO_0ayB=&M5Hn&uB8@oujLkUb5prR%SnxMM8j9<(FG=HdTx4Y zIy*D7Z0yWk?P`SGn!3E3ERJ@L=$<$b5GE$Lha(QO>#6BuCUjL*F7Qo%(6S%Dt*y(Fmfh@T1ZRzmPl2 zd02N?zh5GcYYM3-Vo#oL=$i3>BXw2>_U8HmLMFpKOquoDYD?zT)w(dc_V&Bfe3#L& znnBCUBX7)vx)~IGfO;H#OmoN|5XQ#1=DGtYW6mNA><8{xuA5}A4Q5rmhpgh#5=`ABeZg%!wUSaMPa-8HmCP4o z#aNy_hPV{yHDKD`C|nE=_S||3aVbV)?`Wbg7EBg-tXlO3gjkH*+hjHFQqpCY&tCH` z7LTCzB`b0w*i`RcE1j6!4@YnI_-WH#W^o$*WnH_!sZ|P^W7BJk&vEb~y#_CllF{Ze z7Z4^Vxjog&uB@-t)CPSSGq{*LdTf3pUB{0_jc_sVJ95H)%5kU7VlOLdsX<1pSW4DRS}A39M=5EQ)yA5;Cpj&N9~&Z`uC+Z$%H{^k zE^XARJG+(I#K~>LiI}kl#BzN{ zawNy)Vf4s4?~+s zOUcyr0CCc8ykfiYnh7G7aXr+N6pxG7{k@(G*Rn?bhhd}?3Ey&ctAn>SLPcAUE8ciK zA;+eBdp!ctR4nTi;vzI9DuuPGPVxe-u41uxO3wO-D?-_D zC7Z4cS>&p%Qn#)y$TgWth+g92LdV5{9RjJ?Oj4fgAxudL)*&3+gNTxtk&`Kopu6Bi zckr1+F&>kH{BL{w{FaCRPyXxt7x^vz)BLaUJGL$j8z2AzAOHd&00JNY0w4eaAOHd& z00PG%Fyix$bB<{?r-S|8u|BqwW!nZ%c+a*rX*P$PW~8;%$*c%oZ;W%9li?rf@lNy^ z2Uy;M<}5}z?_}HBoT)H;f;jf`pZ4(o#s3Ze&-|D8-yj?K0Ra#I0T2KI5C8!X009sH z0T2KI5O`b!PWjF}&$TBG8UOlU@Lf2^S!YC8)_a4#(NUI0pcw*R$TupmEtWp;j`&8$ z*k&hTQ0Y186VCLrlt6nz1fBnXdhn+_{J-*l$bX8@^H=!6(7%QLIP|$tEi@Op92yS( zcku6mzaM-jxD-4$`2E4J5B}cZLs9@gAOHd&00JNY0w4eaAOHd&;2;n=-Rt2lO!&8S zZCf#3YNpgmm~MxS?Uw{%6P5hr`+1H0atn)9Yfs1Sxt1OI9U_%gn_X*9+b%px?9Pq* z>70IxGqTyX7(YcUCQ`jjmF119qs-VBrjyVoM~JcXO0OZ$VQxsY6(HY$&;o`|M*%~t zwLp}7|G|wXdZ}QCH5F|&eSv)7!JU2C$CT68zK&$N5hfpRkjQ<8)b?>hBeNi1W>5=5 zTie1AMK?c0KDpq|T<}pb?ZZ?BU4M~$M!}6mz3lAlHM4ViwiO{?Mv&U}8t3m=7y?>0 z-xy>BYzd8l)-J;d%SfDHXExIn{Y;-TLD47pxJj-%84+mhHyoOo z13#ncNHFvc`}$wzY}o|gDPR9M=SUrR*-pb}`}MzP$k%^?OaL$w0(AVJ{L+8?fB*=9 z00@8p2!H?xfB*=900@8p2t36E==i_!{vUXX!$fEx00JNY0w4eaAOHd&00JNY0wC~A z6TtZYGhO`%9t1!D1V8`;KmY_l00ck)1VG>!A<%yR=VwB9o{^9dCJ2B42!H?xfB*=9 z00@8p2t4xyn%myq*}b36mNb1MpW2dR<@Eh+X)9J-%hb!ot@WL1a(!icw|H5*v0J;N zR~t*Z{Q3iNXQ6&?=5{uJ^FgjMw^UMhn~Tkb9^xsMt;t)7zAKX|hNHa_E*v;K^W@f2$clY-C#_UY-w!B-(?_N!39u(q= zC~Ye@7O$&253)(Iap#T2TX$|0?r!X;r5oDvl%%e#Z*0D~A>EqE&duyx%dcLMwx;7t z3s(xt=JKtIUR%!JSS%My>rgezzo5wlkY)X=QD<-jqnx&HIN&t?k~B z3i6%ZH{{a#^u6lN%v5VTUw*Ce=Hhg6y8POD{%&b=THn?S*Kd@_`h$X8+RE$G$=T~- zJE-;i_1R)^d%m=jnO@ypy|z`nHebtROX(XkcT%sdT}yA>PgbtRGb@d_dr;|2GTxs5 z&wqjc!ZUvX000Pp00@8p2!H?xfB*=900@8p2z+1!&UoB-bcKXBwYAqao~2!H?x zfB*=900@8p2!H?xeBcD={QuMZricGO{s;X3@ZaIT#eb9k7ydo|tNdT_U*W$*z6C!vMmIG{gf-U>$vZs$N18nJM%U++i zpYzd$mo9p|-u`}!{~ugcI0^zF00JNY0w4eaAOHd&00JNY0#5^h_V53D8e%~VAOHd& z00JNY0w4eaAOHd&00JNY0`3Ix{_oBOdqDsMKmY_l00ck)1V8`;KmY_l;AtX&_y4D< zHW3R5fB*=900@8p2!H?xfB*=900_7f!27>D7wiQA5C8!X009sH0T2KI5C8!X0D-58 z0N($frrJa-AOHd&00JNY0w4eaAOHd&00JQ3P9PZgp67(`vmX9@=pXvu^M5(GJ@~Hw zRPSBiX9s@J{;%gpJ@edWJYVyC#NnekG!hWzC%C4hSBlz>y4hG$HtSkVS*=w!H|ni* zy{IHrN!E(#ywl*+)Iu)1lnYPKUCq4_jyMl@m5+qy=eo@z;foP=R=pUBHa|KX5Y~yL z`oWTzLWO9{#A6AuAQqI(ihkdzV8jr3dv5yH?Zb7tJ3UW zpmlUZtCjV7U9VQ^in&gMNXH9_LPpk@{x0KT7tCEo`*^g7?Whcs8$+UrJ*!-H#90jUWuoYTD*15 zns&TPotQ_J{k&Loo7S}bI0_J%i@^XcVxuvbtm?RkJDneGH|9mWCk_OJi3#rENJGt+ zT3w*!CgkkW2aN{2lznw3=YBIG_mQwpu7*{u7M`153eVl1nF-%qn4ZlpEQepuEr+wW zm*%JEh@;uu+!DFc+IOGsQVY4)atpb+sobLFR8&8`SB0!+z&XI7wS}j?R(!CY6teO5}qUk=| zx_6Vatmrjzn*A-?LJqXh%Bo&6&y-4-XCmdP(1yCQ(k+r4Q40-yi>P$!rx7dns(M8u zh`Pd$N+&-&Gw<5l z?^5$!M#pLfEibOuR@YU+Y>(EM33aoVBsVUXOSj9tS>R~rh?3{tzZb^FxaPV8s0lH! zAGl+|vbVu>cYBV;0)FAz*n!QWMZDE6*m@s{qn|#*?>&i6?$6T0*xnn10U?{^9=>8W zD0UZN3$uOJI~R59Wyo?Q=+fX^T^y?cGEYalnyBf~uA>j+TD{rh7skiAWd_#hFYDU< zO|4SU9GhMnLdU_2^cuWGN=BQ@TtJwdzFJco^kuATM~}^Kr0e*x zYDC@GFz-8Z!hYki_ZQBiq-aTZzq?@kpZ16N0Ra#I0T2KI5C8!X009sH0T2Lzr<*{~ zd(AWA`CkuzFSHT-lfnNu@vRd--v2v&-{>3mf1At$DEIu4ciH>rJwM02+cQPXo}iz% zGZO(J8s$DQ!Ll+HZCf$3;r1}3_(WR*O7d^wb#wi2;q@P_Ru}kki zKYx~#l#epj^LE^5`{&;NFj+fjD9_&Z%{ey5%0V7?D31)F8lz+HXBf|oG;za53lFFh zV&WL3lN0gBJxoVV>NdV;3xQ0*h&+NR7$JNA!&<#LF7QaEV2lNX-EnS@H)q8haDqa$ zsL|?BL`luZQVG?Z5M!3|8=_riQkh0w+TC3rc`^y9l(vURbnikqKnDgMa%Q(-p4vgN z*N|)NKuqIfT|3U1F5P{Qb)3@ZCar~xvmQQepcAt>;4WJFEJDS8_1K0BqRs1P141>% zH9u~>Jz9k*WIRDBsd=rWlu6x}lvFZbj1^;fR`D*%eXf=7VtBA;+ilb;h?41Oijer2 zEOJYoelZ}#V%%QFdP}vIbkT6eUiB^($Eto)-@didQtf^?+8jD#ooi{xQj2B1LZ?o2 z=1LY4$+(tRlTL#djIiuaE&G?Z!S0yR(Az_U(dNY$0)j&3THdm@l#Xrp)f~?kw6vCY zN_+kgY1Rs(Q_~8DpZz5|N<^Y>|IG7jlF%n7tu3J=)v4%3*EY~GKUM>%=I?2R2AiSK zdcbPk6ph_AZG#M=wkOEQj_Z|99jR87m6Ey|3D?VNsYK={g*Pj8eU;1_={&22r>^Iw zUZ)p>)&3_w5*DMhD_kSvmYPx~54!3qnTO(h%yU9BJ19E;=}~n~?bc&@Z_lu@o`H48 z?`vS$2$@bDa+^idydjWYEz33UoA)#(g)Pjk-8r*XTFK`VD{`SDsB0YlL-y9YzoBDZ zXWO&oZssb8Ha1fqZ9maw`niBmAi*pj77X)9E*UkG(A1)2nSt$$`^SzPhSTR!0x-%D z>D!}Z%58s^+nYS9c3bd@6jzh^WK1?6GMH~?IH!$pT%I>v?K}PF0cU$Zfs*UZ=7~f; zaw;I)BE4n85jcA>shiV(GpR%(lZeO7mejI%ZoeAZ&w*2K%Z|1uw#J$g#{Zw7lm?`L z00@8p2!H?xfB*=900@8p2!Oz2B7pb*#{?E`K>!3m00ck)1V8`;KmY_l00cnb2_k^^ z|0k$2AO!?K00ck)1V8`;KmY_l00ck)1RfIsy#GHYuy6|kAOHd&00JNY0w4eaAOHd& z00K`C0lfb|L6re1AOHd&00JNY0w4eaAOHd&00JQJmsKmY_l00ck)1V8`;KmY_l00cnbi6CH^|Ia@Wf*=9}KmY_l00ck)1V8`; zKmY_l00cnbkrQaY|MTDEzw*eP!yyQO00@8p2!H?xfB*=900@8p2!OyNBJkpi-Yh4J ha$-V~CgfOHlCQ||EAh + + + + 主页 + + {% csrf_token %} + + + +
+
+

主页(留白)

+

用户ID:{{ user_id }}

+

这里留白即可,主页不由当前实现负责。

+
+ +

提示:已使用安全的会话 Cookie 管理登录状态。

+ +

+ + +

+ + +
+ + \ No newline at end of file diff --git a/main/urls.py b/main/urls.py new file mode 100644 index 0000000..78ec49d --- /dev/null +++ b/main/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + + +app_name = "main" + +urlpatterns = [ + path("home/", views.home, name="home"), +] \ No newline at end of file diff --git a/main/views.py b/main/views.py new file mode 100644 index 0000000..e704199 --- /dev/null +++ b/main/views.py @@ -0,0 +1,17 @@ +from django.shortcuts import render, redirect +from django.views.decorators.http import require_http_methods + + +@require_http_methods(["GET"]) +def home(request): + # Enforce login: require session user_id + session_user_id = request.session.get("user_id") + if not session_user_id: + return redirect("/accounts/login/") + + # Show user_id (prefer query param if present, but don't trust it) + user_id_qs = request.GET.get("user_id") + context = { + "user_id": user_id_qs or session_user_id, + } + return render(request, "main/home.html", context) \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..12e2979 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Achievement_Inputing.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 803c1b9..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -flask==3.1.1 -pillow==11.1.0 -openai==1.88.0 -elasticsearch==7.17.0 -pandas==2.2.3 -requests \ No newline at end of file diff --git a/templates/all.html b/templates/all.html deleted file mode 100644 index 0154911..0000000 --- a/templates/all.html +++ /dev/null @@ -1,414 +0,0 @@ -{% extends "base.html" %} - -{% block title %}数据操作 - 紫金·稷下薪火·云枢智海师生成果共创系统{% endblock %} - -{% block content %} - - -
-

所有已录入的奖项信息

-

在此页面可以查看所有已录入的成果信息,并进行编辑和删除操作

- - -
-
-
- - -
- - 已选择 0 项 -
-
- -
- {% if data %} - {% for item in data %} -
-
-
- -

记录 {{ loop.index }}

-
-
- 编辑 - -
-
- -
- {% if item.data %} - {# 从原始数据中解析字段 #} - {% set data_string = item.data %} - {% set pairs = data_string.split('|###|') %} - - {% for pair in pairs %} - {% if ':' in pair %} - {% set key_value = pair.split(':', 1) %} - {% set field_key = key_value[0].strip() %} - {% set field_value = key_value[1].strip() %} - - {# 处理列表格式 [item1|##|item2] #} - {% if field_value.startswith('[') and field_value.endswith(']') %} - {% set list_content = field_value[1:-1] %} - {% set field_value = list_content.split('|##|')|join(', ') %} - {% endif %} - -
- {{ field_key }}: - {{ field_value or '无' }} -
- {% endif %} - {% endfor %} - {% else %} - {# 如果没有data字段,显示解析后的字段 #} - {% for key, value in item.items() %} - {% if key not in ['_id', 'image'] %} -
- {{ key }}: - - {% if value is sequence and value is not string %} - {{ value|join(', ') if value else '无' }} - {% else %} - {{ value or '无' }} - {% endif %} - -
- {% endif %} - {% endfor %} - {% endif %} -
- - -
- {% endfor %} - {% else %} -
暂无数据
- {% endif %} -
- - 返回首页 -
- - - -{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html deleted file mode 100644 index d92b5d3..0000000 --- a/templates/base.html +++ /dev/null @@ -1,275 +0,0 @@ - - - - - - {% block title %}紫金·稷下薪火·云枢智海师生成果共创系统{% endblock %} - - - -
-

紫金 稷下薪火·云枢智海师生成果共创系统

- -
- - - -
- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
- {% for category, message in messages %} -
{{ message }}
- {% endfor %} -
- - {% endif %} - {% endwith %} - - {% block content %} - {% endblock %} -
- - - - - \ No newline at end of file diff --git a/templates/edit.html b/templates/edit.html deleted file mode 100644 index b9c8c8b..0000000 --- a/templates/edit.html +++ /dev/null @@ -1,316 +0,0 @@ -{% extends "base.html" %} - -{% block title %}编辑数据{% endblock %} - -{% block content %} -
-
-

编辑数据

-

修改您的数据信息

-
- -
- - - - - - {% if data.image %} -
-

关联图片

- 数据图片 -
- {% endif %} - - -
-

数据字段

- {% for key, value in data.items() %} - {% if key not in ['_id', 'image', 'user_id'] %} -
- - -
- {% endif %} - {% endfor %} -
- - -
- - - ↩️ - 取消 - -
-
-
- - - - - - - -{% endblock %} \ No newline at end of file diff --git a/templates/edited.html b/templates/edited.html deleted file mode 100644 index 8125021..0000000 --- a/templates/edited.html +++ /dev/null @@ -1,256 +0,0 @@ -{% extends "base.html" %} - -{% block title %}编辑成果信息 - 紫金·稷下薪火·云枢智海师生成果共创系统{% endblock %} - -{% block content %} - - -
-

编辑成果信息

- -
-
- {% if document.data %} - {# 从原始数据中解析字段 #} - {% set data_string = document.data %} - {% set pairs = data_string.split('|###|') %} - - {% for pair in pairs %} - {% if ':' in pair %} - {% set key_value = pair.split(':', 1) %} - {% set field_key = key_value[0].strip() %} - {% set field_value = key_value[1].strip() %} - - {# 处理列表格式 [item1|##|item2] #} - {% if field_value.startswith('[') and field_value.endswith(']') %} - {% set list_content = field_value[1:-1] %} - {% set field_value = list_content.split('|##|')|join(', ') %} - {% endif %} - -
- - - -
- {% endif %} - {% endfor %} - {% else %} - {# 如果没有data字段,显示提示信息 #} -
-

该记录没有可编辑的数据

-
- {% endif %} - - {% if document.image %} -
- -
- 原图片 -
-
当前关联的图片,编辑时无法修改图片
-
- {% endif %} - -
- - 取消返回 - -
-
-
-
- - -{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index c44fe09..0000000 --- a/templates/index.html +++ /dev/null @@ -1,617 +0,0 @@ -{% extends "base.html" %} - -{% block title %}录入成果 - 紫金·稷下薪火·云枢智海师生成果共创系统{% endblock %} - -{% block content %} -
-

- 成果录入 -

-

请上传包含成果信息的图片(如获奖证书、论文封面等),系统将自动识别关键信息

- -
-
- - -
支持JPG、PNG、GIF等格式,文件大小不超过10MB
-
- -
- - - -
- - - - -{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html deleted file mode 100644 index c0cac5d..0000000 --- a/templates/login.html +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - 用户登录 - 成果录入系统 - - - - - - \ No newline at end of file diff --git a/templates/my_data.html b/templates/my_data.html deleted file mode 100644 index 8aa56e2..0000000 --- a/templates/my_data.html +++ /dev/null @@ -1,503 +0,0 @@ -{% extends "base.html" %} - -{% block title %}我的数据{% endblock %} - -{% block content %} -
-

我的数据

-

查看和管理您录入的所有数据

-
- - -
-
-
- - -
-
- {% if keyword %} -
- 搜索关键词: "{{ keyword }}" - 清除搜索 -
- {% endif %} -
- - -
-
- {{ data|length }} - 条记录 -
-
- - -
- {% if data %} -
- {% for item in data %} -
- - {% if item.image %} -
- 数据图片 -
- {% endif %} - - -
- {% for key, value in item.items() %} - {% if key not in ['_id', 'image', 'user_id'] %} -
- {{ key }}: - {{ value }} -
- {% endif %} - {% endfor %} -
- - -
- - ✏️ - 编辑 - - -
-
- {% endfor %} -
- {% else %} -
-
📝
-

暂无数据

-

{% if keyword %}没有找到匹配 "{{ keyword }}" 的数据{% else %}您还没有录入任何数据{% endif %}

- 开始录入数据 -
- {% endif %} -
- - - - - - - - - - -{% endblock %} \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html deleted file mode 100644 index 09adde0..0000000 --- a/templates/profile.html +++ /dev/null @@ -1,538 +0,0 @@ -{% extends "base.html" %} - -{% block title %}个人设置{% endblock %} - -{% block content %} -
-

个人设置

-

管理您的个人信息和账户设置

-
- - -
-

- 👤 - 用户信息 -

- -
-
- 用户名: - {{ session.username }} -
-
- 权限级别: - - {{ '管理员' if session.permission == 0 else '普通用户' }} - -
-
-
- - -
-

- 🔒 - 修改密码 -

- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
- {{ message }} -
- {% endfor %} - {% endif %} - {% endwith %} - -
-
- - -
- -
- - - 密码长度至少6位 -
- -
- - - -
- -
- -
-
-
- - - - -{% endblock %} - - .permission-admin { - background: #e3f2fd; - color: #1976d2; - } - - .permission-user { - background: #f3e5f5; - color: #7b1fa2; - } - - .password-form { - background: white; - border: 2px solid #e9ecef; - border-radius: 12px; - padding: 25px; - } - - .password-form h3 { - color: #333; - margin-bottom: 20px; - font-size: 18px; - } - - .form-group { - margin-bottom: 20px; - } - - .form-group label { - display: block; - margin-bottom: 8px; - color: #555; - font-weight: 500; - } - - .form-group input { - width: 100%; - padding: 12px 16px; - border: 2px solid #e9ecef; - border-radius: 8px; - font-size: 16px; - transition: all 0.3s ease; - background: #f8f9fa; - } - - .form-group input:focus { - outline: none; - border-color: #667eea; - background: white; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); - } - - .btn { - width: 100%; - padding: 12px 24px; - border: none; - border-radius: 8px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; - text-transform: uppercase; - letter-spacing: 0.5px; - } - - .btn-primary { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - } - - .btn-primary:hover { - transform: translateY(-2px); - box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); - } - - .btn-secondary { - background: #6c757d; - color: white; - margin-top: 15px; - } - - .btn-secondary:hover { - background: #5a6268; - transform: translateY(-2px); - } - - .flash-messages { - margin-bottom: 20px; - } - - .flash-message { - padding: 12px 16px; - border-radius: 8px; - margin-bottom: 10px; - font-weight: 500; - } - - .flash-error { - background: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; - } - - .flash-success { - background: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; - } - - .flash-info { - background: #d1ecf1; - color: #0c5460; - border: 1px solid #bee5eb; - } - - .navigation-links { - text-align: center; - margin-top: 30px; - padding-top: 20px; - border-top: 1px solid #e9ecef; - } - - .nav-link { - display: inline-block; - margin: 0 15px; - color: #667eea; - text-decoration: none; - font-weight: 500; - transition: color 0.3s ease; - } - - .nav-link:hover { - color: #764ba2; - text-decoration: underline; - } - - @media (max-width: 600px) { - .profile-container { - padding: 30px 20px; - margin: 10px; - } - - .profile-header h1 { - font-size: 24px; - } - - .info-item { - flex-direction: column; - align-items: flex-start; - gap: 5px; - } - } - - - -
- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
- {% for category, message in messages %} -
- {{ message }} -
- {% endfor %} -
- {% endif %} - {% endwith %} - -
-

个人设置

-

管理您的账户信息和密码

-
- - - - - -
-

修改密码

- -
- - -
- -
- - -
- -
- - -
- - -
- - - -
- - - - \ No newline at end of file diff --git a/templates/register.html b/templates/register.html deleted file mode 100644 index 16ef347..0000000 --- a/templates/register.html +++ /dev/null @@ -1,440 +0,0 @@ -{% extends "base.html" %} - -{% block title %}注册新用户{% endblock %} - -{% block content %} -
-
-
-

注册新用户

-

创建新的系统用户账户

-
- -
-
- - - 用户名长度为3-20个字符,只能包含字母、数字和下划线 -
- -
- - - 密码长度至少6位,建议包含字母和数字 -
- -
- - - 请再次输入相同的密码进行确认 -
- -
- - - - 普通用户:可以上传图片、录入数据、查询数据
- 管理员:拥有所有权限,包括用户管理和数据管理 -
-
- -
- - - - 返回用户管理 - -
-
-
-
- - - - -{% endblock %} \ No newline at end of file diff --git a/templates/results.html b/templates/results.html deleted file mode 100644 index 9c81631..0000000 --- a/templates/results.html +++ /dev/null @@ -1,362 +0,0 @@ - - -{% extends "base.html" %} - -{% block title %}查询统计 - 紫金·稷下薪火·云枢智海师生成果共创系统{% endblock %} - -{% block content %} - - -
-

奖项成果查询

-

输入关键词(如姓名、奖项名等)搜索已录入的成果信息

- -
-
- - -
-
- -
- -
-
- - -{% endblock %} diff --git a/templates/user_management.html b/templates/user_management.html deleted file mode 100644 index b6ea8e3..0000000 --- a/templates/user_management.html +++ /dev/null @@ -1,356 +0,0 @@ -{% extends "base.html" %} - -{% block title %}用户管理{% endblock %} - -{% block content %} -
-
-

用户管理

- - 注册新用户 - -
- -
- - - - - - - - - - - {% for user in users %} - - - - - - - {% endfor %} - -
用户ID用户名权限级别操作
{{ user.user_id }}{{ user.username }} - - {% if user.premission == 0 %}管理员{% else %}普通用户{% endif %} - - - {% if user.username != 'admin' %} - - - - - - - - - {% else %} - 系统管理员 - {% endif %} -
-
-
- - - - - - - - - - - - - -{% endblock %} \ No newline at end of file