"""充值管理、充值优惠活动、API路由""" from datetime import datetime from flask import render_template, request, redirect, url_for, flash, session, jsonify from models import (db, Student, Course, StudentAccount, RechargeActivity, RechargeRecord) from utils import ( login_required, log_operation, permission_required, export_excel, filter_students_query, is_teacher_role, ) import business_rules as rules def register_routes(app): @app.route('/recharge_activities', endpoint='recharge_activity_list') @login_required @permission_required('recharge_activity_view') def recharge_activity_list(): keyword = request.args.get('keyword', '') query = RechargeActivity.query if keyword: query = query.filter(RechargeActivity.name.contains(keyword)) activities = query.order_by(RechargeActivity.id.desc()).all() return render_template('recharge_activity_list.html', activities=activities, keyword=keyword) @app.route('/recharge_activities/add', methods=['GET', 'POST'], endpoint='recharge_activity_add') @login_required @permission_required('recharge_activity_add') def recharge_activity_add(): if request.method == 'POST': defaults = rules.recharge_activity_defaults() activity = RechargeActivity( name=request.form.get('name'), trigger_amount=float(request.form.get('trigger_amount', 0)), gifted_hours=float(request.form.get('gifted_hours', 0)), gift_type=defaults['gift_type'], is_cumulative=defaults['is_cumulative'], cumulative_window=defaults['cumulative_window'], cumulative_clear=defaults['cumulative_clear'], allow_multi_trigger=defaults['allow_multi_trigger'], is_time_limited=int(request.form.get('is_time_limited', 0)), start_date=_parse_date(request.form.get('start_date')), end_date=_parse_date(request.form.get('end_date')), target_audience=defaults['target_audience'], can_stack=int(request.form.get('can_stack', 1)), gift_cap=defaults['gift_cap'], group_discount=defaults['group_discount'], group_min_people=defaults['group_min_people'], group_extra_hours=defaults['group_extra_hours'], referral_reward=defaults['referral_reward'], referral_reward_hours=defaults['referral_reward_hours'], reward_timing='immediate', status=1, ) db.session.add(activity) db.session.commit() log_operation('创建充值活动', f'活动:{activity.name}') flash('充值活动创建成功', 'success') return redirect(url_for('recharge_activity_list')) return render_template('recharge_activity_form.html', activity=None) @app.route('/recharge_activities/edit/', methods=['GET', 'POST'], endpoint='recharge_activity_edit') @login_required @permission_required('recharge_activity_edit') def recharge_activity_edit(id): activity = db.session.get(RechargeActivity, id) if request.method == 'POST': activity.name = request.form.get('name') activity.trigger_amount = float(request.form.get('trigger_amount', 0)) activity.gifted_hours = float(request.form.get('gifted_hours', 0)) activity.is_time_limited = int(request.form.get('is_time_limited', 0)) activity.start_date = _parse_date(request.form.get('start_date')) activity.end_date = _parse_date(request.form.get('end_date')) activity.can_stack = int(request.form.get('can_stack', 1)) activity.status = int(request.form.get('status', 1)) db.session.commit() log_operation('编辑充值活动', f'活动ID:{id}') flash('充值活动更新成功', 'success') return redirect(url_for('recharge_activity_list')) return render_template('recharge_activity_form.html', activity=activity) @app.route('/recharge_activities/delete/', endpoint='recharge_activity_delete') @login_required @permission_required('recharge_activity_edit') def recharge_activity_delete(id): activity = db.session.get(RechargeActivity, id) if activity: activity.status = 0 db.session.commit() log_operation('停用充值活动', f'活动ID:{id}') flash('充值活动已停用', 'success') return redirect(url_for('recharge_activity_list')) @app.route('/recharges', endpoint='recharge_list') @login_required @permission_required('recharge_view') def recharge_list(): keyword = request.args.get('keyword', '') query = RechargeRecord.query if keyword: students = filter_students_query( Student.query.filter(Student.name.contains(keyword)), ).all() student_ids = [s.id for s in students] query = query.filter(RechargeRecord.student_id.in_(student_ids)) if student_ids else query.filter(False) if is_teacher_role(): allowed = {s.id for s in filter_students_query(Student.query).all()} records = [ r for r in query.order_by(RechargeRecord.created_at.desc()).all() if r.student_id in allowed ] else: records = query.order_by(RechargeRecord.created_at.desc()).all() return render_template('recharge_list.html', records=records, keyword=keyword) @app.route('/recharges/add', methods=['GET', 'POST'], endpoint='recharge_add') @login_required @permission_required('recharge_add') def recharge_add(): if request.method == 'POST': student_id = int(request.form.get('student_id')) course_id = int(request.form.get('course_id')) amount = float(request.form.get('amount', 0)) if amount <= 0: flash('充值金额必须大于 0', 'danger') return redirect(url_for('recharge_add')) payment_method = request.form.get('payment_method', '微信') remark = request.form.get('remark', '') course = db.session.get(Course, course_id) normal_hours = rules.calc_normal_hours_from_amount( amount, course.price_per_hour if course else 0 ) gifted_hours, activity_id = rules.match_recharge_activities( student_id, course_id, amount ) record = RechargeRecord( student_id=student_id, course_id=course_id, amount=amount, activity_id=activity_id, normal_hours=normal_hours, gifted_hours=gifted_hours, payment_method=payment_method, operator_id=session.get('user_id'), remark=remark, ) db.session.add(record) account = rules.lock_student_account(student_id, course_id) is_new = not account if is_new: account = StudentAccount(student_id=student_id, course_id=course_id) db.session.add(account) db.session.flush() before = rules.account_snapshot_dict(account) rules.apply_recharge_account( account, amount, normal_hours, gifted_hours, is_new_account=is_new, ) db.session.commit() log_operation( '充值', f'学员ID:{student_id} 充值{amount}元(正课{normal_hours}+赠课{int(gifted_hours)})', before=before, after=rules.account_snapshot_dict(account), ) flash(f'充值成功!正课{normal_hours}课时,赠课{int(gifted_hours)}课时', 'success') return redirect(url_for('recharge_list')) students = filter_students_query(Student.query.filter_by(status=1)).all() courses = Course.query.filter_by(status=1).all() return render_template('recharge_form.html', students=students, courses=courses) @app.route('/referral_reward', methods=['GET', 'POST'], endpoint='referral_reward') @login_required @permission_required('recharge_add') def referral_reward(): """§一 1.5 老带新奖励(管理员手动发放)""" if request.method == 'POST': referrer_id = int(request.form.get('referrer_id')) student_id = int(request.form.get('student_id')) course_id = int(request.form.get('course_id')) reward_hours = rules.normalize_consumption_hours( request.form.get('reward_hours', 1) ) remark = request.form.get('remark', '老带新奖励') account = StudentAccount.query.filter_by( student_id=referrer_id, course_id=course_id ).first() if not account: account = StudentAccount(student_id=referrer_id, course_id=course_id) db.session.add(account) db.session.flush() account.gifted_hours = float(account.gifted_hours or 0) + reward_hours db.session.commit() log_operation( '老带新奖励', f'推荐人学员ID:{referrer_id} 获赠{reward_hours}课时 新学员ID:{student_id}', ) flash(f'已向推荐人发放 {reward_hours} 课时赠课', 'success') return redirect(url_for('referral_reward')) students = Student.query.filter_by(status=1).all() courses = Course.query.filter_by(status=1).all() return render_template( 'referral_reward.html', students=students, courses=courses ) @app.route('/api/student_account//') @login_required @permission_required('recharge_view') def api_student_account(student_id, course_id): account = StudentAccount.query.filter_by( student_id=student_id, course_id=course_id ).first() if account: nh = int(float(account.normal_hours or 0)) gh = int(float(account.gifted_hours or 0)) return jsonify({ 'success': True, 'data': { 'normal_hours': nh, 'gifted_hours': gh, 'total_hours': nh + gh, 'consumed_normal': float(account.consumed_normal or 0), 'consumed_gifted': float(account.consumed_gifted or 0), 'cumulative_amount': float(account.cumulative_amount or 0), 'is_stopped': bool(account.is_stopped), }, }) return jsonify({'success': False, 'message': '未找到账户'}) @app.route('/api/student_accounts/') @login_required @permission_required('recharge_view') def api_student_accounts(student_id): accounts = StudentAccount.query.filter_by(student_id=student_id).all() result = [] for a in accounts: course = db.session.get(Course, a.course_id) nh = int(float(a.normal_hours or 0)) gh = int(float(a.gifted_hours or 0)) result.append({ 'course_id': a.course_id, 'course_name': course.name if course else '', 'normal_hours': nh, 'gifted_hours': gh, 'cumulative_amount': float(a.cumulative_amount or 0), 'is_stopped': bool(a.is_stopped), }) return jsonify({'success': True, 'data': result}) @app.route('/api/calculate_recharge', methods=['POST']) @login_required @permission_required('recharge_view') def api_calculate_recharge(): data = request.get_json() student_id = data.get('student_id') course_id = data.get('course_id') amount = float(data.get('amount', 0)) course = db.session.get(Course, course_id) normal_hours = rules.calc_normal_hours_from_amount( amount, course.price_per_hour if course else 0 ) gifted_hours, _ = rules.match_recharge_activities(student_id, course_id, amount) return jsonify({ 'success': True, 'data': { 'normal_hours': normal_hours, 'gifted_hours': int(gifted_hours), 'total_hours': normal_hours + int(gifted_hours), }, }) @app.route('/recharge_activities/export', endpoint='recharge_activity_export') @login_required @permission_required('recharge_activity_export') def recharge_activity_export(): activities = RechargeActivity.query.order_by(RechargeActivity.id.desc()).all() headers = ['序号', '活动名称', '触发金额', '赠送课时', '可叠加', '期限活动', '开始日期', '结束日期', '状态', '备注'] rows = [] for i, a in enumerate(activities, 1): rows.append([ i, a.name, a.trigger_amount, a.gifted_hours, '是' if a.can_stack else '否', '是' if a.is_time_limited else '否', str(a.start_date) if a.start_date else '', str(a.end_date) if a.end_date else '', '启用' if a.status == 1 else '禁用', '', ]) return export_excel('充值优惠活动', headers, rows) @app.route('/recharges/export', endpoint='recharge_export') @login_required @permission_required('recharge_export') def recharge_export(): from models import User records = RechargeRecord.query.order_by(RechargeRecord.id.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) operator = db.session.get(User, r.operator_id) if r.operator_id else None rows.append([ i, student.name if student else '', course.name if course else '', r.amount or 0, r.normal_hours or 0, r.gifted_hours or 0, r.payment_method or '', operator.real_name if operator else '', r.remark or '', r.created_at.strftime('%Y-%m-%d %H:%M') if r.created_at else '', ]) return export_excel('充值记录', headers, rows) def _parse_date(date_str): if date_str: try: return datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): return None return None