5 Commits

Author SHA1 Message Date
8ef9df65d2 feat: 备用大模型接口管理功能
- 新增 BackupLLMConfig 数据模型存储备用大模型配置
- 支持手动新增、编辑、删除备用大模型接口
- 支持测试连接功能
- 大模型配置页面静态表格改为动态管理的备用接口链接
- 默认初始化5个常用大模型服务商配置
2026-04-16 14:36:13 +08:00
9a36b9245a feat: 拆分邮件通知选项,根据会员权益显示
- 翻译完成邮件通知(所有用户)
- 鮮件带附件发送(VIP专属,free用户显示需VIP)
- 会员到期提醒(仅VIP用户显示)
- 添加 notify_with_attachment 字段
- 更新各等级权益:email_notify/email_attachment
2026-04-15 01:10:59 +08:00
56709b1a65 fix: 个人中心邮箱显示逻辑优化
- 显示用户注册邮箱作为通知邮箱
- 支持更换通知邮箱(一次只能通知一个)
- 添加更换邮箱模态框
- API接口检查邮箱是否已被其他用户使用
2026-04-14 19:17:24 +08:00
71a613ff5f feat: 添加手机号、邀请好友、邮件通知功能
- 用户模型添加:手机号、邀请码、邀请统计、邮件通知设置
- 邀请好友系统:专属邀请码、邀请奖励(¥5/人)
- 邮件通知:翻译完成通知(含附件)、欢迎邮件、到期提醒
- 新增模型:UserInvitation, InviteRewardConfig, EmailNotification, EmailTemplateConfig
- 个人中心添加:手机号绑定、通知设置、邀请好友模块
- email_service.py:邮件发送服务(支持附件)

新用户注册奖励:¥2
邀请人奖励:¥5/人
2026-04-14 18:58:40 +08:00
4aac8ab04c fix: 修正模板变量名plans而非db_plans 2026-04-14 18:46:04 +08:00
10 changed files with 1466 additions and 24 deletions

206
admin.py
View File

@@ -10,7 +10,7 @@ import json
from models import (db, User, Translation, TranslationCache, GuestTranslation,
SystemConfig, OperationLog, DataPackage, UserPackage, DynamicConfig,
UserTypeConfig, MembershipPlanConfig)
UserTypeConfig, MembershipPlanConfig, BackupLLMConfig)
from config import USER_LIMITS, MEMBERSHIP_PLANS
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@@ -1217,4 +1217,206 @@ def get_membership_plan(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]
return [p.to_dict() for p in plans]
# ==================== 备用大模型接口管理 ====================
@admin_bp.route('/backup-llm')
@admin_required
def backup_llm_list():
"""备用大模型接口列表"""
configs = BackupLLMConfig.query.order_by(BackupLLMConfig.sort_order).all()
# 如果数据库中没有数据,初始化默认配置
if not configs:
init_default_backup_llm()
configs = BackupLLMConfig.query.order_by(BackupLLMConfig.sort_order).all()
return render_template('admin/backup_llm.html', configs=configs)
@admin_bp.route('/backup-llm/add', methods=['GET', 'POST'])
@admin_required
def add_backup_llm():
"""添加备用大模型接口"""
if request.method == 'POST':
data = request.json if request.is_json else request.form
config = BackupLLMConfig(
provider_name=data.get('provider_name'),
api_base=data.get('api_base'),
api_key=data.get('api_key'),
model=data.get('model'),
is_active=data.get('is_active', True) if isinstance(data.get('is_active'), bool) else data.get('is_active') == 'true',
sort_order=int(data.get('sort_order', 0)),
description=data.get('description'),
)
db.session.add(config)
db.session.commit()
# 记录日志
log = OperationLog(
user_id=session.get('user_id'),
username='admin',
action='add_backup_llm',
target=config.provider_name,
detail=json.dumps(config.to_dict(), ensure_ascii=False)
)
db.session.add(log)
db.session.commit()
if request.is_json:
return jsonify({'success': True, 'config': config.to_dict()})
flash('备用大模型接口已添加', 'success')
return redirect(url_for('admin.backup_llm_list'))
return render_template('admin/backup_llm_form.html', config=None)
@admin_bp.route('/backup-llm/<int:config_id>/edit', methods=['GET', 'POST'])
@admin_required
def edit_backup_llm(config_id):
"""编辑备用大模型接口"""
config = BackupLLMConfig.query.get_or_404(config_id)
if request.method == 'POST':
data = request.json if request.is_json else request.form
config.provider_name = data.get('provider_name', config.provider_name)
config.api_base = data.get('api_base', config.api_base)
config.api_key = data.get('api_key', config.api_key)
config.model = data.get('model', config.model)
config.is_active = data.get('is_active', True) if isinstance(data.get('is_active'), bool) else data.get('is_active') == 'true'
config.sort_order = int(data.get('sort_order', config.sort_order))
config.description = data.get('description', config.description)
db.session.commit()
# 记录日志
log = OperationLog(
user_id=session.get('user_id'),
username='admin',
action='edit_backup_llm',
target=config.provider_name,
detail=json.dumps(config.to_dict(), ensure_ascii=False)
)
db.session.add(log)
db.session.commit()
if request.is_json:
return jsonify({'success': True, 'config': config.to_dict()})
flash('备用大模型接口已更新', 'success')
return redirect(url_for('admin.backup_llm_list'))
return render_template('admin/backup_llm_form.html', config=config)
@admin_bp.route('/backup-llm/<int:config_id>/delete', methods=['POST'])
@admin_required
def delete_backup_llm(config_id):
"""删除备用大模型接口"""
config = BackupLLMConfig.query.get_or_404(config_id)
provider_name = config.provider_name
db.session.delete(config)
db.session.commit()
# 记录日志
log = OperationLog(
user_id=session.get('user_id'),
username='admin',
action='delete_backup_llm',
target=provider_name
)
db.session.add(log)
db.session.commit()
return jsonify({'success': True})
@admin_bp.route('/backup-llm/<int:config_id>/toggle', methods=['POST'])
@admin_required
def toggle_backup_llm(config_id):
"""切换备用大模型接口状态"""
config = BackupLLMConfig.query.get_or_404(config_id)
config.is_active = not config.is_active
db.session.commit()
return jsonify({'success': True, 'is_active': config.is_active})
@admin_bp.route('/backup-llm/<int:config_id>/test', methods=['POST'])
@admin_required
def test_backup_llm(config_id):
"""测试备用大模型接口"""
config = BackupLLMConfig.query.get_or_404(config_id)
try:
from openai import OpenAI
client = OpenAI(
api_key=config.api_key or 'sk-test',
base_url=config.api_base,
)
model = config.model or 'default'
# 发送简单测试请求
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "Hello"}],
max_tokens=10,
timeout=10,
)
return jsonify({
'success': True,
'provider': config.provider_name,
'model': model,
'response': response.choices[0].message.content[:50] if response.choices else 'OK'
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@admin_bp.route('/backup-llm/init', methods=['POST'])
@admin_required
def init_backup_llm():
"""初始化默认备用大模型"""
init_default_backup_llm()
return jsonify({'success': True})
def init_default_backup_llm():
"""初始化默认备用大模型接口配置"""
defaults = [
('本地LM Studio', 'http://localhost:1234/v1', None, None, 0),
('OpenAI', 'https://api.openai.com/v1', None, 'gpt-4', 1),
('DeepSeek', 'https://api.deepseek.com/v1', None, 'deepseek-chat', 2),
('阿里百炼', 'https://dashscope.aliyuncs.com/compatible-mode/v1', None, 'qwen-turbo', 3),
('SiliconFlow', 'https://api.siliconflow.cn/v1', None, 'Qwen/Qwen2.5-72B-Instruct', 4),
]
for provider_name, api_base, api_key, model, sort_order in defaults:
existing = BackupLLMConfig.query.filter_by(provider_name=provider_name).first()
if not existing:
config = BackupLLMConfig(
provider_name=provider_name,
api_base=api_base,
api_key=api_key,
model=model,
is_active=True,
sort_order=sort_order,
description=f'{provider_name} 默认接口',
)
db.session.add(config)
db.session.commit()
def get_backup_llm_configs():
"""获取所有备用大模型配置(供其他模块使用)"""
configs = BackupLLMConfig.query.filter_by(is_active=True).order_by(BackupLLMConfig.sort_order).all()
return [c.to_dict() for c in configs]

165
app.py
View File

@@ -15,7 +15,9 @@ from werkzeug.utils import secure_filename
from config import *
from models import (db, User, Translation, TranslationCache, GuestTranslation,
DataPackage, UserPackage, DynamicConfig, UserRecharge, UserRefund,
MembershipPurchase, AccountTransaction, MembershipPlanConfig, UserTypeConfig)
MembershipPurchase, AccountTransaction, MembershipPlanConfig, UserTypeConfig,
UserInvitation, InviteRewardConfig, EmailNotification, EmailTemplateConfig)
from email_service import email_service
from services import TranslationService, CacheService, TranslationTask
from admin import admin_bp
@@ -221,6 +223,8 @@ def pricing():
'priority_queue': '优先处理队列',
'custom_instruction': '自定义翻译要求',
'api_access': 'API接口调用',
'email_notify': '邮件通知',
'email_attachment': '邮件附件发送',
}
# 从数据库读取动态配置的会员套餐
@@ -287,10 +291,15 @@ def profile():
daily_remaining = limits['daily_translations'] - user.daily_count if limits['daily_translations'] > 0 else '无限'
max_pages = limits['max_pages'] if limits['max_pages'] > 0 else '无限'
# 检查是否有邮件附件权限
user_features = limits.get('features', [])
has_email_attachment = 'email_attachment' in user_features or user_features == ['all']
return render_template('profile.html',
user=user,
daily_remaining=daily_remaining,
max_pages=max_pages
max_pages=max_pages,
has_email_attachment=has_email_attachment
)
@@ -591,12 +600,15 @@ def login():
def register():
"""注册"""
if request.method == 'GET':
return render_template('register.html')
# 检查邀请码参数
invite_code = request.args.get('invite', None)
return render_template('register.html', invite_code=invite_code)
data = request.json
username = data.get('username')
email = data.get('email')
password = data.get('password')
invite_code = data.get('invite_code', None) # 邀请码
# 检查用户是否存在
if User.query.filter_by(username=username).first():
@@ -608,11 +620,76 @@ def register():
# 创建用户
user = User(username=username, email=email, user_type='free')
user.set_password(password)
# 生成邀请码6位随机字符
import random
import string
user.invite_code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
# 处理邀请
if invite_code:
inviter = User.query.filter_by(invite_code=invite_code).first()
if inviter:
user.invited_by = inviter.id
# 创建邀请记录
invitation = UserInvitation(
inviter_id=inviter.id,
invite_code=invite_code,
invitee_id=user.id,
invitee_email=email,
reward_amount=5.0,
status='registered',
registered_at=datetime.utcnow()
)
db.session.add(invitation)
# 给邀请人奖励
inviter.invite_count += 1
inviter.invite_rewards += 5.0
inviter.balance += 5.0
# 给被邀请人奖励
user.balance += 2.0
# 创建流水
inviter_tx = AccountTransaction(
user_id=inviter.id,
transaction_type='invite_reward',
amount=5.0,
balance_before=inviter.balance - 5.0,
balance_after=inviter.balance,
description=f'邀请用户{username}注册奖励'
)
db.session.add(inviter_tx)
user_tx = AccountTransaction(
user_id=user.id,
transaction_type='invite_bonus',
amount=2.0,
balance_before=0,
balance_after=2.0,
description='新用户注册奖励'
)
db.session.add(user_tx)
# 发送邀请奖励邮件
if inviter.email_notify:
email_service.send_invite_reward(inviter.email, inviter.username, 5.0, inviter.invite_count)
db.session.add(user)
db.session.commit()
session['user_id'] = user.id
return jsonify({'success': True, 'user': user.to_dict()})
# 发送欢迎邮件
email_service.send_welcome_email(user.email, user.username, user.invite_code)
return jsonify({
'success': True,
'user': user.to_dict(),
'invite_reward': user.balance > 0 # 是否获得邀请奖励
})
@app.route('/logout')
@@ -775,6 +852,86 @@ def request_refund():
})
@app.route('/api/profile/settings', methods=['POST'])
def update_settings():
"""更新账户设置"""
user = get_current_user()
if not user:
return jsonify({'error': '请登录'}), 401
data = request.json
# 手机号
if 'phone' in data:
phone = data.get('phone', '')
if phone and len(phone) >= 10:
user.phone = phone
user.phone_verified = False
# 通知邮箱(更换后邮件通知发送到此邮箱)
if 'email' in data:
email = data.get('email', '')
if email and '@' in email:
# 检查邮箱是否已被其他用户使用
existing = User.query.filter(User.email == email, User.id != user.id).first()
if existing:
return jsonify({'error': '该邮箱已被其他用户使用'}), 400
user.email = email
# 通知设置
if 'notify_on_complete' in data:
user.notify_on_complete = data.get('notify_on_complete', True)
if 'notify_on_expire' in data:
user.notify_on_expire = data.get('notify_on_expire', True)
if 'notify_with_attachment' in data:
# 检查是否有附件权限
limits = USER_LIMITS.get(user.user_type, USER_LIMITS['free'])
user_features = limits.get('features', [])
if 'email_attachment' in user_features or user_features == ['all']:
user.notify_with_attachment = data.get('notify_with_attachment', False)
else:
return jsonify({'error': '邮件附件功能需VIP会员', 'feature': 'email_attachment'}), 403
if 'email_notify' in data:
user.email_notify = data.get('email_notify', True)
db.session.commit()
return jsonify({
'success': True,
'user': user.to_dict()
})
@app.route('/api/profile/invitations')
def get_invitations():
"""获取邀请记录"""
user = get_current_user()
if not user:
return jsonify({'error': '请登录'}), 401
invitations = UserInvitation.query.filter_by(inviter_id=user.id)\
.order_by(UserInvitation.created_at.desc()).limit(20).all()
return jsonify({
'success': True,
'invitations': [inv.to_dict() for inv in invitations]
})
@app.route('/api/invite/<invite_code>')
def check_invite(invite_code):
"""检查邀请码"""
inviter = User.query.filter_by(invite_code=invite_code).first()
if not inviter:
return jsonify({'valid': False, 'error': '邀请码无效'})
return jsonify({
'valid': True,
'inviter': inviter.username,
'reward': 5 # 被邀请人奖励
})
# ==================== 初始化 ====================
def init_app():
"""初始化应用"""

View File

@@ -39,19 +39,19 @@ USER_LIMITS = {
"daily_translations": 10,
"max_pages": 50,
"max_file_size": 30 * 1024 * 1024, # 30MB
"features": ["basic_translate", "retranslate", "export_pdf", "history"],
"features": ["basic_translate", "retranslate", "export_pdf", "history", "email_notify"],
},
"vip_basic": { # 基础会员 (月付 ¥29)
"daily_translations": 50,
"max_pages": 100,
"max_file_size": 50 * 1024 * 1024,
"features": ["basic_translate", "compare_view", "retranslate", "history", "priority_queue", "export_pdf"],
"features": ["basic_translate", "compare_view", "retranslate", "history", "priority_queue", "export_pdf", "email_notify", "email_attachment"],
},
"vip_pro": { # 专业会员 (月付 ¥99)
"daily_translations": 200,
"max_pages": 500,
"max_file_size": 100 * 1024 * 1024,
"features": ["basic_translate", "compare_view", "retranslate", "history", "priority_queue", "export_pdf", "batch_translate", "custom_terms"],
"features": ["basic_translate", "compare_view", "retranslate", "history", "priority_queue", "export_pdf", "batch_translate", "custom_terms", "email_notify", "email_attachment"],
},
"vip_enterprise": { # 企业会员 (年付 ¥999)
"daily_translations": -1, # 无限制

210
email_service.py Normal file
View File

@@ -0,0 +1,210 @@
"""
邮件发送服务
支持SMTP邮件发送、附件发送、模板渲染
"""
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
from datetime import datetime
from flask import current_app
class EmailService:
"""邮件发送服务"""
def __init__(self, smtp_host=None, smtp_port=None, smtp_user=None, smtp_pass=None):
self.smtp_host = smtp_host or os.environ.get('SMTP_HOST', 'mail.tphai.com')
self.smtp_port = int(smtp_port or os.environ.get('SMTP_PORT', '587'))
self.smtp_user = smtp_user or os.environ.get('SMTP_USER', 'favor@tphai.com')
self.smtp_pass = smtp_pass or os.environ.get('SMTP_PASS', 'favor@!')
def send_email(self, to_email, subject, body, attachment_path=None, attachment_name=None):
"""发送邮件(支持附件)"""
try:
# 创建邮件对象
msg = MIMEMultipart()
msg['From'] = self.smtp_user
msg['To'] = to_email
msg['Subject'] = subject
msg['Date'] = datetime.now().strftime('%a, %d %b %Y %H:%M:%S +0800')
msg['Reply-To'] = to_email
# 正文
msg.attach(MIMEText(body, 'html', 'utf-8'))
# 附件
if attachment_path and os.path.exists(attachment_path):
with open(attachment_path, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment',
filename=attachment_name or os.path.basename(attachment_path))
msg.attach(part)
# 发送
server = smtplib.SMTP(self.smtp_host, self.smtp_port)
server.ehlo()
server.login(self.smtp_user, self.smtp_pass)
server.sendmail(self.smtp_user, to_email, msg.as_string())
server.quit()
return True, "发送成功"
except Exception as e:
return False, str(e)
def send_translation_complete(self, user_email, username, filename, output_path, translation_id):
"""翻译完成通知"""
subject = f"【PDF翻译助手】翻译完成 - {filename}"
body = f"""
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0;">📄 PDF翻译助手</h1>
</div>
<div style="background: white; padding: 30px; border: 1px solid #eee; border-radius: 0 0 10px 10px;">
<p style="color: #333; font-size: 16px;">您好,{username}</p>
<p style="color: #666;">您的翻译任务已完成:</p>
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0;">
<p style="margin: 5px 0;"><strong>文件:</strong>{filename}</p>
<p style="margin: 5px 0;"><strong>状态:</strong><span style="color: #28a745;">✅ 完成</span></p>
<p style="margin: 5px 0;"><strong>时间:</strong>{datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
</div>
<p style="color: #666;">翻译结果已作为附件发送,您也可以登录网站查看详情。</p>
<div style="text-align: center; margin: 30px 0;">
<a href="http://localhost:19000/translation/{translation_id}"
style="background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px;">
查看翻译结果
</a>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
<p style="color: #999; font-size: 12px; text-align: center;">
此邮件由系统自动发送,请勿回复。<br>
PDF翻译助手 - 让翻译更简单
</p>
</div>
</body>
</html>
"""
# 附件名称
attachment_name = f"{filename}_translated.md"
return self.send_email(user_email, subject, body, output_path, attachment_name)
def send_welcome_email(self, user_email, username, invite_code=None):
"""欢迎邮件"""
subject = "欢迎加入PDF翻译助手"
body = f"""
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0;">📄 PDF翻译助手</h1>
</div>
<div style="background: white; padding: 30px; border: 1px solid #eee; border-radius: 0 0 10px 10px;">
<p style="color: #333; font-size: 16px;">您好,{username}</p>
<p style="color: #666;">欢迎加入PDF翻译助手您已获得</p>
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0;">
<p style="margin: 5px 0;">✅ 每日免费翻译 10 次</p>
<p style="margin: 5px 0;">✅ 单文件最大 50 页</p>
<p style="margin: 5px 0;">✅ 翻译历史记录</p>
<p style="margin: 5px 0;">✅ 不满意重新翻译</p>
</div>
{"<p style='color: #666;'>您的专属邀请码:<strong style='color: #667eea;'>" + invite_code + "</strong>,分享给好友可获得奖励!</p>" if invite_code else ""}
<div style="text-align: center; margin: 30px 0;">
<a href="http://localhost:19000/"
style="background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px;">
开始使用
</a>
</div>
</div>
</body>
</html>
"""
return self.send_email(user_email, subject, body)
def send_expire_reminder(self, user_email, username, expire_date, user_type):
"""会员到期提醒"""
subject = "【PDF翻译助手】会员即将到期提醒"
body = f"""
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0;">📄 PDF翻译助手</h1>
</div>
<div style="background: white; padding: 30px; border: 1px solid #eee; border-radius: 0 0 10px 10px;">
<p style="color: #333; font-size: 16px;">您好,{username}</p>
<div style="background: #fff3cd; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #ffc107;">
<p style="margin: 5px 0; color: #856404;">⚠️ 您的会员即将到期</p>
<p style="margin: 5px 0;"><strong>会员类型:</strong>{user_type}</p>
<p style="margin: 5px 0;"><strong>到期时间:</strong>{expire_date}</p>
</div>
<p style="color: #666;">到期后将降级为免费用户每日翻译次数限制为10次。续费可继续享受会员权益。</p>
<div style="text-align: center; margin: 30px 0;">
<a href="http://localhost:19000/pricing"
style="background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px;">
续费会员
</a>
</div>
</div>
</body>
</html>
"""
return self.send_email(user_email, subject, body)
def send_invite_reward(self, user_email, username, reward_amount, invitee_count):
"""邀请奖励通知"""
subject = f"【PDF翻译助手】邀请奖励 - ¥{reward_amount}"
body = f"""
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="color: white; margin: 0;">🎉 邀请奖励已发放</h1>
</div>
<div style="background: white; padding: 30px; border: 1px solid #eee; border-radius: 0 0 10px 10px;">
<p style="color: #333; font-size: 16px;">您好,{username}</p>
<div style="background: #d4edda; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #28a745;">
<p style="margin: 5px 0; color: #155724;"><strong>奖励金额:</strong>¥{reward_amount}</p>
<p style="margin: 5px 0; color: #155724;"><strong>累计邀请:</strong>{invitee_count}人</p>
</div>
<p style="color: #666;">奖励已发放到您的账户余额,可用于购买会员或翻译服务。</p>
<div style="text-align: center; margin: 30px 0;">
<a href="http://localhost:19000/profile"
style="background: #28a745; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px;">
查看余额
</a>
</div>
</div>
</body>
</html>
"""
return self.send_email(user_email, subject, body)
# 全局邮件服务实例
email_service = EmailService()

236
models.py
View File

@@ -41,8 +41,26 @@ class User(db.Model):
# 余额
balance = db.Column(db.Float, default=0.0) # 账户余额(元)
# 手机号
phone = db.Column(db.String(20), nullable=True) # 手机号
phone_verified = db.Column(db.Boolean, default=False) # 手机号已验证
# 邀请系统
invite_code = db.Column(db.String(10), unique=True, nullable=True) # 用户专属邀请码
invited_by = db.Column(db.Integer, nullable=True) # 邀请人ID
invite_count = db.Column(db.Integer, default=0) # 已邀请人数
invite_rewards = db.Column(db.Float, default=0.0) # 邀请奖励金额
# 邮件通知设置
email_notify = db.Column(db.Boolean, default=True) # 邮件通知开关
notify_on_complete = db.Column(db.Boolean, default=True) # 翻译完成通知
notify_with_attachment = db.Column(db.Boolean, default=False) # 邮件带附件VIP功能
notify_on_expire = db.Column(db.Boolean, default=True) # 会员到期提醒
# 关系
translations = db.relationship('Translation', backref='user', lazy=True)
invitations = db.relationship('UserInvitation', backref='inviter', lazy=True,
foreign_keys='UserInvitation.inviter_id')
recharges = db.relationship('UserRecharge', backref='user', lazy=True)
refunds = db.relationship('UserRefund', backref='user', lazy=True)
@@ -103,11 +121,20 @@ class User(db.Model):
'id': self.id,
'username': self.username,
'email': self.email,
'phone': self.phone,
'phone_verified': self.phone_verified,
'user_type': self.user_type,
'is_vip': self.is_vip(),
'is_admin': self.is_admin,
'is_active': self.is_active,
'balance': self.balance,
'invite_code': self.invite_code,
'invite_count': self.invite_count,
'invite_rewards': self.invite_rewards,
'email_notify': self.email_notify,
'notify_on_complete': self.notify_on_complete,
'notify_with_attachment': self.notify_with_attachment,
'notify_on_expire': self.notify_on_expire,
'daily_count': self.daily_count,
'total_count': self.total_count,
'created_at': self.created_at.isoformat() if self.created_at else None,
@@ -709,4 +736,213 @@ class AccountTransaction(db.Model):
'related_type': self.related_type,
'description': self.description,
'created_at': self.created_at.isoformat() if self.created_at else None,
}
# ==================== 用户邀请记录 ====================
class UserInvitation(db.Model):
"""用户邀请记录"""
__tablename__ = 'user_invitations'
id = db.Column(db.Integer, primary_key=True)
# 邀请人
inviter_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
invite_code = db.Column(db.String(10), nullable=False) # 使用的邀请码
# 被邀请人
invitee_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # 注册后关联
invitee_email = db.Column(db.String(120), nullable=True) # 被邀请人邮箱(注册前)
# 奖励
reward_amount = db.Column(db.Float, default=0.0) # 奖励金额
reward_given = db.Column(db.Boolean, default=False) # 是否已发放奖励
# 状态
status = db.Column(db.String(20), default='pending') # pending, registered, rewarded
# 时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
registered_at = db.Column(db.DateTime, nullable=True) # 被邀请人注册时间
rewarded_at = db.Column(db.DateTime, nullable=True) # 奖励发放时间
# 关系
invitee = db.relationship('User', backref=db.backref('invited_by_record', lazy=True),
foreign_keys=[invitee_id])
def to_dict(self):
return {
'id': self.id,
'inviter_id': self.inviter_id,
'invite_code': self.invite_code,
'invitee_id': self.invitee_id,
'invitee_email': self.invitee_email,
'reward_amount': self.reward_amount,
'reward_given': self.reward_given,
'status': self.status,
'created_at': self.created_at.isoformat() if self.created_at else None,
}
# ==================== 邀请奖励配置 ====================
class InviteRewardConfig(db.Model):
"""邀请奖励配置"""
__tablename__ = 'invite_reward_config'
id = db.Column(db.Integer, primary_key=True)
# 奖励规则
reward_amount = db.Column(db.Float, default=5.0) # 邀请奖励金额(元)
invitee_bonus = db.Column(db.Float, default=2.0) # 被邀请人 bonus
min_invitee_user_type = db.Column(db.String(20), default='free') # 被邀请人最低等级才能奖励
# 限制
max_daily_invites = db.Column(db.Integer, default=10) # 每日最大邀请次数
max_total_invites = db.Column(db.Integer, default=100) # 总最大邀请次数
# 状态
is_active = db.Column(db.Boolean, default=True)
# 时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'reward_amount': self.reward_amount,
'invitee_bonus': self.invitee_bonus,
'min_invitee_user_type': self.min_invitee_user_type,
'max_daily_invites': self.max_daily_invites,
'max_total_invites': self.max_total_invites,
'is_active': self.is_active,
}
# ==================== 邮件通知记录 ====================
class EmailNotification(db.Model):
"""邮件通知记录"""
__tablename__ = 'email_notifications'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
# 邮件信息
email_to = db.Column(db.String(120), nullable=False) # 收件人
email_type = db.Column(db.String(30), nullable=False) # complete, expire, invoice, welcome
# 附件
has_attachment = db.Column(db.Boolean, default=False)
attachment_path = db.Column(db.String(255), nullable=True) # 附件文件路径
attachment_name = db.Column(db.String(100), nullable=True) # 附件显示名称
# 内容
subject = db.Column(db.String(200), nullable=True)
body_preview = db.Column(db.Text, nullable=True) # 正文预览
# 状态
status = db.Column(db.String(20), default='pending') # pending, sent, failed
error_message = db.Column(db.String(255), nullable=True)
# 关联
related_id = db.Column(db.Integer, nullable=True) # 关联ID翻译ID、订单ID等
related_type = db.Column(db.String(30), nullable=True) # 关联类型
# 时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
sent_at = db.Column(db.DateTime, nullable=True)
# 关系
user = db.relationship('User', backref=db.backref('email_notifications', lazy=True))
def to_dict(self):
return {
'id': self.id,
'user_id': self.user_id,
'email_to': self.email_to,
'email_type': self.email_type,
'has_attachment': self.has_attachment,
'subject': self.subject,
'status': self.status,
'created_at': self.created_at.isoformat() if self.created_at else None,
'sent_at': self.sent_at.isoformat() if self.sent_at else None,
}
# ==================== 备用大模型接口配置 ====================
class BackupLLMConfig(db.Model):
"""备用大模型接口配置"""
__tablename__ = 'backup_llm_config'
id = db.Column(db.Integer, primary_key=True)
# 服务商信息
provider_name = db.Column(db.String(100), nullable=False) # 服务商名称: OpenAI, DeepSeek, 阿里百炼, etc.
api_base = db.Column(db.String(255), nullable=False) # API地址
api_key = db.Column(db.String(255), nullable=True) # API Key可选
model = db.Column(db.String(100), nullable=True) # 默认模型
# 状态
is_active = db.Column(db.Boolean, default=True) # 是否启用
sort_order = db.Column(db.Integer, default=0) # 排序
# 备注
description = db.Column(db.String(255), nullable=True) # 备注
# 时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'provider_name': self.provider_name,
'api_base': self.api_base,
'api_key': self.api_key,
'model': self.model,
'is_active': self.is_active,
'sort_order': self.sort_order,
'description': self.description,
'created_at': self.created_at.isoformat() if self.created_at else None,
}
# ==================== 邮件模板配置 ====================
class EmailTemplateConfig(db.Model):
"""邮件模板配置"""
__tablename__ = 'email_template_config'
id = db.Column(db.Integer, primary_key=True)
template_key = db.Column(db.String(50), unique=True, nullable=False) # complete, expire, invoice, welcome, invite
template_name = db.Column(db.String(100), nullable=False) # 显示名称
# 模板内容
subject_template = db.Column(db.String(200), nullable=False) # 主题模板
body_template = db.Column(db.Text, nullable=False) # 正文模板(支持变量)
# 支持变量说明
variables = db.Column(db.Text, nullable=True) # JSON: ["username", "filename", ...]
# 状态
is_active = db.Column(db.Boolean, default=True)
# 时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def get_variables(self):
import json
try:
return json.loads(self.variables) if self.variables else []
except:
return []
def to_dict(self):
return {
'id': self.id,
'template_key': self.template_key,
'template_name': self.template_name,
'subject_template': self.subject_template,
'is_active': self.is_active,
}

View File

@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>备用大模型接口 - 后台管理</title>
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background-color: #f5f5f5; }
.sidebar { position: fixed; top: 0; left: 0; height: 100vh; width: 250px; background: #343a40; padding-top: 60px; }
.sidebar .nav-link { color: #adb5bd; padding: 12px 20px; }
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background: rgba(255,255,255,0.1); }
.main-content { margin-left: 250px; padding: 20px; }
.status-badge { font-size: 0.75rem; }
</style>
</head>
<body>
<nav class="sidebar">
<div class="position-absolute top-0 w-100 p-3 border-bottom border-secondary">
<h5 class="text-white mb-0"><i class="bi bi-gear-fill"></i> 后台管理</h5>
</div>
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.dashboard') }}"><i class="bi bi-speedometer2"></i> 数据概览</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.users') }}"><i class="bi bi-people"></i> 用户管理</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.translations') }}"><i class="bi bi-file-text"></i> 翻译记录</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.cache_list') }}"><i class="bi bi-database"></i> 缓存管理</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.logs') }}"><i class="bi bi-list-check"></i> 操作日志</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
<li class="nav-item"><a class="nav-link active" href="{{ url_for('admin.backup_llm_list') }}"><i class="bi bi-cloud"></i> 备用大模型</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.settings') }}"><i class="bi bi-sliders"></i> 系统配置</a></li>
</ul>
<div class="position-absolute bottom-0 w-100 p-3 border-top border-secondary">
<a href="/" class="btn btn-outline-light btn-sm w-100"><i class="bi bi-house"></i> 返回前台</a>
</div>
</nav>
<main class="main-content">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5><i class="bi bi-cloud"></i> 备用大模型接口</h5>
<div>
<a href="{{ url_for('admin.add_backup_llm') }}" class="btn btn-primary"><i class="bi bi-plus-lg"></i> 新增接口</a>
<button class="btn btn-outline-secondary" onclick="initDefaults()"><i class="bi bi-arrow-counterclockwise"></i> 初始化默认</button>
</div>
</div>
<div class="card">
<div class="card-body">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th style="width: 50px;">#</th>
<th>服务商</th>
<th>API地址</th>
<th>模型</th>
<th style="width: 80px;">状态</th>
<th style="width: 150px;">操作</th>
</tr>
</thead>
<tbody>
{% for config in configs %}
<tr id="row-{{ config.id }}">
<td>{{ config.sort_order }}</td>
<td>
<strong>{{ config.provider_name }}</strong>
{% if config.description %}
<br><small class="text-muted">{{ config.description }}</small>
{% endif %}
</td>
<td><code>{{ config.api_base }}</code></td>
<td><code>{{ config.model or '默认' }}</code></td>
<td>
{% if config.is_active %}
<span class="badge bg-success status-badge">启用</span>
{% else %}
<span class="badge bg-secondary status-badge">禁用</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="testConfig({{ config.id }})" title="测试连接">
<i class="bi bi-plug"></i>
</button>
<a href="{{ url_for('admin.edit_backup_llm', config_id=config.id) }}" class="btn btn-sm btn-outline-secondary" title="编辑">
<i class="bi bi-pencil"></i>
</a>
<button class="btn btn-sm btn-outline-warning" onclick="toggleConfig({{ config.id }})" title="切换状态">
<i class="bi bi-toggle2"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteConfig({{ config.id }})" title="删除">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not configs %}
<div class="text-center text-muted py-4">
<i class="bi bi-cloud-slash" style="font-size: 2rem;"></i>
<p>暂无备用大模型配置</p>
<button class="btn btn-outline-primary" onclick="initDefaults()">点击初始化默认配置</button>
</div>
{% endif %}
</div>
</div>
<div id="testResult" class="mt-3" style="display:none;"></div>
</main>
<script>
function testConfig(id) {
const resultDiv = document.getElementById('testResult');
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div class="alert alert-info"><i class="bi bi-hourglass-split"></i> 正在测试连接...</div>';
fetch(`/admin/backup-llm/${id}/test`, {method: 'POST'})
.then(r => r.json())
.then(res => {
if (res.success) {
resultDiv.innerHTML = `<div class="alert alert-success"><i class="bi bi-check-circle"></i> ${res.provider} 连接成功!模型: ${res.model}</div>`;
} else {
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 连接失败: ${res.error}</div>`;
}
})
.catch(err => {
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 请求失败: ${err}</div>`;
});
}
function toggleConfig(id) {
fetch(`/admin/backup-llm/${id}/toggle`, {method: 'POST'})
.then(r => r.json())
.then(res => {
if (res.success) {
location.reload();
} else {
alert('操作失败');
}
});
}
function deleteConfig(id) {
if (confirm('确定删除此备用大模型接口吗?')) {
fetch(`/admin/backup-llm/${id}/delete`, {method: 'POST'})
.then(r => r.json())
.then(res => {
if (res.success) {
document.getElementById(`row-${id}`).remove();
} else {
alert('删除失败');
}
});
}
}
function initDefaults() {
if (confirm('初始化默认备用大模型配置?')) {
fetch('/admin/backup-llm/init', {method: 'POST'})
.then(r => r.json())
.then(res => {
if (res.success) {
location.reload();
} else {
alert('初始化失败');
}
});
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,188 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{{ config ? '编辑' : '添加' }}备用大模型接口 - 后台管理</title>
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background-color: #f5f5f5; }
.sidebar { position: fixed; top: 0; left: 0; height: 100vh; width: 250px; background: #343a40; padding-top: 60px; }
.sidebar .nav-link { color: #adb5bd; padding: 12px 20px; }
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background: rgba(255,255,255,0.1); }
.main-content { margin-left: 250px; padding: 20px; }
</style>
</head>
<body>
<nav class="sidebar">
<div class="position-absolute top-0 w-100 p-3 border-bottom border-secondary">
<h5 class="text-white mb-0"><i class="bi bi-gear-fill"></i> 后台管理</h5>
</div>
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.dashboard') }}"><i class="bi bi-speedometer2"></i> 数据概览</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.users') }}"><i class="bi bi-people"></i> 用户管理</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.translations') }}"><i class="bi bi-file-text"></i> 翻译记录</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.cache_list') }}"><i class="bi bi-database"></i> 缓存管理</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.logs') }}"><i class="bi bi-list-check"></i> 操作日志</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
<li class="nav-item"><a class="nav-link active" href="{{ url_for('admin.backup_llm_list') }}"><i class="bi bi-cloud"></i> 备用大模型</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.settings') }}"><i class="bi bi-sliders"></i> 系统配置</a></li>
</ul>
<div class="position-absolute bottom-0 w-100 p-3 border-top border-secondary">
<a href="/" class="btn btn-outline-light btn-sm w-100"><i class="bi bi-house"></i> 返回前台</a>
</div>
</nav>
<main class="main-content">
<div class="card">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-cloud-plus"></i> {{ config ? '编辑备用大模型接口' : '添加备用大模型接口' }}</h6>
</div>
<div class="card-body">
<form id="configForm" onsubmit="return saveConfig(event)">
<div class="mb-3">
<label class="form-label">服务商名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="provider_name"
value="{{ config.provider_name if config else '' }}"
placeholder="例如: OpenAI, DeepSeek, 阿里百炼" required>
<small class="text-muted">显示名称,方便识别</small>
</div>
<div class="mb-3">
<label class="form-label">API地址 <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="api_base"
value="{{ config.api_base if config else '' }}"
placeholder="https://api.openai.com/v1" required>
<small class="text-muted">LLM服务的API endpoint</small>
</div>
<div class="mb-3">
<label class="form-label">API Key</label>
<input type="text" class="form-control" name="api_key"
value="{{ config.api_key if config else '' }}"
placeholder="sk-xxx可选">
<small class="text-muted">如果不需要可留空</small>
</div>
<div class="mb-3">
<label class="form-label">默认模型</label>
<input type="text" class="form-control" name="model"
value="{{ config.model if config else '' }}"
placeholder="gpt-4, deepseek-chat">
<small class="text-muted">推荐使用的模型ID</small>
</div>
<div class="mb-3">
<label class="form-label">备注</label>
<input type="text" class="form-control" name="description"
value="{{ config.description if config else '' }}"
placeholder="接口说明">
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">排序</label>
<input type="number" class="form-control" name="sort_order"
value="{{ config.sort_order if config else 0 }}">
</div>
<div class="col-md-6">
<label class="form-label">状态</label>
<select class="form-select" name="is_active">
<option value="true" {% if not config or config.is_active %}selected{% endif %}>启用</option>
<option value="false" {% if config and not config.is_active %}selected{% endif %}>禁用</option>
</select>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> 保存</button>
<button type="button" class="btn btn-outline-secondary" onclick="testConfig()"><i class="bi bi-plug"></i> 测试连接</button>
<a href="{{ url_for('admin.backup_llm_list') }}" class="btn btn-outline-secondary"><i class="bi bi-arrow-left"></i> 返回</a>
</div>
</form>
<div id="testResult" class="mt-3" style="display:none;"></div>
</div>
</div>
</main>
<script>
function saveConfig(e) {
e.preventDefault();
const formData = new FormData(document.getElementById('configForm'));
const data = {};
formData.forEach((value, key) => {
if (key === 'sort_order') {
data[key] = parseInt(value) || 0;
} else if (key === 'is_active') {
data[key] = value === 'true';
} else {
data[key] = value;
}
});
{% if config %}
const url = `/admin/backup-llm/{{ config.id }}/edit`;
{% else %}
const url = '/admin/backup-llm/add';
{% endif %}
fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(r => r.json())
.then(res => {
if (res.success) {
alert('保存成功!');
window.location.href = '/admin/backup-llm';
} else {
alert('保存失败: ' + (res.error || '未知错误'));
}
})
.catch(err => alert('请求失败: ' + err));
return false;
}
function testConfig() {
const formData = new FormData(document.getElementById('configForm'));
const resultDiv = document.getElementById('testResult');
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div class="alert alert-info"><i class="bi bi-hourglass-split"></i> 正在测试连接...</div>';
// 先临时保存,然后测试
{% if config %}
// 已有配置,直接测试
fetch(`/admin/backup-llm/{{ config.id }}/test`, {method: 'POST'})
{% else %}
// 新配置,需要先创建
const data = {};
formData.forEach((value, key) => {
if (key === 'sort_order') data[key] = parseInt(value) || 0;
else if (key === 'is_active') data[key] = value === 'true';
else data[key] = value;
});
// 直接用输入的数据测试(不保存)
resultDiv.innerHTML = '<div class="alert alert-warning"><i class="bi bi-info-circle"></i> 新配置请先保存后再测试</div>';
return;
{% endif %}
.then(r => r.json())
.then(res => {
if (res.success) {
resultDiv.innerHTML = `<div class="alert alert-success"><i class="bi bi-check-circle"></i> ${res.provider} 连接成功!模型: ${res.model}</div>`;
} else {
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 连接失败: ${res.error}</div>`;
}
})
.catch(err => {
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 请求失败: ${err}</div>`;
});
}
</script>
</body>
</html>

View File

@@ -90,20 +90,17 @@
</div>
<div class="card mt-3">
<div class="card-header"><h6 class="mb-0"><i class="bi bi-info-circle"></i> 常用模型配置参考</h6></div>
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-cloud"></i> 备用大模型接口</h6>
<a href="{{ url_for('admin.backup_llm_list') }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-gear"></i> 管理备用接口
</a>
</div>
<div class="card-body">
<table class="table table-sm">
<thead class="table-light">
<tr><th>服务商</th><th>API地址</th><th>模型示例</th></tr>
</thead>
<tbody>
<tr><td>本地LM Studio</td><td>http://localhost:1234/v1</td><td>根据加载的模型</td></tr>
<tr><td>OpenAI</td><td>https://api.openai.com/v1</td><td>gpt-4, gpt-3.5-turbo</td></tr>
<tr><td>DeepSeek</td><td>https://api.deepseek.com/v1</td><td>deepseek-chat</td></tr>
<tr><td>阿里百炼</td><td>https://dashscope.aliyuncs.com/compatible-mode/v1</td><td>qwen-turbo, qwen-plus</td></tr>
<tr><td>SiliconFlow</td><td>https://api.siliconflow.cn/v1</td><td>Qwen/Qwen2.5-72B-Instruct</td></tr>
</tbody>
</table>
<p class="text-muted">备用大模型接口用于主接口不可用时切换备用服务。支持手动新增、编辑、测试连接。</p>
<a href="{{ url_for('admin.backup_llm_list') }}" class="btn btn-outline-secondary">
<i class="bi bi-list"></i> 查看所有备用接口
</a>
</div>
</div>
</main>

View File

@@ -66,7 +66,7 @@
</div>
<!-- 动态套餐 -->
{% for plan in db_plans if plan.is_active %}
{% for plan in plans if plan.is_active %}
<div class="col-lg-3 col-md-6 mb-4">
<div class="card pricing-card h-100 {% if plan.is_recommended %}border-primary{% endif %}">
{% if plan.is_recommended %}

View File

@@ -134,7 +134,7 @@
</div>
<!-- 会员购买记录 -->
<div class="card">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">🎫 会员购买记录</h5>
</div>
@@ -157,6 +157,145 @@
</div>
</div>
</div>
<!-- 账户设置 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">⚙️ 账户设置</h5>
</div>
<div class="card-body">
<div class="row">
<!-- 手机号绑定 -->
<div class="col-md-6 mb-3">
<div class="border rounded p-3">
<h6 class="mb-3">📱 手机号</h6>
<div id="phoneStatus">
{% if user.phone %}
<p class="text-success">已绑定:{{ user.phone }}
{% if user.phone_verified %}<span class="badge bg-success">已验证</span>{% endif %}
</p>
<button class="btn btn-outline-secondary btn-sm" onclick="showPhoneModal()">更换手机号</button>
{% else %}
<p class="text-muted">未绑定手机号</p>
<button class="btn btn-primary btn-sm" onclick="showPhoneModal()">绑定手机号</button>
{% endif %}
</div>
</div>
</div>
<!-- 邮件通知设置 -->
<div class="col-md-6 mb-3">
<div class="border rounded p-3">
<h6 class="mb-3">📧 通知邮箱</h6>
<div id="emailStatus">
{% if user.email %}
<p class="mb-2">当前通知邮箱:<strong>{{ user.email }}</strong></p>
<button class="btn btn-outline-secondary btn-sm mb-3" onclick="showEmailModal()">更换通知邮箱</button>
{% else %}
<p class="text-muted">未设置通知邮箱</p>
<button class="btn btn-primary btn-sm" onclick="showEmailModal()">绑定邮箱</button>
{% endif %}
<hr class="my-2">
<!-- 翻译完成邮件通知(所有用户都有) -->
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="notifyComplete"
{% if user.notify_on_complete %}checked{% endif %}
onchange="updateNotifySettings()">
<label class="form-check-label">翻译完成邮件通知</label>
</div>
<!-- 邮件带附件VIP功能 -->
{% if has_email_attachment %}
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="notifyAttachment"
{% if user.notify_with_attachment %}checked{% endif %}
onchange="updateNotifySettings()">
<label class="form-check-label">邮件带附件发送 <span class="badge bg-success">VIP</span></label>
</div>
{% else %}
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" disabled>
<label class="form-check-label text-muted">邮件带附件发送 <span class="badge bg-secondary">需VIP</span></label>
<a href="/pricing" class="small ms-2">升级会员</a>
</div>
{% endif %}
<!-- 会员到期提醒 -->
{% if user.user_type.startswith('vip') %}
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="notifyExpire"
{% if user.notify_on_expire %}checked{% endif %}
onchange="updateNotifySettings()">
<label class="form-check-label">会员到期提醒</label>
</div>
{% endif %}
<small class="text-muted">通知将发送至上述邮箱</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 邀请好友 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">🎁 邀请好友</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="border rounded p-3 bg-light">
<h6>您的专属邀请码</h6>
<div class="input-group mb-3">
<input type="text" class="form-control form-control-lg text-center fw-bold"
id="inviteCode" value="{{ user.invite_code or '生成中...' }}" readonly>
<button class="btn btn-outline-primary" onclick="copyInviteCode()">复制</button>
</div>
<p class="text-muted mb-0">
分享邀请码给好友,好友注册后您可获得 <strong class="text-success">¥5</strong> 奖励!
</p>
</div>
</div>
<div class="col-md-6">
<div class="border rounded p-3">
<h6>邀请统计</h6>
<div class="d-flex justify-content-around text-center">
<div>
<div class="fs-4 fw-bold text-primary">{{ user.invite_count }}</div>
<small class="text-muted">已邀请</small>
</div>
<div>
<div class="fs-4 fw-bold text-success">¥{{ "%.2f"|format(user.invite_rewards) }}</div>
<small class="text-muted">累计奖励</small>
</div>
</div>
</div>
</div>
</div>
<!-- 邀请记录 -->
<div class="mt-3">
<h6>邀请记录</h6>
<table class="table table-sm">
<thead>
<tr>
<th>被邀请人</th>
<th>注册时间</th>
<th>奖励</th>
<th>状态</th>
</tr>
</thead>
<tbody id="inviteRecords">
<!-- 动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
</main>
<!-- 充值模态框 -->
@@ -229,6 +368,33 @@
</div>
</div>
<!-- 更换邮箱模态框 -->
<div class="modal fade" id="emailModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">更换通知邮箱</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="emailForm">
<div class="mb-3">
<label class="form-label">新邮箱地址</label>
<input type="email" class="form-control" id="newEmail" placeholder="example@email.com" required>
</div>
<div class="alert alert-info">
<small>💡 更换后,通知邮件将发送至新邮箱</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="submitEmail()">确认更换</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 加载账户流水
@@ -340,10 +506,123 @@
// 初始化
loadTransactions();
loadPurchases();
loadInviteRecords();
document.getElementById('transactionFilter').addEventListener('change', function() {
loadTransactions(this.value);
});
// 加载邀请记录
function loadInviteRecords() {
fetch('/api/profile/invitations')
.then(r => r.json())
.then(data => {
const tbody = document.getElementById('inviteRecords');
if (data.invitations && data.invitations.length > 0) {
tbody.innerHTML = data.invitations.map(inv => {
const statusMap = { 'pending': '待注册', 'registered': '已注册', 'rewarded': '已奖励' };
const statusClass = { 'pending': 'text-muted', 'registered': 'text-info', 'rewarded': 'text-success' };
return `<tr>
<td>${inv.invitee_email || '用户' + inv.invitee_id}</td>
<td>${inv.created_at}</td>
<td>¥${inv.reward_amount.toFixed(2)}</td>
<td><span class="${statusClass[inv.status]}">${statusMap[inv.status]}</span></td>
</tr>`;
}).join('');
} else {
tbody.innerHTML = '<tr><td colspan="4" class="text-muted text-center">暂无邀请记录</td></tr>';
}
});
}
// 复制邀请码
function copyInviteCode() {
const code = document.getElementById('inviteCode').value;
navigator.clipboard.writeText(code).then(() => {
alert('邀请码已复制:' + code);
});
}
// 更新通知设置
function updateNotifySettings() {
const notifyComplete = document.getElementById('notifyComplete').checked;
const notifyExpire = document.getElementById('notifyExpire')?.checked || false;
const notifyAttachment = document.getElementById('notifyAttachment')?.checked || false;
fetch('/api/profile/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
notify_on_complete: notifyComplete,
notify_on_expire: notifyExpire,
notify_with_attachment: notifyAttachment
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
console.log('设置已更新');
} else if (data.feature === 'email_attachment') {
alert('邮件附件功能需要VIP会员');
location.href = '/pricing';
} else {
alert('更新失败:' + data.error);
}
});
}
// 绑定手机号
function showPhoneModal() {
// 简单处理,直接弹输入框
const phone = prompt('请输入手机号:');
if (phone && phone.length >= 10) {
fetch('/api/profile/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: phone })
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('手机号已绑定');
location.reload();
} else {
alert('绑定失败:' + data.error);
}
});
} else if (phone) {
alert('手机号格式不正确');
}
}
// 更换邮箱
function showEmailModal() {
new bootstrap.Modal(document.getElementById('emailModal')).show();
}
function submitEmail() {
const newEmail = document.getElementById('newEmail').value;
if (!newEmail || !newEmail.includes('@')) {
alert('请输入有效的邮箱地址');
return;
}
fetch('/api/profile/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: newEmail })
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('通知邮箱已更新为:' + newEmail);
location.reload();
} else {
alert('更新失败:' + data.error);
}
});
}
</script>
</body>
</html>