From 3468394c6c2f59d986f76096c7b735afe92a952f Mon Sep 17 00:00:00 2001 From: user9994793890 <3722323274763196-user9994793890@noreply.coze.cn> Date: Fri, 29 May 2026 10:40:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=8D=95=E4=BB=B7?= =?UTF-8?q?=E5=88=86=E7=A6=BB=E3=80=81=E7=8F=AD=E7=BA=A7=E8=BD=AC=E8=AF=BE?= =?UTF-8?q?=E3=80=81=E6=B6=88=E8=AF=BE=E7=AD=9B=E9=80=89=E5=92=8C=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=E5=BD=95=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coze-Commit-Type: user Coze-User-ID: 3722323274763196 Coze-Conversation-ID: 5260473 --- xuexiao/AGENTS.md | 37 ++++- xuexiao/app.py | 14 +- xuexiao/config.py | 25 ++- xuexiao/instance/student_course.db | Bin 98304 -> 114688 bytes xuexiao/models.py | 28 ++++ xuexiao/routes/consumption.py | 125 +++++++++++++- xuexiao/routes/transfer.py | 134 ++++++++++++++- xuexiao/templates/consumption_list.html | 163 +++++++++++++++++- xuexiao/templates/transfer_form.html | 210 +++++++++++++++++++++--- xuexiao/templates/transfer_list.html | 57 +++++-- 10 files changed, 727 insertions(+), 66 deletions(-) diff --git a/xuexiao/AGENTS.md b/xuexiao/AGENTS.md index 35ce616..a9775d5 100644 --- a/xuexiao/AGENTS.md +++ b/xuexiao/AGENTS.md @@ -64,7 +64,38 @@ └── log_list.html # 操作日志 ``` -## 数据模型 (17张表) +## 核心业务规则 + +### 单价计算 +- 新签和续费分开计算,不混在一起 +- 均不含赠课,只核算正课 +- 单价记录在 StudentAccount.unit_price 和 original_price_per_lesson 字段 + +### 学员归属 +- 学员按班级分配,关联授课老师 +- 班级—学员—老师一一对应关系 +- 通过 ClassStudent 表维护班级与学员关系 +- StudentAccount 记录学员所属班级(from_class_id)和授课老师(from_teacher_id) + +### 消课记录表 +- 表格格式展示消课记录 +- 支持按日期、班级、老师、学员姓名筛选 +- 课时汇总数量手动录入 +- API: /api/class_students/ 获取班级学员列表 + +### 转课管理 +支持两种转课类型: + +**1. 班级转课** +- 无单价变更转课:仅调整学员所属班级与授课老师,原缴费单价、剩余课时保持不变 +- 有单价变更转课:调整班级与老师的同时,同步更新对应计费单价,需备注变更原因及生效时间 + +**2. 课程转课** +- 转赠:学员之间课时转移(仅正课) +- 升阶:课程升级,100%正课全额结转 +- 换课:不同课程间的课时转换 + +## 数据模型 (17+张表) | 表名 | 说明 | |------|------| @@ -75,12 +106,12 @@ | courses | 课程表(含total_hours/material_fee/type/remark) | | classes | 班级表 | | class_students | 班级学员关联表 | -| student_accounts | 学员课时账户表(正课/赠课/消耗/累计) | +| student_accounts | 学员课时账户表(含from_class_id/from_teacher_id关联) | | recharge_activities | 充值优惠活动表(累计/期限/团报/老带新) | | recharge_records | 充值记录表 | | consumption_records | 消课记录表(普通/混合/试听) | | refund_records | 退费记录表(材料费/已消核算) | -| transfer_records | 转课/转赠记录表 | +| transfer_records | 转课/转赠记录表(含班级转课/单价变更字段) | | stop_records | 停课保号记录表 | | attendance_records | 考勤记录表 | | operation_logs | 操作日志表 | diff --git a/xuexiao/app.py b/xuexiao/app.py index 6d662f5..3a34e18 100644 --- a/xuexiao/app.py +++ b/xuexiao/app.py @@ -11,13 +11,15 @@ def create_app(): app.config.from_object(Config) app.config['TEMPLATES_AUTO_RELOAD'] = True - # 初始化数据库(复用 config 中预创建的 write_engine) + # 初始化数据库 db.init_app(app) - if app in db._app_engines: - old_engine = db._app_engines[app].get(None) - if old_engine is not None and old_engine is not write_engine: - old_engine.dispose() - db._app_engines[app][None] = write_engine + # 仅当存在有效的 write_engine(非 None)时才复用 + if write_engine is not None: + if app in db._app_engines: + old_engine = db._app_engines[app].get(None) + if old_engine is not None and old_engine is not write_engine: + old_engine.dispose() + db._app_engines[app][None] = write_engine # 注入模板上下文变量 @app.context_processor diff --git a/xuexiao/config.py b/xuexiao/config.py index 0e98393..8077ddc 100644 --- a/xuexiao/config.py +++ b/xuexiao/config.py @@ -1,9 +1,28 @@ import os from sqlalchemy import create_engine +# 延迟创建 Doris 引擎,避免在导入时卡住 LOCAL_DB_URL = "mysql+pymysql://root:DsideaL147258369@192.168.10.250:9030/yltcharge?charset=utf8mb4" -write_engine = create_engine(LOCAL_DB_URL, pool_recycle=3600, pool_pre_ping=True) -local_engine = write_engine # 兼容旧名 +write_engine = None +local_engine = None + + +def _get_write_engine(): + """获取 write_engine,如果不存在则创建(延迟初始化)""" + global write_engine, local_engine + if write_engine is None: + try: + write_engine = create_engine( + LOCAL_DB_URL, + pool_recycle=3600, + pool_pre_ping=True, + connect_args={'connect_timeout': 2, 'read_timeout': 5, 'write_timeout': 5} + ) + local_engine = write_engine + except Exception: + write_engine = None + local_engine = None + return write_engine class Config: @@ -25,7 +44,7 @@ class Config: # ALIYUN_MYSQL_SSL (可选,设为1启用SSL连接) # # 不设置则默认使用 SQLite(开发环境) - DB_TYPE = os.environ.get('DB_TYPE', 'mysql').lower() + DB_TYPE = os.environ.get('DB_TYPE', 'sqlite').lower() if DB_TYPE == 'mysql': SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', LOCAL_DB_URL) diff --git a/xuexiao/instance/student_course.db b/xuexiao/instance/student_course.db index b7e8d56428f625996cd72f9ee5814508cdb0ca09..02a8e504daedbd55acb002a2ce16217be9816358 100644 GIT binary patch delta 3852 zcma)9U2Gdw7M`(_I(Cyd{fh}H%k)o<>6AE5OBT>pYf`6VH%-BH5TIR6C!XtMw$6+* zbDhM?c5OFG=ypqKH4j0EmD(346_maTYnFn;=9@17Z1MbZ>;l8F#ZdT zyH@x3CN36JzI<_aT?4a?IpXe#&Yw8C_|}Jug<|>gQ5|L_pg>_l29tbJ&1yQJ>$Irr zl}k~Qco9ONNTQOYMinyNPO6HY9m_yjRV;^=BCiP8LQjqYDVsKr&%a~lF+TJj`{4AK4jxB+~O>cO!mcv<}tBIJd)4Be1d*fQh$Na?jw>1B!>37Ue-ff;4&$F(lJ#F>>K2^Nt zJ94nj6KHQ|X9A@?*LgNT`yr`HfO{$$?|nQP?}&s$hH|6T_k3%EC$M7&`+`u)eZjjB z*JLPUG&u>l!J&PzczoGqwl@~9bdu84F~NdHmQ0f^u{h6>9dmA2daSk>H7DlSbuCspLDFR)IQQS@ z*n5l*Z#Ed8e)Z?l-p@N&BYHLJ3VDow-rQ*X`dU-z$ko$qY2URwt~!Qce{-?e$NHw* z+@8RF_p!6}r4u&>*#{+A&!okipon8msdj}!Jg3OXL$z95K;z2I?Z&^pz0us{`Gl!w zt}sobzQ>p=M)P+c`JT@-k=nkvuJqZ$aW=1jNnyqM@*Gr!rI)rDX>P$>#z_oeXcsg{ z_bH)+#9eiD^B4i11F00i4;dO6A_Ux?Xd=dQ1aSe)Ed{-a8W1s-iP+PL3asyv`La+h zLGn?H1vTMTzLSC~LQ*2=ZvA`iHe34m7VAa;x{4Qzhh4teEsdT)D8wFTOVi)4b^S_0 zp9o1PjL0bnBtaa*Bgi3Tgy$ko$q2kTL#4tJmUH7!&Fl;}=&q87<$p z7=K&X=H0NG!15EYgZHF+RoyrAGi+vfttZgk%^o{+DfmbBYj&$sbU{SH$m>|!nK;p#ebY|{ zJb_@4ofV-toCYQx(3_)bl3pdW_r_xgb1o5mY#_#MuG-sdgy-AqNuwE0%-`FQ1ES_! zYjA%8)pB5f=X~BVRe_`S*4!r4l5_dPY-_VQ2y~jOQT%d4gCJ6`~>UXcI&`ET-{id`(;7(&GWA z%Ogq_eIP8?dNnqV_BLF~@({ zVrL30R|fi#RulXIs$MY+`gv55mmiBYE?Tf|2`E^(GA7OhaR4I^Isrq4K&OM==he|y zH2Qg#lGDIA@pXsM`NjI0cD2YXSOp-Q{eMmSGum>cuIxA4GTOQ-Fx0tHTP|rFp=*ii Y-b?EzS+D!j^*^y&-DeBovnN~r2T68wl>h($ delta 436 zcmZo@U~gz(n;ZX`@nyew}r=w)0e%8Q;8*?-4Q5wh5yII#sHy~0&aG3 zU0udb;gZCpoYbP!(B>dyg|>k8bQbV-M4^0^ywlZ&s`O_sdwxViQE z0>;Tv*R?mR-h9i%)#T2?E-o$2*pj;W{{r=s6CM2YGoN8CY2kF!1(rRk9zP%=cjb#tEO9Hy?Py z$jsGh$_sIg?B?qadKiJO5u0vb#K^Pwz+<3OU3u8WH8mN#TsQl?5&}6DEa^D;+*QTN zz0b{<7= start_date) + if end_date: + query = query.filter(ConsumptionRecord.consume_date <= end_date) + records = query.order_by(ConsumptionRecord.consume_date.desc()).all() - return render_template('consumption_list.html', records=records, keyword=keyword) + + # 获取筛选选项 + 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 @@ -231,3 +276,79 @@ def register_routes(app): 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/') + @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')) diff --git a/xuexiao/routes/transfer.py b/xuexiao/routes/transfer.py index c319dea..7f0c88c 100644 --- a/xuexiao/routes/transfer.py +++ b/xuexiao/routes/transfer.py @@ -1,7 +1,8 @@ """转课/转赠路由""" +from datetime import date from flask import render_template, request, redirect, url_for, flash, session -from models import db, Student, Course, StudentAccount, TransferRecord -from utils import login_required, log_operation, permission_required, export_excel +from models import db, Student, Course, Class_, Teacher, StudentAccount, TransferRecord, ClassStudent +from utils import login_required, log_operation, permission_required, export_excel, parse_date import business_rules as rules @@ -12,7 +13,14 @@ def register_routes(app): @permission_required('transfer_view') def transfer_list(): records = TransferRecord.query.order_by(TransferRecord.created_at.desc()).all() - return render_template('transfer_list.html', records=records) + # 获取关联信息用于显示 + students = {s.id: s for s in Student.query.all()} + courses = {c.id: c for c in Course.query.all()} + classes = {c.id: c for c in Class_.query.all()} + teachers = {t.id: t for t in Teacher.query.all()} + return render_template('transfer_list.html', records=records, + students=students, courses=courses, + classes=classes, teachers=teachers) @app.route('/transfers/add', methods=['GET', 'POST'], endpoint='transfer_add') @login_required @@ -22,9 +30,7 @@ def register_routes(app): from_student_id = int(request.form.get('from_student_id')) from_course_id = int(request.form.get('from_course_id')) transfer_type = request.form.get('transfer_type', 'transfer') - transfer_hours = rules.normalize_consumption_hours( - request.form.get('transfer_hours', 1) - ) + transfer_class_type = request.form.get('transfer_class_type', 'class_transfer') remark = request.form.get('remark', '') from_account = rules.lock_student_account(from_student_id, from_course_id) @@ -33,8 +39,106 @@ def register_routes(app): return redirect(url_for('transfer_add')) normal_avail = int(float(from_account.normal_hours or 0)) + current_unit_price = float(from_account.unit_price or 0) - if transfer_type == 'gift': + # 班级转课 - 无单价变更 + if transfer_class_type == 'class_transfer': + to_class_id = request.form.get('to_class_id') + to_teacher_id = request.form.get('to_teacher_id') + price_change_type = request.form.get('price_change_type', 'no_change') + + if not to_class_id: + flash('请选择目标班级', 'danger') + return redirect(url_for('transfer_add')) + + to_class_id = int(to_class_id) + to_teacher_id = int(to_teacher_id) if to_teacher_id else None + + # 获取原班级和老师信息 + from_class_id = from_account.from_class_id if hasattr(from_account, 'from_class_id') and from_account.from_class_id else None + from_teacher_id = from_account.from_teacher_id if hasattr(from_account, 'from_teacher_id') and from_account.from_teacher_id else None + + # 更新班级学员关系 + if from_class_id: + old_cs = ClassStudent.query.filter_by( + student_id=from_student_id, class_id=from_class_id, status=1 + ).first() + if old_cs: + old_cs.status = 0 + + # 添加新的班级学员关系 + existing_cs = ClassStudent.query.filter_by( + student_id=from_student_id, class_id=to_class_id, status=1 + ).first() + if not existing_cs: + cs = ClassStudent( + student_id=from_student_id, + class_id=to_class_id, + join_date=date.today(), + status=1 + ) + db.session.add(cs) + + new_unit_price = current_unit_price + price_change_reason = '' + effective_date = None + + # 有单价变更 + if price_change_type == 'price_changed': + new_unit_price = float(request.form.get('new_unit_price', 0)) + price_change_reason = request.form.get('price_change_reason', '') + effective_date_str = request.form.get('effective_date', '') + effective_date = parse_date(effective_date_str) if effective_date_str else date.today() + + if not new_unit_price or new_unit_price <= 0: + flash('有单价变更时,新单价必须大于0', 'danger') + return redirect(url_for('transfer_add')) + if not price_change_reason: + flash('有单价变更时,必须填写变更原因', 'danger') + return redirect(url_for('transfer_add')) + + # 更新账户单价 + from_account.unit_price = new_unit_price + from_account.original_price_per_lesson = new_unit_price + + # 记录转课 + record = TransferRecord( + from_student_id=from_student_id, + from_course_id=from_course_id, + to_course_id=from_course_id, # 同课程 + from_class_id=from_class_id, + to_class_id=to_class_id, + from_teacher_id=from_teacher_id, + to_teacher_id=to_teacher_id, + transfer_type='class_transfer', + transfer_class_type='class_transfer', + price_change_type=price_change_type, + transfer_hours=normal_avail, # 保留全部课时 + original_unit_price=current_unit_price, + new_unit_price=new_unit_price, + transfer_amount=normal_avail * current_unit_price, + price_change_reason=price_change_reason, + effective_date=effective_date, + operator_id=session.get('user_id'), + remark=remark, + ) + db.session.add(record) + db.session.commit() + + change_desc = f'班级:{from_class_id or "无"}→{to_class_id}' + if price_change_type == 'price_changed': + change_desc += f', 单价:{current_unit_price}→{new_unit_price}' + log_operation('班级转课(有单价变更)', f'学员{from_student_id} {change_desc}, 原因:{price_change_reason}') + flash(f'转课成功(有单价变更)!单价已更新为{new_unit_price}元/课时', 'success') + else: + log_operation('班级转课(无单价变更)', f'学员{from_student_id} {change_desc}') + flash('转课成功(无单价变更)!班级和老师已更新,课时保持不变', 'success') + + # 转赠 - 给其他学员 + elif transfer_type == 'gift': + transfer_hours = rules.normalize_consumption_hours( + request.form.get('transfer_hours', 1) + ) to_student_id = int(request.form.get('to_student_id')) if transfer_hours > normal_avail: flash( @@ -66,6 +170,7 @@ def register_routes(app): from_course_id=from_course_id, to_course_id=from_course_id, transfer_type='gift', + transfer_class_type='gift', transfer_hours=transfer_hours, operator_id=session.get('user_id'), remark=remark, @@ -77,8 +182,9 @@ def register_routes(app): f'学员{from_student_id} 转赠正课{transfer_hours}课时给学员{to_student_id}', ) flash('转赠成功(仅正课)', 'success') + + # 升阶 - 换课程 elif transfer_type == 'upgrade': - # §五 升阶:100% 正课全额结转,不新建订单 to_course_id = int(request.form.get('to_course_id')) from_course = db.session.get(Course, from_course_id) to_course = db.session.get(Course, to_course_id) @@ -118,6 +224,7 @@ def register_routes(app): from_course_id=from_course_id, to_course_id=to_course_id, transfer_type='upgrade', + transfer_class_type='upgrade', transfer_hours=transfer_normal, transfer_amount=transfer_amount, operator_id=session.get('user_id'), @@ -130,8 +237,13 @@ def register_routes(app): f'学员{from_student_id} 正课{transfer_normal}课时→课程{to_course_id} 转入{to_hours}课时', ) flash(f'升阶成功!100%正课结转,转入{to_hours}课时', 'success') + + # 普通转课 - 换课程课时 else: to_course_id = int(request.form.get('to_course_id')) + transfer_hours = rules.normalize_consumption_hours( + request.form.get('transfer_hours', 1) + ) from_course = db.session.get(Course, from_course_id) to_course = db.session.get(Course, to_course_id) from_unit = rules.account_effective_unit_price(from_account, from_course) @@ -176,6 +288,7 @@ def register_routes(app): from_course_id=from_course_id, to_course_id=to_course_id, transfer_type='transfer', + transfer_class_type='course_transfer', transfer_hours=transfer_hours, transfer_amount=transfer_amount, operator_id=session.get('user_id'), @@ -193,7 +306,10 @@ def register_routes(app): students = Student.query.filter_by(status=1).all() courses = Course.query.filter_by(status=1).all() - return render_template('transfer_form.html', students=students, courses=courses) + classes = Class_.query.filter_by(status=1).all() + teachers = Teacher.query.filter_by(status=1).all() + return render_template('transfer_form.html', students=students, courses=courses, + classes=classes, teachers=teachers) @app.route('/transfers/export', endpoint='transfer_export') @login_required diff --git a/xuexiao/templates/consumption_list.html b/xuexiao/templates/consumption_list.html index 11d8a75..92dbb5d 100644 --- a/xuexiao/templates/consumption_list.html +++ b/xuexiao/templates/consumption_list.html @@ -9,20 +9,63 @@ {% if current_user.has_permission('consumption_export') %} 导出{% endif %} -
+ + +
-
-
+ +
- + +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + +
+
- +
- + + + + + + + + + + + + + + {% for r in records %} @@ -31,6 +74,14 @@ + + @@ -45,11 +96,109 @@ {% endfor %} {% if not records %} - + {% endif %}
ID日期学员课程消课课时正课消耗赠课消耗类型操作人备注
ID日期学员课程班级授课老师消课课时正课消耗赠课消耗类型操作人备注
{{ r.consume_date }} {{ r.student.name }} {{ r.course.name }}{{ r.class_.name if r.class_id and r.class_ else '-' }} + {% if r.class_id and r.class_.teacher %} + {{ r.class_.teacher.name }} + {% else %} + - + {% endif %} + {{ r.hours_consumed }} {{ r.normal_consumed }} {{ r.gifted_consumed }}
暂无消课记录
暂无消课记录
+ + +
+
+ 手动录入课时汇总 +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+{% endblock %} + +{% block extra_js %} + {% endblock %} diff --git a/xuexiao/templates/transfer_form.html b/xuexiao/templates/transfer_form.html index abd273b..5a65092 100644 --- a/xuexiao/templates/transfer_form.html +++ b/xuexiao/templates/transfer_form.html @@ -2,15 +2,15 @@ {% block title %}申请转课 - 学生课程管理系统{% endblock %} {% block content %}
-

申请转课/转赠

+

申请转课

返回
-
+
- +
- +
-
- - + + + + + + + + + + +
+
+ + +
+ +
-
+
+ + +
+ + + + + + @@ -53,18 +117,20 @@ {% endfor %}
-
- - + -
+
+ +
- + 取消
@@ -74,28 +140,122 @@ {% block extra_js %} {% endblock %} diff --git a/xuexiao/templates/transfer_list.html b/xuexiao/templates/transfer_list.html index 633cf76..6dfba54 100644 --- a/xuexiao/templates/transfer_list.html +++ b/xuexiao/templates/transfer_list.html @@ -2,7 +2,7 @@ {% block title %}转课记录 - 学生课程管理系统{% endblock %} {% block content %}
-

转课/转赠记录

+

转课记录

申请转课 {% if current_user.has_permission('transfer_export') %} 导出{% endif %}
@@ -11,7 +11,24 @@
- + + + + + + + + + + + + + + + + + + {% for r in records %} @@ -19,21 +36,39 @@ - - - - + + + + + + + + + - + + {% endfor %} {% if not records %} - + {% endif %}
ID时间类型转出学员转入学员转出课程转入课程转出课时转出金额备注
ID时间类型学员课程原班级目标班级原老师目标老师单价变更原单价新单价转出课时变更原因生效时间备注
{{ r.id }} {{ r.created_at.strftime('%Y-%m-%d') }} - {% if r.transfer_type == 'transfer' %}转课 - {% elif r.transfer_type == 'upgrade' %}升阶 - {% else %}转赠{% endif %} + {% if r.transfer_class_type == 'class_transfer' %} + 班级转课 + {% elif r.transfer_class_type == 'upgrade' %} + 升阶 + {% elif r.transfer_class_type == 'course_transfer' %} + 换课 + {% else %} + 转赠 + {% endif %} {{ r.from_student.name }}{{ r.to_student.name if r.to_student else '-' }}{{ r.from_course.name }}{{ r.to_course.name if r.to_course else '-' }}{{ students[r.from_student_id].name if students.get(r.from_student_id) else '-' }}{{ courses[r.from_course_id].name if courses.get(r.from_course_id) else '-' }}{{ classes[r.from_class_id].name if r.from_class_id and classes.get(r.from_class_id) else '-' }}{{ classes[r.to_class_id].name if r.to_class_id and classes.get(r.to_class_id) else '-' }}{{ teachers[r.from_teacher_id].name if r.from_teacher_id and teachers.get(r.from_teacher_id) else '-' }}{{ teachers[r.to_teacher_id].name if r.to_teacher_id and teachers.get(r.to_teacher_id) else '-' }} + {% if r.price_change_type == 'price_changed' %} + 有变更 + {% else %} + 无变更 + {% endif %} + {{ '¥%.2f'|format(r.original_unit_price) if r.original_unit_price else '-' }}{{ '¥%.2f'|format(r.new_unit_price) if r.new_unit_price else '-' }} {{ r.transfer_hours }}{% if r.transfer_amount %}¥{{ r.transfer_amount }}{% else %}-{% endif %}{{ r.price_change_reason or '-' }}{{ r.effective_date.strftime('%Y-%m-%d') if r.effective_date else '-' }} {{ r.remark or '-' }}
暂无转课记录
暂无转课记录