第一次提交
This commit is contained in:
11
app/models/__init__.py
Normal file
11
app/models/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .admin import Admin
|
||||
from .product import Product
|
||||
from .version import Version
|
||||
from .license import License
|
||||
from .device import Device
|
||||
from .ticket import Ticket, TicketReply
|
||||
from .audit_log import AuditLog
|
||||
from .order import Order
|
||||
from .package import Package
|
||||
|
||||
__all__ = ['Admin', 'Product', 'Version', 'License', 'Device', 'Ticket', 'TicketReply', 'AuditLog', 'Order', 'Package']
|
||||
134
app/models/admin.py
Normal file
134
app/models/admin.py
Normal file
@@ -0,0 +1,134 @@
|
||||
from datetime import datetime
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from flask_login import UserMixin
|
||||
from app import db, login_manager
|
||||
import hashlib
|
||||
import hmac
|
||||
import base64
|
||||
|
||||
class Admin(UserMixin, db.Model):
|
||||
"""管理员模型"""
|
||||
__tablename__ = 'admin'
|
||||
|
||||
admin_id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(32), unique=True, nullable=False, index=True)
|
||||
password_hash = db.Column(db.String(256), nullable=False)
|
||||
email = db.Column(db.String(64), nullable=True)
|
||||
role = db.Column(db.Integer, nullable=False, default=0) # 0=普通管理员, 1=超级管理员
|
||||
status = db.Column(db.Integer, nullable=False, default=1) # 0=禁用, 1=正常
|
||||
is_deleted = db.Column(db.Integer, nullable=False, default=0) # 0=未删除, 1=已删除(软删除)
|
||||
create_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
delete_time = db.Column(db.DateTime, nullable=True) # 软删除时间
|
||||
last_login_time = db.Column(db.DateTime, nullable=True)
|
||||
last_login_ip = db.Column(db.String(32), nullable=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Admin, self).__init__(**kwargs)
|
||||
if self.password_hash is None and 'password' in kwargs:
|
||||
self.set_password(kwargs['password'])
|
||||
|
||||
def set_password(self, password):
|
||||
"""设置密码"""
|
||||
# 使用 pbkdf2:sha256 算法,确保哈希长度在合理范围内
|
||||
self.password_hash = generate_password_hash(password, method='pbkdf2:sha256')
|
||||
|
||||
def check_password(self, password):
|
||||
"""验证密码 - 简化版本,更可靠"""
|
||||
import logging
|
||||
|
||||
try:
|
||||
# 基本验证
|
||||
if not self.password_hash:
|
||||
logging.error(f"密码哈希为空 - 用户ID: {self.admin_id}, 用户名: {self.username}")
|
||||
return False
|
||||
|
||||
if not password:
|
||||
logging.warning(f"输入的密码为空 - 用户ID: {self.admin_id}, 用户名: {self.username}")
|
||||
return False
|
||||
|
||||
# 确保密码是字符串类型
|
||||
if not isinstance(password, str):
|
||||
password = str(password)
|
||||
|
||||
# 使用 Werkzeug 的标准方法验证密码
|
||||
result = check_password_hash(self.password_hash, password)
|
||||
|
||||
if result:
|
||||
logging.debug(f"密码验证成功 - 用户ID: {self.admin_id}, 用户名: {self.username}")
|
||||
return True
|
||||
else:
|
||||
logging.warning(f"密码验证失败 - 用户ID: {self.admin_id}, 用户名: {self.username}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"密码验证异常 - 用户ID: {self.admin_id}, 用户名: {self.username}, 错误: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
def is_super_admin(self):
|
||||
"""是否为超级管理员"""
|
||||
return self.role == 1
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
"""账号是否激活"""
|
||||
return self.status == 1
|
||||
|
||||
def update_last_login(self, ip_address):
|
||||
"""更新最后登录信息
|
||||
|
||||
注意:此方法不提交事务,调用者需要负责提交或回滚
|
||||
"""
|
||||
self.last_login_time = datetime.utcnow()
|
||||
self.last_login_ip = ip_address
|
||||
|
||||
def get_id(self):
|
||||
"""Flask-Login 需要的方法,返回用户唯一标识"""
|
||||
return str(self.admin_id)
|
||||
|
||||
# 为了兼容性,也可以添加 id 属性
|
||||
@property
|
||||
def id(self):
|
||||
"""Flask-Login 需要的 id 属性"""
|
||||
return self.admin_id
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'admin_id': self.admin_id,
|
||||
'username': self.username,
|
||||
'email': self.email,
|
||||
'role': self.role,
|
||||
'role_name': '超级管理员' if self.role == 1 else '普通管理员',
|
||||
'status': self.status,
|
||||
'status_name': '正常' if self.status == 1 else '禁用',
|
||||
'is_deleted': self.is_deleted,
|
||||
'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,
|
||||
'delete_time': self.delete_time.strftime('%Y-%m-%d %H:%M:%S') if self.delete_time else None,
|
||||
'last_login_time': self.last_login_time.strftime('%Y-%m-%d %H:%M:%S') if self.last_login_time else None,
|
||||
'last_login_ip': self.last_login_ip
|
||||
}
|
||||
|
||||
def soft_delete(self):
|
||||
"""软删除"""
|
||||
self.is_deleted = 1
|
||||
self.delete_time = datetime.utcnow()
|
||||
|
||||
@staticmethod
|
||||
def get_query():
|
||||
"""获取未删除的查询"""
|
||||
return Admin.query.filter(Admin.is_deleted == 0)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Admin {self.username}>'
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(admin_id):
|
||||
"""Flask-Login用户加载器"""
|
||||
try:
|
||||
# 只加载未删除且激活的用户
|
||||
admin = Admin.query.filter_by(admin_id=int(admin_id), is_deleted=0).first()
|
||||
return admin
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
77
app/models/audit_log.py
Normal file
77
app/models/audit_log.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
import json
|
||||
|
||||
class AuditLog(db.Model):
|
||||
"""审计日志模型"""
|
||||
__tablename__ = 'audit_log'
|
||||
|
||||
log_id = db.Column(db.Integer, primary_key=True)
|
||||
admin_id = db.Column(db.Integer, db.ForeignKey('admin.admin_id'), nullable=False)
|
||||
action = db.Column(db.String(32), nullable=False) # 操作类型:CREATE, UPDATE, DELETE, LOGIN, LOGOUT, TOGGLE_STATUS
|
||||
target_type = db.Column(db.String(32), nullable=False) # 目标类型:ADMIN, PRODUCT, LICENSE等
|
||||
target_id = db.Column(db.String(32), nullable=True) # 目标ID(修改为字符串类型以支持不同类型的ID)
|
||||
details = db.Column(db.Text, nullable=True) # 操作详情(JSON格式)
|
||||
ip_address = db.Column(db.String(32), nullable=True) # 操作IP地址
|
||||
user_agent = db.Column(db.String(256), nullable=True) # 用户代理
|
||||
create_time = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
# 关联管理员
|
||||
admin = db.relationship('Admin', backref=db.backref('audit_logs', lazy='dynamic'))
|
||||
|
||||
def __repr__(self):
|
||||
return f'<AuditLog {self.action} by {self.admin_id}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
# 解析details字段中的JSON字符串
|
||||
details_data = None
|
||||
if self.details:
|
||||
try:
|
||||
details_data = json.loads(self.details)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
details_data = self.details # 如果解析失败,返回原始字符串
|
||||
|
||||
return {
|
||||
'log_id': self.log_id,
|
||||
'admin_id': self.admin_id,
|
||||
'admin_username': self.admin.username if self.admin else None,
|
||||
'action': self.action,
|
||||
'target_type': self.target_type,
|
||||
'target_id': self.target_id,
|
||||
'details': details_data,
|
||||
'ip_address': self.ip_address,
|
||||
'user_agent': self.user_agent,
|
||||
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time else None
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def log_action(admin_id, action, target_type, target_id=None, details=None, ip_address=None, user_agent=None):
|
||||
"""记录审计日志"""
|
||||
from flask import current_app
|
||||
try:
|
||||
# 将details字典序列化为JSON字符串
|
||||
details_str = None
|
||||
if details is not None:
|
||||
if isinstance(details, dict):
|
||||
details_str = json.dumps(details, ensure_ascii=False)
|
||||
else:
|
||||
details_str = str(details)
|
||||
|
||||
log = AuditLog(
|
||||
admin_id=admin_id if admin_id is not None else 0, # 确保admin_id不为None
|
||||
action=action,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
details=details_str,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
if hasattr(current_app, 'logger'):
|
||||
current_app.logger.error(f"记录审计日志失败: {str(e)}")
|
||||
return False
|
||||
112
app/models/device.py
Normal file
112
app/models/device.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
|
||||
class Device(db.Model):
|
||||
"""设备模型"""
|
||||
__tablename__ = 'device'
|
||||
|
||||
device_id = db.Column(db.Integer, primary_key=True)
|
||||
# 修改:移除machine_code的unique约束,允许同一设备使用不同软件
|
||||
# 通过(machine_code, product_id)组合来唯一标识一个设备的软件实例
|
||||
machine_code = db.Column(db.String(64), nullable=False, index=True)
|
||||
license_id = db.Column(db.Integer, db.ForeignKey('license.license_id'), nullable=True)
|
||||
product_id = db.Column(db.String(32), db.ForeignKey('product.product_id'), nullable=False)
|
||||
software_version = db.Column(db.String(16), nullable=True)
|
||||
ip_address = db.Column(db.String(45), nullable=True) # 添加IP地址字段
|
||||
status = db.Column(db.Integer, nullable=False, default=1) # 0=禁用, 1=正常
|
||||
activate_time = db.Column(db.DateTime, nullable=True)
|
||||
last_verify_time = db.Column(db.DateTime, nullable=True)
|
||||
create_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Device, self).__init__(**kwargs)
|
||||
|
||||
def is_active(self):
|
||||
"""设备是否激活"""
|
||||
return self.status == 1
|
||||
|
||||
def is_online(self, days=7):
|
||||
"""设备是否在线(最近N天内有验证记录)"""
|
||||
if not self.last_verify_time:
|
||||
return False
|
||||
return (datetime.utcnow() - self.last_verify_time).days <= days
|
||||
|
||||
def activate(self, license_id, software_version):
|
||||
"""激活设备
|
||||
|
||||
注意:此方法不提交事务,调用者需要负责提交或回滚
|
||||
"""
|
||||
self.license_id = license_id
|
||||
self.software_version = software_version
|
||||
self.status = 1
|
||||
self.activate_time = datetime.utcnow()
|
||||
self.last_verify_time = datetime.utcnow()
|
||||
|
||||
def deactivate(self):
|
||||
"""禁用设备
|
||||
|
||||
注意:此方法不提交事务,调用者需要负责提交或回滚
|
||||
"""
|
||||
self.status = 0
|
||||
|
||||
def unbind_license(self):
|
||||
"""解绑设备与卡密的关联
|
||||
|
||||
注意:此方法不提交事务,调用者需要负责提交或回滚
|
||||
"""
|
||||
self.license_id = None
|
||||
self.status = 1
|
||||
|
||||
def update_verify_time(self):
|
||||
"""更新验证时间
|
||||
|
||||
注意:此方法不提交事务,调用者需要负责提交或回滚
|
||||
"""
|
||||
self.last_verify_time = datetime.utcnow()
|
||||
|
||||
def get_license_info(self):
|
||||
"""获取关联的卡密信息"""
|
||||
if self.license:
|
||||
return {
|
||||
'license_key': self.license.license_key,
|
||||
'type': self.license.type,
|
||||
'type_name': '试用' if self.license.type == 0 else '正式',
|
||||
'expire_time': self.license.expire_time.strftime('%Y-%m-%d %H:%M:%S') if self.license.expire_time else None
|
||||
}
|
||||
return None
|
||||
|
||||
def get_uptime_days(self):
|
||||
"""获取使用天数"""
|
||||
if not self.activate_time:
|
||||
return 0
|
||||
return (datetime.utcnow() - self.activate_time).days
|
||||
|
||||
def to_dict(self, include_license=False):
|
||||
"""转换为字典"""
|
||||
data = {
|
||||
'device_id': self.device_id,
|
||||
'machine_code': self.machine_code,
|
||||
'license_id': self.license_id,
|
||||
'license_key': self.license.license_key if self.license else None,
|
||||
'product_id': self.product_id,
|
||||
'product_name': self.product.product_name if self.product else None,
|
||||
'software_version': self.software_version,
|
||||
'ip_address': self.ip_address, # 添加IP地址字段
|
||||
'status': self.status,
|
||||
'status_name': '正常' if self.status == 1 else ('离线' if self.status == 2 else '禁用'),
|
||||
'is_online': self.is_online(),
|
||||
'activate_time': self.activate_time.strftime('%Y-%m-%d %H:%M:%S') if self.activate_time else None,
|
||||
'last_verify_time': self.last_verify_time.strftime('%Y-%m-%d %H:%M:%S') if self.last_verify_time else None,
|
||||
'uptime_days': self.get_uptime_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
|
||||
}
|
||||
|
||||
if include_license:
|
||||
data['license_info'] = self.get_license_info()
|
||||
|
||||
return data
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Device {self.machine_code}>'
|
||||
361
app/models/license.py
Normal file
361
app/models/license.py
Normal file
@@ -0,0 +1,361 @@
|
||||
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
|
||||
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 = []
|
||||
generated_keys = set()
|
||||
|
||||
for _ in range(count):
|
||||
max_attempts = 100
|
||||
for attempt in range(max_attempts):
|
||||
license_key = cls.generate_license_key(32, prefix)
|
||||
|
||||
if license_key in generated_keys:
|
||||
continue
|
||||
|
||||
existing = cls.query.filter_by(license_key=license_key).first()
|
||||
if existing:
|
||||
continue
|
||||
|
||||
generated_keys.add(license_key)
|
||||
break
|
||||
else:
|
||||
raise ValueError("无法生成唯一的卡密,请稍后重试")
|
||||
|
||||
license_obj = cls(
|
||||
license_key=license_key,
|
||||
product_id=product_id,
|
||||
type=license_type,
|
||||
valid_days=valid_days,
|
||||
status=0
|
||||
)
|
||||
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)
|
||||
|
||||
return True, "激活成功"
|
||||
|
||||
def verify(self, machine_code, software_version):
|
||||
"""验证卡密"""
|
||||
# 参数验证
|
||||
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():
|
||||
self.status = 2 # 标记为已过期
|
||||
return False, "卡密已过期"
|
||||
|
||||
# 检查绑定关系
|
||||
if self.status == 1: # 已激活
|
||||
if self.bind_machine_code != machine_code:
|
||||
return False, "设备不匹配"
|
||||
|
||||
# 检查设备状态
|
||||
if self.license_id:
|
||||
try:
|
||||
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:
|
||||
# 设备查询失败时默认允许通过
|
||||
pass
|
||||
|
||||
# 更新最后验证时间
|
||||
self.last_verify_time = datetime.utcnow()
|
||||
|
||||
return True, "验证通过"
|
||||
|
||||
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})
|
||||
|
||||
return True, "解绑成功"
|
||||
|
||||
def disable(self):
|
||||
"""禁用卡密
|
||||
|
||||
注意:此方法不提交事务,调用者需要负责提交或回滚
|
||||
"""
|
||||
self.status = 3
|
||||
|
||||
# 禁用关联设备
|
||||
self.devices.update({'status': 0})
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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, include_device_info=False):
|
||||
"""转换为字典
|
||||
|
||||
Args:
|
||||
include_device_info: 是否包含设备信息,默认False避免N+1查询
|
||||
"""
|
||||
device_info = None
|
||||
if include_device_info and self.bind_machine_code:
|
||||
device = Device.query.filter_by(
|
||||
machine_code=self.bind_machine_code,
|
||||
license_id=self.license_id
|
||||
).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}>'
|
||||
68
app/models/order.py
Normal file
68
app/models/order.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
|
||||
class Order(db.Model):
|
||||
"""订单模型"""
|
||||
__tablename__ = 'order'
|
||||
|
||||
order_id = db.Column(db.Integer, primary_key=True)
|
||||
order_number = db.Column(db.String(32), unique=True, nullable=False, index=True) # 订单号
|
||||
product_id = db.Column(db.String(32), db.ForeignKey('product.product_id'), nullable=False)
|
||||
package_id = db.Column(db.String(64), nullable=False) # 套餐ID
|
||||
contact_person = db.Column(db.String(64), nullable=False) # 联系人
|
||||
phone = db.Column(db.String(20), nullable=False) # 手机号
|
||||
quantity = db.Column(db.Integer, nullable=False, default=1) # 数量
|
||||
amount = db.Column(db.Float, nullable=False) # 金额
|
||||
status = db.Column(db.Integer, nullable=False, default=0) # 0=待支付, 1=已支付, 2=已取消, 3=已完成
|
||||
payment_method = db.Column(db.String(20), nullable=True) # 支付方式
|
||||
payment_time = db.Column(db.DateTime, nullable=True) # 支付时间
|
||||
create_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关联关系
|
||||
product = db.relationship('Product', backref='orders')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Order, self).__init__(**kwargs)
|
||||
if not self.order_number:
|
||||
self.order_number = self.generate_order_number()
|
||||
|
||||
@classmethod
|
||||
def generate_order_number(cls):
|
||||
"""生成订单号"""
|
||||
import time
|
||||
import random
|
||||
return f"ORD{int(time.time())}{random.randint(100, 999)}"
|
||||
|
||||
def get_status_name(self):
|
||||
"""获取状态名称"""
|
||||
status_map = {
|
||||
0: '待支付',
|
||||
1: '已支付',
|
||||
2: '已取消',
|
||||
3: '已完成'
|
||||
}
|
||||
return status_map.get(self.status, '未知')
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'order_id': self.order_id,
|
||||
'order_number': self.order_number,
|
||||
'product_id': self.product_id,
|
||||
'product_name': self.product.product_name if self.product and hasattr(self.product, 'product_name') else None,
|
||||
'package_id': self.package_id,
|
||||
'contact_person': self.contact_person,
|
||||
'phone': self.phone,
|
||||
'quantity': self.quantity,
|
||||
'amount': self.amount,
|
||||
'status': self.status,
|
||||
'status_name': self.get_status_name(),
|
||||
'payment_method': self.payment_method,
|
||||
'payment_time': self.payment_time.strftime('%Y-%m-%d %H:%M:%S') if self.payment_time else None,
|
||||
'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'<Order {self.order_number}>'
|
||||
74
app/models/package.py
Normal file
74
app/models/package.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
|
||||
class Package(db.Model):
|
||||
"""产品套餐模型"""
|
||||
__tablename__ = 'package'
|
||||
|
||||
package_id = db.Column(db.String(64), primary_key=True) # 套餐ID
|
||||
product_id = db.Column(db.String(32), db.ForeignKey('product.product_id'), nullable=False) # 关联产品ID
|
||||
name = db.Column(db.String(64), nullable=False) # 套餐名称
|
||||
description = db.Column(db.Text, nullable=True) # 套餐描述
|
||||
price = db.Column(db.Float, nullable=False) # 价格
|
||||
duration = db.Column(db.Integer, nullable=False) # 时长(天)
|
||||
max_devices = db.Column(db.Integer, nullable=False, default=1) # 最大设备数
|
||||
stock = db.Column(db.Integer, nullable=False, default=-1) # 库存,-1表示无限
|
||||
status = db.Column(db.Integer, nullable=False, default=1) # 0=禁用, 1=启用
|
||||
sort_order = db.Column(db.Integer, nullable=False, default=0) # 排序
|
||||
create_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关联关系
|
||||
product = db.relationship('Product', backref='packages')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Package, self).__init__(**kwargs)
|
||||
if not self.package_id:
|
||||
# 自动生成套餐ID
|
||||
import uuid
|
||||
self.package_id = f"PKG_{uuid.uuid4().hex[:8]}".upper()
|
||||
|
||||
def is_enabled(self):
|
||||
"""套餐是否启用"""
|
||||
return self.status == 1
|
||||
|
||||
def has_stock(self):
|
||||
"""是否有库存"""
|
||||
return self.stock == -1 or self.stock > 0
|
||||
|
||||
def get_duration_type(self):
|
||||
"""获取时长类型"""
|
||||
if self.duration == -1:
|
||||
return '永久卡'
|
||||
elif self.duration == 1:
|
||||
return '天卡'
|
||||
elif self.duration == 30:
|
||||
return '月卡'
|
||||
elif self.duration == 90:
|
||||
return '季卡'
|
||||
elif self.duration == 365:
|
||||
return '年卡'
|
||||
else:
|
||||
return f'{self.duration}天卡'
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'package_id': self.package_id,
|
||||
'product_id': self.product_id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'price': self.price,
|
||||
'duration': self.duration,
|
||||
'duration_text': self.get_duration_type(),
|
||||
'max_devices': self.max_devices,
|
||||
'stock': self.stock,
|
||||
'status': self.status,
|
||||
'status_name': '启用' if self.status == 1 else '禁用',
|
||||
'sort_order': self.sort_order,
|
||||
'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'<Package {self.name}>'
|
||||
84
app/models/product.py
Normal file
84
app/models/product.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
from app.models.version import Version
|
||||
|
||||
class Product(db.Model):
|
||||
"""产品模型"""
|
||||
__tablename__ = 'product'
|
||||
|
||||
product_id = db.Column(db.String(32), primary_key=True)
|
||||
product_name = db.Column(db.String(64), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
features = db.Column(db.Text, nullable=True) # 产品功能特性
|
||||
image_path = db.Column(db.String(255), nullable=True) # 产品图片路径
|
||||
status = db.Column(db.Integer, nullable=False, default=1) # 0=禁用, 1=启用
|
||||
create_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关联关系
|
||||
versions = db.relationship('Version', backref='product', lazy='dynamic', cascade='all, delete-orphan')
|
||||
licenses = db.relationship('License', backref='product', lazy='dynamic', cascade='all, delete-orphan')
|
||||
devices = db.relationship('Device', backref='product', lazy='dynamic', cascade='all, delete-orphan')
|
||||
tickets = db.relationship('Ticket', backref='product', lazy='dynamic', cascade='all, delete-orphan')
|
||||
|
||||
def __init__(self, product_id=None, **kwargs):
|
||||
super(Product, self).__init__(**kwargs)
|
||||
if product_id:
|
||||
self.product_id = product_id
|
||||
elif not self.product_id:
|
||||
# 自动生成产品ID
|
||||
import uuid
|
||||
self.product_id = f"PROD_{uuid.uuid4().hex[:8]}".upper()
|
||||
|
||||
def get_stats(self):
|
||||
"""获取产品统计信息"""
|
||||
# 如果已经有缓存的统计信息(来自批量查询),直接使用
|
||||
if hasattr(self, '_cached_stats'):
|
||||
return self._cached_stats
|
||||
|
||||
# 否则执行单个查询(用于单个产品详情等场景)
|
||||
total_licenses = self.licenses.count()
|
||||
active_licenses = self.licenses.filter_by(status=1).count()
|
||||
total_devices = self.devices.filter_by(status=1).count()
|
||||
|
||||
return {
|
||||
'total_licenses': total_licenses,
|
||||
'active_licenses': active_licenses,
|
||||
'total_devices': total_devices,
|
||||
'latest_version': self.get_latest_version()
|
||||
}
|
||||
|
||||
def get_latest_version(self):
|
||||
"""获取最新版本"""
|
||||
latest_version = self.versions.filter_by(publish_status=1).order_by(
|
||||
db.desc(Version.update_time), db.desc(Version.create_time)
|
||||
).first()
|
||||
return latest_version.version_num if latest_version else None
|
||||
|
||||
def is_enabled(self):
|
||||
"""产品是否启用"""
|
||||
return self.status == 1
|
||||
|
||||
def to_dict(self, include_stats=False):
|
||||
"""转换为字典"""
|
||||
data = {
|
||||
'product_id': self.product_id,
|
||||
'product_name': self.product_name,
|
||||
'description': self.description,
|
||||
'features': self.features, # 添加功能特性字段
|
||||
'image_path': self.image_path,
|
||||
'image_url': self.image_path, # 兼容前端使用image_url的场景
|
||||
'status': self.status,
|
||||
'status_name': '启用' if self.status == 1 else '禁用',
|
||||
'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
|
||||
}
|
||||
|
||||
if include_stats:
|
||||
stats = self.get_stats()
|
||||
data.update(stats)
|
||||
|
||||
return data
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Product {self.product_name}>'
|
||||
181
app/models/ticket.py
Normal file
181
app/models/ticket.py
Normal file
@@ -0,0 +1,181 @@
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
class TicketReply(db.Model):
|
||||
"""工单回复模型"""
|
||||
__tablename__ = 'ticket_reply'
|
||||
|
||||
reply_id = db.Column(db.Integer, primary_key=True)
|
||||
ticket_id = db.Column(db.Integer, db.ForeignKey('ticket.ticket_id'), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False) # 回复内容(支持HTML)
|
||||
creator = db.Column(db.String(32), nullable=True) # 回复人(管理员账号或系统)
|
||||
create_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'reply_id': self.reply_id,
|
||||
'ticket_id': self.ticket_id,
|
||||
'content': self.content,
|
||||
'creator': self.creator,
|
||||
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time else None
|
||||
}
|
||||
|
||||
class Ticket(db.Model):
|
||||
"""工单模型"""
|
||||
__tablename__ = 'ticket'
|
||||
|
||||
ticket_id = db.Column(db.Integer, primary_key=True)
|
||||
ticket_number = db.Column(db.String(32), unique=True, nullable=False) # 工单编号
|
||||
title = db.Column(db.String(128), nullable=False)
|
||||
product_id = db.Column(db.String(32), db.ForeignKey('product.product_id'), nullable=False)
|
||||
software_version = db.Column(db.String(16), nullable=True)
|
||||
machine_code = db.Column(db.String(64), nullable=True)
|
||||
license_key = db.Column(db.String(32), nullable=True)
|
||||
description = db.Column(db.Text, nullable=False)
|
||||
priority = db.Column(db.Integer, nullable=False, default=1) # 0=低, 1=中, 2=高
|
||||
status = db.Column(db.Integer, nullable=False, default=0) # 0=待处理, 1=处理中, 2=已解决, 3=已关闭
|
||||
operator = db.Column(db.String(32), nullable=True) # 处理人(管理员账号)
|
||||
remark = db.Column(db.Text, nullable=True) # 处理备注
|
||||
contact_person = db.Column(db.String(64), nullable=True) # 联系人姓名
|
||||
phone = db.Column(db.String(20), nullable=True) # 联系电话
|
||||
create_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
resolve_time = db.Column(db.DateTime, nullable=True) # 解决时间
|
||||
close_time = db.Column(db.DateTime, nullable=True) # 关闭时间
|
||||
|
||||
# 关系
|
||||
replies = relationship('TicketReply', backref='ticket', lazy='dynamic', cascade='all, delete-orphan')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Ticket, self).__init__(**kwargs)
|
||||
# 如果没有提供工单编号,则自动生成
|
||||
if not self.ticket_number:
|
||||
self.ticket_number = self.generate_ticket_number()
|
||||
|
||||
def generate_ticket_number(self):
|
||||
"""生成工单编号 TKT+日期+4位序号"""
|
||||
from datetime import datetime
|
||||
import random
|
||||
date_str = datetime.now().strftime('%Y%m%d')
|
||||
random_str = str(random.randint(1000, 9999))
|
||||
return f"TKT{date_str}{random_str}"
|
||||
|
||||
def get_priority_name(self):
|
||||
"""获取优先级名称"""
|
||||
priority_map = {
|
||||
0: '低',
|
||||
1: '中',
|
||||
2: '高'
|
||||
}
|
||||
return priority_map.get(self.priority, '未知')
|
||||
|
||||
def get_status_name(self):
|
||||
"""获取状态名称"""
|
||||
status_map = {
|
||||
0: '待处理',
|
||||
1: '处理中',
|
||||
2: '已解决',
|
||||
3: '已关闭'
|
||||
}
|
||||
return status_map.get(self.status, '未知')
|
||||
|
||||
def is_pending(self):
|
||||
"""是否待处理"""
|
||||
return self.status == 0
|
||||
|
||||
def is_processing(self):
|
||||
"""是否处理中"""
|
||||
return self.status == 1
|
||||
|
||||
def is_resolved(self):
|
||||
"""是否已解决"""
|
||||
return self.status == 2
|
||||
|
||||
def is_closed(self):
|
||||
"""是否已关闭"""
|
||||
return self.status == 3
|
||||
|
||||
def assign_to(self, operator):
|
||||
"""分配给处理人"""
|
||||
self.operator = operator
|
||||
if self.status == 0:
|
||||
self.status = 1 # 待处理 -> 处理中
|
||||
db.session.commit()
|
||||
|
||||
def resolve(self, remark=None):
|
||||
"""解决工单"""
|
||||
self.status = 2
|
||||
self.resolve_time = datetime.utcnow()
|
||||
if remark:
|
||||
self.remark = remark
|
||||
db.session.commit()
|
||||
|
||||
def close(self, remark=None):
|
||||
"""关闭工单"""
|
||||
self.status = 3
|
||||
self.close_time = datetime.utcnow()
|
||||
if remark:
|
||||
self.remark = remark
|
||||
db.session.commit()
|
||||
|
||||
def reopen(self):
|
||||
"""重新打开工单"""
|
||||
self.status = 1 # 处理中
|
||||
self.resolve_time = None
|
||||
self.close_time = None
|
||||
db.session.commit()
|
||||
|
||||
def update_status(self, status, remark=None):
|
||||
"""更新工单状态"""
|
||||
old_status = self.status
|
||||
self.status = status
|
||||
|
||||
# 更新时间戳
|
||||
if status == 2 and old_status != 2:
|
||||
self.resolve_time = datetime.utcnow()
|
||||
elif status == 3 and old_status != 3:
|
||||
self.close_time = datetime.utcnow()
|
||||
|
||||
if remark:
|
||||
self.remark = remark
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def get_processing_days(self):
|
||||
"""获取处理天数"""
|
||||
if self.resolve_time:
|
||||
return (self.resolve_time - self.create_time).days
|
||||
return (datetime.utcnow() - self.create_time).days
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'ticket_id': self.ticket_id,
|
||||
'ticket_number': self.ticket_number,
|
||||
'title': self.title,
|
||||
'product_id': self.product_id,
|
||||
'product_name': self.product.product_name if self.product else None,
|
||||
'software_version': self.software_version,
|
||||
'machine_code': self.machine_code,
|
||||
'license_key': self.license_key,
|
||||
'description': self.description,
|
||||
'priority': self.priority,
|
||||
'priority_name': self.get_priority_name(),
|
||||
'status': self.status,
|
||||
'status_name': self.get_status_name(),
|
||||
'operator': self.operator,
|
||||
'remark': self.remark,
|
||||
'contact_person': self.contact_person,
|
||||
'phone': self.phone,
|
||||
'processing_days': self.get_processing_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,
|
||||
'resolve_time': self.resolve_time.strftime('%Y-%m-%d %H:%M:%S') if self.resolve_time else None,
|
||||
'close_time': self.close_time.strftime('%Y-%m-%d %H:%M:%S') if self.close_time else None,
|
||||
'replies': [reply.to_dict() for reply in self.replies.order_by(TicketReply.create_time.asc()).all()]
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Ticket {self.ticket_id}: {self.title}>'
|
||||
163
app/models/version.py
Normal file
163
app/models/version.py
Normal file
@@ -0,0 +1,163 @@
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
|
||||
class Version(db.Model):
|
||||
"""版本模型"""
|
||||
__tablename__ = 'version'
|
||||
|
||||
version_id = db.Column(db.Integer, primary_key=True)
|
||||
product_id = db.Column(db.String(32), db.ForeignKey('product.product_id'), nullable=False)
|
||||
version_num = db.Column(db.String(16), nullable=False)
|
||||
# 添加新字段
|
||||
platform = db.Column(db.String(32), nullable=True) # 平台信息
|
||||
description = db.Column(db.Text, nullable=True) # 版本描述
|
||||
update_log = db.Column(db.Text, nullable=True)
|
||||
download_url = db.Column(db.String(255), nullable=True)
|
||||
min_license_version = db.Column(db.String(16), nullable=True) # 兼容最低卡密版本
|
||||
force_update = db.Column(db.Integer, nullable=False, default=0) # 0=否, 1=是
|
||||
download_status = db.Column(db.Integer, nullable=False, default=1) # 0=禁用, 1=启用
|
||||
publish_status = db.Column(db.Integer, nullable=False, default=0) # 0=草稿, 1=已发布, 2=已回滚
|
||||
file_hash = db.Column(db.String(64), nullable=True) # 文件SHA256哈希值
|
||||
create_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关联关系
|
||||
# devices 和 version 之间通过 software_version 字段关联,而不是直接的外键关系
|
||||
# tickets 和 version 之间通过 software_version 字段关联,而不是直接的外键关系
|
||||
|
||||
# 复合唯一索引
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('product_id', 'version_num', name='uk_product_version'),
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Version, self).__init__(**kwargs)
|
||||
|
||||
def is_published(self):
|
||||
"""版本是否已发布"""
|
||||
return self.publish_status == 1
|
||||
|
||||
def is_download_enabled(self):
|
||||
"""下载是否启用"""
|
||||
return self.download_status == 1
|
||||
|
||||
def is_force_update(self):
|
||||
"""是否强制更新"""
|
||||
return self.force_update == 1
|
||||
|
||||
def get_download_count(self):
|
||||
"""获取下载次数"""
|
||||
# 如果已经有缓存的统计信息(来自批量查询),直接使用
|
||||
if hasattr(self, '_cached_stats'):
|
||||
return self._cached_stats.get('download_count', 0)
|
||||
|
||||
# 否则执行单个查询(用于单个版本详情等场景)
|
||||
from app.models.device import Device
|
||||
return Device.query.filter_by(
|
||||
product_id=self.product_id,
|
||||
software_version=self.version_num
|
||||
).count()
|
||||
|
||||
def get_active_device_count(self):
|
||||
"""获取活跃设备数"""
|
||||
# 如果已经有缓存的统计信息(来自批量查询),直接使用
|
||||
if hasattr(self, '_cached_stats'):
|
||||
return self._cached_stats.get('active_device_count', 0)
|
||||
|
||||
# 否则执行单个查询(用于单个版本详情等场景)
|
||||
from app.models.device import Device
|
||||
return Device.query.filter_by(
|
||||
product_id=self.product_id,
|
||||
software_version=self.version_num,
|
||||
status=1
|
||||
).count()
|
||||
|
||||
def check_compatibility(self, license_version):
|
||||
"""检查版本兼容性"""
|
||||
if not self.min_license_version:
|
||||
return True # 未设置最低版本要求,全部兼容
|
||||
|
||||
# 简单的版本号比较,实际应用中可能需要更复杂的版本比较逻辑
|
||||
try:
|
||||
license_parts = [int(x) for x in license_version.split('.')]
|
||||
min_parts = [int(x) for x in self.min_license_version.split('.')]
|
||||
|
||||
# 补齐版本号长度
|
||||
max_len = max(len(license_parts), len(min_parts))
|
||||
license_parts.extend([0] * (max_len - len(license_parts)))
|
||||
min_parts.extend([0] * (max_len - len(min_parts)))
|
||||
|
||||
return license_parts >= min_parts
|
||||
except (ValueError, AttributeError):
|
||||
return True # 版本号格式异常时默认兼容
|
||||
|
||||
def publish(self):
|
||||
"""发布版本"""
|
||||
self.publish_status = 1
|
||||
db.session.commit()
|
||||
|
||||
def rollback(self):
|
||||
"""回滚版本"""
|
||||
self.publish_status = 2
|
||||
db.session.commit()
|
||||
|
||||
def unpublish(self):
|
||||
"""取消发布版本"""
|
||||
self.publish_status = 0
|
||||
db.session.commit()
|
||||
|
||||
def to_dict(self, include_stats=False):
|
||||
"""转换为字典"""
|
||||
data = {
|
||||
'version_id': self.version_id,
|
||||
'product_id': self.product_id,
|
||||
'product_name': self.product.product_name if self.product else None,
|
||||
'version_num': self.version_num,
|
||||
'platform': self.platform, # 添加新字段
|
||||
'description': self.description, # 添加新字段
|
||||
'update_log': self.update_log,
|
||||
'download_url': self.download_url,
|
||||
'file_hash': self.file_hash,
|
||||
'min_license_version': self.min_license_version,
|
||||
'force_update': self.force_update,
|
||||
'force_update_name': '是' if self.force_update == 1 else '否',
|
||||
'download_status': self.download_status,
|
||||
'download_status_name': '启用' if self.download_status == 1 else '禁用',
|
||||
'publish_status': self.publish_status,
|
||||
'publish_status_name': {
|
||||
0: '草稿',
|
||||
1: '已发布',
|
||||
2: '已回滚'
|
||||
}.get(self.publish_status, '未知'),
|
||||
'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
|
||||
}
|
||||
|
||||
if include_stats:
|
||||
data.update({
|
||||
'download_count': self.get_download_count(),
|
||||
'active_device_count': self.get_active_device_count()
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
def has_valid_download_url(self):
|
||||
"""检查是否有有效的下载URL"""
|
||||
return bool(self.download_url and self.download_url.strip())
|
||||
|
||||
def is_external_link(self):
|
||||
"""检查是否是外部链接"""
|
||||
if not self.has_valid_download_url():
|
||||
return False
|
||||
# 外部链接以http或https开头
|
||||
return self.download_url.startswith(('http://', 'https://'))
|
||||
|
||||
def is_local_file(self):
|
||||
"""检查是否是本地文件"""
|
||||
if not self.has_valid_download_url():
|
||||
return False
|
||||
# 本地文件以/开头
|
||||
return self.download_url.startswith('/')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Version {self.product.product_name if self.product else "Unknown"} v{self.version_num}>'
|
||||
Reference in New Issue
Block a user