feat: 添加手机号、邀请好友、邮件通知功能
- 用户模型添加:手机号、邀请码、邀请统计、邮件通知设置 - 邀请好友系统:专属邀请码、邀请奖励(¥5/人) - 邮件通知:翻译完成通知(含附件)、欢迎邮件、到期提醒 - 新增模型:UserInvitation, InviteRewardConfig, EmailNotification, EmailTemplateConfig - 个人中心添加:手机号绑定、通知设置、邀请好友模块 - email_service.py:邮件发送服务(支持附件) 新用户注册奖励:¥2 邀请人奖励:¥5/人
This commit is contained in:
138
app.py
138
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
|
||||
|
||||
@@ -591,12 +593,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 +613,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 +845,68 @@ 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 '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 '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():
|
||||
"""初始化应用"""
|
||||
|
||||
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()
|
||||
196
models.py
196
models.py
@@ -41,8 +41,25 @@ 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_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 +120,19 @@ 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_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,
|
||||
@@ -710,3 +735,174 @@ class AccountTransaction(db.Model):
|
||||
'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 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,
|
||||
}
|
||||
@@ -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,111 @@
|
||||
</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 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>
|
||||
<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>
|
||||
<small class="text-muted">通知发送至:{{ user.email }}</small>
|
||||
</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>
|
||||
|
||||
<!-- 充值模态框 -->
|
||||
@@ -340,10 +445,88 @@
|
||||
// 初始化
|
||||
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;
|
||||
|
||||
fetch('/api/profile/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
notify_on_complete: notifyComplete,
|
||||
notify_on_expire: notifyExpire
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 不刷新页面,只显示提示
|
||||
console.log('设置已更新');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定手机号
|
||||
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('手机号格式不正确');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user