#!/usr/bin/env python3 """ Excel数据导入SQLite脚本 导入达尔琳基础数据和课时核对表数据 """ import os import sys import re from datetime import datetime, date from decimal import Decimal # 设置环境变量使用SQLite os.environ['DB_TYPE'] = 'sqlite' import openpyxl from flask import Flask from config import Config from models import db, Role, User, Teacher, Student, Course, Class_, ClassStudent, StudentAccount, RechargeRecord def parse_date(value): """解析日期""" if value is None: return None if isinstance(value, datetime): return value.date() if isinstance(value, date): return value if isinstance(value, str): # 处理中文日期格式如 "2026-04-01" 或 "4.5" value = value.strip() if re.match(r'^\d{4}-\d{2}-\d{2}', value): return datetime.strptime(value[:10], '%Y-%m-%d').date() elif re.match(r'^\d{1,2}\.\d{1,2}$', value): # 格式如 4.5 表示4月5日,使用当前年份 parts = value.split('.') month, day = int(parts[0]), int(parts[1]) return date(2026, month, day) elif re.match(r'^\d+$', value): # Excel日期序列号 try: return datetime.fromordinal(datetime(1900, 1, 1).toordinal() + int(value) - 2).date() except: pass return None def parse_number(value): """解析数字""" if value is None: return 0 if isinstance(value, (int, float)): return float(value) if isinstance(value, str): value = value.strip() if value == '' or value == '系统合并': return 0 try: return float(value) except: return 0 return 0 def clean_name(name): """清理姓名中的括号和备注""" if name is None: return '' name = str(name).strip() # 移除括号及其内容(如 "王莫迪(赛赛)" -> "王莫迪") name = re.sub(r'(.*?)', '', name) name = re.sub(r'\(.*?\)', '', name) return name.strip() def extract_nickname(full_name): """提取昵称""" if full_name is None: return '' full_name = str(full_name).strip() # 匹配中文括号 match = re.search(r'((.+?))', full_name) if match: return match.group(1).strip() # 匹配英文括号 match = re.search(r'\((.+?)\)', full_name) if match: return match.group(1).strip() return '' def get_gender_from_remark(remark): """从备注中推断性别""" if remark and '男' in str(remark): return '男' elif remark and '女' in str(remark): return '女' return '' def create_app(): """创建Flask应用""" app = Flask(__name__) app.config.from_object(Config) db.init_app(app) return app def init_database(app): """初始化数据库表""" with app.app_context(): db.create_all() # 创建默认角色 if not Role.query.filter_by(name='管理员').first(): admin_role = Role(name='管理员', description='系统管理员') db.session.add(admin_role) if not Role.query.filter_by(name='超级管理员').first(): super_role = Role(name='超级管理员', description='超级管理员', permissions='all') db.session.add(super_role) # 创建默认管理员用户 if not User.query.filter_by(username='admin').first(): admin_user = User( username='admin', real_name='管理员', role_id=Role.query.filter_by(name='超级管理员').first().id, phone='13800000000' ) admin_user.set_password('admin123') db.session.add(admin_user) db.session.commit() print("数据库初始化完成") def import_basic_data(app): """导入基础数据""" print("\n" + "=" * 60) print("开始导入基础数据...") print("=" * 60) with app.app_context(): # 读取Excel excel_path = 'assets/达尔琳基础数据.xlsx' wb = openpyxl.load_workbook(excel_path, data_only=True) ws = wb['基础信息'] # 存储课程映射 (课程名 -> Course对象) course_map = {} # 存储班级映射 (班级名 -> Class_对象) class_map = {} # 存储老师映射 (老师名 -> Teacher对象) teacher_map = {} # 首先创建默认课程 default_courses = { 'scratch': Course(name='Scratch图形编程', level='scratch'), 'python': Course(name='Python编程', level='python'), 'c++': Course(name='C++编程', level='c++'), 'lab': Course(name='Lab编程', level='lab'), '大班': Course(name='STEM大班', level='大班'), '中班': Course(name='STEM中班', level='中班'), '小班': Course(name='STEM小班', level='小班'), } for name, course in default_courses.items(): db.session.add(course) db.session.commit() for name, course in default_courses.items(): course_map[name] = course print(f"已创建 {len(default_courses)} 个默认课程") # 从Excel读取数据 students_data = [] for row_idx, row in enumerate(ws.iter_rows(min_row=3, values_only=True), 3): if not row[1]: # 学员姓名 continue student_name = clean_name(row[1]) if not student_name: continue class_name = str(row[2]).strip() if row[2] else '' course_level = str(row[3]).strip() if row[3] else '' nature = str(row[4]).strip() if row[4] else '自费' # 解析新签信息 new_sign_amount = parse_number(row[11]) # 新签金额 new_sign_hours = parse_number(row[12]) # 新签课时 new_sign_gift = parse_number(row[13]) # 新签赠课 new_sign_price = parse_number(row[14]) # 新签课单价 # 解析续费信息 renewal1_amount = parse_number(row[16]) # 续费一金额 renewal1_hours = parse_number(row[17]) # 续费一课时 renewal1_gift = parse_number(row[18]) # 赠送课时一 renewal1_price = parse_number(row[19]) # 续费课单价一 renewal2_amount = parse_number(row[20]) # 续费二金额 renewal2_hours = parse_number(row[21]) # 续费二课时 renewal2_gift = parse_number(row[22]) # 赠送课时二 renewal2_price = parse_number(row[23]) # 续费课单价二 renewal3_amount = parse_number(row[24]) # 续费三金额 renewal3_hours = parse_number(row[25]) # 续费三课时 renewal3_gift = parse_number(row[26]) # 赠送课时三 renewal3_price = parse_number(row[27]) # 续费课单价三 # 剩余课时信息 remain_hours = parse_number(row[30]) # 3月份剩余课时 gift_hours = parse_number(row[31]) # 赠课 remain_total = parse_number(row[32]) # 3月份剩余总课时 unit_price = parse_number(row[33]) # 单价 remain_amount = parse_number(row[34]) # 3月末剩余金额 remark = str(row[35]) if row[35] else '' # 提取昵称 nickname = extract_nickname(str(row[1]) if row[1] else '') gender = get_gender_from_remark(remark) students_data.append({ 'name': student_name, 'nickname': nickname, 'class_name': class_name, 'course_level': course_level, 'nature': nature, 'gender': gender, 'remark': remark, 'new_sign_amount': new_sign_amount, 'new_sign_hours': new_sign_hours, 'new_sign_gift': new_sign_gift, 'new_sign_price': new_sign_price, 'renewal1_amount': renewal1_amount, 'renewal1_hours': renewal1_hours, 'renewal1_gift': renewal1_gift, 'renewal1_price': renewal1_price, 'renewal2_amount': renewal2_amount, 'renewal2_hours': renewal2_hours, 'renewal2_gift': renewal2_gift, 'renewal2_price': renewal2_price, 'renewal3_amount': renewal3_amount, 'renewal3_hours': renewal3_hours, 'renewal3_gift': renewal3_gift, 'renewal3_price': renewal3_price, 'remain_hours': remain_hours, 'gift_hours': gift_hours, 'remain_total': remain_total, 'unit_price': unit_price, 'remain_amount': remain_amount, }) print(f"从基础数据表读取了 {len(students_data)} 条学员记录") # 导入学生 student_map = {} # name -> Student account_count = 0 for data in students_data: # 创建或获取学生 student = Student.query.filter_by(name=data['name']).first() if not student: student = Student( name=data['name'], nickname=data['nickname'], gender=data['gender'], remark=data['remark'], status=1 ) db.session.add(student) db.session.flush() student_map[data['name']] = student elif data['nickname'] and not student.nickname: student.nickname = data['nickname'] # 获取课程 course = course_map.get(data['course_level']) if not course: course = course_map.get('scratch') # 默认 # 创建课时账户 account = StudentAccount.query.filter_by( student_id=student.id, course_id=course.id ).first() if not account: # 计算总课时(正课+赠课) total_normal = data['remain_hours'] total_gift = data['gift_hours'] total_consumed = 0 # 根据单价计算 if data['unit_price'] > 0: account = StudentAccount( student_id=student.id, course_id=course.id, normal_hours=total_normal, gifted_hours=total_gift, consumed_normal=total_consumed, consumed_gifted=0, unit_price=data['unit_price'], original_price_per_lesson=data['unit_price'], cumulative_amount=data['remain_amount'], account_status='active' ) else: account = StudentAccount( student_id=student.id, course_id=course.id, normal_hours=total_normal, gifted_hours=total_gift, consumed_normal=total_consumed, consumed_gifted=0, cumulative_amount=data['remain_amount'], account_status='active' ) db.session.add(account) account_count += 1 # 添加充值记录 operator = User.query.filter_by(username='admin').first() if not operator: continue # 新签记录 if data['new_sign_amount'] > 0: recharge = RechargeRecord( student_id=student.id, course_id=course.id, amount=data['new_sign_amount'], normal_hours=data['new_sign_hours'], gifted_hours=data['new_sign_gift'], operator_id=operator.id, remark='新签' ) db.session.add(recharge) # 续费记录1 if data['renewal1_amount'] > 0: recharge = RechargeRecord( student_id=student.id, course_id=course.id, amount=data['renewal1_amount'], normal_hours=data['renewal1_hours'], gifted_hours=data['renewal1_gift'], operator_id=operator.id, remark='续费一' ) db.session.add(recharge) # 续费记录2 if data['renewal2_amount'] > 0: recharge = RechargeRecord( student_id=student.id, course_id=course.id, amount=data['renewal2_amount'], normal_hours=data['renewal2_hours'], gifted_hours=data['renewal2_gift'], operator_id=operator.id, remark='续费二' ) db.session.add(recharge) # 续费记录3 if data['renewal3_amount'] > 0: recharge = RechargeRecord( student_id=student.id, course_id=course.id, amount=data['renewal3_amount'], normal_hours=data['renewal3_hours'], gifted_hours=data['renewal3_gift'], operator_id=operator.id, remark='续费三' ) db.session.add(recharge) db.session.commit() print(f"导入完成: {len(student_map)} 名学生, {account_count} 个课时账户") print(f"充值记录已添加到各学生账户") def import_consumption_data(app): """导入课时核对表数据(消课记录)""" print("\n" + "=" * 60) print("开始导入课时核对表数据...") print("=" * 60) with app.app_context(): from models import ConsumptionRecord excel_path = 'assets/课时核对表_2026_4月份.xlsx' wb = openpyxl.load_workbook(excel_path, data_only=True) total_consumption = 0 # 处理每个sheet for sheet_name in wb.sheetnames: if sheet_name in ['研学名单', '4月份转账', '校园通转账明细']: continue # 跳过转账相关sheet ws = wb[sheet_name] print(f"\n处理 Sheet: {sheet_name}") # 根据sheet类型确定列位置 if sheet_name == 'code学龄后': # 列: A序号 B学员姓名 C课程级别 D3月份剩余课时 E赠送课时 F3月剩余总课时 # G单价 H3月末剩余金额 I新签金额 J新签课时 K新签赠送课时 L续费金额 M续费课时 # N续费赠送课时 O4月份已上课时 P4月份已上金额 Q退费课时 R账号费 # S退赠送课时 T退费金额 U4月份剩余课时 V4月剩余总课时 W4月末剩余金额 # X4月份上课情况 AA备注 pass # 遍历数据行 for row_idx, row in enumerate(ws.iter_rows(min_row=3, values_only=True), 3): if not row[1]: # 学员姓名 continue student_name = clean_name(row[1]) if not student_name: continue # 找到学生 student = Student.query.filter_by(name=student_name).first() if not student: print(f" 警告: 未找到学生 '{student_name}'") continue # 解析上课日期 # 根据sheet类型,找到上课情况列 if sheet_name == 'code学龄后': attendance_dates = [] # 从X列开始是上课日期 for col_idx in range(23, 31): # X到AE列 if col_idx < len(row) and row[col_idx]: date_val = row[col_idx] if isinstance(date_val, str): # 格式如 "4.5", "4.12", "4.19(补)" date_str = re.sub(r'(.*?)', '', date_val).strip() parsed_date = parse_date(date_str) if parsed_date: attendance_dates.append(parsed_date) elif isinstance(date_val, (datetime, date)): if isinstance(date_val, datetime): attendance_dates.append(date_val.date()) else: attendance_dates.append(date_val) # 获取已上课时 consumed_hours = parse_number(row[14] if len(row) > 14 else 0) # O列 # 创建消课记录 if attendance_dates: for consume_date in attendance_dates: consumption = ConsumptionRecord( student_id=student.id, course_id=student.accounts[0].course_id if student.accounts else 1, hours_consumed=1, # 每次课1课时 consume_type='normal', normal_consumed=1, gifted_consumed=0, operator_id=User.query.filter_by(username='admin').first().id, consume_date=consume_date, remark=f'从{sheet_name}导入' ) db.session.add(consumption) total_consumption += 1 elif sheet_name == 'stem学龄前': attendance_dates = [] for col_idx in range(22, 31): # W列开始 if col_idx < len(row) and row[col_idx]: date_val = row[col_idx] if isinstance(date_val, str): date_str = re.sub(r'(.*?)', '', date_val).strip() parsed_date = parse_date(date_str) if parsed_date: attendance_dates.append(parsed_date) elif isinstance(date_val, (datetime, date)): if isinstance(date_val, datetime): attendance_dates.append(date_val.date()) else: attendance_dates.append(date_val) if attendance_dates: for consume_date in attendance_dates: consumption = ConsumptionRecord( student_id=student.id, course_id=student.accounts[0].course_id if student.accounts else 2, hours_consumed=1, consume_type='normal', normal_consumed=1, gifted_consumed=0, operator_id=User.query.filter_by(username='admin').first().id, consume_date=consume_date, remark=f'从{sheet_name}导入' ) db.session.add(consumption) total_consumption += 1 elif sheet_name == '学龄后lab': attendance_dates = [] for col_idx in range(15, 26): # P列开始 if col_idx < len(row) and row[col_idx]: date_val = row[col_idx] if isinstance(date_val, str): date_str = re.sub(r'(.*?)', '', date_val).strip() parsed_date = parse_date(date_str) if parsed_date: attendance_dates.append(parsed_date) elif isinstance(date_val, (datetime, date)): if isinstance(date_val, datetime): attendance_dates.append(date_val.date()) else: attendance_dates.append(date_val) if attendance_dates: for consume_date in attendance_dates: consumption = ConsumptionRecord( student_id=student.id, course_id=student.accounts[0].course_id if student.accounts else 4, hours_consumed=1, consume_type='normal', normal_consumed=1, gifted_consumed=0, operator_id=User.query.filter_by(username='admin').first().id, consume_date=consume_date, remark=f'从{sheet_name}导入' ) db.session.add(consumption) total_consumption += 1 elif sheet_name == '免费': attendance_dates = [] for col_idx in range(6, 12): # G列开始是上课日期 if col_idx < len(row) and row[col_idx]: date_val = row[col_idx] if isinstance(date_val, str): date_str = re.sub(r'(.*?)', '', date_val).strip() # 免费课日期格式可能不同 if re.match(r'^\d{1,2}\.\d{1,2}', date_str): parts = date_str.split('.') month, day = int(parts[0]), int(parts[1][:2]) parsed_date = date(2026, month, day) attendance_dates.append(parsed_date) elif isinstance(date_val, (datetime, date)): if isinstance(date_val, datetime): attendance_dates.append(date_val.date()) else: attendance_dates.append(date_val) if attendance_dates: for consume_date in attendance_dates: consumption = ConsumptionRecord( student_id=student.id, course_id=1, # 免费课 hours_consumed=1, consume_type='gift', normal_consumed=0, gifted_consumed=1, operator_id=User.query.filter_by(username='admin').first().id, consume_date=consume_date, remark=f'从{sheet_name}导入' ) db.session.add(consumption) total_consumption += 1 db.session.commit() print(f"\n消课记录导入完成: {total_consumption} 条记录") def verify_data(app): """验证数据完整性""" print("\n" + "=" * 60) print("数据完整性验证...") print("=" * 60) with app.app_context(): student_count = Student.query.count() account_count = StudentAccount.query.count() recharge_count = RechargeRecord.query.count() consumption_count = ConsumptionRecord.query.count() if 'ConsumptionRecord' in dir() else 0 course_count = Course.query.count() print(f"\n数据统计:") print(f" - 学生总数: {student_count}") print(f" - 课时账户: {account_count}") print(f" - 充值记录: {recharge_count}") print(f" - 课程数量: {course_count}") # 验证消课记录 from models import ConsumptionRecord consumption_count = ConsumptionRecord.query.count() print(f" - 消课记录: {consumption_count}") # 按课程统计学生 print("\n按课程统计:") for course in Course.query.all(): count = StudentAccount.query.filter_by(course_id=course.id).count() print(f" - {course.name}: {count} 名学生") # 显示部分学生数据 print("\n部分学生数据示例:") for student in Student.query.limit(5).all(): print(f" - {student.name} (ID:{student.id})") for account in student.accounts: print(f" 课程: {account.course.name}, 正课: {account.normal_hours}, 赠课: {account.gifted_hours}, 单价: {account.unit_price}") def main(): """主函数""" print("=" * 60) print("达尔琳数据导入工具") print("=" * 60) # 创建应用 app = create_app() # 初始化数据库 init_database(app) # 导入基础数据 import_basic_data(app) # 导入课时核对表数据 import_consumption_data(app) # 验证数据 verify_data(app) print("\n" + "=" * 60) print("导入完成!") print("=" * 60) if __name__ == '__main__': main()