Files
Neo-ZQYY/apps/backend/app/services/matching.py

166 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: 门店 IDNone 时跳过匹配)
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
]