init
This commit is contained in:
4
backend/backend/__init__.py
Normal file
4
backend/backend/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
||||
16
backend/backend/asgi.py
Normal file
16
backend/backend/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for backend 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/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
19
backend/backend/celery.py
Normal file
19
backend/backend/celery.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import os
|
||||
from celery import Celery
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') # 请将 myproject 替换为你的项目名称
|
||||
app = Celery('backend')
|
||||
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||
app.autodiscover_tasks()
|
||||
|
||||
# Windows 兼容性设置
|
||||
if os.name == 'nt':
|
||||
app.conf.update(
|
||||
task_serializer='json',
|
||||
accept_content=['json'], # Ignore other content
|
||||
result_serializer='json',
|
||||
timezone='Asia/Shanghai',
|
||||
enable_utc=True,
|
||||
)
|
||||
# 强制使用 single-threaded 执行模式
|
||||
app.conf.worker_pool = 'solo'
|
||||
172
backend/backend/settings.py
Normal file
172
backend/backend/settings.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Django settings for backend project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.2.1.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/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
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-m4@pv814c_m^pgpyhz^i96a@mcqh_@m9ccu(17*895t!79e!nb'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'*',
|
||||
]
|
||||
INTERNAL_IPS = [
|
||||
'*',
|
||||
]
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = True # 允许跨域名访问
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_ALL_ORIGINS =True
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"simpleui",
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
"rest_framework",
|
||||
'django_filters',
|
||||
'corsheaders',
|
||||
'rest_framework.authtoken',
|
||||
"system",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = 'system.User'
|
||||
ROOT_URLCONF = 'backend.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'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 = 'backend.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'django-vue',
|
||||
'USER': 'root',
|
||||
'PASSWORD': '',
|
||||
'HOST': 'localhost',
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/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/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'zh-hans'
|
||||
|
||||
# 设置为中国标准时间(CST,东八区)
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
# 启用国际化(多语言)
|
||||
USE_I18N = True
|
||||
|
||||
# 启用本地化(格式化日期、数字等)
|
||||
USE_L10N = True
|
||||
|
||||
# 是否使用时区支持(建议开启)
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') #自己在根目录下创建media文件夹
|
||||
# Default primary key field type
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
|
||||
# celery 配置
|
||||
CELERY_BROKER_URL = 'redis://localhost:6379/6'
|
||||
CELERY_RESULT_BACKEND = 'redis://localhost:6379/6'
|
||||
# 时区设置
|
||||
CELERY_TIMEZONE = 'Asia/Shanghai'
|
||||
# 任务序列化方式
|
||||
CELERY_TASK_SERIALIZER = 'json'
|
||||
CELERY_RESULT_SERIALIZER = 'json'
|
||||
CELERY_ACCEPT_CONTENT = ['json']
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
'every-15-minutes': {
|
||||
'task': 'system.tasks.add', # 任务路径
|
||||
'schedule': 900.0, # 每15分钟执行一次
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if os.path.exists(os.path.join(BASE_DIR, 'backend/local_settings.py')):
|
||||
from backend.local_settings import *
|
||||
23
backend/backend/urls.py
Normal file
23
backend/backend/urls.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
URL configuration for backend project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/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.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/system/', include('system.urls')),
|
||||
]
|
||||
16
backend/backend/wsgi.py
Normal file
16
backend/backend/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for backend 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/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
22
backend/manage.py
Executable file
22
backend/manage.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
13
backend/requirements.txt
Normal file
13
backend/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
Django==5.2.1
|
||||
djangorestframework==3.16.0
|
||||
django-filter==25.1
|
||||
django-cors-headers==4.7.0
|
||||
django-ckeditor==6.7.2
|
||||
openpyxl==3.1.5
|
||||
mysqlclient==2.2.7
|
||||
django-simpleui==2025.5.17
|
||||
requests==2.32.3
|
||||
celery==5.5.3
|
||||
redis==6.2.0
|
||||
eventlet==0.40.0
|
||||
goofish_api==0.0.6
|
||||
0
backend/system/__init__.py
Normal file
0
backend/system/__init__.py
Normal file
3
backend/system/admin.py
Normal file
3
backend/system/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/system/apps.py
Normal file
6
backend/system/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SystemConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'system'
|
||||
990
backend/system/migrations/0001_initial.py
Normal file
990
backend/system/migrations/0001_initial.py
Normal file
@@ -0,0 +1,990 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-29 13:08
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import utils.utils
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DictType",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
default="", max_length=100, verbose_name="字典名称"
|
||||
),
|
||||
),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
db_index=True,
|
||||
default="",
|
||||
max_length=100,
|
||||
verbose_name="字典类型",
|
||||
),
|
||||
),
|
||||
("status", models.BooleanField(default=True)),
|
||||
(
|
||||
"deleted_time",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="删除时间"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "字典类型",
|
||||
"verbose_name_plural": "字典类型",
|
||||
"db_table": "system_dict_type",
|
||||
"ordering": ["-id"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="MenuMeta",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
("title", models.CharField(max_length=200, verbose_name="标题")),
|
||||
(
|
||||
"icon",
|
||||
models.CharField(blank=True, max_length=100, verbose_name="图标"),
|
||||
),
|
||||
("order", models.IntegerField(default=0, verbose_name="排序")),
|
||||
(
|
||||
"affix_tab",
|
||||
models.BooleanField(default=False, verbose_name="固定标签页"),
|
||||
),
|
||||
(
|
||||
"badge",
|
||||
models.CharField(
|
||||
blank=True, max_length=50, verbose_name="徽章文本"
|
||||
),
|
||||
),
|
||||
(
|
||||
"badge_type",
|
||||
models.CharField(
|
||||
blank=True, max_length=20, verbose_name="徽章类型"
|
||||
),
|
||||
),
|
||||
(
|
||||
"badge_variants",
|
||||
models.CharField(
|
||||
blank=True, max_length=20, verbose_name="徽章样式"
|
||||
),
|
||||
),
|
||||
("iframe_src", models.URLField(blank=True, verbose_name="内嵌页面URL")),
|
||||
("link", models.URLField(blank=True, verbose_name="外部链接")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "菜单元数据",
|
||||
"verbose_name_plural": "菜单元数据",
|
||||
"db_table": "system_menu_meta",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Role",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
("name", models.CharField(max_length=100, verbose_name="角色名称")),
|
||||
(
|
||||
"status",
|
||||
models.IntegerField(
|
||||
choices=[(1, "启用"), (0, "禁用")],
|
||||
default=1,
|
||||
verbose_name="角色状态",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sort",
|
||||
models.IntegerField(
|
||||
default=0, help_text="数值越小越靠前", verbose_name="显示排序"
|
||||
),
|
||||
),
|
||||
("remark", models.TextField(blank=True, verbose_name="备注")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "角色管理",
|
||||
"verbose_name_plural": "角色管理",
|
||||
"ordering": ["-create_time"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Dept",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
("name", models.CharField(max_length=100, verbose_name="部门名称")),
|
||||
(
|
||||
"status",
|
||||
models.SmallIntegerField(
|
||||
choices=[(0, "禁用"), (1, "启用")],
|
||||
default=0,
|
||||
verbose_name="部门状态",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
|
||||
),
|
||||
(
|
||||
"sort",
|
||||
models.IntegerField(
|
||||
default=0, help_text="数值越小越靠前", verbose_name="显示排序"
|
||||
),
|
||||
),
|
||||
(
|
||||
"leader",
|
||||
models.CharField(
|
||||
blank=True, max_length=20, null=True, verbose_name="负责人"
|
||||
),
|
||||
),
|
||||
(
|
||||
"phone",
|
||||
models.CharField(
|
||||
blank=True, max_length=20, null=True, verbose_name="联系电话"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, null=True, verbose_name="邮箱"
|
||||
),
|
||||
),
|
||||
("remark", models.TextField(blank=True, verbose_name="备注")),
|
||||
(
|
||||
"pid",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="children",
|
||||
to="system.dept",
|
||||
verbose_name="父部门 ID",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "部门管理",
|
||||
"verbose_name_plural": "部门管理",
|
||||
"ordering": ["-create_time"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="last name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, verbose_name="email address"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||
verbose_name="active",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined"
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
(
|
||||
"mobile",
|
||||
models.CharField(
|
||||
db_comment="手机号",
|
||||
max_length=11,
|
||||
null=True,
|
||||
validators=[utils.utils.validate_mobile],
|
||||
),
|
||||
),
|
||||
(
|
||||
"nickname",
|
||||
models.CharField(
|
||||
blank=True, db_comment="昵称", max_length=50, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"gender",
|
||||
models.SmallIntegerField(
|
||||
blank=True, db_comment="性别", default=0, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"language",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
db_comment="语言",
|
||||
max_length=20,
|
||||
null=True,
|
||||
verbose_name="语言",
|
||||
),
|
||||
),
|
||||
(
|
||||
"city",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
db_comment="城市",
|
||||
max_length=20,
|
||||
null=True,
|
||||
verbose_name="城市",
|
||||
),
|
||||
),
|
||||
(
|
||||
"province",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
db_comment="省份",
|
||||
max_length=50,
|
||||
null=True,
|
||||
verbose_name="省份",
|
||||
),
|
||||
),
|
||||
(
|
||||
"country",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
db_comment="国家",
|
||||
max_length=50,
|
||||
null=True,
|
||||
verbose_name="国家",
|
||||
),
|
||||
),
|
||||
(
|
||||
"avatarUrl",
|
||||
models.URLField(
|
||||
blank=True, db_comment="头像", null=True, verbose_name="头像"
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.BooleanField(
|
||||
db_comment="帐号状态",
|
||||
default=False,
|
||||
verbose_name="<帐号状态>(1正常 0停用)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"login_date",
|
||||
models.DateTimeField(
|
||||
blank=True,
|
||||
db_comment="最后登录时间",
|
||||
null=True,
|
||||
verbose_name="<最后登录时间>",
|
||||
),
|
||||
),
|
||||
(
|
||||
"login_ip",
|
||||
models.GenericIPAddressField(
|
||||
blank=True, db_comment="最后登录IP", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
(
|
||||
"dept",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
related_name="users",
|
||||
to="system.dept",
|
||||
verbose_name="部门",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "用户数据",
|
||||
"verbose_name_plural": "用户数据",
|
||||
"db_table": "system_users",
|
||||
},
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="DictData",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
("sort", models.IntegerField(default=0, verbose_name="字典排序")),
|
||||
(
|
||||
"label",
|
||||
models.CharField(
|
||||
default="", max_length=100, verbose_name="字典标签"
|
||||
),
|
||||
),
|
||||
(
|
||||
"value",
|
||||
models.CharField(
|
||||
default="", max_length=100, verbose_name="字典键值"
|
||||
),
|
||||
),
|
||||
("status", models.BooleanField(default=True)),
|
||||
(
|
||||
"color_type",
|
||||
models.CharField(
|
||||
blank=True, default="", max_length=100, verbose_name="颜色类型"
|
||||
),
|
||||
),
|
||||
(
|
||||
"css_class",
|
||||
models.CharField(
|
||||
blank=True, default="", max_length=100, verbose_name="css 样式"
|
||||
),
|
||||
),
|
||||
(
|
||||
"dict_type",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="dict_data",
|
||||
to="system.dicttype",
|
||||
verbose_name="字典类型",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "字典数据",
|
||||
"verbose_name_plural": "字典数据",
|
||||
"db_table": "system_dict_data",
|
||||
"ordering": ["sort", "id"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Menu",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="创建时间",
|
||||
null=True,
|
||||
verbose_name="创建时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
("name", models.CharField(max_length=100, verbose_name="菜单名称")),
|
||||
(
|
||||
"status",
|
||||
models.IntegerField(
|
||||
choices=[(1, "启用"), (0, "禁用")],
|
||||
default=1,
|
||||
verbose_name="状态",
|
||||
),
|
||||
),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("catalog", "目录"),
|
||||
("menu", "菜单"),
|
||||
("button", "按钮"),
|
||||
("embedded", "内嵌页面"),
|
||||
("link", "外部链接"),
|
||||
],
|
||||
max_length=20,
|
||||
verbose_name="菜单类型",
|
||||
),
|
||||
),
|
||||
(
|
||||
"path",
|
||||
models.CharField(
|
||||
blank=True, max_length=200, verbose_name="路由路径"
|
||||
),
|
||||
),
|
||||
(
|
||||
"component",
|
||||
models.CharField(
|
||||
blank=True, max_length=200, verbose_name="组件路径"
|
||||
),
|
||||
),
|
||||
(
|
||||
"auth_code",
|
||||
models.CharField(
|
||||
blank=True, max_length=100, verbose_name="权限编码"
|
||||
),
|
||||
),
|
||||
(
|
||||
"pid",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="children",
|
||||
to="system.menu",
|
||||
verbose_name="父菜单",
|
||||
),
|
||||
),
|
||||
(
|
||||
"meta",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="system.menumeta",
|
||||
verbose_name="元数据",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "菜单",
|
||||
"verbose_name_plural": "菜单管理",
|
||||
"ordering": ["meta__order", "id"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RolePermission",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"remark",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="备注",
|
||||
max_length=256,
|
||||
null=True,
|
||||
verbose_name="备注",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="创建人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="创建人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modifier",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="修改人",
|
||||
max_length=64,
|
||||
null=True,
|
||||
verbose_name="修改人",
|
||||
),
|
||||
),
|
||||
(
|
||||
"update_time",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="修改时间",
|
||||
null=True,
|
||||
verbose_name="修改时间",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_deleted",
|
||||
models.BooleanField(default=False, verbose_name="是否软删除"),
|
||||
),
|
||||
(
|
||||
"create_time",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="权限关联时间"
|
||||
),
|
||||
),
|
||||
(
|
||||
"menu",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="system.menu",
|
||||
verbose_name="菜单/权限",
|
||||
),
|
||||
),
|
||||
(
|
||||
"role",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="system.role",
|
||||
verbose_name="角色",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "角色权限关联",
|
||||
"verbose_name_plural": "角色权限关联",
|
||||
"db_table": "system_role_permission",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="role",
|
||||
name="permissions",
|
||||
field=models.ManyToManyField(
|
||||
through="system.RolePermission",
|
||||
to="system.menu",
|
||||
verbose_name="关联权限",
|
||||
),
|
||||
),
|
||||
]
|
||||
0
backend/system/migrations/__init__.py
Normal file
0
backend/system/migrations/__init__.py
Normal file
248
backend/system/models.py
Normal file
248
backend/system/models.py
Normal file
@@ -0,0 +1,248 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
from backend import settings
|
||||
from utils.models import CoreModel
|
||||
from utils.utils import validate_mobile
|
||||
|
||||
|
||||
# 定义状态枚举(可根据实际业务扩展)
|
||||
class DepartmentStatus(models.IntegerChoices):
|
||||
DISABLED = 0, "禁用" # 对应数据中的 status: 0
|
||||
ENABLED = 1, "启用" # 对应数据中的 status: 1
|
||||
|
||||
class Dept(CoreModel):
|
||||
pid = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="children",
|
||||
verbose_name="父部门 ID"
|
||||
)
|
||||
name = models.CharField(max_length=100, verbose_name="部门名称")
|
||||
status = models.SmallIntegerField(
|
||||
choices=DepartmentStatus.choices,
|
||||
default=DepartmentStatus.DISABLED,
|
||||
verbose_name="部门状态"
|
||||
)
|
||||
create_time = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name="创建时间",
|
||||
# 若数据中时间需自动解析,可在保存时处理:
|
||||
# default=timezone.now # 或通过数据导入时赋值
|
||||
)
|
||||
sort = models.IntegerField(
|
||||
default=0,
|
||||
verbose_name="显示排序",
|
||||
help_text="数值越小越靠前"
|
||||
)
|
||||
leader = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=20,
|
||||
verbose_name="负责人"
|
||||
)
|
||||
phone = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="联系电话"
|
||||
)
|
||||
email = models.EmailField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="邮箱"
|
||||
)
|
||||
remark = models.TextField(blank=True, verbose_name="备注")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "部门管理"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ["-create_time"] # 按创建时间倒序排列
|
||||
|
||||
# 菜单类型枚举
|
||||
class MenuType(models.TextChoices):
|
||||
CATALOG = 'catalog', '目录'
|
||||
MENU = 'menu', '菜单'
|
||||
BUTTON = 'button', '按钮'
|
||||
EMBEDDED = 'embedded', '内嵌页面'
|
||||
LINK = 'link', '外部链接'
|
||||
|
||||
# 菜单状态枚举
|
||||
class MenuStatus(models.IntegerChoices):
|
||||
ENABLED = 1, '启用'
|
||||
DISABLED = 0, '禁用'
|
||||
|
||||
# 菜单元数据模型(单独存储元数据,避免 JSONField)
|
||||
class MenuMeta(CoreModel):
|
||||
title = models.CharField(max_length=200, verbose_name='标题')
|
||||
icon = models.CharField(max_length=100, blank=True, verbose_name='图标')
|
||||
order = models.IntegerField(default=0, verbose_name='排序')
|
||||
affix_tab = models.BooleanField(default=False, verbose_name='固定标签页')
|
||||
badge = models.CharField(max_length=50, blank=True, verbose_name='徽章文本')
|
||||
badge_type = models.CharField(max_length=20, blank=True, verbose_name='徽章类型')
|
||||
badge_variants = models.CharField(max_length=20, blank=True, verbose_name='徽章样式')
|
||||
iframe_src = models.URLField(blank=True, verbose_name='内嵌页面URL')
|
||||
link = models.URLField(blank=True, verbose_name='外部链接')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
db_table = 'system_menu_meta'
|
||||
verbose_name = '菜单元数据'
|
||||
verbose_name_plural = '菜单元数据'
|
||||
|
||||
# 主菜单模型
|
||||
class Menu(CoreModel):
|
||||
pid = models.ForeignKey(
|
||||
'self',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='children',
|
||||
verbose_name='父菜单'
|
||||
)
|
||||
name = models.CharField(max_length=100, verbose_name='菜单名称')
|
||||
status = models.IntegerField(choices=MenuStatus.choices, default=MenuStatus.ENABLED, verbose_name='状态')
|
||||
type = models.CharField(choices=MenuType.choices, max_length=20, verbose_name='菜单类型')
|
||||
path = models.CharField(max_length=200, blank=True, verbose_name='路由路径')
|
||||
component = models.CharField(max_length=200, blank=True, verbose_name='组件路径')
|
||||
auth_code = models.CharField(max_length=100, blank=True, verbose_name='权限编码')
|
||||
meta = models.OneToOneField(MenuMeta, on_delete=models.CASCADE, verbose_name='元数据')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = '菜单'
|
||||
verbose_name_plural = '菜单管理'
|
||||
ordering = ['meta__order', 'id']
|
||||
|
||||
# 角色状态枚举
|
||||
class RoleStatus(models.IntegerChoices):
|
||||
ENABLED = 1, '启用'
|
||||
DISABLED = 0, '禁用'
|
||||
|
||||
class Role(CoreModel):
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name='角色名称'
|
||||
)
|
||||
status = models.IntegerField(
|
||||
choices=RoleStatus.choices,
|
||||
default=RoleStatus.ENABLED,
|
||||
verbose_name='角色状态'
|
||||
)
|
||||
sort = models.IntegerField(
|
||||
default=0,
|
||||
verbose_name="显示排序",
|
||||
help_text="数值越小越靠前"
|
||||
)
|
||||
remark = models.TextField(
|
||||
blank=True,
|
||||
verbose_name='备注'
|
||||
)
|
||||
# 与菜单权限的多对多关联(假设菜单模型为 Menu,权限字段为 auth_code)
|
||||
permissions = models.ManyToManyField(
|
||||
'Menu', # 引用之前设计的 Menu 模型
|
||||
through='RolePermission',
|
||||
verbose_name='关联权限'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '角色管理'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ["-create_time"] # 按创建时间倒序排列
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# 中间表:角色与权限的关联(可扩展字段如权限生效时间)
|
||||
class RolePermission(CoreModel):
|
||||
role = models.ForeignKey(
|
||||
Role,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name='角色'
|
||||
)
|
||||
menu = models.ForeignKey(
|
||||
'Menu',
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name='菜单/权限'
|
||||
)
|
||||
# 可选:记录权限关联时间
|
||||
create_time = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name='权限关联时间'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'system_role_permission'
|
||||
verbose_name = '角色权限关联'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
class DictType(CoreModel):
|
||||
"""字典类型表"""
|
||||
name = models.CharField(max_length=100, default='', verbose_name='字典名称')
|
||||
type = models.CharField(max_length=100, default='', verbose_name='字典类型', db_index=True)
|
||||
status = models.BooleanField(default=True)
|
||||
deleted_time = models.DateTimeField(null=True, blank=True, verbose_name='删除时间')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '字典类型'
|
||||
verbose_name_plural = '字典类型'
|
||||
db_table = 'system_dict_type'
|
||||
ordering = ['-id']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class DictData(CoreModel):
|
||||
"""字典数据表"""
|
||||
sort = models.IntegerField(default=0, verbose_name='字典排序')
|
||||
label = models.CharField(max_length=100, default='', verbose_name='字典标签')
|
||||
value = models.CharField(max_length=100, default='', verbose_name='字典键值')
|
||||
dict_type = models.ForeignKey(
|
||||
DictType,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='dict_data',
|
||||
verbose_name='字典类型'
|
||||
)
|
||||
status = models.BooleanField(default=True)
|
||||
color_type = models.CharField(max_length=100, blank=True, default='', verbose_name='颜色类型')
|
||||
css_class = models.CharField(max_length=100, blank=True, default='', verbose_name='css 样式')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '字典数据'
|
||||
verbose_name_plural = '字典数据'
|
||||
db_table = 'system_dict_data'
|
||||
ordering = ['sort', 'id']
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
|
||||
class User(AbstractUser, CoreModel):
|
||||
mobile = models.CharField(max_length=11, null=True, validators=[validate_mobile], db_comment="手机号")
|
||||
nickname = models.CharField(max_length=50, blank=True, null=True, db_comment="昵称")
|
||||
gender = models.SmallIntegerField(blank=True, null=True, default=0, db_comment='性别')
|
||||
language = models.CharField('语言', max_length=20, blank=True, null=True, db_comment="语言")
|
||||
city = models.CharField('城市', max_length=20, blank=True, null=True, db_comment="城市")
|
||||
province = models.CharField('省份', max_length=50, blank=True, null=True, db_comment="省份")
|
||||
country = models.CharField('国家', max_length=50, blank=True, null=True, db_comment="国家")
|
||||
avatarUrl = models.URLField('头像', blank=True, null=True, db_comment="头像")
|
||||
|
||||
dept = models.ManyToManyField(
|
||||
'Dept', blank=True, verbose_name='部门', db_constraint=False,
|
||||
related_name='users'
|
||||
)
|
||||
status = models.BooleanField(default=False, verbose_name='<帐号状态>(1正常 0停用)', db_comment="帐号状态")
|
||||
login_date = models.DateTimeField("<最后登录时间>", blank=True, null=True, db_comment="最后登录时间")
|
||||
login_ip = models.GenericIPAddressField(blank=True, null=True, db_comment="最后登录IP")
|
||||
|
||||
class Meta:
|
||||
verbose_name = '用户数据'
|
||||
verbose_name_plural = verbose_name
|
||||
db_table = 'system_users'
|
||||
5
backend/system/serializers.py
Normal file
5
backend/system/serializers.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework import serializers
|
||||
from .models import Department, Menu, MenuMeta, Role
|
||||
|
||||
14
backend/system/tasks.py
Normal file
14
backend/system/tasks.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# 某个 app 目录下的 tasks.py
|
||||
from celery import shared_task
|
||||
|
||||
@shared_task
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
@shared_task
|
||||
def sync_temu_order():
|
||||
pass
|
||||
|
||||
@shared_task
|
||||
def sync_temu_shipping():
|
||||
pass
|
||||
3
backend/system/tests.py
Normal file
3
backend/system/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
19
backend/system/urls.py
Normal file
19
backend/system/urls.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.urls import include, path
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'dept', views.DeptViewSet)
|
||||
router.register(r'menu-meta', views.MenuMetaViewSet)
|
||||
router.register(r'menu', views.MenuViewSet)
|
||||
router.register(r'role', views.RoleViewSet)
|
||||
router.register(r'dict_data', views.DictDataViewSet)
|
||||
router.register(r'dict_type', views.DictTypeViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('login/', views.user.UserLogin.as_view()),
|
||||
path('info/', views.user.UserInfo.as_view()),
|
||||
path('codes/', views.user.Codes.as_view()),
|
||||
]
|
||||
16
backend/system/views/__init__.py
Normal file
16
backend/system/views/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
__all__ = [
|
||||
'DeptViewSet',
|
||||
'MenuViewSet',
|
||||
'MenuMetaViewSet',
|
||||
'RoleViewSet',
|
||||
'DictDataViewSet',
|
||||
'DictTypeViewSet',
|
||||
]
|
||||
|
||||
from system.views.dict_data import DictDataViewSet
|
||||
from system.views.dict_type import DictTypeViewSet
|
||||
from system.views.menu import MenuViewSet, MenuMetaViewSet
|
||||
from system.views.role import RoleViewSet
|
||||
|
||||
from system.views.dept import DeptViewSet
|
||||
from system.views.user import *
|
||||
84
backend/system/views/dept.py
Normal file
84
backend/system/views/dept.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from datetime import timezone, datetime
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import status, serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from system.models import Dept
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class DeptSerializer(serializers.ModelSerializer):
|
||||
"""部门序列化器"""
|
||||
children = serializers.SerializerMethodField()
|
||||
status_text = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Dept
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'create_time']
|
||||
|
||||
def get_children(self, obj):
|
||||
"""获取子部门"""
|
||||
children = obj.children.all().order_by('id')
|
||||
if children:
|
||||
return DeptSerializer(children, many=True).data
|
||||
return []
|
||||
|
||||
def get_status_text(self, obj):
|
||||
"""获取状态文本"""
|
||||
return obj.get_status_display()
|
||||
|
||||
|
||||
class DeptViewSet(CustomModelViewSet):
|
||||
"""部门管理视图集"""
|
||||
queryset = Dept.objects.filter(pid__isnull=True).order_by('id', 'status')
|
||||
serializer_class = DeptSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['status', 'pid']
|
||||
search_fields = ['name']
|
||||
ordering_fields = ['create_time', 'name']
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# 自动设置创建时间
|
||||
if 'create_time' not in serializer.validated_data:
|
||||
serializer.validated_data['create_time'] = datetime.now()
|
||||
serializer.save()
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
pk = kwargs['pk']
|
||||
instance = Dept.objects.get(pk=pk)
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
|
||||
if getattr(instance, '_prefetched_objects_cache', None):
|
||||
# If 'prefetch_related' has been applied to a queryset, we need to
|
||||
# forcibly invalidate the prefetch cache on the instance.
|
||||
instance._prefetched_objects_cache = {}
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
pk = kwargs['pk']
|
||||
instance = Dept.objects.get(pk=pk)
|
||||
self.perform_destroy(instance)
|
||||
return self._build_response(
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def tree(self, request):
|
||||
"""获取部门树形结构"""
|
||||
queryset = self.get_queryset().filter(pid__isnull=True)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
16
backend/system/views/dict_data.py
Normal file
16
backend/system/views/dict_data.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from rest_framework import serializers, viewsets
|
||||
from system.models import DictData
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class DictDataSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DictData
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class DictDataViewSet(CustomModelViewSet):
|
||||
queryset = DictData.objects.filter(is_deleted=False)
|
||||
serializer_class = DictDataSerializer
|
||||
filterset_fields = ['dict_type']
|
||||
15
backend/system/views/dict_type.py
Normal file
15
backend/system/views/dict_type.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from rest_framework import serializers, viewsets
|
||||
from system.models import DictType
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class DictTypeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DictType
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class DictTypeViewSet(CustomModelViewSet):
|
||||
queryset = DictType.objects.filter(is_deleted=False)
|
||||
serializer_class = DictTypeSerializer
|
||||
131
backend/system/views/menu.py
Normal file
131
backend/system/views/menu.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import viewsets, serializers, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from system.models import Menu, MenuMeta
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class MenuMetaSerializer(serializers.ModelSerializer):
|
||||
"""菜单元数据序列化器"""
|
||||
class Meta:
|
||||
model = MenuMeta
|
||||
fields = '__all__'
|
||||
|
||||
class MenuSerializer(serializers.ModelSerializer):
|
||||
"""菜单序列化器"""
|
||||
parent = serializers.CharField(source='pid.name', read_only=True)
|
||||
meta = MenuMetaSerializer()
|
||||
children = serializers.SerializerMethodField()
|
||||
status_text = serializers.SerializerMethodField()
|
||||
type_text = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'create_time', 'update_time']
|
||||
|
||||
def get_children(self, obj):
|
||||
"""获取子菜单"""
|
||||
children = obj.children.all()
|
||||
if children:
|
||||
return MenuSerializer(children, many=True).data
|
||||
return []
|
||||
|
||||
def get_status_text(self, obj):
|
||||
"""获取状态文本"""
|
||||
return obj.get_status_display()
|
||||
|
||||
def get_type_text(self, obj):
|
||||
"""获取菜单类型文本"""
|
||||
return obj.get_type_display()
|
||||
|
||||
def create(self, validated_data):
|
||||
"""创建菜单及关联的元数据"""
|
||||
meta_data = validated_data.pop('meta')
|
||||
meta = MenuMeta.objects.create(**meta_data)
|
||||
menu = Menu.objects.create(meta=meta, **validated_data)
|
||||
return menu
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""更新菜单及关联的元数据"""
|
||||
meta_data = validated_data.pop('meta', {})
|
||||
meta_serializer = self.fields['meta']
|
||||
meta_serializer.update(instance.meta, meta_data)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
|
||||
class MenuMetaViewSet(viewsets.ModelViewSet):
|
||||
"""菜单元数据视图集"""
|
||||
queryset = MenuMeta.objects.all()
|
||||
serializer_class = MenuMetaSerializer
|
||||
|
||||
|
||||
class MenuViewSet(CustomModelViewSet):
|
||||
"""菜单管理视图集"""
|
||||
queryset = Menu.objects.filter(pid__isnull=True).order_by('id', 'status')
|
||||
serializer_class = MenuSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['status', 'type', 'pid', 'name']
|
||||
search_fields = ['name', 'path', 'auth_code']
|
||||
ordering_fields = ['meta__order', 'create_time']
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def tree(self, request):
|
||||
"""获取菜单树形结构"""
|
||||
queryset = self.get_queryset().filter(pid__isnull=True)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='name-exists')
|
||||
def name_exists(self, request):
|
||||
return self._build_response()
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='name-search')
|
||||
def name_search(self, request):
|
||||
name = request.GET.get('name')
|
||||
pk = request.GET.get('id', None)
|
||||
queryset = Menu.objects.all()
|
||||
if pk:
|
||||
queryset = queryset.exclude(pk=pk)
|
||||
if name:
|
||||
queryset = queryset.filter(name=name)
|
||||
has_menu_name = queryset.exists()
|
||||
print(has_menu_name, 'has_menu_name')
|
||||
return self._build_response(data=has_menu_name)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='path-exists')
|
||||
def path_exists(self, request):
|
||||
return self._build_response()
|
||||
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
pk = kwargs['pk']
|
||||
instance = Menu.objects.get(pk=pk)
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
|
||||
if getattr(instance, '_prefetched_objects_cache', None):
|
||||
# If 'prefetch_related' has been applied to a queryset, we need to
|
||||
# forcibly invalidate the prefetch cache on the instance.
|
||||
instance._prefetched_objects_cache = {}
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
pk = kwargs['pk']
|
||||
instance = Menu.objects.get(pk=pk)
|
||||
self.perform_destroy(instance)
|
||||
return self._build_response(
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
92
backend/system/views/role.py
Normal file
92
backend/system/views/role.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import status, serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
|
||||
from system.models import RolePermission, Menu, Role
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class RoleSerializer(serializers.ModelSerializer):
|
||||
"""角色序列化器"""
|
||||
permissions = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Menu.objects.all(),
|
||||
many=True,
|
||||
required=False
|
||||
)
|
||||
status_text = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'create_time']
|
||||
|
||||
def get_status_text(self, obj):
|
||||
"""获取状态文本"""
|
||||
return obj.get_status_display()
|
||||
|
||||
|
||||
|
||||
class RoleViewSet(CustomModelViewSet):
|
||||
"""角色管理视图集"""
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['status']
|
||||
search_fields = ['name']
|
||||
ordering_fields = ['create_time']
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def assign_permissions(self, request, pk=None):
|
||||
"""分配角色权限"""
|
||||
role = self.get_object()
|
||||
menu_ids = request.data.get('menu_ids', [])
|
||||
|
||||
# 清除原有权限
|
||||
role.permissions.clear()
|
||||
|
||||
# 添加新权限
|
||||
for menu_id in menu_ids:
|
||||
menu = get_object_or_404(Menu, id=menu_id)
|
||||
RolePermission.objects.create(role=role, menu=menu)
|
||||
|
||||
serializer = self.get_serializer(role)
|
||||
return Response(serializer.data)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
# 获取请求数据
|
||||
data = request.data.copy()
|
||||
permissions = data.pop('permissions', []) # 提取权限列表
|
||||
|
||||
# 创建角色(不包含权限)
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
role = serializer.save()
|
||||
|
||||
# 处理权限关联(可根据需求自定义)
|
||||
if permissions:
|
||||
try:
|
||||
# 验证权限ID是否存在
|
||||
valid_permissions = Menu.objects.filter(id__in=permissions)
|
||||
|
||||
# 创建中间表记录(如果需要保存额外字段)
|
||||
for menu in valid_permissions:
|
||||
RolePermission.objects.create(
|
||||
role=role,
|
||||
menu=menu,
|
||||
)
|
||||
except Exception as e:
|
||||
# 如果关联失败,删除已创建的角色
|
||||
role.delete()
|
||||
return Response(
|
||||
{'error': f'权限关联失败: {str(e)}'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
79
backend/system/views/user.py
Normal file
79
backend/system/views/user.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.authtoken.views import ObtainAuthToken
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from system.models import User
|
||||
from utils.custom_model_viewSet import CustomModelViewSet
|
||||
|
||||
|
||||
class UserSerializer(CustomModelViewSet):
|
||||
class Meta:
|
||||
model = User
|
||||
exclude = ('password',)
|
||||
|
||||
|
||||
class UserLogin(ObtainAuthToken):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.serializer_class(data=request.data,
|
||||
context={'request': request})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = serializer.validated_data['user']
|
||||
token, created = Token.objects.get_or_create(user=user)
|
||||
return Response({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": user.id,
|
||||
"password": user.password,
|
||||
"realName": user.nickname,
|
||||
"roles": [
|
||||
"super"
|
||||
],
|
||||
"username": user.username,
|
||||
"accessToken": token.key
|
||||
},
|
||||
"error": None,
|
||||
"message": "ok"
|
||||
})
|
||||
|
||||
|
||||
class UserInfo(APIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user = self.request.user
|
||||
return Response({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": user.id,
|
||||
"realName": user.username,
|
||||
"roles": [
|
||||
"super"
|
||||
],
|
||||
"username": user.username,
|
||||
},
|
||||
"error": None,
|
||||
"message": "ok"
|
||||
})
|
||||
|
||||
|
||||
class Codes(APIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response({
|
||||
"code": 0,
|
||||
"data": [
|
||||
"AC_100100",
|
||||
"AC_100110",
|
||||
"AC_100120",
|
||||
"AC_100010"
|
||||
],
|
||||
"error": None,
|
||||
"message": "ok"
|
||||
})
|
||||
|
||||
|
||||
class UserViewSet(ModelViewSet):
|
||||
queryset = User.objects.all().order_by('id')
|
||||
serializer_class = UserSerializer
|
||||
7
backend/utils/authentication.py
Normal file
7
backend/utils/authentication.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
|
||||
class BearerTokenAuthentication(TokenAuthentication):
|
||||
"""
|
||||
使用 'Bearer' 前缀的 Token 认证
|
||||
"""
|
||||
keyword = 'Bearer'
|
||||
149
backend/utils/custom_model_viewSet.py
Normal file
149
backend/utils/custom_model_viewSet.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
class CustomModelViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
自定义ModelViewSet,提供以下增强功能:
|
||||
- 基于动作的序列化器选择
|
||||
- 基于动作的权限控制
|
||||
- 标准化响应格式
|
||||
- 软删除支持
|
||||
- 批量操作支持
|
||||
"""
|
||||
# 动作到序列化器类的映射
|
||||
action_serializers = {}
|
||||
# 动作到权限类的映射
|
||||
action_permissions = {}
|
||||
# 软删除字段名
|
||||
soft_delete_field = 'is_deleted'
|
||||
# 是否支持软删除
|
||||
enable_soft_delete = False
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""根据当前动作获取序列化器类"""
|
||||
return self.action_serializers.get(
|
||||
self.action,
|
||||
super().get_serializer_class()
|
||||
)
|
||||
|
||||
def get_permissions(self):
|
||||
"""根据当前动作获取权限类"""
|
||||
permissions = self.action_permissions.get(
|
||||
self.action,
|
||||
self.permission_classes
|
||||
)
|
||||
return [permission() for permission in permissions]
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""重写列表视图,支持软删除过滤"""
|
||||
queryset = self.get_queryset()
|
||||
|
||||
# 应用软删除过滤
|
||||
if self.enable_soft_delete:
|
||||
queryset = queryset.filter(**{self.soft_delete_field: False})
|
||||
|
||||
# 应用搜索和过滤
|
||||
queryset = self.filter_queryset(queryset)
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""重写详情视图,支持软删除检查"""
|
||||
instance = self.get_object()
|
||||
|
||||
# 检查软删除状态
|
||||
if (self.enable_soft_delete and
|
||||
hasattr(instance, self.soft_delete_field) and
|
||||
getattr(instance, self.soft_delete_field)):
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = self.get_serializer(instance)
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="Object retrieved successfully",
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""重写创建视图,支持批量创建"""
|
||||
is_many = isinstance(request.data, list)
|
||||
|
||||
if is_many:
|
||||
serializer = self.get_serializer(data=request.data, many=True)
|
||||
else:
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
self.perform_destroy(instance)
|
||||
return self._build_response(
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
|
||||
if getattr(instance, '_prefetched_objects_cache', None):
|
||||
# If 'prefetch_related' has been applied to a queryset, we need to
|
||||
# forcibly invalidate the prefetch cache on the instance.
|
||||
instance._prefetched_objects_cache = {}
|
||||
return self._build_response(
|
||||
data=serializer.data,
|
||||
message="ok",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def _build_response(self, code=0, message="成功", data=None, status=status.HTTP_200_OK):
|
||||
"""
|
||||
构建标准化API响应格式
|
||||
|
||||
参数说明:
|
||||
- code: 业务状态码(0表示成功,非0表示错误)
|
||||
- message: 状态描述信息
|
||||
- data: 响应数据(可为None)
|
||||
- status: HTTP状态码(默认200)
|
||||
"""
|
||||
# 构建基础响应结构
|
||||
response_data = {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
|
||||
# 仅当data不为None时添加到响应中
|
||||
if data is not None:
|
||||
response_data["data"] = data
|
||||
|
||||
# 移除可能的空值(如message为空字符串)
|
||||
response_data = {k: v for k, v in response_data.items() if v is not None and v != ""}
|
||||
|
||||
# 返回DRF的Response对象
|
||||
return Response(
|
||||
data=response_data,
|
||||
status=status,
|
||||
content_type="application/json"
|
||||
)
|
||||
20
backend/utils/models.py
Normal file
20
backend/utils/models.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@Remark: 公共基础model类
|
||||
"""
|
||||
from django.db import models
|
||||
|
||||
class CoreModel(models.Model):
|
||||
remark = models.CharField(max_length=256, verbose_name="备注", null=True, blank=True, help_text="备注")
|
||||
creator = models.CharField(max_length=64, null=True, blank=True, help_text="创建人", verbose_name="创建人")
|
||||
modifier = models.CharField(max_length=64, null=True, blank=True, help_text="修改人", verbose_name="修改人")
|
||||
update_time = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间", verbose_name="修改时间")
|
||||
create_time = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
|
||||
verbose_name="创建时间")
|
||||
is_deleted = models.BooleanField(default=False, verbose_name='是否软删除')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = '核心模型'
|
||||
verbose_name_plural = verbose_name
|
||||
56
backend/utils/pagination.py
Normal file
56
backend/utils/pagination.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core import paginator
|
||||
from django.core.paginator import Paginator as DjangoPaginator
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.response import Response
|
||||
from django.core.paginator import InvalidPage
|
||||
|
||||
|
||||
class CustomPagination(PageNumberPagination):
|
||||
page_size = 20
|
||||
page_size_query_param = "pageSize"
|
||||
max_page_size = 999
|
||||
django_paginator_class = DjangoPaginator
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
"""
|
||||
重写paginate_queryset让分页超过正常分页:有原来的4000错误无效页面。改写为返回2000成功,data=[]提示
|
||||
"""
|
||||
page_size = self.get_page_size(request)
|
||||
if not page_size:
|
||||
return None
|
||||
paginator = self.django_paginator_class(queryset, page_size)
|
||||
page_number = self.get_page_number(request, paginator)
|
||||
try:
|
||||
self.page = paginator.page(page_number)
|
||||
except InvalidPage as exc:
|
||||
self.page = []
|
||||
|
||||
if paginator.num_pages > 1 and self.template is not None:
|
||||
# The browsable API should display pagination controls.
|
||||
self.display_page_controls = True
|
||||
|
||||
self.request = request
|
||||
return list(self.page)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
code = 0
|
||||
msg = 'ok'
|
||||
total = self.page.paginator.count if self.page else 0
|
||||
res = {
|
||||
"total": total,
|
||||
"items": data
|
||||
}
|
||||
if not data:
|
||||
code = 0
|
||||
msg = "暂无数据"
|
||||
res['data'] = []
|
||||
|
||||
return Response(OrderedDict([
|
||||
('code', code),
|
||||
('message', msg),
|
||||
('data', res),
|
||||
('error', None),
|
||||
]))
|
||||
8
backend/utils/permissions.py
Normal file
8
backend/utils/permissions.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
class IsSuperUserOrReadOnly(permissions.BasePermission):
|
||||
"""超级用户可读写,普通用户只读"""
|
||||
def has_permission(self, request, view):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
return request.user and request.user.is_superuser
|
||||
118
backend/utils/serializers.py
Normal file
118
backend/utils/serializers.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
@Remark: 自定义序列化器
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from django.utils.functional import cached_property
|
||||
from rest_framework.utils.serializer_helpers import BindingDict
|
||||
|
||||
from system.models import User
|
||||
|
||||
|
||||
class CustomModelSerializer(ModelSerializer):
|
||||
"""
|
||||
增强DRF的ModelSerializer,可自动更新模型的审计字段记录
|
||||
(1)self.request能获取到rest_framework.request.Request对象
|
||||
"""
|
||||
# 修改人的审计字段名称, 默认modifier, 继承使用时可自定义覆盖
|
||||
modifier_field_id = 'modifier'
|
||||
modifier_name = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_modifier_name(self, instance):
|
||||
if not hasattr(instance, 'modifier'):
|
||||
return None
|
||||
queryset = User.objects.filter(id=instance.modifier).values_list('name', flat=True).first()
|
||||
if queryset:
|
||||
return queryset
|
||||
return None
|
||||
|
||||
# 创建人的审计字段名称, 默认creator, 继承使用时可自定义覆盖
|
||||
creator_field_id = 'creator'
|
||||
# 添加默认时间返回格式
|
||||
create_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", required=False, read_only=True)
|
||||
update_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", required=False)
|
||||
|
||||
def __init__(self, instance=None, data=empty, request=None, **kwargs):
|
||||
super().__init__(instance, data, **kwargs)
|
||||
self.request: Request = request or self.context.get('request', None)
|
||||
|
||||
def save(self, **kwargs):
|
||||
return super().save(**kwargs)
|
||||
|
||||
def create(self, validated_data):
|
||||
if self.request:
|
||||
if self.modifier_field_id in self.fields.fields:
|
||||
validated_data[self.modifier_field_id] = self.get_request_username()
|
||||
if self.creator_field_id in self.fields.fields:
|
||||
validated_data[self.creator_field_id] = self.get_request_username()
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if self.request:
|
||||
if hasattr(self.instance, self.modifier_field_id):
|
||||
self.instance.modifier = self.get_request_username()
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def get_request_username(self):
|
||||
if getattr(self.request, 'user', None):
|
||||
return getattr(self.request.user, 'username', None)
|
||||
return None
|
||||
|
||||
def get_request_name(self):
|
||||
if getattr(self.request, 'user', None):
|
||||
return getattr(self.request.user, 'name', None)
|
||||
return None
|
||||
|
||||
def get_request_user_id(self):
|
||||
if getattr(self.request, 'user', None):
|
||||
return getattr(self.request.user, 'id', None)
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def fields(self):
|
||||
fields = BindingDict(self)
|
||||
for key, value in self.get_fields().items():
|
||||
fields[key] = value
|
||||
|
||||
if not hasattr(self, '_context'):
|
||||
return fields
|
||||
is_root = self.root == self
|
||||
parent_is_list_root = self.parent == self.root and getattr(self.parent, 'many', False)
|
||||
if not (is_root or parent_is_list_root):
|
||||
return fields
|
||||
|
||||
try:
|
||||
request = self.request or self.context['request']
|
||||
except KeyError:
|
||||
return fields
|
||||
params = getattr(
|
||||
request, 'query_params', getattr(request, 'GET', None)
|
||||
)
|
||||
if params is None:
|
||||
pass
|
||||
try:
|
||||
filter_fields = params.get('_fields', None).split(',')
|
||||
except AttributeError:
|
||||
filter_fields = None
|
||||
|
||||
try:
|
||||
omit_fields = params.get('_exclude', None).split(',')
|
||||
except AttributeError:
|
||||
omit_fields = []
|
||||
|
||||
existing = set(fields.keys())
|
||||
if filter_fields is None:
|
||||
allowed = existing
|
||||
else:
|
||||
allowed = set(filter(None, filter_fields))
|
||||
|
||||
omitted = set(filter(None, omit_fields))
|
||||
for field in existing:
|
||||
if field not in allowed:
|
||||
fields.pop(field, None)
|
||||
if field in omitted:
|
||||
fields.pop(field, None)
|
||||
|
||||
return fields
|
||||
30
backend/utils/utils.py
Normal file
30
backend/utils/utils.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
|
||||
def validate_mobile(value):
|
||||
if value and not re.findall(r"1\d{10}", value):
|
||||
raise ValidationError('手机格式不正确')
|
||||
|
||||
|
||||
def validate_amount(value):
|
||||
if value is None:
|
||||
raise ValidationError('金额不能为空')
|
||||
if value and value < 0:
|
||||
raise ValidationError('金额不能为负')
|
||||
|
||||
|
||||
def to_cent(value):
|
||||
if value is None:
|
||||
value = 0
|
||||
return Decimal(value).quantize(Decimal('.01'), rounding=ROUND_HALF_UP)
|
||||
|
||||
# 定义一个小工具:从时间戳转换为 aware datetime(如果时间戳有效)
|
||||
def ts_to_aware(ts):
|
||||
if ts:
|
||||
naive_dt = datetime.fromtimestamp(ts)
|
||||
return timezone.make_aware(naive_dt)
|
||||
return None
|
||||
Reference in New Issue
Block a user