feat: 系统配置支持动态增删用户类型和会员套餐

新增功能:
- UserTypeConfig 模型:用户类型配置支持动态增删
- MembershipPlanConfig 模型:会员套餐配置支持动态增删
- 用户类型管理页面:添加、编辑、删除、启用/禁用用户类型
- 会员套餐管理页面:添加、编辑、删除、上架/下架、推荐套餐
- 功能权限配置:支持选择功能列表
- 初始化默认配置功能

改进:
- settings.html 页面重构,提供配置入口链接
- 新增API接口支持增删改查操作
This commit is contained in:
2026-04-11 10:25:03 +08:00
parent 329e795648
commit 8c35a8741a
7 changed files with 1492 additions and 53 deletions

450
admin.py
View File

@@ -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]