diff --git a/backend/dvadmin/system/urls.py b/backend/dvadmin/system/urls.py index 73f1d92..65c9b31 100644 --- a/backend/dvadmin/system/urls.py +++ b/backend/dvadmin/system/urls.py @@ -30,6 +30,7 @@ system_url.register(r'file', FileViewSet) system_url.register(r'api_white_list', ApiWhiteListViewSet) system_url.register(r'system_config', SystemConfigViewSet) system_url.register(r'message_center', MessageCenterViewSet) +system_url.register(r'datav', DataVViewSet) urlpatterns = [ path('system_config/save_content/', SystemConfigViewSet.as_view({'put': 'save_content'})), @@ -41,6 +42,5 @@ urlpatterns = [ path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), path('clause/privacy.html', PrivacyView.as_view()), path('clause/terms_service.html', TermsServiceView.as_view()), - path('homepage_statistics/', DataVViewSet.as_view({'get': 'homepage_statistics'})), ] urlpatterns += system_url.urls diff --git a/backend/dvadmin/system/views/datav.py b/backend/dvadmin/system/views/datav.py index f406096..f30acd5 100644 --- a/backend/dvadmin/system/views/datav.py +++ b/backend/dvadmin/system/views/datav.py @@ -7,7 +7,9 @@ import json import re import time -from django.db.models import Count +from django.db.models import Count, Sum, Q +from django.db.models.functions import TruncMonth, TruncDay +from django.utils import timezone from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import GenericViewSet @@ -17,6 +19,11 @@ from dvadmin.system.models import Users, LoginLog, FileList from dvadmin.system.views.login_log import LoginLogSerializer from dvadmin.utils.json_response import DetailResponse from django.db import connection +from django.utils.timezone import now +from django.db.models import Count +from django.db.models.functions import TruncDate + +from dvadmin.utils.string_util import format_bytes def jx_timestamp(): @@ -35,93 +42,219 @@ class DataVViewSet(GenericViewSet): ordering_fields = ['create_datetime'] @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) - def homepage_statistics(self, request): - # Users 新增 - # LoginLog # 最后登录 - timestr = jx_timestamp().split(" ") - min_time = datetime.datetime.strptime(timestr[0] + " " + "00:00:00", "%Y-%m-%d %H:%M:%S") - max_time = datetime.datetime.strptime(timestr[0] + " " + "23:59:59", "%Y-%m-%d %H:%M:%S") - # 今日注册 - today_register = Users.objects.filter(create_datetime__gte=min_time, is_superuser=0).count() - # 今日登录 - today_login = len(set(LoginLog.objects.filter(create_datetime__gte=min_time).values_list('username'))) - # 三日新增 - Three_days_register = Users.objects.filter( - create_datetime__gte=min_time - datetime.timedelta(days=3), is_superuser=0).count() - # 七日新增 - Seven_days_register = Users.objects.filter( - create_datetime__gte=min_time - datetime.timedelta(days=7), is_superuser=0).count() - # 七日活跃 - Seven_days_login = len(set(LoginLog.objects.filter( - create_datetime__gte=min_time - datetime.timedelta(days=7)).values_list('username'))) - # 月活跃 - month_login = len(set(LoginLog.objects.filter( - create_datetime__gte=min_time - datetime.timedelta(days=30)).values_list('username'))) - # 七日用户登录数 - sum_days_login_list = [] - for i in range(7): - sum_days_login_list.append({"time": (min_time + datetime.timedelta(days=-i)).strftime("%Y-%m-%d"), - "count": len(set(LoginLog.objects.filter( - create_datetime__lte=max_time - datetime.timedelta(days=i), - create_datetime__gte=min_time - datetime.timedelta(days=i)).values_list( - 'username')))}) + def users_login_total(self, request): + """ + 用户登录总数数据 + :param request: + :return: + """ + login_total = LoginLog.objects.all().count() + return DetailResponse(data={"login_total": login_total}, msg="获取成功") - # 七日注册用户数 - sum_days_register_list = [] - for i in range(7): - sum_days_register_list.append( - {"time": (min_time + datetime.timedelta(days=-i)).strftime("%Y-%m-%d"), "count": Users.objects.filter( - create_datetime__lte=max_time - datetime.timedelta(days=i), - create_datetime__gte=min_time - datetime.timedelta(days=i), is_superuser=0).count()}) - # 用户总数 - sum_register = Users.objects.filter(is_superuser=0).count() - # FileList 附件 - today_f_l = FileList.objects.filter(create_datetime__gte=min_time).count() - sum_f_l = FileList.objects.all().count() - # 今日附件 - today_file = {'count': today_f_l, "occupy_space": 0} - # 总附件 - sum_file = {'count': sum_f_l, "occupy_space": 0} + @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) + def users_total(self, request): + """ + 用户总数 + :param request: + :return: + """ + users_total = Users.objects.all().count() + return DetailResponse(data={"users_total": users_total, }, msg="获取成功") - # 获取游标对象 + @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) + def attachment_total(self, request): + """ + 附件统计数据 + :param request: + :return: + """ + count = FileList.objects.all().count() + data = FileList.objects.aggregate(sum_size=Sum('size')) + return DetailResponse(data={"count": count, "occupy_space": format_bytes(data.get('sum_size'))}, msg="获取成功") - cursor = connection.cursor() + @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) + def database_total(self, request): + """ + 数据库统计数据 + :param request: + :return: + """ + count = len(connection.introspection.table_names()) + database_type = connection.settings_dict['ENGINE'] + sql = None + if 'mysql' in database_type: + sql = "SELECT SUM(data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = DATABASE()" + elif 'postgres' in database_type or 'psqlextra' in database_type: + sql = """SELECT SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename))) AS size FROM pg_tables WHERE schemaname = current_schema();""" + elif 'oracle' in database_type: + sql = "SELECT SUM(bytes) AS size FROM user_segments" + elif 'microsoft' in database_type: + sql = "SELECT SUM(size) * 8 AS size FROM sys.database_files" + else: + space = 0 + if sql: + with connection.cursor() as cursor: + try: + cursor.execute(sql) + result = cursor.fetchone() + space = result[0] + except Exception as e: + print(e) + space = '无权限' + return DetailResponse(data={"count": count, "space": format_bytes(space)}, msg="获取成功") - # 拿到游标对象后执行sql语句 + @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) + def registered_user(self, request): + """ + 用户注册趋势 + :param request: + :return: + """ + today = datetime.datetime.today() + seven_days_ago = today - datetime.timedelta(days=30) - cursor.execute("show tables;") + users = Users.objects.filter(date_joined__gte=seven_days_ago).annotate(day=TruncDay('date_joined')).values( + 'day').annotate(count=Count('id')) - # 获取所有的数据 + result = [] + for i in range(30): + date = (today - datetime.timedelta(days=i)).strftime('%Y-%m-%d') + count = 0 + for user in users: + if user['day'] == date: + count = user['count'] + break + result.append({'day': date, 'count': count}) + + # users_last_month = Users.objects.filter(date_joined__gte=last_month).annotate(day=TruncDate('date_joined')).values('day').annotate(count=Count('id')) + return DetailResponse(data={"registered_user_list": result}, msg="获取成功") + + @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) + def registered_user(self, request): + """ + 用户注册趋势 + :param request: + :return: + """ + day = 30 + today = datetime.datetime.today() + seven_days_ago = today - datetime.timedelta(days=day) + users = Users.objects.filter(create_datetime__gte=seven_days_ago).annotate( + day=TruncDay('create_datetime')).values( + 'day').annotate(count=Count('id')) + result = [] + data_dict = {ele.get('day').strftime('%Y-%m-%d'): ele.get('count') for ele in users} + for i in range(day): + date = (today - datetime.timedelta(days=i)).strftime('%Y-%m-%d') + result.append({'day': date, 'count': data_dict[date] if date in data_dict else 0}) + return DetailResponse(data={"registered_user_list": result}, msg="获取成功") + + @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) + def login_user(self, request): + """ + 用户登录趋势 + :param request: + :return: + """ + day = 30 + today = datetime.datetime.today() + seven_days_ago = today - datetime.timedelta(days=day) + users = LoginLog.objects.filter(create_datetime__gte=seven_days_ago).annotate( + day=TruncDay('create_datetime')).values( + 'day').annotate(count=Count('id')) + result = [] + data_dict = {ele.get('day').strftime('%Y-%m-%d'): ele.get('count') for ele in users} + for i in range(day): + date = (today - datetime.timedelta(days=i)).strftime('%Y-%m-%d') + result.append({'day': date, 'count': data_dict[date] if date in data_dict else 0}) + return DetailResponse(data={"login_user": result}, msg="获取成功") + + @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) + def users_active(self, request): + """ + 用户新增活跃数据统计 + :param request: + :return: + """ + today = datetime.date.today() + seven_days_ago = today - datetime.timedelta(days=6) + thirty_days_ago = today - datetime.timedelta(days=29) + + today_users = Users.objects.filter(date_joined__date=today).count() + today_logins = Users.objects.filter(last_login__date=today).count() + three_days_users = Users.objects.filter(date_joined__gte=seven_days_ago).count() + seven_days_users = Users.objects.filter(date_joined__gte=thirty_days_ago).count() + seven_days_active = Users.objects.filter(last_login__gte=seven_days_ago).values('last_login').annotate( + count=Count('id', distinct=True)).count() + monthly_active = Users.objects.filter(last_login__gte=thirty_days_ago).values('last_login').annotate( + count=Count('id', distinct=True)).count() - rows = cursor.fetchall() - tables_list = [] - for row in rows: - tables_list.append(row) - # cursor.execute( - # "select table_schema as db, table_name as tb, table_rows as data_rows, index_length / 1024 as 'storage(KB)' from information_schema.tables where table_schema='{}';".format( - # DATABASE_NAME)) - cursor.execute( - "select table_schema as table_db, sum(index_length) as storage,count(table_name ) as tables from information_schema.tables group by table_schema having table_db='{}';".format( - DATABASE_NAME)) - rows = cursor.fetchall() - count = 0 - space = 0 - for row in rows: - count = row[2] - space = round(row[1] / 1024 / 1024, 2) - database_info = {"count": count, "space": space} data = { - "today_register": today_register, - "today_login": today_login, - "Three_days_register": Three_days_register, - "Seven_days_register": Seven_days_register, - "Seven_days_login": Seven_days_login, - "month_login": month_login, - "sum_days_login_list": sum_days_login_list, - "sum_days_register_list": sum_days_register_list, - "sum_register": sum_register, - "today_file": today_file, - "sum_file": sum_file, - "database_info": database_info, + 'today_users': today_users, + 'today_logins': today_logins, + 'three_days': three_days_users, + 'seven_days': seven_days_users, + 'seven_days_active': seven_days_active, + 'monthly_active': monthly_active } return DetailResponse(data=data, msg="获取成功") + + @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated]) + def login_region(self, request): + """ + 登录用户区域分布 + :param request: + :return: + """ + CHINA_PROVINCES = [ + {'name': '北京', 'code': '110000'}, + {'name': '天津', 'code': '120000'}, + {'name': '河北', 'code': '130000'}, + {'name': '山西', 'code': '140000'}, + {'name': '内蒙古', 'code': '150000'}, + {'name': '辽宁', 'code': '210000'}, + {'name': '吉林', 'code': '220000'}, + {'name': '黑龙江', 'code': '230000'}, + {'name': '上海', 'code': '310000'}, + {'name': '江苏', 'code': '320000'}, + {'name': '浙江', 'code': '330000'}, + {'name': '安徽', 'code': '340000'}, + {'name': '福建', 'code': '350000'}, + {'name': '江西', 'code': '360000'}, + {'name': '山东', 'code': '370000'}, + {'name': '河南', 'code': '410000'}, + {'name': '湖北', 'code': '420000'}, + {'name': '湖南', 'code': '430000'}, + {'name': '广东', 'code': '440000'}, + {'name': '广西', 'code': '450000'}, + {'name': '海南', 'code': '460000'}, + {'name': '重庆', 'code': '500000'}, + {'name': '四川', 'code': '510000'}, + {'name': '贵州', 'code': '520000'}, + {'name': '云南', 'code': '530000'}, + {'name': '西藏', 'code': '540000'}, + {'name': '陕西', 'code': '610000'}, + {'name': '甘肃', 'code': '620000'}, + {'name': '青海', 'code': '630000'}, + {'name': '宁夏', 'code': '640000'}, + {'name': '新疆', 'code': '650000'}, + {'name': '台湾', 'code': '710000'}, + {'name': '香港', 'code': '810000'}, + {'name': '澳门', 'code': '820000'}, + {'name': '未知区域', 'code': '000000'}, + ] + provinces = [x['name'] for x in CHINA_PROVINCES] + day = 30 + today = datetime.datetime.today() + seven_days_ago = today - datetime.timedelta(days=day) + province_data = LoginLog.objects.filter(create_datetime__gte=seven_days_ago).values('province').annotate( + count=Count('id')).order_by('-count') + province_dict = {p: 0 for p in provinces} + for ele in province_data: + if ele.get('province') in province_dict: + province_dict[ele.get('province')] += 1 + else: + province_dict['未知区域'] += ele.get('count') + data = [{'region': key, 'count': val} for key, val in province_dict.items()] + data = sorted(data, key=lambda x: x['count'], reverse=True) + return DetailResponse(data=data, msg="获取成功") diff --git a/backend/dvadmin/utils/string_util.py b/backend/dvadmin/utils/string_util.py index d28a948..6efce7a 100644 --- a/backend/dvadmin/utils/string_util.py +++ b/backend/dvadmin/utils/string_util.py @@ -8,6 +8,7 @@ """ import hashlib import random +from decimal import Decimal CHAR_SET = ("2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", @@ -40,3 +41,25 @@ def has_md5(str, salt='123456'): md.update(str.encode()) res = md.hexdigest() return res + + +def format_bytes(size, decimals=2): + """ + 格式化字节大小 + :param size: + :param decimals: + :return: + """ + if isinstance(size, (str)) and size.isnumeric(): + size = int(size) + elif not isinstance(size, (int, float, Decimal)): + return size + if size == 0: + return "0 Bytes" + units = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] + i = 0 + while size >= 1024: + size /= 1024 + i += 1 + + return f"{round(size, decimals)} {units[i]}" diff --git a/web/src/libs/util.js b/web/src/libs/util.js index cf7e6cd..8c65c12 100644 --- a/web/src/libs/util.js +++ b/web/src/libs/util.js @@ -172,5 +172,22 @@ util.ArrayToTree = function (rootList, parentValue, parentName, list) { } return list } +// 格式化字节大小 +util.formatBytes = function (bytes, decimals = 2) { + if (isNaN(bytes)) { + return bytes + } + + if (bytes === 0) { + return '0 Bytes' + } + + const k = 1024 + const dm = decimals < 0 ? 0 : decimals + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] +} export default util diff --git a/web/src/views/dashboard/workbench/components/attachmentTotal.vue b/web/src/views/dashboard/workbench/components/attachmentTotal.vue index 9d0a5ba..987e893 100644 --- a/web/src/views/dashboard/workbench/components/attachmentTotal.vue +++ b/web/src/views/dashboard/workbench/components/attachmentTotal.vue @@ -19,7 +19,7 @@