第一次提交

This commit is contained in:
2026-03-25 15:24:22 +08:00
commit 0f8ac68d4d
156 changed files with 42365 additions and 0 deletions

11
app/models/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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}>'