249 lines
9.3 KiB
Python
249 lines
9.3 KiB
Python
"""学员管理路由"""
|
|
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()
|