# AI_CHANGELOG # - 2026-03-20 | Prompt: H2 FDW→直连ETL统一改造 | FDW 外部表(fdw_etl.*)改为直连 ETL 库 # 查询 app.v_* RLS 视图。原因:postgres_fdw 不传递 GUC 参数,RLS 门店隔离失效。 # 使用 fdw_queries._fdw_context() 上下文管理器统一管理 ETL 连接。 # -*- coding: utf-8 -*- """ 人员匹配服务 —— 根据申请信息在 ETL 库 RLS 视图中查找候选匹配。 职责: - find_candidates():根据 site_id + phone(+ employee_number)在助教表和员工表中查找匹配 直连 ETL 库查询 app.v_* RLS 视图,通过 _fdw_context 设置 site_id 实现门店隔离。 ETL 库连接失败时优雅降级返回空列表。 """ from __future__ import annotations import logging from app.services.fdw_queries import _fdw_context logger = logging.getLogger(__name__) async def find_candidates( site_id: int | None, phone: str, employee_number: str | None = None, ) -> list[dict]: """ 在助教表和员工表中查找匹配候选。 查询逻辑: 1. 若 site_id 为 None,跳过匹配,返回空列表 2. 设置 app.current_site_id 进行 RLS 隔离(直连 ETL 库) 3. app.v_dim_assistant: WHERE mobile = phone 4. app.v_dim_staff JOIN app.v_dim_staff_ex: WHERE mobile = phone OR job_num = employee_number 5. 合并结果返回统一候选列表 参数: site_id: 门店 ID(None 时跳过匹配) phone: 手机号 employee_number: 员工编号(可选,用于 job_num 匹配) 返回: [{"source_type": "assistant"|"staff", "id": int, "name": str, "mobile": str|None, "job_num": str|None}] """ # site_id 为空时直接返回空列表(需求 5.6) if site_id is None: return [] candidates: list[dict] = [] # CHANGE 2026-03-20 | H2 FDW→直连ETL | 从业务库 fdw_etl.* 改为直连 ETL 库 app.v_* # intent: 修复 RLS 门店隔离失效(postgres_fdw 不传递 GUC 参数) # assumptions: _fdw_context 内部管理 ETL 连接生命周期,无需外部 conn try: with _fdw_context(None, site_id) as cur: # 1. 查询助教匹配 candidates.extend(_query_assistants(cur, phone)) # 2. 查询员工匹配 candidates.extend(_query_staff(cur, phone, employee_number)) except Exception: logger.warning( "ETL 人员匹配查询失败 (site_id=%s, phone=%s),返回空列表", site_id, phone, exc_info=True, ) return [] return candidates def _query_assistants(cur, phone: str) -> list[dict]: """查询 app.v_dim_assistant 中按 mobile 匹配的助教记录(直连 ETL 库)。""" try: # CHANGE 2026-03-20 | H2 | fdw_etl.v_dim_assistant → app.v_dim_assistant # 列名映射: scd2_is_current 是 integer 类型(1=当前),不是 boolean cur.execute( """ SELECT assistant_id, real_name, mobile FROM app.v_dim_assistant WHERE mobile = %s AND scd2_is_current = 1 """, (phone,), ) rows = cur.fetchall() except Exception: logger.warning( "查询 app.v_dim_assistant 失败,跳过助教匹配", exc_info=True, ) return [] return [ { "source_type": "assistant", "id": row[0], "name": row[1] or "", "mobile": row[2], "job_num": None, } for row in rows ] def _query_staff( cur, phone: str, employee_number: str | None ) -> list[dict]: """ 查询 app.v_dim_staff JOIN app.v_dim_staff_ex(直连 ETL 库) 按 mobile 或 job_num 匹配的员工记录。 """ try: # CHANGE 2026-03-20 | H2 | fdw_etl.v_dim_staff/v_dim_staff_ex → app.v_dim_staff/v_dim_staff_ex # 列名映射: scd2_is_current 是 integer 类型(1=当前),不是 boolean if employee_number: cur.execute( """ SELECT s.staff_id, s.staff_name, s.mobile, ex.job_num FROM app.v_dim_staff s LEFT JOIN app.v_dim_staff_ex ex ON s.staff_id = ex.staff_id AND ex.scd2_is_current = 1 WHERE s.scd2_is_current = 1 AND (s.mobile = %s OR ex.job_num = %s) """, (phone, employee_number), ) else: cur.execute( """ SELECT s.staff_id, s.staff_name, s.mobile, ex.job_num FROM app.v_dim_staff s LEFT JOIN app.v_dim_staff_ex ex ON s.staff_id = ex.staff_id AND ex.scd2_is_current = 1 WHERE s.scd2_is_current = 1 AND s.mobile = %s """, (phone,), ) rows = cur.fetchall() except Exception: logger.warning( "查询 app.v_dim_staff 失败,跳过员工匹配", exc_info=True, ) return [] return [ { "source_type": "staff", "id": row[0], "name": row[1] or "", "mobile": row[2], "job_num": row[3], } for row in rows ]