diff --git a/ESConnect.py b/ESConnect.py index 3c93b03..d1abfc4 100644 --- a/ESConnect.py +++ b/ESConnect.py @@ -1,9 +1,8 @@ from elasticsearch import Elasticsearch -import os -import json +# import os +# import json import hashlib import requests -import json # Elasticsearch连接配置 ES_URL = "http://localhost:9200" @@ -15,14 +14,16 @@ AUTH = None # 如需认证则改为("用户名","密码") es = Elasticsearch(["http://localhost:9200"]) # 定义索引名称和类型名称 -index_name = "wordsearch2666" +data_index_name = "wordsearch266666" +users_index_name = "users" def create_index_with_mapping(): """修正后的索引映射配置""" - # 修正映射结构(移除keyword字段的非法参数) - mapping = { + # 新增一个用户mapping + data_mapping = { "mappings": { "properties": { + "writer_id":{"type": "text"}, "data": { "type": "text", # 存储转换后的字符串,支持分词搜索 "analyzer": "ik_max_word", @@ -33,13 +34,32 @@ def create_index_with_mapping(): } } - # 检查索引是否存在,不存在则创建 - if not es.indices.exists(index=index_name): - es.indices.create(index=index_name, body=mapping) - print(f"创建索引 {index_name} 并设置映射") - else: - print(f"索引 {index_name} 已存在") + 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 get_doc_id(data): @@ -85,7 +105,7 @@ def search_data(query): list: 包含搜索结果的列表,每个元素是一个文档的源数据 """ # 执行多字段匹配搜索 - result = es.search(index=index_name, body={"query": {"multi_match": {"query": query, "fields": ["*"]}}}) + result = es.search(index=data_index_name, body={"query": {"multi_match": {"query": query, "fields": ["*"]}}}) # 返回搜索结果的源数据部分 return [hit["_source"] for hit in result['hits']['hits']] @@ -97,7 +117,7 @@ def search_all(): list: 包含所有文档的列表,每个元素包含文档ID和源数据 """ # 执行匹配所有文档的查询 - result = es.search(index=index_name, body={"query": {"match_all": {}}}) + result = es.search(index=data_index_name, body={"query": {"match_all": {}}}) # 返回包含文档ID和源数据的列表 return [{ "_id": hit["_id"], @@ -116,7 +136,7 @@ def delete_by_id(doc_id): """ try: # 执行删除操作 - es.delete(index=index_name, id=doc_id) + es.delete(index=data_index_name, id=doc_id) return True except Exception as e: print("删除失败:", str(e)) @@ -125,9 +145,9 @@ def delete_by_id(doc_id): def search_by_any_field(keyword): """全字段模糊搜索(支持拼写错误)""" try: - # update_mapping() + # update_data_mapping() response = requests.post( - f"{ES_URL}/{index_name}/_search", + f"{ES_URL}/{data_index_name}/_search", auth=AUTH, json={ "query": { @@ -163,7 +183,7 @@ def batch_write_data(data): """批量写入获奖数据""" try: response = requests.post( - f"{ES_URL}/{index_name}/_doc", + f"{ES_URL}/{data_index_name}/_doc", json=data, auth=AUTH, headers={"Content-Type": "application/json"} @@ -175,3 +195,538 @@ def batch_write_data(data): 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/app.py b/app.py index 2d5b7f7..c8c66db 100644 --- a/app.py +++ b/app.py @@ -1,19 +1,62 @@ import base64 -from flask import Flask, request, render_template, redirect, url_for, jsonify +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 ESConnect import * from json_converter import json_to_string, string_to_json from openai import OpenAI +from functools import wraps # import config # 创建Flask应用实例 app = Flask(__name__) +# 设置会话密钥,用于加密会话数据 +app.secret_key = 'your-secret-key-change-this-in-production' # app.config.from_object(config.Config) +# 权限装饰器 +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 + # OCR和信息提取函数,使用大模型API处理图片并提取结构化信息 def ocr_and_extract_info(image_path): """ @@ -138,8 +181,249 @@ def ocr_and_extract_info(image_path): """ +# 登录页面路由 +@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(): """ 渲染首页模板 @@ -151,12 +435,13 @@ def index(): # 图片上传路由 @app.route('/upload', methods=['POST']) +@user_or_admin_required def upload_image(): """ - 处理图片上传请求,调用OCR识别并存储结果 + 处理图片上传请求,调用OCR识别但不存储结果 返回: - JSON: 上传成功或失败的响应 + JSON: 识别结果,供用户编辑确认 """ # 获取上传的文件 file = request.files.get('file') @@ -173,20 +458,13 @@ def upload_image(): print(f"开始处理图片: {image_path}") original_data = ocr_and_extract_info(image_path) # 获取原始JSON数据 if original_data: - # 使用json_converter将JSON数据转换为字符串 - data_string = json_to_string(original_data) - print(f"转换后的数据字符串: {data_string}") - - # 构造新的数据结构,只包含data和image字段 - processed_data = { - "data": data_string, - "image": filename # 存储图片文件名 - } - print(f"准备存储的数据: {processed_data}") - - insert_data(processed_data) # 存入ES - print("✓ 数据成功存储到Elasticsearch") - return jsonify({"message": "成功录入", "data": original_data, "processed": processed_data}) + print(f"识别成功: {original_data}") + # 返回识别结果和图片文件名,供用户编辑确认 + return jsonify({ + "message": "识别成功,请确认数据后点击录入", + "data": original_data, + "image": filename + }) else: print("✗ 无法识别图片内容") return jsonify({"error": "无法识别图片内容"}), 400 @@ -194,8 +472,54 @@ def upload_image(): 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中检索匹配的数据 @@ -235,6 +559,7 @@ def search(): # 结果页面路由 @app.route('/results') +@user_or_admin_required def results_page(): """ 渲染搜索结果页面 @@ -246,6 +571,7 @@ def results_page(): # 显示所有数据路由 @app.route('/all') +@admin_required def show_all(): """ 获取所有数据并渲染到页面 @@ -276,23 +602,134 @@ def show_all(): 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删除数据 + 根据文档ID删除数据(用户只能删除自己的数据,管理员可以删除所有数据) 参数: doc_id (str): 要删除的文档ID 返回: - 重定向到所有数据页面或错误信息 + 重定向到相应页面或错误信息 """ - if delete_by_id(doc_id): - return redirect(url_for('show_all')) + 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('/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')) + # 主程序入口 diff --git a/templates/base.html b/templates/base.html index 7983e7e..d92b5d3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -35,6 +35,9 @@ box-shadow: var(--shadow); position: relative; overflow: hidden; + display: flex; + justify-content: space-between; + align-items: center; } .header:before { @@ -62,6 +65,51 @@ text-shadow: 0 0 5px rgba(0,0,0,0.2); } + .user-info { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 15px; + } + + .user-info .username { + font-size: 14px; + opacity: 0.9; + } + + .user-info .permission-badge { + background: rgba(255, 255, 255, 0.2); + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + } + + .user-info .permission-badge.admin { + background: var(--accent); + } + + .user-info .permission-badge.user { + background: var(--success); + } + + .logout-btn { + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 6px 12px; + border-radius: 4px; + text-decoration: none; + font-size: 12px; + transition: var(--transition); + } + + .logout-btn:hover { + background: rgba(255, 255, 255, 0.3); + color: white; + } + .sidebar { width: 240px; height: calc(100vh - 60px); @@ -152,6 +200,15 @@

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

+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ + {% endif %} + {% endwith %} + {% block content %} {% endblock %}
diff --git a/templates/edit.html b/templates/edit.html new file mode 100644 index 0000000..b9c8c8b --- /dev/null +++ b/templates/edit.html @@ -0,0 +1,316 @@ +{% 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/index.html b/templates/index.html index 96c90ba..c44fe09 100644 --- a/templates/index.html +++ b/templates/index.html @@ -20,72 +20,255 @@ -
+ + {% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..c0cac5d --- /dev/null +++ b/templates/login.html @@ -0,0 +1,162 @@ + + + + + + 用户登录 - 成果录入系统 + + + + + + \ No newline at end of file diff --git a/templates/my_data.html b/templates/my_data.html new file mode 100644 index 0000000..c546f0a --- /dev/null +++ b/templates/my_data.html @@ -0,0 +1,486 @@ +{% 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 new file mode 100644 index 0000000..09adde0 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,538 @@ +{% 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 new file mode 100644 index 0000000..16ef347 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,440 @@ +{% 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 index e895619..9c81631 100644 --- a/templates/results.html +++ b/templates/results.html @@ -82,6 +82,7 @@ box-shadow: 0 2px 10px rgba(0,0,0,0.05); border-left: 4px solid #3498db; transition: transform 0.3s; + cursor: pointer; } .result-item:hover { @@ -89,14 +90,119 @@ box-shadow: 0 5px 15px rgba(0,0,0,0.1); } - .result-item p { - margin-bottom: 10px; - line-height: 1.6; + .result-preview { + margin-bottom: 15px; + } + + .result-preview .field-item { + display: inline-block; + margin-right: 20px; + margin-bottom: 8px; + padding: 5px 10px; + background: #f8f9fa; + border-radius: 4px; + border: 1px solid #e9ecef; + } + + .result-preview .field-label { + font-weight: bold; + color: #2c3e50; + margin-right: 5px; + } + + .result-preview .field-value { color: #34495e; } - .result-item strong { + .result-details { + display: none; + border-top: 1px solid #e9ecef; + padding-top: 15px; + margin-top: 15px; + } + + .result-details.expanded { + display: block; + } + + .result-details .field-item { + margin-bottom: 10px; + padding: 8px 12px; + background: #f8f9fa; + border-radius: 4px; + border-left: 3px solid #3498db; + } + + .result-details .field-label { + font-weight: bold; color: #2c3e50; + display: inline-block; + min-width: 120px; + } + + .result-details .field-value { + color: #34495e; + } + + .expand-indicator { + float: right; + color: #3498db; + font-size: 14px; + transition: all 0.3s; + } + + .result-item.expanded .expand-indicator { + color: #2c3e50; + } + + .image-container { + margin-top: 15px; + text-align: center; + } + + .result-image { + max-width: 100%; + max-height: 300px; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + cursor: pointer; + transition: transform 0.3s; + } + + .result-image:hover { + transform: scale(1.05); + } + + .image-modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.8); + cursor: pointer; + } + + .image-modal img { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 90%; + max-height: 90%; + border-radius: 8px; + } + + .close-modal { + position: absolute; + top: 20px; + right: 30px; + color: white; + font-size: 30px; + font-weight: bold; + cursor: pointer; } /* 加载状态 */ @@ -152,22 +258,47 @@ return; } - const html = realData.map(item => { + const html = realData.map((item, index) => { const source = item._source || {}; - const students = Array.isArray(source.students) - ? source.students.join(', ') - : (source.students || '无'); + const allFields = Object.entries(source).filter(([key, value]) => key !== 'image' && value); - const teacher = Array.isArray(source.teacher) - ? source.teacher.join(', ') - : (source.teacher || '无'); + // 获取前3个字段作为预览 + const previewFields = allFields.slice(0, 3); + const hasMoreFields = allFields.length > 3; + + // 生成预览字段HTML + const previewHtml = previewFields.map(([key, value]) => ` +
+ ${key}: + ${Array.isArray(value) ? value.join(', ') : value} +
+ `).join(''); + + // 生成详细字段HTML + const detailsHtml = allFields.map(([key, value]) => ` +
+ ${key}: + ${Array.isArray(value) ? value.join(', ') : value} +
+ `).join(''); + + // 图片HTML + const imageHtml = source.image ? ` +
+ 相关图片 +
+ ` : ''; return ` -
-

比赛/论文名称:${source.id || '无'}

-

项目名称:${source.name || '无'}

-

学生:${students}

-

指导老师:${teacher}

+
+
+ ${previewHtml} + ${hasMoreFields ? '▼ 点击查看更多' : ''} +
+
+ ${detailsHtml} + ${imageHtml} +
`; }).join(''); @@ -178,5 +309,54 @@ resultsContainer.innerHTML = '
搜索过程中发生错误
'; }); }); + + function toggleDetails(index) { + const resultItem = document.querySelector(`[data-index="${index}"]`); + const detailsDiv = document.getElementById(`details-${index}`); + + if (detailsDiv.classList.contains('expanded')) { + detailsDiv.classList.remove('expanded'); + resultItem.classList.remove('expanded'); + } else { + detailsDiv.classList.add('expanded'); + resultItem.classList.add('expanded'); + } + } + + function openImageModal(imageSrc) { + event.stopPropagation(); // 阻止事件冒泡 + + // 创建模态框 + const modal = document.createElement('div'); + modal.className = 'image-modal'; + modal.innerHTML = ` + × + 图片预览 + `; + + document.body.appendChild(modal); + modal.style.display = 'block'; + + // 点击模态框背景关闭 + modal.addEventListener('click', function(e) { + if (e.target === modal) { + closeImageModal(); + } + }); + } + + function closeImageModal() { + const modal = document.querySelector('.image-modal'); + if (modal) { + modal.remove(); + } + } + + // ESC键关闭模态框 + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + closeImageModal(); + } + }); {% endblock %} diff --git a/templates/user_management.html b/templates/user_management.html new file mode 100644 index 0000000..b6ea8e3 --- /dev/null +++ b/templates/user_management.html @@ -0,0 +1,356 @@ +{% 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