jerry 6 месяцев назад
Родитель
Сommit
28059c1b3a

+ 1 - 1
.env

@@ -1,3 +1,3 @@
-DATABASE_URL=mysql://root:GqLLL7Bofj0WaaOpp.0@visafly.top:3306/book_user_info?charset=utf8mb4&timezone=UTC
+DATABASE_URL=mysql://root:GqLLL7Bofj0WaaOpp.0@visafly.top:3306/book_user_info?charset=utf8mb4
 REDIS_URL=redis://:STEs2x6ML0U1HlpE9SojM6YU7QPhqzY8@45.137.220.138:6379/0
 REDIS_URL=redis://:STEs2x6ML0U1HlpE9SojM6YU7QPhqzY8@45.137.220.138:6379/0
 API_TOKEN=7x9EjFpmv7GjZc6AfVeqxuUBANpqkpkHAtxJM7CAW5oZhs0nEyCJBy39N4XXs5hgfYWXw3jFrcgXqQ42HAx9Qvwtk9vC2GvKBbWz
 API_TOKEN=7x9EjFpmv7GjZc6AfVeqxuUBANpqkpkHAtxJM7CAW5oZhs0nEyCJBy39N4XXs5hgfYWXw3jFrcgXqQ42HAx9Qvwtk9vC2GvKBbWz

+ 675 - 20
app/api/router.py

@@ -1,24 +1,55 @@
 import time
 import time
+import requests
 from typing import List
 from typing import List
-from fastapi import APIRouter, Query, Depends
+from app.core.logger import logger
+from fastapi import APIRouter, Query, Depends, Body, UploadFile, File, HTTPException
+from fastapi.responses import RedirectResponse
+from sqlalchemy.orm import Session
 from app.core.redis import get_redis_client
 from app.core.redis import get_redis_client
+from app.core.database import get_db
 from redis.asyncio import Redis
 from redis.asyncio import Redis
 from app.schemas.user import UserOut
 from app.schemas.user import UserOut
 from app.schemas.troov import TroovRate
 from app.schemas.troov import TroovRate
 from app.schemas.sms import ShortMessageDetail
 from app.schemas.sms import ShortMessageDetail
+from app.schemas.configuration import ConfigurationCreate, ConfigurationUpdate, ConfigurationOut
+from app.schemas.email_authorizations import EmailContent, EmailAuthorizationCreate, EmailAuthorizationUpdate, EmailAuthorizationOut
+from app.schemas.card import CardCreate, CardOut
+from app.schemas.task import TaskCreate, TaskOut, TaskUpdate
+from app.schemas.short_url import ShortUrlCreate
+from app.schemas.auto_booking import AutoBookingCreate, AutoBookingOut
+from app.schemas.http_session import HttpSessionCreate, HttpSessionUpdate,HttpSessionOut
+from app.schemas.visafly_config import VisaflyConfigCreate, VisaflyConfigOut
+from app.schemas.slot import SlotCreate, SlotOut
+from app.services.configuration_service import ConfigurationService
 from app.services.troov_service import get_rate_by_date
 from app.services.troov_service import get_rate_by_date
 from app.services.sms_service import save_short_message, query_short_message
 from app.services.sms_service import save_short_message, query_short_message
+from app.services.email_authorizations_service import EmailAuthorizationService
+from app.services.short_url_service import ShortUrlService
+from app.services.task_service import TaskService
+from app.services.card_service import CardService
+from app.services.seaweedfs_service import SeaweedFSService
+from app.services.auto_booking_service import AutoBookingService
+from app.services.http_session_service import HttpSessionService
+from app.services.visafly_config_service import VisaflyConfigService
+from app.services.slot_service import SlotService
 
 
 # 公共路由
 # 公共路由
-public_router = APIRouter(tags=["public"])
+public_router = APIRouter()
 # 受保护路由
 # 受保护路由
-protected_router = APIRouter(tags=["protected"])
+protected_router = APIRouter()
 
 
-@public_router.get("/ping", summary="心跳检测")
+@public_router.get("/ping", summary="心跳检测", tags=["测试接口"])
 def ping():
 def ping():
     return {"message": "pong"}
     return {"message": "pong"}
 
 
-@public_router.get("/sms/upload", summary="上报短信", response_model=ShortMessageDetail)
+@protected_router.get("/users", summary="查询用户", tags=["通用接口"], response_model=List[UserOut])
+def get_users():
+    return [
+        {"id": 1, "name": "Alice"},
+        {"id": 2, "name": "Bob"}
+    ]
+
+@public_router.get("/sms/upload", summary="上报短信", tags=["短信接口"], response_model=ShortMessageDetail)
 def sms_upload(
 def sms_upload(
     phone: str = Query(..., description="手机号"),
     phone: str = Query(..., description="手机号"),
     message: str = Query(..., description="短信内容"),
     message: str = Query(..., description="短信内容"),
@@ -32,20 +63,7 @@ def sms_upload(
     msg = save_short_message(redis_client, phone, message, received_at, max_ttl)
     msg = save_short_message(redis_client, phone, message, received_at, max_ttl)
     return msg
     return msg
 
 
-@protected_router.get("/users", summary="查询用户", response_model=List[UserOut])
-def get_users():
-    return [
-        {"id": 1, "name": "Alice"},
-        {"id": 2, "name": "Bob"}
-    ]
-
-@protected_router.get("/troov/rate", summary="TROOV 查询rate", response_model=List[TroovRate])
-def troov_rate(date: str = Query(..., description="查询的日期, 格式: YYYY-MM-DD"),
-               redis_client: Redis = Depends(get_redis_client)):
-    # 调用 service 层获取数据
-    return get_rate_by_date(redis_client, date)
-
-@protected_router.get("/sms/download", summary="读取短信", response_model=List[ShortMessageDetail])
+@protected_router.get("/sms/download", summary="读取短信", tags=["短信接口"], response_model=List[ShortMessageDetail])
 def sms_download(
 def sms_download(
     phone: str = Query(..., description="手机号"),
     phone: str = Query(..., description="手机号"),
     keyword: str = Query('', description="短信内容关键字"),
     keyword: str = Query('', description="短信内容关键字"),
@@ -56,4 +74,641 @@ def sms_download(
     查询短信(支持关键字和时间过滤)
     查询短信(支持关键字和时间过滤)
     """
     """
     results = query_short_message(redis_client, phone, keyword or None, sent_at or None)
     results = query_short_message(redis_client, phone, keyword or None, sent_at or None)
-    return results
+    return results
+
+@protected_router.get("/troov/rate", summary="TROOV 查询rate", tags=["通用接口"], response_model=List[TroovRate])
+def troov_rate(date: str = Query(..., description="查询的日期, 格式: YYYY-MM-DD"),
+               redis_client: Redis = Depends(get_redis_client)):
+    # 调用 service 层获取数据
+    return get_rate_by_date(redis_client, date)
+
+@protected_router.post("/dynamic-configurations", summary="创建动态配置", tags=["动态配置"], response_model=ConfigurationOut)
+def dynamic_config_create(config_in: ConfigurationCreate, db: Session = Depends(get_db)):
+    existing = ConfigurationService.get_by_key(db, config_in.config_key)
+    if existing:
+        raise HTTPException(status_code=400, detail=f"配置 Key '{config_in.config_key}' 已存在")
+    return ConfigurationService.create(db, config_in)
+
+
+@protected_router.get("/dynamic-configurations/all", summary="读取所有动态配置", tags=["动态配置"], response_model=List[ConfigurationOut])
+def dynamic_config_get_all(db: Session = Depends(get_db)):
+    return ConfigurationService.get_all(db)
+
+
+@protected_router.get("/dynamic-configurations/key/{config_key}", summary="根据Key读取动态配置", tags=["动态配置"], response_model=ConfigurationOut)
+def dynamic_config_get_by_key(config_key: str, db: Session = Depends(get_db)):
+    config = ConfigurationService.get_by_key(db, config_key)
+    if not config:
+        raise HTTPException(status_code=404, detail=f"配置 Key '{config_key}' 不存在")
+    return config
+
+
+@protected_router.put("/dynamic-configurations/key/{config_key}", summary="根据Key更新动态配置", tags=["动态配置"], response_model=ConfigurationOut)
+def dynamic_config_update_by_key(config_key: str, config_in: ConfigurationUpdate, db: Session = Depends(get_db)):
+    config = ConfigurationService.update_by_key(db, config_key, config_in)
+    if not config:
+        raise HTTPException(status_code=404, detail=f"配置 Key '{config_key}' 不存在")
+    return config
+
+
+@protected_router.delete("/dynamic-configurations/key/{config_key}", summary="根据Key删除动态配置", tags=["动态配置"], response_model=ConfigurationOut)
+def dynamic_config_delete_by_key(config_key: str, db: Session = Depends(get_db)):
+    config = ConfigurationService.delete_by_key(db, config_key)
+    if not config:
+        raise HTTPException(status_code=404, detail=f"配置 Key '{config_key}' 不存在")
+    return config
+
+@protected_router.post(
+    "/http-session",
+    summary="创建http session",
+    tags=["会话管理"],
+    response_model=HttpSessionOut
+)
+def http_session_create(
+    data: HttpSessionCreate,
+    db: Session = Depends(get_db)
+):
+    logger.info(f"[Create HttpSession] sid={data.session_id}")
+    return HttpSessionService.create(db, data)
+
+
+@protected_router.delete(
+    "/http-session",
+    summary="删除http session",
+    tags=["会话管理"]
+)
+def http_session_delete_by_sid(
+    session_id: str = Query(...),
+    db: Session = Depends(get_db)
+):
+    logger.info(f"[Delete HttpSession] sid={session_id}")
+
+    ok = HttpSessionService.delete_by_sid(db, session_id)
+    if not ok:
+        raise HTTPException(status_code=404, detail="session 不存在")
+
+    return {"success": True, "session_id": session_id}
+
+
+@protected_router.put(
+    "/http-session",
+    summary="更新http session",
+    tags=["会话管理"],
+    response_model=HttpSessionOut
+)
+def http_session_update_by_sid(
+    session_id: str = Query(...),
+    data: HttpSessionUpdate = Body(...),
+    db: Session = Depends(get_db)
+):
+    logger.info(f"[Update HttpSession] sid={session_id}")
+
+    obj = HttpSessionService.update_by_sid(db, session_id, data)
+    if not obj:
+        raise HTTPException(status_code=404, detail="session 不存在")
+
+    return obj
+
+@protected_router.get(
+    "/http-session",
+    summary="读取http session",
+    tags=["会话管理"],
+    response_model=HttpSessionOut
+)
+def http_session_get_by_sid(
+    session_id: str = Query(...),
+    db: Session = Depends(get_db)
+):
+    logger.info(f"[Get HttpSession] sid={session_id}")
+
+    obj = HttpSessionService.get_by_sid(db, session_id)
+    if not obj:
+        raise HTTPException(status_code=404, detail="session 不存在")
+
+    return obj
+
+
+@protected_router.get("/email-authorizations", summary="查询所有内部邮箱", tags=["邮箱接口"], response_model=List[EmailAuthorizationOut])
+def email_authorizations_get(db: Session = Depends(get_db)):
+    return EmailAuthorizationService.get_all(db)
+
+
+@protected_router.post("/email-authorizations", summary="创建内部邮箱", tags=["邮箱接口"], response_model=EmailAuthorizationOut)
+def email_authorizations_create(data: EmailAuthorizationCreate, db: Session = Depends(get_db)):
+    existing = EmailAuthorizationService.get_by_email(db, data.email)
+    if existing:
+        raise HTTPException(status_code=400, detail=f"邮箱 {data.email} 已存在")
+    return EmailAuthorizationService.create(db, data)
+
+@protected_router.get("/email-authorizations/{id}", summary="通过id查询内部邮箱", tags=["邮箱接口"], response_model=EmailAuthorizationOut)
+def email_authorizations_get_by_id(id: int, db: Session = Depends(get_db)):
+    email_auth = EmailAuthorizationService.get_by_id(db, id)
+    if not email_auth:
+        raise HTTPException(status_code=404, detail=f"ID={id} 的邮箱记录不存在")
+    return email_auth
+
+
+@protected_router.put("/email-authorizations/{id}", summary="通过id更新内部邮箱", tags=["邮箱接口"], response_model=EmailAuthorizationOut)
+def email_authorizations_update_by_id(id: int, data: EmailAuthorizationUpdate, db: Session = Depends(get_db)):
+    updated = EmailAuthorizationService.update(db, id, data)
+    if not updated:
+        raise HTTPException(status_code=404, detail=f"ID={id} 的邮箱记录不存在")
+    return updated
+
+
+@protected_router.delete("/email-authorizations/{id}", summary="通过id删除内部邮箱", tags=["邮箱接口"], response_model=EmailAuthorizationOut)
+def email_authorizations_delete_by_id(id: int, db: Session = Depends(get_db)):
+    deleted = EmailAuthorizationService.delete(db, id)
+    if not deleted:
+        raise HTTPException(status_code=404, detail=f"ID={id} 的邮箱记录不存在")
+    return deleted
+
+
+@protected_router.get("/email-authorizations/email/{email}", summary="通过邮箱地址查询内部邮箱", tags=["邮箱接口"], response_model=EmailAuthorizationOut)
+def email_authorizations_get_by_email(email: str, db: Session = Depends(get_db)):
+    email_auth = EmailAuthorizationService.get_by_email(db, email)
+    if not email_auth:
+        raise HTTPException(status_code=404, detail=f"邮箱 {email} 不存在")
+    return email_auth
+
+@protected_router.post("/email-authorizations/fetch", summary="读取邮件, 仅限文本内容", tags=["邮箱接口"])
+def email_authorizations_fetch_email(
+    email: str = Query(..., description="收件邮箱账号, 格式: xxx@xxx.xxx"),
+    sender: str = Query(..., description="发件人邮箱账号或者名字"),
+    recipient: str = Query(..., description="收件人账号或者名字"),
+    subjectKeywords: str = Query("", description="邮件主题关键词, 支持多个关键词, 用逗号隔开"),
+    bodyKeywords: str = Query("", description="邮件内容关键词, 支持多个关键词, 用逗号隔开"),
+    sentDate: str = Query(..., description="发件日期, UTC时间, 格式: yyyy-mm-dd hh:mm:ss"),
+    expiry: int = Query(300, description="邮件有效期, 单位秒, 从sentDate 开始算起"),
+    db: Session = Depends(get_db)
+):
+    auth = EmailAuthorizationService.get_by_email(db, email)
+    if not auth:
+        raise HTTPException(status_code=404, detail=f"未找到邮箱授权记录: {email}")
+    result = EmailAuthorizationService.fetch_email_authorizations(
+        auth,
+        sender=sender,
+        recipient=recipient,
+        subject_keywords=subjectKeywords,
+        body_keywords=bodyKeywords,
+        sent_date=sentDate,
+        expiry=expiry,
+        only_text=True
+    )
+    if result is None:
+        raise HTTPException(status_code=404, detail="在有效期内未找到匹配邮件")
+    return {"body": result}
+
+@protected_router.post("/email-authorizations/fetch-top", summary="从最近的几封邮件读取目标邮件,仅限文本内容, 性能会更好, 邮件多时有可能漏读", tags=["邮箱接口"])
+def email_authorizations_fetch_email_from_topn(
+    email: str = Query(..., description="收件邮箱账号, 格式: xxx@xxx.xxx"),
+    sender: str = Query(..., description="发件人邮箱账号或者名字"),
+    recipient: str = Query(..., description="收件人账号或者名字"),
+    subjectKeywords: str = Query("", description="邮件主题关键词, 支持多个关键词, 用逗号隔开"),
+    bodyKeywords: str = Query("", description="邮件内容关键词, 支持多个关键词, 用逗号隔开"),
+    top: int = Query(10, description="指定从最近几封邮件读取"),
+    db: Session = Depends(get_db)
+):
+    auth = EmailAuthorizationService.get_by_email(db, email)
+    if not auth:
+        raise HTTPException(status_code=404, detail=f"未找到邮箱授权记录: {email}")
+    result = EmailAuthorizationService.fetch_email_authorizations_from_top_n(
+        auth,
+        sender=sender,
+        recipient=recipient,
+        subject_keywords=subjectKeywords,
+        body_keywords=bodyKeywords,
+        top=top,
+        only_text=True
+    )
+    if result is None:
+        raise HTTPException(status_code=404, detail=f"未在前{top}封邮件中查找到匹配邮件")
+    return {"body": result}
+
+@protected_router.post("/email-authorizations/forward", summary="转发邮件", tags=["邮箱接口"])
+def email_authorizations_forward_email(
+    emailAccount: str = Query(..., description="收件邮箱账号, 格式: xxx@xxx.xxx"),
+    forwardTo: str = Query(..., description="转发到哪个邮箱地址, 格式: xxx@xxx.xxx"),
+    sender: str = Query(..., description="发件人邮箱账号或者名字"),
+    recipient: str = Query(..., description="收件人账号或者名字"),
+    subjectKeywords: str = Query("", description="邮件主题关键词, 支持多个关键词, 用逗号隔开"),
+    bodyKeywords: str = Query("", description="邮件内容关键词, 支持多个关键词, 用逗号隔开"),
+    db: Session = Depends(get_db)
+):
+    auth = EmailAuthorizationService.get_by_email(db, emailAccount)
+    if not auth:
+        raise HTTPException(status_code=404, detail=f"未找到邮箱授权记录: {emailAccount}")
+    result = EmailAuthorizationService.forward_first_matching_email(
+        auth,
+        forward_to = forwardTo,
+        sender = sender,
+        recipient = recipient,
+        subject_keywords = subjectKeywords,
+        body_keywords = bodyKeywords
+    )
+    if result is None:
+        raise HTTPException(status_code=404, detail=f"未找可转发的邮件")
+    return {"body": result}
+
+@protected_router.post("/email-authorizations/sendmail", summary="发送邮件", tags=["邮箱接口"])
+def email_authorizations_send_email(
+    emailAccount: str = Query(..., description="收件邮箱账号, 格式: xxx@xxx.xxx"),
+    sendTo: str = Query(..., description="收件人邮箱账号"),
+    subject: str = Query(..., description="邮件主题"),
+    contentType: str = Query("text", description="内容格式,支持 text 和 html"),
+    content: EmailContent = Body("", description="邮件内容"),
+    db: Session = Depends(get_db)
+):
+    auth = EmailAuthorizationService.get_by_email(db, emailAccount)
+    if not auth:
+        raise HTTPException(status_code=404, detail=f"未找到邮箱授权记录: {emailAccount}")
+    result = EmailAuthorizationService.send_email(
+        auth,
+        send_to = sendTo,
+        subject = subject,
+        content_type = contentType,
+        content = content.body
+    )
+    if result is None:
+        raise HTTPException(status_code=404, detail=f"邮件发送失败")
+    return {"body": result}
+
+@protected_router.post("/email-authorizations/sendmail-bulk", summary="群发送邮件", tags=["邮箱接口"])
+def email_authorizations_send_email_bulk(
+    emailAccount: str = Query(..., description="收件邮箱账号, 格式: xxx@xxx.xxx"),
+    sendTo: str = Query(..., description="收件人邮箱账号,多个用逗号隔开"),
+    subject: str = Query(..., description="邮件主题"),
+    contentType: str = Query("text", description="内容格式,支持 text 和 html"),
+    content: EmailContent = Body(..., description="邮件内容"),
+    db: Session = Depends(get_db)
+):
+    auth = EmailAuthorizationService.get_by_email(db, emailAccount)
+    if not auth:
+        raise HTTPException(status_code=404, detail=f"未找到邮箱授权记录: {emailAccount}")
+    result = EmailAuthorizationService.send_email_bulk(
+        auth,
+        send_to = sendTo,
+        subject = subject,
+        content_type = contentType,
+        content = content.body
+    )
+    if result is None:
+        raise HTTPException(status_code=404, detail=f"邮件发送失败")
+    return {"body": result}
+
+
+@protected_router.post("/resource/pdf", summary="上传pdf文件", tags=["文件管理"])
+def resource_upload_pdf(pdf: UploadFile = File(...)):
+    if not pdf.filename.lower().endswith(".pdf"):
+        raise HTTPException(status_code=400, detail="仅支持上传 PDF 文件")
+
+    result = SeaweedFSService.upload(pdf)
+    if not result:
+        raise HTTPException(status_code=500, detail="上传失败")
+    return {"success": True, "fid": result["fid"], "url": result["url"]}
+
+@protected_router.get("/resource/pdf/{fid}", summary="读取pdf", tags=["文件管理"])
+def resource_get_pdf(fid: str):
+    data = SeaweedFSService.get(fid)
+    if not data:
+        raise HTTPException(status_code=404, detail="文件不存在")
+    content, mime = data
+    return Response(content=content, media_type=mime)
+
+@protected_router.delete("/resource/pdf/{fid}", summary="删除pdf文件", tags=["文件管理"])
+def resource_delete_pdf(fid: str):
+    ok = SeaweedFSService.delete(fid)
+    if not ok:
+        raise HTTPException(status_code=404, detail="文件不存在或删除失败")
+    return {"success": True, "fid": fid}
+
+@protected_router.post("/resource/image", summary="上传图片", tags=["文件管理"])
+def resource_upload_image(image: UploadFile = File(...)):
+    if not image.content_type.startswith("image/"):
+        raise HTTPException(status_code=400, detail="仅支持上传图片文件")
+    print('upload')
+    result = SeaweedFSService.upload(image)
+    if not result:
+        raise HTTPException(status_code=500, detail="上传失败")
+    return {"success": True, "fid": result["fid"], "url": result["url"]}
+
+@protected_router.get("/resource/image/{fid}", summary="读取图片", tags=["文件管理"])
+def resource_get_image(fid: str):
+    data = SeaweedFSService.get(fid)
+    if not data:
+        raise HTTPException(status_code=404, detail="图片不存在")
+    content, mime = data
+    return Response(content=content, media_type=mime)
+
+@protected_router.delete("/resource/image/{fid}", summary="删除图片", tags=["文件管理"])
+def resource_delete_image(fid: str):
+    ok = SeaweedFSService.delete(fid)
+    if not ok:
+        raise HTTPException(status_code=404, detail="图片不存在或删除失败")
+    return {"success": True, "fid": fid}
+
+@protected_router.post("/s/generate", summary="生成短链接地址<压缩地址长度>", tags=["Short URL"])
+def short_url_generate(
+    data: ShortUrlCreate,
+    db: Session = Depends(get_db),
+):
+    """生成短链接"""
+    record = ShortUrlService.create_short_url(db, data.longUrl)
+    return {
+        "short_key": record.short_key,
+        "short_url": f"/s/{record.short_key}",
+        "long_url": record.long_url,
+        "created_at": record.created_at,
+    }
+
+
+@public_router.get("/s/{shortKey}", summary="访问短链接地址<自动重定向到真实链接地址>", tags=["Short URL"])
+def short_url_request(shortKey: str, db: Session = Depends(get_db)):
+    """访问短链接自动重定向"""
+    long_url = ShortUrlService.get_long_url(db, shortKey)
+    if not long_url:
+        raise HTTPException(status_code=404, detail="短链接不存在或已失效")
+    return RedirectResponse(url=long_url, status_code=302)
+
+@protected_router.post("/tasks", summary="创建任务", tags=["任务管理接口"], response_model=TaskOut)
+def task_create(data: TaskCreate, db: Session = Depends(get_db)):
+    """创建任务"""
+    return TaskService.create(db, data)
+
+@protected_router.get("/tasks/{taskId:int}", summary="根据taskId读取任务状态", tags=["任务管理接口"], response_model=TaskOut)
+def task_get_by_id(taskId: int, db: Session = Depends(get_db)):
+    """获取任务"""
+    task = TaskService.get_by_id(db, taskId)
+    if not task:
+        raise HTTPException(status_code=404, detail=f"任务 {taskId} 不存在")
+    return task
+
+@protected_router.get("/tasks/pending", summary="获取等待执行的任务", tags=["任务管理接口"], response_model=List[TaskOut])
+def task_get_pending(
+    page: int = Query(0, description="第几页"),
+    size: int = Query(10, description="分页大小"),
+    command: str = Query(..., description="任务类型"),
+    db: Session = Depends(get_db),
+):
+    """分页获取等待执行的任务"""
+    return TaskService.get_pending(db, command, page, size)
+
+@protected_router.put("/tasks/{taskId}", summary="根据taskId更新任务状态", tags=["任务管理接口"], response_model=TaskOut)
+def task_update_by_id(taskId: int, data: TaskUpdate, db: Session = Depends(get_db)):
+    """更新任务状态或结果"""
+    updated = TaskService.update(db, taskId, data)
+    if not updated:
+        raise HTTPException(status_code=404, detail=f"任务 {taskId} 不存在")
+    return updated
+
+@protected_router.post("/tg/send_message", summary="推送电报消息", tags=["消息推送接口"])
+def tg_send_message(
+    apiToken: str = Query(..., description="电报的APITOKEN"),
+    chatID: str = Query(..., description="电报群ID"),
+    message: str = Query(..., description="推送的文本信息")
+):
+    url = f"https://api.telegram.org/bot{apiToken}/sendMessage"
+    payload = {
+        "chat_id": chatID,
+        "text": message,
+        "parse_mode": "HTML"
+    }
+
+    try:
+        response = requests.post(url, json=payload, timeout=10)
+        if response.status_code != 200:
+            # logger.error(f"Telegram 推送失败: {response.text}")
+            raise HTTPException(status_code=500, detail=f"Telegram 推送失败: {response.text}")
+        return {"success": True, "detail": "Telegram 消息推送成功"}
+    except Exception as e:
+        # logger.exception("Telegram 发送消息异常")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@protected_router.post("/tg/send_image", summary="推送电报图片", tags=["消息推送接口"])
+def tg_send_image(
+    apiToken: str = Query(..., description="电报的APITOKEN"),
+    chatID: str = Query(..., description="电报群ID"),
+    message: str = Query("", description="推送的文本信息"),
+    image: UploadFile = File(..., description="推送的图像文件")
+):
+    url = f"https://api.telegram.org/bot{apiToken}/sendPhoto"
+
+    files = {"photo": (image.filename, image.file, image.content_type)}
+    data = {"chat_id": chatID, "caption": message}
+
+    try:
+        response = requests.post(url, data=data, files=files, timeout=15)
+        if response.status_code != 200:
+            # logger.error(f"Telegram 图片推送失败: {response.text}")
+            raise HTTPException(status_code=500, detail=f"Telegram 图片推送失败: {response.text}")
+        return {"success": True, "detail": "Telegram 图片推送成功"}
+    except Exception as e:
+        # logger.exception("Telegram 发送图片异常")
+        raise HTTPException(status_code=500, detail=str(e))
+
+
+@protected_router.post("/wechat/send", summary="推送微信消息", tags=["消息推送接口"])
+def wechat_send(
+    apikey: str = Query(..., description="企业微信的APITOKEN"),
+    message: str = Query(..., description="推送的文本信息")
+):
+    """
+    企业微信 WebHook 格式:
+    https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY
+    """
+    url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={apikey}"
+    payload = {"msgtype": "text", "text": {"content": message}}
+
+    try:
+        response = requests.post(url, json=payload, timeout=10)
+        data = response.json()
+
+        if response.status_code != 200 or data.get("errcode") != 0:
+            # logger.error(f"企业微信推送失败: {response.text}")
+            raise HTTPException(status_code=500, detail=f"企业微信推送失败: {response.text}")
+
+        return {"success": True, "detail": "企业微信消息推送成功"}
+    except Exception as e:
+        # logger.exception("企业微信推送异常")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@protected_router.post("/cards/publish", summary="创建新的消息卡片", tags=["信息卡片接口"], response_model=CardOut)
+def cards_publish(
+    data: CardCreate = Body(...),
+    db: Session = Depends(get_db)
+):
+    return CardService.create(db, data)
+
+
+@public_router.get("/cards/view", summary="分页读取全部卡片, 可选择语言", tags=["信息卡片接口"], response_model=List[CardOut])
+def cards_view_paginated(
+    page: int = Query(0, description="第几页"),
+    size: int = Query(10, description="分页大小"),
+    culture: str = Query("english", description="语言, 可设置 chinese, english"),
+    db: Session = Depends(get_db)
+):
+    return CardService.get_paginated(db, page, size, culture)
+
+
+@public_router.get("/cards/view2", summary="根据关键词分页查询卡片, 可选择语言", tags=["信息卡片接口"], response_model=List[CardOut])
+def cards_view_paginated2(
+    keywords: str = Query("", description="查询的关键词,多个关键词用逗号隔开"),
+    page: int = Query(0, description="第几页"),
+    size: int = Query(10, description="分页大小"),
+    culture: str = Query("english", description="语言, 可设置 chinese, english"),
+    db: Session = Depends(get_db)
+):
+    keyword_list = [k.strip() for k in keywords.split(",") if k.strip()]
+    return CardService.get_by_keywords(db, keyword_list, page, size, culture)
+
+@protected_router.post("/autobooking", summary="创建自动预定订单", tags=["自动预定订单管理接口"], response_model=AutoBookingOut)
+def autobooking_create(data: AutoBookingCreate, db: Session = Depends(get_db)):
+    return AutoBookingService.create(db, data)
+
+
+@protected_router.post("/autobooking/create-by-ai", summary="用自然语言创建自动预定订单(底层使用chatgpt)", tags=["自动预定订单管理接口"])
+def autobooking_create_by_ai():
+    # TODO: 这里可以对接 GPT 解析自然语言生成结构化 AutoBooking 数据
+    return {"message": "AI 自动创建暂未实现"}
+
+
+@protected_router.post("/autobooking/batch", summary="批量查询多个自动预定订单信息", tags=["自动预定订单管理接口"])
+def autobooking_get_by_ids(ids: List[int] = Body(...), db: Session = Depends(get_db)):
+    return AutoBookingService.batch_get_by_ids(db, ids)
+
+
+@protected_router.get("/autobooking", summary="分页查询所有的自动预定订单信息", tags=["自动预定订单管理接口"], response_model=List[AutoBookingOut])
+def autobooking_get_paginated(
+    tech_provider: str = Query("", description="签证网站技术提供商"),
+    keyword: str = Query("", description="关键词查询"),
+    page: int = Query(0, description="第几页"),
+    size: int = Query(10, description="分页大小"),
+    db: Session = Depends(get_db)
+):
+    return AutoBookingService.get_paginated(db, tech_provider, keyword, page, size)
+
+
+@protected_router.get("/autobooking/{id:int}", summary="根据id查询自动预定订单详情", tags=["自动预定订单管理接口"], response_model=AutoBookingOut)
+def autobooking_get_by_id(id: int, db: Session = Depends(get_db)):
+    result = AutoBookingService.get_by_id(db, id)
+    if not result:
+        raise HTTPException(status_code=404, detail="未找到订单")
+    return result
+
+
+@protected_router.delete("/autobooking/{id:int}", summary="根据id删除自动预定订单", tags=["自动预定订单管理接口"])
+def autobooking_delete_by_id(id: int, db: Session = Depends(get_db)):
+    ok = AutoBookingService.delete_by_id(db, id)
+    if not ok:
+        raise HTTPException(status_code=404, detail="删除失败或记录不存在")
+    return {"success": True, "id": id}
+
+
+@protected_router.put("/autobooking/{id:int}", summary="根据id更新自动预定订单信息", tags=["自动预定订单管理接口"], response_model=AutoBookingOut)
+def autobooking_update_by_id(id: int, updated_order_info: dict = Body(...), db: Session = Depends(get_db)):
+    result = AutoBookingService.update_by_id(db, id, updated_order_info)
+    if not result:
+        raise HTTPException(status_code=404, detail="更新失败或记录不存在")
+    return result
+
+
+@protected_router.get("/autobooking/statistics", summary="统计自动预定订单信息", tags=["自动预定订单管理接口"])
+def autobooking_statistics(
+    tech_provider: str = Query("", description="签证网站技术提供商"),
+    db: Session = Depends(get_db)
+):
+    return AutoBookingService.statistics(db, tech_provider)
+
+
+@protected_router.get("/autobooking/pending", summary="获取未处理的自动预定订单信息列表", tags=["自动预定订单管理接口"], response_model=List[AutoBookingOut])
+def autobooking_pending(
+    tech_provider: str = Query("", description="签证网站技术提供商"),
+    db: Session = Depends(get_db)
+):
+    return AutoBookingService.get_pending(db, tech_provider)
+
+@protected_router.get("/autobooking/trigger-finish", summary="触发自动预定订单完成操作", tags=["自动预定订单管理接口"])
+def autobooking_trigger_finish(id: int):
+    pass
+
+@protected_router.post("/stripe-price/create", summary="创建stripe 商品", tags=["Stripe 操作接口"])
+def stripe_price_create(
+    stripePrice: str = Body(..., description="stripe 商品价格信息")
+):
+    pass
+
+@protected_router.post(
+    "/slot/report",
+    summary="上报 slot 记录",
+    tags=["SLOT接口"],
+    response_model=SlotOut
+)
+def slot_report(data: SlotCreate = Body(...), db: Session = Depends(get_db)):
+    logger.info(f"[Slot Report] {data}")
+    return SlotService.report(db, data)
+
+
+@protected_router.get(
+    "/slot/search",
+    summary="查询 slot 记录",
+    tags=["SLOT接口"],
+    response_model=SlotOut
+)
+def slot_search(
+    submit_city: str = Query(..., description="提交城市"),
+    travel_country: str = Query(..., description="旅行国家"),
+    visa_type: str = Query(..., description="签证类别"),
+    date_type: str = Query("latest", description="查询方式, latest 最近一条上报的信息, earliest 最早的日期"),
+    db: Session = Depends(get_db)
+):
+    result = SlotService.search(db, submit_city, travel_country, visa_type, date_type)
+    if not result:
+        raise HTTPException(status_code=404, detail="未找到相关记录")
+    return result
+
+
+@protected_router.post(
+    "/visafly-config",
+    summary="创建一条可以被前端查询到的签证类别",
+    tags=["visafly-config接口"],
+    response_model=VisaflyConfigOut
+)
+def visafly_config_create(
+    visafly_config: VisaflyConfigCreate = Body(...),
+    db: Session = Depends(get_db)
+):
+    logger.info(f"[VisaflyConfig Create] {visa_slot_queries}")
+    return VisaflyConfigService.create(db, visa_slot_queries)
+
+
+@protected_router.get(
+    "/visafly-config/submission-countries",
+    summary="查询支持从哪些国家递交申请",
+    tags=["visafly-config接口"]
+)
+def visafly_config_get_submission_countries(db: Session = Depends(get_db)):
+    return VisaflyConfigService.get_submission_countries(db)
+
+
+@protected_router.get(
+    "/visafly-config/cities",
+    summary="查询支持从哪个国家的哪些城市递交申请",
+    tags=["visafly-config接口"]
+)
+def visafly_config_get_cities_by_country_code(
+    country_code: str = Query(..., description="递交申请的国家编号,大写的两个英文字符"),
+    db: Session = Depends(get_db)
+):
+    return VisaflyConfigService.get_cities_by_country(db, country_code)
+
+
+@protected_router.get(
+    "/visafly-config/travel-countries-with-categories",
+    summary="查询某个城市可以办理哪些国家的签证(包含签证类别)",
+    tags=["visafly-config接口"]
+)
+def visafly_config_get_travel_countries_by_city_code(
+    city_code: str = Query(..., description="递交申请的城市编号,大写的三个英文字符"),
+    db: Session = Depends(get_db)
+):
+    return VisaflyConfigService.get_travel_countries_by_city(db, city_code)

+ 29 - 4
app/core/database.py

@@ -2,14 +2,39 @@ from sqlalchemy import create_engine
 from sqlalchemy.orm import sessionmaker, declarative_base
 from sqlalchemy.orm import sessionmaker, declarative_base
 from app.core.config import settings
 from app.core.config import settings
 
 
-engine = create_engine(settings.database_url, echo=settings.debug)
-SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+# =========================
+# 数据库初始化
+# =========================
+# 建立 Engine
+engine = create_engine(
+    settings.database_url,
+    echo=settings.debug,        # 是否打印 SQL 日志
+    pool_pre_ping=True,         # 检测断开的连接
+    pool_recycle=1800,          # 连接回收时间,防止 MySQL 8 小时断开
+    future=True                 # 启用 SQLAlchemy 2.0 风格
+)
+
+# 建立 Session 工厂
+SessionLocal = sessionmaker(
+    autocommit=False,
+    autoflush=False,
+    bind=engine,
+    expire_on_commit=False
+)
+
+# ORM 基类
 Base = declarative_base()
 Base = declarative_base()
 
 
-# 依赖注入
+
+# =========================
+# 数据库依赖
+# =========================
 def get_db():
 def get_db():
+    """
+    FastAPI 的依赖注入函数,用于在请求周期内创建并关闭数据库会话。
+    """
     db = SessionLocal()
     db = SessionLocal()
     try:
     try:
         yield db
         yield db
     finally:
     finally:
-        db.close()
+        db.close()

+ 47 - 0
app/core/logger.py

@@ -0,0 +1,47 @@
+import os
+import logging
+from logging.handlers import TimedRotatingFileHandler
+
+
+# ===============================
+# 基本配置
+# ===============================
+LOG_DIR = os.path.join(os.path.dirname(__file__), "../../logs")
+os.makedirs(LOG_DIR, exist_ok=True)
+
+LOG_FILE = os.path.join(LOG_DIR, "app.log")
+
+
+# ===============================
+# 日志格式
+# ===============================
+LOG_FORMAT = "[%(asctime)s] [%(levelname)s] [%(name)s] [%(filename)s:%(lineno)d] - %(message)s"
+DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+
+# ===============================
+# 主 logger 实例
+# ===============================
+logger = logging.getLogger("app")
+logger.setLevel(logging.INFO)  # 可改为 DEBUG
+
+# 防止重复添加 handler
+if not logger.handlers:
+    # 控制台输出
+    console_handler = logging.StreamHandler()
+    console_handler.setLevel(logging.INFO)
+    console_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
+
+    # 文件输出(每天切割)
+    file_handler = TimedRotatingFileHandler(
+        LOG_FILE, when="midnight", interval=1, backupCount=7, encoding="utf-8"
+    )
+    file_handler.setLevel(logging.INFO)
+    file_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
+
+    # 添加到 logger
+    logger.addHandler(console_handler)
+    logger.addHandler(file_handler)
+
+    # 避免重复日志
+    logger.propagate = False

+ 5 - 2
app/main.py

@@ -23,13 +23,16 @@ app.add_middleware(
 # 路由注册
 # 路由注册
 # -----------------------
 # -----------------------
 # 公共路由,不鉴权
 # 公共路由,不鉴权
-app.include_router(router.public_router, prefix="/api")
+app.include_router(
+    router.public_router,
+    prefix="/api"
+)
 
 
 # 需要鉴权的路由
 # 需要鉴权的路由
 app.include_router(
 app.include_router(
     router.protected_router,
     router.protected_router,
     prefix="/api",
     prefix="/api",
-    dependencies=[Depends(verify_token)]
+    #dependencies=[Depends(verify_token)]
 )
 )
 
 
 # -----------------------
 # -----------------------

+ 46 - 0
app/models/auto_booking.py

@@ -0,0 +1,46 @@
+from sqlalchemy import Column, BigInteger, String, Integer, Text, Date, DateTime
+from app.core.database import Base
+
+class AutoBooking(Base):
+    __tablename__ = "auto_booking"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True)
+    provider = Column(String(100))
+    visa_center = Column(String(100))
+    order_no = Column(String(100))
+    social_account = Column(String(100))
+    account = Column(String(100))
+    password = Column(String(100))
+    last_name = Column(String(100))
+    first_name = Column(String(100))
+    gender = Column(String(10))
+    birthday = Column(Date)
+    email = Column(String(150))
+    alias_email = Column(String(150))
+    phone_country_code = Column(String(20))
+    phone_no = Column(String(50))
+    passport_no = Column(String(50))
+    nationality = Column(String(50))
+    passport_expiry_date = Column(Date)
+    address_line1 = Column(Text)
+    address_line2 = Column(Text)
+    state = Column(String(100))
+    city = Column(String(100))
+    postcode = Column(String(100))
+    travel_date = Column(Date)
+    cover_letter = Column(String(100))
+    passport_image_url = Column(Text)
+    selfie_image_url = Column(Text)
+    application_form_url = Column(Text)
+    priority = Column(Integer, default=0)
+    expected_submit_start = Column(Date)
+    expected_submit_end = Column(Date)
+    rules = Column(Text)
+    status = Column(Integer, default=0)
+    placeholder = Column(Integer, default=0)
+    appointment_datetime = Column(DateTime)
+    appointment_letter_url = Column(Text)
+    pnr_number = Column(String(100))
+    payment_link = Column(Text)
+    payment_help = Column(Integer, default=0)
+    note = Column(Text)

+ 17 - 0
app/models/card.py

@@ -0,0 +1,17 @@
+from sqlalchemy import Column, BigInteger, String, Text, TIMESTAMP, func
+from app.core.database import Base
+
+
+class Card(Base):
+    __tablename__ = "card"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True)
+    image = Column(String(255), nullable=False)
+    title = Column(String(255), nullable=False)
+    content = Column(Text, nullable=False)
+    label = Column(String(255), nullable=True)
+    country = Column(String(255), nullable=True)
+    additional_info = Column(String(255), nullable=True)
+    culture = Column(String(100), default="english")
+    created_at = Column(TIMESTAMP, server_default=func.now())
+    updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())

+ 14 - 0
app/models/configuration.py

@@ -0,0 +1,14 @@
+from sqlalchemy import Column, Integer, String, Text, TIMESTAMP, func
+from app.core.database import Base
+
+
+class Configuration(Base):
+    __tablename__ = "configuration"
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    config_key = Column(String(255), unique=True, nullable=False, comment="配置键")
+    config_value = Column(Text, nullable=False, comment="配置值")
+    description = Column(Text, comment="描述")
+    type = Column(String(50), comment="类型")
+    created_at = Column(TIMESTAMP, server_default=func.now(), comment="创建时间")
+    updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now(), comment="更新时间")

+ 20 - 0
app/models/email_authorizations.py

@@ -0,0 +1,20 @@
+from sqlalchemy import Column, Integer, String, TIMESTAMP, func
+from app.core.database import Base
+
+
+class EmailAuthorization(Base):
+    __tablename__ = "email_authorizations"
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    email = Column(String(255), unique=True, nullable=False, comment="邮箱地址")
+    authorization_code = Column(String(255), nullable=False, comment="授权码")
+    imap_server = Column(String(255), nullable=True, comment="IMAP服务器地址")
+    imap_port = Column(Integer, default=993, comment="IMAP端口")
+    smtp_server = Column(String(255), nullable=True, comment="SMTP服务器地址")
+    smtp_port = Column(Integer, default=465, comment="SMTP端口")
+    proxy_host = Column(String(255), nullable=True, comment="代理主机")
+    proxy_port = Column(Integer, default=0, comment="代理端口")
+    proxy_username = Column(String(255), nullable=True, comment="代理用户名")
+    proxy_password = Column(String(255), nullable=True, comment="代理密码")
+    created_at = Column(TIMESTAMP, server_default=func.now(), comment="创建时间")
+    updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now(), comment="更新时间")

+ 14 - 0
app/models/http_session.py

@@ -0,0 +1,14 @@
+from sqlalchemy import Column, String, Text, DateTime
+from app.core.database import Base
+from datetime import datetime
+
+class HttpSession(Base):
+    __tablename__ = "http_session"
+
+    session_id = Column(String(128), primary_key=True)
+    local_storage = Column(Text)
+    cookies = Column(Text)
+    user_agent = Column(String(512))
+    proxy = Column(String(256))
+    page = Column(Text)
+    create_at = Column(DateTime, nullable=False, default=datetime.utcnow)

+ 11 - 0
app/models/short_url.py

@@ -0,0 +1,11 @@
+from sqlalchemy import Column, BigInteger, String, Text, TIMESTAMP, func
+from app.core.database import Base
+
+
+class ShortUrl(Base):
+    __tablename__ = "short_url"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True)
+    short_key = Column(String(10), unique=True, nullable=False, comment="短链接Key")
+    long_url = Column(Text, nullable=False, comment="原始长链接")
+    created_at = Column(TIMESTAMP, server_default=func.now(), comment="创建时间")

+ 20 - 0
app/models/slot.py

@@ -0,0 +1,20 @@
+from sqlalchemy import Column, Integer, String, Date, DateTime, Text
+from app.core.database import Base
+from datetime import datetime
+
+class Slot(Base):
+    __tablename__ = "visa_slot"
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    submit_country = Column(String(100), nullable=False)
+    submit_city = Column(String(100))
+    travel_country = Column(String(100), nullable=False)
+    visa_type = Column(String(100), nullable=False)
+    lasted_slot_date = Column(Date)
+    available_dates = Column(Text)
+    available_times = Column(Text)
+    slot_number = Column(Integer)
+    website = Column(String(255))
+    open_mode = Column(Integer, default=0)
+    create_at = Column(DateTime, default=datetime.utcnow)
+    update_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

+ 14 - 0
app/models/task.py

@@ -0,0 +1,14 @@
+from sqlalchemy import Column, Integer, String, Text, TIMESTAMP, func, SmallInteger
+from app.core.database import Base
+
+
+class Task(Base):
+    __tablename__ = "task"
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    command = Column(String(255), nullable=False, comment="任务类型/命令")
+    args = Column(Text, nullable=False, comment="任务参数(JSON字符串)")
+    result = Column(Text, nullable=True, comment="任务执行结果(JSON字符串)")
+    status = Column(SmallInteger, default=0, comment="任务状态:0待执行 1执行中 2完成 3失败")
+    create_at = Column(TIMESTAMP, server_default=func.now(), comment="创建时间")
+    update_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now(), comment="更新时间")

+ 16 - 0
app/models/visafly_config.py

@@ -0,0 +1,16 @@
+from sqlalchemy import Column, Integer, String
+from app.core.database import Base
+
+class VisaflyConfig(Base):
+    __tablename__ = "visa_slot_queries"
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    submission_country = Column(String(100), nullable=False)
+    submission_country_code = Column(String(10), nullable=False)
+    submission_city = Column(String(100), nullable=False)
+    submission_city_code = Column(String(10), nullable=False)
+    travel_country = Column(String(100), nullable=False)
+    travel_country_code = Column(String(10), nullable=False)
+    visa_type = Column(String(100), nullable=False)
+    provider = Column(String(100), nullable=True)
+    field_requirement_type = Column(String(100), nullable=True)

+ 52 - 0
app/schemas/auto_booking.py

@@ -0,0 +1,52 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import date, datetime
+
+class AutoBookingBase(BaseModel):
+    provider: Optional[str] = None
+    visa_center: Optional[str] = None
+    order_no: Optional[str] = None
+    social_account: Optional[str] = None
+    account: Optional[str] = None
+    password: Optional[str] = None
+    last_name: Optional[str] = None
+    first_name: Optional[str] = None
+    gender: Optional[str] = None
+    birthday: Optional[date] = None
+    email: Optional[str] = None
+    alias_email: Optional[str] = None
+    phone_country_code: Optional[str] = None
+    phone_no: Optional[str] = None
+    passport_no: Optional[str] = None
+    nationality: Optional[str] = None
+    passport_expiry_date: Optional[date] = None
+    address_line1: Optional[str] = None
+    address_line2: Optional[str] = None
+    state: Optional[str] = None
+    city: Optional[str] = None
+    postcode: Optional[str] = None
+    travel_date: Optional[date] = None
+    cover_letter: Optional[str] = None
+    passport_image_url: Optional[str] = None
+    selfie_image_url: Optional[str] = None
+    application_form_url: Optional[str] = None
+    priority: Optional[int] = None
+    expected_submit_start: Optional[date] = None
+    expected_submit_end: Optional[date] = None
+    rules: Optional[str] = None
+    status: Optional[int] = None
+    placeholder: Optional[int] = None
+    appointment_datetime: Optional[datetime] = None
+    appointment_letter_url: Optional[str] = None
+    pnr_number: Optional[str] = None
+    payment_link: Optional[str] = None
+    payment_help: Optional[int] = None
+    note: Optional[str] = None
+
+class AutoBookingCreate(AutoBookingBase):
+    pass
+
+class AutoBookingOut(AutoBookingBase):
+    id: int
+    class Config:
+        orm_mode = True

+ 28 - 0
app/schemas/card.py

@@ -0,0 +1,28 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+
+
+class CardBase(BaseModel):
+    image: Optional[str] = None
+    title: Optional[str] = None
+    content: Optional[str] = None
+    label: Optional[str] = None
+    country: Optional[str] = None
+    additional_info: Optional[str] = None
+    culture: Optional[str] = "english"
+
+
+class CardCreate(CardBase):
+    image: str
+    title: str
+    content: str
+
+
+class CardOut(CardBase):
+    id: int
+    created_at: datetime
+    updated_at: datetime
+
+    class Config:
+        orm_mode = True

+ 30 - 0
app/schemas/configuration.py

@@ -0,0 +1,30 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+
+
+class ConfigurationBase(BaseModel):
+    config_key: Optional[str] = None
+    config_value: Optional[str] = None
+    description: Optional[str] = None
+    type: Optional[str] = None
+
+
+class ConfigurationCreate(ConfigurationBase):
+    config_key: str
+    config_value: str
+
+
+class ConfigurationUpdate(ConfigurationBase):
+    config_value: Optional[str] = None
+    description: Optional[str] = None
+    type: Optional[str] = None
+
+
+class ConfigurationOut(ConfigurationBase):
+    id: int
+    created_at: datetime
+    updated_at: datetime
+
+    class Config:
+        orm_mode = True

+ 38 - 0
app/schemas/email_authorizations.py

@@ -0,0 +1,38 @@
+from pydantic import BaseModel, EmailStr
+from typing import Optional
+from datetime import datetime
+
+
+# 定义请求体模型
+class EmailContent(BaseModel):
+    body: str
+
+class EmailAuthorizationBase(BaseModel):
+    email: Optional[EmailStr] = None
+    authorization_code: Optional[str] = None
+    imap_server: Optional[str] = None
+    imap_port: Optional[int] = 993
+    smtp_server: Optional[str] = None
+    smtp_port: Optional[int] = 465
+    proxy_host: Optional[str] = None
+    proxy_port: Optional[int] = 0
+    proxy_username: Optional[str] = None
+    proxy_password: Optional[str] = None
+
+
+class EmailAuthorizationCreate(EmailAuthorizationBase):
+    email: EmailStr
+    authorization_code: str
+
+
+class EmailAuthorizationUpdate(EmailAuthorizationBase):
+    pass
+
+
+class EmailAuthorizationOut(EmailAuthorizationBase):
+    id: int
+    created_at: datetime
+    updated_at: datetime
+
+    class Config:
+        orm_mode = True

+ 23 - 0
app/schemas/http_session.py

@@ -0,0 +1,23 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+
+class HttpSessionBase(BaseModel):
+    local_storage: Optional[str] = None
+    cookies: Optional[str] = None
+    user_agent: Optional[str] = None
+    proxy: Optional[str] = None
+    page: Optional[str] = None
+
+class HttpSessionCreate(HttpSessionBase):
+    session_id: str
+
+class HttpSessionUpdate(HttpSessionBase):
+    pass
+
+class HttpSessionOut(HttpSessionBase):
+    session_id: str
+    create_at: datetime
+
+    class Config:
+        orm_mode = True

+ 15 - 0
app/schemas/short_url.py

@@ -0,0 +1,15 @@
+from pydantic import BaseModel, HttpUrl
+from datetime import datetime
+
+
+class ShortUrlBase(BaseModel):
+    short_key: str
+    long_url: HttpUrl
+    created_at: datetime
+
+    class Config:
+        orm_mode = True
+
+
+class ShortUrlCreate(BaseModel):
+    longUrl: HttpUrl

+ 24 - 0
app/schemas/slot.py

@@ -0,0 +1,24 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import date
+
+class SlotBase(BaseModel):
+    submit_country: str
+    submit_city: Optional[str] = None
+    travel_country: str
+    visa_type: str
+    lasted_slot_date: Optional[date] = None
+    available_dates: Optional[str] = None
+    available_times: Optional[str] = None
+    slot_number: Optional[int] = None
+    website: Optional[str] = None
+    open_mode: Optional[int] = 0
+
+class SlotCreate(SlotBase):
+    pass
+
+class SlotOut(SlotBase):
+    id: int
+
+    class Config:
+        orm_mode = True

+ 28 - 0
app/schemas/task.py

@@ -0,0 +1,28 @@
+from pydantic import BaseModel
+from typing import Optional, Any
+from datetime import datetime
+
+
+class TaskBase(BaseModel):
+    command: str
+    args: Any
+    status: Optional[int] = 0
+
+
+class TaskCreate(TaskBase):
+    pass
+
+
+class TaskUpdate(BaseModel):
+    result: Optional[Any] = None
+    status: Optional[int] = None
+
+
+class TaskOut(TaskBase):
+    id: int
+    result: Optional[Any] = None
+    create_at: datetime
+    update_at: datetime
+
+    class Config:
+        orm_mode = True

+ 22 - 0
app/schemas/visafly_config.py

@@ -0,0 +1,22 @@
+from pydantic import BaseModel
+from typing import Optional
+
+class VisaflyConfigBase(BaseModel):
+    submission_country: str
+    submission_country_code: str
+    submission_city: str
+    submission_city_code: str
+    travel_country: str
+    travel_country_code: str
+    visa_type: str
+    provider: Optional[str] = None
+    field_requirement_type: Optional[str] = None
+
+class VisaflyConfigCreate(VisaflyConfigBase):
+    pass
+
+class VisaflyConfigOut(VisaflyConfigBase):
+    id: int
+
+    class Config:
+        orm_mode = True

+ 72 - 0
app/services/auto_booking_service.py

@@ -0,0 +1,72 @@
+from sqlalchemy.orm import Session
+from sqlalchemy import func
+from app.models.auto_booking import AutoBooking
+from app.schemas.auto_booking import AutoBookingCreate
+from typing import List
+
+class AutoBookingService:
+
+    @staticmethod
+    def create(db: Session, obj_in: AutoBookingCreate) -> AutoBooking:
+        db_obj = AutoBooking(**obj_in.dict())
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    @staticmethod
+    def get_by_id(db: Session, id: int):
+        return db.query(AutoBooking).filter(AutoBooking.id == id).first()
+
+    @staticmethod
+    def delete_by_id(db: Session, id: int):
+        obj = db.query(AutoBooking).filter(AutoBooking.id == id).first()
+        if obj:
+            db.delete(obj)
+            db.commit()
+            return True
+        return False
+
+    @staticmethod
+    def update_by_id(db: Session, id: int, updated_data: dict):
+        obj = db.query(AutoBooking).filter(AutoBooking.id == id).first()
+        if not obj:
+            return None
+        for key, value in updated_data.items():
+            setattr(obj, key, value)
+        db.commit()
+        db.refresh(obj)
+        return obj
+
+    @staticmethod
+    def get_paginated(db: Session, tech_provider: str, keyword: str, page: int, size: int):
+        query = db.query(AutoBooking)
+        if tech_provider:
+            query = query.filter(AutoBooking.provider == tech_provider)
+        if keyword:
+            like_str = f"%{keyword}%"
+            query = query.filter(
+                (AutoBooking.first_name.like(like_str)) |
+                (AutoBooking.last_name.like(like_str)) |
+                (AutoBooking.email.like(like_str)) |
+                (AutoBooking.visa_center.like(like_str))
+            )
+        return query.offset(page * size).limit(size).all()
+
+    @staticmethod
+    def batch_get_by_ids(db: Session, ids: List[int]):
+        return db.query(AutoBooking).filter(AutoBooking.id.in_(ids)).all()
+
+    @staticmethod
+    def statistics(db: Session, tech_provider: str):
+        query = db.query(AutoBooking.provider, func.count(AutoBooking.id)).group_by(AutoBooking.provider)
+        if tech_provider:
+            query = query.filter(AutoBooking.provider == tech_provider)
+        return query.all()
+
+    @staticmethod
+    def get_pending(db: Session, tech_provider: str):
+        query = db.query(AutoBooking).filter(AutoBooking.status == 0)
+        if tech_provider:
+            query = query.filter(AutoBooking.provider == tech_provider)
+        return query.all()

+ 48 - 0
app/services/card_service.py

@@ -0,0 +1,48 @@
+from sqlalchemy.orm import Session
+from sqlalchemy import text
+from typing import List, Optional
+from app.models.card import Card
+from app.schemas.card import CardCreate
+
+
+class CardService:
+    @staticmethod
+    def create(db: Session, obj_in: CardCreate) -> Card:
+        db_obj = Card(**obj_in.dict())
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    @staticmethod
+    def get_paginated(db: Session, page: int, size: int, culture: str):
+        offset = page * size
+        return (
+            db.query(Card)
+            .filter(Card.culture == culture)
+            .order_by(Card.created_at.desc())
+            .offset(offset)
+            .limit(size)
+            .all()
+        )
+
+    @staticmethod
+    def get_by_keywords(db: Session, keywords: List[str], page: int, size: int, culture: str):
+        offset = page * size
+        query = db.query(Card).filter(Card.culture == culture)
+
+        # 多关键词模糊查询
+        for kw in keywords:
+            like_str = f"%{kw.strip()}%"
+            query = query.filter(
+                (Card.title.like(like_str)) |
+                (Card.content.like(like_str)) |
+                (Card.label.like(like_str))
+            )
+
+        return (
+            query.order_by(Card.created_at.desc())
+            .offset(offset)
+            .limit(size)
+            .all()
+        )

+ 43 - 0
app/services/configuration_service.py

@@ -0,0 +1,43 @@
+from sqlalchemy.orm import Session
+from typing import List, Optional
+from app.models.configuration import Configuration
+from app.schemas.configuration import ConfigurationCreate, ConfigurationUpdate
+
+
+class ConfigurationService:
+    @staticmethod
+    def create(db: Session, config_in: ConfigurationCreate) -> Configuration:
+        db_obj = Configuration(**config_in.dict())
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    @staticmethod
+    def get_all(db: Session) -> List[Configuration]:
+        return db.query(Configuration).order_by(Configuration.id.desc()).all()
+
+    @staticmethod
+    def get_by_key(db: Session, config_key: str) -> Optional[Configuration]:
+        return db.query(Configuration).filter(Configuration.config_key == config_key).first()
+
+    @staticmethod
+    def update_by_key(db: Session, config_key: str, config_in: ConfigurationUpdate) -> Optional[Configuration]:
+        db_obj = db.query(Configuration).filter(Configuration.config_key == config_key).first()
+        if not db_obj:
+            return None
+        for field, value in config_in.dict(exclude_unset=True).items():
+            setattr(db_obj, field, value)
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    @staticmethod
+    def delete_by_key(db: Session, config_key: str) -> Optional[Configuration]:
+        db_obj = db.query(Configuration).filter(Configuration.config_key == config_key).first()
+        if not db_obj:
+            return None
+        db.delete(db_obj)
+        db.commit()
+        return db_obj

+ 630 - 0
app/services/email_authorizations_service.py

@@ -0,0 +1,630 @@
+import re
+import time
+import socket
+import smtplib
+import socks
+import imaplib
+import email
+from datetime import datetime, timedelta, timezone
+from email.message import EmailMessage
+from email.header import decode_header
+from sqlalchemy.orm import Session
+from typing import List, Optional
+from app.models.email_authorizations import EmailAuthorization
+from app.schemas.email_authorizations import EmailAuthorizationCreate, EmailAuthorizationUpdate
+
+
+class EmailAuthorizationService:
+    
+    DEFAULT_READ_TOP_N_EMAIL = 10       # 最多读取最近多少封邮件
+    RETRY_DELAY_SECONDS = 5             # 每次轮询间隔
+    
+    @staticmethod
+    def get_all(db: Session) -> List[EmailAuthorization]:
+        return db.query(EmailAuthorization).order_by(EmailAuthorization.id.desc()).all()
+
+    @staticmethod
+    def get_by_id(db: Session, id: int) -> Optional[EmailAuthorization]:
+        return db.query(EmailAuthorization).filter(EmailAuthorization.id == id).first()
+
+    @staticmethod
+    def get_by_email(db: Session, email: str) -> Optional[EmailAuthorization]:
+        return db.query(EmailAuthorization).filter(EmailAuthorization.email == email).first()
+
+    @staticmethod
+    def create(db: Session, obj_in: EmailAuthorizationCreate) -> EmailAuthorization:
+        db_obj = EmailAuthorization(**obj_in.dict(exclude_unset=True))
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    @staticmethod
+    def update(db: Session, id: int, obj_in: EmailAuthorizationUpdate) -> Optional[EmailAuthorization]:
+        db_obj = db.query(EmailAuthorization).filter(EmailAuthorization.id == id).first()
+        if not db_obj:
+            return None
+        for field, value in obj_in.dict(exclude_unset=True).items():
+            setattr(db_obj, field, value)
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    @staticmethod
+    def delete(db: Session, id: int) -> Optional[EmailAuthorization]:
+        db_obj = db.query(EmailAuthorization).filter(EmailAuthorization.id == id).first()
+        if not db_obj:
+            return None
+        db.delete(db_obj)
+        db.commit()
+        return db_obj
+    
+    @staticmethod
+    def _connect_imap_with_proxy(
+        host: str,
+        port: int,
+        proxy_host: Optional[str] = None,
+        proxy_port: Optional[int] = None,
+        proxy_user: Optional[str] = None,
+        proxy_password: Optional[str] = None,
+    ) -> imaplib.IMAP4_SSL:
+        """
+        创建支持 SOCKS5 / HTTP 代理的 IMAP SSL 连接
+        """
+        if proxy_host and proxy_port and proxy_port > 0:
+            original_socket = socket.socket
+            socks.setdefaultproxy(
+                proxy_type=socks.SOCKS5,  # 可改为 socks.HTTP
+                addr=proxy_host,
+                port=proxy_port,
+                username=proxy_user or None,
+                password=proxy_password or None,
+            )
+            socket.socket = socks.socksocket
+            try:
+                imap = imaplib.IMAP4_SSL(host, port)
+            finally:
+                socket.socket = original_socket  # 恢复原始 socket
+        else:
+            imap = imaplib.IMAP4_SSL(host, port)
+        return imap
+    
+    
+    @staticmethod
+    def _connect_smtp_with_proxy(
+        host: str,
+        port: int,
+        proxy_host: Optional[str] = None,
+        proxy_port: Optional[int] = None,
+        proxy_user: Optional[str] = None,
+        proxy_password: Optional[str] = None,
+    ) -> smtplib.SMTP_SSL:
+        """
+        创建支持 SOCKS5 / HTTP 代理的 SMTP SSL 连接
+        (逻辑完全与 _connect_imap_with_proxy 保持一致,且强制 SSL)
+        """
+        if proxy_host and proxy_port and proxy_port > 0:
+            original_socket = socket.socket
+            socks.setdefaultproxy(
+                proxy_type=socks.SOCKS5,  # 如需改 HTTP,这里改
+                addr=proxy_host,
+                port=proxy_port,
+                username=proxy_user or None,
+                password=proxy_password or None,
+            )
+            socket.socket = socks.socksocket
+            try:
+                smtp = smtplib.SMTP_SSL(host, port)
+            finally:
+                socket.socket = original_socket  # 恢复原始 socket
+        else:
+            # 无代理
+            smtp = smtplib.SMTP_SSL(host, port)
+
+        return smtp
+
+
+    @staticmethod
+    def fetch_email_authorizations(
+        auth,
+        sender: str,
+        recipient: str,
+        subject_keywords: str,
+        body_keywords: str,
+        sent_date: str,
+        expiry: int = 300,
+        only_text: bool = True
+    ) -> Optional[str]:
+        """
+        在有效期内循环读取邮箱,找到符合条件的邮件(使用最后一条 Received 头作为收件时间)
+        """
+
+        EMAIL_ACCOUNT = auth.email
+        EMAIL_PASSWORD = auth.authorization_code
+        IMAP_SERVER = auth.imap_server
+        IMAP_PORT = auth.imap_port
+
+        subject_keys = [s.strip() for s in subject_keywords.split(",") if s.strip()]
+        body_keys = [s.strip() for s in body_keywords.split(",") if s.strip()]
+
+        # === 时间计算 ===
+        sent_dt = datetime.strptime(sent_date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
+        max_wait_time = min(5 * 60, expiry)   # 最长等待5分钟
+        expiry_at = time.time() + max_wait_time
+
+        def get_received_time(msg):
+            """
+            使用最后一条 Received 头解析收件时间
+            """
+            received_headers = msg.get_all("Received", [])
+            if not received_headers:
+                return None
+            for i, header in enumerate(received_headers, 1):
+                print(f"  [{i}] {header}")
+            last_received = received_headers[-1]
+            if ";" not in last_received:
+                return None
+            time_str = last_received.split(";")[-1].strip()
+            dt_tuple = email.utils.parsedate_tz(time_str)
+            if not dt_tuple:
+                return None
+            return datetime.fromtimestamp(email.utils.mktime_tz(dt_tuple), tz=timezone.utc)
+
+        try:
+            mail = EmailAuthorizationService._connect_imap_with_proxy(
+                IMAP_SERVER,
+                IMAP_PORT,
+                auth.proxy_host,
+                auth.proxy_port,
+                auth.proxy_username,
+                auth.proxy_password,
+            )
+            mail.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
+            mail.select("INBOX")
+
+            while time.time() < expiry_at:
+                mail.noop()  # 刷新邮箱状态
+                _, data = mail.search(None, "ALL")
+                mail_ids = data[0].split()
+                if not mail_ids:
+                    time.sleep(EmailAuthorizationService.RETRY_DELAY_SECONDS)
+                    continue
+
+                recent_ids = mail_ids[-EmailAuthorizationService.DEFAULT_READ_TOP_N_EMAIL:]
+                messages = []
+                debug = True
+
+                for email_id in reversed(recent_ids):
+                    res, msg_data = mail.fetch(email_id, "(RFC822)")
+                    if res != "OK" or not msg_data:
+                        if debug:
+                            print(f"[WARN] 邮件 ID={email_id.decode()} 获取失败")
+                        continue
+
+                    msg_bytes = None
+                    for part in msg_data:
+                        if isinstance(part, tuple):
+                            msg_bytes = part[1]
+
+                    if not msg_bytes:
+                        if debug:
+                            print(f"[WARN] 邮件 ID={email_id.decode()} 无正文")
+                        continue
+
+                    msg = email.message_from_bytes(msg_bytes)
+                    received_dt = get_received_time(msg)
+                    if not received_dt:
+                        if debug:
+                            print(f"[WARN] 邮件 ID={email_id.decode()} 未解析出 Received 时间")
+                        continue
+
+                    messages.append((msg, received_dt))
+
+                if debug:
+                    print(f"[DEBUG] 成功解析邮件数: {len(messages)}")
+                    print(f"[DEBUG] 收件时间列表: {[m[1] for m in messages]}")
+
+                # 按收件时间降序排序
+                messages.sort(key=lambda x: x[1], reverse=True)
+
+                for msg, received_dt in messages:
+                    # 判断是否在发送时间后的有效窗口内
+                    if received_dt < sent_dt:
+                        if debug:
+                            print(f"[INFO] 邮件太旧: {received_dt}")
+                        continue
+                    if received_dt > sent_dt + timedelta(seconds=expiry):
+                        if debug:
+                            print(f"[INFO] 邮件太新: {received_dt}")
+                        continue
+
+                    # 匹配发件人/收件人
+                    msg_from = msg.get("From", "")
+                    msg_to = msg.get("To", "")
+                    if sender.lower() not in msg_from.lower():
+                        if debug:
+                            print("发件人不匹配")
+                        continue
+                    if recipient.lower() not in msg_to.lower():
+                        if debug:
+                            print("收件人不匹配")
+                        continue
+
+                    # 匹配主题
+                    subject, enc = decode_header(msg.get("Subject"))[0]
+                    if isinstance(subject, bytes):
+                        subject = subject.decode(enc or "utf-8", errors="ignore")
+                    if subject_keys and not any(k.lower() in subject.lower() for k in subject_keys):
+                        continue
+
+                    # 提取正文
+                    body = EmailAuthorizationService._extract_body(msg, only_text)
+                    if body_keys and not any(k.lower() in body.lower() for k in body_keys):
+                        continue
+
+                    # 找到匹配邮件 → 返回内容
+                    mail.close()
+                    mail.logout()
+                    return body.strip()
+
+                # 未匹配到 → 等待重试
+                time.sleep(EmailAuthorizationService.RETRY_DELAY_SECONDS)
+
+            mail.close()
+            mail.logout()
+            return None  # 超时未找到
+
+        except Exception as e:
+            raise e
+        
+
+    @staticmethod
+    def fetch_email_authorizations_from_top_n(
+        auth,
+        sender: str,
+        recipient: str,
+        subject_keywords: str,
+        body_keywords: str,
+        top: int = 10,
+        only_text: bool = True
+    ) -> Optional[str]:
+        """
+        在有效期内循环读取邮箱,找到符合条件的邮件(使用最后一条 Received 头作为收件时间)
+        """
+
+        EMAIL_ACCOUNT = auth.email
+        EMAIL_PASSWORD = auth.authorization_code
+        IMAP_SERVER = auth.imap_server
+        IMAP_PORT = auth.imap_port
+
+        subject_keys = [s.strip() for s in subject_keywords.split(",") if s.strip()]
+        body_keys = [s.strip() for s in body_keywords.split(",") if s.strip()]
+
+        try:
+            mail = EmailAuthorizationService._connect_imap_with_proxy(
+                IMAP_SERVER,
+                IMAP_PORT,
+                auth.proxy_host,
+                auth.proxy_port,
+                auth.proxy_username,
+                auth.proxy_password,
+            )
+            mail.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
+            mail.select("INBOX")
+
+            _, data = mail.search(None, "ALL")
+            mail_ids = data[0].split()
+            if not mail_ids:
+                return None
+
+            recent_ids = mail_ids[-top:]
+            messages = []
+            debug = True
+
+            for email_id in reversed(recent_ids):
+                res, msg_data = mail.fetch(email_id, "(RFC822)")
+                if res != "OK" or not msg_data:
+                    if debug:
+                        print(f"[WARN] 邮件 ID={email_id.decode()} 获取失败")
+                    continue
+
+                msg_bytes = None
+                for part in msg_data:
+                    if isinstance(part, tuple):
+                        msg_bytes = part[1]
+
+                if not msg_bytes:
+                    if debug:
+                        print(f"[WARN] 邮件 ID={email_id.decode()} 无正文")
+                    continue
+
+                msg = email.message_from_bytes(msg_bytes)
+                messages.append(msg)
+
+            if debug:
+                print(f"[DEBUG] 成功解析邮件数: {len(messages)}")
+
+            for msg in messages:
+          
+                # 匹配发件人/收件人
+                msg_from = msg.get("From", "")
+                msg_to = msg.get("To", "")
+                if sender.lower() not in msg_from.lower():
+                    if debug:
+                        print("发件人不匹配")
+                    continue
+                if recipient.lower() not in msg_to.lower():
+                    if debug:
+                        print("收件人不匹配")
+                    continue
+
+                # 匹配主题
+                subject, enc = decode_header(msg.get("Subject"))[0]
+                if isinstance(subject, bytes):
+                    subject = subject.decode(enc or "utf-8", errors="ignore")
+                if subject_keys and not any(k.lower() in subject.lower() for k in subject_keys):
+                    continue
+
+                # 提取正文
+                body = EmailAuthorizationService._extract_body(msg, only_text)
+                if body_keys and not any(k.lower() in body.lower() for k in body_keys):
+                    continue
+
+                # 找到匹配邮件 → 返回内容
+                mail.close()
+                mail.logout()
+                return body.strip()
+
+            mail.close()
+            mail.logout()
+            return None  # 超时未找到
+
+        except Exception as e:
+            raise e
+        
+    # ----------------------------------------------------------------------
+    # 主方法:模仿 Java forwardFirstMatchingEmail
+    # ----------------------------------------------------------------------
+    def forward_first_matching_email(
+            auth,
+            forward_to: str,
+            sender: str,
+            recipient: str,
+            subject_keywords: str,
+            body_keywords: str
+    ):
+        EMAIL_ACCOUNT = auth.email
+        EMAIL_PASSWORD = auth.authorization_code
+        IMAP_SERVER = auth.imap_server
+        IMAP_PORT = auth.imap_port
+
+        subject_keys = [s.strip() for s in subject_keywords.split(",") if s.strip()]
+        body_keys = [s.strip() for s in body_keywords.split(",") if s.strip()]
+
+        try:
+            mail = EmailAuthorizationService._connect_imap_with_proxy(
+                IMAP_SERVER,
+                IMAP_PORT,
+                auth.proxy_host,
+                auth.proxy_port,
+                auth.proxy_username,
+                auth.proxy_password,
+            )
+            mail.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
+            mail.select("INBOX")
+
+            target = recipient
+            query = f'(HEADER To "{target}")'
+            res, data = mail.uid("search", None, query)
+            if res != "OK":
+                return None
+
+            uids = data[0].split()
+
+            # 获取邮件,按 sentDate 排序
+            msgs = []
+            for uid in uids:
+                res, msg_data = mail.uid("fetch", uid, "(RFC822)")
+                if res != "OK":
+                    continue
+                msg = email.message_from_bytes(msg_data[0][1])
+                date_ = email.utils.parsedate_to_datetime(msg.get("Date"))
+                msgs.append((date_, msg))
+
+            msgs.sort(key=lambda x: x[0], reverse=True)
+
+            # -----------------------------------------------------------
+            # 搜索匹配邮件
+            # -----------------------------------------------------------
+            for _, msg in msgs:
+                # 匹配发件人/收件人
+                msg_from = msg.get("From", "")
+                msg_to = msg.get("To", "")
+                if sender.lower() not in msg_from.lower():
+                    if debug:
+                        print("发件人不匹配")
+                    continue
+                if recipient.lower() not in msg_to.lower():
+                    if debug:
+                        print("收件人不匹配")
+                    continue
+
+                # 匹配主题
+                subject, enc = decode_header(msg.get("Subject"))[0]
+                if isinstance(subject, bytes):
+                    subject = subject.decode(enc or "utf-8", errors="ignore")
+                if subject_keys and not any(k.lower() in subject.lower() for k in subject_keys):
+                    continue
+
+                # 提取正文
+                body = EmailAuthorizationService._extract_body(msg, True)
+                if body_keys and not any(k.lower() in body.lower() for k in body_keys):
+                    continue
+                
+                msg.replace_header("From", EMAIL_ACCOUNT)
+                msg.replace_header("To", forward_to)
+
+                # 可选:修改 Subject
+                subject_raw = msg.get("Subject", "")
+                msg.replace_header("Subject", f"FWD: {subject_raw}")
+
+                # 发送
+                EmailAuthorizationService.send_email_smtp(
+                    auth,
+                    msg
+                )
+
+                return f"邮件 '{subject}' 已成功转发至: {forward_to}"
+            return None
+        except Exception as e:
+            raise e
+        
+    # ----------------------------------------------------------------------
+    # 主方法:模仿 Java send_email
+    # ----------------------------------------------------------------------
+    def send_email(
+            auth,
+            send_to: str,
+            subject: str,
+            content_type: str,
+            content: str
+    ):
+        try:
+            EMAIL_ACCOUNT = auth.email
+            msg = EmailMessage()
+            msg["From"] = EMAIL_ACCOUNT
+            msg["To"] = send_to
+            msg["Subject"] = subject
+            
+            html_content = None;
+            text_content = None;
+            if ("html" == content_type.lower()):
+                html_content = content
+            else:
+                text_content = content
+
+            # 设置正文内容
+            if html_content and text_content:
+                # multipart/alternative
+                msg.set_content(text_content)
+                msg.add_alternative(html_content, subtype="html")
+            elif html_content:
+                msg.add_alternative(html_content, subtype="html")
+            elif text_content:
+                msg.set_content(text_content)
+            else:
+                msg.set_content("")  # 空邮件
+            
+            # 发送
+            EmailAuthorizationService.send_email_smtp(
+                auth,
+                msg
+            )
+
+            return f"邮件 '{subject}' 成功发送至: {send_to}"
+        except Exception as e:
+            raise e
+        
+        
+    # ----------------------------------------------------------------------
+    # 主方法:模仿 Java send_email
+    # ----------------------------------------------------------------------
+    def send_email_bulk(
+            auth,
+            send_to: str,
+            subject: str,
+            content_type: str,
+            content: str
+    ):
+        try:
+            bcc_list = [s.strip() for s in send_to.split(",") if s.strip()]
+            EMAIL_ACCOUNT = auth.email
+            msg = EmailMessage()
+            msg["From"] = EMAIL_ACCOUNT
+            # TO 可以留空或放一个默认收件人
+            msg["To"] = bcc_list[0]  # 或者固定一个自己邮箱作为 TO
+            # BCC 添加所有收件人
+            bcc_list = bcc_list
+            msg["Subject"] = subject
+            
+            html_content = None;
+            text_content = None;
+            if ("html" == content_type.lower()):
+                html_content = content
+            else:
+                text_content = content
+
+            # 设置正文内容
+            if html_content and text_content:
+                # multipart/alternative
+                msg.set_content(text_content)
+                msg.add_alternative(html_content, subtype="html")
+            elif html_content:
+                msg.add_alternative(html_content, subtype="html")
+            elif text_content:
+                msg.set_content(text_content)
+            else:
+                msg.set_content("")  # 空邮件
+            
+            # 发送
+            EmailAuthorizationService.send_email_smtp(
+                auth,
+                msg,
+                bcc_list = bcc_list
+            )
+
+            return f"邮件 '{subject}' 成功发送至: {send_to}"
+        except Exception as e:
+            raise e
+
+    # ----------------------------------------------------------------------
+    # SMTP 发送邮件
+    # ----------------------------------------------------------------------
+    @staticmethod
+    def send_email_smtp(auth, msg, bcc_list=[]):
+        EMAIL_ACCOUNT = auth.email
+        EMAIL_PASSWORD = auth.authorization_code
+        SMTP_SERVER = auth.smtp_server
+        SMTP_PORT = auth.smtp_port
+        mail = EmailAuthorizationService._connect_smtp_with_proxy(
+            SMTP_SERVER,
+            SMTP_PORT,
+            auth.proxy_host,
+            auth.proxy_port,
+            auth.proxy_username,
+            auth.proxy_password,
+        )
+        mail.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
+        if bcc_list:
+            mail.send_message(msg, to_addrs=bcc_list)
+        else:
+            mail.send_message(msg)
+
+    # ==============================================================
+    # 辅助函数:提取邮件正文
+    # ==============================================================
+    @staticmethod
+    def _extract_body(msg, only_text: bool = True) -> str:
+        """根据 only_text 参数提取邮件内容"""
+        body_parts = []
+        if msg.is_multipart():
+            for part in msg.walk():
+                ctype = part.get_content_type()
+                if only_text and ctype != "text/plain":
+                    continue
+                if not only_text and ctype not in ["text/plain", "text/html"]:
+                    continue
+                if part.get("Content-Disposition"):
+                    continue
+                charset = part.get_content_charset() or "utf-8"
+                try:
+                    text = part.get_payload(decode=True).decode(charset, errors="ignore")
+                    body_parts.append(text)
+                except Exception:
+                    continue
+        else:
+            charset = msg.get_content_charset() or "utf-8"
+            body_parts.append(msg.get_payload(decode=True).decode(charset, errors="ignore"))
+        body = "\n".join(body_parts)
+        return re.sub(r"\s+", " ", body.strip())

+ 39 - 0
app/services/http_session_service.py

@@ -0,0 +1,39 @@
+from sqlalchemy.orm import Session
+from app.models.http_session import HttpSession
+from app.schemas.http_session import HttpSessionCreate, HttpSessionUpdate
+from typing import Optional
+
+class HttpSessionService:
+
+    @staticmethod
+    def create(db: Session, data: HttpSessionCreate) -> HttpSession:
+        obj = HttpSession(**data.dict())
+        db.add(obj)
+        db.commit()
+        db.refresh(obj)
+        return obj
+
+    @staticmethod
+    def get_by_sid(db: Session, session_id: str) -> Optional[HttpSession]:
+        return db.query(HttpSession).filter(HttpSession.session_id == session_id).first()
+
+    @staticmethod
+    def delete_by_sid(db: Session, session_id: str) -> bool:
+        obj = db.query(HttpSession).filter(HttpSession.session_id == session_id).first()
+        if not obj:
+            return False
+        db.delete(obj)
+        db.commit()
+        return True
+
+    @staticmethod
+    def update_by_sid(db: Session, session_id: str, data: HttpSessionUpdate):
+        obj = db.query(HttpSession).filter(HttpSession.session_id == session_id).first()
+        if not obj:
+            return None
+        for k, v in data.dict().items():
+            if v is not None:
+                setattr(obj, k, v)
+        db.commit()
+        db.refresh(obj)
+        return obj

+ 70 - 0
app/services/seaweedfs_service.py

@@ -0,0 +1,70 @@
+import requests
+from fastapi import UploadFile
+from app.core.logger import logger
+
+
+class SeaweedFSService:
+    MASTER_URL = "http://visafly.top:9333"  # 你的 SeaweedFS master 地址
+
+    @classmethod
+    def upload(cls, file: UploadFile):
+        """上传文件到 SeaweedFS"""
+        try:
+            # 1️⃣ 获取可上传的 volume 地址
+            assign_resp = requests.get(f"{cls.MASTER_URL}/dir/assign", timeout=5)
+            assign_data = assign_resp.json()
+            fid = assign_data["fid"]
+            public_url = assign_data["publicUrl"]
+
+            # 2️⃣ 上传文件数据
+            upload_url = f"http://{public_url}/{fid}"
+            files = {"file": (file.filename, file.file, file.content_type)}
+            upload_resp = requests.post(upload_url, files=files, timeout=10)
+
+            if upload_resp.status_code == 201:
+                return {"fid": fid, "url": upload_url}
+            else:
+                logger.error(f"上传失败: {upload_resp.text}")
+                return None
+        except Exception as e:
+            logger.exception(f"SeaweedFS 上传异常, 原因={e}")
+            return None
+
+    @classmethod
+    def get(cls, fid: str):
+        """根据 fid 读取文件"""
+        try:
+            resp = requests.get(f"{cls.MASTER_URL}/dir/lookup?volumeId={fid.split(',')[0]}", timeout=5)
+            data = resp.json()
+            if not data.get("locations"):
+                return None
+
+            public_url = data["locations"][0]["publicUrl"]
+            file_url = f"http://{public_url}/{fid}"
+
+            file_resp = requests.get(file_url, timeout=10)
+            if file_resp.status_code == 200:
+                return file_resp.content, file_resp.headers.get("Content-Type", "application/octet-stream")
+            else:
+                return None
+        except Exception as e:
+            logger.exception(f"SeaweedFS 读取异常, 原因={e}")
+            return None
+
+    @classmethod
+    def delete(cls, fid: str):
+        """删除文件"""
+        try:
+            resp = requests.get(f"{cls.MASTER_URL}/dir/lookup?volumeId={fid.split(',')[0]}", timeout=5)
+            data = resp.json()
+            if not data.get("locations"):
+                return False
+
+            public_url = data["locations"][0]["publicUrl"]
+            delete_url = f"http://{public_url}/{fid}"
+
+            del_resp = requests.delete(delete_url, timeout=10)
+            return del_resp.status_code == 202
+        except Exception as e:
+            logger.exception(f"SeaweedFS 删除异常, 原因={e}")
+            return False

+ 37 - 0
app/services/short_url_service.py

@@ -0,0 +1,37 @@
+import string
+import random
+from sqlalchemy.orm import Session
+from app.models.short_url import ShortUrl
+
+
+class ShortUrlService:
+    @staticmethod
+    def generate_short_key(length: int = 8) -> str:
+        """生成随机短 Key(由字母+数字组成)"""
+        chars = string.ascii_letters + string.digits
+        return ''.join(random.choices(chars, k=length))
+
+    @staticmethod
+    def create_short_url(db: Session, long_url: str) -> ShortUrl:
+        """创建短链接"""
+        # 检查是否已经存在相同的长链接
+        existing = db.query(ShortUrl).filter(ShortUrl.long_url == long_url).first()
+        if existing:
+            return existing
+
+        # 生成唯一 short_key
+        short_key = ShortUrlService.generate_short_key()
+        while db.query(ShortUrl).filter(ShortUrl.short_key == short_key).first():
+            short_key = ShortUrlService.generate_short_key()
+
+        db_obj = ShortUrl(short_key=short_key, long_url=long_url)
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    @staticmethod
+    def get_long_url(db: Session, short_key: str) -> str:
+        """通过短 key 获取原始长链接"""
+        record = db.query(ShortUrl).filter(ShortUrl.short_key == short_key).first()
+        return record.long_url if record else ""

+ 42 - 0
app/services/slot_service.py

@@ -0,0 +1,42 @@
+from sqlalchemy.orm import Session
+from sqlalchemy import asc, desc
+from app.models.slot import Slot
+from app.schemas.slot import SlotCreate
+from typing import List
+
+class SlotService:
+
+    @staticmethod
+    def report(db: Session, obj_in: SlotCreate) -> Slot:
+        # 可以根据 submit_city + travel_country + visa_type 查重,决定是更新还是新增
+        obj = db.query(Slot).filter(
+            Slot.submit_city == obj_in.submit_city,
+            Slot.travel_country == obj_in.travel_country,
+            Slot.visa_type == obj_in.visa_type
+        ).first()
+        if obj:
+            # 更新现有记录
+            for k, v in obj_in.dict().items():
+                setattr(obj, k, v)
+        else:
+            obj = VisaSlot(**obj_in.dict())
+            db.add(obj)
+        db.commit()
+        db.refresh(obj)
+        return obj
+
+    @staticmethod
+    def search(db: Session, submit_city: str, travel_country: str, visa_type: str, date_type: str):
+        query = db.query(Slot).filter(
+            Slot.submit_city == submit_city,
+            Slot.travel_country == travel_country,
+            Slot.visa_type == visa_type
+        )
+        if date_type == "latest":
+            obj = query.order_by(desc(Slot.update_at)).first()
+            return obj
+        elif date_type == "earliest":
+            obj = query.order_by(asc(Slot.lasted_slot_date)).first()
+            return obj
+        else:
+            return query.all()

+ 51 - 0
app/services/task_service.py

@@ -0,0 +1,51 @@
+import json
+from sqlalchemy.orm import Session
+from typing import List, Optional
+from app.models.task import Task
+from app.schemas.task import TaskCreate, TaskUpdate
+
+
+class TaskService:
+    @staticmethod
+    def create(db: Session, obj_in: TaskCreate) -> Task:
+        db_obj = Task(
+            command=obj_in.command,
+            args=obj_in.args,
+            status=obj_in.status or 0,
+        )
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    @staticmethod
+    def get_by_id(db: Session, task_id: int) -> Optional[Task]:
+        return db.query(Task).filter(Task.id == task_id).first()
+
+    @staticmethod
+    def update(db: Session, task_id: int, obj_in: TaskUpdate) -> Optional[Task]:
+        db_obj = db.query(Task).filter(Task.id == task_id).first()
+        if not db_obj:
+            return None
+
+        if obj_in.result is not None:
+            db_obj.result = obj_in.result
+        if obj_in.status is not None:
+            db_obj.status = obj_in.status
+
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    @staticmethod
+    def get_pending(db: Session, command: str, page: int, size: int) -> List[Task]:
+        offset = page * size
+        return (
+            db.query(Task)
+            .filter(Task.command == command, Task.status == 0)
+            .order_by(Task.create_at.asc())
+            .offset(offset)
+            .limit(size)
+            .all()
+        )

+ 50 - 0
app/services/visafly_config_service.py

@@ -0,0 +1,50 @@
+from sqlalchemy.orm import Session
+from app.models.visafly_config import VisaflyConfig
+from app.schemas.visafly_config import VisaflyConfigCreate
+from typing import List
+
+class VisaflyConfigService:
+
+    @staticmethod
+    def create(db: Session, obj_in: VisaflyConfigCreate) -> VisaflyConfig:
+        db_obj = VisaflyConfig(**obj_in.dict())
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    @staticmethod
+    def get_submission_countries(db: Session) -> List[dict]:
+        rows = db.query(
+            VisaflyConfig.submission_country,
+            VisaflyConfig.submission_country_code
+        ).distinct().all()
+        return [{"country": r[0], "country_code": r[1]} for r in rows]
+
+    @staticmethod
+    def get_cities_by_country(db: Session, country_code: str) -> List[dict]:
+        rows = db.query(
+            VisaflyConfig.submission_city,
+            VisaflyConfig.submission_city_code
+        ).filter(VisaflyConfig.submission_country_code == country_code).distinct().all()
+        return [{"city": r[0], "city_code": r[1]} for r in rows]
+
+    @staticmethod
+    def get_travel_countries_by_city(db: Session, city_code: str) -> List[dict]:
+        rows = db.query(
+            VisaflyConfig.travel_country,
+            VisaflyConfig.travel_country_code,
+            VisaflyConfig.visa_type,
+            VisaflyConfig.provider,
+            VisaflyConfig.field_requirement_type
+        ).filter(VisaflyConfig.submission_city_code == city_code).all()
+
+        return [
+            {
+                "travel_country": r[0],
+                "travel_country_code": r[1],
+                "visa_type": r[2],
+                "provider": r[3],
+                "field_requirement_type": r[4]
+            } for r in rows
+        ]