feat: add legacy device migration flow

This commit is contained in:
QiuSW
2026-07-21 10:06:51 +08:00
parent 7b80ae3a7a
commit 1af47d043e
20 changed files with 1030 additions and 6 deletions
+12
View File
@@ -12,6 +12,8 @@ from .views import (
GenerateImageView,
GenerateTitleView,
ModelsView,
MigrationRequestCreateView,
MigrationRequestDetailView,
RechargeCreateView,
RechargeStatusView,
WechatRechargeCallbackView,
@@ -30,6 +32,16 @@ urlpatterns = [
DeviceHeartbeatView.as_view(),
name="api-client-device-heartbeat",
),
path(
"v1/client/migration-requests",
MigrationRequestCreateView.as_view(),
name="api-client-migration-request-create",
),
path(
"v1/client/migration-requests/<uuid:request_id>",
MigrationRequestDetailView.as_view(),
name="api-client-migration-request-detail",
),
path(
"v1/client/releases/latest",
ClientLatestReleaseView.as_view(),
+85
View File
@@ -66,12 +66,15 @@ from apps.billing.services import (
from apps.portal.models import DownloadRelease
from apps.licensing.authentication import DeviceSessionAuthentication
from apps.licensing.services import (
LicensingError,
DeviceRegistrationError,
DeviceSessionValidationError,
create_migration_request,
record_device_heartbeat,
register_device,
resolve_optional_device_session,
)
from apps.licensing.models import MigrationRequest
logger = logging.getLogger(__name__)
@@ -478,6 +481,88 @@ class DeviceHeartbeatView(DeviceSessionApiView):
)
def _migration_request_response(migration_request, *, credential_token=None, confirmation_url=""):
data = {
"request_id": str(migration_request.request_id),
"status": migration_request.status,
"product_code": migration_request.device.product_code,
"expires_at": migration_request.expires_at.isoformat(),
"confirmed_at": (
migration_request.confirmed_at.isoformat()
if migration_request.confirmed_at
else None
),
"confirmation_url": confirmation_url,
}
if credential_token is not None:
data["device_credential_token"] = credential_token
return data
class MigrationRequestCreateView(ExternalApiView):
def post(self, request):
try:
device = self.optional_client_device(request)
except ApiRequestError as exc:
return Response(exc.as_response_data(), status=exc.http_status)
if device is None:
return Response(
api_error("device_session_required", "必须提供当前设备会话"),
status=status.HTTP_401_UNAUTHORIZED,
)
try:
migration_request, credential_token = create_migration_request(
user=request.user,
device=device,
)
except LicensingError as exc:
response_status = (
status.HTTP_403_FORBIDDEN
if exc.code in {"migration_not_eligible", "license_expired"}
else status.HTTP_400_BAD_REQUEST
)
return Response(api_error(exc.code, exc.message), status=response_status)
confirmation_url = request.build_absolute_uri(
f"/migration/confirm/{migration_request.request_id}"
)
return Response(
_migration_request_response(
migration_request,
credential_token=credential_token,
confirmation_url=confirmation_url,
),
status=status.HTTP_201_CREATED,
)
class MigrationRequestDetailView(ExternalApiView):
def get(self, request, request_id):
try:
device = self.optional_client_device(request)
except ApiRequestError as exc:
return Response(exc.as_response_data(), status=exc.http_status)
if device is None:
return Response(
api_error("device_session_required", "必须提供当前设备会话"),
status=status.HTTP_401_UNAUTHORIZED,
)
migration_request = (
MigrationRequest.objects.select_related("device")
.filter(request_id=request_id, user=request.user, device=device)
.first()
)
if migration_request is None:
return Response(
api_error("migration_request_not_found", "迁移请求不存在"),
status=status.HTTP_404_NOT_FOUND,
)
return Response(
_migration_request_response(migration_request),
status=status.HTTP_200_OK,
)
def _release_unpublished_response(platform: str) -> dict:
return {
"platform": platform,