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

327 lines
15 KiB
Python

"""充值管理、充值优惠活动、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/<int:id>', 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/<int:id>', 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/<int:student_id>/<int:course_id>')
@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/<int:student_id>')
@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