Files
SBrainCO/db/pipeline/config.py
T
freedakgmail 2fc1b96f44 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: 管线文档
2026-08-18 22:17:26 +08:00

265 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
数据导入流水线配置
统一管理数据源路径、数据库连接参数、导入脚本路径
"""
import os
import glob
import sys
# ============================================================
# 路径常量
# ============================================================
PIPELINE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_DIR = os.path.dirname(PIPELINE_DIR)
BASE_DIR = os.path.dirname(DB_DIR)
# 数据源根目录(西部马华数据分析 下有完整的业务数据)
DATA_ROOT = os.path.join(os.path.dirname(BASE_DIR), '西部马华数据分析')
# SBrainCO3 数据目录(部分文件只在这里有)
DATA_SBRAIN = os.path.join(BASE_DIR, '数据')
# ============================================================
# 数据库配置
# ============================================================
DB_CONFIG = {
'host': 'localhost',
'port': 5432,
'user': 'freedak',
'password': '',
}
# ============================================================
# 月份配置
# ============================================================
MONTH_CONFIG = {
'2026-04-01': {
'month_dir': '4月',
'date_range': '2026-04-01--2026-04-30',
'dist_half1': '2026年4月1-15日',
'dist_half2': '2026年4月16-30日',
'inventory_file': '4.13盘点倒挤成本报表2026年4月明细.xlsx',
'dish_cost_file': '菜品成本分析报表.xlsx',
'expense_file': '2026年4月营业费用分析.xls',
'ck_subdir': '中央厨房4月',
'salary_file': '薪资拆分明细表_脱敏.xlsx',
'attendance_file': '考勤数据表-脱敏.xlsx',
'store_info_file': '各店信息新(20260508.xls',
'bill_records_subdir': '账单查询',
'dish_sales_subdir': '账单查询-菜品销售明细表',
'data_subdirs': {}, # 4月文件平铺在月份目录下
},
'2026-05-01': {
'month_dir': '5月',
'date_range': '2026-05-01--2026-05-31',
'dist_half1': '2026年5月1-15日',
'dist_half2': '2026年5月16-31日',
'inventory_file': '4.13盘点倒挤成本报表2026年5月.xlsx',
'dish_cost_file': '2026年5月菜品成本分析报表.xlsx',
'expense_file': '2026年5月营业费用分析.xls',
'ck_subdir': '中央厨房',
'salary_file': '2026年5月薪资拆分明细表_北京西部马华餐饮有限公司.xlsx',
'attendance_file': '2026年5月考勤数据表.xlsx',
'store_info_file': None, # 5月无新各店信息文件,复用4月
'bill_records_subdir': None, # 5月无独立账单查询目录
'dish_sales_subdir': '账单查询-菜品销售明细表',
'data_subdirs': { # 5月文件在子目录中
'inventory': '供应链数据',
'dish_cost': '营业数据',
'operating_expense': '',
'distribution': '供应链数据',
'salary': '营业数据',
'attendance': '营业数据',
'central_kitchen': '中央厨房',
'dish_sales': '营业数据',
},
},
}
def _find_file(base_dirs, pattern):
"""在多个候选目录中查找文件"""
for d in base_dirs:
if not d:
continue
path = os.path.join(d, pattern)
if os.path.isfile(path):
return path
matches = glob.glob(os.path.join(d, pattern))
if matches:
return matches[0]
return None
def get_data_sources(month):
"""
根据月份返回数据源路径字典
month: 'YYYY-MM-01' 格式
"""
cfg = MONTH_CONFIG.get(month)
if not cfg:
print(f"警告: 未知月份 {month},请在 config.py MONTH_CONFIG 中添加配置")
return {}
month_dir = cfg['month_dir']
data_dir = os.path.join(DATA_ROOT, month_dir)
sbrain_data = os.path.join(DATA_SBRAIN, month_dir)
subdirs = cfg.get('data_subdirs', {})
sources = {}
def resolve(subdir_key, filename):
"""在 data_dir 和 sbrain_data 中按子目录结构查找文件"""
sub = subdirs.get(subdir_key, '')
candidates = []
if sub:
candidates.append(os.path.join(sbrain_data, sub, filename))
candidates.append(os.path.join(data_dir, sub, filename))
candidates.append(os.path.join(sbrain_data, filename))
candidates.append(os.path.join(data_dir, filename))
for c in candidates:
if os.path.isfile(c):
return c
return candidates[0]
def resolve_dir(subdir_key, dirname):
"""在 data_dir 和 sbrain_data 中按子目录结构查找目录"""
sub = subdirs.get(subdir_key, '')
candidates = []
if sub:
candidates.append(os.path.join(sbrain_data, sub, dirname))
candidates.append(os.path.join(data_dir, sub, dirname))
candidates.append(os.path.join(sbrain_data, dirname))
candidates.append(os.path.join(data_dir, dirname))
for c in candidates:
if os.path.isdir(c):
return c
return candidates[0]
# 1. 账单查询(bill_records- 目录
bill_subdir = cfg.get('bill_records_subdir')
if bill_subdir:
sources['bill_records'] = resolve_dir('bill_records', bill_subdir)
else:
sources['bill_records'] = None
# 2. 菜品销售明细 - 目录
sources['dish_sales'] = resolve_dir('dish_sales', cfg['dish_sales_subdir'])
# 3. 库存成本
sources['inventory'] = resolve('inventory', cfg['inventory_file'])
# 4. 菜品成本BOM
sources['dish_cost'] = resolve('dish_cost', cfg['dish_cost_file'])
# 5. 营业费用
sources['operating_expense'] = resolve('operating_expense', cfg['expense_file'])
# 6. 中央厨房(4个子文件)
ck_subdir = cfg['ck_subdir']
ck_dir_sbrain = os.path.join(sbrain_data, ck_subdir)
ck_dir_data = os.path.join(data_dir, ck_subdir)
ck_dir = ck_dir_sbrain if os.path.isdir(ck_dir_sbrain) else ck_dir_data
date_range = cfg['date_range']
sources['central_kitchen_finished'] = _find_file(
[ck_dir], f'{date_range} 23 59 59完工入库统计分析报表.xlsx'
) or os.path.join(ck_dir, f'{date_range} 23 59 59完工入库统计分析报表.xlsx')
sources['central_kitchen_recipe'] = _find_file(
[ck_dir], f'按配方导出-货品实际与理论耗用({date_range}).xlsx'
) or os.path.join(ck_dir, f'按配方导出-货品实际与理论耗用({date_range}).xlsx')
sources['central_kitchen_material'] = _find_file(
[ck_dir], f'按货品导出-货品实际与理论耗用({date_range}).xlsx'
) or os.path.join(ck_dir, f'按货品导出-货品实际与理论耗用({date_range}).xlsx')
sources['central_kitchen_processing'] = _find_file(
[ck_dir], '加工单价分析报表.xlsx'
) or os.path.join(ck_dir, '加工单价分析报表.xlsx')
# 7. 配送明细(2个文件)
sources['distribution'] = resolve('distribution', f'全部门店-全部仓库-统计货品明细报表{cfg["dist_half1"]}.xlsx')
sources['distribution_2'] = resolve('distribution', f'全部门店-全部仓库-统计货品明细报表{cfg["dist_half2"]}.xlsx')
# 8. 薪资+考勤
sources['salary'] = resolve('salary', cfg['salary_file'])
sources['attendance'] = resolve('attendance', cfg['attendance_file'])
# 9. 门店位置
store_info_file = cfg.get('store_info_file')
if store_info_file:
sources['store_location'] = resolve('', store_info_file)
else:
# 5月无新文件,复用4月
for p in [
os.path.join(DATA_SBRAIN, '4月', '各店信息新(20260508.xls'),
os.path.join(DATA_ROOT, '4月', '各店信息新(20260508.xls'),
]:
if os.path.isfile(p):
sources['store_location'] = p
break
else:
sources['store_location'] = None
return sources
# ============================================================
# 物化视图刷新顺序(按依赖分层,同层可并行)
# ============================================================
MATVIEW_LAYERS = [
# L0: 基础物化视图,依赖基础表
['bill_fact'],
# L1: 依赖 bill_fact 或基础表
[
'mv_daily_revenue',
'mv_dish_basket_monthly',
'mv_dish_pair_summary_monthly',
'mv_dish_sku_summary_monthly',
'mv_dish_store_summary_monthly',
'mv_district_site_benchmark_monthly',
'mv_inventory_cost_classified_monthly',
'mv_site_segment_benchmark_monthly',
'mv_store_action_priority_deep_monthly',
'mv_store_area_efficiency_monthly',
'mv_store_benchmark_composite_monthly',
'mv_store_category_mix_monthly',
'mv_store_deep_diagnosis_monthly',
'mv_store_member_opportunity_monthly',
'mv_store_overlap_risk_monthly',
'mv_store_platform_economics_monthly',
'mv_store_repeat_summary_monthly',
'mv_store_risk_rating_monthly',
'mv_store_scorecard',
'mv_store_site_profile_monthly',
'mv_store_site_replication_monthly',
'mv_store_theoretical_actual_cost_monthly',
'dish_sales_april',
'v_store_benchmark',
'v_store_category_mix',
'v_store_platform_economics',
],
# L2: 依赖 L1 物化视图
[
'dish_basket_april',
'dish_category_summary_april',
'dish_member_sku_april',
'dish_sku_summary_april',
'dish_store_sku_april',
'mv_dish_sku_abc_monthly',
'v_store_action_list',
],
# L3: 依赖 L2
[
'dish_store_summary_april',
'mv_store_action_priority_deep_april',
'mv_store_theoretical_actual_cost_april',
'v_store_execution_priority',
],
]
def get_dbname():
"""获取默认数据库名"""
return 'bill_query_test'