init. 初始化项目,并初步完成前端模板样式
This commit is contained in:
@@ -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()
|
||||
@@ -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
@@ -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)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user