Initial commit

This commit is contained in:
user9994793890
2026-05-29 10:28:07 +08:00
commit ee860ce0ae
87 changed files with 12595 additions and 0 deletions

248
xuexiao/routes/student.py Normal file
View File

@@ -0,0 +1,248 @@
"""学员管理路由"""
from datetime import date, datetime
from flask import render_template, request, redirect, url_for, flash, session
from sqlalchemy.orm import joinedload
from models import (
db, Student, StudentAccount, RechargeRecord, ConsumptionRecord,
RefundRecord, StopRecord, ClassStudent, Class_,
)
from utils import (
login_required, log_operation, parse_date, permission_required,
export_students, filter_students_query,
)
from keshi_display import (
get_latest_period, attach_latest_snapshots,
snapshot_lessons_display, summarize_students_ledger,
fmt_money, fmt_hours, fmt_price,
)
from finance_display import build_students_finance, build_refund_preview
from ui_sort import parse_sort, sort_rows_in_memory, account_ledger_sort_keys
def register_routes(app):
@app.route('/students', endpoint='student_list')
@login_required
@permission_required('student_view')
def student_list():
keyword = request.args.get('keyword', '')
sort = request.args.get('sort', '').strip()
order = request.args.get('order', 'asc').strip().lower()
if order not in ('asc', 'desc'):
order = 'asc'
query = filter_students_query(Student.query)
if keyword:
query = query.filter(Student.name.contains(keyword))
students = query.all()
py, pm = get_latest_period()
student_ids = [s.id for s in students]
student_summaries = summarize_students_ledger(student_ids, py, pm)
accounts = []
if student_ids:
accounts = StudentAccount.query.filter(
StudentAccount.student_id.in_(student_ids),
).all()
accounts_by_student: dict[int, list] = {}
for acc in accounts:
accounts_by_student.setdefault(acc.student_id, []).append(acc)
ledger_by_account = {}
snap_by_account = attach_latest_snapshots(accounts, py, pm)
for acc in accounts:
ledger_by_account[acc.id] = snapshot_lessons_display(
snap_by_account.get(acc.id), acc,
)
student_finance = build_students_finance(
students, accounts_by_student, ledger_by_account, snap_by_account,
)
reverse = order == 'desc'
missing_num = float('inf') if not reverse else float('-inf')
if sort == 'refund':
students.sort(
key=lambda s: student_finance.get(s.id, {}).get('sort_value', missing_num),
reverse=reverse,
)
elif sort == 'hours':
students.sort(
key=lambda s: (
student_summaries.get(s.id, {}).get('total_lessons')
if student_summaries.get(s.id, {}).get('course_count')
else missing_num
),
reverse=reverse,
)
elif sort == 'balance':
students.sort(
key=lambda s: (
student_summaries.get(s.id, {}).get('total_balance')
if student_summaries.get(s.id, {}).get('course_count')
else missing_num
),
reverse=reverse,
)
elif sort == 'status':
students.sort(key=lambda s: s.status or 0, reverse=reverse)
elif sort == 'created':
students.sort(
key=lambda s: s.created_at or (
datetime.max if not reverse else datetime.min
),
reverse=reverse,
)
else:
students.sort(key=lambda s: s.id, reverse=True)
return render_template(
'student_list.html',
students=students,
keyword=keyword,
sort=sort,
order=order,
student_summaries=student_summaries,
student_finance=student_finance,
ledger_period=f'{py}-{pm:02d}' if py and pm else '',
fmt_money=fmt_money,
fmt_hours=fmt_hours,
)
@app.route('/students/add', methods=['GET', 'POST'], endpoint='student_add')
@login_required
@permission_required('student_add')
def student_add():
if request.method == 'POST':
student = Student(
name=request.form.get('name'),
gender=request.form.get('gender'),
phone=request.form.get('phone'),
parent_phone=request.form.get('parent_phone'),
birthday=parse_date(request.form.get('birthday')),
address=request.form.get('address'),
source=request.form.get('source'),
remark=request.form.get('remark', ''),
status=1
)
db.session.add(student)
db.session.commit()
log_operation('创建学员', f'学员:{student.name}')
flash('学员创建成功', 'success')
return redirect(url_for('student_list'))
return render_template('student_form.html', student=None)
@app.route('/students/edit/<int:id>', methods=['GET', 'POST'], endpoint='student_edit')
@login_required
@permission_required('student_edit')
def student_edit(id):
student = db.session.get(Student, id)
if request.method == 'POST':
student.name = request.form.get('name')
student.birthday = parse_date(request.form.get('birthday'))
student.address = request.form.get('address')
student.source = request.form.get('source')
student.status = int(request.form.get('status', 1))
student.remark = request.form.get('remark', '')
db.session.commit()
log_operation('编辑学员', f'学员ID:{id}')
flash('学员更新成功', 'success')
return redirect(url_for('student_list'))
return render_template('student_form.html', student=student)
@app.route('/students/delete/<int:id>', endpoint='student_delete')
@login_required
@permission_required('student_delete')
def student_delete(id):
student = db.session.get(Student, id)
if student:
student.status = 0
db.session.commit()
log_operation('删除学员', f'学员ID:{id}')
flash('学员已停用', 'success')
return redirect(url_for('student_list'))
@app.route('/students/detail/<int:id>', endpoint='student_detail')
@login_required
@permission_required('student_view')
def student_detail(id):
student = db.session.get(Student, id)
accounts = (
StudentAccount.query.options(joinedload(StudentAccount.course))
.filter_by(student_id=id)
.all()
)
py, pm = get_latest_period()
latest_snaps = attach_latest_snapshots(accounts, py, pm)
account_ledger = {
a.id: snapshot_lessons_display(latest_snaps.get(a.id), a)
for a in accounts
}
refund_previews = {
a.id: build_refund_preview(
a, a.course, account_ledger[a.id], latest_snaps.get(a.id),
)
for a in accounts
}
detail_sort_allowed = {
'course', 'end_lessons', 'end_gift', 'end_total',
'end_balance', 'unit_price', 'refund_due',
}
sort, order = parse_sort(
request.args.get('sort'),
request.args.get('order'),
detail_sort_allowed,
default='',
)
if sort:
items = [
(a, account_ledger[a.id], refund_previews[a.id])
for a in accounts
]
items = sort_rows_in_memory(
items, sort, order, account_ledger_sort_keys(),
)
accounts = [x[0] for x in items]
class_relations = (
ClassStudent.query.options(
joinedload(ClassStudent.class_).joinedload(Class_.course),
joinedload(ClassStudent.class_).joinedload(Class_.teacher),
)
.filter_by(student_id=id)
.all()
)
refund_records = RefundRecord.query.filter_by(student_id=id).order_by(
RefundRecord.created_at.desc(),
).all()
stop_records = StopRecord.query.filter_by(student_id=id).order_by(
StopRecord.start_date.desc(),
).all()
recharge_records = RechargeRecord.query.filter_by(student_id=id).order_by(
RechargeRecord.created_at.desc()).all()
consumption_records = ConsumptionRecord.query.filter_by(student_id=id).order_by(
ConsumptionRecord.consume_date.desc()).all()
return render_template(
'student_detail.html',
student=student,
accounts=accounts,
account_ledger=account_ledger,
refund_previews=refund_previews,
class_relations=class_relations,
refund_records=refund_records,
stop_records=stop_records,
recharge_records=recharge_records,
consumption_records=consumption_records,
fmt_money=fmt_money,
fmt_hours=fmt_hours,
fmt_price=fmt_price,
ledger_period=f'{py}-{pm:02d}' if py and pm else '',
sort=sort,
order=order,
)
@app.route('/students/export', endpoint='student_export')
@login_required
@permission_required('student_export')
def student_export():
return export_students()