diff --git a/models.py b/models.py index 64b5a60..7349a54 100644 --- a/models.py +++ b/models.py @@ -100,11 +100,19 @@ class User(db.Model): return False def is_active(self): - """用户是否可用(未过期、未封禁、已启用)""" + """用户是否可用(未过期、未封禁、已启用) + + SQLite 不存 tzinfo, 从数据库读出的 datetime 是 naive; + 我们统一当作 UTC 处理后再比较, 避免 aware/naive TypeError. + """ if not self.enabled or self.banned: return False - if self.expire_at and datetime.now(timezone.utc) > self.expire_at: - return False + if self.expire_at: + exp = self.expire_at + if exp.tzinfo is None: + exp = exp.replace(tzinfo=timezone.utc) + if datetime.now(timezone.utc) > exp: + return False return True def __repr__(self): diff --git a/services/user_service.py b/services/user_service.py index a7a127f..6fe0a2b 100644 --- a/services/user_service.py +++ b/services/user_service.py @@ -15,6 +15,39 @@ _FAIL2BAN_MAX = 10 _FAIL2BAN_WINDOW = 600 +def _parse_expire_at(value): + """从 HTML form / API JSON 接收 expire_at, 转成 UTC-aware datetime 或 None. + + 支持: + - None / 空字符串 -> None + - ISO 字符串 (含 'T' 或 ' ') -> datetime (UTC) + - 已经是 datetime 的 -> 视为 UTC (如果是 naive 就补 tz) + - 非法格式 -> None + """ + if not value: + return None + if isinstance(value, datetime): + return value if value.tzinfo else value.replace(tzinfo=timezone.utc) + s = str(value).strip() + if not s: + return None + # HTML datetime-local 格式 "2026-12-31T23:59" 或 "2026-12-31 23:59" + for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"): + try: + dt = datetime.strptime(s, fmt) + return dt.replace(tzinfo=timezone.utc) + except ValueError: + continue + # ISO 完整格式 (fromisoformat 自动处理 tzinfo) + try: + dt = datetime.fromisoformat(s.replace("Z", "+00:00")) + if not dt.tzinfo: + dt = dt.replace(tzinfo=timezone.utc) + return dt + except ValueError: + return None + + class UserService: """用户管理服务。""" @@ -66,6 +99,9 @@ class UserService: } def create_user(self, username, password="", **kwargs): + # expire_at 在 web form 里是字符串 (datetime-local), 转成 datetime + if "expire_at" in kwargs: + kwargs["expire_at"] = _parse_expire_at(kwargs["expire_at"]) with db.app.app_context(): if db.session.query(User).filter_by(username=username).first(): return {"error": "用户名已存在"} @@ -78,6 +114,9 @@ class UserService: return {"id": u.id} def update_user(self, uid, **kwargs): + # expire_at 字符串转 datetime + if "expire_at" in kwargs: + kwargs["expire_at"] = _parse_expire_at(kwargs["expire_at"]) with db.app.app_context(): u = db.session.query(User).get(uid) if not u: