diff --git a/applications/common/admin.py b/applications/common/admin.py index 1b7155e..4550d7d 100644 --- a/applications/common/admin.py +++ b/applications/common/admin.py @@ -2,7 +2,7 @@ from io import BytesIO from flask import session, make_response -from applications.common.utils.gen_captcha import vieCode +from applications.common.utils.captcha import vieCode # 生成验证码 diff --git a/applications/common/utils/cache.py b/applications/common/utils/cache.py new file mode 100644 index 0000000..cc130f8 --- /dev/null +++ b/applications/common/utils/cache.py @@ -0,0 +1,56 @@ +import time + +cache_dict = {} + + +def cache_set_internal(key, value, expired=5): + """ + 程序内部实现的记录缓存,用于简单、体量不大的缓存记录,在程序结束后销毁。对于高速、体量大的环境请配置 Redis 等服务自行记录。 + 记录缓存,存储键值对,并记录当前时间作为缓存的时间戳。 + + :param key: 键 + :param value: 值 + :param expired: 过期时间(秒),默认5秒 + """ + cache_dict[key] = { + 'value': value, + 'expired_time': time.time() + expired + } + + +def cache_get_internal(key): + """ + 获取缓存,根据键从缓存中获取值,并检查是否过期。 + + :param key: 键 + :return: 如果缓存存在且未过期,返回缓存的值;否则返回 None + """ + if key in cache_dict: + cache_item = cache_dict[key] + if time.time() < cache_item['expired_time']: + return cache_item['value'] + else: + # 如果缓存已过期,删除该缓存 + del cache_dict[key] + return None + + +def cache_auto_internal(key, call, expired=5): + """ + 如果缓存存在直接返回缓存内容,缓存不存在或者过期执行 call 函数,并取得返回值记录并返回。 + + :param key: 键 + :param call: 获取新值的地方 + :param expired: 过期时间(秒),默认5秒 + """ + + data = cache_get_internal(key) + + if data is not None: + return data + + data = call() + cache_set_internal(key, data, expired) + + return data + diff --git a/applications/common/utils/gen_captcha.py b/applications/common/utils/captcha.py similarity index 98% rename from applications/common/utils/gen_captcha.py rename to applications/common/utils/captcha.py index c72738c..83145b5 100644 --- a/applications/common/utils/gen_captcha.py +++ b/applications/common/utils/captcha.py @@ -14,7 +14,7 @@ class vieCode: __inCurve = True # 是否画干扰线 __inNoise = True # 是否画干扰点 __type = 2 # 验证码类型 1、纯字母 2、数字字母混合 - __fontPatn = 'applications/common/utils/fonts/1.ttf' # 字体 + __fontPatn = 'applications/common/utils/fonts/captcha.ttf' # 字体 def GetCodeImage(self, size=80, length=4): '''获取验证码图片 diff --git a/applications/common/utils/fonts/1.ttf b/applications/common/utils/fonts/captcha.ttf similarity index 100% rename from applications/common/utils/fonts/1.ttf rename to applications/common/utils/fonts/captcha.ttf diff --git a/applications/common/utils/validate.py b/applications/common/utils/validate.py index 882baf5..8d90948 100644 --- a/applications/common/utils/validate.py +++ b/applications/common/utils/validate.py @@ -11,7 +11,7 @@ def str_escape(s): between = validators.between -''' +""" 验证数字是否介于最小值和/或最大值之间。 这将适用于任何类似的类型,如浮点数、小数和日期,而不仅仅是整数。 between(value, min=None, max=None) @@ -35,10 +35,10 @@ between(value, min=None, max=None) ... min=datetime(1999, 11, 11) ... ) True -''' +""" domain = validators.domain -''' +""" 返回给定值是否为有效域 如果值是有效域名,则此函数返回 True ,否则返回 ValidationFailure domain(value) @@ -49,10 +49,10 @@ domain(value) >>> domain('example.com/') ValidationFailure(func=domain, ...) -''' +""" email = validators.email -''' +""" 验证电子邮件地址。验证成功时返回 True ,验证失败时返回 >>> email('someone@example.com') @@ -60,10 +60,10 @@ email = validators.email >>> email('bogus@@') ValidationFailure(func=email, ...) -''' +""" iban = validators.iban -''' +""" 返回给定值是否为有效的IBAN代码。 如果值是有效的IBAN,则此函数返回 True ,否则返回 ValidationFailure 。 @@ -72,10 +72,10 @@ iban = validators.iban >>> iban('123456') ValidationFailure(func=iban, ...) -''' +""" ipv4 = validators.ipv4 -''' +""" 返回给定值是否为有效的IPv4地址。 >>> ipv4('123.0.0.7') @@ -83,20 +83,20 @@ ipv4 = validators.ipv4 >>> ipv4('900.80.70.11') ValidationFailure(func=ipv4, args={'value': '900.80.70.11'}) -''' +""" ipv6 = validators.ipv6 -''' +""" 返回给定值是否为有效的IP版本6地址。 >>> ipv6('abcd:ef::42:1') True >>> ipv6('abc.0.0.1') ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'}) -''' +""" length = validators.length -''' +""" 返回给定字符串的长度是否在指定范围内。 >>> length('something', min=2) True @@ -106,10 +106,10 @@ length = validators.length >>> length('something', max=5) ValidationFailure(func=length, ...) -''' +""" mac_address = validators.mac_address -''' +""" 返回给定值是否为有效MAC地址。 如果该值是有效的MAC地址,则此函数返回 True ,否则返回 ValidationFailure 。 @@ -118,10 +118,10 @@ mac_address = validators.mac_address >>> mac_address('00:00:00:00:00') ValidationFailure(func=mac_address, args={'value': '00:00:00:00:00'}) -''' +""" slug = validators.slug -''' +""" 验证给定值是否为有效的块。 有效的短信息只能包含字母数字字符、连字符和下划线。 >>> slug('my.slug') @@ -129,15 +129,15 @@ slug = validators.slug >>> slug('my-slug-2134') True -''' +""" #truthy = validators.truthy -''' +""" 验证给定值不是错误值。 -''' +""" url = validators.url -''' +""" 返回给定值是否为有效URL。 如果值是有效URL,则此函数返回 True ,否则返回 ValidationFailure 。 @@ -152,10 +152,10 @@ url = validators.url >>> url('http://10.0.0.1', public=True) ValidationFailure(func=url, ...) -''' +""" uuid = validators.uuid -''' +""" 返回给定值是否为有效UUID。 如果值是有效的UUID,则此函数返回 True ,否则返回 ValidationFailure 。 @@ -164,7 +164,7 @@ uuid = validators.uuid >>> uuid('2bc1c94f 0deb-43e9-92a1-4775189ec9f8') ValidationFailure(func=uuid, ...) -''' +""" @validator @@ -172,7 +172,7 @@ def even(value): return not (value % 2) -''' +""" 一个装饰器,它使给定的函数验证器 每当给定函数被调用并返回 False 值时,这个装饰器返回 ValidationFailure 对象。 >>> @validator @@ -184,4 +184,4 @@ True >>> even(5) ValidationFailure(func=even, args={'value': 5}) -''' +""" diff --git a/applications/view/system/monitor.py b/applications/view/system/monitor.py index 972d49c..ac295d2 100644 --- a/applications/view/system/monitor.py +++ b/applications/view/system/monitor.py @@ -1,47 +1,33 @@ import os -import platform import re import sys import time +import psutil +import platform + from datetime import datetime -import psutil -from flask import Blueprint, render_template, jsonify +from flask import Blueprint, render_template +from applications.common.utils.http import table_api, success_api from applications.common.utils.rights import authorize +from applications.common.utils.cache import cache_auto_internal + bp = Blueprint('adminMonitor', __name__, url_prefix='/monitor') -# 系统监控 -@bp.get('/') -@authorize("system:monitor:main") -def main(): - # 主机名称 - hostname = platform.node() - # 系统版本 - system_version = platform.platform() - # python版本 - python_version = platform.python_version() - # 逻辑cpu数量 - cpu_count = psutil.cpu_count() - # cpu使用率 - cpus_percent = psutil.cpu_percent(interval=0.1, percpu=False) # percpu 获取主使用率 - # 内存 - memory_information = psutil.virtual_memory() - # 内存使用率 - memory_usage = memory_information.percent - memory_used: int = memory_information.used - memory_total: int = memory_information.total - memory_free: int = memory_information.free - # 磁盘信息 - +def get_disk_partitions_list(): disk_partitions_list = [] # 判断是否在容器中 if not os.path.exists('/.dockerenv'): disk_partitions = psutil.disk_partitions() for i in disk_partitions: - a = psutil.disk_usage(i.device) + try: + a = psutil.disk_usage(i.device) + except PermissionError: + continue + disk_partitions_dict = { 'device': i.device, 'fstype': i.fstype, @@ -51,30 +37,58 @@ def main(): 'percent': a.percent } disk_partitions_list.append(disk_partitions_dict) + else: + try: + usage = psutil.disk_usage('/') + except PermissionError: + pass + + disk_partitions_list.append({ + 'device': '/', # 设备名称(根文件系统) + 'fstype': psutil.disk_partitions()[0].fstype, # 文件系统类型 + 'total': usage.total, # 总容量(字节) + 'used': usage.used, # 已用空间(字节) + 'free': usage.free, # 可用空间(字节) + 'percent': usage.percent # 使用百分比 + }) + + return disk_partitions_list + +def get_basic_info(): + # 主机名称 + hostname = platform.node() + # 系统版本 + system_version = platform.platform() + # Python 版本 + python_version = platform.python_version() # 开机时间 boot_time = datetime.fromtimestamp(psutil.boot_time()).replace(microsecond=0) up_time = datetime.now().replace(microsecond=0) - boot_time up_time_list = re.split(r':', str(up_time)) - up_time_format = " {} 小时{} 分钟{} 秒".format(up_time_list[0], up_time_list[1], up_time_list[2]) + up_time_format = "{} 小时 {} 分钟 {} 秒".format(up_time_list[0], up_time_list[1], up_time_list[2]) + up_time_format = up_time_format.replace("days,", "天") + + return { + 'hostname': hostname, + 'system_version': system_version, + 'python_version': python_version, + 'boot_time': boot_time, + 'up_time_format': up_time_format + } + +# 系统监控 +@bp.get('/') +@authorize("system:monitor:main") +def main(): + # 当前时间 time_now = time.strftime('%H:%M:%S ', time.localtime(time.time())) return render_template( 'system/monitor.html', - hostname=hostname, - system_version=system_version, - python_version=python_version, - cpus_percent=cpus_percent, - memory_usage=memory_usage, - cpu_count=cpu_count, - memory_used=memory_used, - memory_total=memory_total, - memory_free=memory_free, - boot_time=boot_time, - up_time_format=up_time_format, - disk_partitions_list=disk_partitions_list, - time_now=time_now + time_now=time_now, + **get_basic_info() ) @@ -82,19 +96,74 @@ def main(): @bp.get('/polling') @authorize("system:monitor:main") def ajax_polling(): - # 获取cpu使用率 - cpus_percent = psutil.cpu_percent(interval=0.1, percpu=False) # percpu 获取主使用率 - # 获取内存使用率 - memory_information = psutil.virtual_memory() + # 获取 CPU 核心数 + cpu_count = cache_auto_internal('cpu_count', lambda: psutil.cpu_count(), expired=999999999) + + # 获取 CPU 使用率 + cpus_percent = cache_auto_internal('cpus_percent', + lambda: psutil.cpu_percent(interval=1, percpu=False), + expired=5) + + # 每个 CPU 的使用率 + cpu_percent_per_core = cache_auto_internal('cpu_percent_per_core', + lambda: list(enumerate(psutil.cpu_percent(interval=1, percpu=True))), + expired=5) + + # 获取空闲率、等待率 + cpu_times_percent = cache_auto_internal('cpu_times_percent', + lambda: psutil.cpu_times_percent(interval=1, percpu=False), + expired=5) + + # 内存信息 + memory_information = cache_auto_internal('memory_information', + psutil.virtual_memory, + expired=5) + + # 硬盘信息 + disk_partitions_list = cache_auto_internal('disk_partitions_list', + get_disk_partitions_list, + expired=5) + + # 系统信息 + basic_info = cache_auto_internal('basic_info', + get_basic_info, + expired=5) + memory_usage = memory_information.percent - time_now = time.strftime('%H:%M:%S ', time.localtime(time.time())) - return jsonify(cups_percent=cpus_percent, memory_used=memory_usage, time_now=time_now) + memory_used = memory_information.used + memory_total = memory_information.total + memory_free = memory_information.free + + cpu_idle_percent = cpu_times_percent.idle + if hasattr(cpu_times_percent, 'iowait'): + cpu_wait_percent = cpu_times_percent.iowait + else: + cpu_wait_percent = "-" + + return table_api(msg="请求成功", + count=0, + data={ + 'cpu_count': cpu_count, + 'cpus_percent': cpus_percent, + 'cpu_idle_percent': cpu_idle_percent, + 'cpu_wait_percent': cpu_wait_percent, + 'cpu_percent_per_core': cpu_percent_per_core, + 'memory_used': memory_used, + 'memory_total': memory_total, + 'memory_free': memory_free, + 'memory_usage': memory_usage, + 'disk_partitions_list': disk_partitions_list, + 'time_now': time.strftime('%H:%M:%S', time.localtime(time.time())), + 'basic_info': basic_info + }) # 关闭程序 @bp.get('/kill') @authorize("system:monitor:main") def kill(): + # 注:若是多 worker 则不生效 + return success_api(msg="关闭命令已发送,请修改代码以生效。") for proc in psutil.process_iter(): if proc.pid == os.getpid(): proc.kill() diff --git a/static/system/admin/css/other/monitor.css b/static/system/admin/css/other/monitor.css new file mode 100644 index 0000000..687b22c --- /dev/null +++ b/static/system/admin/css/other/monitor.css @@ -0,0 +1,187 @@ +.pear-container { + background-color: whitesmoke; + margin: 10px; +} + +.pear-card { + width: 100%; + height: 66px; + background-color: #F8F8F8; + display: inline-block; + border-radius: 5px; + text-align: center; + margin-bottom: 3px; +} + +.pear-card:hover, +.pear-card2:hover { + box-shadow: 2px 0 8px 0 lightgray !important; +} + +.pear-card2 { + width: 100%; + height: 90px; + background-color: #F8F8F8; + display: inline-block; + border-radius: 5px; + text-align: center; + margin-bottom: 3px; +} + +.pear-card2 i { + font-size: 30px; + height: 90px; + line-height: 90px; +} + +.pear-card i { + font-size: 30px; + height: 66px; + line-height: 66px; +} + +.layui-col-md3 { + text-align: center; +} + +.pear-card-title { + margin-top: 3px; +} + +.person img { + width: 90px; + height: 90px; + border-radius: 4px; + margin-top: 8px; + margin-left: 8px; +} + +.pear-card2 .count { + color: var(--global-primary-color); + font-size: 30px; + margin-top: 12px; +} + +.pear-card2 .title { + color: gray; + font-size: 14px; + margin-top: 14px; +} + +.pear-card-status { + padding: 0 10px 10px; +} + +.pear-card-status li { + position: relative; + padding: 10px 0; + border-bottom: 1px solid #EEE; +} + +.pear-card-status li h3 { + padding-bottom: 5px; + font-weight: 700; +} + +.pear-card-status li p { + padding-bottom: 10px; + padding-top: 3px; +} + +.pear-card-status li > span { + color: #999; + height: 24px; + line-height: 24px; +} + +.pear-reply { + position: absolute; + right: 20px; + height: 24px; + line-height: 24px; +} + +.person .title { + font-size: 17px; + font-weight: 600; + margin-left: 18px; + margin-top: 16px; + position: absolute; + display: inline-block; +} + +.person .desc { + font-size: 16px; + font-weight: 600; + margin-left: 115px; + margin-top: -30px; + position: absolute; + display: inline-block; +} + +#tooltip { + opacity: 0; /* 默认完全透明 */ + transition: opacity 0.3s ease-in-out; /* 添加淡入淡出动画 */ + position: absolute; + border-radius: 5px; + padding: 10px; + z-index: 1000; +} + +#tooltip.show { + opacity: 1; /* 完全显示 */ +} + +#tooltip .layui-card-body ul { + display: grid; /* 启用 Grid 布局 */ + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); /* 自动填充列 */ + gap: 5px; /* 设置子元素之间的间距 */ + padding: 0; /* 移除默认的 ul 内边距 */ + margin: 0; /* 移除默认的 ul 外边距 */ + list-style: none; /* 移除列表项的默认样式 */ +} + +#tooltip .layui-card-body ul li span { + width: 8em; + background-color: #E7EEFC !important; + color: #6197F8 !important; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.progress-ring { + position: relative; + display: inline-block; + width: 120px; + height: 120px; + text-align: center; +} + +.progress-ring .circle { + transform: rotate(-90deg); +} + +.progress-ring .circle-bg { + fill: none; + stroke: #e6e6e6; /* 背景色 */ + stroke-width: 8; /* 环的宽度(细一点) */ +} + +.progress-ring .circle-progress { + fill: none; + stroke: var(--global-primary-color); /* 使用 CSS 变量 */ + stroke-width: 8; /* 环的宽度(细一点) */ + stroke-dasharray: 339.292; + stroke-dashoffset: calc(339.292 - (339.292 * var(--progress) / 100)); + transition: stroke-dashoffset 1s; +} + +.progress-ring .progress-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 18px; + font-weight: bold; + color: #333; +} + diff --git a/static/system/component/pear/css/pear.css b/static/system/component/pear/css/pear.css index 82ca512..ca73968 100644 --- a/static/system/component/pear/css/pear.css +++ b/static/system/component/pear/css/pear.css @@ -14,4 +14,5 @@ @import url("module/dtree/font/dtreefont.css"); @import url("module/dtree/dtree.css"); @import url("module/layer.css"); -@import url("module/layout.css"); \ No newline at end of file +@import url("module/layout.css"); +@import url("module/popover.min.css"); \ No newline at end of file diff --git a/templates/system/common/memory.html b/templates/system/common/memory.html deleted file mode 100644 index d7a0821..0000000 --- a/templates/system/common/memory.html +++ /dev/null @@ -1,13 +0,0 @@ -{% macro memory_format(memory) %} - {%- if memory > 1024 ** 4 * 2 -%} - {{- (memory / 1024 ** 4) | round(2) -}}TB - {% elif memory > 1024 ** 3 * 2 %} - {{- (memory / 1024 ** 3) | round(2) -}}GB - {% elif memory > 1024 ** 2 * 2 %} - {{- (memory / 1024 ** 2) | round(2) -}}MB - {% elif memory > 1024 ** 1 * 2 %} - {{- (memory / 1024 ** 1) | round(2) -}}KB - {% else %} - {{- memory -}}B - {% endif %} -{% endmacro %} \ No newline at end of file diff --git a/templates/system/monitor.html b/templates/system/monitor.html index 4e6aac9..22ac9f8 100644 --- a/templates/system/monitor.html +++ b/templates/system/monitor.html @@ -1,16 +1,15 @@ -{% from 'system/common/memory.html' import memory_format %} 首页 {% include 'system/common/header.html' %} - +
-
+
主机信息 @@ -20,25 +19,25 @@
核心数
-
{{ cpu_count }}
+
-
空闲率
-
+
-%
等待率
-
+
-%
使用率
-
{{ cpus_percent }}%
+
-%
@@ -55,25 +54,25 @@
空闲内存
-
{{ memory_format(memory_free) }}
+
-
最大内存
-
{{ memory_format(memory_total) }}
+
-
已用内存
-
{{ memory_format(memory_used) }}
+
-
内存使用
-
{{ memory_usage }}%
+
-%
@@ -93,27 +92,6 @@
-
-
磁盘信息
-
-
    - {% for disk in disk_partitions_list %} -
  • -

    {{ disk.device }}

    -

    {{ disk.fstype }}

    - 磁盘大小: {{ memory_format(disk.total) }}   - 空闲大小: {{ memory_format(disk.free) }}   -
    -
    - 已经使用: {{ memory_format(disk.used) }}   - 使用率: {{ disk.percent }}% -
    - 详情 -
  • - {% endfor %} -
-
-
主机信息
@@ -127,47 +105,135 @@ 名称 - {{ hostname }} + {{ hostname }} 系统 - {{ system_version }} + {{ system_version }} 开机时间 - {{ boot_time }} + {{ boot_time }} 运行时长 - {{ up_time_format }} + {{ up_time_format }} - python版本 - {{ python_version }} + Python 版本 + {{ python_version }} 程序操作 关闭程序 + class="layui-btn layui-btn-xs layui-btn-primary">关闭程序
+
+
磁盘信息
+
+ 加载中...... +
+
+ +
+
+
+ 加载中...... +
+
+
+ + {% include 'system/common/footer.html' %}