"""R8 适配器:政企收入全链路穿透 / 拆单规避。 源明细:SrcContract / SrcContractApproval / SrcPayment 映射到:Entity(CONTRACT, CUSTOMER, ACCOUNT, ADDRESS, LEGAL_PERSON) + 关系 + 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 SrcContract, SrcContractApproval, SrcPayment from app.ingest.base import BaseAdapter, IngestResult from app.ingest.registry import register_adapter @register_adapter class ContractAdapter(BaseAdapter): """SrcContract → Entity(CONTRACT, CUSTOMER, ACCOUNT, ADDRESS, LEGAL_PERSON) + 关系。""" source_system = "CONTRACT" staging_table = "src_contract" def ingest( self, session: Session, data_version_id: uuid.UUID | None = None, batch_size: int = 1000, ) -> IngestResult: result = IngestResult() query = session.query(SrcContract) if data_version_id: query = query.filter(SrcContract.data_version_id == data_version_id) rows = query.limit(batch_size).all() for row in rows: try: # 合同实体 contract_entity = upsert_entity( session, entity_type=EntityType.CONTRACT, business_key=row.contract_no, display_name=f"合同-{row.contract_no}", attributes={ "amount": row.amount, "sign_date": str(row.sign_date) if row.sign_date else None, "approval_threshold": row.approval_threshold, "approval_level": row.approval_level, }, data_version_id=data_version_id, ) result.entities.append(contract_entity) # 客户实体 + 签约关系 cust_entity = upsert_entity( session, entity_type=EntityType.CUSTOMER, business_key=row.customer_key, display_name=row.customer_name, data_version_id=data_version_id, ) result.entities.append(cust_entity) rel = add_relationship( session, RelationshipType.SIGNED, cust_entity, contract_entity, data_version_id=data_version_id, ) result.relationships.append(rel) # 回款账户 → Entity(ACCOUNT) + 关系 PAID_BY if row.pay_account: acct_entity = upsert_entity( session, entity_type=EntityType.ACCOUNT, business_key=row.pay_account, data_version_id=data_version_id, ) result.entities.append(acct_entity) rel = add_relationship( session, RelationshipType.PAID_BY, contract_entity, acct_entity, data_version_id=data_version_id, ) result.relationships.append(rel) # 注册地址 if row.register_address: addr_entity = upsert_entity( session, entity_type=EntityType.ADDRESS, business_key=row.register_address, display_name=row.register_address, data_version_id=data_version_id, ) rel = add_relationship( session, RelationshipType.REGISTERED_AT, cust_entity, addr_entity, data_version_id=data_version_id, ) result.relationships.append(rel) # 法人 if row.legal_person: lp_entity = upsert_entity( session, entity_type=EntityType.LEGAL_PERSON, business_key=row.legal_person, display_name=row.legal_person, data_version_id=data_version_id, ) rel = add_relationship( session, RelationshipType.LEGAL_REP_OF, lp_entity, cust_entity, data_version_id=data_version_id, ) result.relationships.append(rel) result.row_count += 1 except Exception: result.error_count += 1 return result @register_adapter class ContractApprovalAdapter(BaseAdapter): """SrcContractApproval → MetricEvent(审批时序事件)。""" source_system = "CONTRACT" staging_table = "src_contract_approval" def ingest( self, session: Session, data_version_id: uuid.UUID | None = None, batch_size: int = 1000, ) -> IngestResult: result = IngestResult() query = session.query(SrcContractApproval) if data_version_id: query = query.filter(SrcContractApproval.data_version_id == data_version_id) rows = query.limit(batch_size).all() for row in rows: try: if row.approval_time: event = MetricEvent( event_time=row.approval_time, subject_type="contract", subject_key=row.contract_no, metric_name="approval_step", metric_value=float(row.approval_step), attributes={ "approver": row.approver, "result": row.approval_result, "remark": row.remark, }, 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 PaymentAdapter(BaseAdapter): """SrcPayment → MetricEvent(回款时序事件) + 关系补强。""" source_system = "FIN" staging_table = "src_payment" def ingest( self, session: Session, data_version_id: uuid.UUID | None = None, batch_size: int = 1000, ) -> IngestResult: result = IngestResult() query = session.query(SrcPayment) if data_version_id: query = query.filter(SrcPayment.data_version_id == data_version_id) rows = query.limit(batch_size).all() for row in rows: try: if row.pay_date: import datetime as dt event_time = dt.datetime.combine( row.pay_date, dt.time.min, tzinfo=dt.timezone.utc ) event = MetricEvent( event_time=event_time, subject_type="contract", subject_key=row.contract_no, metric_name="payment", metric_value=row.pay_amount, attributes={ "pay_account": row.pay_account, "pay_type": row.pay_type, "overdue_flag": row.overdue_flag, }, data_version_id=data_version_id, ) session.add(event) result.metric_events.append(event) # 强化合同→账户关系 if row.pay_account: contract_entity = upsert_entity( session, entity_type=EntityType.CONTRACT, business_key=row.contract_no, data_version_id=data_version_id, ) acct_entity = upsert_entity( session, entity_type=EntityType.ACCOUNT, business_key=row.pay_account, data_version_id=data_version_id, ) rel = add_relationship( session, RelationshipType.PAID_BY, contract_entity, acct_entity, data_version_id=data_version_id, ) result.relationships.append(rel) result.row_count += 1 except Exception: result.error_count += 1 return result