打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。 新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。 补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
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
|