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,10 +100,18 @@ 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:
|
||||
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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user