Kamixitong/app/models/license.py
2025-12-28 16:34:34 +08:00

396 lines
15 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) # 解绑次数
remark = db.Column(db.Text, nullable=True) # 备注
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
# 先查找是否已存在该设备的该软件实例
# 注意:现在通过(machine_code, product_id)组合来查找
# 允许同一个machine_code对应不同product_id的设备记录
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)
db.session.commit()
return True, "激活成功"
def verify(self, machine_code, software_version):
"""验证卡密"""
# 将整个方法包装在 try-except 中,确保任何异常都不会传播
try:
try:
# 参数验证
if not machine_code:
return False, "机器码不能为空"
if not software_version:
software_version = "1.0.0"
# 检查状态
if self.status == 3:
return False, "卡密已禁用"
# 检查过期
if self.is_expired():
# 只更新内存中的状态,不提交事务
# 在验证过程中不应该修改数据库
try:
self.status = 2 # 标记为已过期
except Exception as e:
# 更新状态失败不影响验证结果,静默处理
pass
return False, "卡密已过期"
# 检查绑定关系
if self.status == 1: # 已激活
if self.bind_machine_code != machine_code:
return False, "设备不匹配"
# 检查设备是否被禁用 - 添加异常处理
try:
if self.license_id:
device = Device.query.filter_by(machine_code=machine_code, license_id=self.license_id).first()
if device and not device.is_active():
return False, "设备已被禁用"
except Exception as e:
# 查询失败时跳过设备检查,避免阻塞验证
pass
# 检查版本兼容性 - 添加异常处理
try:
product = self.product
if product:
try:
latest_version = product.versions.filter_by(publish_status=1).order_by(
db.desc(Version.create_time)
).first()
if latest_version:
if not latest_version.check_compatibility("1.0"):
return False, "版本不兼容"
except Exception as e:
# 版本检查失败时默认允许通过
pass
except Exception as e:
# 产品信息获取失败时跳过版本检查
pass
# 更新最后验证时间 - 使用最安全的方式
try:
# 只更新内存中的值,不提交事务,避免事务问题
self.last_verify_time = datetime.utcnow()
# 标记为已修改,但不在验证过程中提交
# db.session.add(self) # 不需要SQLAlchemy会自动跟踪变化
except Exception as e:
# 更新失败不影响验证结果
pass
return True, "验证通过"
except Exception as e:
# 内部异常处理,记录但不抛出
try:
import traceback
error_trace = traceback.format_exc()
print(f"卡密验证内部异常: {str(e)}\n{error_trace}")
except:
pass
return False, "验证过程出错,请联系管理员"
except Exception as e:
# 最外层异常处理,确保任何异常都不会传播
try:
import traceback
error_trace = traceback.format_exc()
print(f"卡密验证最外层异常: {str(e)}\n{error_trace}")
except:
pass
return False, "验证过程出错,请联系管理员"
def unbind(self):
"""解绑设备"""
# 检查是否可以解绑 - 避免在模型中使用current_app
try:
# 安全获取配置值
from flask import current_app
max_unbind_times = current_app.config.get('MAX_UNBIND_TIMES', 3)
except:
# 如果无法获取配置,使用默认值
max_unbind_times = 3
if not self.can_unbind(max_unbind_times):
return False, f"解绑次数已达上限({max_unbind_times}次)"
# 更新卡密状态
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 disable(self):
"""禁用卡密"""
# 更新卡密状态为已禁用(不同时解绑)
self.status = 3
# 不修改其他字段,只禁用
# 禁用关联设备
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}>'