初始化项目:数据同步服务源码与文档
- src: config_loader / source_watcher / excel_parser / sync_writer / runner - 169 Excel 全量同步至 114 SQL Server + Access 生产库 - 新增 .gitignore(忽略 venv、日志、数据、含凭据的 config.yaml 等) - 新增 README.md(用途、安装、配置、运行模式、部署说明)
This commit is contained in:
97
src/runner.py
Normal file
97
src/runner.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""同步服务主入口(NSSM 部署)。
|
||||
|
||||
用法:
|
||||
python -m src.runner # 服务循环模式(NSSM)
|
||||
python -m src.runner --once # 单次运行后退出
|
||||
python -m src.runner --local X.xlsx # 用本地文件作源(跳过 169 下载,联调用)
|
||||
python -m src.runner --local X.xlsx --dry-run # 只解析并打印,不写库
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from .config_loader import load_config
|
||||
from .excel_parser import parse_excel
|
||||
from .sync_writer import sync_all, _build_insert
|
||||
from . import source_watcher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_logging(cfg):
|
||||
log_cfg = cfg.get("logging", {})
|
||||
level = getattr(logging, log_cfg.get("level", "INFO").upper(), logging.INFO)
|
||||
log_dir = Path(log_cfg.get("dir", "logs"))
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
||||
root = logging.getLogger()
|
||||
root.setLevel(level)
|
||||
fh = logging.FileHandler(log_dir / "sync.log", encoding="utf-8")
|
||||
fh.setFormatter(fmt)
|
||||
root.addHandler(fh)
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(fmt)
|
||||
root.addHandler(sh)
|
||||
|
||||
|
||||
def run_once(cfg, source_path=None, dry_run=False):
|
||||
if source_path:
|
||||
local_file = source_path
|
||||
logger.info("使用本地源: %s", local_file)
|
||||
else:
|
||||
unc = cfg["source"]["unc_path"]
|
||||
cache = cfg["source"]["local_cache_dir"]
|
||||
meta_path = os.path.join(cache, ".source_meta.json")
|
||||
if not source_watcher.has_changed(unc, meta_path):
|
||||
return
|
||||
local_file = source_watcher.download(unc, cache)
|
||||
source_watcher.save_baseline(meta_path, source_watcher.get_source_meta(unc))
|
||||
|
||||
records, fields = parse_excel(local_file, cfg.get("field_map", {}))
|
||||
|
||||
if dry_run:
|
||||
import_col = cfg.get("auto_fields", {}).get("import_time_field", "导入时间")
|
||||
s = cfg["sql_server"]
|
||||
sql_tbl = f"{s['schema']}.[{s['table']}]"
|
||||
logger.info("[dry-run] 字段数=%d 行数=%d", len(fields), len(records))
|
||||
logger.info("[dry-run] SQL INSERT: %s", _build_insert(sql_tbl, fields, import_col))
|
||||
for i, r in enumerate(records[:3]):
|
||||
logger.info("[dry-run] 样本%d: %s", i + 1, {k: r[k] for k in fields[:6]})
|
||||
return
|
||||
|
||||
sync_all(cfg, records, fields)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="采购执行情况即时数据 同步服务")
|
||||
parser.add_argument("--once", action="store_true", help="单次运行后退出")
|
||||
parser.add_argument("--local", metavar="PATH", help="用本地文件作源(跳过 169 下载)")
|
||||
parser.add_argument("--dry-run", action="store_true", help="只解析并打印,不写库")
|
||||
parser.add_argument("--config", default="config/config.yaml")
|
||||
args = parser.parse_args()
|
||||
|
||||
cfg = load_config(args.config)
|
||||
setup_logging(cfg)
|
||||
|
||||
if args.local or args.dry_run:
|
||||
run_once(cfg, source_path=args.local, dry_run=args.dry_run)
|
||||
return
|
||||
if args.once:
|
||||
run_once(cfg)
|
||||
return
|
||||
|
||||
interval = cfg["scan"]["interval_seconds"]
|
||||
logger.info("服务循环启动,扫描间隔 %s 秒", interval)
|
||||
while True:
|
||||
try:
|
||||
run_once(cfg)
|
||||
except Exception as e:
|
||||
logger.exception("同步周期失败: %s", e)
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user