o
    i1=                     @   s   d Z ddlmZmZmZ ddlmZmZmZ ddlZddl	m
Z
 ddlmZ ddlmZ ddlmZ dd	lmZ d
ZdZdZddhZh dZh dZG dd dZdedefddZdS )u0  增量更新器 — 三层检测策略

检测流程（按开销升序）：
  第1层：列表页日期过滤  — 只抓 window_days 内发布的文档
  第2层：source_url 存在性  — DB 查询 μs 级
  第3层：HTTP HEAD Last-Modified + content_hash 对比  — 轻量网络请求 + DB 比较
    )datetime	timedeltadate)DictListOptionalN)logger)Session)TaxDocument)CategoryProcessor)ProcessorEngine            >            >         r   c                	   @   s  e Zd ZdZd#dedefddZefdeded	efd
dZ	defde
ee  ded	ee fddZd	ee fddZd$de
e fddZdede
e d	e
ee  fddZd%dededed	efddZd&deded	efddZd%ded e
e ded	efd!d"ZdS )'IncrementalUpdateru
  
    增量更新器

    三层检测策略（开销升序）：
      1. 列表页日期过滤  — 限制只检查 window_days 内的条目，大幅减少候选数量
      2. source_url DB 查询  — 已存在则进第 3 层，不存在则标记为「新文档」
      3. HTTP HEAD Last-Modified + content_hash  — 仅对已存在文档发送 HEAD 请求
         - 若服务器返回 Last-Modified 且未变化 → 跳过
         - 若 Last-Modified 变化或缺失 → 计算远端 content_hash 进行对比
       dbrequest_timeoutc                 C   s$   || _ || _t | _t|d| _d S )N)timeout)r   r   r   category_processorr   _engine)selfr   r    r   a/lsinfo/ai/hellotax_ai/data_center/backend/app/services/tax_data_processor/incremental_updater.py__init__4   s   zIncrementalUpdater.__init__category_idwindow_daysreturnc              
      s|  | j |}|s|d| dS td| d|d  d| d |dkr1t t|d	  nd
}| ||I d
H }|d
u rH|dg g ddS td| dt	| d|sWdn| d d g }g }d}|D ]5}	|	d }
| j
ttj|
k }|d
u r||	 qh| j|
||dI d
H }|r||	 qh|d7 }qhtd| dt	| dt	| d|  ||d |||d
dS )u  
        检查单个分类的新增/变更文档。

        Args:
            category_id: 分类ID
            window_days: 只检查最近 N 天发布的文档；0 = 检查全部

        Returns:
            {
              "category_id": int,
              "category_name": str,
              "new_documents":     [{"url", "title", "date"}, ...],
              "updated_documents": [{"url", "title", "date"}, ...],
              "skipped": int,    # 未变化跳过数
              "error": str | None,
            }
        u   未知分类ID: )r!   erroru   [增量] 检查分类 u   （nameu   ），窗口=u   天r   )daysNu   获取文档列表失败)r!   r$   new_documentsupdated_documentsskippedu   [增量] 分类 u    候选文档 u    条（u   全部u	   天窗口u   ）urlr!   r   u    完成: 新增=    更新=u    跳过=)r!   category_namer'   r(   r)   r$   )r   get_category_configr   infor   utcnowr   r   _fetch_candidate_listlenr   queryr
   filter
source_urlfirstappend_is_content_changed)r   r!   r"   configcutoff
candidatesr'   r(   r)   docdoc_urldb_docchangedr   r   r   check_category_updates>   st   


z)IncrementalUpdater.check_category_updatesNcategory_idsc                    s   |du rt | jj }g }|D ]}| j||dI dH }|| qtdd |D }tdd |D }tdt	| d| d|  |S )	u6  
        检查多个分类的更新（串行，避免并发对官网造成压力）。

        Args:
            category_ids: 要检查的分类ID列表；None = 检查全部 8 类
            window_days:  增量时间窗口（天）

        Returns:
            每个分类的检查结果列表
        Nr"   c                 s        | ]}t |d g V  qdS )r'   Nr2   get.0rr   r   r   	<genexpr>       z6IncrementalUpdater.check_categories.<locals>.<genexpr>c                 s   rC   )r(   NrD   rF   r   r   r   rI      rJ   u   [增量] 全部 u    类检查完成: 新增=r,   )
listr   
CATEGORIESkeysr@   r7   sumr   r/   r2   )r   rA   r"   resultscidresult	total_newtotal_updatedr   r   r   check_categories   s"   z#IncrementalUpdater.check_categoriesc                    s   | j tdI dH S )uA   兼容旧接口 — 检查全部 8 类，使用日增量窗口。rB   N)rT   DAILY_WINDOW_DAYS)r   r   r   r   check_all_categories   s   z'IncrementalUpdater.check_all_categoriesc                 C   s\   t  }| jt}|dur|tj|k}|jd|idd | j  t	
d|  dS )u   
        更新最后检查时间（写入 TaxDocument.last_check_time）。

        Args:
            category_id: 指定分类；None = 全部
        Nlast_check_timeF)synchronize_sessionu-   [增量] 更新 last_check_time: category_id=)r   r0   r   r3   r
   r4   r!   updatecommitr   r/   )r   r!   nowr3   r   r   r   update_last_check_time   s   
z)IncrementalUpdater.update_last_check_timer:   c           	   
      s   z| j |I dH }W n  ty, } ztjd| d| dd W Y d}~dS d}~ww |du r3|S g }|D ],}|d}|sF|| q7zt|}||krT|| W q7 tyc   || Y q7w |S )u   
        获取候选文档列表，并按 cutoff 过滤。

        Returns:
            [{url, title, date, category_id}, ...] 或 None（失败）
        Nu   [增量] 获取分类 u    列表失败: T)exc_infor   )r   fetch_all_documents	Exceptionr   r$   rE   r7   _parse_date)	r   r!   r:   all_docsefilteredr<   doc_date_strdoc_dater   r   r   r1      s2   	


z(IncrementalUpdater._fetch_candidate_listr   r*   r>   c              
      sX  zPt j| jdd4 I dH 7}||I dH }|jdv r5td|j d|  	 W d  I dH  W dS |jd}W d  I dH  n1 I dH sKw   Y  W n t	yp } ztd	| d
|  d}W Y d}~nd}~ww |r|j
rz ddlm} ||}	t|	jdd|j
  }
|
dk rW dS W n	 t	y   Y nw | j||j|dI dH S )u  
        第3层：判断远端文档内容是否变化。

        策略：
          1. 发送 HTTP HEAD 请求，取 Last-Modified
          2. 与 db_doc.updated_at 对比 — 若无变化 → False（跳过）
          3. 若 Last-Modified 变化或不可用 → 下载文档计算 content_hash 对比

        Returns:
            True = 需要更新，False = 无变化
        T)r   follow_redirectsN)i  i  u   [增量] 文档已失效 (z): Fzlast-modifiedu4   [增量] HEAD 请求失败，降级为 hash 对比:     — r   )parsedate_to_datetime)tzinfo<   r+   )httpxAsyncClientr   headstatus_coder   warningheadersrE   r_   
updated_atemail.utilsrh   absreplacetotal_seconds_hash_changedcontent_hash)r   r*   r>   r!   clientresplast_modified_strrb   rh   last_modifieddiffr   r   r   r8      sB   
(
z&IncrementalUpdater._is_content_changedr   	source_idc              
      s$  ddl m}m} ddlm} ddlm} | }| j|	|j
|k }|s-|ddS zKt|j|j|j|jd}	|||	}
tdd	 | j|j	|j|k D }g }|
 2 z3 d H W }|j|vrp||j|jd
 q[6 ||g dW S  ty } z|t|dW  Y d }~S d }~ww )Nr   )
DataSourcer
   )get_adapter)get_settingsu   数据源不存在)r}   r$   )r   max_retries	delay_min	delay_maxc                 s   s    | ]}|d  V  qdS )r   Nr   rF   r   r   r   rI   3  s    
z2IncrementalUpdater.check_source.<locals>.<genexpr>)r*   title)r}   r'   r(   )app.models.tax_datar~   r
   (app.services.tax_data_processor.adaptersr   
app.configr   r   r3   r4   idr6   r   crawler_request_timeoutr   request_delay_minrequest_delay_maxsetr5   r}   alllist_all_documentsr*   r7   r   r_   str)r   r}   r"   r~   r
   r   r   settingssourceengineadapter
local_urlsr'   itemrb   r   r   r   check_source   sD   


zIncrementalUpdater.check_sourcestored_hashc              
      s   |sdS z%| j ||dI dH }|r|ds#td|  W dS |dd}W n tyH } ztd	| d
|  W Y d}~dS d}~ww |rY||krYtd|  dS dS )u   
        通过 ProcessorEngine 获取文档正文并计算 Markdown content_hash，
        与 DB 中 stored_hash 对比（保持 hash 口径一致）。

        Returns:
            True = hash 不同（内容变化），False = 相同
        Tz/tmpNsuccessu7   [增量] process_document 失败，跳过 hash 对比: Frw    u%   [增量] hash 对比异常，跳过: rg   u%   [增量] hash 变化，标记更新: )r   process_documentrE   r   ro   r_   r/   )r   r*   r   r!   rQ   new_hashrb   r   r   r   rv   E  s$   z IncrementalUpdater._hash_changed)r   )N)r   )r   )__name__
__module____qualname____doc__r	   intr    rU   r   r@   r   r   rT   rV   r\   r   r1   r   r
   boolr8   dictr   rv   r   r   r   r   r   (   s@    
_



$+$%r   date_strr#   c              	   C   sH   |   } dD ]}zt| | W   S  ty   Y qw td|  )u0   解析多种日期格式，返回 date 对象。)z%Y-%m-%dz%Y/%m/%du   %Y年%m月%d日z%Y.%m.%du   无法解析日期: )stripr   strptimer   
ValueError)r   fmtr   r   r   r`   d  s   r`   )r   r   r   r   typingr   r   r   rk   logurur   sqlalchemy.ormr	   r   r
   2app.services.tax_data_processor.category_processorr   0app.services.tax_data_processor.processor_enginer   rU   WEEKLY_WINDOW_DAYSFULL_CHECK_WINDOW_DAYSDAILY_CATEGORIESWEEKLY_CATEGORIESMEDIUM_CATEGORIESr   r   r`   r   r   r   r   <module>   s&      >