"""R15 适配器:员工内部舞弊与资源滥用。 源明细:SrcEmployeeOperation / SrcInternalMsisdn / SrcPointsTransaction 映射到:Entity(EMPLOYEE, MSISDN) + 关系(OPERATES) + MetricEvent """ from __future__ import annotations import uuid from sqlalchemy.orm import Session from app.datahub.graph_repo import add_relationship, upsert_entity from app.datahub.models import MetricEvent from app.datahub.ontology import EntityType, RelationshipType from app.datahub.staging import ( SrcEmployeeOperation, SrcInternalMsisdn, SrcPointsTransaction, ) from app.ingest.base import BaseAdapter, IngestResult from app.ingest.registry import register_adapter @register_adapter class EmployeeOperationAdapter(BaseAdapter): """SrcEmployeeOperation → Entity(EMPLOYEE) + 关系(OPERATES) + MetricEvent。""" source_system = "BSS" staging_table = "src_employee_operation" def ingest( self, session: Session, data_version_id: uuid.UUID | None = None, batch_size: int = 1000, ) -> IngestResult: result = IngestResult() query = session.query(SrcEmployeeOperation) if data_version_id: query = query.filter(SrcEmployeeOperation.data_version_id == data_version_id) rows = query.limit(batch_size).all() for row in rows: try: # 员工实体 emp_entity = upsert_entity( session, entity_type=EntityType.EMPLOYEE, business_key=row.employee_key, display_name=row.employee_name, attributes={ "position": row.position, "department": row.department, "role_permissions": row.role_permissions, }, data_version_id=data_version_id, ) result.entities.append(emp_entity) # 操作目标 → OPERATES 关系(如操作对象是号码或账户) if row.operation_target: # 尝试识别操作目标类型(简单启发式:以1开头长度11为号码,否则为账户) target_key = row.operation_target.strip() if target_key.isdigit() and len(target_key) == 11: target_entity = upsert_entity( session, entity_type=EntityType.MSISDN, business_key=target_key, data_version_id=data_version_id, ) rel = add_relationship( session, RelationshipType.OPERATES, emp_entity, target_entity, attributes={"operation_type": row.operation_type}, data_version_id=data_version_id, ) result.relationships.append(rel) # 操作日志事件 if row.operation_time: event = MetricEvent( event_time=row.operation_time, subject_type="employee", subject_key=row.employee_key, metric_name="operation_log", metric_value=1.0, attributes={ "operation_type": row.operation_type, "operation_target": row.operation_target, "position": row.position, "department": row.department, }, data_version_id=data_version_id, ) session.add(event) result.metric_events.append(event) result.row_count += 1 except Exception: result.error_count += 1 return result @register_adapter class InternalMsisdnAdapter(BaseAdapter): """SrcInternalMsisdn → Entity(MSISDN, EMPLOYEE) + 关系(OPERATES) + MetricEvent。""" source_system = "BSS" staging_table = "src_internal_msisdn" def ingest( self, session: Session, data_version_id: uuid.UUID | None = None, batch_size: int = 1000, ) -> IngestResult: result = IngestResult() query = session.query(SrcInternalMsisdn) if data_version_id: query = query.filter(SrcInternalMsisdn.data_version_id == data_version_id) rows = query.limit(batch_size).all() for row in rows: try: # 内部号码实体 msisdn_entity = upsert_entity( session, entity_type=EntityType.MSISDN, business_key=row.msisdn, display_name=row.msisdn, attributes={"purpose": row.purpose, "internal": True}, data_version_id=data_version_id, ) result.entities.append(msisdn_entity) # 分配员工 → OPERATES 关系 if row.assigned_employee: emp_entity = upsert_entity( session, entity_type=EntityType.EMPLOYEE, business_key=row.assigned_employee, data_version_id=data_version_id, ) rel = add_relationship( session, RelationshipType.OPERATES, emp_entity, msisdn_entity, attributes={"purpose": row.purpose}, data_version_id=data_version_id, ) result.relationships.append(rel) # 内部号用量事件 import datetime as dt try: event_time = dt.datetime.strptime( row.report_month, "%Y-%m" ).replace(tzinfo=dt.timezone.utc) if row.report_month else dt.datetime.now(dt.timezone.utc) except ValueError: event_time = dt.datetime.now(dt.timezone.utc) event = MetricEvent( event_time=event_time, subject_type="msisdn", subject_key=row.msisdn, metric_name="internal_usage", metric_value=row.traffic_mb, attributes={ "voice_min": row.voice_min, "revenue_attributed": row.revenue_attributed, "assigned_employee": row.assigned_employee, "purpose": row.purpose, }, data_version_id=data_version_id, ) session.add(event) result.metric_events.append(event) result.row_count += 1 except Exception: result.error_count += 1 return result @register_adapter class PointsTransactionAdapter(BaseAdapter): """SrcPointsTransaction → MetricEvent(积分发放/兑换时序)。""" source_system = "BSS" staging_table = "src_points_transaction" def ingest( self, session: Session, data_version_id: uuid.UUID | None = None, batch_size: int = 1000, ) -> IngestResult: result = IngestResult() query = session.query(SrcPointsTransaction) if data_version_id: query = query.filter(SrcPointsTransaction.data_version_id == data_version_id) rows = query.limit(batch_size).all() for row in rows: try: # 确保操作人实体存在 upsert_entity( session, entity_type=EntityType.EMPLOYEE, business_key=row.operator_key, data_version_id=data_version_id, ) # 积分事件 if row.transaction_time: event = MetricEvent( event_time=row.transaction_time, subject_type="employee", subject_key=row.operator_key, metric_name="points_transaction", metric_value=row.points_amount, attributes={ "transaction_no": row.transaction_no, "target_account": row.target_account, "transaction_type": row.transaction_type, "cash_value": row.cash_value, }, data_version_id=data_version_id, ) session.add(event) result.metric_events.append(event) result.row_count += 1 except Exception: result.error_count += 1 return result