166 lines
5.3 KiB
Python
166 lines
5.3 KiB
Python
# 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
|
||
]
|