Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d36888488 | |||
| ee5e672901 | |||
| 4f33e92abf | |||
| 436ac2cb66 | |||
| bae0ba9a6d | |||
| c1e929fc8a |
228
app.py
228
app.py
@@ -14,7 +14,8 @@ 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)
|
||||
from services import TranslationService, CacheService, TranslationTask
|
||||
from admin import admin_bp
|
||||
|
||||
@@ -206,7 +207,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 ====================
|
||||
@@ -551,6 +636,145 @@ 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': '退款申请已提交,等待管理员审核'
|
||||
})
|
||||
|
||||
|
||||
# ==================== 初始化 ====================
|
||||
def init_app():
|
||||
"""初始化应用"""
|
||||
|
||||
189
models.py
189
models.py
@@ -38,8 +38,13 @@ 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) # 账户余额(元)
|
||||
|
||||
# 关系
|
||||
translations = db.relationship('Translation', backref='user', lazy=True)
|
||||
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)
|
||||
@@ -102,6 +107,7 @@ class User(db.Model):
|
||||
'is_vip': self.is_vip(),
|
||||
'is_admin': self.is_admin,
|
||||
'is_active': self.is_active,
|
||||
'balance': self.balance,
|
||||
'daily_count': self.daily_count,
|
||||
'total_count': self.total_count,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
@@ -520,4 +526,187 @@ 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,
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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,93 +41,69 @@
|
||||
<h2 class="text-center mb-5">会员套餐</h2>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<!-- 免费用户 -->
|
||||
<div class="col-md-3 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">¥0<small>/永久</small></div>
|
||||
|
||||
<ul class="features text-start">
|
||||
<li>✅ 每日翻译10次</li>
|
||||
<li>✅ 单文件最大50页</li>
|
||||
<li>✅ 翻译历史记录</li>
|
||||
<li>✅ 不满意重新翻译</li>
|
||||
<li>✅ 导出PDF格式</li>
|
||||
<li class="text-muted">❌ 原文译文对比</li>
|
||||
<li class="text-muted">❌ 批量翻译</li>
|
||||
{% for feat in free_features %}
|
||||
<li>{% if feat.has %}✅{% else %}<span class="text-muted">❌</span>{% endif %} {{ feat.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% 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">
|
||||
<div class="card-body text-center">
|
||||
<h4 class="card-title">基础会员</h4>
|
||||
<div class="price my-3">¥29<small>/月</small></div>
|
||||
|
||||
<ul class="features text-start">
|
||||
<li>✅ 每日翻译50次</li>
|
||||
<li>✅ 单文件最大100页</li>
|
||||
<li>✅ 翻译历史记录</li>
|
||||
<li>✅ 不满意重新翻译</li>
|
||||
<li>✅ 原文译文对比查看</li>
|
||||
<li>✅ 导出PDF格式</li>
|
||||
<li>✅ 优先处理队列</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-outline-primary w-100 mt-3">立即购买</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 专业会员 -->
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card pricing-card h-100 border-primary">
|
||||
<!-- 动态套餐 -->
|
||||
{% for plan in db_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>
|
||||
<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>
|
||||
|
||||
<!-- 功能对比表 -->
|
||||
|
||||
349
templates/profile.html
Normal file
349
templates/profile.html
Normal file
@@ -0,0 +1,349 @@
|
||||
<!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">
|
||||
<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>
|
||||
</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();
|
||||
|
||||
document.getElementById('transactionFilter').addEventListener('change', function() {
|
||||
loadTransactions(this.value);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user