Spaces:
Running
Running
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 +4 -3
- backend/app/core.py +2 -1
- backend/app/database_user.py +17 -12
- backend/scripts/sync_data.py +4 -3
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 =
|
| 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=
|
| 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=
|
| 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 =
|
| 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=
|
| 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=
|
| 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=
|
| 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=
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 <
|
| 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 |
-
"""获取今日已使用次数(
|
| 343 |
-
today =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 = (
|
| 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 |
|