Files
SMANAGE/gvsds/apps/core/views.py
T
TermiNexus a208dbb02a chore(tools): 根目录散落工具脚本归位 tools/ 目录
- find/listdir/r/sound/SendWX 及 WARN.mp3 移入 tools/(sound.py 相对路径同目录保持可用)
- Version 死代码(import 自增 VER)归档 tools/Version
- Main.py(生产入口候选)与 TODO 保留根目录待确认
2026-08-13 18:15:30 +08:00

287 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import mimetypes
from pathlib import Path
from django.http import HttpResponse, Http404, JsonResponse, HttpResponseRedirect
from django.views import View
from django.conf import settings
from gvsds.template_engine import render_template
TEMPLATE_DIR = Path(settings.BASE_DIR) / "TempLate"
MIME_TYPES = {
'.js': 'application/javascript',
'.mjs': 'application/javascript',
'.css': 'text/css',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.json': 'application/json',
'.xml': 'application/xml',
'.html': 'text/html',
'.htm': 'text/html',
'.txt': 'text/plain',
'.pdf': 'application/pdf',
'.zip': 'application/zip',
'.mp4': 'video/mp4',
'.webp': 'image/webp',
'.map': 'application/json',
}
SUBDOMAIN_CONFIG = {
'www': {'dir': '', 'root_is_index': True},
'gerp': {'dir': 'GERP', 'root_is_index': True},
'ugm': {'dir': '', 'root_is_index': True},
'api': {'dir': None, 'root_is_index': False},
'mc': {'dir': 'MC', 'root_is_index': True},
'3crb': {'dir': 'CCCRB', 'root_is_index': True},
'ccgo': {'dir': 'CCGO', 'root_is_index': True},
'invite': {'dir': '', 'root_is_index': True},
'doc': {'dir': '', 'root_is_index': True},
'discuss': {'dir': '', 'root_is_index': True},
'community': {'dir': '', 'root_is_index': True},
'comm': {'dir': '', 'root_is_index': True},
't4': {'dir': None, 'root_is_index': False},
'verify': {'dir': 'Verify', 'root_is_index': True},
'verifox': {'dir': 'Verifox', 'root_is_index': True},
'nav': {'dir': 'nav', 'root_is_index': True},
'history': {'dir': 'history', 'root_is_index': True},
'cdn': {'dir': 'cdn', 'root_is_index': False, 'root_text': 'GVSDS CDN'},
'yc8z': {'dir': 'yc8z', 'root_is_index': True},
'omc': {'dir': 'OMC', 'root_is_index': True},
'sve': {'dir': 'SVE', 'root_is_index': True},
'tailwind': {'dir': 'tailwindcss', 'root_is_index': False, 'root_redirect': '/3.4.17'},
'nanjingmassacre': {'dir': 'NanjingMassacre', 'root_is_index': True},
'3c': {'dir': 'CreateECentralCourt', 'root_is_index': True},
'vu': {'dir': None, 'root_is_index': False},
'links': {'dir': None, 'root_is_index': False},
'unclear': {'dir': 'Unclear', 'root_is_index': True},
}
def _get_subdomain(request):
return getattr(request, 'subdomain', 'www') or 'www'
def _get_subdomain_dir(subdomain):
config = SUBDOMAIN_CONFIG.get(subdomain, {})
return config.get('dir', '')
def _resolve_file(subdomain, file_path):
if '..' in file_path.split('/') or '..' in file_path.split(os.sep):
return None
subdomain_dir = _get_subdomain_dir(subdomain)
if subdomain_dir is None:
return None
if subdomain_dir:
candidate = TEMPLATE_DIR / subdomain_dir / file_path
try:
candidate.resolve().relative_to(TEMPLATE_DIR.resolve())
except ValueError:
return None
if candidate.is_file():
return candidate
candidate = TEMPLATE_DIR / file_path
try:
candidate.resolve().relative_to(TEMPLATE_DIR.resolve())
except ValueError:
return None
if candidate.is_file():
return candidate
return None
def _serve_file(full_path, subdomain=None):
ext = full_path.suffix.lower()
content_type = MIME_TYPES.get(ext, mimetypes.guess_type(str(full_path))[0] or 'application/octet-stream')
if ext in ('.html', '.htm'):
rel_path = full_path.relative_to(TEMPLATE_DIR)
template_name = str(rel_path).replace('\\', '/')
base = None
use_base = False
if subdomain:
config = SUBDOMAIN_CONFIG.get(subdomain, {})
base = config.get('base')
use_base = config.get('use_base', False)
if base:
response = render_template(request=None, template_name=template_name, UseBase='false', SubBase=base)
elif not use_base:
response = render_template(request=None, template_name=template_name, UseBase='false')
else:
response = render_template(request=None, template_name=template_name, UseBase='true')
# HTML 永不缓存:Vue SPA 的 index.html 引用了带 hash 的资源名,
# 若被浏览器/代理缓存,会引用已删除的旧资源导致 404、页面加载不全
response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
return response
with open(full_path, 'rb') as f:
response = HttpResponse(f.read(), content_type=content_type)
if ext in ('.css', '.js', '.mjs', '.json', '.xml', '.txt', '.svg'):
response['Cache-Control'] = 'public, max-age=3600'
elif ext in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.mp4'):
response['Cache-Control'] = 'public, max-age=86400'
return response
# ============================================================================
# doc 子域 JSON API(页面由 GVSite SPA 渲染)
# /API/GetDoc/<uuid> → 文章 JSON
# /API/GetSerieDirectory/<uuid> → 系列目录 / 单文档标记 JSON
# ============================================================================
def _doc_api(file_path):
"""doc 子域 JSON API:API/GetDoc/<uuid> 与 API/GetSerieDirectory/<uuid>。"""
from gvsds.apps.doc.manager import DocManager
from gvsdsdk.model_utils import UUIDObj
parts = file_path.split('/')
if len(parts) < 3 or parts[0] != 'API':
return None
action = parts[1]
uuid_text = parts[2].strip('/')
try:
uuid_bytes = UUIDObj(uuid_text).bytes
except (ValueError, TypeError, AttributeError):
return JsonResponse({'error': 'invalid_uuid'}, status=404)
if action == 'GetDoc':
doc = DocManager.GetDocument(uuid_bytes)
if doc is None:
return JsonResponse({'error': 'not_found'}, status=404)
return JsonResponse({
'DocTitle': doc.DocTitle or '未命名文档',
'DocContent': doc.DocContent or '',
'DocFormat': doc.DocFormat,
'DocPermission': 1,
'DocDecrypt': True,
})
if action == 'GetSerieDirectory':
from gvsds.apps.doc.models import CommunityDocument, CommunitySeries
try:
serie = CommunitySeries.objects.get(DocSerieUUID=uuid_bytes)
except CommunitySeries.DoesNotExist:
serie = None
if serie is not None:
docs = CommunityDocument.objects.filter(DocSerieUUID=uuid_bytes)
chapters = [
{
'id': UUIDObj(d.DocUUID).str.replace('-', ''),
'title': d.DocTitle or '未命名文档',
'sections': [],
}
for d in docs
]
return JsonResponse({
'title': serie.DocSerieName or '未命名系列',
'document': False,
'chapters': chapters,
})
# 非系列 UUID:单文档模式,返回所属系列的全部文章供侧边栏展示
doc = DocManager.GetDocument(uuid_bytes)
if doc is None:
return JsonResponse({'error': 'not_found'}, status=404)
chapters = []
if doc.DocSerieUUID:
serie_docs = CommunityDocument.objects.filter(DocSerieUUID=doc.DocSerieUUID)
chapters = [
{
'id': UUIDObj(d.DocUUID).str.replace('-', ''),
'title': d.DocTitle or '未命名文档',
'sections': [],
}
for d in serie_docs
]
return JsonResponse({
'title': doc.DocTitle or '未命名文档',
'document': True,
'id': UUIDObj(doc.DocUUID).str.replace('-', ''),
'chapters': chapters,
})
return None
class SubdomainView(View):
def get(self, request, file_path=''):
subdomain = _get_subdomain(request)
config = SUBDOMAIN_CONFIG.get(subdomain, {})
if config.get('dir') is None:
raise Http404
# doc 子域:动态阅读 JSON API(页面由 GVSite SPA 渲染,走下方通用静态/SPA fallback)
# doc.gvsds.com/API/GetDoc/<uuid> → 文章 JSON
# doc.gvsds.com/API/GetSerieDirectory/<uuid> → 目录 JSON
if subdomain == 'doc' and file_path and file_path.startswith('API/'):
api_resp = _doc_api(file_path)
if api_resp:
return api_resp
# ugm 子域:未登录则跳转登录页,已登录则跳转 gerp
if subdomain == 'ugm':
# JWT Cookie 认证:检查 AccessToken 或 RefreshToken
has_jwt = request.COOKIES.get('AccessToken') or request.COOKIES.get('RefreshToken')
if not has_jwt:
# 未登录 → 跳转 www 登录页
if file_path and file_path not in ('/', 'login.html', 'register.html'):
login_url = f"https://www.gvsds.com/login?redirect=https://gerp.gvsds.com/"
return HttpResponseRedirect(login_url)
if not file_path or file_path == '/':
return HttpResponseRedirect('https://www.gvsds.com/login')
else:
# 已登录 → 跳转 gerp
if not file_path or file_path == '/' or file_path == 'index.html':
return HttpResponseRedirect('https://gerp.gvsds.com/')
if not file_path or file_path == '/':
if config.get('root_redirect'):
return HttpResponseRedirect(config['root_redirect'])
if config.get('root_text') is not None:
return HttpResponse(config['root_text'])
if config.get('root_is_index'):
file_path = 'index.html'
else:
raise Http404
if file_path.endswith('/'):
file_path += 'index.html'
if subdomain == 'tailwind' and not file_path.endswith('.js') and not file_path.endswith('.json'):
js_path = _resolve_file(subdomain, file_path + '.js')
if js_path:
return _serve_file(js_path, subdomain)
resolved = _resolve_file(subdomain, file_path)
if resolved:
return _serve_file(resolved, subdomain)
# www/invite 子域:旧 .html 页面已被 Vue SPA 取代,去掉后缀重定向到 Vue 路由
# 兼容历史链接(如 /login.html、/gallery.html 等被外部页面/脚本引用的旧地址)
if subdomain in ('www', 'invite') and file_path.endswith('.html'):
new_path = file_path[:-5] or '/'
redirect_url = new_path
if request.META.get('QUERY_STRING'):
redirect_url += '?' + request.META['QUERY_STRING']
return HttpResponseRedirect(redirect_url)
# SPA fallback:前端单页应用(如 GERP Vue)的客户端路由路径(无扩展名)
# 找不到对应文件时,回退到 index.html,由前端路由处理
if config.get('root_is_index') and '.' not in os.path.basename(file_path):
index_resolved = _resolve_file(subdomain, 'index.html')
if index_resolved:
return _serve_file(index_resolved, subdomain)
raise Http404