"""工具函数模块 - 权限体系、装饰器、导出、初始化""" import io from datetime import datetime, date from functools import wraps from flask import session, flash, redirect, url_for, request, send_file, jsonify from openpyxl import Workbook from openpyxl.styles import Font, Alignment, Border, Side, PatternFill from models import db, User, Role, Student, Course, Teacher, Class_, \ StudentAccount, RechargeActivity, RechargeRecord, ConsumptionRecord, \ RefundRecord, TransferRecord, StopRecord, AttendanceRecord, OperationLog, Schedule # ========== 权限体系定义 ========== PERMISSIONS = { # 学员管理 'student_view': '学员管理-查看', 'student_add': '学员管理-新增', 'student_edit': '学员管理-编辑', 'student_delete': '学员管理-删除', 'student_export': '学员管理-导出', 'student_account_adjust': '学员管理-手动调课时', # 课程管理 'course_view': '课程管理-查看', 'course_add': '课程管理-新增', 'course_edit': '课程管理-编辑', 'course_delete': '课程管理-删除', 'course_export': '课程管理-导出', # 老师管理 'teacher_view': '老师管理-查看', 'teacher_add': '老师管理-新增', 'teacher_edit': '老师管理-编辑', 'teacher_delete': '老师管理-删除', 'teacher_export': '老师管理-导出', # 班级管理 'class_view': '班级管理-查看', 'class_add': '班级管理-新增', 'class_edit': '班级管理-编辑', 'class_delete': '班级管理-删除', 'class_export': '班级管理-导出', # 充值优惠 'recharge_view': '充值管理-查看', 'recharge_add': '充值管理-新增', 'recharge_export': '充值管理-导出', 'recharge_activity_view': '优惠活动-查看', 'recharge_activity_add': '优惠活动-新增', 'recharge_activity_edit': '优惠活动-编辑', # 消课管理 'consumption_view': '消课管理-查看', 'consumption_add': '消课管理-新增', 'consumption_export': '消课管理-导出', # 退费管理 'refund_view': '退费管理-查看', 'refund_add': '退费管理-新增', 'refund_export': '退费管理-导出', # 转课/转赠 'transfer_view': '转课转赠-查看', 'transfer_add': '转课转赠-新增', 'transfer_export': '转课转赠-导出', # 停课保号 'stop_view': '停课保号-查看', 'stop_add': '停课保号-新增', 'stop_export': '停课保号-导出', # 考勤管理 'attendance_view': '考勤管理-查看', 'attendance_add': '考勤管理-录入', 'attendance_export': '考勤管理-导出', # 统计报表 'statistics_view': '统计报表-查看', 'statistics_export': '统计报表-导出', # 课程安排 'schedule_view': '课程安排-查看', 'schedule_add': '课程安排-新增', 'schedule_export': '课程安排-导出', # 系统管理 'user_view': '用户管理-查看', 'user_add': '用户管理-新增', 'user_edit': '用户管理-编辑', 'role_view': '角色管理-查看', 'role_add': '角色管理-新增', 'role_edit': '角色管理-编辑', 'role_delete': '角色管理-删除', 'log_view': '操作日志-查看', 'log_export': '操作日志-导出', # 课时核对表 'keshibiao_view': '课时核对表-查看', 'keshibiao_import': '课时核对表-导入', 'keshibiao_export': '课时核对表-导出', } # 按模块分组的权限(用于角色编辑界面) PERMISSION_GROUPS = [ ('学员管理', [ 'student_view', 'student_add', 'student_edit', 'student_delete', 'student_export', 'student_account_adjust', ]), ('课程管理', ['course_view', 'course_add', 'course_edit', 'course_delete', 'course_export']), ('老师管理', ['teacher_view', 'teacher_add', 'teacher_edit', 'teacher_delete', 'teacher_export']), ('班级管理', ['class_view', 'class_add', 'class_edit', 'class_delete', 'class_export']), ('充值管理', ['recharge_view', 'recharge_add', 'recharge_export']), ('优惠活动', ['recharge_activity_view', 'recharge_activity_add', 'recharge_activity_edit']), ('消课管理', ['consumption_view', 'consumption_add', 'consumption_export']), ('退费管理', ['refund_view', 'refund_add', 'refund_export']), ('转课转赠', ['transfer_view', 'transfer_add', 'transfer_export']), ('停课保号', ['stop_view', 'stop_add', 'stop_export']), ('考勤管理', ['attendance_view', 'attendance_add', 'attendance_export']), ('统计报表', ['statistics_view', 'statistics_export']), ('课程安排', ['schedule_view', 'schedule_add', 'schedule_export']), ('用户管理', ['user_view', 'user_add', 'user_edit']), ('角色管理', ['role_view', 'role_add', 'role_edit', 'role_delete']), ('操作日志', ['log_view', 'log_export']), ('课时核对表', ['keshibiao_view', 'keshibiao_import', 'keshibiao_export']), ] # 导航菜单与权限的映射(侧边栏过滤) MENU_PERMISSIONS = { 'student': 'student_view', 'course': 'course_view', 'teacher': 'teacher_view', 'class': 'class_view', 'schedule': 'schedule_view', 'attendance': 'attendance_view', 'recharge_activity': 'recharge_activity_view', 'recharge': 'recharge_view', 'consumption': 'consumption_view', 'refund': 'refund_view', 'transfer': 'transfer_view', 'stop': 'stop_view', 'statistics': 'statistics_view', 'user': 'user_view', 'role': 'role_view', 'log': 'log_view', } # ========== 权限检查工具 ========== def get_current_user(): """获取当前登录用户对象""" user_id = session.get('user_id') if not user_id: return None return db.session.get(User, user_id) def get_current_permissions(): """获取当前用户的所有权限列表""" user = get_current_user() if not user or not user.role: return [] if user.role.name == '超级管理员': return list(PERMISSIONS.keys()) perms = user.role.permissions or '' return [p.strip() for p in perms.split(',') if p.strip() in PERMISSIONS] def has_permission(perm): """检查当前用户是否拥有某权限""" return perm in get_current_permissions() def is_admin(): """检查当前用户是否为超级管理员""" user = get_current_user() return user and user.role and user.role.name == '超级管理员' def is_teacher_role(): """是否授课老师角色(需数据范围过滤)""" user = get_current_user() return bool(user and user.role and user.role.name == '授课老师') def get_teacher_scope(): """ 授课老师数据范围:返回 dict(teacher_id, class_ids, student_ids) 或 None(不限制) """ if not is_teacher_role(): return None from models import Teacher, Class_, ClassStudent user = get_current_user() teacher = Teacher.query.filter_by(user_id=user.id).first() if not teacher: return {'teacher_id': None, 'class_ids': [], 'student_ids': []} class_ids = [ c.id for c in Class_.query.filter_by(teacher_id=teacher.id, status=1).all() ] student_ids = [] if class_ids: rows = ClassStudent.query.filter( ClassStudent.class_id.in_(class_ids), ClassStudent.status == 1, ).all() student_ids = list({r.student_id for r in rows}) return { 'teacher_id': teacher.id, 'class_ids': class_ids, 'student_ids': student_ids, } def teacher_can_access_class(class_id): scope = get_teacher_scope() if scope is None: return True return int(class_id) in scope['class_ids'] def teacher_can_access_student(student_id): scope = get_teacher_scope() if scope is None: return True return int(student_id) in scope['student_ids'] def filter_students_query(query): """授课老师仅看本班学员""" scope = get_teacher_scope() if scope is None: return query if not scope['student_ids']: return query.filter(False) from models import Student return query.filter(Student.id.in_(scope['student_ids'])) def filter_classes_query(query): scope = get_teacher_scope() if scope is None: return query if not scope['class_ids']: return query.filter(False) from models import Class_ return query.filter(Class_.id.in_(scope['class_ids'])) def login_required(f): """登录检查装饰器""" from functools import wraps @wraps(f) def decorated(*args, **kwargs): if not session.get('user_id'): from flask import request, jsonify if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.headers.get('Content-Type') == 'application/json': return jsonify({'error': '请先登录'}), 401 flash('请先登录', 'warning') return redirect(url_for('login')) return f(*args, **kwargs) return decorated def parse_date(date_str): """解析日期字符串""" if date_str: try: from datetime import datetime as dt return dt.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): return None return None def log_operation(action, detail='', before=None, after=None, *, commit=True): """记录操作日志(可选 before/after 快照;commit=False 时与业务同事务提交)""" import json from models import OperationLog if before is not None or after is not None: payload = {'message': detail} if before is not None: payload['before'] = before if after is not None: payload['after'] = after detail = json.dumps(payload, ensure_ascii=False) user_id = session.get('user_id') if not user_id: return log = OperationLog( user_id=user_id, operation_type=action, operation_detail=detail, ip_address=request.remote_addr if request else '', ) db.session.add(log) if commit: db.session.commit() def permission_required(perm): """权限检查装饰器""" def decorator(f): @wraps(f) def decorated(*args, **kwargs): if not session.get('user_id'): flash('请先登录', 'warning') return redirect(url_for('login')) if is_admin(): return f(*args, **kwargs) if not has_permission(perm): flash('您没有该操作的权限', 'danger') return redirect(url_for('dashboard')) return f(*args, **kwargs) return decorated return decorator def export_permission_required(module_name): """导出权限检查装饰器""" perm = f'{module_name}_export' return permission_required(perm) # ========== Excel 导出工具 ========== def export_excel(title, headers, rows, filename=None): """ 生成Excel文件并返回下载响应 Args: title: 工作表标题(第一行合并标题) headers: 表头列表 ['姓名', '电话', ...] rows: 数据行列表 [[val1, val2, ...], ...] filename: 下载文件名(不含扩展名), 默认用title+日期 """ wb = Workbook() ws = wb.active ws.title = title[:31] # Excel工作表名最长31字符 # 样式定义 title_font = Font(name='微软雅黑', size=14, bold=True) header_font = Font(name='微软雅黑', size=10, bold=True, color='FFFFFF') header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid') cell_font = Font(name='微软雅黑', size=10) center_align = Alignment(horizontal='center', vertical='center', wrap_text=True) thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) # 标题行 ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(headers)) title_cell = ws.cell(row=1, column=1, value=title) title_cell.font = title_font title_cell.alignment = Alignment(horizontal='center', vertical='center') ws.row_dimensions[1].height = 36 # 表头行 for col, header in enumerate(headers, 1): cell = ws.cell(row=2, column=col, value=header) cell.font = header_font cell.fill = header_fill cell.alignment = center_align cell.border = thin_border ws.row_dimensions[2].height = 24 # 数据行 for row_idx, row_data in enumerate(rows, 3): for col_idx, value in enumerate(row_data, 1): cell = ws.cell(row=row_idx, column=col_idx, value=value) cell.font = cell_font cell.alignment = center_align cell.border = thin_border # 自动调整列宽 for col in range(1, len(headers) + 1): max_length = 0 for row in ws.iter_rows(min_row=2, max_row=ws.max_row, min_col=col, max_col=col): for cell in row: if cell.value: length = len(str(cell.value)) # 中文字符算2个宽度 cn_count = sum(1 for c in str(cell.value) if '\u4e00' <= c <= '\u9fff') length = length + cn_count max_length = max(max_length, length) ws.column_dimensions[ws.cell(row=2, column=col).column_letter].width = min(max_length + 4, 40) # 保存到内存 output = io.BytesIO() wb.save(output) output.seek(0) if not filename: filename = f"{title}_{datetime.now().strftime('%Y%m%d%H%M%S')}" filename = filename.replace(' ', '_') return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=f'{filename}.xlsx' ) # ========== 各模块导出数据查询 ========== def export_students(keyword=None): """导出学员信息""" query = Student.query if keyword: query = query.filter( db.or_(Student.name.contains(keyword), Student.phone.contains(keyword)) ) students = query.order_by(Student.id.desc()).all() headers = ['序号', '姓名', '生日', '来源', '备注', '创建时间'] rows = [] for i, s in enumerate(students, 1): rows.append([ i, s.name, str(s.birthday) if s.birthday else '', s.source or '', s.remark or '', s.created_at.strftime('%Y-%m-%d %H:%M') if s.created_at else '' ]) return export_excel('学员信息', headers, rows, '学员信息') def export_courses(): """导出课程信息""" courses = Course.query.order_by(Course.id.desc()).all() headers = ['序号', '课程名称', '单价(元/课时)', '总课时', '材料费', '课程类型', '等级', '状态', '备注'] rows = [] for i, c in enumerate(courses, 1): rows.append([ i, c.name, c.price_per_hour or 0, c.total_hours or 0, c.material_fee or 0, c.type or '', c.level or '', '启用' if c.status == 1 else '停用', c.remark or '' ]) return export_excel('课程信息', headers, rows, '课程信息') def export_teachers(): """导出老师信息""" teachers = Teacher.query.order_by(Teacher.id.desc()).all() headers = ['序号', '姓名', '电话', '专长', '状态', '备注'] rows = [] for i, t in enumerate(teachers, 1): rows.append([ i, t.name, t.phone or '', t.specialty or '', '在职' if t.status == 1 else '离职', t.remark or '' ]) return export_excel('老师信息', headers, rows, '老师信息') def export_recharges(student_id=None, start_date=None, end_date=None): """导出充值记录""" query = RechargeRecord.query if student_id: query = query.filter(RechargeRecord.student_id == student_id) if start_date: query = query.filter(RechargeRecord.created_at >= start_date) if end_date: query = query.filter(RechargeRecord.created_at <= end_date) records = query.order_by(RechargeRecord.id.desc()).all() headers = ['序号', '学员姓名', '课程名称', '充值金额', '充值课时', '赠送课时', '支付方式', '优惠活动', '备注', '充值时间'] rows = [] for i, r in enumerate(records, 1): rows.append([ i, r.student.name if r.student else '', r.course.name if r.course else '', r.amount or 0, r.hours_recharged or 0, r.hours_gifted or 0, r.payment_method or '', r.activity.name if r.activity else '', r.remark or '', r.created_at.strftime('%Y-%m-%d %H:%M') if r.created_at else '' ]) return export_excel('充值记录', headers, rows, '充值记录') def export_consumptions(student_id=None, start_date=None, end_date=None): """导出消课记录""" query = ConsumptionRecord.query if student_id: query = query.filter(ConsumptionRecord.student_id == student_id) 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.id.desc()).all() headers = ['序号', '学员姓名', '课程名称', '班级名称', '消课课时', '消课类型', '是否补课', '是否试听', '消课日期', '备注'] rows = [] for i, r in enumerate(records, 1): consume_type = '普通' if r.is_trial: consume_type = '试听' elif r.consume_type == 'mixed': consume_type = '混合' rows.append([ i, r.student.name if r.student else '', r.course.name if r.course else '', r.class_.name if r.class_ else '', r.hours_consumed or 0, consume_type, '是' if r.is_makeup else '否', '是' if r.is_trial else '否', str(r.consume_date) if r.consume_date else '', r.remark or '' ]) return export_excel('消课记录', headers, rows, '消课记录') def export_refunds(): """导出退费记录""" records = RefundRecord.query.order_by(RefundRecord.id.desc()).all() headers = ['序号', '学员姓名', '课程名称', '充值总额', '已消课时金额', '材料费扣除', '赠课扣回', '退费金额', '退费原因', '退费时间'] rows = [] for i, r in enumerate(records, 1): rows.append([ i, r.student.name if r.student else '', r.course.name if r.course else '', r.total_recharged or 0, r.consumed_hours_value or 0, r.material_fee_deducted or 0, r.gifted_hours_deducted or 0, r.refund_amount or 0, r.reason or '', r.created_at.strftime('%Y-%m-%d %H:%M') if r.created_at else '' ]) return export_excel('退费记录', headers, rows, '退费记录') def export_transfers(): """导出转课/转赠记录""" records = TransferRecord.query.order_by(TransferRecord.id.desc()).all() headers = ['序号', '学员姓名', '转出课程', '转入课程/学员', '转出课时', '折算金额', '类型', '备注', '时间'] rows = [] for i, r in enumerate(records, 1): target = '' if r.transfer_type == 'gift' and r.to_student: target = f'转赠给: {r.to_student.name}' elif r.to_course: target = f'转入: {r.to_course.name}' type_label = {'transfer': '转课', 'gift': '转赠', 'upgrade': '升阶'}.get( r.transfer_type, r.transfer_type ) rows.append([ i, r.from_student.name if r.from_student else '', r.from_course.name if r.from_course else '', target, r.transfer_hours or 0, r.transfer_amount or 0, type_label, r.remark or '', r.created_at.strftime('%Y-%m-%d %H:%M') if r.created_at else '', ]) return export_excel('转课转赠记录', headers, rows, '转课转赠记录') def export_stops(): """导出停课记录""" records = StopRecord.query.order_by(StopRecord.id.desc()).all() headers = ['序号', '学员姓名', '课程名称', '开始日期', '结束日期', '状态', '备注', '创建时间'] rows = [] for i, r in enumerate(records, 1): status = '已复课' if r.status == 0 else '停课中' if r.status == 1 and r.end_date and r.end_date < date.today(): status = '已过期' rows.append([ i, r.student.name if r.student else '', r.course.name if r.course else '', str(r.start_date) if r.start_date else '', str(r.end_date) if r.end_date else '', status, r.reason or '', r.created_at.strftime('%Y-%m-%d %H:%M') if r.created_at else '' ]) return export_excel('停课保号记录', headers, rows, '停课保号记录') def export_attendances(class_id=None, start_date=None, end_date=None): """导出考勤记录""" query = AttendanceRecord.query if class_id: query = query.filter(AttendanceRecord.class_id == class_id) if start_date: query = query.filter(AttendanceRecord.date >= start_date) if end_date: query = query.filter(AttendanceRecord.date <= end_date) records = query.order_by(AttendanceRecord.id.desc()).all() headers = ['序号', '学员姓名', '班级名称', '考勤日期', '状态', '备注'] rows = [] status_map = {'present': '出勤', 'absent': '缺勤', 'late': '迟到', 'leave': '请假'} for i, r in enumerate(records, 1): rows.append([ i, r.student.name if r.student else '', r.class_.name if r.class_ else '', str(r.date) if r.date else '', status_map.get(r.status, r.status or ''), r.remark or '' ]) return export_excel('考勤记录', headers, rows, '考勤记录') def export_classes(): """导出班级信息""" classes = Class_.query.order_by(Class_.id.desc()).all() headers = ['序号', '班级名称', '课程', '老师', '开课日期', '结课日期', '上课时间', '最大人数', '当前人数', '状态', '备注'] rows = [] for i, c in enumerate(classes, 1): rows.append([ i, c.name, c.course.name if c.course else '', c.teacher.name if c.teacher else '', str(c.start_date) if c.start_date else '', str(c.end_date) if c.end_date else '', c.schedule or '', c.max_students or 0, c.current_students or 0, '启用' if c.status == 1 else '停用', c.remark or '' ]) return export_excel('班级信息', headers, rows, '班级信息') def export_schedules(): """导出课程安排""" schedules = Schedule.query.order_by(Schedule.date.desc()).all() headers = ['序号', '班级名称', '上课日期', '开始时间', '结束时间', '授课老师', '课程主题'] rows = [] for i, s in enumerate(schedules, 1): rows.append([ i, s.class_.name if s.class_ else '', str(s.date) if s.date else '', s.start_time or '', s.end_time or '', s.teacher.name if s.teacher else '', s.topic or '' ]) return export_excel('课程安排', headers, rows, '课程安排') def export_operation_logs(): """导出操作日志""" query = OperationLog.query start_date = request.args.get('start_date', '').strip() end_date = request.args.get('end_date', '').strip() if start_date: query = query.filter(OperationLog.created_at >= start_date) if end_date: query = query.filter(OperationLog.created_at <= end_date + ' 23:59:59') records = query.order_by(OperationLog.id.desc()).limit(10000).all() headers = ['序号', '操作人', '操作类型', '操作详情', 'IP地址', '操作时间'] rows = [] for i, r in enumerate(records, 1): rows.append([ i, r.user.real_name if r.user else (str(r.user_id) if r.user_id else ''), r.operation_type or '', r.operation_detail or '', r.ip_address or '', r.created_at.strftime('%Y-%m-%d %H:%M') if r.created_at else '' ]) return export_excel('操作日志', headers, rows, '操作日志') def export_statistics_data(): """导出统计数据(兼容旧入口)""" from keshi_stats import resolve_report_period year, month = resolve_report_period() return export_statistics_snapshot_data(year, month) def export_statistics_snapshot_data(year: int, month: int): """导出基于课时核对表快照的统计数据""" from keshi_stats import ( period_overview, year_overview, cumulative_finance, course_stats_for_period, ) label = f'{year}-{month:02d}' month_ov = period_overview(year, month) year_ov = year_overview(year) finance = cumulative_finance() courses = course_stats_for_period(year, month) headers = ['统计项', '数值'] rows = [ ['统计月份', label], ['当月充值(元)', month_ov['recharge']], ['当月消课(节)', month_ov['consumed_hours']], ['当月已消金额(元)', month_ov['consumed_amount']], ['当月退费(元)', month_ov['refund']], ['当月新签学员', month_ov['new_signups']], ['当月月末余额合计(元)', month_ov['end_balance']], [], [f'{year}年度充值(元)', year_ov['recharge']], [f'{year}年度消课(节)', year_ov['consumed_hours']], [f'{year}年度退费(元)', year_ov['refund']], [], ['累计充值(元)', finance['total_recharge']], ['累计退费(元)', finance['total_refund']], ['净收入(元)', finance['net_income']], ] if courses: rows.append([]) rows.append(['=== 各课程当月统计 ===', '']) rows.append(['课程', '充值', '消课节', '退费', '月末余额']) for c in courses: rows.append([ c['code'], c['recharge'], c['consumed_hours'], c['refund'], c['end_balance'], ]) return export_excel('统计报表', headers, rows, f'统计报表_{label}') # ========== 数据库自动迁移 ========== def is_mysql(): """判断当前是否使用 MySQL 协议(含 Doris)""" from flask import current_app uri = current_app.config.get('SQLALCHEMY_DATABASE_URI', '') return uri.startswith('mysql') def is_doris(): """是否 Apache Doris""" from db_bootstrap import is_doris as _is_doris return _is_doris() def _migrate_col_type(col): """迁移用列类型(Doris 与 MySQL 略有差异)""" from db_bootstrap import _col_type_doris, is_doris as _is_doris if _is_doris(): return _col_type_doris(col) return str(col.type).upper() def auto_migrate(): """自动迁移:检测并补齐数据库中缺失的列;Doris 下缺失表由 doris_create_all 处理""" from db_bootstrap import is_doris as _is_doris, doris_create_all inspector = db.inspect(db.engine) existing_tables = set(inspector.get_table_names()) if _is_doris(): model_tables = {m.class_.__tablename__ for m in db.Model.registry.mappers} if model_tables - existing_tables: doris_create_all() inspector = db.inspect(db.engine) existing_tables = set(inspector.get_table_names()) # 遍历所有模型类 for mapper in db.Model.registry.mappers: model = mapper.class_ table = model.__table__ table_name = table.name if table_name not in existing_tables: continue if _is_doris(): try: rows = db.session.execute( db.text(f'SHOW COLUMNS FROM `{table_name}`') ).fetchall() existing_cols = {r[0] for r in rows} except Exception: existing_cols = set() else: existing_cols = {c['name'] for c in inspector.get_columns(table_name)} model_cols = {c.name for c in table.columns} missing_cols = model_cols - existing_cols if not missing_cols: continue for col_name in missing_cols: col = table.columns[col_name] col_type = _migrate_col_type(col) if is_mysql(): default_val = '' if col.primary_key or col.autoincrement: continue if col.default is not None: if col.default.is_scalar: if isinstance(col.default.arg, (int, float)): default_val = f' DEFAULT {col.default.arg}' if not _is_doris() else f' DEFAULT "{col.default.arg}"' else: default_val = f" DEFAULT '{col.default.arg}'" elif not col.nullable: # NOT NULL且无默认值,给合理默认 if 'INT' in col_type: default_val = ' DEFAULT 0' if not _is_doris() else ' DEFAULT "0"' elif 'VARCHAR' in col_type or 'TEXT' in col_type or 'STRING' in col_type: default_val = " DEFAULT ''" elif 'DATE' in col_type or 'TIME' in col_type: default_val = '' elif 'BOOLEAN' in col_type or 'TINYINT' in col_type: default_val = ' DEFAULT 0' if not _is_doris() else ' DEFAULT "0"' elif 'FLOAT' in col_type or 'DECIMAL' in col_type: default_val = ' DEFAULT 0.0' if not _is_doris() else ' DEFAULT "0"' nullable = '' if col.nullable else ' NOT NULL' sql = f'ALTER TABLE `{table_name}` ADD COLUMN `{col_name}` {col_type}{default_val}{nullable}' else: # SQLite default_val = '' if col.default is not None and col.default.is_scalar: if isinstance(col.default.arg, (int, float)): default_val = f' DEFAULT {col.default.arg}' elif isinstance(col.default.arg, str): default_val = f" DEFAULT '{col.default.arg}'" elif not col.nullable: if 'INT' in col_type: default_val = ' DEFAULT 0' elif 'VARCHAR' in col_type or 'TEXT' in col_type: default_val = " DEFAULT ''" elif 'BOOLEAN' in col_type: default_val = ' DEFAULT 0' elif 'FLOAT' in col_type or 'DECIMAL' in col_type: default_val = ' DEFAULT 0.0' sql = f'ALTER TABLE {table_name} ADD COLUMN {col_name} {col_type}{default_val}' try: db.session.execute(db.text(sql)) db.session.commit() print(f' [auto_migrate] Added column: {table_name}.{col_name}') except Exception as e: db.session.rollback() print(f' [auto_migrate] Failed to add {table_name}.{col_name}: {e}') # ========== 初始化默认数据 ========== def init_default_data(): """初始化默认数据:确保admin用户和超级管理员角色存在""" # 确保超级管理员角色存在 admin_role = Role.query.filter_by(name='超级管理员').first() if not admin_role: admin_role = Role( name='超级管理员', permissions=','.join(PERMISSIONS.keys()), remark='系统默认超级管理员,拥有所有权限' ) db.session.add(admin_role) db.session.commit() elif not admin_role.permissions or len(admin_role.permissions.split(',')) < len(PERMISSIONS): # 超级管理员权限补齐 admin_role.permissions = ','.join(PERMISSIONS.keys()) db.session.commit() # 确保admin用户存在且密码正确 admin = User.query.filter_by(username='admin').first() if not admin: admin = User( username='admin', real_name='系统管理员', role_id=admin_role.id, status=1, remark='系统默认管理员' ) admin.set_password('admin123') db.session.add(admin) db.session.commit() else: # 确保密码是admin123 if not admin.check_password('admin123'): admin.set_password('admin123') # 确保角色正确 if not admin.role_id or admin.role_id != admin_role.id: admin.role_id = admin_role.id db.session.commit()