Files
drl_2/xuexiao/routes/consumption.py
user9994793890 ee860ce0ae Initial commit
2026-05-29 10:28:07 +08:00

234 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""消课管理路由"""
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)