2025-11-11 21:39:12 +08:00
|
|
|
|
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) # 解绑次数
|
2025-12-12 11:35:14 +08:00
|
|
|
|
remark = db.Column(db.Text, nullable=True) # 备注
|
2025-11-11 21:39:12 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2025-12-12 11:35:14 +08:00
|
|
|
|
# 创建或更新设备记录
|
2025-11-11 21:39:12 +08:00
|
|
|
|
from app.models.device import Device
|
2025-12-12 11:35:14 +08:00
|
|
|
|
# 先查找是否已存在该设备
|
|
|
|
|
|
device = Device.query.filter_by(machine_code=machine_code, product_id=self.product_id).first()
|
|
|
|
|
|
|
|
|
|
|
|
if device:
|
|
|
|
|
|
# 如果设备已存在,更新其信息
|
|
|
|
|
|
device.license_id = self.license_id
|
|
|
|
|
|
device.software_version = software_version
|
|
|
|
|
|
device.status = 1
|
|
|
|
|
|
device.activate_time = datetime.utcnow()
|
|
|
|
|
|
device.last_verify_time = datetime.utcnow()
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 如果设备不存在,创建新记录
|
|
|
|
|
|
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)
|
2025-11-11 21:39:12 +08:00
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
|
"""解绑设备"""
|
2025-12-12 11:35:14 +08:00
|
|
|
|
# 检查是否可以解绑
|
|
|
|
|
|
max_unbind_times = current_app.config.get('MAX_UNBIND_TIMES', 3)
|
|
|
|
|
|
if not self.can_unbind(max_unbind_times):
|
|
|
|
|
|
return False, f"解绑次数已达上限({max_unbind_times}次)"
|
2025-11-11 21:39:12 +08:00
|
|
|
|
|
|
|
|
|
|
# 更新卡密状态
|
|
|
|
|
|
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, "解绑成功"
|
|
|
|
|
|
|
2025-12-12 11:35:14 +08:00
|
|
|
|
def disable(self):
|
|
|
|
|
|
"""禁用卡密"""
|
|
|
|
|
|
# 更新卡密状态为已禁用
|
|
|
|
|
|
self.status = 3
|
|
|
|
|
|
|
|
|
|
|
|
# 同时解绑设备(但不重复提交)
|
|
|
|
|
|
# 更新卡密状态
|
|
|
|
|
|
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, "禁用成功"
|
|
|
|
|
|
|
2025-11-11 21:39:12 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2025-11-16 21:15:06 +08:00
|
|
|
|
def get_status_name(self):
|
|
|
|
|
|
"""获取状态名称"""
|
|
|
|
|
|
status_map = {
|
|
|
|
|
|
0: '未激活',
|
|
|
|
|
|
1: '已激活',
|
|
|
|
|
|
2: '已过期',
|
|
|
|
|
|
3: '已禁用'
|
|
|
|
|
|
}
|
|
|
|
|
|
return status_map.get(self.status, '未知')
|
|
|
|
|
|
|
2025-11-19 22:49:24 +08:00
|
|
|
|
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}天卡'
|
|
|
|
|
|
|
2025-11-11 21:39:12 +08:00
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
"""转换为字典"""
|
2025-11-19 22:49:24 +08:00
|
|
|
|
# 获取绑定的设备信息
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 21:39:12 +08:00
|
|
|
|
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,
|
2025-11-19 22:49:24 +08:00
|
|
|
|
'duration_type': self.get_duration_type(),
|
2025-11-11 21:39:12 +08:00
|
|
|
|
'bind_machine_code': self.bind_machine_code,
|
2025-11-19 22:49:24 +08:00
|
|
|
|
'device_info': device_info, # 添加设备信息字段
|
2025-11-11 21:39:12 +08:00
|
|
|
|
'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}>'
|