django 分表设计及兼容 admin 后台

2,257 阅读9分钟

django 分表设计及兼容 admin 后台

其实, django 分表的业务并不是很常见,因为 django 速度上不太适合做大量的数据查询,还有就是现代的数据库(mysqlpg)其实已经很能扛了, 根本不需要分表, 百万级数据以上才可能考虑分表(我瞎说的, 因为还没接触到这种海量数据)。 一些废话:原本这篇博文计划很早了,但是由于种种原因,时间不是很充裕,就一直耽搁了(就是太懒了),终于今晚腾出来一点时间做个回顾(根本原因是,最近在做odoo的技术栈,不是很顺利,回来django找找自信!) 前言:小学生的流水账哦,大佬可以略过。

分表的逻辑

  • 按照固定的 key 进行分表 商品表: 按照商品种类分表: [水果(shop_fruit),蔬菜(shop_vegetable),......] (可能不太准确, 能理解就行)

  • 按照时间维度进行分表 账单详情表按年:[bill_202201, bill_202202, ......] 请求日志表按月:[request_log_2022, request_log_2023, ......]

自定义分表model

class ShareModel(models.Model):
    """
        分表继承的model, 按年份, 按月份, 自定义
    """

    @classmethod
    def get_model(cls, sharing_key: str, verbose=None):
        key = sharing_key.split('=')[-1]

        class Meta:
            managed = False
            db_table = '{}_{}'.format(cls._meta.db_table, key.lower())
            verbose_name = verbose if verbose else f'{cls._meta.verbose_name}({cls.get_shared_keys().get(key)})'
            verbose_name_plural = verbose_name

        attrs = {
            'Meta': Meta,
            '__module__': cls.__module__,
        }
        # 这里会报一个 RuntimeWarning: Model ... was already registered. 没办法去除 生产环境无影响
        return type(''.join([item.title() for item in Meta.db_table.split('_')]), (cls,), attrs)

    @classmethod
    def table_exists(cls, sharing_key) -> bool:
        """
            判断是否已经在数据库
			暂未实现
        :param sharing_key:
        :return:
        """
        # TODO: 确认表是否存在
        return False

    @classmethod
    def get_shared_year(cls, start_year=2020):
        """
            年份: 获取所有分表的年份
        """
        current_year = datetime.today().year
        max = (current_year - start_year) + 1
        year = [current_year - offset for offset in range(max)]
        return year

    @classmethod
    def get_shared_month(cls, first_year=2020, first_month=7):
        """
            所有 分表月份: 基于年份

        :return: list ['202001', '202002', 。。。]
        """
        all_month = []
        years = cls.get_shared_year()
        for year in years:
            if year == first_year:
                start_month = first_month
            else:
                start_month = 1
            range_month = datetime.today().month + 1 if year == datetime.today().year else 13
            month = [str(year)+'0'+str(item) if item < 10 else str(year)+str(item) for item in range(start_month, range_month)]
            all_month.extend(month)

        return all_month

    def delete(self, using=None, keep_parents=False, **kwargs):
        if hasattr(self, 'shared_key'):
            delattr(self, 'shared_key')
        return super().delete()

    @classmethod
    def get_shared_keys(cls) -> dict:
        """
          获取 所有的分表 key;若分表方式不同, 重写此方法
        :return:
        """
        return {item: f'{item}年' for item in cls.get_shared_year()}

    class Meta:
		# 虚拟model, 只有通过调用 get_model 才会实例化出来一个
        abstract = True

使用示例

这里我们选一个中等复杂度的分表,既有固定key及 时间 两个维度综合的分表策略 (其实也不复杂,不过django官方没有相关的方案 就很难受)

这里我们只需要继承上面的 model,重写 get_shared_key 就可以实现动态分表啦


class VideoShareModel(ShareModel):
    """
        Video
        我们假定 有很多不同频台的视频需要存储
        平台:抖音,西瓜,快手
            抖音: 视频量大 需要按月存
            西瓜:视频量小,按年存
            快手:同西瓜
    """
    video_source = models.CharField(max_length=10, blank=True, null=True, verbose_name='视频来源', help_text='视频来源')

    @classmethod
    def get_shared_keys(cls):

        all_platform = (('douyin', '抖音'),
                        ('xigua', '西瓜'),
                        ('kuaishou', '快手'),)

        all_shared_key = {}

        for shared_item in all_platform:
            if shared_item[0].lower() == 'douyin':
                # 抖音 按月存
                shared_time = cls.get_shared_month()
            else:
                # 其他 按年存
                shared_time = cls.get_shared_year(start_year=2020)
            shared_key = {f'{shared_item[0]}_{time_key}': f'{shared_item[1]}_{time_key}' for time_key in shared_time}

            all_shared_key.update(shared_key)

        return all_shared_key

    class Meta:
		# 这里注意下 当前 model 只是虚拟 model
        abstract = True
        db_table = 'video'
        verbose_name = '视频管理'
        verbose_name_plural = verbose_name

# 半自动生成表(运行`python manage.py makemigrations` 及 `python manage.py migrate` 时会自动生成分表)
for k, item in VideoShareModel.get_shared_keys().items():
	VideoShareModel.get_model(k)

分表的orm查询

current_source = 'douyin'
query = VideoShareModel.get_model(current_source).objects.filter(...)
# 其他方法类似

分表的后台展示

首先重写一个 分表的过滤器 因为需要查询的话必须首先得有这个key

class SharedKeyFilter(SimpleListFilter, ABC):
    """
        通用分表的过滤器
        继承 此过滤器 可以在 后台选择对应的分表 进行增删改查
    """

    title = '选择分表'
    parameter_name = 'shared_key'

    def lookups(self, request, model_admin):
        return [(k, v) for k, v in model_admin.model.get_shared_keys().items()]

然后重写一个 model admin的类 用于在后台展示数据

class SharedModelAdmin(ModelAdmin):
    """
        通用的分表 ModelAdmin
        变量命名可能不是很准确
        * source_model:  默认查询的分表 model 初始化查询的 model 必须指定
        * VIRTUAL_SHARE_MODEL: 所有分表 model 的基类 也就是继承 ShareModel 的类名 必须指定
    """
    __url = None
    source_model = None
    VIRTUAL_SHARE_MODEL = None

    # 优化
    show_full_result_count = False

    # @display(description='所属分表') django2.1 没有 这个装饰器
    def shared_key_readonly(self, obj):
        """"""
        if obj:
            vertual_db_table = f'{self.VIRTUAL_SHARE_MODEL.Meta.db_table}_'
            shared_key = obj._meta.db_table.replace(vertual_db_table, '').title()

            return self.VIRTUAL_SHARE_MODEL.get_shared_keys().get(shared_key, None)
        else:
            return '--'

    shared_key_readonly.short_description = '所属分表'

    def get_readonly_fields(self, request, obj=None):
        result = super().get_readonly_fields(request, obj)
        if obj:
            result = list(result)
            result.insert(0, 'shared_key_readonly')
            return set(result)
        return result

    def get_fieldsets(self, request, obj=None):
        """"""
        result = super().get_fieldsets(request, obj)

        if obj:
            result = list(result)
            result.pop(0)
            result.insert(0, ('所属分表', {'fields': ('shared_key_readonly',)}))
        else:
            result = list(result)
            result.insert(0, ('所属分表', {'fields': ('shared_key',)}))
        return result

    def get_queryset(self, request):
        """"""
        if 'shared_key' in request.request_data and len(request.request_data) > 1:
            shared_key = request.request_data.pop('shared_key')
            request.GET._mutable = True
            request.GET.pop('shared_key')
            # self.model = self.VIRTUAL_SHARE_MODEL.get_model(shared_key)

        return super().get_queryset(request)

    def get_form(self, request, obj=None, change=False, **kwargs):
        """
        Return a Form class for use in the admin add view. This is used by
        add_view and change_view.
        """
        self.model = self.source_model
        # 生成一个 假的 数据库字段
        shared_key_filed = CharField(choices=((k, v) for k, v in self.VIRTUAL_SHARE_MODEL.get_shared_keys().items()),
                                     verbose_name='所属分表', help_text='所属分表', name='shared_key')

        # 设置本字段相关值, 骗过 django 底层的字段检测
        attr_dict = {
            'attname': 'shared_key',
            'concrete': True,
            'model': self.model,
            'concrete_model': self.model,
            'column': None,
        }

        for k, v in attr_dict.items():
            setattr(shared_key_filed, k, v)

        # 添加 到 私有字段
        if len(self.model._meta.private_fields) == 0:
            self.model._meta.private_fields.append(shared_key_filed)

        return super().get_form(request, obj, change, **kwargs)

    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
        if add or change:
            self.model = self.source_model
        return super().render_change_form(request, context, add, change, form_url, obj)

    def _changeform_view(self, request, object_id, form_url, extra_context):
        shared_key = request.request_data.get('_changelist_filters', None) or request.request_data.get('shared_key',
                                                                                                       None)
        shared_key = [item for item in shared_key.split('&') if 'shared_key' in
                      item]
        if object_id and shared_key:
            self.model = self.VIRTUAL_SHARE_MODEL.get_model(shared_key[0].split('=')[-1])

        return super()._changeform_view(request, object_id, form_url, extra_context)

    def delete_model(self, request, obj):
        """"""
        obj._meta.model = self.VIRTUAL_SHARE_MODEL.get_model(obj.shared_key)
        obj.delete()
        obj.__delattr__('shared_key')
        obj._meta.model = self.source_model

    def save_model(self, request, obj, form, change):
        """"""
        if change:
            shared_key = request.request_data.get('_changelist_filters', None) or request.request_data.get('shared_key',
                                                                                                           None)

            shared_key = ''.join([item for item in shared_key.split('&') if 'shared_key' in
                                  item])

        else:
            shared_key = form.data.get('shared_key', '')
        obj._meta.model = self.VIRTUAL_SHARE_MODEL.get_model(shared_key.split('=')[-1])
        obj.save()
        obj._meta.model = self.source_model

    def get_urls(self):
        if self.__url:
            return self.__url
        self.__url = super().get_urls()
        return self.__url

    def response_add(self, request, obj, post_url_continue=None):
        self.model = self.source_model
        return super().response_add(request, obj, post_url_continue)

    def response_change(self, request, obj):
        """"""
        self.model = self.source_model
        return super().response_change(request, obj)

使用示例

VideoModel = VideoShareModel.get_model('douyin', '视频信息')  # 默认查询抖音

class VideoListFilter(SharedKeyFilter):
	"""
		video 分表过滤器
	"""
    def queryset(self, request, queryset):
        q = self.value()
        qs = OrderList.get_model(q, ).objects.get_queryset() if q else queryset
        return qs

@register(VideoModel)
class VideoModelAdmin(SharedModelAdmin):
	"""
		video 分表后台展示
	"""
    list_display = (..., )
    search_fields = (...)

    fieldsets = (..., )
    list_filter = (InventoryFilter, )

    source_model = VideoModel
    VIRTUAL_SHARE_MODEL = VideoShareModel

使用以上的方式, 就可以兼容后台的增删改查

其他关于django 后台的适配(就是 魔改AdminModel)

非外键的过滤及展示

比如 有个 商品表, 里面有个 shop_creator_id 但是 这个没有物理外键, 你又想按照 用户名在django后台展示并且选择查询 就可以按照下面的方法

class UserIdNotForeignKeyFilter(SimpleListFilter, ABC):
    """

    """
    title = '选择用户'
    parameter_name = 'user_id_filter'
    user_list: list = None
	# 这里指定当前model中的 user_id 字段值
    user_id_field_name = ''

    @classmethod
    def set_user_list(cls, user_list):
        """
			user_list: [(user_id, user_name)]
        :return:
        """
        return type('UserIdNotForeignKeyFilter', (UserIdNotForeignKeyFilter,), {'user_list': user_list})

    def lookups(self, request, model_admin):
        if not self.user_id_field_name:
            try:
                model_admin.model._meta.get_field('user_id')
                self.user_id_field_name = 'user_id'
            except Exception:
                self.user_id_field_name = 'add_user_id'

        return self.user_list

    def queryset(self, request, queryset):
        """

        :param request:
        :param queryset:
        :return:
        """
        user_id_filter = request.request_data.get(self.parameter_name, '')
        if user_id_filter:
            return queryset.filter(**{self.user_id_field_name: user_id_filter})
        return queryset.filter()

class UserIdNotForeignKeyMinMx(object):
    """
        对于 非外键 的 user_id  进行 处理
    """

    UserList = None

    def user_name(self, obj):
        """
			将 user_id 转换 为 user_name 显示
        :param obj:
        :return:
        """
        user_query = self.UserList
        if user_query:
            user_dict = dict(user_query)
			# 这里 应该是可以优化的 直接使用 user_id = getattr(obj, self.user_id_field_name, None)
			# 很久之前的代码了,需要验证哈
            user_id = getattr(obj, 'user_id', None)
            if not user_id:
                user_id = getattr(obj, 'add_user_id', None)
            return user_dict.get(user_id)

    user_name.short_description = '添加用户名称'

class UserIdNotForeignKeyAdmin(UserIdNotForeignKeyMinMx, ModelAdmin):

    def get_list_filter(self, request):
        self.list_filter = list(super().get_list_filter(request))
        temp = UserIdNotForeignKeyFilter.set_user_list(list(self.UserList))
        for item in self.list_filter:
            if isinstance(item, str):
                continue
            if issubclass(item, UserIdNotForeignKeyFilter):
                return self.list_filter
        self.list_filter.append(temp)
        return self.list_filter

    def get_list_display(self, request):
        self.list_display = list(super().get_list_display(request))
        if 'user_name' not in self.list_display:
            self.list_display.append('user_name')

        return self.list_display

使用示例:

@register(ShopModel)
class ShopModelAdmin(UserIdNotForeignKeyIEAdmin):
    """
        商品表后台展示
    """
	# 这里 因为 django 使用缓存 有可能会导致 新添加的用户 不能及时添加进来,导致不能被搜索及展示
    UserList = UserInfo.objects.filter().values_list('id', 'name')

    list_select_related = (......)
    list_filter = (...... )
    search_fields = (......)
    list_display = (......,  'user_id') # 这里必须添加一个 user_id 否则 获取不到user_id 就无法进行匹配

    show_full_result_count = False

时间戳的过滤及展示

class TimestampFilter(SimpleListFilter, ABC):
    """
        时间戳 过滤
    """
    title = '时间范围'
    parameter_name = 'add_time__range'

    def lookups(self, request, model_admin):
        return [
            (Time.get_day_timestamp_range(-1), '昨天'),
            (Time.get_day_timestamp_range(), '今天'),
            (Time.get_week_timestamp_range(-1), '上周'),
            (Time.get_week_timestamp_range(), '本周'),
            (Time.get_month_timestamp_range(), '本月'),
            (Time.get_month_timestamp_range(-1), '上个月'),
            ((Time.get_month_timestamp_range(-3)[0], Time.get_month_timestamp_range(-1)[1]), '前三个月'),
            (Time.get_year_timestamp_range(), '今年'),
            (Time.get_year_timestamp_range(-1), '去年'),
			# 这里get__timestamp_range` 返回 (int(开始的时间戳), int(结束的时间戳))
        ]

    def queryset(self, request, queryset):
        """
            根据 时间范围筛选 数据
        :param request:
        :param queryset:
        :return:
        """
        add_time__range = request.request_data.get(self.parameter_name, '')
        if add_time__range:
			# 时间太过久远了 这里使用eval的原因已经想不起来了
			# 这里应该是可以优化的 
			# return queryset.filter(**{f'{self.timestamp_filed_name}__range': eval(add_time__range)})
            return queryset.filter(add_time__range=eval(add_time__range))
        return queryset.filter()



class TimestampMixin(object):
    timestamp_filed_name = 'add_time'

    def datetime_format(self, obj):
        return Time.timestamp_format(getattr(obj, self.timestamp_filed_name))

    datetime_format.short_description = '添加时间'

class TimestampAdmin(TimestampMixin, ModelAdmin):

    def get_list_filter(self, request):
        self.list_filter = list(super().get_list_filter(request))
        if TimestampFilter not in self.list_filter:
            self.list_filter.append(TimestampFilter)
        return self.list_filter

    def get_list_display(self, request):
        self.list_display = list(super().get_list_display(request))
        if 'datetime_format' not in self.list_display:
            self.list_display.append('datetime_format')

        return self.list_display

使用示例

@register(TestModel)
class TestTimestampAdmin(TimestampAdmin):
    """
        产品分类管理
    """
	timestamp_filed_name = 'create_timestamp' # 时间戳字段
    list_display = ()

    list_filter = (......)
    search_fields = ('name', )
    show_full_result_count = False

下面是一些自己封装(魔改)的通用类

下面这些 包含了上面说的 非外键字段,时间戳, 导入导出, 分表 展示过滤的通用类(适用于 admin 后台哦 ) 具体使用方法参照上面的两个示例,很简单的。

# -*- coding:utf-8  -*-
# @Time     : 2021/12/17 14:28
# @Author   : BGLB
# @Software : PyCharm
import datetime
from abc import ABC
from urllib import parse

from django.contrib.admin import SimpleListFilter, ModelAdmin
from django.db.models import CharField
from django.utils.translation import gettext_lazy as _
from import_export.admin import ImportMixin, ExportMixin, ExportActionMixin
from simpleui.admin import AjaxAdmin

from utils.util import Time


class SharedKeyFilter(SimpleListFilter, ABC):
    """
        通用分表的过滤器
        继承 此过滤器 可以在 后台选择对应的分表 进行增删改查
    """

    title = '选择分表'
    parameter_name = 'shared_key'

    def lookups(self, request, model_admin):
        return [(k, v) for k, v in model_admin.model.get_shared_keys().items()]


class UserIdNotForeignKeyFilter(SimpleListFilter, ABC):
    """

    """
    title = '选择用户'
    parameter_name = 'user_id_filter'
    user_list: list = None
    user_id_field_name = ''

    @classmethod
    def set_user_list(cls, user_list):
        """

        :return:
        """
        return type('UserIdNotForeignKeyFilter', (UserIdNotForeignKeyFilter,), {'user_list': user_list})

    def lookups(self, request, model_admin):
        if not self.user_id_field_name:
            try:
                model_admin.model._meta.get_field('user_id')
                self.user_id_field_name = 'user_id'
            except Exception:
                self.user_id_field_name = 'add_user_id'

        return self.user_list

    def queryset(self, request, queryset):
        """

        :param request:
        :param queryset:
        :return:
        """
        user_id_filter = request.request_data.get(self.parameter_name, '')
        if user_id_filter:
            return queryset.filter(**{self.user_id_field_name: user_id_filter})
        return queryset.filter()


class TimestampFilter(SimpleListFilter, ABC):
    """
        时间戳 过滤
    """
    title = '时间范围'
    parameter_name = 'add_time__range'

    def lookups(self, request, model_admin):
        return [
            (Time.get_day_timestamp_range(-1), '昨天'),
            (Time.get_day_timestamp_range(), '今天'),
            (Time.get_week_timestamp_range(-1), '上周'),
            (Time.get_week_timestamp_range(), '本周'),
            (Time.get_month_timestamp_range(), '本月'),
            (Time.get_month_timestamp_range(-1), '上个月'),
            ((Time.get_month_timestamp_range(-3)[0], Time.get_month_timestamp_range(-1)[1]), '前三个月'),
            (Time.get_year_timestamp_range(), '今年'),
            (Time.get_year_timestamp_range(-1), '去年'),
        ]

    def queryset(self, request, queryset):
        """
            根据 时间范围筛选 数据
        :param request:
        :param queryset:
        :return:
        """
        add_time__range = request.request_data.get(self.parameter_name, '')
        if add_time__range:
            return queryset.filter(add_time__range=eval(add_time__range))
        return queryset.filter()


class SharedModelAdmin(ModelAdmin):
    """
        通用的分表 ModelAdmin
        变量命名可能不是很准确
        * source_model:  默认查询的分表 model 初始化查询的 model 必须指定
        * VIRTUAL_SHARE_MODEL: 所有分表 model 的基类 也就是继承 ShareModel 的类名 必须指定
    """
    __url = None
    source_model = None
    VIRTUAL_SHARE_MODEL = None

    # 优化
    show_full_result_count = False

    # @display(description='所属分表') django2.1 没有 这个装饰器
    def shared_key_readonly(self, obj):
        """"""
        if obj:
            vertual_db_table = f'{self.VIRTUAL_SHARE_MODEL.Meta.db_table}_'
            shared_key = obj._meta.db_table.replace(vertual_db_table, '').title()

            return self.VIRTUAL_SHARE_MODEL.get_shared_keys().get(shared_key, None)
        else:
            return '--'

    shared_key_readonly.short_description = '所属分表'

    def get_readonly_fields(self, request, obj=None):
        result = super().get_readonly_fields(request, obj)
        if obj:
            result = list(result)
            result.insert(0, 'shared_key_readonly')
            return set(result)
        return result

    def get_fieldsets(self, request, obj=None):
        """"""
        result = super().get_fieldsets(request, obj)

        if obj:
            result = list(result)
            result.pop(0)
            result.insert(0, ('所属分表', {'fields': ('shared_key_readonly',)}))
        else:
            result = list(result)
            result.insert(0, ('所属分表', {'fields': ('shared_key',)}))
        return result

    def get_queryset(self, request):
        """"""
        if 'shared_key' in request.request_data and len(request.request_data) > 1:
            shared_key = request.request_data.pop('shared_key')
            request.GET._mutable = True
            request.GET.pop('shared_key')
            # self.model = self.VIRTUAL_SHARE_MODEL.get_model(shared_key)

        return super().get_queryset(request)

    def get_form(self, request, obj=None, change=False, **kwargs):
        """
        Return a Form class for use in the admin add view. This is used by
        add_view and change_view.
        """
        self.model = self.source_model
        # 生成一个 假的 数据库字段
        shared_key_filed = CharField(choices=((k, v) for k, v in self.VIRTUAL_SHARE_MODEL.get_shared_keys().items()),
                                     verbose_name='所属分表', help_text='所属分表', name='shared_key')

        # 设置本字段相关值, 骗过 django 底层的字段检测
        attr_dict = {
            'attname': 'shared_key',
            'concrete': True,
            'model': self.model,
            'concrete_model': self.model,
            'column': None,
        }

        for k, v in attr_dict.items():
            setattr(shared_key_filed, k, v)

        # 添加 到 私有字段
        if len(self.model._meta.private_fields) == 0:
            self.model._meta.private_fields.append(shared_key_filed)

        return super().get_form(request, obj, change, **kwargs)

    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
        if add or change:
            self.model = self.source_model
        return super().render_change_form(request, context, add, change, form_url, obj)

    def _changeform_view(self, request, object_id, form_url, extra_context):
        shared_key = request.request_data.get('_changelist_filters', None) or request.request_data.get('shared_key',
                                                                                                       None)
        shared_key = [item for item in shared_key.split('&') if 'shared_key' in
                      item]
        if object_id and shared_key:
            self.model = self.VIRTUAL_SHARE_MODEL.get_model(shared_key[0].split('=')[-1])

        return super()._changeform_view(request, object_id, form_url, extra_context)

    def delete_model(self, request, obj):
        """"""
        obj._meta.model = self.VIRTUAL_SHARE_MODEL.get_model(obj.shared_key)
        obj.delete()
        obj.__delattr__('shared_key')
        obj._meta.model = self.source_model

    def save_model(self, request, obj, form, change):
        """"""
        if change:
            shared_key = request.request_data.get('_changelist_filters', None) or request.request_data.get('shared_key',
                                                                                                           None)

            shared_key = ''.join([item for item in shared_key.split('&') if 'shared_key' in
                                  item])

        else:
            shared_key = form.data.get('shared_key', '')
        obj._meta.model = self.VIRTUAL_SHARE_MODEL.get_model(shared_key.split('=')[-1])
        obj.save()
        obj._meta.model = self.source_model

    def get_urls(self):
        if self.__url:
            return self.__url
        self.__url = super().get_urls()
        return self.__url

    def response_add(self, request, obj, post_url_continue=None):
        self.model = self.source_model
        return super().response_add(request, obj, post_url_continue)

    def response_change(self, request, obj):
        """"""
        self.model = self.source_model
        return super().response_change(request, obj)


class UserIdNotForeignKeyMinMx(object):
    """
        对于 非外键 的 user_id  进行 处理
    """

    UserList = None

    def user_name(self, obj):
        """

        :param obj:
        :return:
        """
        user_query = self.UserList
        if user_query:
            user_dict = dict(user_query)
            user_id = getattr(obj, 'user_id', None)
            if not user_id:
                user_id = getattr(obj, 'add_user_id', None)
            return user_dict.get(user_id)

    user_name.short_description = '添加用户名称'


class TimestampMixin(object):
    timestamp_filed_name = 'add_time'

    def datetime_format(self, obj):
        return Time.timestamp_format(getattr(obj, self.timestamp_filed_name))

    datetime_format.short_description = '添加时间'


class UserIdNotForeignKeyAdmin(UserIdNotForeignKeyMinMx, ModelAdmin):

    def get_list_filter(self, request):
        self.list_filter = list(super().get_list_filter(request))
        temp = UserIdNotForeignKeyFilter.set_user_list(list(self.UserList))
        for item in self.list_filter:
            if isinstance(item, str):
                continue
            if issubclass(item, UserIdNotForeignKeyFilter):
                return self.list_filter
        self.list_filter.append(temp)
        return self.list_filter

    def get_list_display(self, request):
        self.list_display = list(super().get_list_display(request))
        if 'user_name' not in self.list_display:
            self.list_display.append('user_name')

        return self.list_display


class TimestampAdmin(TimestampMixin, ModelAdmin):

    def get_list_filter(self, request):
        self.list_filter = list(super().get_list_filter(request))
        if TimestampFilter not in self.list_filter:
            self.list_filter.append(TimestampFilter)
        return self.list_filter

    def get_list_display(self, request):
        self.list_display = list(super().get_list_display(request))
        if 'datetime_format' not in self.list_display:
            self.list_display.append('datetime_format')

        return self.list_display


class TimestampUserIdNotForeignKeyAdmin(TimestampMixin, UserIdNotForeignKeyMinMx, ModelAdmin):
    """
        适用于
        1. user_id 不是真实外键
        2. 有时间字段是是时间戳格式

    """
    def get_list_filter(self, request):
        self.list_filter = list(super().get_list_filter(request))
        if TimestampFilter not in self.list_filter:
            self.list_filter.append(TimestampFilter)
        temp = UserIdNotForeignKeyFilter.set_user_list(list(self.UserList))
        for item in self.list_filter:
            if isinstance(item, str):
                continue
            if issubclass(item, UserIdNotForeignKeyFilter):
                return self.list_filter
        self.list_filter.append(temp)

        return self.list_filter

    def get_list_display(self, request):
        self.list_display = list(super().get_list_display(request))

        if 'datetime_format' not in self.list_display:
            self.list_display.append('datetime_format')

        if 'user_name' not in self.list_display:
            self.list_display.append('user_name')

        return self.list_display


class ImportExportActioinMixin(ImportMixin, ExportMixin):
    """
        导入导出 Mixin
    """
    def get_export_filename(self, request, queryset, file_format):
        """

        :param request:
        :param queryset:
        :param file_format:
        :return:
        """
        add_time_range = request.request_data.get('add_time__range', [])
        if add_time_range:
            add_time_range = eval(add_time_range)
            date_str_start = datetime.datetime.fromtimestamp(add_time_range[0]).strftime('%Y-%m-%d')
            date_str_end = datetime.datetime.fromtimestamp(add_time_range[1]).strftime('%Y-%m-%d')
            date_str = '-({0}_{1})'.format(date_str_start, date_str_end)
        else:
            date_str = ''
        filename = "%s%s.%s"%(self.model._meta.verbose_name,
                               date_str,
                               file_format.get_extension())
        return parse.quote(filename)

    def get_actions(self, request):
        actions = super().get_actions(request)
        if self.has_export_permission(request):
            actions.update(
                export_admin_action=(
                    ExportActionMixin.export_admin_action,
                    "export_admin_action",
                    _("Export selected %(verbose_name_plural)s"),
                )
            )
            return actions
        return actions


class ImportExportAdmin(ImportExportActioinMixin, ModelAdmin):
    """
        导入导出继承类
    """
    pass


class ImportExportAjaxAdmin(ImportExportActioinMixin, AjaxAdmin):
    pass


class TimestampIEAdmin(ImportExportActioinMixin, TimestampAdmin):
    """

    """
    pass


class UserIdNotForeignKeyIEAdmin(ImportExportActioinMixin, UserIdNotForeignKeyAdmin):
    """
        适用于 user_id 没有真实外键关系的 导入导出
    """
    pass


class TimestampUserIdNotForeignKeyIEAdmin(ImportExportActioinMixin, TimestampUserIdNotForeignKeyAdmin):
    pass

结语

啊!这项任务终于是完成了,感觉找回了一点自信,回顾之前的一些代码,还是有用的,可以弥补之前的一些逻辑缺陷及设计缺陷!