From 4f33e92abfa00758ae2f3c9c82dd94bf7edaa0c9 Mon Sep 17 00:00:00 2001 From: coder Date: Tue, 14 Apr 2026 18:29:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 账户余额显示和管理 - 充值功能(模拟模式) - 退款申请功能(需管理员审核) - 账户流水记录查看 - 会员购买记录查看 - 使用统计展示 - 新增模型:UserRecharge, UserRefund, MembershipPurchase, AccountTransaction - User模型添加balance字段 - 导航栏添加个人中心入口 --- app.py | 160 ++++++++++++++++++- models.py | 189 ++++++++++++++++++++++ templates/index.html | 1 + templates/pricing.html | 1 + templates/profile.html | 349 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 699 insertions(+), 1 deletion(-) create mode 100644 templates/profile.html 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 %}