2fc1b96f44
- 整合所有导入脚本到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: 管线文档
199 lines
5.8 KiB
Python
199 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
账单查询原始数据导入脚本
|
||
从Excel文件导入bill_records和bill_columns表。
|
||
|
||
用法:
|
||
python3 import_bill_records.py --month 2026-04-01 --file <文件或目录> --db bill_query_test
|
||
python3 import_bill_records.py --month 2026-04-01 --file <目录> --db bill_query_test --skip-columns
|
||
"""
|
||
import argparse
|
||
import hashlib
|
||
import os
|
||
import sys
|
||
|
||
import pandas as pd
|
||
import psycopg2
|
||
from psycopg2.extras import execute_values
|
||
|
||
|
||
def file_sha256(filepath):
|
||
h = hashlib.sha256()
|
||
with open(filepath, 'rb') as f:
|
||
for chunk in iter(lambda: f.read(8192), b''):
|
||
h.update(chunk)
|
||
return h.hexdigest()
|
||
|
||
|
||
def to_text(val):
|
||
if val is None:
|
||
return None
|
||
if isinstance(val, float) and pd.isna(val):
|
||
return None
|
||
s = str(val).strip()
|
||
if s.lower() in ('nan', 'none', ''):
|
||
return None
|
||
return s
|
||
|
||
|
||
def import_bill_columns(conn, filepaths):
|
||
"""从第一个Excel文件提取列定义,导入bill_columns表"""
|
||
cur = conn.cursor()
|
||
cur.execute("SELECT count(*) FROM public.bill_columns")
|
||
existing = cur.fetchone()[0]
|
||
if existing > 0:
|
||
print(f"bill_columns已有{existing}行,跳过列定义导入")
|
||
return
|
||
|
||
if not filepaths:
|
||
print("无文件可用,跳过列定义导入")
|
||
return
|
||
|
||
fpath = filepaths[0]
|
||
df = pd.read_excel(fpath, header=None, nrows=2, engine='openpyxl')
|
||
main_headers = df.iloc[0].tolist()
|
||
sub_headers = df.iloc[1].tolist()
|
||
|
||
columns = []
|
||
for i, (mh, sh) in enumerate(zip(main_headers, sub_headers)):
|
||
col_name = f"c{i+1:03d}"
|
||
group_h = to_text(mh) or ''
|
||
sub_h = to_text(sh) or ''
|
||
# If group and sub are the same, only keep group
|
||
if group_h and sub_h and group_h == sub_h:
|
||
sub_h = ''
|
||
columns.append((col_name, i + 1, group_h, sub_h))
|
||
|
||
execute_values(
|
||
cur,
|
||
"INSERT INTO public.bill_columns (column_name, excel_position, group_header, sub_header) VALUES %s",
|
||
columns,
|
||
page_size=500
|
||
)
|
||
conn.commit()
|
||
print(f"bill_columns导入完成: {len(columns)} 列")
|
||
cur.close()
|
||
|
||
|
||
def import_bill_records(conn, filepath, report_month):
|
||
"""导入单个Excel文件的账单数据到bill_records"""
|
||
fname = os.path.basename(filepath)
|
||
sha = file_sha256(filepath)
|
||
|
||
cur = conn.cursor()
|
||
|
||
# 幂等检查
|
||
cur.execute(
|
||
"SELECT 1 FROM public.bill_records WHERE source_file = %s LIMIT 1",
|
||
(fname,)
|
||
)
|
||
if cur.fetchone():
|
||
print(f" {fname} 已导入过,跳过")
|
||
cur.close()
|
||
return 0
|
||
|
||
# 读取Excel:row0=主表头, row1=子表头, 数据从row2开始
|
||
df = pd.read_excel(filepath, header=None, engine='openpyxl')
|
||
# 跳过前2行(表头)
|
||
data_df = df.iloc[2:]
|
||
# 过滤空行(第一列为空)
|
||
data_df = data_df.dropna(subset=[data_df.columns[0]])
|
||
data_rows = data_df.values.tolist()
|
||
|
||
print(f" 数据行数: {len(data_rows)}, 列数: {data_df.shape[1]}")
|
||
|
||
if not data_rows:
|
||
cur.close()
|
||
return 0
|
||
|
||
# 获取DB列名 (c001 ~ c196)
|
||
num_cols = data_df.shape[1]
|
||
col_names = [f"c{i+1:03d}" for i in range(num_cols)]
|
||
all_cols = ['source_file', 'source_row'] + col_names
|
||
col_str = ','.join(all_cols)
|
||
|
||
batch = []
|
||
for idx, row in enumerate(data_rows):
|
||
source_row = idx + 3 # 1-indexed, data starts at Excel row 3
|
||
vals = [fname, source_row]
|
||
for i in range(num_cols):
|
||
v = row[i] if i < len(row) else None
|
||
vals.append(to_text(v))
|
||
batch.append(tuple(vals))
|
||
if len(batch) >= 500:
|
||
execute_values(
|
||
cur,
|
||
f"INSERT INTO public.bill_records ({col_str}) VALUES %s",
|
||
batch,
|
||
page_size=500
|
||
)
|
||
conn.commit()
|
||
if (idx + 1) % 10000 == 0:
|
||
print(f" 已导入 {idx + 1}/{len(data_rows)} 行")
|
||
batch = []
|
||
|
||
if batch:
|
||
execute_values(
|
||
cur,
|
||
f"INSERT INTO public.bill_records ({col_str}) VALUES %s",
|
||
batch,
|
||
page_size=500
|
||
)
|
||
conn.commit()
|
||
|
||
print(f" 已导入 {len(data_rows)}/{len(data_rows)} 行")
|
||
cur.close()
|
||
return len(data_rows)
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description='账单查询原始数据导入')
|
||
parser.add_argument('--month', required=True, help='报告月份 (YYYY-MM-01)')
|
||
parser.add_argument('--file', required=True, help='Excel文件路径或目录')
|
||
parser.add_argument('--db', default='bill_query', help='目标数据库 (默认bill_query)')
|
||
parser.add_argument('--skip-columns', action='store_true', help='跳过bill_columns导入')
|
||
|
||
args = parser.parse_args()
|
||
|
||
conn = psycopg2.connect(host='localhost', port=5432, dbname=args.db, user='freedak')
|
||
conn.autocommit = False
|
||
|
||
# 收集文件
|
||
files = []
|
||
if os.path.isdir(args.file):
|
||
for f in sorted(os.listdir(args.file)):
|
||
if f.endswith('.xlsx') and not f.startswith('.'):
|
||
files.append(os.path.join(args.file, f))
|
||
else:
|
||
files.append(args.file)
|
||
|
||
print(f"\n=== 账单查询原始数据导入 ===")
|
||
print(f"文件数: {len(files)}")
|
||
print(f"数据库: {args.db}")
|
||
|
||
try:
|
||
# 导入列定义
|
||
if not args.skip_columns:
|
||
import_bill_columns(conn, files)
|
||
|
||
# 导入数据
|
||
total = 0
|
||
for idx, fpath in enumerate(files):
|
||
fname = os.path.basename(fpath)
|
||
print(f"\n[{idx+1}/{len(files)}] {fname}")
|
||
total += import_bill_records(conn, fpath, args.month)
|
||
|
||
print(f"\n账单数据导入完成: 共 {total} 行")
|
||
print("=== 导入完成 ===")
|
||
|
||
except Exception as e:
|
||
conn.rollback()
|
||
print(f"错误: {e}", file=sys.stderr)
|
||
raise
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|