Files
shanxia2.0/backend/scripts/refresh_dispatch_calendar_site.py
T

130 lines
6.4 KiB
Python

"""按租户和派车日期补派历史记录,并刷新已有自动现场记录。
这是运维脚本,使用显式租户范围和专用审计身份;不提供 HTTP 入口。
默认只读预览,--apply 才写入。历史记录仅在命中现有派发规则时启用后续同步。
在 backend 目录执行,例如:
venv/bin/python scripts/refresh_dispatch_calendar_site.py --env-file .env \
--tenant shanxia2.0 --start-date 2026-10-28 --end-date 2026-10-30
增加 --apply 正式执行;生产配置使用 --env-file .env.production。
"""
import argparse
import json
import sys
from datetime import date
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from dotenv import dotenv_values
from sqlalchemy import create_engine, or_
from sqlalchemy.orm import Session
from app.core.db_auth import DbUserContext
from app.models.pl_dispatch_calendar import PlDispatchCalendar as Record
from app.services import pl_dispatch_calendar_service as svc
SOURCE_KINDS = ('acid', 'alkali', 'other')
COPY_FIELDS = ('dispatch_date', 'vehicle_id', 'license_plate', 'product_id', 'product_name',
'origin', 'destination', 'quantity', 'remark')
def refresh(db: Session, *, tenant: str, start: date, end: date,
operator: str, apply: bool = False) -> dict:
"""一次事务内补派;不改手工现场行,不按日期/车牌猜测关联。"""
if not tenant.strip() or not operator.strip() or len(operator) > 64 or start > end:
raise ValueError('须提供租户、有效审计身份及正确日期范围')
if not date(1900, 1, 1) <= start <= end <= date(2100, 12, 31):
raise ValueError('派车日期须在 1900-01-01 至 2100-12-31 范围内')
user = DbUserContext(operator, '现场日历补派脚本', '', 'super_admin', tenant)
result = {'tenant': tenant, 'startDate': start.isoformat(), 'endDate': end.isoformat(),
'apply': apply, 'counts': {key: 0 for key in
('created', 'updated', 'restored', 'removed', 'unchanged', 'ignored', 'enabled')},
'records': []}
try:
query = db.query(Record).filter(
Record.tenant_no == tenant, Record.calendar_kind.in_(SOURCE_KINDS),
Record.dispatch_date >= start, Record.dispatch_date <= end,
or_(Record.del_flag == 0, Record.site_sync_enabled == 1),
).order_by(Record.id)
if apply:
query = query.populate_existing().with_for_update()
sources = query.all()
for source in sources:
target_query = db.query(Record).filter(
Record.tenant_no == tenant, Record.source_record_id == source.id,
)
if apply:
target_query = target_query.populate_existing().with_for_update()
target = target_query.one_or_none()
if target is not None and target.calendar_kind != 'site':
raise ValueError('来源关联到了非现场日历,停止刷新:' + source.id)
document_type = None if source.del_flag == 1 else svc._site_document_type(
source.origin, source.destination)
enable = not source.site_sync_enabled and document_type is not None
if not document_type:
action = 'removed' if target is not None and target.del_flag == 0 else 'ignored'
elif target is None:
action = 'created'
elif target.del_flag == 1:
action = 'restored'
elif target.document_type != document_type or any(
getattr(target, field) != getattr(source, field) for field in COPY_FIELDS):
action = 'updated'
else:
action = 'unchanged'
changes_target = action in ('created', 'updated', 'restored', 'removed')
# 预览与正式执行一致:异常关联须人工核对,不能静默越过同步开关。
if changes_target and not (source.site_sync_enabled or enable):
raise ValueError('自动关联存在但来源未启用同步,停止刷新:' + source.id)
result['counts'][action] += 1
result['counts']['enabled'] += int(enable)
if apply:
if enable:
source.site_sync_enabled = 1
source.update_by = operator
if changes_target:
svc._sync_site_record(db, tenant, user, source)
db.flush()
result['records'].append({'sourceId': source.id, 'action': action,
'enabled': bool(enable)})
if apply:
db.commit()
else:
db.rollback()
return result
except Exception:
db.rollback()
raise
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--env-file', required=True, help='明确指定 .env 或 .env.production')
parser.add_argument('--tenant', required=True)
parser.add_argument('--start-date', type=date.fromisoformat, required=True)
parser.add_argument('--end-date', type=date.fromisoformat, required=True)
parser.add_argument('--operator', default='calendar-site-refresh', help='写入 update_by 的运维审计身份')
parser.add_argument('--apply', action='store_true', help='提交变更,默认只读预览')
args = parser.parse_args()
# 直接读取指定文件,避免进程环境 DATABASE_URL 覆盖所选数据库。
url = dotenv_values(args.env_file).get('DATABASE_URL')
if not url:
parser.error('指定配置文件没有 DATABASE_URL')
engine = create_engine(url, pool_pre_ping=True, connect_args={
'connect_timeout': 10, 'read_timeout': 60, 'write_timeout': 60})
try:
with Session(engine, autoflush=False, expire_on_commit=False) as db:
if engine.dialect.name == 'mysql':
from sqlalchemy import text
db.execute(text('SET SESSION innodb_lock_wait_timeout = 15'))
result = refresh(db, tenant=args.tenant, start=args.start_date, end=args.end_date,
operator=args.operator, apply=args.apply)
print(json.dumps({'database': engine.url.database, 'host': engine.url.host, **result},
ensure_ascii=False, indent=2))
finally:
engine.dispose()
if __name__ == '__main__':
main()