From d37d60b896ad691c46a07d35cfdb0775d1f5a9f9 Mon Sep 17 00:00:00 2001 From: Viajero-tect <2737079298@qq.com> Date: Thu, 13 Nov 2025 16:52:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E2=80=9C=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Achievement_Inputing/settings.py | 15 ++- Achievement_Inputing/urls.py | 5 + accounts/urls.py | 2 - db.sqlite3 | Bin 131072 -> 131072 bytes elastic/documents.py | 22 +---- elastic/es_connect.py | 8 +- elastic/urls.py | 7 +- elastic/views.py | 158 +++++++++++++++++++++++++++++++ main/templates/main/home.html | 28 ++++-- requirements.txt | 4 +- 10 files changed, 210 insertions(+), 39 deletions(-) diff --git a/Achievement_Inputing/settings.py b/Achievement_Inputing/settings.py index a9ce614..77b2626 100644 --- a/Achievement_Inputing/settings.py +++ b/Achievement_Inputing/settings.py @@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ """ from pathlib import Path +import os +from elastic.indexes import INDEX_NAME # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -120,6 +122,10 @@ USE_TZ = True STATIC_URL = 'static/' +# Media files (uploaded images) +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + # Security settings for cookies and headers (dev-friendly defaults) SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax' @@ -144,7 +150,10 @@ ELASTICSEARCH_DSL = { # Elasticsearch index settings ELASTICSEARCH_INDEX_NAMES = { - 'elastic.documents.AchievementDocument': 'wordsearch266666', - 'elastic.documents.UserDocument': 'users', - 'elastic.documents.NewsDocument': 'elastic_news', + 'elastic.documents.AchievementDocument': INDEX_NAME, + 'elastic.documents.UserDocument': INDEX_NAME, } + +# AI Studio/OpenAI client settings +AISTUDIO_API_KEY = os.environ.get('AISTUDIO_API_KEY', '') +OPENAI_BASE_URL = os.environ.get('OPENAI_BASE_URL', 'https://aistudio.baidu.com/llm/lmapi/v3') diff --git a/Achievement_Inputing/urls.py b/Achievement_Inputing/urls.py index 4d400e9..f500b7a 100644 --- a/Achievement_Inputing/urls.py +++ b/Achievement_Inputing/urls.py @@ -16,6 +16,8 @@ Including another URLconf """ from django.contrib import admin from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static from main.views import home as main_home urlpatterns = [ @@ -25,3 +27,6 @@ urlpatterns = [ path('elastic/', include('elastic.urls', namespace='elastic')), path('', main_home, name='root_home'), ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/accounts/urls.py b/accounts/urls.py index 3d0b481..769cbda 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,7 +1,5 @@ from django.urls import path from . import views - - app_name = "accounts" urlpatterns = [ diff --git a/db.sqlite3 b/db.sqlite3 index 0f0d574ce136c3515770405b7bfbee2d5d314243..664e4fe6b582ea17faea02ff9020845a5ea00000 100644 GIT binary patch delta 762 zcmaiy&yLby6vazsNRX(Fi907|Ox!}+kG5aS!bli~X=xe$6$dtzwov*PN?XcT7r>af zGwK`o2xBIh2kQ%B&CsFAeD}Q2$u|aQC2GSD4oqJ#ay#+lZIVO0RdrfYWTl;&H z8zO~{x7$|U dV)!lobL-&-fAjL7l=`pf`(I@rJl`{Ae*@*1^KJkD delta 753 zcmaiyOK#dw6oySmB-BA@J8c!EQl>$^*T&ckqFSVYT@xPW5tvXvvAM>8ZERml6_7y( zowVISrEVZqrDOqJLl@AU)F|pK5wr8suk)SrZ#KEjCinGKhWYloa>NjY&o6)K-@oLD z=LC_Lj{85}oF1>YQTC{D^O1K9d!d#>NX^(~1VT!;IDe*xD4sLOjIdrXMC|qLdC;}t z`-F@;{#5GmYAbo?UUeM>Z{=<0AXTDx4~9j?^kFhZidke_10uzkf!NaLVPXZP2PNb+ zrAnHw(}Fsy1|wNugjp4-17{`Jk*(E5Cyf&gI-zO{OruWz z$3kkoq3co8i8*0{C$&(oH9d}ZSW)Vis#c8+myCN!b7nw4PQ`Kz{Vc`W`QR{rpTGb0 z;=`xBdzNiv_PHRt8`}5agH-zL$#dak!THd-xxKs19KL-pOV diff --git a/elastic/documents.py b/elastic/documents.py index 17fb0b4..cff614a 100644 --- a/elastic/documents.py +++ b/elastic/documents.py @@ -1,17 +1,13 @@ from django_elasticsearch_dsl import Document, fields, Index from .models import AchievementData, User, ElasticNews +from .indexes import INDEX_NAME -# 获奖数据索引 - 对应Flask项目中的wordsearch266666 -ACHIEVEMENT_INDEX = Index('wordsearch266666') +ACHIEVEMENT_INDEX = Index(INDEX_NAME) ACHIEVEMENT_INDEX.settings(number_of_shards=1, number_of_replicas=0) -# 用户数据索引 - 对应Flask项目中的users -USER_INDEX = Index('users') -USER_INDEX.settings(number_of_shards=1, number_of_replicas=0) +USER_INDEX = ACHIEVEMENT_INDEX + -# 新闻索引 - 保留原有功能 -NEWS_INDEX = Index('elastic_news') -NEWS_INDEX.settings(number_of_shards=1, number_of_replicas=0) @ACHIEVEMENT_INDEX.doc_type class AchievementDocument(Document): @@ -41,13 +37,3 @@ class UserDocument(Document): model = User # fields列表应该只包含需要特殊处理的字段,或者可以完全省略 # 因为我们已经显式定义了所有字段 - -@NEWS_INDEX.doc_type -class NewsDocument(Document): - """新闻文档映射 - 保留原有功能""" - id = fields.IntegerField(attr='id') - title = fields.TextField(fields={'keyword': {'type': 'keyword'}}) - content = fields.TextField(fields={'keyword': {'type': 'keyword'}}) - - class Django: - model = ElasticNews \ No newline at end of file diff --git a/elastic/es_connect.py b/elastic/es_connect.py index a25fde8..d23b6a4 100644 --- a/elastic/es_connect.py +++ b/elastic/es_connect.py @@ -5,6 +5,7 @@ Django版本的ES连接和操作模块 from elasticsearch import Elasticsearch from elasticsearch_dsl import connections from .documents import AchievementDocument, UserDocument +from .indexes import ACHIEVEMENT_INDEX_NAME, USER_INDEX_NAME import hashlib import time @@ -14,9 +15,8 @@ connections.create_connection(hosts=['localhost:9200']) # 获取默认的ES客户端 es = connections.get_connection() -# 索引名称(与Flask项目保持一致) -DATA_INDEX_NAME = "wordsearch_sb" -USERS_INDEX_NAME = "users" +DATA_INDEX_NAME = ACHIEVEMENT_INDEX_NAME +USERS_INDEX_NAME = USER_INDEX_NAME def create_index_with_mapping(): """创建索引和映射配置""" @@ -360,4 +360,4 @@ def update_user_permission(username, new_permission): return False except Exception as e: print(f"更新用户权限失败: {str(e)}") - return False \ No newline at end of file + return False diff --git a/elastic/urls.py b/elastic/urls.py index 466086e..e34bf39 100644 --- a/elastic/urls.py +++ b/elastic/urls.py @@ -23,4 +23,9 @@ urlpatterns = [ path('users/add/', views.add_user, name='add_user'), path('users//delete/', views.delete_user, name='delete_user'), path('users//update/', views.update_user, name='update_user'), -] \ No newline at end of file + + # 图片上传与确认 + path('upload-page/', views.upload_page, name='upload_page'), + path('upload/', views.upload, name='upload'), + path('confirm/', views.confirm, name='confirm'), +] diff --git a/elastic/views.py b/elastic/views.py index de99233..141a2b8 100644 --- a/elastic/views.py +++ b/elastic/views.py @@ -1,8 +1,14 @@ """ ES相关的API视图 """ +import os +import re +import uuid +import base64 import json +from django.conf import settings from django.http import JsonResponse +from django.shortcuts import render from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import csrf_exempt from .es_connect import ( @@ -19,6 +25,7 @@ from .es_connect import ( delete_user_by_username, update_user_permission ) +from openai import OpenAI @require_http_methods(["GET", "POST"]) @@ -180,3 +187,154 @@ def update_user(request, username): return JsonResponse({"status": "error", "message": "用户权限更新失败"}, status=500) except Exception as e: return JsonResponse({"status": "error", "message": str(e)}, status=500) + + +# 辅助:JSON 转换(兼容 a.py 行为) +def json_to_string(obj): + try: + return json.dumps(obj, ensure_ascii=False) + except Exception: + return str(obj) + + +def string_to_json(s): + try: + return json.loads(s) + except Exception: + return {} + + +# 移植自 a.py 的核心:调用大模型进行 OCR/信息抽取 +def ocr_and_extract_info(image_path: str): + def encode_image(path: str) -> str: + with open(path, "rb") as f: + return base64.b64encode(f.read()).decode("utf-8") + + base64_image = encode_image(image_path) + + api_key = getattr(settings, "AISTUDIO_API_KEY", "") + base_url = getattr(settings, "OPENAI_BASE_URL", "https://aistudio.baidu.com/llm/lmapi/v3") + if not api_key: + raise RuntimeError("缺少 AISTUDIO_API_KEY,请在环境变量或 settings 中配置") + + client = OpenAI(api_key=api_key, base_url=base_url) + + 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", + ) + + response_text = chat_completion.choices[0].message.content + + def parse_response(text: str): + try: + result = json.loads(text) + if result: + return result + except json.JSONDecodeError: + pass + + m = re.search(r"```json\n(.*?)```", text, re.DOTALL) + if m: + try: + result = json.loads(m.group(1)) + if result: + return result + except json.JSONDecodeError: + pass + + try: + fixed = text.replace("'", '"') + result = json.loads(fixed) + if result: + return result + except json.JSONDecodeError: + pass + + return None + + return parse_response(response_text) + + +# 上传页面 +@require_http_methods(["GET"]) +def upload_page(request): + # if not request.session.get("user_id"): + # from django.shortcuts import redirect + # return redirect("/accounts/login/") + return render(request, "elastic/upload.html") + + +# 上传并识别(不入库) +@require_http_methods(["POST"]) +def upload(request): + if not request.session.get("user_id"): + return JsonResponse({"status": "error", "message": "未登录"}, status=401) + + file = request.FILES.get("file") + if not file: + return JsonResponse({"status": "error", "message": "未选择文件"}, status=400) + + images_dir = os.path.join(settings.MEDIA_ROOT, "images") + os.makedirs(images_dir, exist_ok=True) + filename = f"{uuid.uuid4()}_{file.name}" + abs_path = os.path.join(images_dir, filename) + + with open(abs_path, "wb") as dst: + for chunk in file.chunks(): + dst.write(chunk) + + try: + data = ocr_and_extract_info(abs_path) + if not data: + return JsonResponse({"status": "error", "message": "无法识别图片内容"}, status=400) + + rel_path = f"images/{filename}" + image_url = request.build_absolute_uri(settings.MEDIA_URL + rel_path) + return JsonResponse({ + "status": "success", + "message": "识别成功,请确认数据后点击录入", + "data": data, + "image": rel_path, + "image_url": image_url, + }) + except Exception as e: + return JsonResponse({"status": "error", "message": str(e)}, status=500) + + +# 确认并入库 +@require_http_methods(["POST"]) +def confirm(request): + if not request.session.get("user_id"): + return JsonResponse({"status": "error", "message": "未登录"}, status=401) + + try: + payload = json.loads(request.body.decode("utf-8")) + except json.JSONDecodeError: + return JsonResponse({"status": "error", "message": "JSON无效"}, status=400) + + edited = payload.get("data") or {} + image_rel = payload.get("image") or "" + if not isinstance(edited, dict) or not edited: + return JsonResponse({"status": "error", "message": "数据不能为空"}, status=400) + + to_store = { + "writer_id": str(request.session.get("user_id")), + "data": json_to_string(edited), + "image": image_rel, + } + + ok = insert_data(to_store) + if not ok: + return JsonResponse({"status": "error", "message": "写入ES失败"}, status=500) + + return JsonResponse({"status": "success", "message": "数据录入成功", "data": edited}) diff --git a/main/templates/main/home.html b/main/templates/main/home.html index 6de7308..146ba8f 100644 --- a/main/templates/main/home.html +++ b/main/templates/main/home.html @@ -12,19 +12,27 @@ -
-
+
+ + +

主页(留白)

用户ID:{{ user_id }}

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

+

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

-

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

- -

- - -

- +
- \ No newline at end of file + diff --git a/requirements.txt b/requirements.txt index ba7b35b..7aa9ef5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ Django==5.2.8 elasticsearch==8.17.1 django-elasticsearch-dsl==7.3.0 -requests==2.32.3 \ No newline at end of file +requests==2.32.3 +openai==1.52.2 +Pillow==10.4.0