diff --git a/app.py b/app.py
index aea6239..85c4c25 100644
--- a/app.py
+++ b/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)
from services import TranslationService, CacheService, TranslationTask
from admin import admin_bp
@@ -210,6 +211,24 @@ def pricing():
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 ====================
@app.route('/api/upload', methods=['POST'])
def upload_pdf():
@@ -552,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():
"""初始化应用"""
diff --git a/models.py b/models.py
index e1557f9..9fdfde1 100644
--- a/models.py
+++ b/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,
}
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
index a396373..93fc975 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -22,6 +22,7 @@
会员套餐
{% if user %}
翻译历史
+ 个人中心
{% endif %}
diff --git a/templates/pricing.html b/templates/pricing.html
index 49eca83..fadc827 100644
--- a/templates/pricing.html
+++ b/templates/pricing.html
@@ -21,6 +21,7 @@
会员套餐
{% if user %}
翻译历史
+
个人中心
{% endif %}
diff --git a/templates/profile.html b/templates/profile.html
new file mode 100644
index 0000000..ea99ab4
--- /dev/null
+++ b/templates/profile.html
@@ -0,0 +1,349 @@
+
+
+
+
+
+
个人中心 - PDF翻译助手
+
+
+
+
+
+
+
+
+
+
+ {% if user.user_type == 'vip_enterprise' %}
+
+ 👑 企业会员 | 无限翻译 | 到期:{{ user.membership_expire.strftime('%Y-%m-%d') if user.membership_expire else '永久' }}
+
+ {% elif user.user_type == 'vip_pro' %}
+
+ ⭐ 专业会员 | 每日200次·500页 | 到期:{{ user.membership_expire.strftime('%Y-%m-%d') if user.membership_expire else '永久' }}
+
+ {% elif user.user_type == 'vip_basic' %}
+
+ 💚 基础会员 | 每日50次·100页 | 到期:{{ user.membership_expire.strftime('%Y-%m-%d') if user.membership_expire else '永久' }}
+
+ {% else %}
+
+
👤 免费用户 | 每日10次·50页 |
升级会员
+
+ {% endif %}
+
+
+
+
+
+
+
💰 账户余额
+
+ ¥{{ "%.2f"|format(user.balance) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
今日翻译
+
{{ user.daily_count }}
+
+
+
累计翻译
+
{{ user.total_count }}
+
+
+
剩余次数
+
{{ daily_remaining }}
+
+
+
最大页数
+
{{ max_pages }}页
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 时间 |
+ 类型 |
+ 金额 |
+ 余额变化 |
+ 描述 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 套餐 |
+ 金额 |
+ 有效期 |
+ 状态 |
+ 时间 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file