Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ef9df65d2 | |||
| 9a36b9245a | |||
| 56709b1a65 | |||
| 71a613ff5f | |||
| 4aac8ab04c |
206
admin.py
206
admin.py
@@ -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
165
app.py
@@ -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():
|
||||
"""初始化应用"""
|
||||
|
||||
@@ -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
210
email_service.py
Normal 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
236
models.py
@@ -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,
|
||||
}
|
||||
173
templates/admin/backup_llm.html
Normal file
173
templates/admin/backup_llm.html
Normal 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>
|
||||
188
templates/admin/backup_llm_form.html
Normal file
188
templates/admin/backup_llm_form.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user