From e44802ae40dfa7f3d9c58b50d9e5a823f79256d4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 16 Jul 2026 23:12:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=20500=20=E9=94=99=E8=AF=AF=20(expire=5Fat=20?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2=20vs=20datetime)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit web form 提交时 expire_at 字段是 HTML datetime-local 字符串 (如 '2027-12-31T23:59'), SQLAlchemy 直接写入 datetime 列会抛 TypeError. 同样, API 路径里如果传 ISO 字符串而不带 tzinfo, 写入后再读出是 naive datetime, is_active() 比较时 会抛 'can't compare offset-naive and offset-aware datetimes'. 修复: - services/user_service.py: 增加 _parse_expire_at() helper, 统一转成 UTC-aware datetime, 支持 datetime-local 'T'/' ' 多种分隔符 + ISO 格式 - services/user_service.py: create_user/update_user 调用 helper - models.py: User.is_active() 兼容 naive datetime (SQLite 不存 tzinfo) --- models.py | 14 +++++++++++--- services/user_service.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) 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: