Kamixitong/app/models/license.py
2025-11-19 22:49:24 +08:00

305 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import datetime, timedelta
from app import db
import hashlib
import secrets
import string
from app.models.version import Version
from app.models.device import Device
class License(db.Model):
"""许可证(卡密)模型"""
__tablename__ = 'license'
license_id = db.Column(db.Integer, primary_key=True)
license_key = db.Column(db.String(35), unique=True, nullable=False, index=True) # 32字符+3个连字符
product_id = db.Column(db.String(32), db.ForeignKey('product.product_id'), nullable=False)
type = db.Column(db.Integer, nullable=False, default=1) # 0=试用, 1=正式
status = db.Column(db.Integer, nullable=False, default=0) # 0=未激活, 1=已激活, 2=已过期, 3=已禁用
valid_days = db.Column(db.Integer, nullable=False) # 有效期(天,-1=永久)
bind_machine_code = db.Column(db.String(64), nullable=True)
activate_time = db.Column(db.DateTime, nullable=True)
expire_time = db.Column(db.DateTime, nullable=True)
last_verify_time = db.Column(db.DateTime, nullable=True)
unbind_count = db.Column(db.Integer, default=0) # 解绑次数
create_time = db.Column(db.DateTime, default=datetime.utcnow)
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联关系
devices = db.relationship('Device', backref='license', lazy='dynamic', cascade='all, delete-orphan')
def __init__(self, **kwargs):
super(License, self).__init__(**kwargs)
if not self.license_key:
self.license_key = self.generate_license_key()
@classmethod
def generate_license_key(cls, length=32, prefix=''):
"""生成卡密格式XXXX-XXXX-XXXX-XXXX"""
# 生成随机字符串
chars = string.ascii_uppercase + string.digits
# 生成32位字符4组每组8位
random_chars = ''.join(secrets.choice(chars) for _ in range(32 - len(prefix)))
# 格式化为XXXX-XXXX-XXXX-XXXX格式
formatted_key = '-'.join([
random_chars[i:i+8] for i in range(0, len(random_chars), 8)
])
# 组合前缀和格式化后的密钥
license_key = prefix + formatted_key if prefix else formatted_key
# 确保唯一性
while cls.query.filter_by(license_key=license_key).first():
random_chars = ''.join(secrets.choice(chars) for _ in range(32 - len(prefix)))
formatted_key = '-'.join([
random_chars[i:i+8] for i in range(0, len(random_chars), 8)
])
license_key = prefix + formatted_key if prefix else formatted_key
return license_key
@classmethod
def generate_batch(cls, product_id, count, license_type=1, valid_days=365, prefix='', length=32):
"""批量生成卡密"""
licenses = []
for _ in range(count):
license_obj = cls(
product_id=product_id,
type=license_type,
valid_days=valid_days,
status=0 # 未激活
)
# 注意我们忽略length参数因为我们总是生成32个字符+3个连字符的格式
if prefix:
license_obj.license_key = cls.generate_license_key(32, prefix)
licenses.append(license_obj)
return licenses
def is_active(self):
"""是否已激活"""
return self.status == 1
def is_expired(self):
"""是否已过期"""
if self.valid_days == -1: # 永久
return False
if not self.expire_time:
return False
return datetime.utcnow() > self.expire_time
def is_trial(self):
"""是否为试用卡密"""
return self.type == 0
def is_formal(self):
"""是否为正式卡密"""
return self.type == 1
def can_activate(self):
"""是否可以激活"""
return self.status == 0 and not self.is_expired()
def can_unbind(self, max_unbind_times=3):
"""是否可以解绑"""
return self.unbind_count < max_unbind_times
def activate(self, machine_code, software_version):
"""激活卡密"""
if not self.can_activate():
return False, "卡密状态不允许激活"
if self.bind_machine_code and self.bind_machine_code != machine_code:
return False, "卡密已绑定其他设备"
# 绑定机器码
self.bind_machine_code = machine_code
self.status = 1
self.activate_time = datetime.utcnow()
self.last_verify_time = datetime.utcnow()
# 计算过期时间
if self.valid_days != -1:
self.expire_time = self.activate_time + timedelta(days=self.valid_days)
# 创建设备记录
from app.models.device import Device
device = Device(
machine_code=machine_code,
license_id=self.license_id,
product_id=self.product_id,
software_version=software_version,
status=1,
activate_time=datetime.utcnow(),
last_verify_time=datetime.utcnow()
)
db.session.add(device)
db.session.commit()
return True, "激活成功"
def verify(self, machine_code, software_version):
"""验证卡密"""
# 检查状态
if self.status == 3:
return False, "卡密已禁用"
# 检查过期
if self.is_expired():
self.status = 2 # 标记为已过期
db.session.commit()
return False, "卡密已过期"
# 检查绑定关系
if self.status == 1: # 已激活
if self.bind_machine_code != machine_code:
return False, "设备不匹配"
# 检查设备是否被禁用
device = Device.query.filter_by(machine_code=machine_code, license_id=self.license_id).first()
if device and not device.is_active():
return False, "设备已被禁用"
# 检查版本兼容性
product = self.product
if product:
latest_version = product.versions.filter_by(publish_status=1).order_by(
db.desc(Version.create_time)
).first()
if latest_version and not latest_version.check_compatibility("1.0"):
return False, "版本不兼容"
# 更新最后验证时间
self.last_verify_time = datetime.utcnow()
db.session.commit()
return True, "验证通过"
def unbind(self):
"""解绑设备"""
if not self.can_unbind():
return False, "解绑次数已达上限"
# 更新卡密状态
self.status = 0 # 未激活
self.bind_machine_code = None
self.activate_time = None
self.expire_time = None
self.unbind_count += 1
# 禁用关联设备
self.devices.update({'status': 0})
db.session.commit()
return True, "解绑成功"
def extend_validity(self, days):
"""延长有效期"""
if self.is_trial():
return False, "试用卡密不能延长有效期"
if self.valid_days == -1:
return False, "永久卡密无需延长"
if self.expire_time:
self.expire_time = self.expire_time + timedelta(days=days)
elif self.activate_time:
self.expire_time = self.activate_time + timedelta(days=self.valid_days + days)
else:
self.valid_days += days
db.session.commit()
return True, "延期成功"
def convert_to_formal(self, valid_days):
"""试用转正式"""
if not self.is_trial():
return False, "只有试用卡密才能转换"
if self.status != 1:
return False, "只有已激活的试用卡密才能转换"
self.type = 1
self.valid_days = valid_days
self.expire_time = datetime.utcnow() + timedelta(days=valid_days)
db.session.commit()
return True, "转换成功"
def get_remaining_days(self):
"""获取剩余天数"""
if self.valid_days == -1:
return -1 # 永久
if not self.expire_time:
return self.valid_days
remaining = self.expire_time - datetime.utcnow()
return max(0, remaining.days)
def get_status_name(self):
"""获取状态名称"""
status_map = {
0: '未激活',
1: '已激活',
2: '已过期',
3: '已禁用'
}
return status_map.get(self.status, '未知')
def get_duration_type(self):
"""获取时长类型"""
if self.valid_days == -1:
return '永久卡'
elif self.valid_days == 1:
return '天卡'
elif self.valid_days == 30:
return '月卡'
elif self.valid_days == 90:
return '季卡'
elif self.valid_days == 365:
return '年卡'
else:
return f'{self.valid_days}天卡'
def to_dict(self):
"""转换为字典"""
# 获取绑定的设备信息
device_info = None
if self.bind_machine_code and self.devices:
device = self.devices.first()
if device:
device_info = {
'machine_code': device.machine_code,
'ip_address': device.ip_address
}
return {
'license_id': self.license_id,
'license_key': self.license_key,
'product_id': self.product_id,
'product_name': self.product.product_name if self.product else None,
'type': self.type,
'type_name': '试用' if self.type == 0 else '正式',
'status': self.status,
'status_name': {
0: '未激活',
1: '已激活',
2: '已过期',
3: '已禁用'
}.get(self.status, '未知'),
'valid_days': self.valid_days,
'duration_type': self.get_duration_type(),
'bind_machine_code': self.bind_machine_code,
'device_info': device_info, # 添加设备信息字段
'activate_time': self.activate_time.strftime('%Y-%m-%d %H:%M:%S') if self.activate_time else None,
'expire_time': self.expire_time.strftime('%Y-%m-%d %H:%M:%S') if self.expire_time else None,
'last_verify_time': self.last_verify_time.strftime('%Y-%m-%d %H:%M:%S') if self.last_verify_time else None,
'unbind_count': self.unbind_count,
'remaining_days': self.get_remaining_days(),
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time else None,
'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S') if self.update_time else None
}
def __repr__(self):
return f'<License {self.license_key}>'