feat: 新增db/pipeline数据导入管线,适配5月数据格式

- 整合所有导入脚本到db/pipeline/目录
- config.py: 按月份动态配置数据源路径,支持4月/5月不同目录结构
- import_bill_records_may.py: 5月专用账单导入(51列全渠道订单明细→bill_records + 50列品项销售明细→dish_sales_details)
- import_salary_attendance.py: 添加--salary-file/--attendance-file参数支持动态文件路径
- import_may_data.sh: 5月一键导入脚本(12步全流程)
- run_all.py: 一键全量导入+物化视图刷新编排
- refresh_materialized_views.py: 按依赖顺序刷新38个物化视图
- verify.py/verify_import.py: 数据一致性验证
- README.md: 管线文档
This commit is contained in:
freedakgmail
2026-08-18 22:17:26 +08:00
parent 9b3a9cdb4b
commit 2fc1b96f44
14 changed files with 4096 additions and 65 deletions
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""
一键全量导入 + 物化视图刷新
用法: python3 run_all.py --month 2026-04-01 --db bill_query_test
python3 run_all.py --month 2026-04-01 --db bill_query_test --skip-import # 仅刷新物化
python3 run_all.py --month 2026-04-01 --db bill_query_test --skip-views # 仅导入
python3 run_all.py --month 2026-04-01 --db bill_query_test --only bill_records # 仅导入某类
"""
import argparse
import os
import subprocess
import sys
import time
from config import DB_CONFIG, get_data_sources, PIPELINE_DIR
def run_script(name, cmd, cwd=None):
"""运行子进程并实时输出"""
print(f"\n{'='*60}")
print(f" 执行: {name}")
print(f" 命令: {' '.join(cmd)}")
print(f"{'='*60}")
t0 = time.time()
result = subprocess.run(cmd, cwd=cwd or PIPELINE_DIR)
elapsed = time.time() - t0
status = "成功" if result.returncode == 0 else f"失败(rc={result.returncode})"
print(f"{name}: {status} ({elapsed:.1f}s)")
return result.returncode == 0
def main():
parser = argparse.ArgumentParser(description='全量数据导入与物化刷新流水线')
parser.add_argument('--month', required=True, help='报告月份 (YYYY-MM-01)')
parser.add_argument('--db', default='bill_query_test', help='目标数据库')
parser.add_argument('--skip-import', action='store_true', help='跳过数据导入,仅刷新物化视图')
parser.add_argument('--skip-views', action='store_true', help='跳过物化视图刷新')
parser.add_argument('--only', default=None,
help='仅导入指定数据集: bill_records, monthly, salary, store_location, derived')
parser.add_argument('--diagnosis-date', default=None, help='诊断日期 (默认今天)')
args = parser.parse_args()
sources = get_data_sources(args.month)
if not sources:
print(f"错误: 无法找到月份 {args.month} 的数据源")
sys.exit(1)
print(f"\n{'#'*60}")
print(f" 数据导入与物化刷新流水线")
print(f" 月份: {args.month}")
print(f" 数据库: {args.db}")
print(f" 数据源根: {sources.get('bill_records', 'N/A')}")
print(f"{'#'*60}")
py = sys.executable
results = []
# ============================================================
# Step 1: 数据导入
# ============================================================
if not args.skip_import:
target = args.only
# 1.1 薪资+考勤
if target in (None, 'salary'):
cmd = [py, 'import_salary_attendance.py', '--month', args.month, '--db', args.db]
sal_file = sources.get('salary')
att_file = sources.get('attendance')
if sal_file and os.path.isfile(sal_file):
cmd.extend(['--salary-file', sal_file])
if att_file and os.path.isfile(att_file):
cmd.extend(['--attendance-file', att_file])
ok = run_script("薪资+考勤导入", cmd)
results.append(('薪资+考勤', ok))
# 1.2 账单查询原始数据(bill_records + bill_columns
if target in (None, 'bill_records'):
bill_dir = sources.get('bill_records')
if bill_dir and os.path.exists(bill_dir):
ok = run_script("账单查询导入(bill_records)", [
py, 'import_bill_records.py',
'--month', args.month,
'--file', bill_dir,
'--db', args.db,
])
results.append(('bill_records', ok))
else:
print(f"\n 跳过 bill_records: 路径不存在 {bill_dir}")
results.append(('bill_records', False))
# 1.3 月度业务数据(库存/销售/成本/费用/中央厨房/配送)
if target in (None, 'monthly'):
monthly_datasets = [
('inventory', 'inventory', None),
('dish_sales', 'dish_sales', None),
('dish_cost', 'dish_cost', None),
('operating_expense', 'operating_expense', None),
('central_kitchen_finished', 'central_kitchen', 'finished'),
('central_kitchen_recipe', 'central_kitchen', 'recipe'),
('central_kitchen_material', 'central_kitchen', 'material'),
('central_kitchen_processing', 'central_kitchen', 'processing'),
('distribution', 'distribution', None),
('distribution_2', 'distribution', None),
]
for src_key, dataset, ck_type in monthly_datasets:
filepath = sources.get(src_key)
if not filepath or not os.path.exists(filepath):
print(f"\n 跳过 {dataset}/{ck_type or ''}: 文件不存在 {filepath}")
results.append((f'{dataset}/{ck_type or src_key}', False))
continue
cmd = [
py, 'import_monthly_data.py',
'--dataset', dataset,
'--month', args.month,
'--file', filepath,
'--db', args.db,
]
if ck_type:
cmd.extend(['--ck-type', ck_type])
label = f"月度数据-{dataset}" + (f"-{ck_type}" if ck_type else f"({os.path.basename(filepath)})")
ok = run_script(label, cmd)
results.append((label, ok))
# 1.4 门店位置与映射表
if target in (None, 'store_location'):
store_file = sources.get('store_location')
if store_file and os.path.exists(store_file):
ok = run_script("门店位置与映射表导入", [
py, 'import_store_location.py',
'--file', store_file,
'--db', args.db,
])
results.append(('门店位置', ok))
else:
print(f"\n 跳过门店位置: 文件不存在 {store_file}")
results.append(('门店位置', False))
# 1.5 派生分析数据
if target in (None, 'derived'):
diag_date = args.diagnosis_date or ''
cmd = [
py, 'import_derived_data.py',
'--month', args.month,
'--db', args.db,
]
if diag_date:
cmd.extend(['--diagnosis-date', diag_date])
ok = run_script("派生分析数据生成", cmd)
results.append(('派生数据', ok))
# ============================================================
# Step 2: 物化视图刷新
# ============================================================
if not args.skip_views:
ok = run_script("物化视图刷新", [
py, 'refresh_materialized_views.py',
'--db', args.db,
], cwd=PIPELINE_DIR)
results.append(('物化视图', ok))
# ============================================================
# Step 3: 验证
# ============================================================
ok = run_script("数据一致性验证", [
py, 'verify.py',
'--db', args.db,
], cwd=PIPELINE_DIR)
results.append(('验证', ok))
# ============================================================
# 汇总
# ============================================================
print(f"\n{'#'*60}")
print(f" 流水线执行汇总")
print(f"{'#'*60}")
all_ok = True
for name, ok in results:
status = "✓ 成功" if ok else "✗ 失败"
print(f" {status} {name}")
if not ok:
all_ok = False
if all_ok:
print(f"\n 全部完成!")
sys.exit(0)
else:
print(f"\n 部分失败,请检查上方日志")
sys.exit(1)
if __name__ == '__main__':
main()