c8b0663508
批量添加替代CSV并支持去重;脚本库页内嵌执行历史;推送历史记录 rsync 权限策略; 脚本执行不再依赖 Agent 在线;服务器同步日志与相关单测补齐。 Co-authored-by: Cursor <cursoragent@cursor.com>
473 lines
22 KiB
Python
473 lines
22 KiB
Python
"""Nexus — Domain Models
|
||
SQLAlchemy ORM models for all database tables.
|
||
"""
|
||
|
||
import datetime
|
||
from datetime import timezone
|
||
import uuid
|
||
from sqlalchemy import (
|
||
Column, Integer, String, Boolean,
|
||
DateTime, Text, ForeignKey, Index, JSON
|
||
)
|
||
from sqlalchemy.orm import declarative_base, relationship
|
||
|
||
|
||
def _utcnow():
|
||
"""Callable default for DateTime columns — evaluated per-row, not at module import"""
|
||
return datetime.datetime.now(timezone.utc)
|
||
|
||
|
||
Base = declarative_base()
|
||
|
||
|
||
# ──────────────────────────────────────────────
|
||
# Platform & Node (资产组织层)
|
||
# ──────────────────────────────────────────────
|
||
|
||
class Platform(Base):
|
||
"""Asset type template — defines what kind of asset this is"""
|
||
__tablename__ = "platforms"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(100), unique=True, nullable=False, comment="平台名称: Linux服务器/Windows/MySQL")
|
||
category = Column(String(50), nullable=False, comment="分类: host/database/device")
|
||
type = Column(String(50), nullable=False, comment="类型: linux/windows/mysql/switch")
|
||
default_protocols = Column(JSON, nullable=True, comment="默认协议: [{name:'ssh',port:22,primary:true}]")
|
||
charset = Column(String(20), default="utf-8", comment="默认字符集")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
assets = relationship("Server", back_populates="platform")
|
||
|
||
|
||
class Node(Base):
|
||
"""Tree-structured asset grouping"""
|
||
__tablename__ = "nodes"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(100), nullable=False, comment="节点名称")
|
||
parent_id = Column(Integer, ForeignKey("nodes.id", ondelete="SET NULL"), nullable=True, comment="父节点")
|
||
sort_order = Column(Integer, default=0, comment="排序权重")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
parent = relationship("Node", remote_side=[id], back_populates="children")
|
||
children = relationship("Node", back_populates="parent")
|
||
assets = relationship("Server", back_populates="node")
|
||
|
||
__table_args__ = (
|
||
Index('idx_nodes_parent_id', 'parent_id'),
|
||
)
|
||
|
||
|
||
# ──────────────────────────────────────────────
|
||
# Server (核心资产表 — 保持原名,扩展字段)
|
||
# ──────────────────────────────────────────────
|
||
|
||
class Server(Base):
|
||
"""Sub-server configuration / Asset"""
|
||
__tablename__ = "servers"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(100), unique=True, nullable=False, comment="服务器名称")
|
||
domain = Column(String(255), nullable=False, comment="域名或IP")
|
||
port = Column(Integer, default=22, comment="SSH端口")
|
||
username = Column(String(100), default="root", comment="SSH用户名")
|
||
auth_method = Column(String(20), default="key", comment="认证方式: key/password")
|
||
password = Column(String(255), nullable=True, comment="SSH密码(加密存储)")
|
||
ssh_key_path = Column(String(500), nullable=True, comment="SSH私钥路径")
|
||
# Text columns: RSA 4096 private key PEM is ~3.2 KB; Fernet-encrypted ~4.3 KB.
|
||
# String(500) would cause DataError in MySQL strict mode for large keys.
|
||
ssh_key_private = Column(Text, nullable=True, comment="加密后的私钥内容(Fernet)")
|
||
ssh_key_public = Column(Text, nullable=True, comment="公钥内容")
|
||
ssh_key_configured = Column(Boolean, default=False, comment="是否已配置SSH Key")
|
||
agent_port = Column(Integer, default=8601, comment="Agent API端口")
|
||
agent_api_key = Column(String(255), nullable=True, comment="Agent API密钥")
|
||
preset_id = Column(Integer, nullable=True, comment="关联的密码预设ID(SSH认证)")
|
||
ssh_key_preset_id = Column(Integer, nullable=True, comment="关联的密钥预设ID(SSH认证)")
|
||
description = Column(Text, nullable=True, comment="备注说明")
|
||
target_path = Column(String(500), nullable=True, comment="推送目标路径")
|
||
category = Column(String(100), nullable=True, comment="服务器分类(旧字段,保留兼容)")
|
||
|
||
# ── 新增:资产组织字段 ──
|
||
platform_id = Column(Integer, ForeignKey("platforms.id", ondelete="SET NULL"), nullable=True, comment="平台类型ID")
|
||
node_id = Column(Integer, ForeignKey("nodes.id", ondelete="SET NULL"), nullable=True, comment="所属节点ID")
|
||
protocols = Column(JSON, nullable=True, comment="实例级协议覆盖: [{name:'ssh',port:2222}]")
|
||
extra_attrs = Column(JSON, nullable=True, comment="扩展属性: {os:'ubuntu',arch:'x86_64',cpu:8,mem:'32G'}")
|
||
|
||
# Status (from Agent heartbeat)
|
||
is_online = Column(Boolean, default=False, comment="是否在线")
|
||
last_heartbeat = Column(DateTime, nullable=True, comment="最后心跳时间")
|
||
system_info = Column(Text, nullable=True, comment="系统信息JSON(Agent上报)")
|
||
agent_version = Column(String(20), nullable=True, comment="Agent版本")
|
||
|
||
# ── 新增:连接状态 ──
|
||
connectivity = Column(String(20), default="unknown", comment="连接状态: ok/err/unknown")
|
||
last_checked_at = Column(DateTime, nullable=True, comment="上次连接测试时间")
|
||
|
||
# Metadata
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||
|
||
# Relationships
|
||
sync_logs = relationship("SyncLog", back_populates="server", cascade="all, delete-orphan")
|
||
platform = relationship("Platform", back_populates="assets")
|
||
node = relationship("Node", back_populates="assets")
|
||
|
||
__table_args__ = (
|
||
Index('idx_servers_is_online', 'is_online'),
|
||
Index('idx_servers_category', 'category'),
|
||
Index('idx_servers_platform_id', 'platform_id'),
|
||
Index('idx_servers_node_id', 'node_id'),
|
||
)
|
||
|
||
|
||
class SyncLog(Base):
|
||
"""Push operation log"""
|
||
__tablename__ = "sync_logs"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
|
||
source_path = Column(String(500), nullable=False, comment="源目录")
|
||
target_path = Column(String(500), nullable=False, comment="目标目录")
|
||
trigger_type = Column(String(20), comment="触发类型: manual/schedule/batch")
|
||
operator = Column(String(100), nullable=True, comment="操作人用户名")
|
||
status = Column(String(20), comment="pending/running/success/failed")
|
||
sync_mode = Column(String(20), default="incremental", comment="同步模式: incremental/full/overwrite/checksum")
|
||
files_total = Column(Integer, default=0)
|
||
files_transferred = Column(Integer, default=0)
|
||
files_skipped = Column(Integer, default=0)
|
||
bytes_transferred = Column(Integer, default=0)
|
||
duration_seconds = Column(Integer, default=0)
|
||
diff_summary = Column(Text, nullable=True, comment="rsync输出摘要")
|
||
push_permission = Column(String(120), nullable=True, comment="推送后权限策略 chown=… chmod=…")
|
||
error_message = Column(Text, nullable=True)
|
||
started_at = Column(DateTime, default=_utcnow)
|
||
finished_at = Column(DateTime, nullable=True)
|
||
|
||
server = relationship("Server", back_populates="sync_logs")
|
||
|
||
__table_args__ = (
|
||
Index('idx_sync_logs_srv_start', 'server_id', 'started_at'),
|
||
)
|
||
|
||
|
||
class Admin(Base):
|
||
"""Admin user for web login"""
|
||
__tablename__ = "admins"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
username = Column(String(100), unique=True, nullable=False)
|
||
password_hash = Column(String(255), nullable=False)
|
||
email = Column(String(255), nullable=True)
|
||
totp_secret = Column(String(64), nullable=True)
|
||
totp_enabled = Column(Boolean, default=False)
|
||
is_active = Column(Boolean, default=True)
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||
last_login = Column(DateTime, nullable=True)
|
||
|
||
token_version = Column(Integer, default=0, comment="令牌版本(重用时递增使旧token失效)")
|
||
|
||
|
||
class LoginAttempt(Base):
|
||
"""Login attempt log for brute-force protection"""
|
||
__tablename__ = "login_attempts"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
username = Column(String(100), nullable=False)
|
||
ip_address = Column(String(45), nullable=False)
|
||
attempted_at = Column(DateTime, default=_utcnow)
|
||
success = Column(Boolean, default=False)
|
||
|
||
__table_args__ = (
|
||
Index('idx_login_attempts_user_time', 'username', 'attempted_at'),
|
||
)
|
||
|
||
|
||
class AdminRefreshToken(Base):
|
||
"""Refresh token hash persisted for login sessions (Redis + MySQL dual store)."""
|
||
__tablename__ = "admin_refresh_tokens"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
admin_id = Column(Integer, nullable=False, comment="管理员 ID")
|
||
token_hash = Column(String(64), nullable=False, unique=True, comment="SHA-256(refresh token)")
|
||
expires_at = Column(DateTime, nullable=False, comment="过期时间 UTC")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
__table_args__ = (
|
||
Index("idx_admin_refresh_admin_expires", "admin_id", "expires_at"),
|
||
)
|
||
|
||
|
||
class Setting(Base):
|
||
"""Key-value system settings (including brand config)"""
|
||
__tablename__ = "settings"
|
||
|
||
key = Column(String(100), primary_key=True)
|
||
value = Column(Text, nullable=True)
|
||
updated_at = Column(DateTime, default=_utcnow)
|
||
|
||
|
||
class PasswordPreset(Base):
|
||
"""SSH password preset — encrypted storage"""
|
||
__tablename__ = "password_presets"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(100), nullable=False, comment="预设名称")
|
||
username = Column(String(100), default="root", comment="SSH用户名")
|
||
encrypted_pw = Column(String(500), nullable=False, comment="加密后的密码")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
|
||
class SshKeyPreset(Base):
|
||
"""SSH key preset — encrypted private key storage"""
|
||
__tablename__ = "ssh_key_presets"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(100), nullable=False, comment="预设名称")
|
||
username = Column(String(100), default="root", comment="SSH用户名")
|
||
encrypted_private_key = Column(Text, nullable=False, comment="加密后的私钥内容(Fernet)")
|
||
public_key = Column(Text, nullable=True, comment="公钥内容(不加密)")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
|
||
class PendingServer(Base):
|
||
"""Servers awaiting successful SSH credential polling before promotion to servers table."""
|
||
__tablename__ = "pending_servers"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(100), nullable=False, comment="显示名称")
|
||
domain = Column(String(255), nullable=False, comment="IP或域名")
|
||
port = Column(Integer, default=22, comment="SSH端口")
|
||
agent_port = Column(Integer, default=8601, comment="Agent端口")
|
||
target_path = Column(String(500), nullable=True, comment="推送目标路径")
|
||
category = Column(String(100), nullable=True, comment="分类")
|
||
platform_id = Column(Integer, nullable=True, comment="平台ID")
|
||
node_id = Column(Integer, nullable=True, comment="节点ID")
|
||
attempts = Column(Integer, default=0, comment="轮询尝试次数")
|
||
last_error = Column(Text, nullable=True, comment="最后一次失败摘要")
|
||
last_attempt_at = Column(DateTime, nullable=True, comment="最后尝试时间")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
__table_args__ = (
|
||
Index("idx_pending_servers_domain", "domain"),
|
||
)
|
||
|
||
|
||
class PushSchedule(Base):
|
||
"""Scheduled task — cron-triggered file push OR script execution."""
|
||
__tablename__ = "push_schedules"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(100), nullable=False, comment="调度名称")
|
||
|
||
# ── Schedule type ──
|
||
# 'push': rsync file sync (default, original behaviour)
|
||
# 'script': execute a shell command / script via Agent
|
||
schedule_type = Column(String(20), default="push", comment="调度类型: push | script")
|
||
|
||
# ── Push-specific fields (schedule_type = 'push') ──
|
||
source_path = Column(String(500), nullable=True, comment="推送源目录(push 类型必填)")
|
||
target_path = Column(String(500), nullable=True, comment="推送目标路径(留空则各机 target_path)")
|
||
sync_mode = Column(String(20), default="incremental", comment="同步模式: incremental/full/overwrite/checksum")
|
||
|
||
# ── Script-specific fields (schedule_type = 'script') ──
|
||
script_id = Column(Integer, ForeignKey("scripts.id", ondelete="SET NULL"), nullable=True,
|
||
comment="脚本库引用(script 类型,可留空使用 script_content)")
|
||
script_content = Column(Text, nullable=True, comment="临时命令内容(script 类型,不保存到脚本库)")
|
||
exec_timeout = Column(Integer, default=60, comment="执行超时(秒)")
|
||
long_task = Column(Boolean, default=False, comment="长任务 nohup 模式")
|
||
|
||
# ── Common fields ──
|
||
server_ids = Column(Text, nullable=False, comment="JSON数组: 目标服务器ID列表")
|
||
run_mode = Column(String(10), default="once", comment="运行模式: once(一次性) | cron(循环)")
|
||
cron_expr = Column(String(100), nullable=True, comment="cron表达式 (run_mode=cron时必填)")
|
||
fire_at = Column(DateTime, nullable=True, comment="一次性执行时间 (run_mode=once时必填)")
|
||
enabled = Column(Boolean, default=True)
|
||
last_run_at = Column(DateTime, nullable=True)
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
__table_args__ = (
|
||
Index('idx_push_schedules_enabled', 'enabled'),
|
||
)
|
||
|
||
|
||
class PushRetryJob(Base):
|
||
"""Failed push retry queue"""
|
||
__tablename__ = "push_retry_jobs"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
|
||
server_name = Column(String(100), comment="冗余字段便于列表显示")
|
||
operator = Column(String(100), nullable=True, comment="操作人用户名")
|
||
source_path = Column(String(500), nullable=False)
|
||
target_path = Column(String(500), nullable=False)
|
||
status = Column(String(20), default="pending")
|
||
retry_count = Column(Integer, default=0)
|
||
max_retries = Column(Integer, default=3)
|
||
next_retry_at = Column(DateTime)
|
||
last_error = Column(Text)
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
updated_at = Column(DateTime, default=_utcnow)
|
||
|
||
server = relationship("Server")
|
||
|
||
__table_args__ = (
|
||
Index('idx_push_retry_pending', 'status', 'next_retry_at'),
|
||
)
|
||
|
||
|
||
class AuditLog(Base):
|
||
"""Audit log — record all key operations"""
|
||
__tablename__ = "audit_logs"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
admin_username = Column(String(100), nullable=True, comment="操作人用户名")
|
||
action = Column(String(50), nullable=False, comment="操作类型")
|
||
target_type = Column(String(50), nullable=True, comment="目标类型")
|
||
target_id = Column(Integer, nullable=True, comment="目标ID")
|
||
detail = Column(Text, nullable=True, comment="操作详情JSON")
|
||
ip_address = Column(String(45), nullable=True, comment="操作IP")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
__table_args__ = (
|
||
Index('idx_audit_logs_action_time', 'action', 'created_at'),
|
||
Index('idx_audit_logs_created_at', 'created_at'),
|
||
)
|
||
|
||
|
||
class Script(Base):
|
||
"""Script library — reusable shell scripts"""
|
||
__tablename__ = "scripts"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(100), nullable=False, comment="脚本名称")
|
||
category = Column(String(50), default="ops", comment="分类: ops/deploy/check/cleanup")
|
||
content = Column(Text, nullable=False, comment="Shell命令内容")
|
||
description = Column(Text, nullable=True, comment="说明")
|
||
created_by = Column(String(100), nullable=True, comment="创建人")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
updated_at = Column(DateTime, default=_utcnow)
|
||
|
||
|
||
class ScriptExecution(Base):
|
||
"""Script execution log — results per server"""
|
||
__tablename__ = "script_executions"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
script_id = Column(Integer, ForeignKey("scripts.id", ondelete="SET NULL"), nullable=True, comment="关联脚本(手动输入时为null)")
|
||
command = Column(Text, nullable=False, comment="实际执行的命令")
|
||
server_ids = Column(Text, nullable=False, comment="JSON数组: 目标服务器ID列表")
|
||
credential_id = Column(Integer, ForeignKey("db_credentials.id", ondelete="SET NULL"), nullable=True, comment="DB凭据替换$DB_*变量")
|
||
status = Column(String(20), default="pending", comment="pending/running/completed/failed")
|
||
results = Column(Text, nullable=True, comment="JSON: 每台服务器执行结果{server_id: {stdout,stderr,exit_code}}")
|
||
operator = Column(String(100), nullable=True, comment="操作人用户名")
|
||
started_at = Column(DateTime, default=_utcnow)
|
||
completed_at = Column(DateTime, nullable=True)
|
||
|
||
script = relationship("Script")
|
||
|
||
__table_args__ = (
|
||
Index('idx_script_exec_script_id', 'script_id'),
|
||
)
|
||
|
||
|
||
class DbCredential(Base):
|
||
"""Database credentials — encrypted storage for $DB_* variable substitution"""
|
||
__tablename__ = "db_credentials"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(100), nullable=False, comment="凭据名称")
|
||
db_type = Column(String(20), default="mysql", comment="数据库类型: mysql/postgresql/etc")
|
||
host = Column(String(255), default="localhost", comment="数据库主机")
|
||
port = Column(Integer, default=3306, comment="数据库端口")
|
||
username = Column(String(100), nullable=False, comment="数据库用户名")
|
||
encrypted_password = Column(String(500), nullable=False, comment="加密后的密码")
|
||
database = Column(String(100), nullable=True, comment="数据库库名")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
|
||
# ──────────────────────────────────────────────
|
||
# Web SSH 会话 & 命令日志
|
||
# ──────────────────────────────────────────────
|
||
|
||
class AlertLog(Base):
|
||
"""Persistent alert & recovery history."""
|
||
__tablename__ = "alert_logs"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
|
||
server_name = Column(String(100), nullable=True, comment="冗余服务器名,便于列表显示")
|
||
alert_type = Column(String(20), nullable=False, comment="cpu|mem|disk|time_drift|system")
|
||
value = Column(String(50), nullable=True, comment="指标值(百分比或描述)")
|
||
is_recovery = Column(Boolean, default=False, comment="True=恢复通知")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
server = relationship("Server")
|
||
|
||
__table_args__ = (
|
||
Index("idx_alert_logs_server_id", "server_id"),
|
||
Index("idx_alert_logs_created_at", "created_at"),
|
||
Index("idx_alert_logs_type", "alert_type"),
|
||
)
|
||
|
||
|
||
class SshSession(Base):
|
||
"""Web SSH session lifecycle tracking"""
|
||
__tablename__ = "ssh_sessions"
|
||
|
||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="UUID")
|
||
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False, comment="服务器ID")
|
||
admin_id = Column(Integer, ForeignKey("admins.id", ondelete="SET NULL"), nullable=True, comment="操作人ID")
|
||
remote_addr = Column(String(45), nullable=True, comment="客户端IP")
|
||
status = Column(String(20), default="active", comment="active/closed")
|
||
started_at = Column(DateTime, default=_utcnow)
|
||
closed_at = Column(DateTime, nullable=True)
|
||
|
||
server = relationship("Server")
|
||
admin = relationship("Admin")
|
||
command_logs = relationship("CommandLog", back_populates="session", cascade="all, delete-orphan")
|
||
|
||
__table_args__ = (
|
||
Index('idx_ssh_sessions_server_id', 'server_id'),
|
||
Index('idx_ssh_sessions_admin_id', 'admin_id'),
|
||
)
|
||
|
||
|
||
class CommandLog(Base):
|
||
"""SSH command log — record every command executed in terminal"""
|
||
__tablename__ = "command_logs"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
session_id = Column(String(36), ForeignKey("ssh_sessions.id", ondelete="CASCADE"), nullable=True, comment="SSH会话ID")
|
||
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False, comment="服务器ID")
|
||
admin_id = Column(Integer, ForeignKey("admins.id", ondelete="SET NULL"), nullable=True, comment="操作人ID")
|
||
command = Column(String(2000), nullable=False, comment="执行的命令")
|
||
remote_addr = Column(String(45), nullable=True, comment="客户端IP")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
|
||
session = relationship("SshSession", back_populates="command_logs")
|
||
server = relationship("Server")
|
||
admin = relationship("Admin")
|
||
|
||
__table_args__ = (
|
||
Index('idx_cmdlog_srv_time', 'server_id', 'created_at'),
|
||
Index('idx_cmdlog_session_id', 'session_id'),
|
||
)
|
||
|
||
|
||
# ──────────────────────────────────────────────
|
||
# Quick Commands (终端快捷命令)
|
||
# ──────────────────────────────────────────────
|
||
|
||
class QuickCommand(Base):
|
||
"""Terminal quick commands — persistent across browsers, shareable across users"""
|
||
__tablename__ = "quick_commands"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(100), nullable=False, comment="命令名称")
|
||
cmd = Column(String(500), nullable=False, comment="命令内容")
|
||
is_builtin = Column(Boolean, default=False, comment="系统内置标记(不可删除/编辑)")
|
||
sort_order = Column(Integer, default=0, comment="排序权重")
|
||
created_by = Column(String(100), nullable=True, comment="创建人")
|
||
created_at = Column(DateTime, default=_utcnow)
|
||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow) |