12 Commits

Author SHA1 Message Date
DSQ
0dd7879389 [0.2.8.12][ci]
All checks were successful
CI / docker-ci (push) Successful in 30s
2026-03-23 12:42:27 +08:00
DSQ
19f805c818 Merge remote-tracking branch 'origin/Django' into Django 2026-03-23 12:38:33 +08:00
DSQ
d84d0218cd [0.2.8.11][ci] 2026-03-23 12:38:14 +08:00
e92964ce71 [0.2.8.10][ci]
All checks were successful
CI / docker-ci (push) Successful in 24s
2026-03-23 11:53:10 +08:00
1a3aee39e0 [0.2.8.10]
All checks were successful
CI / docker-ci (push) Has been skipped
2026-03-23 11:52:41 +08:00
DSQ
7fa7b42b1a Merge remote-tracking branch 'origin/Django' into Django
All checks were successful
CI / docker-ci (push) Has been skipped
2026-03-23 11:51:34 +08:00
DSQ
26452161f8 [0.2.8.0][ci] 2026-03-23 11:51:02 +08:00
07d3a4420c 生成镜像[0.2.7.9][ci]
All checks were successful
CI / docker-ci (push) Successful in 25s
2026-03-23 11:28:14 +08:00
2c3c2d6acf Merge branch 'Django' of gitea.spdis.space:Viajero/Achievement_Inputing into Django
All checks were successful
CI / docker-ci (push) Has been skipped
2026-03-23 11:06:59 +08:00
afc663844b 修复主页的类型分析的500问题[0.2.7.9][ci] 2026-03-23 11:06:45 +08:00
DSQ
9e3fe7150b [0.2.7.8][ci]
All checks were successful
CI / docker-ci (push) Successful in 26s
2026-03-23 11:02:00 +08:00
DSQ
c9611fa622 [0.2.7.7][ci]
All checks were successful
CI / docker-ci (push) Successful in 35s
2026-03-23 10:37:46 +08:00
7 changed files with 409 additions and 249 deletions

View File

@@ -48,7 +48,7 @@
<!-- 侧边栏 --> <!-- 侧边栏 -->
<div class="sidebar"> <div class="sidebar">
<div class="user-id-sidebar"> <div class="user-id-sidebar">
<h3>你好,{{ username|default:"访客" }}</h3> <h3>你好,<span id="sidebarUsername">{{ username|default:"访客" }}</span></h3>
</div> </div>
<div class="navigation-links"> <div class="navigation-links">
<a href="{% url 'main:home' %}">返回主页</a> <a href="{% url 'main:home' %}">返回主页</a>
@@ -58,6 +58,62 @@
</div> </div>
<div class="main-content"> <div class="main-content">
{% if subpage %}
<div class="profile-card">
<div class="profile-header">
<div class="profile-info">
<h2>{{ subpage_title }}</h2>
</div>
</div>
<div style="margin-bottom: 12px;">
<a href="{% url 'accounts:profile' %}" style="color:#2d8cf0; text-decoration:none;">返回个人中心</a>
</div>
{% if subpage == "username" %}
<form id="nameForm">
<div class="form-group">
<label for="newUsername">新用户名</label>
<input type="text" id="newUsername" placeholder="请输入新用户名" required>
</div>
<button type="submit" class="btn">保存</button>
<div id="nameMsg" class="msg"></div>
</form>
{% endif %}
{% if subpage == "password" %}
{% if permission_name != "管理员" and not profile_user.manage_key %}
<form id="pwdForm">
<div class="form-group">
<label for="newPassword">新密码</label>
<input type="password" id="newPassword" autocomplete="new-password" required>
</div>
<div class="form-group">
<label for="confirmPassword">确认密码</label>
<input type="password" id="confirmPassword" autocomplete="new-password" required>
</div>
<button type="submit" class="btn">保存</button>
<div id="pwdMsg" class="msg"></div>
</form>
{% else %}
<div class="msg error">无权限</div>
{% endif %}
{% endif %}
{% if subpage == "registration-code" %}
<form id="rcForm">
<div class="form-group">
<label for="newRegCode">新注册码</label>
<input type="text" id="newRegCode" placeholder="输入新注册码后替换原有 key" required>
</div>
<div class="form-group">
<label>预览</label>
<div id="rcPreview" style="background:#f8fafc; border:1px solid #e5e7eb; border-radius:10px; padding:10px 12px; font-size:13px; color:#334155;">
<div style="color:#64748b;">输入注册码后自动显示 key 预览</div>
</div>
</div>
<button type="submit" class="btn">替换</button>
<div id="rcMsg" class="msg"></div>
</form>
{% endif %}
</div>
{% else %}
<div class="profile-card"> <div class="profile-card">
<div class="profile-header"> <div class="profile-header">
<div class="profile-info"> <div class="profile-info">
@@ -65,7 +121,7 @@
</div> </div>
</div> </div>
<div class="profile-details"> <div class="profile-details">
<p><span class="label">用户名:</span> {{ profile_user.username }}</p> <p><span class="label">用户名:</span> <span id="profileUsername">{{ profile_user.username }}</span></p>
<p><span class="label">用户ID:</span> {{ profile_user.user_id }}</p> <p><span class="label">用户ID:</span> {{ profile_user.user_id }}</p>
<p><span class="label">注册码:</span> {{ profile_user.registration_code|default:"无" }}</p> <p><span class="label">注册码:</span> {{ profile_user.registration_code|default:"无" }}</p>
<p><span class="label">所属:</span> {{ profile_user.key|join:"、"|default:"未填写" }}</p> <p><span class="label">所属:</span> {{ profile_user.key|join:"、"|default:"未填写" }}</p>
@@ -74,6 +130,21 @@
</div> </div>
</div> </div>
<div class="profile-card">
<div class="profile-header">
<div class="profile-info">
<h2>账号设置</h2>
</div>
</div>
<div style="display:flex; gap:12px; flex-wrap:wrap;">
<a class="btn" href="{% url 'accounts:profile_username' %}">修改用户名</a>
{% if permission_name != "管理员" and not profile_user.manage_key %}
<a class="btn" href="{% url 'accounts:profile_password' %}">修改密码</a>
{% endif %}
<a class="btn" href="{% url 'accounts:profile_registration_code' %}">替换注册码</a>
</div>
</div>
<div class="section-title">我的提交</div> <div class="section-title">我的提交</div>
{% if achievements %} {% if achievements %}
<div class="image-grid"> <div class="image-grid">
@@ -96,49 +167,6 @@
<a href="{% url 'elastic:upload_page' %}" style="color: #2d8cf0; text-decoration: none;">去上传第一张图片吧!</a> <a href="{% url 'elastic:upload_page' %}" style="color: #2d8cf0; text-decoration: none;">去上传第一张图片吧!</a>
</div> </div>
{% endif %} {% endif %}
<div class="profile-card rc-card">
<div class="profile-header">
<div class="profile-info">
<h2>替换注册码</h2>
</div>
</div>
<form id="rcForm">
<div class="form-group">
<label for="newRegCode">新注册码</label>
<input type="text" id="newRegCode" placeholder="输入新注册码后替换原有 key" required>
</div>
<div class="form-group">
<label>预览</label>
<div id="rcPreview" style="background:#f8fafc; border:1px solid #e5e7eb; border-radius:10px; padding:10px 12px; font-size:13px; color:#334155;">
<div style="color:#64748b;">输入注册码后自动显示 key 预览</div>
</div>
</div>
<button type="submit" class="btn">替换</button>
<div id="rcMsg" class="msg"></div>
</form>
</div>
{% if permission_name != "管理员" and not profile_user.manage_key %}
<div class="profile-card">
<div class="profile-header">
<div class="profile-info">
<h2>修改密码</h2>
</div>
</div>
<form id="pwdForm">
<div class="form-group">
<label for="newPassword">新密码</label>
<input type="password" id="newPassword" autocomplete="new-password" required>
</div>
<div class="form-group">
<label for="confirmPassword">确认密码</label>
<input type="password" id="confirmPassword" autocomplete="new-password" required>
</div>
<button type="submit" class="btn">保存</button>
<div id="pwdMsg" class="msg"></div>
</form>
</div>
{% endif %} {% endif %}
</div> </div>
@@ -230,6 +258,62 @@
}); });
} }
const nameForm = document.getElementById('nameForm');
if (nameForm) {
nameForm.addEventListener('submit', async (e) => {
e.preventDefault();
const msg = document.getElementById('nameMsg');
msg.textContent = '';
msg.className = 'msg';
const input = document.getElementById('newUsername');
const newName = (input.value || '').trim();
const currentNameEl = document.getElementById('profileUsername');
const currentName = (currentNameEl && currentNameEl.textContent ? currentNameEl.textContent : '').trim();
if (!newName) {
msg.textContent = '请输入用户名';
msg.className = 'msg error';
return;
}
if (newName.length > 50) {
msg.textContent = '用户名过长';
msg.className = 'msg error';
return;
}
if (currentName && newName === currentName) {
msg.textContent = '用户名未变化';
msg.className = 'msg error';
return;
}
try {
const csrftoken = getCookie('csrftoken');
const resp = await fetch('/accounts/profile/username/update/', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken || ''
},
body: JSON.stringify({ username: newName })
});
const data = await resp.json();
if (resp.ok && data.ok) {
msg.textContent = '修改成功';
msg.className = 'msg success';
if (currentNameEl) currentNameEl.textContent = data.username || newName;
const sidebarName = document.getElementById('sidebarUsername');
if (sidebarName) sidebarName.textContent = data.username || newName;
input.value = '';
} else {
msg.textContent = (data && data.message) ? data.message : '操作失败';
msg.className = 'msg error';
}
} catch (err) {
msg.textContent = '操作失败';
msg.className = 'msg error';
}
});
}
const rcForm = document.getElementById('rcForm'); const rcForm = document.getElementById('rcForm');
if (rcForm) { if (rcForm) {
let rcPreviewTimer = null; let rcPreviewTimer = null;

View File

@@ -13,6 +13,10 @@ urlpatterns = [
path("register/submit/", views.register_submit, name="register_submit"), path("register/submit/", views.register_submit, name="register_submit"),
path("email/send-code/", views.send_email_code, name="send_email_code"), path("email/send-code/", views.send_email_code, name="send_email_code"),
path("profile/", views.profile_page, name="profile"), path("profile/", views.profile_page, name="profile"),
path("profile/username/", views.profile_username_page, name="profile_username"),
path("profile/password/", views.profile_password_page, name="profile_password"),
path("profile/registration-code/", views.profile_registration_code_page, name="profile_registration_code"),
path("profile/username/update/", views.update_profile_username_view, name="update_profile_username"),
path("profile/registration-code/replace/", views.replace_registration_code_view, name="replace_registration_code"), path("profile/registration-code/replace/", views.replace_registration_code_view, name="replace_registration_code"),
path("profile/registration-code/preview/", views.registration_code_preview_view, name="registration_code_preview"), path("profile/registration-code/preview/", views.registration_code_preview_view, name="registration_code_preview"),
path("registration-code/request/submit/", views.submit_registration_code_request_view, name="submit_registration_code_request"), path("registration-code/request/submit/", views.submit_registration_code_request_view, name="submit_registration_code_request"),

View File

@@ -71,33 +71,62 @@ def set_session_key(request):
request.session["session_enc_key_b64"] = base64.b64encode(key_bytes).decode("ascii") request.session["session_enc_key_b64"] = base64.b64encode(key_bytes).decode("ascii")
return JsonResponse({"ok": True}) return JsonResponse({"ok": True})
@require_http_methods(["GET"]) def _build_profile_context(request):
@ensure_csrf_cookie
def profile_page(request):
session_user_id = request.session.get("user_id") session_user_id = request.session.get("user_id")
if session_user_id is None: if session_user_id is None:
return redirect("/accounts/login/") return None
# 获取用户信息
user = get_user_by_id(session_user_id) user = get_user_by_id(session_user_id)
if not user: if not user:
return redirect("/accounts/login/") return None
# 获取个人提交的成就(图片)
from elastic.es_connect import search_all from elastic.es_connect import search_all
from elastic.views import _attach_image_urls from elastic.views import _attach_image_urls
raw_results = [r for r in search_all() if str(r.get("writer_id", "")) == str(session_user_id)] raw_results = [r for r in search_all() if str(r.get("writer_id", "")) == str(session_user_id)]
achievements = _attach_image_urls(request, raw_results) achievements = _attach_image_urls(request, raw_results)
permission_name = "管理员" if int(user.get("permission", 1)) == 0 else "普通用户" permission_name = "管理员" if int(user.get("permission", 1)) == 0 else "普通用户"
return {
context = {
"username": request.session.get("username"), "username": request.session.get("username"),
"profile_user": user, "profile_user": user,
"permission_name": permission_name, "permission_name": permission_name,
"achievements": achievements, "achievements": achievements,
} }
@require_http_methods(["GET"])
@ensure_csrf_cookie
def profile_page(request):
context = _build_profile_context(request)
if context is None:
return redirect("/accounts/login/")
context["subpage"] = ""
return render(request, "accounts/profile.html", context)
@require_http_methods(["GET"])
@ensure_csrf_cookie
def profile_username_page(request):
context = _build_profile_context(request)
if context is None:
return redirect("/accounts/login/")
context["subpage"] = "username"
context["subpage_title"] = "修改用户名"
return render(request, "accounts/profile.html", context)
@require_http_methods(["GET"])
@ensure_csrf_cookie
def profile_password_page(request):
context = _build_profile_context(request)
if context is None:
return redirect("/accounts/login/")
context["subpage"] = "password"
context["subpage_title"] = "修改密码"
return render(request, "accounts/profile.html", context)
@require_http_methods(["GET"])
@ensure_csrf_cookie
def profile_registration_code_page(request):
context = _build_profile_context(request)
if context is None:
return redirect("/accounts/login/")
context["subpage"] = "registration-code"
context["subpage_title"] = "替换注册码"
return render(request, "accounts/profile.html", context) return render(request, "accounts/profile.html", context)
@require_http_methods(["POST"]) @require_http_methods(["POST"])
@@ -304,6 +333,34 @@ def replace_registration_code_view(request):
return JsonResponse({"ok": False, "message": "替换失败"}, status=500) return JsonResponse({"ok": False, "message": "替换失败"}, status=500)
return JsonResponse({"ok": True}) return JsonResponse({"ok": True})
@require_http_methods(["POST"])
@csrf_protect
def update_profile_username_view(request):
session_user_id = request.session.get("user_id")
if session_user_id is None:
return JsonResponse({"ok": False, "message": "未登录"}, status=401)
try:
payload = json.loads(request.body.decode("utf-8"))
except json.JSONDecodeError:
return JsonResponse({"ok": False, "message": "JSON无效"}, status=400)
new_username = (payload.get("username") or "").strip()
if not new_username:
return JsonResponse({"ok": False, "message": "请输入用户名"}, status=400)
if len(new_username) > 50:
return JsonResponse({"ok": False, "message": "用户名过长"}, status=400)
me = get_user_by_id(session_user_id) or {}
if str(me.get("username", "")).strip() == new_username:
request.session["username"] = new_username
return JsonResponse({"ok": True, "username": new_username})
existing = es_get_user_by_username(new_username)
if existing and str(existing.get("user_id")) != str(session_user_id):
return JsonResponse({"ok": False, "message": "用户名已存在"}, status=409)
ok = update_user_by_id(session_user_id, username=new_username)
if not ok:
return JsonResponse({"ok": False, "message": "修改失败"}, status=500)
request.session["username"] = new_username
return JsonResponse({"ok": True, "username": new_username})
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def registration_code_preview_view(request): def registration_code_preview_view(request):
session_user_id = request.session.get("user_id") session_user_id = request.session.get("user_id")

View File

@@ -831,12 +831,8 @@ def get_user_by_username(username):
def get_all_users(): def get_all_users():
"""获取所有用户""" """获取所有用户"""
try: try:
search = UserDocument.search()
search = search.query("match_all")
response = search.execute()
users = [] users = []
for hit in response: for hit in UserDocument.search().query("match_all").scan():
users.append({ users.append({
"user_id": hit.user_id, "user_id": hit.user_id,
"username": hit.username, "username": hit.username,
@@ -848,7 +844,6 @@ def get_all_users():
"key": list(getattr(hit, 'key', []) or []), "key": list(getattr(hit, 'key', []) or []),
"manage_key": list(getattr(hit, 'manage_key', []) or []), "manage_key": list(getattr(hit, 'manage_key', []) or []),
}) })
return users return users
except Exception as e: except Exception as e:
print(f"获取所有用户失败: {str(e)}") print(f"获取所有用户失败: {str(e)}")

View File

@@ -445,6 +445,11 @@ async function generateReport() {
try { try {
const params = buildReportParams(); const params = buildReportParams();
const resp = await fetch(`/elastic/report/?${params.toString()}`, { credentials: 'same-origin' }); const resp = await fetch(`/elastic/report/?${params.toString()}`, { credentials: 'same-origin' });
const ct = (resp.headers.get('content-type') || '').toLowerCase();
if (!ct.includes('application/json')) {
const text = await resp.text();
throw new Error(text ? String(text).slice(0, 200) : `HTTP ${resp.status}`);
}
const data = await resp.json(); const data = await resp.json();
if (data.status !== 'success') { if (data.status !== 'success') {
reportBox.className = 'search-result error'; reportBox.className = 'search-result error';

View File

@@ -494,6 +494,11 @@ uploadForm.addEventListener('submit', async (e) => {
body: formData, body: formData,
}); });
clearInterval(timer); clearInterval(timer);
const ct = (resp.headers.get('content-type') || '').toLowerCase();
if (!ct.includes('application/json')) {
const text = await resp.text();
throw new Error(text ? String(text).slice(0, 200) : `HTTP ${resp.status}`);
}
const data = await resp.json(); const data = await resp.json();
if (!resp.ok || data.status !== 'success') { if (!resp.ok || data.status !== 'success') {
throw new Error(data.message || '上传识别失败'); throw new Error(data.message || '上传识别失败');

View File

@@ -8,6 +8,7 @@ import base64
import json import json
import csv import csv
import io import io
from datetime import datetime, timezone, timedelta
import tempfile import tempfile
import concurrent.futures import concurrent.futures
from django.conf import settings from django.conf import settings
@@ -721,6 +722,7 @@ def upload_page(request):
# 上传并识别(不入库) # 上传并识别(不入库)
@require_http_methods(["POST"]) @require_http_methods(["POST"])
def upload(request): def upload(request):
try:
if request.session.get("user_id") is None: if request.session.get("user_id") is None:
fallback_uid = request.POST.get("user_id") or request.GET.get("user_id") fallback_uid = request.POST.get("user_id") or request.GET.get("user_id")
if fallback_uid: if fallback_uid:
@@ -740,11 +742,10 @@ def upload(request):
images_dir = os.path.join(settings.MEDIA_ROOT, "images") images_dir = os.path.join(settings.MEDIA_ROOT, "images")
os.makedirs(images_dir, exist_ok=True) os.makedirs(images_dir, exist_ok=True)
# 按照原始文件进行分组处理
file_results = [] file_results = []
for f in files: for f in files:
group_images = [] # 存储该文件生成的所有图片路径信息 (abs_path, filename) group_images = []
is_pdf = f.name.lower().endswith('.pdf') is_pdf = f.name.lower().endswith('.pdf')
if is_pdf: if is_pdf:
@@ -779,7 +780,6 @@ def upload(request):
dst.write(chunk) dst.write(chunk)
group_images.append((abs_path, filename)) group_images.append((abs_path, filename))
# 对该组图片并行进行 OCR 识别
def run_ocr(img_info): def run_ocr(img_info):
abs_p, fname = img_info abs_p, fname = img_info
try: try:
@@ -796,23 +796,24 @@ def upload(request):
if res: if res:
group_data_list.append(res) group_data_list.append(res)
# 合并该文件的多页识别结果
merged_group_data = {} merged_group_data = {}
for item in group_data_list: for item in group_data_list:
if not isinstance(item, dict): continue if not isinstance(item, dict):
continue
for k, v in item.items(): for k, v in item.items():
key = str(k).strip() key = str(k).strip()
if not key: continue if not key:
continue
if key not in merged_group_data or merged_group_data.get(key) in (None, ''): if key not in merged_group_data or merged_group_data.get(key) in (None, ''):
merged_group_data[key] = v merged_group_data[key] = v
elif merged_group_data.get(key) != v: elif merged_group_data.get(key) != v:
base = key base = key
idx = 2 idx = 2
while f"{base}_{idx}" in merged_group_data: idx += 1 while f"{base}_{idx}" in merged_group_data:
idx += 1
merged_group_data[f"{base}_{idx}"] = v merged_group_data[f"{base}_{idx}"] = v
if not merged_group_data: if not merged_group_data:
# 如果没识别到,至少保留一个空结构或者包含文件名的提示
merged_group_data = {"文件名": f.name, "提示": "未识别到具体内容"} merged_group_data = {"文件名": f.name, "提示": "未识别到具体内容"}
rel_paths = [f"images/{img[1]}" for img in group_images] rel_paths = [f"images/{img[1]}" for img in group_images]
@@ -830,6 +831,8 @@ def upload(request):
"message": f"成功处理 {len(file_results)} 个文件,请确认数据后点击录入", "message": f"成功处理 {len(file_results)} 个文件,请确认数据后点击录入",
"items": file_results, "items": file_results,
}) })
except Exception as e:
return JsonResponse({"status": "error", "message": str(e) or "上传失败"}, status=500)
# 确认并入库 # 确认并入库
@@ -1024,7 +1027,7 @@ def analytics_types_view(request):
size_int = int(size) if size is not None else 10 size_int = int(size) if size is not None else 10
except Exception: except Exception:
size_int = 10 size_int = 10
data = es_analytics_types(gte=gte, lte=lte, size=size_int) data = es_analytics_types(gte=gte, lte=lte, limit=size_int)
return JsonResponse({"status": "success", "data": data}) return JsonResponse({"status": "success", "data": data})
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)
@@ -1261,6 +1264,10 @@ def add_key_view(request):
request.session.modified = True request.session.modified = True
except Exception: except Exception:
pass pass
cur_manage = [str(x).strip() for x in list((me or {}).get("manage_key") or []) if str(x).strip()]
if key_name not in cur_manage:
cur_manage.append(key_name)
es_update_user_by_id(uid, manage_key=cur_manage)
elif can_manage_reg: elif can_manage_reg:
cur = [str(x).strip() for x in list((me or {}).get("registration_manage_keys") or []) if str(x).strip()] cur = [str(x).strip() for x in list((me or {}).get("registration_manage_keys") or []) if str(x).strip()]
if key_name not in cur: if key_name not in cur:
@@ -1580,6 +1587,7 @@ def _dt_label(dt, interval: str):
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def report_view(request): def report_view(request):
try:
session_user_id = request.session.get("user_id") session_user_id = request.session.get("user_id")
if session_user_id is None: if session_user_id is None:
return JsonResponse({"status": "error", "message": "未登录"}, status=401) return JsonResponse({"status": "error", "message": "未登录"}, status=401)
@@ -1678,6 +1686,8 @@ def report_view(request):
}, },
} }
) )
except Exception as e:
return JsonResponse({"status": "error", "message": str(e) or "生成失败"}, status=500)
@require_http_methods(["GET"]) @require_http_methods(["GET"])