fix: 修复添加用户 500 错误 (expire_at 字符串 vs datetime)
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)
This commit is contained in:
@@ -100,11 +100,19 @@ class User(db.Model):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
"""用户是否可用(未过期、未封禁、已启用)"""
|
"""用户是否可用(未过期、未封禁、已启用)
|
||||||
|
|
||||||
|
SQLite 不存 tzinfo, 从数据库读出的 datetime 是 naive;
|
||||||
|
我们统一当作 UTC 处理后再比较, 避免 aware/naive TypeError.
|
||||||
|
"""
|
||||||
if not self.enabled or self.banned:
|
if not self.enabled or self.banned:
|
||||||
return False
|
return False
|
||||||
if self.expire_at and datetime.now(timezone.utc) > self.expire_at:
|
if self.expire_at:
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|||||||
@@ -15,6 +15,39 @@ _FAIL2BAN_MAX = 10
|
|||||||
_FAIL2BAN_WINDOW = 600
|
_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:
|
class UserService:
|
||||||
"""用户管理服务。"""
|
"""用户管理服务。"""
|
||||||
|
|
||||||
@@ -66,6 +99,9 @@ class UserService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def create_user(self, username, password="", **kwargs):
|
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():
|
with db.app.app_context():
|
||||||
if db.session.query(User).filter_by(username=username).first():
|
if db.session.query(User).filter_by(username=username).first():
|
||||||
return {"error": "用户名已存在"}
|
return {"error": "用户名已存在"}
|
||||||
@@ -78,6 +114,9 @@ class UserService:
|
|||||||
return {"id": u.id}
|
return {"id": u.id}
|
||||||
|
|
||||||
def update_user(self, uid, **kwargs):
|
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():
|
with db.app.app_context():
|
||||||
u = db.session.query(User).get(uid)
|
u = db.session.query(User).get(uid)
|
||||||
if not u:
|
if not u:
|
||||||
|
|||||||
Reference in New Issue
Block a user