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:
Your Name
2026-07-16 23:12:45 +08:00
parent 0bd9bcac12
commit e44802ae40
2 changed files with 50 additions and 3 deletions
+10 -2
View File
@@ -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
+39
View File
@@ -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: