增加了api功能:

创建API文档页面 :新建了 status/templates/status/api_docs.html 模板文件,包含:

- API认证方式说明
- 所有API端点的详细说明(POST /api/checkin/、GET /api/services/、GET /api/services/{id}/history/、GET /api/status-summary/)
- 每个端点的请求体和响应体示例
- 状态码和检测类型的说明
This commit is contained in:
2025-09-07 19:51:04 +08:00
parent 1cac84b9d4
commit 16e3b14984
14 changed files with 421 additions and 3 deletions

Binary file not shown.

12
status/api_urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import path
from . import api_views
urlpatterns = [
# 客户端上报接口
path('checkin/', api_views.CheckInAPIView.as_view(), name='api-checkin'),
# 查询接口
path('services/', api_views.ServiceListAPIView.as_view(), name='api-service-list'),
path('services/<int:service_id>/history/', api_views.ServiceHistoryAPIView.as_view(), name='api-service-history'),
path('status-summary/', api_views.StatusSummaryAPIView.as_view(), name='api-status-summary'),
]

142
status/api_views.py Normal file
View File

@@ -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)

View File

@@ -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 = '服务'

96
status/serializers.py Normal file
View File

@@ -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()

View File

@@ -0,0 +1,130 @@
{% extends 'status/base.html' %}
{% block title %}API说明{% endblock %}
{% block content %}
<div class="bg-white rounded-xl p-6 card-shadow mb-6">
<h1 class="text-2xl font-bold text-gray-800 mb-4">API接口说明</h1>
<p class="text-gray-600 mb-6">本系统提供以下RESTful API接口用于服务状态的上报和查询。</p>
<div class="mb-8">
<h2 class="text-xl font-bold text-gray-800 mb-4">认证方式</h2>
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 mb-4">
<p class="text-blue-700">目前所有API接口均无需认证可直接访问。后续版本可能会添加认证机制。</p>
</div>
</div>
<div class="mb-8">
<h2 class="text-xl font-bold text-gray-800 mb-4">API端点列表</h2>
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-2">POST /api/checkin/ - 客户端上报接口</h3>
<p class="text-gray-600 mb-3">用于客户端上报服务状态信息。如果服务不存在,系统会自动创建。</p>
<div class="mb-4">
<h4 class="font-medium text-gray-700 mb-2">请求体示例:</h4>
<pre class="bg-gray-800 text-gray-100 p-4 rounded-lg overflow-x-auto"><code>{
"service_name": "Web服务",
"host": "192.168.1.100",
"port": 80,
"check_type": "http",
"status": "UP",
"response_time": 120.5,
"message": "连接成功"
}</code></pre>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2">响应示例:</h4>
<pre class="bg-gray-800 text-gray-100 p-4 rounded-lg overflow-x-auto"><code>{
"code": 200,
"message": "服务状态已更新",
"service_id": 1
}</code></pre>
</div>
</div>
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-2">GET /api/services/ - 服务列表查询接口</h3>
<p class="text-gray-600 mb-3">获取所有服务的列表及其最新状态。</p>
<div>
<h4 class="font-medium text-gray-700 mb-2">响应示例:</h4>
<pre class="bg-gray-800 text-gray-100 p-4 rounded-lg overflow-x-auto"><code>[
{
"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
}
]</code></pre>
</div>
</div>
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-2">GET /api/services/{id}/history/ - 服务历史记录查询接口</h3>
<p class="text-gray-600 mb-3">获取指定服务的历史检测记录。</p>
<div>
<h4 class="font-medium text-gray-700 mb-2">响应示例:</h4>
<pre class="bg-gray-800 text-gray-100 p-4 rounded-lg overflow-x-auto"><code>{
"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"
}
]
}</code></pre>
</div>
</div>
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-2">GET /api/status-summary/ - 状态摘要查询接口</h3>
<p class="text-gray-600 mb-3">获取所有服务的状态摘要统计信息。</p>
<div>
<h4 class="font-medium text-gray-700 mb-2">响应示例:</h4>
<pre class="bg-gray-800 text-gray-100 p-4 rounded-lg overflow-x-auto"><code>{
"total_services": 10,
"up_count": 8,
"down_count": 1,
"unknown_count": 1
}</code></pre>
</div>
</div>
</div>
<div class="mb-6">
<h2 class="text-xl font-bold text-gray-800 mb-4">状态码说明</h2>
<ul class="list-disc pl-5 text-gray-600 space-y-1">
<li><strong>UP</strong>: 服务正常运行</li>
<li><strong>DOWN</strong>: 服务不可用</li>
<li><strong>UNKNOWN</strong>: 服务状态未知</li>
</ul>
</div>
<div class="mb-6">
<h2 class="text-xl font-bold text-gray-800 mb-4">检测类型说明</h2>
<ul class="list-disc pl-5 text-gray-600 space-y-1">
<li><strong>http</strong>: HTTP检测</li>
<li><strong>https</strong>: HTTPS检测</li>
<li><strong>tcp</strong>: TCP端口检测</li>
<li><strong>ping</strong>: PING检测</li>
</ul>
</div>
</div>
{% endblock %}

View File

@@ -56,9 +56,17 @@
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold">服务状态监控</h1>
</div>
<div class="flex items-center space-x-2">
<i class="fas fa-refresh animate-spin mr-2"></i>
<span id="last-updated" class="text-sm md:text-base">最后更新: 加载中...</span>
</div>
<a href="/admin/" class="text-white hover:text-gray-200 transition-colors">
<i class="fas fa-cog mr-1"></i> 管理后台
</a>
<a href="/api-docs/" class="text-white hover:text-gray-200 transition-colors">
<i class="fas fa-book mr-1"></i> API说明
</a>
</div>
<div class="flex items-center space-x-2">
<i class="fas fa-refresh animate-spin mr-2"></i>
<span id="last-updated" class="text-sm md:text-base">最后更新: 加载中...</span>
</div>
<div class="mt-4 md:mt-0">
<a href="/services/" class="bg-white text-primary px-4 py-2 rounded-lg font-medium hover:bg-gray-100 transition-colors">
<i class="fas fa-list mr-2"></i> 查看所有服务

View File

@@ -19,6 +19,9 @@ urlpatterns = [
path('api/services/', views.api_services, name='api_services'),
path('api/services/<int:service_id>/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路由配置已加载")

View File

@@ -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')

View File

@@ -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')),
]