327 lines
15 KiB
Python
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
|