superxuu commited on
Commit
de2aac1
·
1 Parent(s): 1c521b8

feat: unify all system time records to Beijing Time (UTC+8)

Browse files
backend/app/api.py CHANGED
@@ -34,6 +34,7 @@ from .database_user import (
34
  register_user,
35
  delete_user as db_delete_user,
36
  sync_user_db_after_update,
 
37
  )
38
  from .limiter import limiter
39
 
@@ -111,7 +112,7 @@ def _get_token_from_header(authorization: Optional[str]) -> Optional[str]:
111
 
112
  def check_vip_status(user_id: int, db: Session) -> dict:
113
  membership = db.get(UserMembership, user_id)
114
- now = datetime.utcnow()
115
  if membership is None or membership.vip_expire_at is None:
116
  return {"is_vip": False, "vip_expire_at": None}
117
 
@@ -175,7 +176,7 @@ async def register(request: Request, payload: RegisterRequest, db: Session = Dep
175
  session = UserSession(
176
  token=token,
177
  user_id=user.id,
178
- expire_at=datetime.utcnow() + timedelta(days=SESSION_EXPIRE_DAYS)
179
  )
180
  db.add(session)
181
  db.commit()
@@ -195,7 +196,7 @@ async def login(request: Request, payload: LoginRequest, db: Session = Depends(g
195
  session = UserSession(
196
  token=token,
197
  user_id=user.id,
198
- expire_at=datetime.utcnow() + timedelta(days=SESSION_EXPIRE_DAYS)
199
  )
200
  db.add(session)
201
  db.commit()
 
34
  register_user,
35
  delete_user as db_delete_user,
36
  sync_user_db_after_update,
37
+ get_beijing_time,
38
  )
39
  from .limiter import limiter
40
 
 
112
 
113
  def check_vip_status(user_id: int, db: Session) -> dict:
114
  membership = db.get(UserMembership, user_id)
115
+ now = get_beijing_time()
116
  if membership is None or membership.vip_expire_at is None:
117
  return {"is_vip": False, "vip_expire_at": None}
118
 
 
176
  session = UserSession(
177
  token=token,
178
  user_id=user.id,
179
+ expire_at=get_beijing_time() + timedelta(days=SESSION_EXPIRE_DAYS)
180
  )
181
  db.add(session)
182
  db.commit()
 
196
  session = UserSession(
197
  token=token,
198
  user_id=user.id,
199
+ expire_at=get_beijing_time() + timedelta(days=SESSION_EXPIRE_DAYS)
200
  )
201
  db.add(session)
202
  db.commit()
backend/app/core.py CHANGED
@@ -11,6 +11,7 @@ from typing import Optional, List, Dict, Any
11
  from pydantic import BaseModel
12
 
13
  from .database import get_db
 
14
 
15
  logger = logging.getLogger(__name__)
16
 
@@ -68,7 +69,7 @@ def get_eligible_stocks() -> List[Dict[str, Any]]:
68
  db = get_db()
69
  logger.info("Querying eligible stocks from database (first time)...")
70
 
71
- cutoff_date = datetime.now() - timedelta(days=MIN_LISTING_YEARS * 365)
72
 
73
 
74
  result = db.conn.execute("""
 
11
  from pydantic import BaseModel
12
 
13
  from .database import get_db
14
+ from .database_user import get_beijing_time
15
 
16
  logger = logging.getLogger(__name__)
17
 
 
69
  db = get_db()
70
  logger.info("Querying eligible stocks from database (first time)...")
71
 
72
+ cutoff_date = get_beijing_time() - timedelta(days=MIN_LISTING_YEARS * 365)
73
 
74
 
75
  result = db.conn.execute("""
backend/app/database_user.py CHANGED
@@ -17,7 +17,7 @@ from dotenv import load_dotenv
17
  load_dotenv()
18
 
19
  from dataclasses import dataclass
20
- from datetime import datetime, timedelta
21
  from functools import wraps
22
  from pathlib import Path
23
  from typing import Optional, Callable, Any
@@ -51,13 +51,18 @@ class Base(DeclarativeBase):
51
  pass
52
 
53
 
 
 
 
 
 
54
  class User(Base):
55
  __tablename__ = "users"
56
 
57
  id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
58
  username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
59
  password_hash: Mapped[str] = mapped_column(String(128), nullable=False)
60
- created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
61
 
62
  @property
63
  def is_admin(self) -> bool:
@@ -70,7 +75,7 @@ class UserMembership(Base):
70
 
71
  user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), primary_key=True)
72
  vip_expire_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
73
- updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
74
 
75
 
76
  class PaymentOrder(Base):
@@ -83,7 +88,7 @@ class PaymentOrder(Base):
83
  pay_type: Mapped[int] = mapped_column(Integer, default=1) # 1: 支付宝, 2: 微信
84
  vip_duration_months: Mapped[int] = mapped_column(Integer, default=1) # 购买月数
85
  status: Mapped[str] = mapped_column(String(32), default="pending", index=True) # pending, paid, expired
86
- created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
87
  paid_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
88
  raw_payload: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
89
 
@@ -94,7 +99,7 @@ class UserSession(Base):
94
  token: Mapped[str] = mapped_column(String(128), primary_key=True)
95
  user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
96
  expire_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
97
- created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
98
 
99
 
100
  class DailyUsage(Base):
@@ -274,7 +279,7 @@ def create_session_token() -> str:
274
  def create_payment_order(db: Session, user_id: int, amount: float, pay_type: int = 1, months: int = 1) -> str:
275
  """创建待支付订单"""
276
  # 生成订单号: YYYYMMDDHHMMSS + 6位随机
277
- order_id = datetime.utcnow().strftime("%Y%m%d%H%M%S") + secrets.token_hex(3)
278
 
279
  order = PaymentOrder(
280
  order_id=order_id,
@@ -293,7 +298,7 @@ def create_payment_order(db: Session, user_id: int, amount: float, pay_type: int
293
  @sync_user_db_after_update
294
  def extend_vip_membership(db: Session, user_id: int, days: int = 30) -> datetime:
295
  membership = db.get(UserMembership, user_id)
296
- now = datetime.utcnow()
297
 
298
  if membership is None:
299
  membership = UserMembership(user_id=user_id, vip_expire_at=now + timedelta(days=days))
@@ -316,7 +321,7 @@ def update_order_status(db: Session, order_id: str, status: str) -> bool:
316
 
317
  order.status = status
318
  if status == "paid" and not order.paid_at:
319
- order.paid_at = datetime.utcnow()
320
  elif status == "pending":
321
  order.paid_at = None
322
 
@@ -328,7 +333,7 @@ def get_user_by_token(db: Session, token: str) -> Optional[User]:
328
  session_row = db.get(UserSession, token)
329
  if not session_row:
330
  return None
331
- if session_row.expire_at < datetime.utcnow():
332
  db.delete(session_row)
333
  db.commit()
334
  return None
@@ -339,8 +344,8 @@ FREE_DAILY_LIMIT = int(os.getenv("FREE_DAILY_LIMIT", "3"))
339
 
340
 
341
  def get_daily_usage(db: Session, user_id: int) -> int:
342
- """获取今日已使用次数(UTC 日期)"""
343
- today = datetime.utcnow().date()
344
  row = db.get(DailyUsage, {"user_id": user_id, "use_date": today})
345
  return row.count if row else 0
346
 
@@ -348,7 +353,7 @@ def get_daily_usage(db: Session, user_id: int) -> int:
348
  @sync_user_db_after_update
349
  def increment_daily_usage(db: Session, user_id: int) -> int:
350
  """今日使用次数 +1,返回更新后的次数"""
351
- today = datetime.utcnow().date()
352
  row = db.get(DailyUsage, {"user_id": user_id, "use_date": today})
353
  if row is None:
354
  row = DailyUsage(user_id=user_id, use_date=today, count=1)
 
17
  load_dotenv()
18
 
19
  from dataclasses import dataclass
20
+ from datetime import datetime, timedelta, timezone
21
  from functools import wraps
22
  from pathlib import Path
23
  from typing import Optional, Callable, Any
 
51
  pass
52
 
53
 
54
+ def get_beijing_time() -> datetime:
55
+ """获取北京时间 (UTC+8)"""
56
+ return datetime.now(timezone(timedelta(hours=8))).replace(tzinfo=None)
57
+
58
+
59
  class User(Base):
60
  __tablename__ = "users"
61
 
62
  id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
63
  username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
64
  password_hash: Mapped[str] = mapped_column(String(128), nullable=False)
65
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=get_beijing_time)
66
 
67
  @property
68
  def is_admin(self) -> bool:
 
75
 
76
  user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), primary_key=True)
77
  vip_expire_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
78
+ updated_at: Mapped[datetime] = mapped_column(DateTime, default=get_beijing_time, onupdate=get_beijing_time)
79
 
80
 
81
  class PaymentOrder(Base):
 
88
  pay_type: Mapped[int] = mapped_column(Integer, default=1) # 1: 支付宝, 2: 微信
89
  vip_duration_months: Mapped[int] = mapped_column(Integer, default=1) # 购买月数
90
  status: Mapped[str] = mapped_column(String(32), default="pending", index=True) # pending, paid, expired
91
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=get_beijing_time)
92
  paid_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
93
  raw_payload: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
94
 
 
99
  token: Mapped[str] = mapped_column(String(128), primary_key=True)
100
  user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
101
  expire_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
102
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=get_beijing_time)
103
 
104
 
105
  class DailyUsage(Base):
 
279
  def create_payment_order(db: Session, user_id: int, amount: float, pay_type: int = 1, months: int = 1) -> str:
280
  """创建待支付订单"""
281
  # 生成订单号: YYYYMMDDHHMMSS + 6位随机
282
+ order_id = get_beijing_time().strftime("%Y%m%d%H%M%S") + secrets.token_hex(3)
283
 
284
  order = PaymentOrder(
285
  order_id=order_id,
 
298
  @sync_user_db_after_update
299
  def extend_vip_membership(db: Session, user_id: int, days: int = 30) -> datetime:
300
  membership = db.get(UserMembership, user_id)
301
+ now = get_beijing_time()
302
 
303
  if membership is None:
304
  membership = UserMembership(user_id=user_id, vip_expire_at=now + timedelta(days=days))
 
321
 
322
  order.status = status
323
  if status == "paid" and not order.paid_at:
324
+ order.paid_at = get_beijing_time()
325
  elif status == "pending":
326
  order.paid_at = None
327
 
 
333
  session_row = db.get(UserSession, token)
334
  if not session_row:
335
  return None
336
+ if session_row.expire_at < get_beijing_time():
337
  db.delete(session_row)
338
  db.commit()
339
  return None
 
344
 
345
 
346
  def get_daily_usage(db: Session, user_id: int) -> int:
347
+ """获取今日已使用次数(北京时间日期)"""
348
+ today = get_beijing_time().date()
349
  row = db.get(DailyUsage, {"user_id": user_id, "use_date": today})
350
  return row.count if row else 0
351
 
 
353
  @sync_user_db_after_update
354
  def increment_daily_usage(db: Session, user_id: int) -> int:
355
  """今日使用次数 +1,返回更新后的次数"""
356
+ today = get_beijing_time().date()
357
  row = db.get(DailyUsage, {"user_id": user_id, "use_date": today})
358
  if row is None:
359
  row = DailyUsage(user_id=user_id, use_date=today, count=1)
backend/scripts/sync_data.py CHANGED
@@ -21,6 +21,7 @@ from huggingface_hub import hf_hub_download, upload_file
21
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
22
 
23
  from app.database import get_db
 
24
 
25
  logging.basicConfig(
26
  level=logging.INFO,
@@ -92,7 +93,7 @@ def get_target_daily(code: str, start_date: str, market: str) -> Optional[pd.Dat
92
  max_retries = 3 # 增加重试次数
93
  for attempt in range(max_retries):
94
  try:
95
- end_date = datetime.now().strftime('%Y%m%d')
96
  fetch_start = start_date.replace('-', '')
97
  df = None
98
  if market == 'INDEX':
@@ -158,7 +159,7 @@ def get_last_trading_day() -> str:
158
  logger.warning(f"Failed to get last trading day from index data: {e}")
159
 
160
  # 回退:按工作日估算
161
- d = datetime.now()
162
  while d.weekday() >= 5: # 5=周六, 6=周日
163
  d -= timedelta(days=1)
164
  return d.strftime('%Y-%m-%d')
@@ -217,7 +218,7 @@ def sync_stock_daily(targets: List[Dict[str, str]], last_trade_day: str) -> int:
217
  if latest_map[code] >= last_trade_day: continue
218
  start_dt = (pd.to_datetime(latest_map[code]) + timedelta(days=1)).strftime('%Y-%m-%d')
219
  else:
220
- start_dt = (datetime.now() - timedelta(days=YEARS_OF_DATA * 365)).strftime('%Y-%m-%d')
221
  t['start_dt'] = start_dt
222
  pending.append(t)
223
 
 
21
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
22
 
23
  from app.database import get_db
24
+ from app.database_user import get_beijing_time
25
 
26
  logging.basicConfig(
27
  level=logging.INFO,
 
93
  max_retries = 3 # 增加重试次数
94
  for attempt in range(max_retries):
95
  try:
96
+ end_date = get_beijing_time().strftime('%Y%m%d')
97
  fetch_start = start_date.replace('-', '')
98
  df = None
99
  if market == 'INDEX':
 
159
  logger.warning(f"Failed to get last trading day from index data: {e}")
160
 
161
  # 回退:按工作日估算
162
+ d = get_beijing_time()
163
  while d.weekday() >= 5: # 5=周六, 6=周日
164
  d -= timedelta(days=1)
165
  return d.strftime('%Y-%m-%d')
 
218
  if latest_map[code] >= last_trade_day: continue
219
  start_dt = (pd.to_datetime(latest_map[code]) + timedelta(days=1)).strftime('%Y-%m-%d')
220
  else:
221
+ start_dt = (get_beijing_time() - timedelta(days=YEARS_OF_DATA * 365)).strftime('%Y-%m-%d')
222
  t['start_dt'] = start_dt
223
  pending.append(t)
224