新增“数据编辑”

This commit is contained in:
2025-11-13 16:52:23 +08:00
parent 1bbd777565
commit d37d60b896
10 changed files with 210 additions and 39 deletions

View File

@@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
""" """
from pathlib import Path from pathlib import Path
import os
from elastic.indexes import INDEX_NAME
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@@ -120,6 +122,10 @@ USE_TZ = True
STATIC_URL = 'static/' STATIC_URL = 'static/'
# Media files (uploaded images)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Security settings for cookies and headers (dev-friendly defaults) # Security settings for cookies and headers (dev-friendly defaults)
SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax' SESSION_COOKIE_SAMESITE = 'Lax'
@@ -144,7 +150,10 @@ ELASTICSEARCH_DSL = {
# Elasticsearch index settings # Elasticsearch index settings
ELASTICSEARCH_INDEX_NAMES = { ELASTICSEARCH_INDEX_NAMES = {
'elastic.documents.AchievementDocument': 'wordsearch266666', 'elastic.documents.AchievementDocument': INDEX_NAME,
'elastic.documents.UserDocument': 'users', 'elastic.documents.UserDocument': INDEX_NAME,
'elastic.documents.NewsDocument': 'elastic_news',
} }
# 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')

View File

@@ -16,6 +16,8 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include 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 from main.views import home as main_home
urlpatterns = [ urlpatterns = [
@@ -25,3 +27,6 @@ urlpatterns = [
path('elastic/', include('elastic.urls', namespace='elastic')), path('elastic/', include('elastic.urls', namespace='elastic')),
path('', main_home, name='root_home'), path('', main_home, name='root_home'),
] ]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,7 +1,5 @@
from django.urls import path from django.urls import path
from . import views from . import views
app_name = "accounts" app_name = "accounts"
urlpatterns = [ urlpatterns = [

Binary file not shown.

View File

@@ -1,17 +1,13 @@
from django_elasticsearch_dsl import Document, fields, Index from django_elasticsearch_dsl import Document, fields, Index
from .models import AchievementData, User, ElasticNews from .models import AchievementData, User, ElasticNews
from .indexes import INDEX_NAME
# 获奖数据索引 - 对应Flask项目中的wordsearch266666 ACHIEVEMENT_INDEX = Index(INDEX_NAME)
ACHIEVEMENT_INDEX = Index('wordsearch266666')
ACHIEVEMENT_INDEX.settings(number_of_shards=1, number_of_replicas=0) ACHIEVEMENT_INDEX.settings(number_of_shards=1, number_of_replicas=0)
# 用户数据索引 - 对应Flask项目中的users USER_INDEX = ACHIEVEMENT_INDEX
USER_INDEX = Index('users')
USER_INDEX.settings(number_of_shards=1, number_of_replicas=0)
# 新闻索引 - 保留原有功能
NEWS_INDEX = Index('elastic_news')
NEWS_INDEX.settings(number_of_shards=1, number_of_replicas=0)
@ACHIEVEMENT_INDEX.doc_type @ACHIEVEMENT_INDEX.doc_type
class AchievementDocument(Document): class AchievementDocument(Document):
@@ -41,13 +37,3 @@ class UserDocument(Document):
model = User model = User
# fields列表应该只包含需要特殊处理的字段或者可以完全省略 # 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

View File

@@ -5,6 +5,7 @@ Django版本的ES连接和操作模块
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
from elasticsearch_dsl import connections from elasticsearch_dsl import connections
from .documents import AchievementDocument, UserDocument from .documents import AchievementDocument, UserDocument
from .indexes import ACHIEVEMENT_INDEX_NAME, USER_INDEX_NAME
import hashlib import hashlib
import time import time
@@ -14,9 +15,8 @@ connections.create_connection(hosts=['localhost:9200'])
# 获取默认的ES客户端 # 获取默认的ES客户端
es = connections.get_connection() es = connections.get_connection()
# 索引名称与Flask项目保持一致 DATA_INDEX_NAME = ACHIEVEMENT_INDEX_NAME
DATA_INDEX_NAME = "wordsearch_sb" USERS_INDEX_NAME = USER_INDEX_NAME
USERS_INDEX_NAME = "users"
def create_index_with_mapping(): def create_index_with_mapping():
"""创建索引和映射配置""" """创建索引和映射配置"""
@@ -360,4 +360,4 @@ def update_user_permission(username, new_permission):
return False return False
except Exception as e: except Exception as e:
print(f"更新用户权限失败: {str(e)}") print(f"更新用户权限失败: {str(e)}")
return False return False

View File

@@ -23,4 +23,9 @@ urlpatterns = [
path('users/add/', views.add_user, name='add_user'), path('users/add/', views.add_user, name='add_user'),
path('users/<str:username>/delete/', views.delete_user, name='delete_user'), path('users/<str:username>/delete/', views.delete_user, name='delete_user'),
path('users/<str:username>/update/', views.update_user, name='update_user'), path('users/<str:username>/update/', views.update_user, name='update_user'),
]
# 图片上传与确认
path('upload-page/', views.upload_page, name='upload_page'),
path('upload/', views.upload, name='upload'),
path('confirm/', views.confirm, name='confirm'),
]

View File

@@ -1,8 +1,14 @@
""" """
ES相关的API视图 ES相关的API视图
""" """
import os
import re
import uuid
import base64
import json import json
from django.conf import settings
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from .es_connect import ( from .es_connect import (
@@ -19,6 +25,7 @@ from .es_connect import (
delete_user_by_username, delete_user_by_username,
update_user_permission update_user_permission
) )
from openai import OpenAI
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
@@ -180,3 +187,154 @@ def update_user(request, username):
return JsonResponse({"status": "error", "message": "用户权限更新失败"}, status=500) return JsonResponse({"status": "error", "message": "用户权限更新失败"}, status=500)
except Exception as e: except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500) 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})

View File

@@ -12,19 +12,27 @@
<!-- CSRF token to assist logout POST via cookie/header --> <!-- CSRF token to assist logout POST via cookie/header -->
</head> </head>
<body> <body>
<div class="container"> <div class="container" style="display:flex; gap:16px;">
<div class="card"> <aside style="width:220px; background:#fff; border-radius:10px; box-shadow: 0 6px 18px rgba(0,0,0,0.06); padding:16px; height: fit-content;">
<h3 style="margin-top:0; font-size:16px;">导航</h3>
<nav>
<ul style="list-style:none; padding-left:0; line-height:1.9;">
<li><a href="/" style="text-decoration:none; color:#1677ff;">主页</a></li>
<li><a href="/elastic/upload-page/" style="text-decoration:none; color:#1677ff;">图片上传与识别</a></li>
</ul>
</nav>
<hr/>
<button id="logoutBtn" style="padding:8px 12px; width:100%; background:#ff4d4f; color:#fff; border:none; border-radius:6px; cursor:pointer;">退出登录</button>
<div id="logoutMsg" class="muted" style="margin-top:8px;"></div>
</aside>
<div class="card" style="flex:1;">
<h2>主页(留白)</h2> <h2>主页(留白)</h2>
<p>用户ID{{ user_id }}</p> <p>用户ID{{ user_id }}</p>
<p>这里留白即可,主页不由当前实现负责。</p> <p>这里留白即可,主页不由当前实现负责。</p>
<p class="muted">提示:已使用安全的会话 Cookie 管理登录状态。</p>
</div> </div>
<p style="margin-top: 16px; color: #666;">提示:已使用安全的会话 Cookie 管理登录状态。</p>
<p>
<button id="logoutBtn" style="padding:8px 12px; background:#ff4d4f; color:#fff; border:none; border-radius:6px; cursor:pointer;">退出登录清除Cookie</button>
<span id="logoutMsg" style="margin-left:8px; color:#666;"></span>
</p>
<script> <script>
function getCookie(name) { function getCookie(name) {
const value = `; ${document.cookie}`; const value = `; ${document.cookie}`;
@@ -57,7 +65,7 @@
} }
}); });
</script> </script>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,4 +1,6 @@
Django==5.2.8 Django==5.2.8
elasticsearch==8.17.1 elasticsearch==8.17.1
django-elasticsearch-dsl==7.3.0 django-elasticsearch-dsl==7.3.0
requests==2.32.3 requests==2.32.3
openai==1.52.2
Pillow==10.4.0