fix: 生成月度快照数据修复余额显示
Coze-Commit-Type: user Coze-User-ID: 3722323274763196 Coze-Conversation-ID: 5260473
This commit is contained in:
212
xuexiao/generate_snapshots.py
Normal file
212
xuexiao/generate_snapshots.py
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/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()
|
||||
Binary file not shown.
Reference in New Issue
Block a user