Initial commit
This commit is contained in:
248
xuexiao/routes/student.py
Normal file
248
xuexiao/routes/student.py
Normal 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()
|
||||
Reference in New Issue
Block a user