repository.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. """采集结果入库:videos / leads / comments。"""
  2. from datetime import datetime
  3. from sqlalchemy import select
  4. from sqlalchemy.orm import Session
  5. from config.settings import LEAD_STATUS_COLLECTED
  6. from db.models import Comment, Lead, Video
  7. def upsert_video(session: Session, meta: dict) -> Video:
  8. """按平台 video_id 插入或更新视频,返回 ORM 对象(含主键 id)。"""
  9. platform_video_id = (meta.get("video_id") or "").strip()
  10. if not platform_video_id:
  11. raise ValueError("video_id 为空,无法入库")
  12. now = datetime.now()
  13. video = session.scalar(select(Video).where(Video.video_id == platform_video_id))
  14. if video is None:
  15. video = Video(video_id=platform_video_id, create_at=now, update_at=now)
  16. session.add(video)
  17. else:
  18. video.update_at = now
  19. video.keyword = meta.get("keyword") or video.keyword
  20. video.title = meta.get("title") or video.title
  21. video.author_nickname = meta.get("author_nickname") or video.author_nickname
  22. video.author_douyin_id = meta.get("author_douyin_id") or video.author_douyin_id
  23. video.author_link = meta.get("author_link") or video.author_link
  24. video.video_url = meta.get("video_url") or video.video_url
  25. if meta.get("summary"):
  26. video.summary = str(meta["summary"])[:512]
  27. session.flush()
  28. return video
  29. def upsert_lead(
  30. session: Session,
  31. *,
  32. douyin_id: str,
  33. nick_name: str | None,
  34. user_link: str | None,
  35. ) -> Lead:
  36. """按抖音号插入或更新线索;已存在时不覆盖更高阶 status。"""
  37. douyin_id = douyin_id.strip()
  38. if not douyin_id:
  39. raise ValueError("douyin_id 为空,无法入库")
  40. now = datetime.now()
  41. lead = session.scalar(select(Lead).where(Lead.douyin_id == douyin_id))
  42. if lead is None:
  43. lead = Lead(
  44. douyin_id=douyin_id,
  45. nick_name=nick_name,
  46. user_link=user_link,
  47. status=LEAD_STATUS_COLLECTED,
  48. create_at=now,
  49. update_at=now,
  50. )
  51. session.add(lead)
  52. else:
  53. if nick_name:
  54. lead.nick_name = nick_name
  55. if user_link:
  56. lead.user_link = user_link
  57. if lead.status is None:
  58. lead.status = LEAD_STATUS_COLLECTED
  59. lead.update_at = now
  60. session.flush()
  61. return lead
  62. def insert_comment_if_new(
  63. session: Session,
  64. *,
  65. video_pk: int,
  66. lead_id: int | None,
  67. douyin_id: str | None,
  68. nick_name: str | None,
  69. content: str | None,
  70. comment_time: str | None,
  71. is_reply: bool = False,
  72. ) -> Comment | None:
  73. """
  74. 写入评论。
  75. comments.video_id = videos.id(主键)。
  76. 唯一性:同一视频主键 + 同一抖音号 + 同一正文。
  77. """
  78. if not video_pk:
  79. raise ValueError("video_pk 为空,评论无法入库")
  80. content_norm = (content or "").strip()
  81. douyin_norm = (douyin_id or "").strip() or None
  82. if not douyin_norm or not content_norm:
  83. return None
  84. exists = session.scalar(
  85. select(Comment.id).where(
  86. Comment.video_id == video_pk,
  87. Comment.douyin_id == douyin_norm,
  88. Comment.content == content_norm,
  89. )
  90. )
  91. if exists:
  92. return None
  93. comment = Comment(
  94. video_id=video_pk,
  95. lead_id=lead_id,
  96. douyin_id=douyin_norm,
  97. nick_name=nick_name,
  98. content=content_norm,
  99. comment_time=comment_time,
  100. type=0 if is_reply else 1,
  101. create_at=datetime.now(),
  102. )
  103. session.add(comment)
  104. session.flush()
  105. return comment
  106. def save_video_only(session: Session, video_meta: dict) -> dict:
  107. """仅 upsert 视频;返回平台 video_id 与表主键 id。"""
  108. video = upsert_video(session, video_meta)
  109. return {"video_id": video.video_id, "id": video.id}
  110. def save_comments_batch(session: Session, video_pk: int, comments: list[dict]) -> dict:
  111. """
  112. 写入线索与评论。
  113. video_pk: videos.id
  114. 用户唯一键:douyin_id
  115. 评论唯一键:videos.id + douyin_id + content
  116. """
  117. if not video_pk:
  118. raise ValueError("video_pk 为空")
  119. lead_count = 0
  120. comment_count = 0
  121. skip_count = 0
  122. no_id_count = 0
  123. for item in comments:
  124. douyin_id = (item.get("douyin_id") or "").strip()
  125. if not douyin_id:
  126. no_id_count += 1
  127. skip_count += 1
  128. continue
  129. lead = upsert_lead(
  130. session,
  131. douyin_id=douyin_id,
  132. nick_name=item.get("nick_name"),
  133. user_link=item.get("user_link"),
  134. )
  135. lead_count += 1
  136. saved = insert_comment_if_new(
  137. session,
  138. video_pk=video_pk,
  139. lead_id=lead.id,
  140. douyin_id=douyin_id,
  141. nick_name=item.get("nick_name"),
  142. content=item.get("content"),
  143. comment_time=item.get("comment_time"),
  144. is_reply=bool(item.get("is_reply")),
  145. )
  146. if saved is None:
  147. skip_count += 1
  148. else:
  149. comment_count += 1
  150. return {
  151. "video_pk": video_pk,
  152. "leads": lead_count,
  153. "comments": comment_count,
  154. "skipped_comments": skip_count,
  155. "no_douyin_id": no_id_count,
  156. }
  157. def save_collect_batch(session: Session, video_meta: dict, comments: list[dict]) -> dict:
  158. """兼容旧调用:一次事务写入视频 + 评论。"""
  159. video = upsert_video(session, video_meta)
  160. return save_comments_batch(session, video.id, comments)