@@ -207,6 +207,7 @@ def login():
|
||||
'username': user.get('username', username),
|
||||
'nickname': user.get('nickname', 'User'),
|
||||
'avatar': user.get('avatar', '/avatar2.jpg'),
|
||||
'timezone': str(user.get('timezone') or '').strip(),
|
||||
'role': {
|
||||
'id': user.get('role', 'admin'),
|
||||
'permissions': _get_permissions(user.get('role', 'admin'))
|
||||
@@ -419,6 +420,7 @@ def login_with_code():
|
||||
'nickname': user.get('nickname', user['username']),
|
||||
'email': user.get('email'),
|
||||
'avatar': user.get('avatar', '/avatar2.jpg'),
|
||||
'timezone': str(user.get('timezone') or '').strip(),
|
||||
'role': {
|
||||
'id': user.get('role', 'user'),
|
||||
'permissions': _get_permissions(user.get('role', 'user'))
|
||||
@@ -685,6 +687,7 @@ def register():
|
||||
'nickname': username,
|
||||
'email': email,
|
||||
'avatar': '/avatar2.jpg',
|
||||
'timezone': '',
|
||||
'role': {
|
||||
'id': 'user',
|
||||
'permissions': _get_permissions('user')
|
||||
@@ -1049,6 +1052,7 @@ def get_user_info():
|
||||
'nickname': user_data.get('nickname', 'User'),
|
||||
'email': user_data.get('email'),
|
||||
'avatar': user_data.get('avatar', '/avatar2.jpg'),
|
||||
'timezone': str(user_data.get('timezone') or '').strip(),
|
||||
'role': {
|
||||
'id': user_data.get('role', 'user'),
|
||||
'permissions': _get_permissions(user_data.get('role', 'user'))
|
||||
@@ -1065,6 +1069,7 @@ def get_user_info():
|
||||
'username': username,
|
||||
'nickname': 'Admin',
|
||||
'avatar': '/avatar2.jpg',
|
||||
'timezone': '',
|
||||
'role': {
|
||||
'id': role,
|
||||
'permissions': _get_permissions(role)
|
||||
|
||||
@@ -3,6 +3,7 @@ Portfolio API routes (local-only).
|
||||
Manages manual positions (user's existing holdings) and AI monitoring tasks.
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from datetime import date, datetime, timezone
|
||||
import json
|
||||
import traceback
|
||||
import time
|
||||
@@ -39,6 +40,27 @@ def _now_ts() -> int:
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def _serialize_monitor_ts(value):
|
||||
"""
|
||||
JSON 序列化监控时间字段。PostgreSQL TIMESTAMP 无 tz 时按 UTC 解释(与 Docker 默认一致),
|
||||
输出带 Z 的 ISO,避免前端把无时区字符串当本地时间而偏差 8 小时。
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
if value.tzinfo is None:
|
||||
dt = value.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
dt = value.astimezone(timezone.utc)
|
||||
s = dt.isoformat()
|
||||
if s.endswith("+00:00"):
|
||||
return s[:-6] + "Z"
|
||||
return s
|
||||
if isinstance(value, date):
|
||||
return value.isoformat()
|
||||
return value
|
||||
|
||||
|
||||
def _normalize_symbol(symbol: str) -> str:
|
||||
return (symbol or '').strip().upper()
|
||||
|
||||
@@ -519,12 +541,12 @@ def get_monitors():
|
||||
'config': _safe_json_loads(row.get('config'), {}),
|
||||
'notification_config': _safe_json_loads(row.get('notification_config'), {}),
|
||||
'is_active': bool(row.get('is_active')),
|
||||
'last_run_at': row.get('last_run_at'),
|
||||
'next_run_at': row.get('next_run_at'),
|
||||
'last_run_at': _serialize_monitor_ts(row.get('last_run_at')),
|
||||
'next_run_at': _serialize_monitor_ts(row.get('next_run_at')),
|
||||
'last_result': _safe_json_loads(row.get('last_result'), {}),
|
||||
'run_count': row.get('run_count') or 0,
|
||||
'created_at': row.get('created_at'),
|
||||
'updated_at': row.get('updated_at')
|
||||
'created_at': _serialize_monitor_ts(row.get('created_at')),
|
||||
'updated_at': _serialize_monitor_ts(row.get('updated_at'))
|
||||
})
|
||||
|
||||
return jsonify({'code': 1, 'msg': 'success', 'data': monitors})
|
||||
|
||||
@@ -872,12 +872,16 @@ def get_strategy_notifications():
|
||||
cur.close()
|
||||
|
||||
# Convert created_at to UTC timestamp (seconds) for frontend
|
||||
from datetime import timezone as _dt_tz
|
||||
processed_rows = []
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
created_at = item.get('created_at')
|
||||
if created_at:
|
||||
if hasattr(created_at, 'timestamp'):
|
||||
# 无时区 datetime:连接已 SET TIME ZONE UTC,按 UTC 解释再转 Unix,避免服务端本地 TZ 误判
|
||||
if getattr(created_at, 'tzinfo', None) is None:
|
||||
created_at = created_at.replace(tzinfo=_dt_tz.utc)
|
||||
item['created_at'] = int(created_at.timestamp())
|
||||
elif isinstance(created_at, str):
|
||||
try:
|
||||
|
||||
@@ -5,6 +5,7 @@ Provides endpoints for user CRUD operations, role management, etc.
|
||||
Only accessible by admin users.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from app.services.user_service import get_user_service
|
||||
from app.utils.auth import login_required, admin_required
|
||||
@@ -13,6 +14,8 @@ from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_PROFILE_TIMEZONE_RE = re.compile(r'^[A-Za-z0-9_/+\-.]+$')
|
||||
|
||||
user_bp = Blueprint('user_manage', __name__)
|
||||
|
||||
|
||||
@@ -411,6 +414,7 @@ def update_profile():
|
||||
Request body:
|
||||
nickname: str (optional)
|
||||
avatar: str (optional)
|
||||
timezone: str (optional, IANA id; empty = follow client)
|
||||
|
||||
Note: Email cannot be changed after registration (for security).
|
||||
Only admin can change user email via User Management.
|
||||
@@ -429,6 +433,16 @@ def update_profile():
|
||||
if field in data:
|
||||
allowed[field] = data[field]
|
||||
|
||||
if 'timezone' in data:
|
||||
tz = (data.get('timezone') or '').strip()
|
||||
if tz and (len(tz) > 64 or not _PROFILE_TIMEZONE_RE.match(tz)):
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'msg': 'Invalid timezone identifier',
|
||||
'data': None
|
||||
}), 400
|
||||
allowed['timezone'] = tz
|
||||
|
||||
if not allowed:
|
||||
return jsonify({'code': 0, 'msg': 'No valid fields to update', 'data': None}), 400
|
||||
|
||||
|
||||
Reference in New Issue
Block a user