From 16e3b14984f1ee266fb130af78e34319d7377f10 Mon Sep 17 00:00:00 2001 From: xiaji Date: Sun, 7 Sep 2025 19:51:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86api=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=9A=20=E5=88=9B=E5=BB=BAAPI=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=20=EF=BC=9A=E6=96=B0=E5=BB=BA=E4=BA=86=20sta?= =?UTF-8?q?tus/templates/status/api=5Fdocs.html=20=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E5=8C=85=E5=90=AB=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API认证方式说明 - 所有API端点的详细说明(POST /api/checkin/、GET /api/services/、GET /api/services/{id}/history/、GET /api/status-summary/) - 每个端点的请求体和响应体示例 - 状态码和检测类型的说明 --- db.sqlite3 | Bin 155648 -> 155648 bytes status/__pycache__/models.cpython-38.pyc | Bin 3483 -> 4125 bytes status/__pycache__/urls.cpython-38.pyc | Bin 839 -> 881 bytes status/__pycache__/views.cpython-38.pyc | Bin 5401 -> 5555 bytes status/api_urls.py | 12 ++ status/api_views.py | 142 +++++++++++++++++++++ status/models.py | 21 +++ status/serializers.py | 96 ++++++++++++++ status/templates/status/api_docs.html | 130 +++++++++++++++++++ status/templates/status/index.html | 14 +- status/urls.py | 3 + status/views.py | 5 + statuspage/__pycache__/urls.cpython-38.pyc | Bin 1018 -> 1055 bytes statuspage/urls.py | 1 + 14 files changed, 421 insertions(+), 3 deletions(-) create mode 100644 status/api_urls.py create mode 100644 status/api_views.py create mode 100644 status/serializers.py create mode 100644 status/templates/status/api_docs.html diff --git a/db.sqlite3 b/db.sqlite3 index 70bf373489051523d998d6a75f4248efd64db378..1b51f2037de876184aa82555c4c44c2cedbe45ae 100644 GIT binary patch delta 433 zcmZoTz}awsbAmLZ#zYxsMvaXLOXS5k`E?oiU-Dn!-_4)Q-_1XZzk=VJUw5;jf-pa; z3@@`i=k)nWjFLuTJj}+N#U+U)rN!~ZsYPX($*Ji@`K1NIEX>-BiKQhO@dc?xxtYbq znfZAtg51oSoXxfR+iUe1+b^&&^8aDr|Fc=J;XS_~0|NsyGbbl6kY?on%)tK{DEyLN zoSTt_L7KrOH7&6;r-TbE`IUkHD^T(^zbF^8G$&LFqHiw)|K81l1)KSO9U0jf1lcVe zIYR>+G=bWHz&SrJFEzO&HASJgG&wo7xHzpeC#TZLz{phBz*5)1T*1J?%GAKh$WYJ1 z!otwf3}WOY2L4GvBRlx(a~Qc8ROOk3GdUeOpKa}av3}~aIlWK278)9v>lpx{q5OoB z9dqSQo?MQKI(xeCcSnW=dt3dtF%$=R8CRtm^^vDV>M5eow;2!nkuw|y@GhtQ)0upqMpu>2pgzyuJp1c3Yx DzYHJ~ diff --git a/status/__pycache__/models.cpython-38.pyc b/status/__pycache__/models.cpython-38.pyc index 54831d7e40592dad8c941d07a0569b9376253918..666250e4cbb71fdb10843e771aa01a785270a627 100644 GIT binary patch delta 1008 zcmb7CK~EDw7~R>W-DTUYKtWp&ZB?pGlz@sTaIpj$64Qf?9(vg{J0lcYYGxNrOwHCv zJZRLIF>p{43GraOU{FjEypFJ1M>KXAT<#8w&;W-@QSyzjj)^V)sZvl0tk z(KJ=yt1ta==AQN@v?`&`Y+YJAi4Wu0qLAB<}=m=!SMW^ti|w|_l)Uwgdd zKCik<>p$;ry45>0&grD&$dnjmicQCNtw2r7@taoOS}W%Fm)6)zB)dKnM6S=|u2tQ458UO= z-^*_^Nr?{dJ{{!XsKyL2<_OmF7Ci&(KMA_(Ugb%3t570CT-lhS*KQKyuVGJ7{+Fc2 z9@aiQZIZ-xG)m0t<$05MPi&edQkSMdJ+>=QtVx~*i@_~*i>E=Km#nIoC+v1Ybr_)xn^5OedKz5tJ2TrK zMOpS;{T7}8ZXbZx9ft1Lz=&^(7gt#*IF=uQCJd&d0G^=-stW+%a@5IUId4rCND)(* zKo0^dmxyWRrwP3XlmwJCU%u(RfOZ4d2{6T82YZv3po9;crZ}YeG21vOsDtk|*kZ`1 RNHoQ^f+dtlHU3_nfr$-6iYOip5!Yr zoqV5bKj$s>qSWO4qLkvzi@D7i87(K@=E)Yh#a3LBSW;Siiz_)JH90#qB|foaasaO+ zqut~z-Yc@UKqHFyL4+NUxW!wPT3nEySDYGOl9`)2Igrna)deW-IC%n}O`;)48OYuu zBM<@BDh*;8fC#XDF1MWg#1gm6)SQ$eCy=NNkhsO3n_66)n4Vf>0TN;c2{Gs8mlxTC zxFC-eiGT=~$-4Zqn(iQ`Fo>`O5+Iw31%LzxqYw}ZfFKheD+hCt<>X@iT%ac(@>?^? F0059?RG9z( diff --git a/status/__pycache__/urls.cpython-38.pyc b/status/__pycache__/urls.cpython-38.pyc index e303279833a8619e9aab779e70e04e1f15cddbd2..1588a2aff97d57c91299003f1fc0eb89d721cc9b 100644 GIT binary patch delta 172 zcmX@k_K{6Hl$V!_0SHth_hxi5GcY^`agYHIkmCTv#g`^(Z&gZVOc70G&Js)!OX28c zPUnacN@ZOjoFcxEk&z*VHJCwDV&fM{M$szH#DYxSl>Fpk{aYMBW;}>FnU_h9QDm|X zQ?+6dAJD{G+@(c11&JjksYQ9kw^%as((+d_6o~_6io_=$W=djIn5@Pu$I8t2nMD8q Dz}G4k delta 112 zcmey!cAQN+l$V!_0SG!B_GXwdGcY^`agYHwkmCTv#YZM;Zxu;rj}lB}T_BVqx{#5P zA%!)VK~roq6Qd;KWCJESM&Zf+Ox2UmGl{W@0d*9KO@7Uk#3(n}k6DiS8w)Q0dM6dv diff --git a/status/__pycache__/views.cpython-38.pyc b/status/__pycache__/views.cpython-38.pyc index 0f47acc73e2233b824263f57be1562028c76c317..fc50f9974821cadced09b81cdf504a8ad3a7b736 100644 GIT binary patch delta 203 zcmbQKwON}tl$V!_0SFW$_hz(D^k-`?tpeeoCgfUYnKA91u z4hleQW*~M3;$n~iB@8tTDU8ibK=n+e{6GQ60MBRB+Mg|4{Bq&emva|Yi4>P4mXsFj zCl+MJr{pIW>t&SW=4dh&F#**ADTT>}Lc-D/history/', api_views.ServiceHistoryAPIView.as_view(), name='api-service-history'), + path('status-summary/', api_views.StatusSummaryAPIView.as_view(), name='api-status-summary'), +] \ No newline at end of file diff --git a/status/api_views.py b/status/api_views.py new file mode 100644 index 0000000..05ca42c --- /dev/null +++ b/status/api_views.py @@ -0,0 +1,142 @@ +from rest_framework import status, generics +from rest_framework.views import APIView +from rest_framework.response import Response +from django.db.models import Count, Q +from .models import Service, ServiceCheckRecord, ServiceGroup +from .serializers import ( + ServiceSerializer, + ServiceCheckRecordSerializer, + ServiceCheckInSerializer, + StatusSummarySerializer +) + +# 引入loguru库用于日志记录 +try: + from loguru import logger +except ImportError: + import logging + logger = logging.getLogger(__name__) + + +class CheckInAPIView(APIView): + """客户端上报接口""" + + def post(self, request, *args, **kwargs): + """处理客户端上报请求""" + serializer = ServiceCheckInSerializer(data=request.data) + + if serializer.is_valid(): + try: + result = serializer.create(serializer.validated_data) + logger.info(f"服务上报成功,服务ID: {result['service_id']}") + return Response({ + "code": status.HTTP_200_OK, + "message": "上报成功", + "service_id": result['service_id'] + }) + except Exception as e: + logger.error(f"服务上报失败: {str(e)}") + return Response({ + "code": status.HTTP_500_INTERNAL_SERVER_ERROR, + "message": f"上报失败: {str(e)}" + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + else: + logger.warning(f"服务上报数据无效: {serializer.errors}") + return Response({ + "code": status.HTTP_400_BAD_REQUEST, + "message": "数据无效", + "errors": serializer.errors + }, status=status.HTTP_400_BAD_REQUEST) + + +class ServiceListAPIView(generics.ListAPIView): + """获取所有服务列表(含最新状态)""" + serializer_class = ServiceSerializer + queryset = Service.objects.all().select_related('group') + + def get_queryset(self): + queryset = super().get_queryset() + + # 支持按分组过滤 + group_name = self.request.query_params.get('group') + if group_name: + queryset = queryset.filter(group__name=group_name) + + # 支持按状态过滤 + status_filter = self.request.query_params.get('status') + if status_filter: + # 获取每个服务的最新状态 + service_ids_with_status = [] + for service in queryset: + if service.get_latest_status() == status_filter.upper(): + service_ids_with_status.append(service.id) + queryset = queryset.filter(id__in=service_ids_with_status) + + # 支持按名称搜索 + search = self.request.query_params.get('search') + if search: + queryset = queryset.filter( + Q(name__icontains=search) | + Q(host__icontains=search) | + Q(description__icontains=search) + ) + + logger.info(f"获取服务列表,过滤参数: group={group_name}, status={status_filter}, search={search}") + return queryset + + +class ServiceHistoryAPIView(generics.ListAPIView): + """获取某服务历史记录(分页)""" + serializer_class = ServiceCheckRecordSerializer + + def get_queryset(self): + service_id = self.kwargs.get('service_id') + try: + service = Service.objects.get(id=service_id) + except Service.DoesNotExist: + logger.warning(f"请求的服务不存在,ID: {service_id}") + return ServiceCheckRecord.objects.none() + + queryset = ServiceCheckRecord.objects.filter(service=service) + logger.info(f"获取服务历史记录,服务ID: {service_id}") + return queryset + + +class StatusSummaryAPIView(APIView): + """获取全局状态摘要""" + + def get(self, request, *args, **kwargs): + """获取全局状态摘要""" + services = Service.objects.all() + total_services = services.count() + + up_services = 0 + down_services = 0 + unknown_services = 0 + + for service in services: + latest_status = service.get_latest_status() + if latest_status == 'UP': + up_services += 1 + elif latest_status == 'DOWN': + down_services += 1 + else: + unknown_services += 1 + + # 计算正常运行时间百分比 + if total_services > 0: + uptime_percentage = (up_services / total_services) * 100 + else: + uptime_percentage = 0 + + data = { + 'total_services': total_services, + 'up_services': up_services, + 'down_services': down_services, + 'unknown_services': unknown_services, + 'uptime_percentage': round(uptime_percentage, 2) + } + + serializer = StatusSummarySerializer(data) + logger.info(f"获取状态摘要: {data}") + return Response(serializer.data) \ No newline at end of file diff --git a/status/models.py b/status/models.py index fef97af..057c210 100644 --- a/status/models.py +++ b/status/models.py @@ -43,6 +43,27 @@ class Service(models.Model): def __str__(self): return f"{self.name} ({self.host})" + def get_latest_status(self): + """获取最新状态""" + latest_record = self.records.first() + if latest_record: + return latest_record.status + return 'UNKNOWN' + + def get_latest_check_time(self): + """获取最新检测时间""" + latest_record = self.records.first() + if latest_record: + return latest_record.checked_at + return None + + def get_latest_response_time(self): + """获取最新响应时间""" + latest_record = self.records.first() + if latest_record: + return latest_record.response_time + return None + class Meta: verbose_name = '服务' verbose_name_plural = '服务' diff --git a/status/serializers.py b/status/serializers.py new file mode 100644 index 0000000..4ed77e0 --- /dev/null +++ b/status/serializers.py @@ -0,0 +1,96 @@ +from rest_framework import serializers +from .models import ServiceGroup, Service, ServiceCheckRecord + +# 引入loguru库用于日志记录 +try: + from loguru import logger +except ImportError: + import logging + logger = logging.getLogger(__name__) + + +class ServiceGroupSerializer(serializers.ModelSerializer): + class Meta: + model = ServiceGroup + fields = '__all__' + + +class ServiceCheckRecordSerializer(serializers.ModelSerializer): + class Meta: + model = ServiceCheckRecord + fields = '__all__' + + +class ServiceSerializer(serializers.ModelSerializer): + latest_status = serializers.CharField(source='get_latest_status', read_only=True) + latest_check_time = serializers.DateTimeField(source='get_latest_check_time', read_only=True) + latest_response_time = serializers.FloatField(source='get_latest_response_time', read_only=True) + + class Meta: + model = Service + fields = '__all__' + + +class ServiceCheckInSerializer(serializers.Serializer): + """客户端上报数据序列化器""" + service_name = serializers.CharField(max_length=200) + host = serializers.CharField(max_length=255) + port = serializers.IntegerField(required=False, allow_null=True) + check_type = serializers.CharField(max_length=50, default='tcp') + status = serializers.ChoiceField(choices=Service.STATUS_CHOICES) + response_time = serializers.FloatField(required=False, allow_null=True) + message = serializers.CharField(required=False, allow_blank=True) + + def create(self, validated_data): + """创建或更新服务记录""" + service_name = validated_data['service_name'] + host = validated_data['host'] + port = validated_data.get('port') + check_type = validated_data.get('check_type', 'tcp') + status = validated_data['status'] + response_time = validated_data.get('response_time') + message = validated_data.get('message', '') + + logger.info(f"收到服务上报: {service_name} ({host}:{port}), 状态: {status}") + + # 获取或创建默认分组 + group_name = 'Default' + group, created = ServiceGroup.objects.get_or_create(name=group_name) + if created: + logger.info(f"创建默认分组: {group_name}") + + # 获取或创建服务 + service, created = Service.objects.get_or_create( + name=service_name, + host=host, + port=port, + check_type=check_type, + defaults={'group': group} + ) + + if created: + logger.info(f"创建新服务: {service_name} ({host}:{port})") + + # 创建检测记录 + record = ServiceCheckRecord.objects.create( + service=service, + status=status, + response_time=response_time, + message=message + ) + + logger.info(f"保存检测记录: {service_name}, 状态: {status}, 响应时间: {response_time}ms") + + return { + 'service_id': service.id, + 'record_id': record.id + } + + +class StatusSummarySerializer(serializers.Serializer): + """状态摘要序列化器""" + total_services = serializers.IntegerField() + up_services = serializers.IntegerField() + down_services = serializers.IntegerField() + unknown_services = serializers.IntegerField() + uptime_percentage = serializers.FloatField() \ No newline at end of file diff --git a/status/templates/status/api_docs.html b/status/templates/status/api_docs.html new file mode 100644 index 0000000..4306815 --- /dev/null +++ b/status/templates/status/api_docs.html @@ -0,0 +1,130 @@ +{% extends 'status/base.html' %} + +{% block title %}API说明{% endblock %} + +{% block content %} +
+

API接口说明

+

本系统提供以下RESTful API接口,用于服务状态的上报和查询。

+ +
+

认证方式

+
+

目前所有API接口均无需认证,可直接访问。后续版本可能会添加认证机制。

+
+
+ +
+

API端点列表

+ +
+

POST /api/checkin/ - 客户端上报接口

+

用于客户端上报服务状态信息。如果服务不存在,系统会自动创建。

+ +
+

请求体示例:

+
{
+  "service_name": "Web服务",
+  "host": "192.168.1.100",
+  "port": 80,
+  "check_type": "http",
+  "status": "UP",
+  "response_time": 120.5,
+  "message": "连接成功"
+}
+
+ +
+

响应示例:

+
{
+  "code": 200,
+  "message": "服务状态已更新",
+  "service_id": 1
+}
+
+
+ +
+

GET /api/services/ - 服务列表查询接口

+

获取所有服务的列表及其最新状态。

+ +
+

响应示例:

+
[
+  {
+    "id": 1,
+    "name": "Web服务",
+    "host": "192.168.1.100",
+    "port": 80,
+    "check_type": "http",
+    "group": "Web服务组",
+    "description": "公司主网站",
+    "latest_status": "UP",
+    "latest_check_time": "2025-06-15T10:30:00Z",
+    "latest_response_time": 120.5
+  }
+]
+
+
+ +
+

GET /api/services/{id}/history/ - 服务历史记录查询接口

+

获取指定服务的历史检测记录。

+ +
+

响应示例:

+
{
+  "count": 1,
+  "total_pages": 1,
+  "current_page": 1,
+  "page_size": 20,
+  "results": [
+    {
+      "id": 1,
+      "service": 1,
+      "status": "UP",
+      "response_time": 120.5,
+      "message": "连接成功",
+      "check_time": "2025-06-15T10:30:00Z"
+    }
+  ]
+}
+
+
+ +
+

GET /api/status-summary/ - 状态摘要查询接口

+

获取所有服务的状态摘要统计信息。

+ +
+

响应示例:

+
{
+  "total_services": 10,
+  "up_count": 8,
+  "down_count": 1,
+  "unknown_count": 1
+}
+
+
+
+ +
+

状态码说明

+
    +
  • UP: 服务正常运行
  • +
  • DOWN: 服务不可用
  • +
  • UNKNOWN: 服务状态未知
  • +
+
+ +
+

检测类型说明

+
    +
  • http: HTTP检测
  • +
  • https: HTTPS检测
  • +
  • tcp: TCP端口检测
  • +
  • ping: PING检测
  • +
+
+
+{% endblock %} \ No newline at end of file diff --git a/status/templates/status/index.html b/status/templates/status/index.html index f3736ee..ac73009 100644 --- a/status/templates/status/index.html +++ b/status/templates/status/index.html @@ -56,9 +56,17 @@

服务状态监控

- - 最后更新: 加载中... -
+ + 管理后台 + + + API说明 + + +
+ +最后更新: 加载中... +
查看所有服务 diff --git a/status/urls.py b/status/urls.py index 78abbca..2129f87 100644 --- a/status/urls.py +++ b/status/urls.py @@ -19,6 +19,9 @@ urlpatterns = [ path('api/services/', views.api_services, name='api_services'), path('api/services//history/', views.api_service_history, name='api_service_history'), path('api/status-summary/', views.api_status_summary, name='api_status_summary'), + + # API文档页面 + path('api-docs/', views.api_docs, name='api_docs'), ] logger.info("URL路由配置已加载") \ No newline at end of file diff --git a/status/views.py b/status/views.py index f34cf84..7f78e23 100644 --- a/status/views.py +++ b/status/views.py @@ -217,3 +217,8 @@ def api_status_summary(request): """获取全局状态摘要(如:总共服务数、正常数、异常数)""" summary = get_status_summary() return JsonResponse(summary) + +# API文档页面 +def api_docs(request): + """API文档页面""" + return render(request, 'status/api_docs.html') diff --git a/statuspage/__pycache__/urls.cpython-38.pyc b/statuspage/__pycache__/urls.cpython-38.pyc index 938d5946d0c14e250e1cba5272265cfde76d17da..20813f06579db5fef6fb2ed67f2923540de124af 100644 GIT binary patch delta 119 zcmeyxKA%H7l$V!_0SIi}_hu9`GcY^`agYHUkmCTv#Xb|Yt;JGVQrJ^jQ#hI#o0-y? zqc~DHgBdirHWut*60TxNEXdTa;x8^qEGaG419IXgYcL-X-~no85#V7IVCG>2!oL6? CH5Zit delta 85 zcmbQw@rzwMl$V!_0SJUe0y12f85kaeILLq%$Z-JTVuy*^)?DdKQ5-4k!3>%l8*BD3 WP4;I#zz5RKBEZ8az|6zQ^A7+I#0>lZ diff --git a/statuspage/urls.py b/statuspage/urls.py index 123b6d1..24e1c53 100644 --- a/statuspage/urls.py +++ b/statuspage/urls.py @@ -20,4 +20,5 @@ from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('status.urls')), + path('api/', include('status.api_urls')), ]