#!/usr/bin/env python3 """ 生成月度课时快照(MonthlySnapshot) 基于现有数据,为每个账户生成当前月份的快照 """ import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) os.environ['DB_TYPE'] = 'sqlite' from flask import Flask from config import Config from models import db, StudentAccount, RechargeRecord, ConsumptionRecord, MonthlySnapshot from datetime import datetime def generate_snapshots(): """为每个学员账户生成月度快照""" app = Flask(__name__) app.config.from_object(Config) db.init_app(app) with app.app_context(): # 清空现有快照 MonthlySnapshot.query.delete() accounts = StudentAccount.query.all() print(f"开始为 {len(accounts)} 个账户生成快照...") # 获取所有充值记录并按账户分组 recharges_by_account = {} all_recharges = RechargeRecord.query.all() for r in all_recharges: key = (r.student_id, r.course_id) if key not in recharges_by_account: recharges_by_account[key] = [] recharges_by_account[key].append(r) # 获取所有消课记录并按账户和年月分组 consumptions_by_account = {} all_consumptions = ConsumptionRecord.query.all() for c in all_consumptions: key = (c.student_id, c.course_id) if key not in consumptions_by_account: consumptions_by_account[key] = [] consumptions_by_account[key].append(c) # 按年月统计消课 def get_yearly_consumption(student_id, course_id, year, month): """获取某年某月的消课量""" key = (student_id, course_id) consumptions = consumptions_by_account.get(key, []) total = 0.0 for c in consumptions: if c.consume_date and c.consume_date.year == year and c.consume_date.month == month: total += float(c.hours_consumed or 0) return total def get_yearly_recharge(student_id, course_id, year, month): """获取某年某月的充值金额""" key = (student_id, course_id) recharges = recharges_by_account.get(key, []) total_amount = 0.0 total_lessons = 0.0 total_gift = 0.0 for r in recharges: if r.created_at and r.created_at.year == year and r.created_at.month == month: total_amount += float(r.amount or 0) total_lessons += float(r.normal_hours or 0) total_gift += float(r.gifted_hours or 0) return total_amount, total_lessons, total_gift def get_cumulative_consumption_until(student_id, course_id, year, month): """获取截至某年某月的累计消课量""" key = (student_id, course_id) consumptions = consumptions_by_account.get(key, []) total = 0.0 for c in consumptions: if c.consume_date: # 计算到指定月份为止 if (c.consume_date.year < year) or \ (c.consume_date.year == year and c.consume_date.month <= month): total += float(c.hours_consumed or 0) return total def get_total_recharge(student_id, course_id): """获取累计充值总金额""" key = (student_id, course_id) recharges = recharges_by_account.get(key, []) return sum(float(r.amount or 0) for r in recharges) for idx, account in enumerate(accounts): if (idx + 1) % 50 == 0: print(f" 处理进度: {idx + 1}/{len(accounts)}") student_id = account.student_id course_id = account.course_id # 获取总充值金额 total_recharge_amount = get_total_recharge(student_id, course_id) # 计算截至2026年4月的累计消课 cumulative_consumed = get_cumulative_consumption_until(student_id, course_id, 2026, 4) # 当前剩余课时 current_remaining_normal = float(account.normal_hours or 0) current_remaining_gift = float(account.gifted_hours or 0) # 计算总购买课时 total_bought_lessons = current_remaining_normal + current_remaining_gift + cumulative_consumed # 单价 unit_price = float(account.unit_price or 0) if unit_price == 0 and total_bought_lessons > 0: unit_price = total_recharge_amount / total_bought_lessons # 月末余额 = 总充值 - 已消课金额 end_balance = total_recharge_amount - cumulative_consumed * unit_price # 1. 生成 2026年4月 快照(期末快照) consumed_apr = get_yearly_consumption(student_id, course_id, 2026, 4) # 月初余额 prev_balance_apr = total_recharge_amount - (cumulative_consumed - consumed_apr) * unit_price prev_lessons_apr = total_bought_lessons - cumulative_consumed + consumed_apr - current_remaining_gift snap_apr = MonthlySnapshot( account_id=account.id, year=2026, month=4, seq_no=1, prev_lessons=max(0, prev_lessons_apr), prev_gift_lessons=current_remaining_gift, prev_total_lessons=max(0, prev_lessons_apr) + current_remaining_gift, prev_balance=prev_balance_apr, new_signup_lessons=0, new_signup_gift_lessons=0, new_signup_amount=0, renewal_lessons=0, renewal_gift_lessons=0, renewal_amount=0, consumed_lessons=consumed_apr, end_lessons=current_remaining_normal, end_gift_lessons=current_remaining_gift, end_total_lessons=current_remaining_normal + current_remaining_gift, end_balance=end_balance, unit_price=unit_price, notes='From Excel import' ) db.session.add(snap_apr) # 2. 生成 2026年5月 快照 consumed_may = get_yearly_consumption(student_id, course_id, 2026, 5) cumulative_consumed_may = get_cumulative_consumption_until(student_id, course_id, 2026, 5) end_balance_may = total_recharge_amount - cumulative_consumed_may * unit_price prev_balance_may = end_balance prev_lessons_may = current_remaining_normal + (consumed_apr if consumed_apr > 0 else 0) snap_may = MonthlySnapshot( account_id=account.id, year=2026, month=5, seq_no=1, prev_lessons=current_remaining_normal, prev_gift_lessons=current_remaining_gift, prev_total_lessons=current_remaining_normal + current_remaining_gift, prev_balance=end_balance, new_signup_lessons=0, new_signup_gift_lessons=0, new_signup_amount=0, renewal_lessons=0, renewal_gift_lessons=0, renewal_amount=0, consumed_lessons=consumed_may, end_lessons=current_remaining_normal, end_gift_lessons=current_remaining_gift, end_total_lessons=current_remaining_normal + current_remaining_gift, end_balance=end_balance_may, unit_price=unit_price, notes='From Excel import' ) db.session.add(snap_may) db.session.commit() print(f"完成! 共生成 {MonthlySnapshot.query.count()} 条月度快照") # 显示统计 print("\n=== 按月份统计 ===") from sqlalchemy import func stats = db.session.query( MonthlySnapshot.year, MonthlySnapshot.month, func.count(MonthlySnapshot.id) ).group_by(MonthlySnapshot.year, MonthlySnapshot.month).order_by( MonthlySnapshot.year.desc(), MonthlySnapshot.month.desc() ).all() for y, m, c in stats: print(f" {y}-{m:02d}: {c} 条记录") # 验证数据 print("\n=== 样本数据验证 ===") for snap in MonthlySnapshot.query.filter_by(year=2026, month=4).limit(5).all(): print(f" Account {snap.account_id}: {snap.year}-{snap.month:02d}") print(f" 月末正课: {snap.end_lessons}, 赠课: {snap.end_gift_lessons}, 总课时: {snap.end_total_lessons}") print(f" 月末余额: {snap.end_balance:.2f}, 单价: {snap.unit_price:.4f}") if __name__ == '__main__': generate_snapshots()