2026-07-21 09:27:05 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import hashlib
|
|
|
|
|
import hmac
|
|
|
|
|
import secrets
|
2026-07-21 10:06:51 +08:00
|
|
|
import uuid
|
2026-07-21 09:27:05 +08:00
|
|
|
|
|
|
|
|
from django.conf import settings
|
2026-07-21 09:51:29 +08:00
|
|
|
from django.core.exceptions import ValidationError
|
2026-07-21 09:27:05 +08:00
|
|
|
from django.db import models
|
2026-07-21 09:51:29 +08:00
|
|
|
from django.db.models import Q
|
2026-07-21 09:27:05 +08:00
|
|
|
from django.utils import timezone
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ClientDevice(models.Model):
|
|
|
|
|
class ProductCode(models.TextChoices):
|
|
|
|
|
CMSHOPEE = "cmshopee", "虾皮圈优化助手"
|
|
|
|
|
|
|
|
|
|
class Platform(models.TextChoices):
|
|
|
|
|
WINDOWS = "windows", "Windows"
|
|
|
|
|
MACOS = "macos", "macOS"
|
|
|
|
|
LINUX = "linux", "Linux"
|
|
|
|
|
|
|
|
|
|
class Status(models.TextChoices):
|
|
|
|
|
ACTIVE = "active", "有效"
|
|
|
|
|
REVOKED = "revoked", "已吊销"
|
|
|
|
|
|
|
|
|
|
user = models.ForeignKey(
|
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
|
verbose_name="用户",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="client_devices",
|
|
|
|
|
)
|
|
|
|
|
product_code = models.CharField(
|
|
|
|
|
"产品代码",
|
|
|
|
|
max_length=32,
|
|
|
|
|
choices=ProductCode.choices,
|
|
|
|
|
)
|
|
|
|
|
device_id_version = models.CharField("设备标识版本", max_length=32)
|
|
|
|
|
device_fingerprint = models.CharField("设备摘要", max_length=64, editable=False)
|
|
|
|
|
public_key_fingerprint = models.CharField("安装公钥摘要", max_length=64, editable=False)
|
|
|
|
|
platform = models.CharField("平台", max_length=16, choices=Platform.choices)
|
|
|
|
|
client_version = models.CharField("客户端版本", max_length=64)
|
|
|
|
|
status = models.CharField(
|
|
|
|
|
"状态",
|
|
|
|
|
max_length=20,
|
|
|
|
|
choices=Status.choices,
|
|
|
|
|
default=Status.ACTIVE,
|
|
|
|
|
)
|
|
|
|
|
first_seen_at = models.DateTimeField("首次登记时间", auto_now_add=True)
|
|
|
|
|
last_seen_at = models.DateTimeField("最后活跃时间", auto_now_add=True)
|
|
|
|
|
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
|
|
|
|
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "client_device"
|
|
|
|
|
verbose_name = "客户端设备"
|
|
|
|
|
verbose_name_plural = "客户端设备"
|
|
|
|
|
ordering = ("-last_seen_at", "-id")
|
|
|
|
|
constraints = [
|
|
|
|
|
models.UniqueConstraint(
|
|
|
|
|
fields=("user", "product_code", "device_fingerprint"),
|
|
|
|
|
name="client_device_user_product_fingerprint_unique",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("user", "product_code", "status")),
|
|
|
|
|
models.Index(fields=("product_code", "last_seen_at")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.user} {self.product_code} {self.platform}"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def fingerprint_device_id(device_id_version: str, device_id: str) -> str:
|
|
|
|
|
payload = f"{device_id_version}:{device_id}".encode("utf-8")
|
|
|
|
|
pepper = settings.DEVICE_IDENTIFIER_PEPPER.encode("utf-8")
|
|
|
|
|
return hmac.new(pepper, payload, hashlib.sha256).hexdigest()
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def fingerprint_public_key(public_key: str) -> str:
|
|
|
|
|
return hashlib.sha256(public_key.encode("utf-8")).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 09:51:29 +08:00
|
|
|
class SoftwarePlan(models.Model):
|
|
|
|
|
class Status(models.TextChoices):
|
|
|
|
|
ACTIVE = "active", "启用"
|
|
|
|
|
INACTIVE = "inactive", "停用"
|
|
|
|
|
|
|
|
|
|
product_code = models.CharField(
|
|
|
|
|
"产品代码",
|
|
|
|
|
max_length=32,
|
|
|
|
|
choices=ClientDevice.ProductCode.choices,
|
|
|
|
|
)
|
|
|
|
|
name = models.CharField("套餐名称", max_length=120)
|
|
|
|
|
duration_days = models.PositiveIntegerField("有效天数")
|
|
|
|
|
price = models.DecimalField("套餐价格", max_digits=12, decimal_places=2)
|
|
|
|
|
device_limit = models.PositiveSmallIntegerField("设备数量")
|
|
|
|
|
grace_days = models.PositiveSmallIntegerField("宽限天数", default=0)
|
|
|
|
|
status = models.CharField(
|
|
|
|
|
"状态",
|
|
|
|
|
max_length=20,
|
|
|
|
|
choices=Status.choices,
|
|
|
|
|
default=Status.ACTIVE,
|
|
|
|
|
)
|
|
|
|
|
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
|
|
|
|
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "software_plan"
|
2026-07-22 15:13:28 +08:00
|
|
|
verbose_name = "会员套餐"
|
|
|
|
|
verbose_name_plural = "会员套餐"
|
2026-07-21 09:51:29 +08:00
|
|
|
ordering = ("product_code", "name", "id")
|
|
|
|
|
constraints = [
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(duration_days__gt=0),
|
|
|
|
|
name="software_plan_duration_days_positive",
|
|
|
|
|
),
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(price__gt=0),
|
|
|
|
|
name="software_plan_price_positive",
|
|
|
|
|
),
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(device_limit__gt=0),
|
|
|
|
|
name="software_plan_device_limit_positive",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("product_code", "status")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.product_code} {self.name}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SoftwareEntitlement(models.Model):
|
|
|
|
|
class Status(models.TextChoices):
|
|
|
|
|
ACTIVE = "active", "有效"
|
|
|
|
|
REVOKED = "revoked", "已撤销"
|
|
|
|
|
EXPIRED = "expired", "已过期"
|
|
|
|
|
|
|
|
|
|
user = models.ForeignKey(
|
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
|
verbose_name="用户",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="software_entitlements",
|
|
|
|
|
)
|
|
|
|
|
product_code = models.CharField(
|
|
|
|
|
"产品代码",
|
|
|
|
|
max_length=32,
|
|
|
|
|
choices=ClientDevice.ProductCode.choices,
|
|
|
|
|
)
|
|
|
|
|
source_plan = models.ForeignKey(
|
|
|
|
|
SoftwarePlan,
|
|
|
|
|
verbose_name="来源套餐",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
related_name="entitlements",
|
|
|
|
|
)
|
|
|
|
|
plan_name = models.CharField("套餐名称快照", max_length=120)
|
|
|
|
|
plan_duration_days = models.PositiveIntegerField("套餐有效天数快照")
|
|
|
|
|
plan_price = models.DecimalField("套餐价格快照", max_digits=12, decimal_places=2)
|
|
|
|
|
plan_device_limit = models.PositiveSmallIntegerField("设备数量快照")
|
|
|
|
|
plan_grace_days = models.PositiveSmallIntegerField("宽限天数快照", default=0)
|
|
|
|
|
status = models.CharField(
|
|
|
|
|
"状态",
|
|
|
|
|
max_length=20,
|
|
|
|
|
choices=Status.choices,
|
|
|
|
|
default=Status.ACTIVE,
|
|
|
|
|
)
|
|
|
|
|
starts_at = models.DateTimeField("开始时间")
|
|
|
|
|
expires_at = models.DateTimeField("到期时间")
|
|
|
|
|
grace_expires_at = models.DateTimeField("宽限截止时间")
|
|
|
|
|
revoked_at = models.DateTimeField("撤销时间", null=True, blank=True)
|
|
|
|
|
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
|
|
|
|
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "software_entitlement"
|
2026-07-22 15:13:28 +08:00
|
|
|
verbose_name = "用户会员"
|
|
|
|
|
verbose_name_plural = "用户会员"
|
2026-07-21 09:51:29 +08:00
|
|
|
ordering = ("-expires_at", "-id")
|
|
|
|
|
constraints = [
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(plan_duration_days__gt=0),
|
|
|
|
|
name="software_entitlement_duration_positive",
|
|
|
|
|
),
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(plan_price__gt=0),
|
|
|
|
|
name="software_entitlement_price_positive",
|
|
|
|
|
),
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(plan_device_limit__gt=0),
|
|
|
|
|
name="software_entitlement_device_limit_positive",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("user", "product_code", "status")),
|
|
|
|
|
models.Index(fields=("product_code", "expires_at")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.user} {self.product_code} {self.plan_name}"
|
|
|
|
|
|
|
|
|
|
def clean(self) -> None:
|
|
|
|
|
super().clean()
|
|
|
|
|
if self.expires_at and self.starts_at and self.expires_at <= self.starts_at:
|
|
|
|
|
raise ValidationError({"expires_at": "到期时间必须晚于开始时间。"})
|
|
|
|
|
if (
|
|
|
|
|
self.grace_expires_at
|
|
|
|
|
and self.expires_at
|
|
|
|
|
and self.grace_expires_at < self.expires_at
|
|
|
|
|
):
|
|
|
|
|
raise ValidationError({"grace_expires_at": "宽限截止时间不能早于到期时间。"})
|
|
|
|
|
|
|
|
|
|
def is_usable_at(self, now=None) -> bool:
|
|
|
|
|
now = now or timezone.now()
|
|
|
|
|
return self.status == self.Status.ACTIVE and self.grace_expires_at > now
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 11:52:49 +08:00
|
|
|
class SoftwareOrder(models.Model):
|
|
|
|
|
class PayMethod(models.TextChoices):
|
|
|
|
|
WEIXIN = "weixin", "微信"
|
|
|
|
|
|
|
|
|
|
class Status(models.TextChoices):
|
|
|
|
|
PENDING = "pending", "待支付"
|
|
|
|
|
PAID = "paid", "已支付"
|
|
|
|
|
FAILED = "failed", "下单失败"
|
|
|
|
|
EXPIRED = "expired", "已过期"
|
|
|
|
|
|
|
|
|
|
user = models.ForeignKey(
|
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
|
verbose_name="用户",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="software_orders",
|
|
|
|
|
)
|
|
|
|
|
source_plan = models.ForeignKey(
|
|
|
|
|
SoftwarePlan,
|
|
|
|
|
verbose_name="来源套餐",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
related_name="software_orders",
|
|
|
|
|
)
|
|
|
|
|
entitlement = models.ForeignKey(
|
|
|
|
|
SoftwareEntitlement,
|
|
|
|
|
verbose_name="发放权益",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="software_orders",
|
|
|
|
|
)
|
|
|
|
|
fulfillment_event = models.OneToOneField(
|
|
|
|
|
"LicenseEvent",
|
|
|
|
|
verbose_name="权益发放事件",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="software_order",
|
|
|
|
|
)
|
|
|
|
|
order_no = models.CharField("软件订单号", max_length=64, unique=True, editable=False)
|
|
|
|
|
product_code = models.CharField(
|
|
|
|
|
"产品代码",
|
|
|
|
|
max_length=32,
|
|
|
|
|
choices=ClientDevice.ProductCode.choices,
|
|
|
|
|
)
|
|
|
|
|
plan_name = models.CharField("套餐名称快照", max_length=120)
|
|
|
|
|
plan_duration_days = models.PositiveIntegerField("套餐有效天数快照")
|
|
|
|
|
plan_price = models.DecimalField("套餐价格快照", max_digits=12, decimal_places=2)
|
|
|
|
|
plan_device_limit = models.PositiveSmallIntegerField("设备数量快照")
|
|
|
|
|
plan_grace_days = models.PositiveSmallIntegerField("宽限天数快照", default=0)
|
|
|
|
|
amount_money = models.DecimalField("支付金额", max_digits=12, decimal_places=2)
|
|
|
|
|
currency = models.CharField("币种", max_length=8, default="CNY")
|
|
|
|
|
pay_method = models.CharField("支付方式", max_length=20, choices=PayMethod.choices)
|
|
|
|
|
status = models.CharField(
|
|
|
|
|
"订单状态",
|
|
|
|
|
max_length=20,
|
|
|
|
|
choices=Status.choices,
|
|
|
|
|
default=Status.PENDING,
|
|
|
|
|
)
|
|
|
|
|
code_url = models.TextField("二维码票据", blank=True)
|
|
|
|
|
expires_at = models.DateTimeField("支付票据过期时间", null=True, blank=True)
|
|
|
|
|
payment_txn_no = models.CharField("支付交易号", max_length=128, null=True, blank=True)
|
|
|
|
|
paid_at = models.DateTimeField("支付时间", null=True, blank=True)
|
|
|
|
|
fulfilled_at = models.DateTimeField("权益发放时间", null=True, blank=True)
|
|
|
|
|
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
|
|
|
|
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "software_order"
|
|
|
|
|
verbose_name = "软件套餐订单"
|
|
|
|
|
verbose_name_plural = "软件套餐订单"
|
|
|
|
|
ordering = ("-created_at", "-id")
|
|
|
|
|
constraints = [
|
|
|
|
|
models.UniqueConstraint(
|
|
|
|
|
fields=("pay_method", "payment_txn_no"),
|
|
|
|
|
name="software_order_pay_method_txn_unique",
|
|
|
|
|
),
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(plan_duration_days__gt=0),
|
|
|
|
|
name="software_order_duration_positive",
|
|
|
|
|
),
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(plan_price__gt=0),
|
|
|
|
|
name="software_order_plan_price_positive",
|
|
|
|
|
),
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(plan_device_limit__gt=0),
|
|
|
|
|
name="software_order_device_limit_positive",
|
|
|
|
|
),
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(amount_money__gt=0),
|
|
|
|
|
name="software_order_amount_positive",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("user", "product_code", "status")),
|
|
|
|
|
models.Index(fields=("status", "expires_at")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.order_no} {self.user} {self.plan_name}"
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 09:51:29 +08:00
|
|
|
class LicenseSeat(models.Model):
|
|
|
|
|
entitlement = models.ForeignKey(
|
|
|
|
|
SoftwareEntitlement,
|
|
|
|
|
verbose_name="软件权益",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="seats",
|
|
|
|
|
)
|
|
|
|
|
seat_number = models.PositiveSmallIntegerField("席位序号")
|
|
|
|
|
device = models.ForeignKey(
|
|
|
|
|
ClientDevice,
|
|
|
|
|
verbose_name="绑定设备",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
related_name="license_seats",
|
|
|
|
|
)
|
|
|
|
|
bound_at = models.DateTimeField("绑定时间", null=True, blank=True)
|
|
|
|
|
released_at = models.DateTimeField("解绑时间", null=True, blank=True)
|
|
|
|
|
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
|
|
|
|
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "license_seat"
|
|
|
|
|
verbose_name = "授权席位"
|
|
|
|
|
verbose_name_plural = "授权席位"
|
|
|
|
|
ordering = ("entitlement_id", "seat_number")
|
|
|
|
|
constraints = [
|
|
|
|
|
models.UniqueConstraint(
|
|
|
|
|
fields=("entitlement", "seat_number"),
|
|
|
|
|
name="license_seat_entitlement_number_unique",
|
|
|
|
|
),
|
|
|
|
|
models.CheckConstraint(
|
|
|
|
|
condition=Q(seat_number__gt=0),
|
|
|
|
|
name="license_seat_number_positive",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("device", "updated_at")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.entitlement} #{self.seat_number}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LicenseEvent(models.Model):
|
|
|
|
|
class Action(models.TextChoices):
|
|
|
|
|
GRANTED = "granted", "人工授予"
|
|
|
|
|
RENEWED = "renewed", "续期"
|
|
|
|
|
REVOKED = "revoked", "撤销"
|
|
|
|
|
SEAT_ASSIGNED = "seat_assigned", "绑定席位"
|
|
|
|
|
SEAT_RELEASED = "seat_released", "解绑席位"
|
2026-07-21 10:06:51 +08:00
|
|
|
MIGRATION_GRANTED = "migration_granted", "迁移资格授予"
|
|
|
|
|
CREDENTIAL_ISSUED = "credential_issued", "设备凭证签发"
|
|
|
|
|
CREDENTIAL_REVOKED = "credential_revoked", "设备凭证吊销"
|
2026-07-21 11:52:49 +08:00
|
|
|
ORDER_FULFILLED = "order_fulfilled", "套餐订单权益发放"
|
2026-07-21 09:51:29 +08:00
|
|
|
|
|
|
|
|
entitlement = models.ForeignKey(
|
|
|
|
|
SoftwareEntitlement,
|
|
|
|
|
verbose_name="软件权益",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="events",
|
|
|
|
|
)
|
|
|
|
|
seat = models.ForeignKey(
|
|
|
|
|
LicenseSeat,
|
|
|
|
|
verbose_name="授权席位",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
related_name="events",
|
|
|
|
|
)
|
|
|
|
|
device = models.ForeignKey(
|
|
|
|
|
ClientDevice,
|
|
|
|
|
verbose_name="关联设备",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
related_name="license_events",
|
|
|
|
|
)
|
|
|
|
|
actor = models.ForeignKey(
|
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
|
verbose_name="操作人",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
related_name="license_events_performed",
|
|
|
|
|
)
|
|
|
|
|
action = models.CharField("动作", max_length=32, choices=Action.choices)
|
|
|
|
|
reason = models.CharField("原因", max_length=255)
|
|
|
|
|
metadata = models.JSONField("附加信息", default=dict, blank=True)
|
|
|
|
|
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "license_event"
|
|
|
|
|
verbose_name = "授权事件"
|
|
|
|
|
verbose_name_plural = "授权事件"
|
|
|
|
|
ordering = ("-created_at", "-id")
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("entitlement", "created_at")),
|
|
|
|
|
models.Index(fields=("device", "created_at")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.entitlement} {self.action}"
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 10:06:51 +08:00
|
|
|
class LegacyMigrationGrant(models.Model):
|
|
|
|
|
class Status(models.TextChoices):
|
|
|
|
|
ACTIVE = "active", "有效"
|
|
|
|
|
REVOKED = "revoked", "已撤销"
|
|
|
|
|
|
|
|
|
|
user = models.ForeignKey(
|
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
|
verbose_name="用户",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="legacy_migration_grants",
|
|
|
|
|
)
|
|
|
|
|
product_code = models.CharField(
|
|
|
|
|
"产品代码",
|
|
|
|
|
max_length=32,
|
|
|
|
|
choices=ClientDevice.ProductCode.choices,
|
|
|
|
|
)
|
|
|
|
|
entitlement = models.OneToOneField(
|
|
|
|
|
SoftwareEntitlement,
|
|
|
|
|
verbose_name="迁移权益",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="legacy_migration_grant",
|
|
|
|
|
)
|
|
|
|
|
eligibility_snapshot = models.JSONField("资格快照", default=dict, blank=True)
|
|
|
|
|
status = models.CharField(
|
|
|
|
|
"状态",
|
|
|
|
|
max_length=20,
|
|
|
|
|
choices=Status.choices,
|
|
|
|
|
default=Status.ACTIVE,
|
|
|
|
|
)
|
|
|
|
|
reason = models.CharField("迁移原因", max_length=255)
|
|
|
|
|
actor = models.ForeignKey(
|
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
|
verbose_name="操作人",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
related_name="legacy_migration_grants_performed",
|
|
|
|
|
)
|
|
|
|
|
revoked_at = models.DateTimeField("撤销时间", null=True, blank=True)
|
|
|
|
|
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
|
|
|
|
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "legacy_migration_grant"
|
|
|
|
|
verbose_name = "存量迁移资格"
|
|
|
|
|
verbose_name_plural = "存量迁移资格"
|
|
|
|
|
ordering = ("-created_at", "-id")
|
|
|
|
|
constraints = [
|
|
|
|
|
models.UniqueConstraint(
|
|
|
|
|
fields=("user", "product_code"),
|
|
|
|
|
name="legacy_migration_grant_user_product_unique",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("product_code", "status")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.user} {self.product_code} 迁移资格"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MigrationRequest(models.Model):
|
|
|
|
|
class Status(models.TextChoices):
|
|
|
|
|
PENDING = "pending", "待确认"
|
|
|
|
|
CONFIRMED = "confirmed", "已确认"
|
|
|
|
|
EXPIRED = "expired", "已过期"
|
|
|
|
|
REVOKED = "revoked", "已撤销"
|
|
|
|
|
|
|
|
|
|
request_id = models.UUIDField("公开迁移请求 ID", default=uuid.uuid4, unique=True, editable=False)
|
|
|
|
|
user = models.ForeignKey(
|
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
|
verbose_name="用户",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="migration_requests",
|
|
|
|
|
)
|
|
|
|
|
device = models.ForeignKey(
|
|
|
|
|
ClientDevice,
|
|
|
|
|
verbose_name="当前设备",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="migration_requests",
|
|
|
|
|
)
|
|
|
|
|
migration_grant = models.ForeignKey(
|
|
|
|
|
LegacyMigrationGrant,
|
|
|
|
|
verbose_name="迁移资格",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="requests",
|
|
|
|
|
)
|
|
|
|
|
credential_token_hash = models.CharField("待签发凭证哈希", max_length=64, unique=True, editable=False)
|
|
|
|
|
credential_token_prefix = models.CharField("待签发凭证前缀", max_length=20, editable=False)
|
|
|
|
|
status = models.CharField(
|
|
|
|
|
"状态",
|
|
|
|
|
max_length=20,
|
|
|
|
|
choices=Status.choices,
|
|
|
|
|
default=Status.PENDING,
|
|
|
|
|
)
|
|
|
|
|
expires_at = models.DateTimeField("确认截止时间")
|
|
|
|
|
confirmed_at = models.DateTimeField("确认时间", null=True, blank=True)
|
|
|
|
|
revoked_at = models.DateTimeField("撤销时间", null=True, blank=True)
|
|
|
|
|
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
|
|
|
|
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "migration_request"
|
|
|
|
|
verbose_name = "迁移确认请求"
|
|
|
|
|
verbose_name_plural = "迁移确认请求"
|
|
|
|
|
ordering = ("-created_at", "-id")
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("user", "device", "status")),
|
|
|
|
|
models.Index(fields=("status", "expires_at")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.user} {self.device} {self.status}"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def generate_plaintext_credential_token() -> str:
|
|
|
|
|
return f"dvc_cmhub_{secrets.token_urlsafe(32)}"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def hash_credential_token(raw_token: str) -> str:
|
|
|
|
|
return hashlib.sha256(raw_token.encode("utf-8")).hexdigest()
|
|
|
|
|
|
|
|
|
|
def is_pending_at(self, now=None) -> bool:
|
|
|
|
|
now = now or timezone.now()
|
|
|
|
|
return self.status == self.Status.PENDING and self.expires_at > now
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeviceCredential(models.Model):
|
|
|
|
|
user = models.ForeignKey(
|
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
|
verbose_name="用户",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="device_credentials",
|
|
|
|
|
)
|
|
|
|
|
product_code = models.CharField(
|
|
|
|
|
"产品代码",
|
|
|
|
|
max_length=32,
|
|
|
|
|
choices=ClientDevice.ProductCode.choices,
|
|
|
|
|
)
|
|
|
|
|
device = models.ForeignKey(
|
|
|
|
|
ClientDevice,
|
|
|
|
|
verbose_name="设备",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="credentials",
|
|
|
|
|
)
|
|
|
|
|
entitlement = models.ForeignKey(
|
|
|
|
|
SoftwareEntitlement,
|
|
|
|
|
verbose_name="软件权益",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="device_credentials",
|
|
|
|
|
)
|
|
|
|
|
seat = models.ForeignKey(
|
|
|
|
|
LicenseSeat,
|
|
|
|
|
verbose_name="授权席位",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="device_credentials",
|
|
|
|
|
)
|
|
|
|
|
migration_request = models.OneToOneField(
|
|
|
|
|
MigrationRequest,
|
|
|
|
|
verbose_name="来源迁移请求",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="credential",
|
|
|
|
|
)
|
|
|
|
|
token_hash = models.CharField("设备凭证哈希", max_length=64, unique=True, editable=False)
|
|
|
|
|
token_prefix = models.CharField("设备凭证前缀", max_length=20, editable=False)
|
|
|
|
|
expires_at = models.DateTimeField("凭证到期时间")
|
|
|
|
|
revoked_at = models.DateTimeField("吊销时间", null=True, blank=True)
|
|
|
|
|
revoke_reason = models.CharField("吊销原因", max_length=255, blank=True)
|
|
|
|
|
created_at = models.DateTimeField("签发时间", auto_now_add=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "device_credential"
|
|
|
|
|
verbose_name = "设备凭证"
|
|
|
|
|
verbose_name_plural = "设备凭证"
|
|
|
|
|
ordering = ("-created_at", "-id")
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("user", "product_code", "revoked_at")),
|
|
|
|
|
models.Index(fields=("device", "revoked_at")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.user} {self.product_code} credential"
|
|
|
|
|
|
|
|
|
|
def is_active_at(self, now=None) -> bool:
|
|
|
|
|
now = now or timezone.now()
|
|
|
|
|
return self.revoked_at is None and self.expires_at > now
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 09:27:05 +08:00
|
|
|
class DeviceSession(models.Model):
|
|
|
|
|
TOKEN_PREFIX_LENGTH = 12
|
|
|
|
|
|
|
|
|
|
device = models.ForeignKey(
|
|
|
|
|
ClientDevice,
|
|
|
|
|
verbose_name="客户端设备",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="sessions",
|
|
|
|
|
)
|
|
|
|
|
token_hash = models.CharField("会话令牌哈希", max_length=64, unique=True, editable=False)
|
|
|
|
|
expires_at = models.DateTimeField("过期时间")
|
|
|
|
|
last_used_at = models.DateTimeField("最后使用时间", null=True, blank=True)
|
|
|
|
|
revoked_at = models.DateTimeField("吊销时间", null=True, blank=True)
|
|
|
|
|
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "device_session"
|
|
|
|
|
verbose_name = "设备会话"
|
|
|
|
|
verbose_name_plural = "设备会话"
|
|
|
|
|
ordering = ("-created_at", "-id")
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("device", "expires_at")),
|
|
|
|
|
models.Index(fields=("expires_at", "revoked_at")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.device} session"
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def generate_plaintext_token(cls) -> str:
|
|
|
|
|
return f"dvs_cmhub_{secrets.token_urlsafe(32)}"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def hash_token(raw_token: str) -> str:
|
|
|
|
|
return hashlib.sha256(raw_token.encode("utf-8")).hexdigest()
|
|
|
|
|
|
|
|
|
|
def matches_token(self, raw_token: str) -> bool:
|
|
|
|
|
return hmac.compare_digest(self.token_hash, self.hash_token(raw_token))
|
|
|
|
|
|
|
|
|
|
def is_active_at(self, now=None) -> bool:
|
|
|
|
|
now = now or timezone.now()
|
|
|
|
|
return self.revoked_at is None and self.expires_at > now
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeviceBindingAudit(models.Model):
|
|
|
|
|
class Action(models.TextChoices):
|
|
|
|
|
REGISTERED = "registered", "已登记"
|
|
|
|
|
SESSION_ISSUED = "session_issued", "会话已签发"
|
|
|
|
|
HEARTBEAT = "heartbeat", "心跳"
|
|
|
|
|
REJECTED = "rejected", "请求被拒绝"
|
|
|
|
|
REVOKED = "revoked", "设备已吊销"
|
|
|
|
|
|
|
|
|
|
user = models.ForeignKey(
|
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
|
verbose_name="用户",
|
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
|
related_name="device_binding_audits",
|
|
|
|
|
)
|
|
|
|
|
device = models.ForeignKey(
|
|
|
|
|
ClientDevice,
|
|
|
|
|
verbose_name="客户端设备",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
related_name="audits",
|
|
|
|
|
)
|
|
|
|
|
api_key = models.ForeignKey(
|
|
|
|
|
"users.ApiKey",
|
|
|
|
|
verbose_name="来源 API 密钥",
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
related_name="device_binding_audits",
|
|
|
|
|
)
|
|
|
|
|
action = models.CharField("动作", max_length=32, choices=Action.choices)
|
|
|
|
|
reason = models.CharField("原因", max_length=120, blank=True)
|
|
|
|
|
client_version = models.CharField("客户端版本", max_length=64, blank=True)
|
|
|
|
|
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
db_table = "device_binding_audit"
|
|
|
|
|
verbose_name = "设备绑定审计"
|
|
|
|
|
verbose_name_plural = "设备绑定审计"
|
|
|
|
|
ordering = ("-created_at", "-id")
|
|
|
|
|
indexes = [
|
|
|
|
|
models.Index(fields=("user", "created_at")),
|
|
|
|
|
models.Index(fields=("device", "created_at")),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"{self.user} {self.action}"
|