142 lines
6.1 KiB
Python
142 lines
6.1 KiB
Python
|
|
"""业务接入开关与幂等排队;HTTP 不执行导入。"""
|
|||
|
|
|
|||
|
|
from uuid import NAMESPACE_URL, uuid5
|
|||
|
|
|
|||
|
|
from sqlalchemy import inspect, select
|
|||
|
|
from sqlalchemy.exc import IntegrityError
|
|||
|
|
|
|||
|
|
from app.core.exceptions import BadRequestError, ConflictError, ForbiddenError
|
|||
|
|
from app.models import TPlusBusinessSyncConfig, TPlusWorkRun, User
|
|||
|
|
from app.services.authorization_service import require_permission
|
|||
|
|
from app.services.tplus.business_sync import START, preview
|
|||
|
|
from app.services.tplus.task_service import history_source_database, new_batch_no
|
|||
|
|
from app.services.tplus.workbench import WorkbenchError, digest, work_out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def configuration(session):
|
|||
|
|
source = history_source_database(session)
|
|||
|
|
ready = inspect(session.connection()).has_table(TPlusBusinessSyncConfig.__tablename__)
|
|||
|
|
row = session.scalar(select(TPlusBusinessSyncConfig).where(TPlusBusinessSyncConfig.source_database == source)) if ready else None
|
|||
|
|
return {"enabled": bool(row and row.enabled), "start_date": (row.start_date if row else START).isoformat(),
|
|||
|
|
"version": row.version if row else 0, "editable": ready and bool(source),
|
|||
|
|
"last_error": row.last_error if row else None}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def queue(session, actor_id, start_date, request_key):
|
|||
|
|
require_permission(session, session.get(User, actor_id), "tplus:import")
|
|||
|
|
source = history_source_database(session)
|
|||
|
|
if not source:
|
|||
|
|
raise BadRequestError("请先完成 T+ 数据采集")
|
|||
|
|
key = str(request_key)
|
|||
|
|
found = session.scalar(select(TPlusWorkRun).where(TPlusWorkRun.request_key == key))
|
|||
|
|
values = {"start_date": start_date.isoformat()}
|
|||
|
|
def existing_run(found):
|
|||
|
|
if found.kind != "BUSINESS_SYNC" or found.requested_by != actor_id or found.source_database != source or found.input_json != values:
|
|||
|
|
raise BadRequestError("请求键已用于不同任务")
|
|||
|
|
return work_out(found)
|
|||
|
|
if found:
|
|||
|
|
return existing_run(found)
|
|||
|
|
run = TPlusWorkRun(run_no=new_batch_no(), request_key=key, source_database=source,
|
|||
|
|
requested_by=actor_id, kind="BUSINESS_SYNC", status="QUEUED", input_json=values)
|
|||
|
|
try:
|
|||
|
|
with session.begin_nested():
|
|||
|
|
session.add(run)
|
|||
|
|
session.flush()
|
|||
|
|
session.commit()
|
|||
|
|
except IntegrityError:
|
|||
|
|
session.rollback()
|
|||
|
|
found = session.scalar(select(TPlusWorkRun).where(TPlusWorkRun.request_key == key))
|
|||
|
|
if found is None:
|
|||
|
|
raise ConflictError() from None
|
|||
|
|
return existing_run(found)
|
|||
|
|
return work_out(run)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def save(session, payload, actor_id):
|
|||
|
|
require_permission(session, session.get(User, actor_id), "tplus:import")
|
|||
|
|
source = history_source_database(session)
|
|||
|
|
if not source:
|
|||
|
|
raise BadRequestError("请先完成 T+ 数据采集")
|
|||
|
|
row = session.scalar(select(TPlusBusinessSyncConfig).where(TPlusBusinessSyncConfig.source_database == source).with_for_update())
|
|||
|
|
if (row.version if row else 0) != payload.expected_version:
|
|||
|
|
raise ConflictError()
|
|||
|
|
if row is None:
|
|||
|
|
row = TPlusBusinessSyncConfig(source_database=source, updated_by=actor_id)
|
|||
|
|
session.add(row)
|
|||
|
|
row.enabled, row.start_date = int(payload.enabled), payload.start_date
|
|||
|
|
row.version, row.updated_by, row.last_error = payload.expected_version + 1, actor_id, None
|
|||
|
|
try:
|
|||
|
|
session.commit()
|
|||
|
|
except IntegrityError:
|
|||
|
|
session.rollback()
|
|||
|
|
raise ConflictError() from None
|
|||
|
|
if row.enabled:
|
|||
|
|
schedule(session)
|
|||
|
|
return configuration(session)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def schedule(session):
|
|||
|
|
"""仅成功快照触发;去重周期与配置版本绑定,不改变原采集结果。"""
|
|||
|
|
if not inspect(session.connection()).has_table(TPlusBusinessSyncConfig.__tablename__):
|
|||
|
|
return None
|
|||
|
|
source = history_source_database(session)
|
|||
|
|
config = session.scalar(select(TPlusBusinessSyncConfig).where(TPlusBusinessSyncConfig.source_database == source))
|
|||
|
|
if config is None or not config.enabled:
|
|||
|
|
return None
|
|||
|
|
try:
|
|||
|
|
require_permission(session, session.get(User, config.updated_by), "tplus:import")
|
|||
|
|
except ForbiddenError:
|
|||
|
|
config.last_error = "TASK_AUTHORIZATION_REVOKED"
|
|||
|
|
session.commit()
|
|||
|
|
return None
|
|||
|
|
from app.services.tplus.snapshots import source_manifest
|
|||
|
|
try:
|
|||
|
|
manifest = list(source_manifest(session, source).items())
|
|||
|
|
except WorkbenchError:
|
|||
|
|
config.last_error = "SOURCE_REFRESH_REQUIRED"
|
|||
|
|
session.commit()
|
|||
|
|
return None
|
|||
|
|
key = uuid5(NAMESPACE_URL, digest([source, config.version, manifest, "business-sync-v1"]))
|
|||
|
|
result = queue(session, config.updated_by, config.start_date, key)
|
|||
|
|
if result["status"] != "FAILED" and config.last_error is not None:
|
|||
|
|
config.last_error = None
|
|||
|
|
session.commit()
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
|
|||
|
|
def readonly_preview(session, start_date=START):
|
|||
|
|
return preview(session, history_source_database(session), start_date)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def record_schedule_failure(session, source):
|
|||
|
|
"""保留采集成功状态,同时在接入设置中提示排队异常。"""
|
|||
|
|
config = session.scalar(select(TPlusBusinessSyncConfig).where(
|
|||
|
|
TPlusBusinessSyncConfig.source_database == source,
|
|||
|
|
TPlusBusinessSyncConfig.enabled == 1,
|
|||
|
|
).with_for_update())
|
|||
|
|
if config:
|
|||
|
|
config.last_error = "BUSINESS_SYNC_SCHEDULE_FAILED"
|
|||
|
|
session.commit()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def record_authorization_failure(session, source, actor_id):
|
|||
|
|
"""仅标记仍由该授权人启用的配置,由调用方提交任务失败状态。"""
|
|||
|
|
config = session.scalar(select(TPlusBusinessSyncConfig).where(
|
|||
|
|
TPlusBusinessSyncConfig.source_database == source,
|
|||
|
|
TPlusBusinessSyncConfig.enabled == 1,
|
|||
|
|
TPlusBusinessSyncConfig.updated_by == actor_id,
|
|||
|
|
).with_for_update())
|
|||
|
|
if config:
|
|||
|
|
config.last_error = "TASK_AUTHORIZATION_REVOKED"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def clear_execution_failure(session, source, actor_id):
|
|||
|
|
"""当前授权人重试成功后清除旧错误,由调用方提交任务结果。"""
|
|||
|
|
config = session.scalar(select(TPlusBusinessSyncConfig).where(
|
|||
|
|
TPlusBusinessSyncConfig.source_database == source,
|
|||
|
|
TPlusBusinessSyncConfig.enabled == 1,
|
|||
|
|
TPlusBusinessSyncConfig.updated_by == actor_id,
|
|||
|
|
).with_for_update())
|
|||
|
|
if config:
|
|||
|
|
config.last_error = None
|