init. 初始化项目,并初步完成前端模板样式

This commit is contained in:
2026-03-26 11:08:42 +08:00
commit af9d7a676f
116 changed files with 5266 additions and 0 deletions
View File
+16
View File
@@ -0,0 +1,16 @@
"""
ASGI config for server project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
application = get_asgi_application()
+316
View File
@@ -0,0 +1,316 @@
"""
Django settings for server project.
Generated by 'django-admin startproject' using Django 6.0.3.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# 日志文件夹不存在时创建
logs_path = os.path.join(BASE_DIR, 'db/logs')
if not os.path.exists(logs_path):
os.makedirs(logs_path)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-gr&k$v)_t*ax)yr1v9iz3x%$-j812ajpxs+fe2dn*dgy=a!730'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["*"]
AUTH_USER_MODEL = 'users.User'
# Application definition
INSTALLED_APPS = [
'simpleui',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_ckeditor_5',
'base.apps.BaseConfig',
'article.apps.ArticleConfig',
'users.apps.UsersConfig',
'front.apps.ContentConfig'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'server.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates']
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'server.wsgi.application'
ASGI_APPLICATION = 'server.asgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = "zh-Hans"
TIME_ZONE = "Asia/Shanghai"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
# STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = (
os.path.join(os.path.join(BASE_DIR, 'static')),
)
# 日志配置
LOGGING = {
'version': 1, # 版本
'disable_existing_loggers': False, # 是否禁用已经存在的日志器
'formatters': { # 日志信息显示的格式
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s'
},
'simple': {
'format': '%(levelname)s %(module)s %(lineno)d %(message)s'
},
},
'filters': { # 过滤器
'require_debug_true': { # django在debug模式下才输出日志
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': { # 日志处理方法
'console': { # 向终端中输出日志
'level': 'DEBUG', # 输出等级为“INFO”
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'file': { # 向文件中输出日志
'level': os.getenv('LOGGER_LEVEL') if os.getenv('LOGGER_LEVEL') else 'INFO', # 输出等级为“INFO”
# 新增内容
# 'class': 'logging.handlers.TimedRotatingFileHandler',
# 'filename': os.path.join(BASE_DIR, 'logs/logs.log'),
# 'when': 'm',
# 'interval': 10,
# 'backupCount': 10,
'class': 'logging.handlers.RotatingFileHandler',
'filename': BASE_DIR / "db/logs/django_server.log", # 日志文件的位置
'maxBytes': 5 * 1024 * 1024, # 日志文件的大小(300*1024*1024为300MB
'backupCount': 10, # 日志文件的数量(超过设定的最大值会自动备份,备份数量最大值为10)
'formatter': 'verbose', # 日志输出格式:使用了在之前定义的'verbose'
'encoding': 'utf-8' # 新增此行,指定文件编码为UTF-8
},
},
'loggers': { # 日志器
'ptools': { # 定义了一个名为django的日志器
'handlers': ['console', 'file'], # 可以同时向终端与文件中输出日志
'propagate': True, # 是否继续传递日志信息
'level': os.getenv('LOGGER_LEVEL') if os.getenv('LOGGER_LEVEL') else 'INFO', # 日志器接收的最低日志级别
},
'requests': {
'level': 'WARNING',
'handlers': ['console'], # 或你定义的其他 handler
'propagate': False,
},
'urllib3': {
'level': 'WARNING',
'handlers': ['console'],
'propagate': False,
},
}
}
# 调整POST传输数据文件大小限制
DATA_UPLOAD_MAX_MEMORY_SIZE = 25 * 1024 * 1024 * 1024
FILE_UPLOAD_MAX_MEMORY_SIZE = 25 * 1024 * 1024 * 1024
# 自定义UI配置
SIMPLEUI_HOME_TITLE = '长白山消防支队'
SIMPLEUI_HOME_ICON = 'fa fa-optin-monster'
SIMPLEUI_LOGO = 'https://avatars2.githubusercontent.com/u/13655483?s=60&v=4'
SIMPLEUI_HOME_INFO = False
SIMPLEUI_INDEX = ''
SIMPLEUI_ANALYSIS = False
SIMPLEUI_STATIC_OFFLINE = True
SIMPLEUI_HOME_ACTION = False
CKEDITOR_CONFIGS = {
'default': {
'toolbar': 'full',
'height': 300,
'width': '100%',
},
}
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
customColorPalette = [
{
'color': 'hsl(4, 90%, 58%)',
'label': 'Red'
},
{
'color': 'hsl(340, 82%, 52%)',
'label': 'Pink'
},
{
'color': 'hsl(291, 64%, 42%)',
'label': 'Purple'
},
{
'color': 'hsl(262, 52%, 47%)',
'label': 'Deep Purple'
},
{
'color': 'hsl(231, 48%, 48%)',
'label': 'Indigo'
},
{
'color': 'hsl(207, 90%, 54%)',
'label': 'Blue'
},
]
CKEDITOR_5_CUSTOM_CSS = 'path_to.css' # optional
CKEDITOR_5_FILE_STORAGE = "path_to_storage.CustomStorage" # optional
CKEDITOR_5_CONFIGS = {
'default': {
'toolbar': {
'items': ['heading', '|', 'bold', 'italic', 'link',
'bulletedList', 'numberedList', 'blockQuote', 'imageUpload', ],
}
},
'extends': {
'blockToolbar': [
'paragraph', 'heading1', 'heading2', 'heading3',
'|',
'bulletedList', 'numberedList',
'|',
'blockQuote',
],
'toolbar': {
'items': ['heading', '|', 'outdent', 'indent', '|', 'bold', 'italic', 'link', 'underline', 'strikethrough',
'code', 'subscript', 'superscript', 'highlight', '|', 'codeBlock', 'sourceEditing', 'insertImage',
'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', '|',
'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'mediaEmbed', 'removeFormat',
'insertTable',
],
'shouldNotGroupWhenFull': 'true'
},
'image': {
'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft',
'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'],
'styles': [
'full',
'side',
'alignLeft',
'alignRight',
'alignCenter',
]
},
'table': {
'contentToolbar': ['tableColumn', 'tableRow', 'mergeTableCells',
'tableProperties', 'tableCellProperties'],
'tableProperties': {
'borderColors': customColorPalette,
'backgroundColors': customColorPalette
},
'tableCellProperties': {
'borderColors': customColorPalette,
'backgroundColors': customColorPalette
}
},
'heading': {
'options': [
{'model': 'paragraph', 'title': 'Paragraph', 'class': 'ck-heading_paragraph'},
{'model': 'heading1', 'view': 'h1', 'title': 'Heading 1', 'class': 'ck-heading_heading1'},
{'model': 'heading2', 'view': 'h2', 'title': 'Heading 2', 'class': 'ck-heading_heading2'},
{'model': 'heading3', 'view': 'h3', 'title': 'Heading 3', 'class': 'ck-heading_heading3'}
]
}
},
'list': {
'properties': {
'styles': 'true',
'startIndex': 'true',
'reversed': 'true',
}
}
}
# Define a constant in settings.py to specify file upload permissions
CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" # Possible values: "staff", "authenticated", "any"
# CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME = "custom_upload_file"
+160
View File
@@ -0,0 +1,160 @@
"""
URL configuration for server project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path, include
import logging
import os
import pkgutil
import traceback
import importlib
import jwt
from django.contrib.auth.models import User
from django.http import JsonResponse
from jwt import exceptions
from ninja import NinjaAPI
from ninja.errors import ValidationError
from ninja.security import HttpBearer
from .settings import SECRET_KEY
from utils.common_response import CommonResponse
logger = logging.getLogger('ptools')
class GlobalAuth(HttpBearer):
def authenticate(self, request, token):
try:
logger.debug('用户认证中')
logger.debug(f'解析 Auth Token{token}')
res = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
user = User.objects.filter(id=res.get('id'), username=res.get('username')).first()
logger.debug(user)
if not user:
raise exceptions.PyJWKError('用户不存在')
request.user = user
return token
except exceptions.PyJWKError:
msg = 'Token 认证失败,请重新登陆!'
logger.error(msg)
raise
except exceptions.ExpiredSignatureError:
msg = '认证令牌已过期,请重新登陆!'
logger.error(msg)
raise
except Exception as e:
msg = '认证失败,请检查用户是否存在'
logger.error(msg)
raise
# 2. 获取当前文件所在的目录路径 (假设此文件与各个 app 文件夹同级,或者在特定配置目录下)
# 如果你的此文件在项目根目录,而 app 文件夹也在根目录:
from django.conf import settings
project_root = settings.BASE_DIR
# 如果 app 文件夹是 Django 的已安装应用,可能需要通过 settings.INSTALLED_APPS 获取,
# 但通常直接扫描物理目录更灵活。这里假设扫描当前目录下的所有子文件夹。
# 3. 自动发现并注册路由
def auto_load_routers(base_path, package_name_prefix=None):
"""
base_path: 物理路径,例如 /path/to/project
package_name_prefix: 包名前缀,例如 'myproject.' (如果这些文件夹是包的一部分)
"""
# 获取该路径下所有子模块/包
# 注意:这需要这些文件夹是合法的 Python 包 (即包含 __init__.py),或者在 Python 路径中
for importer, modname, ispkg in pkgutil.iter_modules([base_path]):
# 过滤条件:
# 1. 必须是包 (文件夹)
# 2. 不以 '_' 开头 (排除 __pycache__, _private 等)
# 3. 排除当前文件所在的脚本名 (防止递归或错误)
if ispkg and not modname.startswith('_'):
# 构造完整的模块路径:例如 "authorize.views"
# 如果这些文件夹直接位于根路径且不是大包的一部分,package_name_prefix 可能为空
if package_name_prefix:
full_module_path = f"{package_name_prefix}{modname}.views"
else:
full_module_path = f"{modname}.views"
try:
# 动态导入 views 模块
module = importlib.import_module(full_module_path)
# 检查是否存在 'router' 变量
if hasattr(module, 'router'):
router = getattr(module, 'router')
# 使用文件夹名称 (modname) 作为 URL 前缀
# 例如: 文件夹 'authorize' -> 前缀 '/authorize'
url_prefix = f"/{modname}"
api_v1.add_router(url_prefix, router)
print(f"✅ 自动注册: {url_prefix} (来自 {full_module_path})")
else:
print(f"⚠️ 跳过: {modname} (在 {full_module_path} 中未找到 'router' 变量)")
except ImportError as e:
# 如果文件夹里没有 views.py,会报 ImportError,这是正常的,跳过即可
if "No module named" in str(e) and "views" in str(e):
print(f"️ 跳过: {modname} (没有 views.py 模块)")
else:
print(f"❌ 导入错误 {modname}: {e}")
except Exception as e:
print(f"❌ 注册失败 {modname}: {e}")
api_v1 = NinjaAPI(version='1.0.0', auth=GlobalAuth())
auto_load_routers(project_root)
@api_v1.exception_handler(ValidationError)
def validation_errors(request, exc):
logger.error(request.body)
logger.error(exc)
logger.debug(traceback.format_exc())
return JsonResponse(data=CommonResponse.error(msg=f"输入参数验证失败!").to_dict(), status=422, safe=False)
@api_v1.exception_handler(exceptions.PyJWKError) # 捕获 HttpBearer 认证异常
def custom_http_bearer_exception_handler(request, exc):
logger.error(exc)
return JsonResponse(data=CommonResponse.error(msg=f"用户Token认证失败:{exc}").to_dict(), status=401)
@api_v1.exception_handler(exceptions.ExpiredSignatureError) # 捕获 HttpBearer 认证异常
def custom_http_bearer_exception_handler(request, exc):
logger.error(exc)
return JsonResponse(data=CommonResponse.error(msg=f"用户登录信息已过期,请重新登录!!").to_dict(), status=401)
urlpatterns = [
path('', include("front.urls")),
path('admin/', admin.site.urls),
path("api/", api_v1.urls),
path("ckeditor5/", include('django_ckeditor_5.urls')),
# path("upload/", custom_upload_function, name="custom_upload_file"),
]
urlpatterns += staticfiles_urlpatterns()
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+16
View File
@@ -0,0 +1,16 @@
"""
WSGI config for server project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
application = get_wsgi_application()