feat: add legacy device migration flow
This commit is contained in:
@@ -9,10 +9,13 @@ from django.utils import timezone
|
||||
|
||||
from apps.licensing.models import (
|
||||
ClientDevice,
|
||||
DeviceCredential,
|
||||
DeviceBindingAudit,
|
||||
DeviceSession,
|
||||
LegacyMigrationGrant,
|
||||
LicenseEvent,
|
||||
LicenseSeat,
|
||||
MigrationRequest,
|
||||
SoftwareEntitlement,
|
||||
SoftwarePlan,
|
||||
)
|
||||
@@ -389,3 +392,190 @@ def release_license_seat(*, seat: LicenseSeat, reason: str, actor=None, now=None
|
||||
actor=actor,
|
||||
)
|
||||
return locked_seat
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_legacy_migration_grant(
|
||||
*,
|
||||
user,
|
||||
plan: SoftwarePlan,
|
||||
reason: str,
|
||||
actor=None,
|
||||
eligibility_snapshot: dict | None = None,
|
||||
):
|
||||
reason = _required_reason(reason)
|
||||
if LegacyMigrationGrant.objects.filter(
|
||||
user=user,
|
||||
product_code=plan.product_code,
|
||||
).exists():
|
||||
raise LicensingError("migration_grant_exists", "该用户已有此产品的迁移资格")
|
||||
|
||||
entitlement = grant_software_entitlement(
|
||||
user=user,
|
||||
plan=plan,
|
||||
reason=reason,
|
||||
actor=actor,
|
||||
)
|
||||
snapshot = {
|
||||
"source": "manual",
|
||||
"plan_id": plan.id,
|
||||
"plan_name": plan.name,
|
||||
"plan_device_limit": plan.device_limit,
|
||||
"plan_duration_days": plan.duration_days,
|
||||
}
|
||||
snapshot.update(eligibility_snapshot or {})
|
||||
grant = LegacyMigrationGrant.objects.create(
|
||||
user=user,
|
||||
product_code=plan.product_code,
|
||||
entitlement=entitlement,
|
||||
eligibility_snapshot=snapshot,
|
||||
reason=reason,
|
||||
actor=actor,
|
||||
)
|
||||
_create_license_event(
|
||||
entitlement=entitlement,
|
||||
action=LicenseEvent.Action.MIGRATION_GRANTED,
|
||||
reason=reason,
|
||||
actor=actor,
|
||||
metadata={"migration_grant_id": grant.id},
|
||||
)
|
||||
return grant
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_migration_request(*, user, device: ClientDevice, now=None):
|
||||
now = now or timezone.now()
|
||||
if device.user_id != user.id:
|
||||
raise LicensingError("device_mismatch", "设备不属于当前账号")
|
||||
if device.status != ClientDevice.Status.ACTIVE:
|
||||
raise LicensingError("device_revoked", "设备已被吊销")
|
||||
|
||||
grant = (
|
||||
LegacyMigrationGrant.objects.select_related("entitlement")
|
||||
.select_for_update()
|
||||
.filter(
|
||||
user=user,
|
||||
product_code=device.product_code,
|
||||
status=LegacyMigrationGrant.Status.ACTIVE,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if grant is None:
|
||||
raise LicensingError("migration_not_eligible", "当前账号没有可用的存量迁移资格")
|
||||
if not grant.entitlement.is_usable_at(now):
|
||||
raise LicensingError("license_expired", "迁移权益已过期或不可用")
|
||||
if DeviceCredential.objects.filter(
|
||||
device=device,
|
||||
entitlement=grant.entitlement,
|
||||
revoked_at__isnull=True,
|
||||
).exists():
|
||||
raise LicensingError("device_credential_exists", "当前设备已完成迁移绑定")
|
||||
|
||||
raw_token = MigrationRequest.generate_plaintext_credential_token()
|
||||
request = MigrationRequest.objects.create(
|
||||
user=user,
|
||||
device=device,
|
||||
migration_grant=grant,
|
||||
credential_token_hash=MigrationRequest.hash_credential_token(raw_token),
|
||||
credential_token_prefix=raw_token[:20],
|
||||
expires_at=now + timedelta(seconds=max(60, settings.MIGRATION_REQUEST_TTL_SECONDS)),
|
||||
)
|
||||
return request, raw_token
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def confirm_migration_request(*, request_id, user, now=None):
|
||||
now = now or timezone.now()
|
||||
request = (
|
||||
MigrationRequest.objects.select_for_update()
|
||||
.select_related("user", "device", "migration_grant", "migration_grant__entitlement")
|
||||
.filter(request_id=request_id)
|
||||
.first()
|
||||
)
|
||||
if request is None:
|
||||
raise LicensingError("migration_request_not_found", "迁移请求不存在")
|
||||
if request.user_id != user.id:
|
||||
raise LicensingError("migration_request_forbidden", "迁移请求不属于当前账号")
|
||||
existing_credential = DeviceCredential.objects.filter(migration_request=request).first()
|
||||
if existing_credential is not None:
|
||||
return request, existing_credential, False
|
||||
if not request.is_pending_at(now):
|
||||
raise LicensingError("migration_request_expired", "迁移请求已过期或不可用")
|
||||
grant = request.migration_grant
|
||||
if grant.status != LegacyMigrationGrant.Status.ACTIVE:
|
||||
raise LicensingError("migration_not_eligible", "迁移资格已撤销")
|
||||
if not grant.entitlement.is_usable_at(now):
|
||||
raise LicensingError("license_expired", "迁移权益已过期或不可用")
|
||||
|
||||
seat = assign_license_seat(
|
||||
entitlement=grant.entitlement,
|
||||
device=request.device,
|
||||
reason="存量迁移网页登录确认绑定",
|
||||
actor=user,
|
||||
now=now,
|
||||
)
|
||||
existing_device_credential = DeviceCredential.objects.filter(
|
||||
device=request.device,
|
||||
entitlement=grant.entitlement,
|
||||
revoked_at__isnull=True,
|
||||
).first()
|
||||
if existing_device_credential is not None:
|
||||
raise LicensingError("device_credential_exists", "当前设备已完成迁移绑定")
|
||||
|
||||
credential = DeviceCredential.objects.create(
|
||||
user=user,
|
||||
product_code=request.device.product_code,
|
||||
device=request.device,
|
||||
entitlement=grant.entitlement,
|
||||
seat=seat,
|
||||
migration_request=request,
|
||||
token_hash=request.credential_token_hash,
|
||||
token_prefix=request.credential_token_prefix,
|
||||
expires_at=grant.entitlement.grace_expires_at,
|
||||
)
|
||||
request.status = MigrationRequest.Status.CONFIRMED
|
||||
request.confirmed_at = now
|
||||
request.save(update_fields=("status", "confirmed_at", "updated_at"))
|
||||
_create_license_event(
|
||||
entitlement=grant.entitlement,
|
||||
seat=seat,
|
||||
device=request.device,
|
||||
action=LicenseEvent.Action.CREDENTIAL_ISSUED,
|
||||
reason="存量迁移网页登录确认签发设备凭证",
|
||||
actor=user,
|
||||
metadata={"migration_request_id": str(request.request_id)},
|
||||
)
|
||||
return request, credential, True
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def revoke_device_credential(*, credential: DeviceCredential, reason: str, actor=None, now=None):
|
||||
reason = _required_reason(reason)
|
||||
now = now or timezone.now()
|
||||
locked_credential = (
|
||||
DeviceCredential.objects.select_for_update()
|
||||
.select_related("seat", "entitlement", "device")
|
||||
.get(pk=credential.pk)
|
||||
)
|
||||
if locked_credential.revoked_at is not None:
|
||||
return locked_credential
|
||||
|
||||
locked_credential.revoked_at = now
|
||||
locked_credential.revoke_reason = reason
|
||||
locked_credential.save(update_fields=("revoked_at", "revoke_reason"))
|
||||
_create_license_event(
|
||||
entitlement=locked_credential.entitlement,
|
||||
seat=locked_credential.seat,
|
||||
device=locked_credential.device,
|
||||
action=LicenseEvent.Action.CREDENTIAL_REVOKED,
|
||||
reason=reason,
|
||||
actor=actor,
|
||||
)
|
||||
if locked_credential.seat.device_id == locked_credential.device_id:
|
||||
release_license_seat(
|
||||
seat=locked_credential.seat,
|
||||
reason=reason,
|
||||
actor=actor,
|
||||
now=now,
|
||||
)
|
||||
return locked_credential
|
||||
|
||||
Reference in New Issue
Block a user