初始化项目:数据同步服务源码与文档

- 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:
Misaka_Company
2026-07-15 14:14:39 +08:00
commit 5ae7acffde
9 changed files with 557 additions and 0 deletions

97
src/runner.py Normal file
View 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()