feat: 系统配置支持动态增删用户类型和会员套餐
新增功能: - UserTypeConfig 模型:用户类型配置支持动态增删 - MembershipPlanConfig 模型:会员套餐配置支持动态增删 - 用户类型管理页面:添加、编辑、删除、启用/禁用用户类型 - 会员套餐管理页面:添加、编辑、删除、上架/下架、推荐套餐 - 功能权限配置:支持选择功能列表 - 初始化默认配置功能 改进: - settings.html 页面重构,提供配置入口链接 - 新增API接口支持增删改查操作
This commit is contained in:
450
admin.py
450
admin.py
@@ -9,7 +9,8 @@ from sqlalchemy import func, desc
|
||||
import json
|
||||
|
||||
from models import (db, User, Translation, TranslationCache, GuestTranslation,
|
||||
SystemConfig, OperationLog, DataPackage, UserPackage, DynamicConfig)
|
||||
SystemConfig, OperationLog, DataPackage, UserPackage, DynamicConfig,
|
||||
UserTypeConfig, MembershipPlanConfig)
|
||||
from config import USER_LIMITS, MEMBERSHIP_PLANS
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
@@ -771,4 +772,449 @@ def get_llm_config():
|
||||
'max_tokens': DynamicConfig.get('llm_max_tokens', LLM_CONFIG.get('max_tokens')),
|
||||
'chunk_size': DynamicConfig.get('llm_chunk_size', LLM_CONFIG.get('chunk_size')),
|
||||
'timeout': DynamicConfig.get('llm_timeout', LLM_CONFIG.get('timeout')),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ==================== 用户类型配置管理(动态增删) ====================
|
||||
@admin_bp.route('/user-types')
|
||||
@admin_required
|
||||
def user_types():
|
||||
"""用户类型配置列表"""
|
||||
user_types = UserTypeConfig.query.order_by(UserTypeConfig.sort_order).all()
|
||||
|
||||
# 如果数据库中没有数据,初始化默认配置
|
||||
if not user_types:
|
||||
init_default_user_types()
|
||||
user_types = UserTypeConfig.query.order_by(UserTypeConfig.sort_order).all()
|
||||
|
||||
return render_template('admin/user_types.html', user_types=user_types)
|
||||
|
||||
|
||||
@admin_bp.route('/user-types/add', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def add_user_type():
|
||||
"""添加用户类型"""
|
||||
if request.method == 'POST':
|
||||
data = request.json if request.is_json else request.form
|
||||
|
||||
# 检查type_key是否已存在
|
||||
existing = UserTypeConfig.query.filter_by(type_key=data.get('type_key')).first()
|
||||
if existing:
|
||||
return jsonify({'error': '类型标识已存在'}), 400
|
||||
|
||||
user_type = UserTypeConfig(
|
||||
type_key=data.get('type_key'),
|
||||
display_name=data.get('display_name'),
|
||||
daily_translations=int(data.get('daily_translations', 10)),
|
||||
max_pages=int(data.get('max_pages', 50)),
|
||||
max_file_size=int(data.get('max_file_size_mb', 30)) * 1024 * 1024,
|
||||
features=data.get('features', '[]'),
|
||||
sort_order=int(data.get('sort_order', 0)),
|
||||
is_active=data.get('is_active', True),
|
||||
is_system=False,
|
||||
)
|
||||
|
||||
db.session.add(user_type)
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='add_user_type',
|
||||
target=user_type.type_key,
|
||||
detail=json.dumps(user_type.to_dict(), ensure_ascii=False)
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
if request.is_json:
|
||||
return jsonify({'success': True, 'user_type': user_type.to_dict()})
|
||||
flash('用户类型已添加', 'success')
|
||||
return redirect(url_for('admin.user_types'))
|
||||
|
||||
# 所有可用功能
|
||||
all_features = [
|
||||
{'key': 'basic_translate', 'name': '基础翻译'},
|
||||
{'key': 'compare_view', 'name': '对照查看'},
|
||||
{'key': 'retranslate', 'name': '重新翻译'},
|
||||
{'key': 'history', 'name': '历史记录'},
|
||||
{'key': 'priority_queue', 'name': '优先队列'},
|
||||
{'key': 'export_pdf', 'name': '导出PDF'},
|
||||
{'key': 'batch_translate', 'name': '批量翻译'},
|
||||
{'key': 'custom_terms', 'name': '自定义术语'},
|
||||
]
|
||||
|
||||
return render_template('admin/user_type_form.html', user_type=None, all_features=all_features)
|
||||
|
||||
|
||||
@admin_bp.route('/user-types/<int:type_id>/edit', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def edit_user_type(type_id):
|
||||
"""编辑用户类型"""
|
||||
user_type = UserTypeConfig.query.get_or_404(type_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
data = request.json if request.is_json else request.form
|
||||
|
||||
user_type.display_name = data.get('display_name', user_type.display_name)
|
||||
user_type.daily_translations = int(data.get('daily_translations', user_type.daily_translations))
|
||||
user_type.max_pages = int(data.get('max_pages', user_type.max_pages))
|
||||
user_type.max_file_size = int(data.get('max_file_size_mb', user_type.max_file_size // 1024 // 1024)) * 1024 * 1024
|
||||
user_type.features = data.get('features', user_type.features)
|
||||
user_type.sort_order = int(data.get('sort_order', user_type.sort_order))
|
||||
user_type.is_active = data.get('is_active', True) if isinstance(data.get('is_active'), bool) else data.get('is_active') == 'true'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='edit_user_type',
|
||||
target=user_type.type_key,
|
||||
detail=json.dumps(user_type.to_dict(), ensure_ascii=False)
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
if request.is_json:
|
||||
return jsonify({'success': True, 'user_type': user_type.to_dict()})
|
||||
flash('用户类型已更新', 'success')
|
||||
return redirect(url_for('admin.user_types'))
|
||||
|
||||
# 所有可用功能
|
||||
all_features = [
|
||||
{'key': 'basic_translate', 'name': '基础翻译'},
|
||||
{'key': 'compare_view', 'name': '对照查看'},
|
||||
{'key': 'retranslate', 'name': '重新翻译'},
|
||||
{'key': 'history', 'name': '历史记录'},
|
||||
{'key': 'priority_queue', 'name': '优先队列'},
|
||||
{'key': 'export_pdf', 'name': '导出PDF'},
|
||||
{'key': 'batch_translate', 'name': '批量翻译'},
|
||||
{'key': 'custom_terms', 'name': '自定义术语'},
|
||||
]
|
||||
|
||||
return render_template('admin/user_type_form.html', user_type=user_type, all_features=all_features)
|
||||
|
||||
|
||||
@admin_bp.route('/user-types/<int:type_id>/delete', methods=['POST'])
|
||||
@admin_required
|
||||
def delete_user_type(type_id):
|
||||
"""删除用户类型"""
|
||||
user_type = UserTypeConfig.query.get_or_404(type_id)
|
||||
|
||||
# 系统内置类型不可删除
|
||||
if user_type.is_system:
|
||||
return jsonify({'error': '系统内置类型不可删除'}), 400
|
||||
|
||||
# 检查是否有用户使用此类型
|
||||
users_count = User.query.filter_by(user_type=user_type.type_key).count()
|
||||
if users_count > 0:
|
||||
return jsonify({'error': f'有 {users_count} 个用户使用此类型,请先修改用户类型'}), 400
|
||||
|
||||
type_key = user_type.type_key
|
||||
db.session.delete(user_type)
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='delete_user_type',
|
||||
target=type_key
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@admin_bp.route('/user-types/<int:type_id>/toggle', methods=['POST'])
|
||||
@admin_required
|
||||
def toggle_user_type(type_id):
|
||||
"""切换用户类型状态"""
|
||||
user_type = UserTypeConfig.query.get_or_404(type_id)
|
||||
user_type.is_active = not user_type.is_active
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'is_active': user_type.is_active})
|
||||
|
||||
|
||||
@admin_bp.route('/user-types/init', methods=['POST'])
|
||||
@admin_required
|
||||
def init_user_types():
|
||||
"""初始化默认用户类型"""
|
||||
init_default_user_types()
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
def init_default_user_types():
|
||||
"""初始化默认用户类型配置"""
|
||||
from config import USER_LIMITS
|
||||
|
||||
defaults = [
|
||||
('guest', '访客', USER_LIMITS.get('guest', {}), 0, True),
|
||||
('free', '免费用户', USER_LIMITS.get('free', {}), 1, True),
|
||||
('vip_basic', '基础会员', USER_LIMITS.get('vip_basic', {}), 2, True),
|
||||
('vip_pro', '专业会员', USER_LIMITS.get('vip_pro', {}), 3, True),
|
||||
('vip_enterprise', '企业会员', USER_LIMITS.get('vip_enterprise', {}), 4, True),
|
||||
]
|
||||
|
||||
for type_key, display_name, limits, sort_order, is_system in defaults:
|
||||
existing = UserTypeConfig.query.filter_by(type_key=type_key).first()
|
||||
if not existing:
|
||||
user_type = UserTypeConfig(
|
||||
type_key=type_key,
|
||||
display_name=display_name,
|
||||
daily_translations=limits.get('daily_translations', 10),
|
||||
max_pages=limits.get('max_pages', 50),
|
||||
max_file_size=limits.get('max_file_size', 30*1024*1024),
|
||||
features=json.dumps(limits.get('features', []), ensure_ascii=False),
|
||||
sort_order=sort_order,
|
||||
is_active=True,
|
||||
is_system=is_system,
|
||||
)
|
||||
db.session.add(user_type)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# ==================== 会员套餐配置管理(动态增删) ====================
|
||||
@admin_bp.route('/membership-plans')
|
||||
@admin_required
|
||||
def membership_plans():
|
||||
"""会员套餐配置列表"""
|
||||
plans = MembershipPlanConfig.query.order_by(MembershipPlanConfig.sort_order).all()
|
||||
|
||||
# 如果数据库中没有数据,初始化默认配置
|
||||
if not plans:
|
||||
init_default_membership_plans()
|
||||
plans = MembershipPlanConfig.query.order_by(MembershipPlanConfig.sort_order).all()
|
||||
|
||||
# 获取所有用户类型供选择
|
||||
user_types = UserTypeConfig.query.filter_by(is_active=True).all()
|
||||
|
||||
return render_template('admin/membership_plans.html', plans=plans, user_types=user_types)
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/add', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def add_membership_plan():
|
||||
"""添加会员套餐"""
|
||||
if request.method == 'POST':
|
||||
data = request.json if request.is_json else request.form
|
||||
|
||||
# 检查plan_key是否已存在
|
||||
existing = MembershipPlanConfig.query.filter_by(plan_key=data.get('plan_key')).first()
|
||||
if existing:
|
||||
return jsonify({'error': '套餐标识已存在'}), 400
|
||||
|
||||
plan = MembershipPlanConfig(
|
||||
plan_key=data.get('plan_key'),
|
||||
display_name=data.get('display_name'),
|
||||
price=float(data.get('price', 0)),
|
||||
original_price=float(data.get('original_price')) if data.get('original_price') else None,
|
||||
period=data.get('period', 'month'),
|
||||
period_days=int(data.get('period_days', 30)),
|
||||
description=data.get('description'),
|
||||
user_type_key=data.get('user_type_key'),
|
||||
sort_order=int(data.get('sort_order', 0)),
|
||||
is_active=data.get('is_active', True),
|
||||
is_recommended=data.get('is_recommended', False),
|
||||
is_system=False,
|
||||
)
|
||||
|
||||
db.session.add(plan)
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='add_membership_plan',
|
||||
target=plan.plan_key,
|
||||
detail=json.dumps(plan.to_dict(), ensure_ascii=False)
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
if request.is_json:
|
||||
return jsonify({'success': True, 'plan': plan.to_dict()})
|
||||
flash('会员套餐已添加', 'success')
|
||||
return redirect(url_for('admin.membership_plans'))
|
||||
|
||||
# 获取所有用户类型供选择
|
||||
user_types = UserTypeConfig.query.filter_by(is_active=True).all()
|
||||
|
||||
return render_template('admin/membership_plan_form.html', plan=None, user_types=user_types)
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/<int:plan_id>/edit', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def edit_membership_plan(plan_id):
|
||||
"""编辑会员套餐"""
|
||||
plan = MembershipPlanConfig.query.get_or_404(plan_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
data = request.json if request.is_json else request.form
|
||||
|
||||
plan.display_name = data.get('display_name', plan.display_name)
|
||||
plan.price = float(data.get('price', plan.price))
|
||||
plan.original_price = float(data.get('original_price')) if data.get('original_price') else None
|
||||
plan.period = data.get('period', plan.period)
|
||||
plan.period_days = int(data.get('period_days', plan.period_days))
|
||||
plan.description = data.get('description', plan.description)
|
||||
plan.user_type_key = data.get('user_type_key', plan.user_type_key)
|
||||
plan.sort_order = int(data.get('sort_order', plan.sort_order))
|
||||
plan.is_active = data.get('is_active', True) if isinstance(data.get('is_active'), bool) else data.get('is_active') == 'true'
|
||||
plan.is_recommended = data.get('is_recommended', False) if isinstance(data.get('is_recommended'), bool) else data.get('is_recommended') == 'true'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='edit_membership_plan',
|
||||
target=plan.plan_key,
|
||||
detail=json.dumps(plan.to_dict(), ensure_ascii=False)
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
if request.is_json:
|
||||
return jsonify({'success': True, 'plan': plan.to_dict()})
|
||||
flash('会员套餐已更新', 'success')
|
||||
return redirect(url_for('admin.membership_plans'))
|
||||
|
||||
# 获取所有用户类型供选择
|
||||
user_types = UserTypeConfig.query.filter_by(is_active=True).all()
|
||||
|
||||
return render_template('admin/membership_plan_form.html', plan=plan, user_types=user_types)
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/<int:plan_id>/delete', methods=['POST'])
|
||||
@admin_required
|
||||
def delete_membership_plan(plan_id):
|
||||
"""删除会员套餐"""
|
||||
plan = MembershipPlanConfig.query.get_or_404(plan_id)
|
||||
|
||||
# 系统内置套餐不可删除
|
||||
if plan.is_system:
|
||||
return jsonify({'error': '系统内置套餐不可删除'}), 400
|
||||
|
||||
plan_key = plan.plan_key
|
||||
db.session.delete(plan)
|
||||
db.session.commit()
|
||||
|
||||
# 记录日志
|
||||
log = OperationLog(
|
||||
user_id=session.get('user_id'),
|
||||
username='admin',
|
||||
action='delete_membership_plan',
|
||||
target=plan_key
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/<int:plan_id>/toggle', methods=['POST'])
|
||||
@admin_required
|
||||
def toggle_membership_plan(plan_id):
|
||||
"""切换会员套餐状态"""
|
||||
plan = MembershipPlanConfig.query.get_or_404(plan_id)
|
||||
plan.is_active = not plan.is_active
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'is_active': plan.is_active})
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/<int:plan_id>/recommend', methods=['POST'])
|
||||
@admin_required
|
||||
def recommend_membership_plan(plan_id):
|
||||
"""设置推荐套餐"""
|
||||
plan = MembershipPlanConfig.query.get_or_404(plan_id)
|
||||
plan.is_recommended = not plan.is_recommended
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'is_recommended': plan.is_recommended})
|
||||
|
||||
|
||||
@admin_bp.route('/membership-plans/init', methods=['POST'])
|
||||
@admin_required
|
||||
def init_membership_plans():
|
||||
"""初始化默认会员套餐"""
|
||||
init_default_membership_plans()
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
def init_default_membership_plans():
|
||||
"""初始化默认会员套餐配置"""
|
||||
from config import MEMBERSHIP_PLANS
|
||||
|
||||
defaults = [
|
||||
('vip_basic', MEMBERSHIP_PLANS.get('vip_basic', {}), 'vip_basic', 0, True),
|
||||
('vip_pro', MEMBERSHIP_PLANS.get('vip_pro', {}), 'vip_pro', 1, True),
|
||||
('vip_enterprise', MEMBERSHIP_PLANS.get('vip_enterprise', {}), 'vip_enterprise', 2, True),
|
||||
]
|
||||
|
||||
for plan_key, plan_data, user_type_key, sort_order, is_system in defaults:
|
||||
existing = MembershipPlanConfig.query.filter_by(plan_key=plan_key).first()
|
||||
if not existing:
|
||||
plan = MembershipPlanConfig(
|
||||
plan_key=plan_key,
|
||||
display_name=plan_data.get('name', plan_key),
|
||||
price=plan_data.get('price', 0),
|
||||
original_price=None,
|
||||
period=plan_data.get('period', 'month'),
|
||||
period_days=30 if plan_data.get('period') == 'month' else 365 if plan_data.get('period') == 'year' else 90,
|
||||
description=plan_data.get('description', ''),
|
||||
user_type_key=user_type_key,
|
||||
sort_order=sort_order,
|
||||
is_active=True,
|
||||
is_recommended=False,
|
||||
is_system=is_system,
|
||||
)
|
||||
db.session.add(plan)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# ==================== API: 获取用户限制配置(供其他模块使用) ====================
|
||||
def get_user_limits(user_type_key):
|
||||
"""获取指定用户类型的限制配置"""
|
||||
config = UserTypeConfig.query.filter_by(type_key=user_type_key, is_active=True).first()
|
||||
if config:
|
||||
return {
|
||||
'daily_translations': config.daily_translations,
|
||||
'max_pages': config.max_pages,
|
||||
'max_file_size': config.max_file_size,
|
||||
'features': config.get_features(),
|
||||
}
|
||||
# 如果数据库中没有,使用默认配置
|
||||
from config import USER_LIMITS
|
||||
return USER_LIMITS.get(user_type_key, USER_LIMITS.get('free', {}))
|
||||
|
||||
|
||||
def get_all_user_types():
|
||||
"""获取所有用户类型配置"""
|
||||
types = UserTypeConfig.query.filter_by(is_active=True).order_by(UserTypeConfig.sort_order).all()
|
||||
return {t.type_key: get_user_limits(t.type_key) for t in types}
|
||||
|
||||
|
||||
def get_membership_plan(plan_key):
|
||||
"""获取指定会员套餐配置"""
|
||||
plan = MembershipPlanConfig.query.filter_by(plan_key=plan_key, is_active=True).first()
|
||||
if plan:
|
||||
return plan.to_dict()
|
||||
from config import MEMBERSHIP_PLANS
|
||||
return MEMBERSHIP_PLANS.get(plan_key, {})
|
||||
|
||||
|
||||
def get_all_membership_plans():
|
||||
"""获取所有会员套餐配置"""
|
||||
plans = MembershipPlanConfig.query.filter_by(is_active=True).order_by(MembershipPlanConfig.sort_order).all()
|
||||
return [p.to_dict() for p in plans]
|
||||
Reference in New Issue
Block a user