router.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. import time
  2. import requests
  3. from typing import List
  4. from app.core.logger import logger
  5. from fastapi import APIRouter, Query, Depends, Body, UploadFile, File, HTTPException
  6. from fastapi.responses import RedirectResponse
  7. from sqlalchemy.orm import Session
  8. from app.core.redis import get_redis_client
  9. from app.core.database import get_db
  10. from redis.asyncio import Redis
  11. from app.schemas.user import UserOut
  12. from app.schemas.troov import TroovRate
  13. from app.schemas.sms import ShortMessageDetail
  14. from app.schemas.configuration import ConfigurationCreate, ConfigurationUpdate, ConfigurationOut
  15. from app.schemas.email_authorizations import EmailContent, EmailAuthorizationCreate, EmailAuthorizationUpdate, EmailAuthorizationOut
  16. from app.schemas.card import CardCreate, CardOut
  17. from app.schemas.task import TaskCreate, TaskOut, TaskUpdate
  18. from app.schemas.short_url import ShortUrlCreate
  19. from app.schemas.auto_booking import AutoBookingCreate, AutoBookingOut
  20. from app.schemas.http_session import HttpSessionCreate, HttpSessionUpdate,HttpSessionOut
  21. from app.schemas.visafly_config import VisaflyConfigCreate, VisaflyConfigOut
  22. from app.schemas.slot import SlotCreate, SlotOut
  23. from app.services.configuration_service import ConfigurationService
  24. from app.services.troov_service import get_rate_by_date
  25. from app.services.sms_service import save_short_message, query_short_message
  26. from app.services.email_authorizations_service import EmailAuthorizationService
  27. from app.services.short_url_service import ShortUrlService
  28. from app.services.task_service import TaskService
  29. from app.services.card_service import CardService
  30. from app.services.seaweedfs_service import SeaweedFSService
  31. from app.services.auto_booking_service import AutoBookingService
  32. from app.services.http_session_service import HttpSessionService
  33. from app.services.visafly_config_service import VisaflyConfigService
  34. from app.services.slot_service import SlotService
  35. # 公共路由
  36. public_router = APIRouter()
  37. # 受保护路由
  38. protected_router = APIRouter()
  39. @public_router.get("/ping", summary="心跳检测", tags=["测试接口"])
  40. def ping():
  41. return {"message": "pong"}
  42. @protected_router.get("/users", summary="查询用户", tags=["通用接口"], response_model=List[UserOut])
  43. def get_users():
  44. return [
  45. {"id": 1, "name": "Alice"},
  46. {"id": 2, "name": "Bob"}
  47. ]
  48. @public_router.get("/sms/upload", summary="上报短信", tags=["短信接口"], response_model=ShortMessageDetail)
  49. def sms_upload(
  50. phone: str = Query(..., description="手机号"),
  51. message: str = Query(..., description="短信内容"),
  52. max_ttl: int = Query(300, description="短信保存时间(秒)"),
  53. redis_client: Redis = Depends(get_redis_client)
  54. ):
  55. """
  56. 保存短信到 Redis
  57. """
  58. received_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
  59. msg = save_short_message(redis_client, phone, message, received_at, max_ttl)
  60. return msg
  61. @protected_router.get("/sms/download", summary="读取短信", tags=["短信接口"], response_model=List[ShortMessageDetail])
  62. def sms_download(
  63. phone: str = Query(..., description="手机号"),
  64. keyword: str = Query('', description="短信内容关键字"),
  65. sent_at: str = Query('', description="筛选时间(可选)"),
  66. redis_client: Redis = Depends(get_redis_client)
  67. ):
  68. """
  69. 查询短信(支持关键字和时间过滤)
  70. """
  71. results = query_short_message(redis_client, phone, keyword or None, sent_at or None)
  72. return results
  73. @protected_router.get("/troov/rate", summary="TROOV 查询rate", tags=["通用接口"], response_model=List[TroovRate])
  74. def troov_rate(date: str = Query(..., description="查询的日期, 格式: YYYY-MM-DD"),
  75. redis_client: Redis = Depends(get_redis_client)):
  76. # 调用 service 层获取数据
  77. return get_rate_by_date(redis_client, date)
  78. @protected_router.post("/dynamic-configurations", summary="创建动态配置", tags=["动态配置"], response_model=ConfigurationOut)
  79. def dynamic_config_create(config_in: ConfigurationCreate, db: Session = Depends(get_db)):
  80. existing = ConfigurationService.get_by_key(db, config_in.config_key)
  81. if existing:
  82. raise HTTPException(status_code=400, detail=f"配置 Key '{config_in.config_key}' 已存在")
  83. return ConfigurationService.create(db, config_in)
  84. @protected_router.get("/dynamic-configurations/all", summary="读取所有动态配置", tags=["动态配置"], response_model=List[ConfigurationOut])
  85. def dynamic_config_get_all(db: Session = Depends(get_db)):
  86. return ConfigurationService.get_all(db)
  87. @protected_router.get("/dynamic-configurations/key/{config_key}", summary="根据Key读取动态配置", tags=["动态配置"], response_model=ConfigurationOut)
  88. def dynamic_config_get_by_key(config_key: str, db: Session = Depends(get_db)):
  89. config = ConfigurationService.get_by_key(db, config_key)
  90. if not config:
  91. raise HTTPException(status_code=404, detail=f"配置 Key '{config_key}' 不存在")
  92. return config
  93. @protected_router.put("/dynamic-configurations/key/{config_key}", summary="根据Key更新动态配置", tags=["动态配置"], response_model=ConfigurationOut)
  94. def dynamic_config_update_by_key(config_key: str, config_in: ConfigurationUpdate, db: Session = Depends(get_db)):
  95. config = ConfigurationService.update_by_key(db, config_key, config_in)
  96. if not config:
  97. raise HTTPException(status_code=404, detail=f"配置 Key '{config_key}' 不存在")
  98. return config
  99. @protected_router.delete("/dynamic-configurations/key/{config_key}", summary="根据Key删除动态配置", tags=["动态配置"], response_model=ConfigurationOut)
  100. def dynamic_config_delete_by_key(config_key: str, db: Session = Depends(get_db)):
  101. config = ConfigurationService.delete_by_key(db, config_key)
  102. if not config:
  103. raise HTTPException(status_code=404, detail=f"配置 Key '{config_key}' 不存在")
  104. return config
  105. @protected_router.post(
  106. "/http-session",
  107. summary="创建http session",
  108. tags=["会话管理"],
  109. response_model=HttpSessionOut
  110. )
  111. def http_session_create(
  112. data: HttpSessionCreate,
  113. db: Session = Depends(get_db)
  114. ):
  115. logger.info(f"[Create HttpSession] sid={data.session_id}")
  116. return HttpSessionService.create(db, data)
  117. @protected_router.delete(
  118. "/http-session",
  119. summary="删除http session",
  120. tags=["会话管理"]
  121. )
  122. def http_session_delete_by_sid(
  123. session_id: str = Query(...),
  124. db: Session = Depends(get_db)
  125. ):
  126. logger.info(f"[Delete HttpSession] sid={session_id}")
  127. ok = HttpSessionService.delete_by_sid(db, session_id)
  128. if not ok:
  129. raise HTTPException(status_code=404, detail="session 不存在")
  130. return {"success": True, "session_id": session_id}
  131. @protected_router.put(
  132. "/http-session",
  133. summary="更新http session",
  134. tags=["会话管理"],
  135. response_model=HttpSessionOut
  136. )
  137. def http_session_update_by_sid(
  138. session_id: str = Query(...),
  139. data: HttpSessionUpdate = Body(...),
  140. db: Session = Depends(get_db)
  141. ):
  142. logger.info(f"[Update HttpSession] sid={session_id}")
  143. obj = HttpSessionService.update_by_sid(db, session_id, data)
  144. if not obj:
  145. raise HTTPException(status_code=404, detail="session 不存在")
  146. return obj
  147. @protected_router.get(
  148. "/http-session",
  149. summary="读取http session",
  150. tags=["会话管理"],
  151. response_model=HttpSessionOut
  152. )
  153. def http_session_get_by_sid(
  154. session_id: str = Query(...),
  155. db: Session = Depends(get_db)
  156. ):
  157. logger.info(f"[Get HttpSession] sid={session_id}")
  158. obj = HttpSessionService.get_by_sid(db, session_id)
  159. if not obj:
  160. raise HTTPException(status_code=404, detail="session 不存在")
  161. return obj
  162. @protected_router.get("/email-authorizations", summary="查询所有内部邮箱", tags=["邮箱接口"], response_model=List[EmailAuthorizationOut])
  163. def email_authorizations_get(db: Session = Depends(get_db)):
  164. return EmailAuthorizationService.get_all(db)
  165. @protected_router.post("/email-authorizations", summary="创建内部邮箱", tags=["邮箱接口"], response_model=EmailAuthorizationOut)
  166. def email_authorizations_create(data: EmailAuthorizationCreate, db: Session = Depends(get_db)):
  167. existing = EmailAuthorizationService.get_by_email(db, data.email)
  168. if existing:
  169. raise HTTPException(status_code=400, detail=f"邮箱 {data.email} 已存在")
  170. return EmailAuthorizationService.create(db, data)
  171. @protected_router.get("/email-authorizations/{id}", summary="通过id查询内部邮箱", tags=["邮箱接口"], response_model=EmailAuthorizationOut)
  172. def email_authorizations_get_by_id(id: int, db: Session = Depends(get_db)):
  173. email_auth = EmailAuthorizationService.get_by_id(db, id)
  174. if not email_auth:
  175. raise HTTPException(status_code=404, detail=f"ID={id} 的邮箱记录不存在")
  176. return email_auth
  177. @protected_router.put("/email-authorizations/{id}", summary="通过id更新内部邮箱", tags=["邮箱接口"], response_model=EmailAuthorizationOut)
  178. def email_authorizations_update_by_id(id: int, data: EmailAuthorizationUpdate, db: Session = Depends(get_db)):
  179. updated = EmailAuthorizationService.update(db, id, data)
  180. if not updated:
  181. raise HTTPException(status_code=404, detail=f"ID={id} 的邮箱记录不存在")
  182. return updated
  183. @protected_router.delete("/email-authorizations/{id}", summary="通过id删除内部邮箱", tags=["邮箱接口"], response_model=EmailAuthorizationOut)
  184. def email_authorizations_delete_by_id(id: int, db: Session = Depends(get_db)):
  185. deleted = EmailAuthorizationService.delete(db, id)
  186. if not deleted:
  187. raise HTTPException(status_code=404, detail=f"ID={id} 的邮箱记录不存在")
  188. return deleted
  189. @protected_router.get("/email-authorizations/email/{email}", summary="通过邮箱地址查询内部邮箱", tags=["邮箱接口"], response_model=EmailAuthorizationOut)
  190. def email_authorizations_get_by_email(email: str, db: Session = Depends(get_db)):
  191. email_auth = EmailAuthorizationService.get_by_email(db, email)
  192. if not email_auth:
  193. raise HTTPException(status_code=404, detail=f"邮箱 {email} 不存在")
  194. return email_auth
  195. @protected_router.post("/email-authorizations/fetch", summary="读取邮件, 仅限文本内容", tags=["邮箱接口"])
  196. def email_authorizations_fetch_email(
  197. email: str = Query(..., description="收件邮箱账号, 格式: xxx@xxx.xxx"),
  198. sender: str = Query(..., description="发件人邮箱账号或者名字"),
  199. recipient: str = Query(..., description="收件人账号或者名字"),
  200. subjectKeywords: str = Query("", description="邮件主题关键词, 支持多个关键词, 用逗号隔开"),
  201. bodyKeywords: str = Query("", description="邮件内容关键词, 支持多个关键词, 用逗号隔开"),
  202. sentDate: str = Query(..., description="发件日期, UTC时间, 格式: yyyy-mm-dd hh:mm:ss"),
  203. expiry: int = Query(300, description="邮件有效期, 单位秒, 从sentDate 开始算起"),
  204. db: Session = Depends(get_db)
  205. ):
  206. auth = EmailAuthorizationService.get_by_email(db, email)
  207. if not auth:
  208. raise HTTPException(status_code=404, detail=f"未找到邮箱授权记录: {email}")
  209. result = EmailAuthorizationService.fetch_email_authorizations(
  210. auth,
  211. sender=sender,
  212. recipient=recipient,
  213. subject_keywords=subjectKeywords,
  214. body_keywords=bodyKeywords,
  215. sent_date=sentDate,
  216. expiry=expiry,
  217. only_text=True
  218. )
  219. if result is None:
  220. raise HTTPException(status_code=404, detail="在有效期内未找到匹配邮件")
  221. return {"body": result}
  222. @protected_router.post("/email-authorizations/fetch-top", summary="从最近的几封邮件读取目标邮件,仅限文本内容, 性能会更好, 邮件多时有可能漏读", tags=["邮箱接口"])
  223. def email_authorizations_fetch_email_from_topn(
  224. email: str = Query(..., description="收件邮箱账号, 格式: xxx@xxx.xxx"),
  225. sender: str = Query(..., description="发件人邮箱账号或者名字"),
  226. recipient: str = Query(..., description="收件人账号或者名字"),
  227. subjectKeywords: str = Query("", description="邮件主题关键词, 支持多个关键词, 用逗号隔开"),
  228. bodyKeywords: str = Query("", description="邮件内容关键词, 支持多个关键词, 用逗号隔开"),
  229. top: int = Query(10, description="指定从最近几封邮件读取"),
  230. db: Session = Depends(get_db)
  231. ):
  232. auth = EmailAuthorizationService.get_by_email(db, email)
  233. if not auth:
  234. raise HTTPException(status_code=404, detail=f"未找到邮箱授权记录: {email}")
  235. result = EmailAuthorizationService.fetch_email_authorizations_from_top_n(
  236. auth,
  237. sender=sender,
  238. recipient=recipient,
  239. subject_keywords=subjectKeywords,
  240. body_keywords=bodyKeywords,
  241. top=top,
  242. only_text=True
  243. )
  244. if result is None:
  245. raise HTTPException(status_code=404, detail=f"未在前{top}封邮件中查找到匹配邮件")
  246. return {"body": result}
  247. @protected_router.post("/email-authorizations/forward", summary="转发邮件", tags=["邮箱接口"])
  248. def email_authorizations_forward_email(
  249. emailAccount: str = Query(..., description="收件邮箱账号, 格式: xxx@xxx.xxx"),
  250. forwardTo: str = Query(..., description="转发到哪个邮箱地址, 格式: xxx@xxx.xxx"),
  251. sender: str = Query(..., description="发件人邮箱账号或者名字"),
  252. recipient: str = Query(..., description="收件人账号或者名字"),
  253. subjectKeywords: str = Query("", description="邮件主题关键词, 支持多个关键词, 用逗号隔开"),
  254. bodyKeywords: str = Query("", description="邮件内容关键词, 支持多个关键词, 用逗号隔开"),
  255. db: Session = Depends(get_db)
  256. ):
  257. auth = EmailAuthorizationService.get_by_email(db, emailAccount)
  258. if not auth:
  259. raise HTTPException(status_code=404, detail=f"未找到邮箱授权记录: {emailAccount}")
  260. result = EmailAuthorizationService.forward_first_matching_email(
  261. auth,
  262. forward_to = forwardTo,
  263. sender = sender,
  264. recipient = recipient,
  265. subject_keywords = subjectKeywords,
  266. body_keywords = bodyKeywords
  267. )
  268. if result is None:
  269. raise HTTPException(status_code=404, detail=f"未找可转发的邮件")
  270. return {"body": result}
  271. @protected_router.post("/email-authorizations/sendmail", summary="发送邮件", tags=["邮箱接口"])
  272. def email_authorizations_send_email(
  273. emailAccount: str = Query(..., description="收件邮箱账号, 格式: xxx@xxx.xxx"),
  274. sendTo: str = Query(..., description="收件人邮箱账号"),
  275. subject: str = Query(..., description="邮件主题"),
  276. contentType: str = Query("text", description="内容格式,支持 text 和 html"),
  277. content: EmailContent = Body("", description="邮件内容"),
  278. db: Session = Depends(get_db)
  279. ):
  280. auth = EmailAuthorizationService.get_by_email(db, emailAccount)
  281. if not auth:
  282. raise HTTPException(status_code=404, detail=f"未找到邮箱授权记录: {emailAccount}")
  283. result = EmailAuthorizationService.send_email(
  284. auth,
  285. send_to = sendTo,
  286. subject = subject,
  287. content_type = contentType,
  288. content = content.body
  289. )
  290. if result is None:
  291. raise HTTPException(status_code=404, detail=f"邮件发送失败")
  292. return {"body": result}
  293. @protected_router.post("/email-authorizations/sendmail-bulk", summary="群发送邮件", tags=["邮箱接口"])
  294. def email_authorizations_send_email_bulk(
  295. emailAccount: str = Query(..., description="收件邮箱账号, 格式: xxx@xxx.xxx"),
  296. sendTo: str = Query(..., description="收件人邮箱账号,多个用逗号隔开"),
  297. subject: str = Query(..., description="邮件主题"),
  298. contentType: str = Query("text", description="内容格式,支持 text 和 html"),
  299. content: EmailContent = Body(..., description="邮件内容"),
  300. db: Session = Depends(get_db)
  301. ):
  302. auth = EmailAuthorizationService.get_by_email(db, emailAccount)
  303. if not auth:
  304. raise HTTPException(status_code=404, detail=f"未找到邮箱授权记录: {emailAccount}")
  305. result = EmailAuthorizationService.send_email_bulk(
  306. auth,
  307. send_to = sendTo,
  308. subject = subject,
  309. content_type = contentType,
  310. content = content.body
  311. )
  312. if result is None:
  313. raise HTTPException(status_code=404, detail=f"邮件发送失败")
  314. return {"body": result}
  315. @protected_router.post("/resource/pdf", summary="上传pdf文件", tags=["文件管理"])
  316. def resource_upload_pdf(pdf: UploadFile = File(...)):
  317. if not pdf.filename.lower().endswith(".pdf"):
  318. raise HTTPException(status_code=400, detail="仅支持上传 PDF 文件")
  319. result = SeaweedFSService.upload(pdf)
  320. if not result:
  321. raise HTTPException(status_code=500, detail="上传失败")
  322. return {"success": True, "fid": result["fid"], "url": result["url"]}
  323. @protected_router.get("/resource/pdf/{fid}", summary="读取pdf", tags=["文件管理"])
  324. def resource_get_pdf(fid: str):
  325. data = SeaweedFSService.get(fid)
  326. if not data:
  327. raise HTTPException(status_code=404, detail="文件不存在")
  328. content, mime = data
  329. return Response(content=content, media_type=mime)
  330. @protected_router.delete("/resource/pdf/{fid}", summary="删除pdf文件", tags=["文件管理"])
  331. def resource_delete_pdf(fid: str):
  332. ok = SeaweedFSService.delete(fid)
  333. if not ok:
  334. raise HTTPException(status_code=404, detail="文件不存在或删除失败")
  335. return {"success": True, "fid": fid}
  336. @protected_router.post("/resource/image", summary="上传图片", tags=["文件管理"])
  337. def resource_upload_image(image: UploadFile = File(...)):
  338. if not image.content_type.startswith("image/"):
  339. raise HTTPException(status_code=400, detail="仅支持上传图片文件")
  340. print('upload')
  341. result = SeaweedFSService.upload(image)
  342. if not result:
  343. raise HTTPException(status_code=500, detail="上传失败")
  344. return {"success": True, "fid": result["fid"], "url": result["url"]}
  345. @protected_router.get("/resource/image/{fid}", summary="读取图片", tags=["文件管理"])
  346. def resource_get_image(fid: str):
  347. data = SeaweedFSService.get(fid)
  348. if not data:
  349. raise HTTPException(status_code=404, detail="图片不存在")
  350. content, mime = data
  351. return Response(content=content, media_type=mime)
  352. @protected_router.delete("/resource/image/{fid}", summary="删除图片", tags=["文件管理"])
  353. def resource_delete_image(fid: str):
  354. ok = SeaweedFSService.delete(fid)
  355. if not ok:
  356. raise HTTPException(status_code=404, detail="图片不存在或删除失败")
  357. return {"success": True, "fid": fid}
  358. @protected_router.post("/s/generate", summary="生成短链接地址<压缩地址长度>", tags=["Short URL"])
  359. def short_url_generate(
  360. data: ShortUrlCreate,
  361. db: Session = Depends(get_db),
  362. ):
  363. """生成短链接"""
  364. record = ShortUrlService.create_short_url(db, data.longUrl)
  365. return {
  366. "short_key": record.short_key,
  367. "short_url": f"/s/{record.short_key}",
  368. "long_url": record.long_url,
  369. "created_at": record.created_at,
  370. }
  371. @public_router.get("/s/{shortKey}", summary="访问短链接地址<自动重定向到真实链接地址>", tags=["Short URL"])
  372. def short_url_request(shortKey: str, db: Session = Depends(get_db)):
  373. """访问短链接自动重定向"""
  374. long_url = ShortUrlService.get_long_url(db, shortKey)
  375. if not long_url:
  376. raise HTTPException(status_code=404, detail="短链接不存在或已失效")
  377. return RedirectResponse(url=long_url, status_code=302)
  378. @protected_router.post("/tasks", summary="创建任务", tags=["任务管理接口"], response_model=TaskOut)
  379. def task_create(data: TaskCreate, db: Session = Depends(get_db)):
  380. """创建任务"""
  381. return TaskService.create(db, data)
  382. @protected_router.get("/tasks/{taskId:int}", summary="根据taskId读取任务状态", tags=["任务管理接口"], response_model=TaskOut)
  383. def task_get_by_id(taskId: int, db: Session = Depends(get_db)):
  384. """获取任务"""
  385. task = TaskService.get_by_id(db, taskId)
  386. if not task:
  387. raise HTTPException(status_code=404, detail=f"任务 {taskId} 不存在")
  388. return task
  389. @protected_router.get("/tasks/pending", summary="获取等待执行的任务", tags=["任务管理接口"], response_model=List[TaskOut])
  390. def task_get_pending(
  391. page: int = Query(0, description="第几页"),
  392. size: int = Query(10, description="分页大小"),
  393. command: str = Query(..., description="任务类型"),
  394. db: Session = Depends(get_db),
  395. ):
  396. """分页获取等待执行的任务"""
  397. return TaskService.get_pending(db, command, page, size)
  398. @protected_router.put("/tasks/{taskId}", summary="根据taskId更新任务状态", tags=["任务管理接口"], response_model=TaskOut)
  399. def task_update_by_id(taskId: int, data: TaskUpdate, db: Session = Depends(get_db)):
  400. """更新任务状态或结果"""
  401. updated = TaskService.update(db, taskId, data)
  402. if not updated:
  403. raise HTTPException(status_code=404, detail=f"任务 {taskId} 不存在")
  404. return updated
  405. @protected_router.post("/tg/send_message", summary="推送电报消息", tags=["消息推送接口"])
  406. def tg_send_message(
  407. apiToken: str = Query(..., description="电报的APITOKEN"),
  408. chatID: str = Query(..., description="电报群ID"),
  409. message: str = Query(..., description="推送的文本信息")
  410. ):
  411. url = f"https://api.telegram.org/bot{apiToken}/sendMessage"
  412. payload = {
  413. "chat_id": chatID,
  414. "text": message,
  415. "parse_mode": "HTML"
  416. }
  417. try:
  418. response = requests.post(url, json=payload, timeout=10)
  419. if response.status_code != 200:
  420. # logger.error(f"Telegram 推送失败: {response.text}")
  421. raise HTTPException(status_code=500, detail=f"Telegram 推送失败: {response.text}")
  422. return {"success": True, "detail": "Telegram 消息推送成功"}
  423. except Exception as e:
  424. # logger.exception("Telegram 发送消息异常")
  425. raise HTTPException(status_code=500, detail=str(e))
  426. @protected_router.post("/tg/send_image", summary="推送电报图片", tags=["消息推送接口"])
  427. def tg_send_image(
  428. apiToken: str = Query(..., description="电报的APITOKEN"),
  429. chatID: str = Query(..., description="电报群ID"),
  430. message: str = Query("", description="推送的文本信息"),
  431. image: UploadFile = File(..., description="推送的图像文件")
  432. ):
  433. url = f"https://api.telegram.org/bot{apiToken}/sendPhoto"
  434. files = {"photo": (image.filename, image.file, image.content_type)}
  435. data = {"chat_id": chatID, "caption": message}
  436. try:
  437. response = requests.post(url, data=data, files=files, timeout=15)
  438. if response.status_code != 200:
  439. # logger.error(f"Telegram 图片推送失败: {response.text}")
  440. raise HTTPException(status_code=500, detail=f"Telegram 图片推送失败: {response.text}")
  441. return {"success": True, "detail": "Telegram 图片推送成功"}
  442. except Exception as e:
  443. # logger.exception("Telegram 发送图片异常")
  444. raise HTTPException(status_code=500, detail=str(e))
  445. @protected_router.post("/wechat/send", summary="推送微信消息", tags=["消息推送接口"])
  446. def wechat_send(
  447. apikey: str = Query(..., description="企业微信的APITOKEN"),
  448. message: str = Query(..., description="推送的文本信息")
  449. ):
  450. """
  451. 企业微信 WebHook 格式:
  452. https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY
  453. """
  454. url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={apikey}"
  455. payload = {"msgtype": "text", "text": {"content": message}}
  456. try:
  457. response = requests.post(url, json=payload, timeout=10)
  458. data = response.json()
  459. if response.status_code != 200 or data.get("errcode") != 0:
  460. # logger.error(f"企业微信推送失败: {response.text}")
  461. raise HTTPException(status_code=500, detail=f"企业微信推送失败: {response.text}")
  462. return {"success": True, "detail": "企业微信消息推送成功"}
  463. except Exception as e:
  464. # logger.exception("企业微信推送异常")
  465. raise HTTPException(status_code=500, detail=str(e))
  466. @protected_router.post("/cards/publish", summary="创建新的消息卡片", tags=["信息卡片接口"], response_model=CardOut)
  467. def cards_publish(
  468. data: CardCreate = Body(...),
  469. db: Session = Depends(get_db)
  470. ):
  471. return CardService.create(db, data)
  472. @public_router.get("/cards/view", summary="分页读取全部卡片, 可选择语言", tags=["信息卡片接口"], response_model=List[CardOut])
  473. def cards_view_paginated(
  474. page: int = Query(0, description="第几页"),
  475. size: int = Query(10, description="分页大小"),
  476. culture: str = Query("english", description="语言, 可设置 chinese, english"),
  477. db: Session = Depends(get_db)
  478. ):
  479. return CardService.get_paginated(db, page, size, culture)
  480. @public_router.get("/cards/view2", summary="根据关键词分页查询卡片, 可选择语言", tags=["信息卡片接口"], response_model=List[CardOut])
  481. def cards_view_paginated2(
  482. keywords: str = Query("", description="查询的关键词,多个关键词用逗号隔开"),
  483. page: int = Query(0, description="第几页"),
  484. size: int = Query(10, description="分页大小"),
  485. culture: str = Query("english", description="语言, 可设置 chinese, english"),
  486. db: Session = Depends(get_db)
  487. ):
  488. keyword_list = [k.strip() for k in keywords.split(",") if k.strip()]
  489. return CardService.get_by_keywords(db, keyword_list, page, size, culture)
  490. @protected_router.post("/autobooking", summary="创建自动预定订单", tags=["自动预定订单管理接口"], response_model=AutoBookingOut)
  491. def autobooking_create(data: AutoBookingCreate, db: Session = Depends(get_db)):
  492. return AutoBookingService.create(db, data)
  493. @protected_router.post("/autobooking/create-by-ai", summary="用自然语言创建自动预定订单(底层使用chatgpt)", tags=["自动预定订单管理接口"])
  494. def autobooking_create_by_ai():
  495. # TODO: 这里可以对接 GPT 解析自然语言生成结构化 AutoBooking 数据
  496. return {"message": "AI 自动创建暂未实现"}
  497. @protected_router.post("/autobooking/batch", summary="批量查询多个自动预定订单信息", tags=["自动预定订单管理接口"])
  498. def autobooking_get_by_ids(ids: List[int] = Body(...), db: Session = Depends(get_db)):
  499. return AutoBookingService.batch_get_by_ids(db, ids)
  500. @protected_router.get("/autobooking", summary="分页查询所有的自动预定订单信息", tags=["自动预定订单管理接口"], response_model=List[AutoBookingOut])
  501. def autobooking_get_paginated(
  502. tech_provider: str = Query("", description="签证网站技术提供商"),
  503. keyword: str = Query("", description="关键词查询"),
  504. page: int = Query(0, description="第几页"),
  505. size: int = Query(10, description="分页大小"),
  506. db: Session = Depends(get_db)
  507. ):
  508. return AutoBookingService.get_paginated(db, tech_provider, keyword, page, size)
  509. @protected_router.get("/autobooking/{id:int}", summary="根据id查询自动预定订单详情", tags=["自动预定订单管理接口"], response_model=AutoBookingOut)
  510. def autobooking_get_by_id(id: int, db: Session = Depends(get_db)):
  511. result = AutoBookingService.get_by_id(db, id)
  512. if not result:
  513. raise HTTPException(status_code=404, detail="未找到订单")
  514. return result
  515. @protected_router.delete("/autobooking/{id:int}", summary="根据id删除自动预定订单", tags=["自动预定订单管理接口"])
  516. def autobooking_delete_by_id(id: int, db: Session = Depends(get_db)):
  517. ok = AutoBookingService.delete_by_id(db, id)
  518. if not ok:
  519. raise HTTPException(status_code=404, detail="删除失败或记录不存在")
  520. return {"success": True, "id": id}
  521. @protected_router.put("/autobooking/{id:int}", summary="根据id更新自动预定订单信息", tags=["自动预定订单管理接口"], response_model=AutoBookingOut)
  522. def autobooking_update_by_id(id: int, updated_order_info: dict = Body(...), db: Session = Depends(get_db)):
  523. result = AutoBookingService.update_by_id(db, id, updated_order_info)
  524. if not result:
  525. raise HTTPException(status_code=404, detail="更新失败或记录不存在")
  526. return result
  527. @protected_router.get("/autobooking/statistics", summary="统计自动预定订单信息", tags=["自动预定订单管理接口"])
  528. def autobooking_statistics(
  529. tech_provider: str = Query("", description="签证网站技术提供商"),
  530. db: Session = Depends(get_db)
  531. ):
  532. return AutoBookingService.statistics(db, tech_provider)
  533. @protected_router.get("/autobooking/pending", summary="获取未处理的自动预定订单信息列表", tags=["自动预定订单管理接口"], response_model=List[AutoBookingOut])
  534. def autobooking_pending(
  535. tech_provider: str = Query("", description="签证网站技术提供商"),
  536. db: Session = Depends(get_db)
  537. ):
  538. return AutoBookingService.get_pending(db, tech_provider)
  539. @protected_router.get("/autobooking/trigger-finish", summary="触发自动预定订单完成操作", tags=["自动预定订单管理接口"])
  540. def autobooking_trigger_finish(id: int):
  541. pass
  542. @protected_router.post("/stripe-price/create", summary="创建stripe 商品", tags=["Stripe 操作接口"])
  543. def stripe_price_create(
  544. stripePrice: str = Body(..., description="stripe 商品价格信息")
  545. ):
  546. pass
  547. @protected_router.post(
  548. "/slot/report",
  549. summary="上报 slot 记录",
  550. tags=["SLOT接口"],
  551. response_model=SlotOut
  552. )
  553. def slot_report(data: SlotCreate = Body(...), db: Session = Depends(get_db)):
  554. logger.info(f"[Slot Report] {data}")
  555. return SlotService.report(db, data)
  556. @protected_router.get(
  557. "/slot/search",
  558. summary="查询 slot 记录",
  559. tags=["SLOT接口"],
  560. response_model=SlotOut
  561. )
  562. def slot_search(
  563. submit_city: str = Query(..., description="提交城市"),
  564. travel_country: str = Query(..., description="旅行国家"),
  565. visa_type: str = Query(..., description="签证类别"),
  566. date_type: str = Query("latest", description="查询方式, latest 最近一条上报的信息, earliest 最早的日期"),
  567. db: Session = Depends(get_db)
  568. ):
  569. result = SlotService.search(db, submit_city, travel_country, visa_type, date_type)
  570. if not result:
  571. raise HTTPException(status_code=404, detail="未找到相关记录")
  572. return result
  573. @protected_router.post(
  574. "/visafly-config",
  575. summary="创建一条可以被前端查询到的签证类别",
  576. tags=["visafly-config接口"],
  577. response_model=VisaflyConfigOut
  578. )
  579. def visafly_config_create(
  580. visafly_config: VisaflyConfigCreate = Body(...),
  581. db: Session = Depends(get_db)
  582. ):
  583. logger.info(f"[VisaflyConfig Create] {visa_slot_queries}")
  584. return VisaflyConfigService.create(db, visa_slot_queries)
  585. @protected_router.get(
  586. "/visafly-config/submission-countries",
  587. summary="查询支持从哪些国家递交申请",
  588. tags=["visafly-config接口"]
  589. )
  590. def visafly_config_get_submission_countries(db: Session = Depends(get_db)):
  591. return VisaflyConfigService.get_submission_countries(db)
  592. @protected_router.get(
  593. "/visafly-config/cities",
  594. summary="查询支持从哪个国家的哪些城市递交申请",
  595. tags=["visafly-config接口"]
  596. )
  597. def visafly_config_get_cities_by_country_code(
  598. country_code: str = Query(..., description="递交申请的国家编号,大写的两个英文字符"),
  599. db: Session = Depends(get_db)
  600. ):
  601. return VisaflyConfigService.get_cities_by_country(db, country_code)
  602. @protected_router.get(
  603. "/visafly-config/travel-countries-with-categories",
  604. summary="查询某个城市可以办理哪些国家的签证(包含签证类别)",
  605. tags=["visafly-config接口"]
  606. )
  607. def visafly_config_get_travel_countries_by_city_code(
  608. city_code: str = Query(..., description="递交申请的城市编号,大写的三个英文字符"),
  609. db: Session = Depends(get_db)
  610. ):
  611. return VisaflyConfigService.get_travel_countries_by_city(db, city_code)