"""消课管理路由""" from datetime import date from flask import render_template, request, redirect, url_for, flash, session from utils import ( permission_required, export_excel, login_required, log_operation, parse_date, filter_students_query, teacher_can_access_class, teacher_can_access_student, ) from models import (db, Student, Course, Class_, ClassStudent, ConsumptionRecord) import business_rules as rules def register_routes(app): @app.route('/consumptions', endpoint='consumption_list') @login_required @permission_required('consumption_view') def consumption_list(): keyword = request.args.get('keyword', '') query = ConsumptionRecord.query if keyword: sq = filter_students_query(Student.query.filter(Student.name.contains(keyword))) student_ids = [s.id for s in sq.all()] if student_ids: query = query.filter(ConsumptionRecord.student_id.in_(student_ids)) else: query = query.filter(False) records = query.order_by(ConsumptionRecord.consume_date.desc()).all() return render_template('consumption_list.html', records=records, keyword=keyword) @app.route('/consumptions/add', methods=['GET', 'POST'], endpoint='consumption_add') @login_required @permission_required('consumption_add') def consumption_add(): if request.method == 'POST': student_id = int(request.form.get('student_id')) course_id = int(request.form.get('course_id')) if not teacher_can_access_student(student_id): flash('无权操作该学员', 'danger') return redirect(url_for('consumption_add')) class_id = request.form.get('class_id') or None if class_id: class_id = int(class_id) is_makeup = int(request.form.get('is_makeup', 0)) is_trial = int(request.form.get('is_trial', 0)) remark = request.form.get('remark', '') try: if is_trial: hours_consumed = 0 else: hours_consumed = rules.normalize_consumption_hours( request.form.get('hours_consumed', 1), ) except ValueError as e: flash(str(e), 'danger') return redirect(url_for('consumption_add')) consume_date = parse_date(request.form.get('consume_date')) or date.today() if is_trial: record = ConsumptionRecord( student_id=student_id, class_id=class_id, course_id=course_id, hours_consumed=0, consume_type='trial', normal_consumed=0, gifted_consumed=0, operator_id=session.get('user_id'), consume_date=consume_date, is_makeup=is_makeup, is_trial=1, remark=remark or '试听课', ) db.session.add(record) db.session.commit() log_operation('试听消课', f'学员ID:{student_id} 试听课(不扣课)') flash('试听课消课成功(不扣课时)', 'success') return redirect(url_for('consumption_list')) account = rules.lock_student_account(student_id, course_id) if not account: flash('未找到该学员的课时账户', 'danger') return redirect(url_for('consumption_add')) if account.is_stopped: flash('该学员正在停课保号中,无法消课', 'danger') return redirect(url_for('consumption_add')) before = rules.account_snapshot_dict(account) course = db.session.get(Course, course_id) unit_at = rules.account_effective_unit_price(account, course) try: normal_consumed, gifted_consumed, consume_type = rules.deduct_hours( account, hours_consumed, ) except ValueError as e: db.session.rollback() flash(str(e), 'danger') return redirect(url_for('consumption_add')) record = ConsumptionRecord( student_id=student_id, class_id=class_id, course_id=course_id, hours_consumed=hours_consumed, consume_type=consume_type, normal_consumed=normal_consumed, gifted_consumed=gifted_consumed, unit_price_at_consume=unit_at, operator_id=session.get('user_id'), consume_date=consume_date, is_makeup=is_makeup, is_trial=is_trial, remark=remark, ) db.session.add(record) rules.sync_account_snapshots(account, consume_date) log_operation( '消课', f'学员ID:{student_id} 消课{hours_consumed}课时 归属{consume_date}', before=before, after=rules.account_snapshot_dict(account), commit=False, ) db.session.commit() flash('消课成功', 'success') return redirect(url_for('consumption_list')) students = filter_students_query(Student.query.filter_by(status=1)).all() courses = Course.query.filter_by(status=1).all() classes = Class_.query.filter_by(status=1).all() return render_template( 'consumption_form.html', students=students, courses=courses, classes=classes, batch=False, ) @app.route('/consumptions/batch', methods=['GET', 'POST'], endpoint='consumption_batch') @login_required @permission_required('consumption_add') def consumption_batch(): if request.method == 'POST': class_id = int(request.form.get('class_id')) course_id = int(request.form.get('course_id')) cls = db.session.get(Class_, class_id) if not cls: flash('班级不存在', 'danger') return redirect(url_for('consumption_batch')) if not teacher_can_access_class(class_id): flash('无权对该班级批量消课', 'danger') return redirect(url_for('consumption_batch')) if int(cls.course_id) != course_id: flash('所选课程必须与班级绑定课程一致', 'danger') return redirect(url_for('consumption_batch')) try: hours_consumed = rules.normalize_consumption_hours( request.form.get('hours_consumed', 1), ) except ValueError as e: flash(str(e), 'danger') return redirect(url_for('consumption_batch')) consume_date = parse_date(request.form.get('consume_date')) or date.today() remark = request.form.get('remark', '') class_students = ClassStudent.query.filter_by(class_id=class_id, status=1).all() count = 0 skipped = [] for cs in class_students: account = rules.lock_student_account(cs.student_id, course_id) if not account or account.is_stopped: student = db.session.get(Student, cs.student_id) skipped.append(f'{student.name if student else cs.student_id}(无账户或停课)') continue try: normal_consumed, gifted_consumed, consume_type = rules.deduct_hours( account, hours_consumed, ) except ValueError as e: student = db.session.get(Student, cs.student_id) skipped.append(f'{student.name if student else cs.student_id}({e})') continue course = db.session.get(Course, course_id) unit_at = rules.account_effective_unit_price(account, course) record = ConsumptionRecord( student_id=cs.student_id, class_id=class_id, course_id=course_id, hours_consumed=hours_consumed, consume_type=consume_type, normal_consumed=normal_consumed, gifted_consumed=gifted_consumed, unit_price_at_consume=unit_at, operator_id=session.get('user_id'), consume_date=consume_date, remark=remark, ) db.session.add(record) rules.sync_account_snapshots(account, consume_date) count += 1 log_operation( '批量消课', f'班级ID:{class_id} 成功{count}人 跳过{len(skipped)}人', commit=False, ) db.session.commit() flash(f'批量消课完成:成功 {count} 人', 'success') if skipped: flash('未消课学员:' + ';'.join(skipped[:10]) + ( f'…等{len(skipped)}人' if len(skipped) > 10 else '' ), 'warning') return redirect(url_for('consumption_list')) from utils import filter_classes_query classes = filter_classes_query(Class_.query.filter_by(status=1)).all() courses = Course.query.filter_by(status=1).all() return render_template( 'consumption_form.html', classes=classes, courses=courses, batch=True, ) @app.route('/consumptions/export', endpoint='consumption_export') @login_required @permission_required('consumption_export') def consumption_export(): query = ConsumptionRecord.query student_name = request.args.get('student_name', '').strip() if student_name: query = query.join(Student).filter(Student.name.contains(student_name)) start_date = request.args.get('start_date', '') if start_date: query = query.filter(ConsumptionRecord.consume_date >= start_date) end_date = request.args.get('end_date', '') if end_date: query = query.filter(ConsumptionRecord.consume_date <= end_date) records = query.order_by(ConsumptionRecord.consume_date.desc()).all() headers = ['序号', '学员', '课程', '消耗课时', '消费日期', '类型', '补课', '备注', '操作时间'] rows = [] for i, r in enumerate(records, 1): student = db.session.get(Student, r.student_id) course = db.session.get(Course, r.course_id) consume_type = '试听' if r.is_trial else ('混合' if r.consume_type == 'mixed' else '普通') makeup = '是' if r.is_makeup else '否' rows.append([ i, student.name if student else '', course.name if course else '', r.hours_consumed, str(r.consume_date), consume_type, makeup, r.remark or '', r.created_at.strftime('%Y-%m-%d %H:%M') if r.created_at else '', ]) return export_excel('消课记录', headers, rows)