Files
drl_2/xuexiao/routes/consumption.py
user9994793890 e8b980539a feat: 消课添加页面添加学员搜索功能
Coze-Commit-Type: user
Coze-User-ID: 3722323274763196
Coze-Conversation-ID: 5260473
2026-05-29 11:16:16 +08:00

372 lines
16 KiB
Python
Raw Permalink 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, Teacher, ConsumptionRecord)
import business_rules as rules
def register_routes(app):
@app.route('/api/students/search', endpoint='api_student_search')
@login_required
def api_student_search():
"""学员搜索API"""
keyword = request.args.get('q', '')
if len(keyword) < 1:
return {'data': []}
# 直接搜索学员,不受权限限制
students = Student.query.filter(
Student.name.contains(keyword),
Student.status == 1
).limit(20).all()
results = [{'id': s.id, 'name': s.name} for s in students]
return {'data': results}
@app.route('/consumptions', endpoint='consumption_list')
@login_required
@permission_required('consumption_view')
def consumption_list():
keyword = request.args.get('keyword', '')
class_id = request.args.get('class_id', '')
teacher_id = request.args.get('teacher_id', '')
start_date = request.args.get('start_date', '')
end_date = request.args.get('end_date', '')
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)
# 按班级筛选
if class_id:
class_students = ClassStudent.query.filter_by(class_id=int(class_id), status=1).all()
student_ids = [cs.student_id for cs in class_students]
if student_ids:
query = query.filter(ConsumptionRecord.student_id.in_(student_ids))
else:
query = query.filter(False)
# 按老师筛选
if teacher_id:
teacher_classes = Class_.query.filter_by(teacher_id=int(teacher_id), status=1).all()
class_ids = [c.id for c in teacher_classes]
class_students = ClassStudent.query.filter(
ClassStudent.class_id.in_(class_ids),
ClassStudent.status == 1
).all()
student_ids = [cs.student_id for cs in class_students]
if student_ids:
query = query.filter(ConsumptionRecord.student_id.in_(student_ids))
else:
query = query.filter(False)
# 按日期范围筛选
if start_date:
query = query.filter(ConsumptionRecord.consume_date >= start_date)
if end_date:
query = query.filter(ConsumptionRecord.consume_date <= end_date)
records = query.order_by(ConsumptionRecord.consume_date.desc()).all()
# 获取筛选选项
classes = Class_.query.filter_by(status=1).all()
teachers = Teacher.query.filter_by(status=1).all()
return render_template('consumption_list.html', records=records, keyword=keyword,
class_id=class_id, teacher_id=teacher_id,
start_date=start_date, end_date=end_date,
classes=classes, teachers=teachers)
@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)
@app.route('/api/class_students/<int:class_id>')
@login_required
def api_class_students(class_id):
"""获取班级学员列表"""
class_students = ClassStudent.query.filter_by(class_id=class_id, status=1).all()
student_ids = [cs.student_id for cs in class_students]
students = Student.query.filter(Student.id.in_(student_ids), Student.status == 1).all()
return {'success': True, 'data': [{'id': s.id, 'name': s.name} for s in students]}
@app.route('/consumptions/manual', methods=['POST'], endpoint='consumption_manual_add')
@login_required
@permission_required('consumption_add')
def consumption_manual_add():
"""手动录入消课汇总"""
consume_date = parse_date(request.form.get('consume_date')) or date.today()
student_id = int(request.form.get('student_id'))
class_id = request.form.get('class_id')
if class_id:
class_id = int(class_id)
hours = float(request.form.get('hours', 2))
consume_type = request.form.get('consume_type', 'normal')
remark = request.form.get('remark', '')
# 获取班级关联的课程
cls = db.session.get(Class_, class_id) if class_id else None
course_id = cls.course_id if cls else None
if not course_id:
flash('请选择有效的班级', 'danger')
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_list'))
if account.is_stopped:
flash('该学员正在停课保号中,无法消课', 'danger')
return redirect(url_for('consumption_list'))
try:
normal_consumed, gifted_consumed, actual_type = rules.deduct_hours(
account, hours,
)
except ValueError as e:
flash(str(e), 'danger')
return redirect(url_for('consumption_list'))
course = db.session.get(Course, course_id)
unit_at = rules.account_effective_unit_price(account, course)
record = ConsumptionRecord(
student_id=student_id,
class_id=class_id,
course_id=course_id,
hours_consumed=hours,
consume_type=actual_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=0,
is_trial=0,
remark='手动录入: ' + remark if remark else '手动录入',
)
db.session.add(record)
rules.sync_account_snapshots(account, consume_date)
log_operation(
'手动消课录入',
f'学员ID:{student_id} 手动消课{hours}课时 归属{consume_date}',
)
db.session.commit()
flash('手动录入消课成功', 'success')
return redirect(url_for('consumption_list'))