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 # 先查找是否已存在该设备 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): """验证卡密""" # 检查状态 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): """解绑设备""" # 检查是否可以解绑 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}次)" # 更新卡密状态 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.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''