feat: add software entitlement foundations

This commit is contained in:
QiuSW
2026-07-21 09:51:29 +08:00
parent 11c0d63ac8
commit 7b80ae3a7a
12 changed files with 1191 additions and 13 deletions
@@ -0,0 +1,149 @@
# Generated by Django 5.2.15 on 2026-07-21 01:45
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('licensing', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='SoftwareEntitlement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product_code', models.CharField(choices=[('cmshopee', '虾皮圈优化助手')], max_length=32, verbose_name='产品代码')),
('plan_name', models.CharField(max_length=120, verbose_name='套餐名称快照')),
('plan_duration_days', models.PositiveIntegerField(verbose_name='套餐有效天数快照')),
('plan_price', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='套餐价格快照')),
('plan_device_limit', models.PositiveSmallIntegerField(verbose_name='设备数量快照')),
('plan_grace_days', models.PositiveSmallIntegerField(default=0, verbose_name='宽限天数快照')),
('status', models.CharField(choices=[('active', '有效'), ('revoked', '已撤销'), ('expired', '已过期')], default='active', max_length=20, verbose_name='状态')),
('starts_at', models.DateTimeField(verbose_name='开始时间')),
('expires_at', models.DateTimeField(verbose_name='到期时间')),
('grace_expires_at', models.DateTimeField(verbose_name='宽限截止时间')),
('revoked_at', models.DateTimeField(blank=True, null=True, verbose_name='撤销时间')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='software_entitlements', to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],
options={
'verbose_name': '软件权益',
'verbose_name_plural': '软件权益',
'db_table': 'software_entitlement',
'ordering': ('-expires_at', '-id'),
},
),
migrations.CreateModel(
name='LicenseSeat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('seat_number', models.PositiveSmallIntegerField(verbose_name='席位序号')),
('bound_at', models.DateTimeField(blank=True, null=True, verbose_name='绑定时间')),
('released_at', models.DateTimeField(blank=True, null=True, verbose_name='解绑时间')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='license_seats', to='licensing.clientdevice', verbose_name='绑定设备')),
('entitlement', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='seats', to='licensing.softwareentitlement', verbose_name='软件权益')),
],
options={
'verbose_name': '授权席位',
'verbose_name_plural': '授权席位',
'db_table': 'license_seat',
'ordering': ('entitlement_id', 'seat_number'),
},
),
migrations.CreateModel(
name='LicenseEvent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action', models.CharField(choices=[('granted', '人工授予'), ('renewed', '续期'), ('revoked', '撤销'), ('seat_assigned', '绑定席位'), ('seat_released', '解绑席位')], max_length=32, verbose_name='动作')),
('reason', models.CharField(max_length=255, verbose_name='原因')),
('metadata', models.JSONField(blank=True, default=dict, verbose_name='附加信息')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='license_events_performed', to=settings.AUTH_USER_MODEL, verbose_name='操作人')),
('device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='license_events', to='licensing.clientdevice', verbose_name='关联设备')),
('seat', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='events', to='licensing.licenseseat', verbose_name='授权席位')),
('entitlement', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='events', to='licensing.softwareentitlement', verbose_name='软件权益')),
],
options={
'verbose_name': '授权事件',
'verbose_name_plural': '授权事件',
'db_table': 'license_event',
'ordering': ('-created_at', '-id'),
},
),
migrations.CreateModel(
name='SoftwarePlan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product_code', models.CharField(choices=[('cmshopee', '虾皮圈优化助手')], max_length=32, verbose_name='产品代码')),
('name', models.CharField(max_length=120, verbose_name='套餐名称')),
('duration_days', models.PositiveIntegerField(verbose_name='有效天数')),
('price', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='套餐价格')),
('device_limit', models.PositiveSmallIntegerField(verbose_name='设备数量')),
('grace_days', models.PositiveSmallIntegerField(default=0, verbose_name='宽限天数')),
('status', models.CharField(choices=[('active', '启用'), ('inactive', '停用')], default='active', max_length=20, verbose_name='状态')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '软件套餐',
'verbose_name_plural': '软件套餐',
'db_table': 'software_plan',
'ordering': ('product_code', 'name', 'id'),
'indexes': [models.Index(fields=['product_code', 'status'], name='software_pl_product_3580ff_idx')],
'constraints': [models.CheckConstraint(condition=models.Q(('duration_days__gt', 0)), name='software_plan_duration_days_positive'), models.CheckConstraint(condition=models.Q(('price__gt', 0)), name='software_plan_price_positive'), models.CheckConstraint(condition=models.Q(('device_limit__gt', 0)), name='software_plan_device_limit_positive')],
},
),
migrations.AddField(
model_name='softwareentitlement',
name='source_plan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='entitlements', to='licensing.softwareplan', verbose_name='来源套餐'),
),
migrations.AddIndex(
model_name='licenseseat',
index=models.Index(fields=['device', 'updated_at'], name='license_sea_device__85c03f_idx'),
),
migrations.AddConstraint(
model_name='licenseseat',
constraint=models.UniqueConstraint(fields=('entitlement', 'seat_number'), name='license_seat_entitlement_number_unique'),
),
migrations.AddConstraint(
model_name='licenseseat',
constraint=models.CheckConstraint(condition=models.Q(('seat_number__gt', 0)), name='license_seat_number_positive'),
),
migrations.AddIndex(
model_name='licenseevent',
index=models.Index(fields=['entitlement', 'created_at'], name='license_eve_entitle_a66e88_idx'),
),
migrations.AddIndex(
model_name='licenseevent',
index=models.Index(fields=['device', 'created_at'], name='license_eve_device__e70841_idx'),
),
migrations.AddIndex(
model_name='softwareentitlement',
index=models.Index(fields=['user', 'product_code', 'status'], name='software_en_user_id_cd6e26_idx'),
),
migrations.AddIndex(
model_name='softwareentitlement',
index=models.Index(fields=['product_code', 'expires_at'], name='software_en_product_c6ffbf_idx'),
),
migrations.AddConstraint(
model_name='softwareentitlement',
constraint=models.CheckConstraint(condition=models.Q(('plan_duration_days__gt', 0)), name='software_entitlement_duration_positive'),
),
migrations.AddConstraint(
model_name='softwareentitlement',
constraint=models.CheckConstraint(condition=models.Q(('plan_price__gt', 0)), name='software_entitlement_price_positive'),
),
migrations.AddConstraint(
model_name='softwareentitlement',
constraint=models.CheckConstraint(condition=models.Q(('plan_device_limit__gt', 0)), name='software_entitlement_device_limit_positive'),
),
]