9 Commits

Author SHA1 Message Date
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
1d36888488 feat: 套餐权益列表从后台UserTypeConfig动态读取
- 权益列表从数据库配置读取
- 支持按用户类型显示/状态
- 更新各等级用户权益配置
2026-04-14 18:42:21 +08:00
ee5e672901 feat: pricing页面使用后台管理配置的会员套餐数据
- 从数据库MembershipPlanConfig读取动态配置
- 支持推荐标记、原价显示
- 按用户状态显示按钮
2026-04-14 18:39:04 +08:00
4f33e92abf feat: 用户个人中心功能
- 账户余额显示和管理
- 充值功能(模拟模式)
- 退款申请功能(需管理员审核)
- 账户流水记录查看
- 会员购买记录查看
- 使用统计展示
- 新增模型:UserRecharge, UserRefund, MembershipPurchase, AccountTransaction
- User模型添加balance字段
- 导航栏添加个人中心入口
2026-04-14 18:29:46 +08:00
436ac2cb66 fix: pricing页面导航栏动态显示用户状态
- 登录用户显示:首页/会员套餐/翻译历史 + 用户名/退出
- 未登录用户显示:首页/会员套餐 + 登录/注册
- 添加navbar-toggler支持移动端展开
2026-04-14 18:21:18 +08:00
bae0ba9a6d feat: 套餐按钮根据用户会员状态显示
- 当前套餐:绿色disabled按钮
- 已升级:灰色disabled按钮
- 未登录/低等级:显示购买按钮
- pricing路由传递user对象
2026-04-14 18:18:42 +08:00
c1e929fc8a fix: 四个套餐卡片一排显示 (col-lg-3) 2026-04-14 18:13:16 +08:00
5572e41d08 feat: 更新会员套餐页面权益显示
- 添加免费用户卡片,显示可用权益
- 免费用户:重新翻译、导出PDF、对比查看
- 基础会员:增加对比查看功能
- 专业会员:完整功能列表
- 功能对比表同步更新
2026-04-14 18:09:25 +08:00
6 changed files with 1561 additions and 50 deletions

364
app.py
View File

@@ -14,7 +14,10 @@ from werkzeug.utils import secure_filename
from config import *
from models import (db, User, Translation, TranslationCache, GuestTranslation,
DataPackage, UserPackage, DynamicConfig)
DataPackage, UserPackage, DynamicConfig, UserRecharge, UserRefund,
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
@@ -206,7 +209,91 @@ def history():
@app.route('/pricing')
def pricing():
"""会员定价页"""
return render_template('pricing.html', plans=MEMBERSHIP_PLANS)
user = get_current_user()
# 权益名称映射
feature_names = {
'basic_translate': '基础翻译功能',
'history': '翻译历史记录',
'retranslate': '不满意重新翻译',
'export_pdf': '导出PDF格式',
'compare_view': '原文译文对比查看',
'batch_translate': '批量翻译',
'custom_terms': '自定义术语库',
'priority_queue': '优先处理队列',
'custom_instruction': '自定义翻译要求',
'api_access': 'API接口调用',
}
# 从数据库读取动态配置的会员套餐
db_plans = MembershipPlanConfig.query.filter_by(is_active=True)\
.order_by(MembershipPlanConfig.sort_order).all()
# 读取用户类型配置获取权益
user_types = UserTypeConfig.query.filter_by(is_active=True).all()
user_type_map = {ut.type_key: ut for ut in user_types}
# 为每个套餐添加权益列表
plan_features = {}
for plan in db_plans:
ut = user_type_map.get(plan.user_type_key)
if ut:
features = ut.get_features()
plan_features[plan.plan_key] = [
{'key': f, 'name': feature_names.get(f, f), 'has': True}
for f in features
]
# 添加限制信息
plan_features[plan.plan_key].insert(0, {
'key': 'daily_translations',
'name': f"每日翻译{ut.daily_translations if ut.daily_translations > 0 else '无限'}",
'has': True
})
plan_features[plan.plan_key].insert(1, {
'key': 'max_pages',
'name': f"单文件最大{ut.max_pages if ut.max_pages > 0 else '无限'}",
'has': True
})
# 免费用户权益
free_ut = user_type_map.get('free')
free_features = []
if free_ut:
free_features = [
{'key': 'daily_translations', 'name': f"每日翻译{free_ut.daily_translations}", 'has': True},
{'key': 'max_pages', 'name': f"单文件最大{free_ut.max_pages}", 'has': True},
]
for f in free_ut.get_features():
free_features.append({'key': f, 'name': feature_names.get(f, f), 'has': True})
# 添加没有的功能
all_features = ['compare_view', 'batch_translate', 'custom_terms']
for f in all_features:
if f not in free_ut.get_features():
free_features.append({'key': f, 'name': feature_names.get(f, f), 'has': False})
return render_template('pricing.html',
plans=db_plans,
plan_features=plan_features,
free_features=free_features,
user=user)
@app.route('/profile')
def profile():
"""个人中心"""
user = get_current_user()
if not user:
return redirect(url_for('login'))
limits = USER_LIMITS.get(user.user_type, USER_LIMITS['free'])
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 '无限'
return render_template('profile.html',
user=user,
daily_remaining=daily_remaining,
max_pages=max_pages
)
# ==================== 路由: API ====================
@@ -506,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():
@@ -523,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')
@@ -551,6 +706,207 @@ def user_info():
})
# ==================== 个人中心API ====================
@app.route('/api/profile/transactions')
def get_transactions():
"""获取账户流水"""
user = get_current_user()
if not user:
return jsonify({'error': '请登录'}), 401
filter_type = request.args.get('type', 'all')
query = AccountTransaction.query.filter_by(user_id=user.id)
if filter_type != 'all':
query = query.filter_by(transaction_type=filter_type)
transactions = query.order_by(AccountTransaction.created_at.desc()).limit(50).all()
return jsonify({
'success': True,
'transactions': [t.to_dict() for t in transactions]
})
@app.route('/api/profile/purchases')
def get_purchases():
"""获取会员购买记录"""
user = get_current_user()
if not user:
return jsonify({'error': '请登录'}), 401
purchases = MembershipPurchase.query.filter_by(user_id=user.id)\
.order_by(MembershipPurchase.created_at.desc()).limit(20).all()
return jsonify({
'success': True,
'purchases': [p.to_dict() for p in purchases]
})
@app.route('/api/profile/recharge', methods=['POST'])
def recharge_balance():
"""充值"""
user = get_current_user()
if not user:
return jsonify({'error': '请登录'}), 401
data = request.json
amount = float(data.get('amount', 0))
payment_method = data.get('payment_method', 'balance')
if amount < 10:
return jsonify({'error': '充值金额最少10元'}), 400
if amount > 10000:
return jsonify({'error': '充值金额最多10000元'}), 400
balance_before = user.balance
# 创建充值记录
order_no = f"RC{datetime.now().strftime('%Y%m%d%H%M%S')}{user.id}"
recharge = UserRecharge(
user_id=user.id,
amount=amount,
balance_before=balance_before,
payment_method=payment_method,
status='completed',
order_no=order_no,
completed_at=datetime.utcnow()
)
# 更新余额
user.balance += amount
recharge.balance_after = user.balance
db.session.add(recharge)
# 创建流水记录
transaction = AccountTransaction(
user_id=user.id,
transaction_type='recharge',
amount=amount,
balance_before=balance_before,
balance_after=user.balance,
related_id=recharge.id,
related_type='recharge',
description=f'充值¥{amount}'
)
db.session.add(transaction)
db.session.commit()
return jsonify({
'success': True,
'balance': user.balance,
'recharge_id': recharge.id
})
@app.route('/api/profile/refund', methods=['POST'])
def request_refund():
"""申请退款"""
user = get_current_user()
if not user:
return jsonify({'error': '请登录'}), 401
data = request.json
amount = float(data.get('amount', 0))
reason = data.get('reason', '')
if amount <= 0:
return jsonify({'error': '退款金额必须大于0'}), 400
if amount > user.balance:
return jsonify({'error': '退款金额不能超过余额'}), 400
if not reason:
return jsonify({'error': '请填写退款原因'}), 400
balance_before = user.balance
# 创建退款申请
refund = UserRefund(
user_id=user.id,
amount=amount,
balance_before=balance_before,
reason=reason,
reason_type='user_request',
status='pending'
)
db.session.add(refund)
db.session.commit()
return jsonify({
'success': True,
'refund_id': refund.id,
'message': '退款申请已提交,等待管理员审核'
})
@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
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()

385
models.py
View File

@@ -38,8 +38,30 @@ class User(db.Model):
is_active = db.Column(db.Boolean, default=True) # 是否启用
is_admin = db.Column(db.Boolean, default=False) # 是否管理员
# 余额
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)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
@@ -98,10 +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,
@@ -520,4 +551,358 @@ class MembershipPlanConfig(db.Model):
'is_active': self.is_active,
'is_recommended': self.is_recommended,
'is_system': self.is_system,
}
# ==================== 用户充值记录 ====================
class UserRecharge(db.Model):
"""用户充值记录"""
__tablename__ = 'user_recharges'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
# 充值信息
amount = db.Column(db.Float, nullable=False) # 充值金额
balance_before = db.Column(db.Float, default=0) # 充值前余额
balance_after = db.Column(db.Float, default=0) # 充值后余额
# 支付方式
payment_method = db.Column(db.String(20), default='balance') # balance, alipay, wechat, manual
# 状态
status = db.Column(db.String(20), default='pending') # pending, completed, failed, cancelled
order_no = db.Column(db.String(64), unique=True, nullable=True) # 订单号
# 备注
remark = db.Column(db.String(255), nullable=True) # 管理员备注
operator_id = db.Column(db.Integer, nullable=True) # 操作管理员ID
# 时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
completed_at = db.Column(db.DateTime, nullable=True)
def to_dict(self):
return {
'id': self.id,
'user_id': self.user_id,
'amount': self.amount,
'balance_before': self.balance_before,
'balance_after': self.balance_after,
'payment_method': self.payment_method,
'status': self.status,
'order_no': self.order_no,
'remark': self.remark,
'created_at': self.created_at.isoformat() if self.created_at else None,
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
}
# ==================== 用户退款记录 ====================
class UserRefund(db.Model):
"""用户退款记录"""
__tablename__ = 'user_refunds'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
recharge_id = db.Column(db.Integer, db.ForeignKey('user_recharges.id'), nullable=True) # 关联充值记录
# 退款信息
amount = db.Column(db.Float, nullable=False) # 退款金额
balance_before = db.Column(db.Float, default=0) # 退款前余额
balance_after = db.Column(db.Float, default=0) # 退款后余额
# 原因
reason = db.Column(db.String(255), nullable=True) # 退款原因
reason_type = db.Column(db.String(20), default='user_request') # user_request, system_error, admin_initiated
# 状态
status = db.Column(db.String(20), default='pending') # pending, approved, completed, rejected
# 操作
operator_id = db.Column(db.Integer, nullable=True) # 处理管理员ID
operator_remark = db.Column(db.String(255), nullable=True) # 管理员处理备注
# 时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
processed_at = db.Column(db.DateTime, nullable=True)
completed_at = db.Column(db.DateTime, nullable=True)
def to_dict(self):
return {
'id': self.id,
'user_id': self.user_id,
'recharge_id': self.recharge_id,
'amount': self.amount,
'balance_before': self.balance_before,
'balance_after': self.balance_after,
'reason': self.reason,
'reason_type': self.reason_type,
'status': self.status,
'operator_remark': self.operator_remark,
'created_at': self.created_at.isoformat() if self.created_at else None,
'processed_at': self.processed_at.isoformat() if self.processed_at else None,
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
}
# ==================== 会员购买记录 ====================
class MembershipPurchase(db.Model):
"""会员购买记录"""
__tablename__ = 'membership_purchases'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
# 套餐信息
plan_key = db.Column(db.String(50), nullable=False) # vip_basic, vip_pro, vip_enterprise
plan_name = db.Column(db.String(100), nullable=False) # 基础会员, 专业会员
# 价格
price = db.Column(db.Float, nullable=False) # 实付金额
original_price = db.Column(db.Float, nullable=True) # 原价
# 有效期
period_days = db.Column(db.Integer, default=30) # 购买天数
expire_before = db.Column(db.DateTime, nullable=True) # 购买前到期时间
expire_after = db.Column(db.DateTime, nullable=True) # 购买后到期时间
# 支付方式
payment_method = db.Column(db.String(20), default='balance') # balance, alipay, wechat
# 状态
status = db.Column(db.String(20), default='completed') # pending, completed, refunded
# 时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关系
user = db.relationship('User', backref=db.backref('membership_purchases', lazy=True))
def to_dict(self):
return {
'id': self.id,
'user_id': self.user_id,
'plan_key': self.plan_key,
'plan_name': self.plan_name,
'price': self.price,
'original_price': self.original_price,
'period_days': self.period_days,
'expire_after': self.expire_after.isoformat() if self.expire_after else None,
'payment_method': self.payment_method,
'status': self.status,
'created_at': self.created_at.isoformat() if self.created_at else None,
}
# ==================== 账户流水记录 ====================
class AccountTransaction(db.Model):
"""账户流水记录"""
__tablename__ = 'account_transactions'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
# 流水信息
transaction_type = db.Column(db.String(20), nullable=False) # recharge, refund, purchase, consume, refund_purchase
amount = db.Column(db.Float, nullable=False) # 金额(正数加,负数减)
balance_before = db.Column(db.Float, default=0) # 操作前余额
balance_after = db.Column(db.Float, default=0) # 操作后余额
# 关联
related_id = db.Column(db.Integer, nullable=True) # 关联记录ID充值ID、退款ID等
related_type = db.Column(db.String(50), nullable=True) # 关联类型recharge, refund, membership_purchase等
# 描述
description = db.Column(db.String(255), nullable=True) # 流水描述
# 时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关系
user = db.relationship('User', backref=db.backref('transactions', lazy=True))
def to_dict(self):
return {
'id': self.id,
'user_id': self.user_id,
'transaction_type': self.transaction_type,
'amount': self.amount,
'balance_before': self.balance_before,
'balance_after': self.balance_after,
'related_id': self.related_id,
'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 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

@@ -22,6 +22,7 @@
<li class="nav-item"><a class="nav-link" href="/pricing">会员套餐</a></li>
{% if user %}
<li class="nav-item"><a class="nav-link" href="/history">翻译历史</a></li>
<li class="nav-item"><a class="nav-link" href="/profile">个人中心</a></li>
{% endif %}
</ul>
<div class="navbar-nav">

View File

@@ -12,9 +12,27 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">📄 PDF翻译助手</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/">首页</a>
<a class="nav-link" href="/login">登录</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item"><a class="nav-link" href="/">首页</a></li>
<li class="nav-item"><a class="nav-link active" href="/pricing">会员套餐</a></li>
{% if user %}
<li class="nav-item"><a class="nav-link" href="/history">翻译历史</a></li>
<li class="nav-item"><a class="nav-link" href="/profile">个人中心</a></li>
{% endif %}
</ul>
<div class="navbar-nav">
{% if user %}
<span class="nav-link text-light">👋 {{ user.username }}</span>
<a class="nav-link" href="/logout">退出</a>
{% else %}
<a class="nav-link" href="/login">登录</a>
<a class="nav-link" href="/register">注册</a>
{% endif %}
</div>
</div>
</div>
</nav>
@@ -23,68 +41,69 @@
<h2 class="text-center mb-5">会员套餐</h2>
<div class="row justify-content-center">
<!-- 基础会员 -->
<div class="col-md-4 mb-4">
<!-- 免费用户(固定) -->
<div class="col-lg-3 col-md-6 mb-4">
<div class="card pricing-card h-100">
<div class="card-body text-center">
<h4 class="card-title">基础会员</h4>
<div class="price my-3">¥29<small>/</small></div>
<h4 class="card-title">免费用户</h4>
<div class="price my-3">¥0<small>/永久</small></div>
<ul class="features text-start">
<li>✅ 每日翻译50次</li>
<li>✅ 单文件最大100页</li>
<li>✅ 翻译历史记录</li>
<li>✅ 优先处理队列</li>
<li>✅ 导出PDF格式</li>
{% for feat in free_features %}
<li>{% if feat.has %}✅{% else %}<span class="text-muted"></span>{% endif %} {{ feat.name }}</li>
{% endfor %}
</ul>
<button class="btn btn-outline-primary w-100 mt-3">立即购买</button>
{% if user and user.user_type in ['free', 'vip_basic', 'vip_pro', 'vip_enterprise', 'admin'] %}
<span class="btn btn-secondary w-100 mt-3 disabled">
{% if user.user_type == 'free' %}当前套餐{% else %}已升级{% endif %}
</span>
{% else %}
<a href="/register" class="btn btn-outline-secondary w-100 mt-3">免费注册</a>
{% endif %}
</div>
</div>
</div>
<!-- 专业会员 -->
<div class="col-md-4 mb-4">
<div class="card pricing-card h-100 border-primary">
<!-- 动态套餐 -->
{% 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 %}
<div class="card-header bg-primary text-white text-center">
<strong>推荐</strong>
</div>
{% endif %}
<div class="card-body text-center">
<h4 class="card-title">专业会员</h4>
<div class="price my-3">¥99<small>/月</small></div>
<h4 class="card-title">{{ plan.display_name }}</h4>
<div class="price my-3">
¥{{ plan.price }}<small>/{{ plan.period }}</small>
{% if plan.original_price and plan.original_price > plan.price %}
<br><span class="text-muted" style="font-size:0.8em">原价¥{{ plan.original_price }}</span>
{% endif %}
</div>
<p class="text-muted">{{ plan.description }}</p>
<ul class="features text-start">
<li>✅ 每日翻译200次</li>
<li>单文件最大500页</li>
<li>✅ 所有基础会员功能</li>
<li>✅ 批量翻译</li>
<li>✅ 自定义术语库</li>
{% for feat in plan_features.get(plan.plan_key, []) %}
<li>{{ feat.name }}</li>
{% endfor %}
</ul>
<button class="btn btn-primary w-100 mt-3">立即购买</button>
</div>
</div>
</div>
<!-- 企业会员 -->
<div class="col-md-4 mb-4">
<div class="card pricing-card h-100">
<div class="card-body text-center">
<h4 class="card-title">企业会员</h4>
<div class="price my-3">¥999<small>/年</small></div>
<ul class="features text-start">
<li>✅ 翻译次数无限制</li>
<li>✅ 页数无限制</li>
<li>✅ 所有功能解锁</li>
<li>✅ 专属客服支持</li>
<li>✅ API接口调用</li>
</ul>
<button class="btn btn-outline-primary w-100 mt-3">联系购买</button>
{% if user and user.user_type == plan.user_type_key %}
<span class="btn btn-success w-100 mt-3 disabled">当前套餐</span>
{% elif user and user.user_type in ['vip_enterprise', 'admin'] %}
<span class="btn btn-secondary w-100 mt-3 disabled">已升级</span>
{% elif user and user.user_type in ['vip_pro', 'vip_basic'] and plan.user_type_key not in ['vip_basic'] %}
<span class="btn btn-secondary w-100 mt-3 disabled">已升级</span>
{% else %}
<button class="btn {% if plan.is_recommended %}btn-primary{% else %}btn-outline-primary{% endif %} w-100 mt-3">立即购买</button>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
<!-- 功能对比表 -->
@@ -148,7 +167,7 @@
<tr>
<td>对比查看</td>
<td class="text-danger"></td>
<td class="text-success"></td>
<td class="text-danger"></td>
<td class="text-success"></td>
<td class="text-success"></td>
<td class="text-success"></td>
@@ -156,7 +175,7 @@
<tr>
<td>导出PDF</td>
<td class="text-danger"></td>
<td class="text-danger"></td>
<td class="text-success"></td>
<td class="text-success"></td>
<td class="text-success"></td>
<td class="text-success"></td>
@@ -177,6 +196,14 @@
<td class="text-success"></td>
<td class="text-success"></td>
</tr>
<tr>
<td>自定义翻译要求</td>
<td class="text-danger"></td>
<td class="text-danger"></td>
<td class="text-danger"></td>
<td class="text-danger"></td>
<td class="text-success"></td>
</tr>
<tr>
<td>API调用</td>
<td class="text-danger"></td>

532
templates/profile.html Normal file
View File

@@ -0,0 +1,532 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>个人中心 - PDF翻译助手</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="/static/css/style.css" rel="stylesheet">
<style>
.balance-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.stat-item { border-left: 3px solid #dee2e6; padding-left: 12px; }
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">📄 PDF翻译助手</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item"><a class="nav-link" href="/">首页</a></li>
<li class="nav-item"><a class="nav-link" href="/pricing">会员套餐</a></li>
<li class="nav-item"><a class="nav-link" href="/history">翻译历史</a></li>
<li class="nav-item"><a class="nav-link active" href="/profile">个人中心</a></li>
</ul>
<div class="navbar-nav">
<span class="nav-link text-light">👋 {{ user.username }}</span>
<a class="nav-link" href="/logout">退出</a>
</div>
</div>
</div>
</nav>
<main class="container my-4">
<!-- 会员状态栏 -->
{% if user.user_type == 'vip_enterprise' %}
<div class="alert alert-dark py-2 mb-4 text-center">
<strong>👑 企业会员</strong> | 无限翻译 | 到期:{{ user.membership_expire.strftime('%Y-%m-%d') if user.membership_expire else '永久' }}
</div>
{% elif user.user_type == 'vip_pro' %}
<div class="alert alert-warning py-2 mb-4 text-center">
<strong>⭐ 专业会员</strong> | 每日200次·500页 | 到期:{{ user.membership_expire.strftime('%Y-%m-%d') if user.membership_expire else '永久' }}
</div>
{% elif user.user_type == 'vip_basic' %}
<div class="alert alert-success py-2 mb-4 text-center">
<strong>💚 基础会员</strong> | 每日50次·100页 | 到期:{{ user.membership_expire.strftime('%Y-%m-%d') if user.membership_expire else '永久' }}
</div>
{% else %}
<div class="alert alert-info py-2 mb-4 text-center">
<strong>👤 免费用户</strong> | 每日10次·50页 | <a href="/pricing" class="alert-link">升级会员</a>
</div>
{% endif %}
<div class="row">
<!-- 左侧:账户余额 -->
<div class="col-md-4 mb-4">
<div class="card balance-card text-white h-100">
<div class="card-body text-center">
<h5 class="card-title mb-3">💰 账户余额</h5>
<div class="balance-amount my-4">
<span class="fs-1 fw-bold">¥{{ "%.2f"|format(user.balance) }}</span>
</div>
<div class="d-grid gap-2">
<button class="btn btn-light" onclick="showRechargeModal()">充值</button>
<button class="btn btn-outline-light" onclick="showRefundModal()">申请退款</button>
</div>
</div>
</div>
</div>
<!-- 右侧:使用统计 -->
<div class="col-md-8 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">📊 使用统计</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-6 stat-item mb-3">
<small class="text-muted">今日翻译</small>
<div class="fs-4 fw-bold text-primary">{{ user.daily_count }}</div>
</div>
<div class="col-6 stat-item mb-3">
<small class="text-muted">累计翻译</small>
<div class="fs-4 fw-bold">{{ user.total_count }}</div>
</div>
<div class="col-6 stat-item mb-3">
<small class="text-muted">剩余次数</small>
<div class="fs-4 fw-bold text-success">{{ daily_remaining }}</div>
</div>
<div class="col-6 stat-item mb-3">
<small class="text-muted">最大页数</small>
<div class="fs-4 fw-bold">{{ max_pages }}页</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 账户流水 -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">📜 账户流水</h5>
<select class="form-select form-select-sm" id="transactionFilter" style="width: auto;">
<option value="all">全部</option>
<option value="recharge">充值</option>
<option value="refund">退款</option>
<option value="purchase">购买会员</option>
<option value="consume">消费</option>
</select>
</div>
<div class="card-body">
<div id="transactionsList">
<table class="table table-striped">
<thead>
<tr>
<th>时间</th>
<th>类型</th>
<th>金额</th>
<th>余额变化</th>
<th>描述</th>
</tr>
</thead>
<tbody id="transactionBody">
<!-- 动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 会员购买记录 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">🎫 会员购买记录</h5>
</div>
<div class="card-body">
<div id="membershipPurchases">
<table class="table">
<thead>
<tr>
<th>套餐</th>
<th>金额</th>
<th>有效期</th>
<th>状态</th>
<th>时间</th>
</tr>
</thead>
<tbody id="purchaseBody">
<!-- 动态加载 -->
</tbody>
</table>
</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>
<!-- 充值模态框 -->
<div class="modal fade" id="rechargeModal" 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="rechargeForm">
<div class="mb-3">
<label class="form-label">充值金额(元)</label>
<input type="number" class="form-control" id="rechargeAmount" min="10" max="10000" step="10" value="100" required>
</div>
<div class="mb-3">
<label class="form-label">支付方式</label>
<select class="form-select" id="paymentMethod">
<option value="balance">余额支付</option>
<option value="alipay">支付宝</option>
<option value="wechat">微信支付</option>
</select>
</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="submitRecharge()">确认充值</button>
</div>
</div>
</div>
</div>
<!-- 退款模态框 -->
<div class="modal fade" id="refundModal" 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="refundForm">
<div class="mb-3">
<label class="form-label">当前余额</label>
<input type="text" class="form-control" value="¥{{ "%.2f"|format(user.balance) }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">退款金额(元)</label>
<input type="number" class="form-control" id="refundAmount" min="1" max="{{ user.balance }}" step="1" value="{{ user.balance }}" required>
</div>
<div class="mb-3">
<label class="form-label">退款原因</label>
<textarea class="form-control" id="refundReason" rows="3" placeholder="请填写退款原因" required></textarea>
</div>
<div class="alert alert-warning">
<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-warning" onclick="submitRefund()">提交申请</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 加载账户流水
function loadTransactions(type = 'all') {
fetch('/api/profile/transactions?type=' + type)
.then(r => r.json())
.then(data => {
const tbody = document.getElementById('transactionBody');
tbody.innerHTML = data.transactions.map(t => {
const typeMap = {
'recharge': '充值',
'refund': '退款',
'purchase': '购买会员',
'consume': '消费',
'refund_purchase': '会员退款'
};
const typeClass = {
'recharge': 'text-success',
'refund': 'text-warning',
'purchase': 'text-primary',
'consume': 'text-danger',
'refund_purchase': 'text-info'
};
return `<tr>
<td>${t.created_at}</td>
<td><span class="${typeClass[t.transaction_type]}">${typeMap[t.transaction_type]}</span></td>
<td class="${t.amount > 0 ? 'text-success' : 'text-danger'}">¥${t.amount.toFixed(2)}</td>
<td>¥${t.balance_before.toFixed(2)} → ¥${t.balance_after.toFixed(2)}</td>
<td>${t.description || '-'}</td>
</tr>`;
}).join('');
});
}
// 加载会员购买记录
function loadPurchases() {
fetch('/api/profile/purchases')
.then(r => r.json())
.then(data => {
const tbody = document.getElementById('purchaseBody');
tbody.innerHTML = data.purchases.map(p => {
const statusMap = { 'completed': '已完成', 'pending': '待支付', 'refunded': '已退款' };
const statusClass = { 'completed': 'text-success', 'pending': 'text-warning', 'refunded': 'text-info' };
return `<tr>
<td>${p.plan_name}</td>
<td>¥${p.price.toFixed(2)}</td>
<td>${p.period_days}天(至${p.expire_after || '永久'})</td>
<td><span class="${statusClass[p.status]}">${statusMap[p.status]}</span></td>
<td>${p.created_at}</td>
</tr>`;
}).join('');
});
}
// 充值
function showRechargeModal() {
new bootstrap.Modal(document.getElementById('rechargeModal')).show();
}
function submitRecharge() {
const amount = parseFloat(document.getElementById('rechargeAmount').value);
const method = document.getElementById('paymentMethod').value;
fetch('/api/profile/recharge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount, payment_method: method })
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('充值成功!余额:¥' + data.balance.toFixed(2));
location.reload();
} else {
alert('充值失败:' + data.error);
}
});
}
// 退款
function showRefundModal() {
if ({{ user.balance }} <= 0) {
alert('余额不足,无法退款');
return;
}
new bootstrap.Modal(document.getElementById('refundModal')).show();
}
function submitRefund() {
const amount = parseFloat(document.getElementById('refundAmount').value);
const reason = document.getElementById('refundReason').value;
fetch('/api/profile/refund', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount, reason })
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('退款申请已提交,等待管理员审核');
location.reload();
} else {
alert('申请失败:' + data.error);
}
});
}
// 初始化
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>