Files
SBrainCO/db/pipeline/import_bill_records.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

199 lines
5.8 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
"""
账单查询原始数据导入脚本
从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
# 读取Excelrow0=主表头, 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()