feat: add device registration observation

This commit is contained in:
QiuSW
2026-07-21 09:27:05 +08:00
parent 20bdf4a8ce
commit 69f4517958
21 changed files with 958 additions and 5 deletions
+26
View File
@@ -4,6 +4,7 @@ from django.conf import settings
from rest_framework import serializers
from apps.billing.models import RechargeOrder
from apps.licensing.models import ClientDevice
class GenerateTitleRequestSerializer(serializers.Serializer):
@@ -129,3 +130,28 @@ class RechargeStatusRequestSerializer(serializers.Serializer):
allow_blank=False,
max_length=64,
)
class DeviceRegistrationRequestSerializer(serializers.Serializer):
product_code = serializers.ChoiceField(choices=ClientDevice.ProductCode.values)
device_id = serializers.CharField(
trim_whitespace=True,
allow_blank=False,
max_length=256,
)
device_id_version = serializers.CharField(
trim_whitespace=True,
allow_blank=False,
max_length=32,
)
installation_public_key = serializers.CharField(
trim_whitespace=True,
allow_blank=False,
max_length=4096,
)
platform = serializers.ChoiceField(choices=ClientDevice.Platform.values)
client_version = serializers.CharField(
trim_whitespace=True,
allow_blank=False,
max_length=64,
)
+12
View File
@@ -5,6 +5,8 @@ from .views import (
AnalyzeImagesView,
BalanceView,
ClientLatestReleaseView,
DeviceHeartbeatView,
DeviceRegistrationView,
GenerateImageTaskDetailView,
GenerateImageTaskSubmitView,
GenerateImageView,
@@ -18,6 +20,16 @@ from .views import (
urlpatterns = [
path("v1/balance", BalanceView.as_view(), name="api-balance"),
path("v1/models", ModelsView.as_view(), name="api-models"),
path(
"v1/client/devices/register",
DeviceRegistrationView.as_view(),
name="api-client-device-register",
),
path(
"v1/client/devices/heartbeat",
DeviceHeartbeatView.as_view(),
name="api-client-device-heartbeat",
),
path(
"v1/client/releases/latest",
ClientLatestReleaseView.as_view(),
+83
View File
@@ -27,6 +27,7 @@ from apps.api.image_tasks import (
from apps.api.models import ImageGenerationTask
from apps.api.serializers import (
AnalyzeImagesRequestSerializer,
DeviceRegistrationRequestSerializer,
GenerateImageRequestSerializer,
GenerateTitleRequestSerializer,
RechargeCreateRequestSerializer,
@@ -63,6 +64,12 @@ from apps.billing.services import (
query_and_apply_recharge_payment,
)
from apps.portal.models import DownloadRelease
from apps.licensing.authentication import DeviceSessionAuthentication
from apps.licensing.services import (
DeviceRegistrationError,
record_device_heartbeat,
register_device,
)
logger = logging.getLogger(__name__)
@@ -263,6 +270,82 @@ class ModelsView(ExternalApiView):
)
def _device_response(device) -> dict:
return {
"product_code": device.product_code,
"platform": device.platform,
"client_version": device.client_version,
"status": device.status,
"first_seen_at": device.first_seen_at.isoformat(),
"last_seen_at": device.last_seen_at.isoformat(),
}
class DeviceRegistrationView(ExternalApiView):
def post(self, request):
serializer = DeviceRegistrationRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
api_error("bad_request", "参数错误"),
status=status.HTTP_400_BAD_REQUEST,
)
data = serializer.validated_data
try:
result = register_device(
user=request.user,
api_key=request.auth,
product_code=data["product_code"],
device_id_version=data["device_id_version"],
device_id=data["device_id"],
public_key=data["installation_public_key"],
platform=data["platform"],
client_version=data["client_version"],
)
except DeviceRegistrationError as exc:
return Response(
api_error(exc.code, exc.message),
status=status.HTTP_403_FORBIDDEN,
)
return Response(
{
"device": _device_response(result.device),
"device_session_token": result.session_token,
"expires_at": result.session.expires_at.isoformat(),
},
status=status.HTTP_201_CREATED if result.created else status.HTTP_200_OK,
)
class DeviceSessionApiView(APIView):
authentication_classes = (DeviceSessionAuthentication,)
permission_classes = (IsAuthenticated,)
def permission_denied(self, request, message=None, code=None):
if request.authenticators and not request.successful_authenticator:
raise DeviceSessionAuthentication.authentication_failed(request)
super().permission_denied(request, message=message, code=code)
class DeviceHeartbeatView(DeviceSessionApiView):
def post(self, request):
try:
updated = record_device_heartbeat(request.auth)
except DeviceRegistrationError as exc:
return Response(
api_error(exc.code, exc.message),
status=status.HTTP_403_FORBIDDEN,
)
request.auth.device.refresh_from_db()
return Response(
{
"device": _device_response(request.auth.device),
"activity_updated": updated,
"expires_at": request.auth.expires_at.isoformat(),
},
status=status.HTTP_200_OK,
)
def _release_unpublished_response(platform: str) -> dict:
return {
"platform": platform,