4 Commits

Author SHA1 Message Date
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
5 changed files with 747 additions and 9 deletions

163
app.py
View File

@@ -14,7 +14,8 @@ from werkzeug.utils import secure_filename
from config import * from config import *
from models import (db, User, Translation, TranslationCache, GuestTranslation, from models import (db, User, Translation, TranslationCache, GuestTranslation,
DataPackage, UserPackage, DynamicConfig) DataPackage, UserPackage, DynamicConfig, UserRecharge, UserRefund,
MembershipPurchase, AccountTransaction)
from services import TranslationService, CacheService, TranslationTask from services import TranslationService, CacheService, TranslationTask
from admin import admin_bp from admin import admin_bp
@@ -206,7 +207,26 @@ def history():
@app.route('/pricing') @app.route('/pricing')
def pricing(): def pricing():
"""会员定价页""" """会员定价页"""
return render_template('pricing.html', plans=MEMBERSHIP_PLANS) user = get_current_user()
return render_template('pricing.html', plans=MEMBERSHIP_PLANS, 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 ==================== # ==================== 路由: API ====================
@@ -551,6 +571,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(): def init_app():
"""初始化应用""" """初始化应用"""

189
models.py
View File

@@ -38,8 +38,13 @@ class User(db.Model):
is_active = db.Column(db.Boolean, default=True) # 是否启用 is_active = db.Column(db.Boolean, default=True) # 是否启用
is_admin = db.Column(db.Boolean, default=False) # 是否管理员 is_admin = db.Column(db.Boolean, default=False) # 是否管理员
# 余额
balance = db.Column(db.Float, default=0.0) # 账户余额(元)
# 关系 # 关系
translations = db.relationship('Translation', backref='user', lazy=True) 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): def set_password(self, password):
self.password_hash = generate_password_hash(password) self.password_hash = generate_password_hash(password)
@@ -102,6 +107,7 @@ class User(db.Model):
'is_vip': self.is_vip(), 'is_vip': self.is_vip(),
'is_admin': self.is_admin, 'is_admin': self.is_admin,
'is_active': self.is_active, 'is_active': self.is_active,
'balance': self.balance,
'daily_count': self.daily_count, 'daily_count': self.daily_count,
'total_count': self.total_count, 'total_count': self.total_count,
'created_at': self.created_at.isoformat() if self.created_at else None, '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_active': self.is_active,
'is_recommended': self.is_recommended, 'is_recommended': self.is_recommended,
'is_system': self.is_system, '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,
} }

View File

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

View File

@@ -12,9 +12,27 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container"> <div class="container">
<a class="navbar-brand" href="/">📄 PDF翻译助手</a> <a class="navbar-brand" href="/">📄 PDF翻译助手</a>
<div class="navbar-nav ms-auto"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<a class="nav-link" href="/">首页</a> <span class="navbar-toggler-icon"></span>
<a class="nav-link" href="/login">登录</a> </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>
</div> </div>
</nav> </nav>
@@ -24,7 +42,7 @@
<div class="row justify-content-center"> <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 pricing-card h-100">
<div class="card-body text-center"> <div class="card-body text-center">
<h4 class="card-title">免费用户</h4> <h4 class="card-title">免费用户</h4>
@@ -40,13 +58,19 @@
<li class="text-muted">❌ 批量翻译</li> <li class="text-muted">❌ 批量翻译</li>
</ul> </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> <a href="/register" class="btn btn-outline-secondary w-100 mt-3">免费注册</a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<!-- 基础会员 --> <!-- 基础会员 -->
<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 pricing-card h-100">
<div class="card-body text-center"> <div class="card-body text-center">
<h4 class="card-title">基础会员</h4> <h4 class="card-title">基础会员</h4>
@@ -62,13 +86,19 @@
<li>✅ 优先处理队列</li> <li>✅ 优先处理队列</li>
</ul> </ul>
{% if user and user.user_type == 'vip_basic' %}
<span class="btn btn-success w-100 mt-3 disabled">当前套餐</span>
{% elif user and user.user_type in ['vip_pro', 'vip_enterprise', 'admin'] %}
<span class="btn btn-secondary w-100 mt-3 disabled">已升级</span>
{% else %}
<button class="btn btn-outline-primary w-100 mt-3">立即购买</button> <button class="btn btn-outline-primary w-100 mt-3">立即购买</button>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<!-- 专业会员 --> <!-- 专业会员 -->
<div class="col-md-4 mb-4"> <div class="col-lg-3 col-md-6 mb-4">
<div class="card pricing-card h-100 border-primary"> <div class="card pricing-card h-100 border-primary">
<div class="card-header bg-primary text-white text-center"> <div class="card-header bg-primary text-white text-center">
<strong>推荐</strong> <strong>推荐</strong>
@@ -86,13 +116,19 @@
<li>✅ 自定义术语库</li> <li>✅ 自定义术语库</li>
</ul> </ul>
{% if user and user.user_type == 'vip_pro' %}
<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>
{% else %}
<button class="btn btn-primary w-100 mt-3">立即购买</button> <button class="btn btn-primary w-100 mt-3">立即购买</button>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<!-- 企业会员 --> <!-- 企业会员 -->
<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 pricing-card h-100">
<div class="card-body text-center"> <div class="card-body text-center">
<h4 class="card-title">企业会员</h4> <h4 class="card-title">企业会员</h4>
@@ -106,7 +142,11 @@
<li>✅ API接口调用</li> <li>✅ API接口调用</li>
</ul> </ul>
{% if user and user.user_type in ['vip_enterprise', 'admin'] %}
<span class="btn btn-success w-100 mt-3 disabled">当前套餐</span>
{% else %}
<button class="btn btn-outline-primary w-100 mt-3">联系购买</button> <button class="btn btn-outline-primary w-100 mt-3">联系购买</button>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

349
templates/profile.html Normal file
View 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>