"""学员管理路由""" 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/', 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/', 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/', 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()