用戶系統從0到1:登錄、權限、積分一網打盡

👤 用戶系統從0到1:登錄、權限、積分一網打盡

副標題Flask-Login + 多級權限 + 積分會員系統實戰
項目原型:https://madechango.com
難度等級:???☆☆
預計閱讀時間:20分鐘


🎯 引子:用戶體驗的痛點

想象一下這樣的場景:你精心搭建了一個功能強大的學術平臺,但用戶注冊后發現:

“咦?為什么我和VIP用戶享受同樣的服務?”
“每天使用功能沒有任何獎勵,感覺沒有成就感…”
“權限管理混亂,不知道自己能做什么不能做什么…”

這些都是典型的用戶系統設計不完善導致的問題。一個優秀的用戶系統不僅僅是登錄注冊那么簡單,它應該是:

  • 🔐 安全可靠:完善的認證機制,保護用戶隱私
  • 🏆 層次分明:清晰的權限體系,差異化服務
  • 🎯 激勵有效:積分獎勵機制,提升用戶粘性
  • 🤖 智能增長:AI輔助的用戶增長策略

今天我們就基于Madechango的真實實踐,從零開始構建一個完整的用戶系統!

🎁 你將收獲什么?

  • ? 認證系統:注冊、登錄、郵箱驗證、密碼重置全流程
  • ? 權限控制:基于角色的權限管理,裝飾器實現
  • ? 積分系統:多樣化積分獲取,會員等級升級機制
  • ? AI增長:智能生成虛擬用戶,提升平臺活躍度
  • ? 安全防護:會話管理、登錄限制、行為監控

在這里插入圖片描述

🧠 技術背景:用戶系統的核心組件

在深入實戰之前,讓我們先了解現代用戶系統的技術架構。

🔧 技術棧選擇

# 用戶系統核心技術棧
Flask-Login          # 會話管理,簡單易用
Flask-Mail           # 郵件發送,驗證通知
Flask-WTF           # 表單驗證,CSRF防護
itsdangerous        # 令牌生成,安全可靠
Celery              # 異步任務,郵件隊列
GLM-4 API           # AI服務,智能用戶生成

🏗? 權限設計模式對比

權限模式RBACACLABACMadechango選擇
復雜度中等簡單復雜RBAC + 簡化ACL
擴展性很好滿足當前需求
性能很好中等緩存優化后優秀
維護性中等代碼清晰易懂
學習成本中等適合團隊技能

為什么選擇RBAC(基于角色的訪問控制)?

RBAC權限模型
角色 Role
用戶 User
權限 Permission
資源 Resource
user角色
普通用戶
vip角色
VIP用戶
admin角色
管理員
基礎功能權限
高級功能權限
管理功能權限

在這里插入圖片描述

💰 積分系統設計理念

一個好的積分系統應該遵循"行為驅動,價值回報"的原則:

# 積分獲取規則設計
POINT_RULES = {'daily_login': {'points': 10, 'desc': '每日登錄獎勵'},'complete_profile': {'points': 50, 'desc': '完善個人資料'},'share_content': {'points': 5, 'desc': '分享內容'},'use_ai_analysis': {'points': 2, 'desc': '使用AI分析'},'invite_friend': {'points': 100, 'desc': '邀請好友注冊'},'write_review': {'points': 20, 'desc': '撰寫評價'},'upload_document': {'points': 8, 'desc': '上傳文檔'},'complete_task': {'points': 25, 'desc': '完成寫作任務'}
}# 會員等級設計
MEMBER_LEVELS = {'bronze': {'min_points': 0, 'max_ai_calls': 10, 'features': ['基礎搜索']},'silver': {'min_points': 500, 'max_ai_calls': 50, 'features': ['高級搜索', '導出功能']},'gold': {'min_points': 2000, 'max_ai_calls': 200, 'features': ['AI分析', '批量操作']},'platinum': {'min_points': 5000, 'max_ai_calls': -1, 'features': ['全部功能', '優先支持']}
}

🏗? 系統架構設計

🌐 用戶系統整體架構

用戶系統架構
前端層
應用層
服務層
數據層
緩存層
用戶會話
權限緩存
積分緩存
用戶表
角色表
積分歷史
用戶活動
用戶服務
郵件服務
積分服務
AI服務
認證模塊
權限模塊
積分模塊
AI用戶模塊
登錄注冊界面
用戶中心
權限提示

📊 數據模型設計

UserintidPKstringusernameUKstringemailUKstringpassword_hashstringnicknamestringavatar_urlbooleanis_activebooleanis_verifiedstringrolestringmember_levelinttotal_pointsintavailable_pointsdatetimelast_login_atUserActivityintidPKintuser_idFKstringactiontextdetailsstringip_addressdatetimecreated_atPointsHistoryintidPKintuser_idFKintpointsstringreasonintbalance_afterdatetimecreated_atRoleLoginHistoryhashasbelongs_tohas

💻 核心功能實戰開發

🔐 第一步:增強用戶認證系統

讓我們在第一篇的基礎上,構建一個更完善的用戶認證系統:

# app/models/user.py - 增強版用戶模型
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from itsdangerous import URLSafeTimedSerializer
from datetime import datetime, timedelta
from .base import BaseModel, dbclass User(BaseModel, UserMixin):"""增強版用戶模型"""__tablename__ = 'users'# 基本信息username = db.Column(db.String(80), unique=True, nullable=False, index=True)email = db.Column(db.String(120), unique=True, nullable=False, index=True)password_hash = db.Column(db.String(255), nullable=False)# 個人資料nickname = db.Column(db.String(100))avatar_url = db.Column(db.String(255))bio = db.Column(db.Text)location = db.Column(db.String(100))website = db.Column(db.String(255))# 賬戶狀態is_active = db.Column(db.Boolean, default=True, index=True)is_verified = db.Column(db.Boolean, default=False)email_confirmed_at = db.Column(db.DateTime)last_login_at = db.Column(db.DateTime)last_login_ip = db.Column(db.String(45))# 權限和等級role = db.Column(db.String(20), default='user', index=True)  # user, vip, adminmember_level = db.Column(db.String(20), default='bronze', index=True)# 積分系統total_points = db.Column(db.Integer, default=0)available_points = db.Column(db.Integer, default=0)points_used = db.Column(db.Integer, default=0)# AI使用統計ai_calls_today = db.Column(db.Integer, default=0)ai_calls_total = db.Column(db.Integer, default=0)last_ai_call_date = db.Column(db.Date)# 關系activities = db.relationship('UserActivity', backref='user', lazy='dynamic')points_history = db.relationship('PointsHistory', backref='user', lazy='dynamic')def set_password(self, password):"""設置密碼哈希"""self.password_hash = generate_password_hash(password)def check_password(self, password):"""驗證密碼"""return check_password_hash(self.password_hash, password)def generate_confirmation_token(self):"""生成郵箱確認令牌"""from flask import current_appserializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])return serializer.dumps({'user_id': self.id}, salt='email-confirm')def confirm_email(self, token, expiration=3600):"""確認郵箱"""from flask import current_appserializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])try:data = serializer.loads(token, salt='email-confirm', max_age=expiration)if data['user_id'] != self.id:return Falseself.is_verified = Trueself.email_confirmed_at = datetime.utcnow()self.save()return Trueexcept:return Falsedef can_use_ai(self):"""檢查是否可以使用AI功能"""# 重置每日計數today = datetime.now().date()if self.last_ai_call_date != today:self.ai_calls_today = 0self.last_ai_call_date = todayself.save()# 檢查權限limits = {'bronze': 10,'silver': 50, 'gold': 200,'platinum': -1  # 無限制}limit = limits.get(self.member_level, 10)return limit == -1 or self.ai_calls_today < limitdef use_ai_call(self):"""記錄AI調用"""self.ai_calls_today += 1self.ai_calls_total += 1self.last_ai_call_date = datetime.now().date()self.save()def get_display_name(self):"""獲取顯示名稱"""return self.nickname or self.usernamedef is_admin(self):"""檢查是否為管理員"""return self.role == 'admin'def is_vip(self):"""檢查是否為VIP用戶"""return self.role in ['vip', 'admin'] or self.member_level in ['gold', 'platinum']

🏆 第二步:權限裝飾器系統

權限控制是用戶系統的核心,我們設計一套靈活的裝飾器系統:

# app/utils/decorators.py - 權限控制裝飾器
from functools import wraps
from flask import abort, flash, redirect, url_for, request, jsonify
from flask_login import current_userdef require_roles(*roles):"""角色權限裝飾器"""def decorator(f):@wraps(f)def decorated_function(*args, **kwargs):if not current_user.is_authenticated:if request.is_json:return jsonify({'error': '請先登錄', 'code': 401}), 401flash('請先登錄', 'warning')return redirect(url_for('auth.login', next=request.url))if current_user.role not in roles:if request.is_json:return jsonify({'error': '權限不足', 'code': 403}), 403flash('權限不足', 'error')abort(403)return f(*args, **kwargs)return decorated_functionreturn decoratordef require_verified_email(f):"""郵箱驗證裝飾器"""@wraps(f)def decorated_function(*args, **kwargs):if not current_user.is_authenticated:return redirect(url_for('auth.login'))if not current_user.is_verified:if request.is_json:return jsonify({'error': '請先驗證郵箱', 'code': 403}), 403flash('請先驗證郵箱', 'warning')return redirect(url_for('auth.verify_email'))return f(*args, **kwargs)return decorated_functiondef require_member_level(min_level):"""會員等級裝飾器"""level_hierarchy = {'bronze': 1, 'silver': 2, 'gold': 3, 'platinum': 4}def decorator(f):@wraps(f)def decorated_function(*args, **kwargs):if not current_user.is_authenticated:return redirect(url_for('auth.login'))user_level = level_hierarchy.get(current_user.member_level, 1)required_level = level_hierarchy.get(min_level, 1)if user_level < required_level:if request.is_json:return jsonify({'error': f'需要 {min_level} 等級才能使用此功能','current_level': current_user.member_level,'required_level': min_level}), 403flash(f'需要 {min_level} 等級才能使用此功能', 'warning')return redirect(url_for('user.upgrade'))return f(*args, **kwargs)return decorated_functionreturn decoratordef ai_usage_limit(f):"""AI使用限制裝飾器"""@wraps(f)def decorated_function(*args, **kwargs):if not current_user.is_authenticated:return redirect(url_for('auth.login'))if not current_user.can_use_ai():limits = {'bronze': '10次/天', 'silver': '50次/天', 'gold': '200次/天', 'platinum': '無限制'}limit = limits.get(current_user.member_level, '10次/天')if request.is_json:return jsonify({'error': f'今日AI使用次數已達上限 ({limit})','current_level': current_user.member_level,'upgrade_url': url_for('user.upgrade')}), 403flash(f'今日AI使用次數已達上限 ({limit}),請升級會員或明日再試', 'warning')return redirect(url_for('user.upgrade'))# 記錄AI調用current_user.use_ai_call()return f(*args, **kwargs)return decorated_functiondef rate_limit(max_requests=60, window=60, per='ip'):"""頻率限制裝飾器"""def decorator(f):@wraps(f)def decorated_function(*args, **kwargs):from app.utils.redis_client import redis_client# 生成限制鍵if per == 'ip':key = f"rate_limit:{request.remote_addr}:{f.__name__}"elif per == 'user' and current_user.is_authenticated:key = f"rate_limit:user:{current_user.id}:{f.__name__}"else:key = f"rate_limit:anonymous:{f.__name__}"# 檢查頻率限制current_requests = redis_client.redis.get(key)if current_requests and int(current_requests) >= max_requests:if request.is_json:return jsonify({'error': f'請求過于頻繁,請在{window}秒后重試','retry_after': window}), 429flash('請求過于頻繁,請稍后重試', 'warning')abort(429)# 記錄請求pipe = redis_client.redis.pipeline()pipe.incr(key)pipe.expire(key, window)pipe.execute()return f(*args, **kwargs)return decorated_functionreturn decorator

在這里插入圖片描述

🎯 第三步:積分系統實現

積分系統是提升用戶粘性的關鍵,讓我們實現一個完整的積分管理系統:

# app/services/points_service.py - 積分系統服務
from app.models.user import User
from app.models.points import PointsHistory, UserActivity
from datetime import datetime, timedelta
from app.models import dbclass PointsService:"""積分系統服務"""# 積分規則配置POINT_RULES = {'daily_login': {'points': 10, 'desc': '每日登錄獎勵', 'daily_limit': 1},'complete_profile': {'points': 50, 'desc': '完善個人資料', 'once_only': True},'first_ai_use': {'points': 20, 'desc': '首次使用AI分析', 'once_only': True},'share_content': {'points': 5, 'desc': '分享內容', 'daily_limit': 3},'invite_friend': {'points': 100, 'desc': '邀請好友注冊', 'daily_limit': 5},'write_review': {'points': 15, 'desc': '撰寫評價', 'daily_limit': 2},'upload_document': {'points': 8, 'desc': '上傳文檔', 'daily_limit': 10},'complete_task': {'points': 25, 'desc': '完成寫作任務'},'continuous_login': {'points': 5, 'desc': '連續登錄額外獎勵'}}@classmethoddef award_points(cls, user, action, custom_points=None, custom_reason=None):"""獎勵積分"""if action not in cls.POINT_RULES and custom_points is None:return {'success': False, 'message': '無效的積分規則'}rule = cls.POINT_RULES.get(action, {})points = custom_points or rule.get('points', 0)reason = custom_reason or rule.get('desc', f'自定義獎勵: {custom_points}')# 檢查一次性獎勵if rule.get('once_only'):existing = PointsHistory.query.filter_by(user_id=user.id,reason=reason).first()if existing:return {'success': False, 'message': '該獎勵已經獲得過了'}# 檢查每日限制daily_limit = rule.get('daily_limit')if daily_limit:today = datetime.now().date()today_count = PointsHistory.query.filter(PointsHistory.user_id == user.id,PointsHistory.reason == reason,db.func.date(PointsHistory.created_at) == today,PointsHistory.points > 0).count()if today_count >= daily_limit:return {'success': False, 'message': f'今日該獎勵已達上限 ({daily_limit}次)'}# 添加積分user.add_points(points, reason)# 記錄用戶活動activity = UserActivity(user_id=user.id,action=f'earn_points_{action}',details=f'獲得 {points} 積分: {reason}',ip_address=self._get_client_ip())activity.save()return {'success': True,'points': points,'reason': reason,'total_points': user.total_points,'new_level': user.member_level}@classmethoddef consume_points(cls, user, points, reason):"""消費積分"""if user.available_points < points:return {'success': False,'message': f'積分不足,當前可用積分: {user.available_points}','required': points,'available': user.available_points}# 扣除積分user.available_points -= pointsuser.points_used += points# 記錄積分歷史history = PointsHistory(user_id=user.id,points=-points,  # 負數表示消費reason=reason,balance_after=user.available_points)history.save()# 記錄用戶活動activity = UserActivity(user_id=user.id,action='consume_points',details=f'消費 {points} 積分: {reason}',ip_address=self._get_client_ip())activity.save()user.save()return {'success': True,'consumed': points,'remaining': user.available_points,'reason': reason}@classmethoddef check_continuous_login(cls, user):"""檢查連續登錄并給予獎勵"""# 獲取最近的登錄記錄recent_activities = UserActivity.query.filter_by(user_id=user.id,action='earn_points_daily_login').order_by(UserActivity.created_at.desc()).limit(7).all()if not recent_activities:return 0# 計算連續登錄天數continuous_days = 1current_date = datetime.now().date()for activity in recent_activities[1:]:  # 跳過今天的記錄activity_date = activity.created_at.date()expected_date = current_date - timedelta(days=continuous_days)if activity_date == expected_date:continuous_days += 1else:break# 連續登錄獎勵if continuous_days >= 3:  # 連續3天以上給額外獎勵bonus_points = min(continuous_days * 2, 20)  # 最多20分cls.award_points(user, 'continuous_login', custom_points=bonus_points,custom_reason=f'連續登錄{continuous_days}天獎勵')return bonus_pointsreturn 0@classmethoddef get_points_ranking(cls, limit=10, timeframe='all'):"""獲取積分排行榜"""query = User.query.filter(User.is_active == True)if timeframe == 'month':# 本月積分排行month_start = datetime.now().replace(day=1, hour=0, minute=0, second=0)month_points = db.session.query(PointsHistory.user_id,db.func.sum(PointsHistory.points).label('month_points')).filter(PointsHistory.created_at >= month_start,PointsHistory.points > 0).group_by(PointsHistory.user_id).subquery()query = query.join(month_points, User.id == month_points.c.user_id)\.order_by(month_points.c.month_points.desc())else:# 總積分排行query = query.order_by(User.total_points.desc())return query.limit(limit).all()@classmethoddef get_user_points_summary(cls, user):"""獲取用戶積分匯總"""# 近30天積分獲得thirty_days_ago = datetime.now() - timedelta(days=30)recent_earned = db.session.query(db.func.sum(PointsHistory.points)).filter(PointsHistory.user_id == user.id,PointsHistory.points > 0,PointsHistory.created_at >= thirty_days_ago).scalar() or 0# 積分來源統計points_sources = db.session.query(PointsHistory.reason,db.func.sum(PointsHistory.points).label('total'),db.func.count(PointsHistory.id).label('count')).filter(PointsHistory.user_id == user.id,PointsHistory.points > 0).group_by(PointsHistory.reason).order_by(db.func.sum(PointsHistory.points).desc()).all()# 等級信息next_level_info = cls.get_next_level_info(user)return {'total_points': user.total_points,'available_points': user.available_points,'points_used': user.points_used,'recent_earned': recent_earned,'points_sources': [{'reason': source.reason,'total': source.total,'count': source.count,'average': round(source.total / source.count, 1)} for source in points_sources],'current_level': user.member_level,'next_level': next_level_info}@classmethoddef get_next_level_info(cls, user):"""獲取下一級別信息"""levels = [('bronze', 0),('silver', 500),('gold', 2000),('platinum', 5000)]current_points = user.total_pointsfor i, (level, min_points) in enumerate(levels):if current_points < min_points:return {'level': level,'required_points': min_points,'remaining_points': min_points - current_points,'progress_percent': round((current_points / min_points) * 100, 1) if min_points > 0 else 0}# 已經是最高等級return {'level': 'platinum','required_points': 5000,'remaining_points': 0,'progress_percent': 100}@staticmethoddef _get_client_ip():"""獲取客戶端IP"""from flask import requestreturn request.headers.get('X-Forwarded-For', request.remote_addr)

🤖 第四步:AI虛擬用戶生成系統

為了提升平臺活躍度,我們實現一個AI驅動的虛擬用戶生成系統:

# app/services/virtual_user_service.py - AI虛擬用戶生成
import random
import json
from datetime import datetime, timedelta
from app.models.user import User
from app.models.points import UserActivity
from app.services.points_service import PointsServiceclass VirtualUserService:"""AI虛擬用戶生成服務"""def __init__(self):# 傳統用戶名模板(AI生成失敗時使用)self.username_templates = ['student_{random}', 'scholar_{random}', 'researcher_{random}','academic_{random}', 'learner_{random}', 'writer_{random}','reader_{random}', 'thinker_{random}']# 郵箱域名列表self.email_domains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com','163.com', 'qq.com', 'sina.com', 'edu.my', 'student.edu']# 虛擬用戶昵稱庫self.nicknames = {'chinese': ['學術新手', '研究者', '論文達人', '知識探索者', '學習愛好者','書蟲', '思考者', '求知者', '學者', '智慧追求者','文獻獵手', '知識收割機', '學術小白', '研究狂人'],'english': ['Academic', 'Scholar', 'Researcher', 'Student', 'Learner','Reader', 'Thinker', 'Explorer', 'Seeker', 'Analyst','BookLover', 'KnowledgeHunter', 'StudyBuddy', 'ResearchFan']}# 虛擬用戶簡介模板self.bio_templates = ['熱愛學術研究的{}','專注于{}領域的研究','追求學術excellence的{}','{}方向的研究生','對{}充滿熱情的學者','致力于{}研究的學生']# 研究領域self.research_fields = ['計算機科學', '人工智能', '數據科學', '軟件工程', '網絡安全','機器學習', '深度學習', '自然語言處理', '計算機視覺', '大數據','教育學', '心理學', '管理學', '經濟學', '社會學']# 馬來西亞城市self.locations = ['吉隆坡', '檳城', '新山', '馬六甲', '怡保', '古晉', '亞庇','Kuala Lumpur', 'Penang', 'Johor Bahru', 'Malacca', 'Ipoh']def generate_ai_user_info(self, user_type='student'):"""使用AI生成用戶信息"""try:# 這里可以集成GLM-4 API進行AI生成# 為了演示,我們使用智能的傳統方法return self._generate_smart_user_info(user_type)except Exception as e:print(f"AI生成用戶信息失敗: {e}")return self._generate_fallback_user_info()def _generate_smart_user_info(self, user_type):"""智能生成用戶信息"""random_num = random.randint(1000, 9999)# 生成用戶名if user_type == 'researcher':template = random.choice(['researcher_{random}', 'scholar_{random}'])elif user_type == 'student':template = random.choice(['student_{random}', 'learner_{random}'])else:template = random.choice(self.username_templates)username = template.format(random=random_num)# 確保用戶名唯一while User.query.filter_by(username=username).first():random_num = random.randint(1000, 9999)username = template.format(random=random_num)# 生成郵箱domain = random.choice(self.email_domains)email = f"{username}@{domain}"# 確保郵箱唯一while User.query.filter_by(email=email).first():random_num = random.randint(1000, 9999)username = template.format(random=random_num)email = f"{username}@{domain}"# 生成昵稱nickname_type = random.choice(['chinese', 'english'])nickname = random.choice(self.nicknames[nickname_type])# 添加隨機數字避免重復if random.random() < 0.3:nickname += str(random.randint(1, 99))# 生成個人簡介field = random.choice(self.research_fields)bio_template = random.choice(self.bio_templates)bio = bio_template.format(field)# 生成位置location = random.choice(self.locations)return {'username': username,'nickname': nickname,'email': email,'bio': bio,'location': location,'research_field': field}def _generate_fallback_user_info(self):"""傳統方式生成用戶信息(備用方案)"""random_num = random.randint(1000, 9999)template = random.choice(self.username_templates)username = template.format(random=random_num)# 確保用戶名唯一while User.query.filter_by(username=username).first():random_num = random.randint(1000, 9999)username = template.format(random=random_num)domain = random.choice(self.email_domains)email = f"{username}@{domain}"return {'username': username,'nickname': random.choice(self.nicknames['chinese'] + self.nicknames['english']),'email': email,'bio': random.choice(['熱愛學術研究', '專注于知識分享', '追求學術excellence']),'location': random.choice(self.locations),'research_field': random.choice(self.research_fields)}def create_virtual_user(self, user_type='student'):"""創建虛擬用戶"""user_info = self.generate_ai_user_info(user_type)# 創建用戶user = User(username=user_info['username'],email=user_info['email'],nickname=user_info['nickname'],bio=user_info['bio'],location=user_info['location'],is_verified=True,  # 虛擬用戶自動驗證member_level=random.choices(['bronze', 'silver', 'gold'], weights=[70, 25, 5]  # 70%青銅,25%白銀,5%黃金)[0])# 設置隨機密碼user.set_password(f"virtual_{random.randint(100000, 999999)}")user.save()# 添加初始積分initial_points = random.randint(20, 150)PointsService.award_points(user, 'complete_profile',  # 使用現有規則custom_points=initial_points,custom_reason='虛擬用戶初始積分')# 模擬一些歷史活動self._simulate_user_history(user)# 記錄創建活動activity = UserActivity(user_id=user.id,action='virtual_user_created',details=f'AI生成虛擬用戶: {user_type}, 研究領域: {user_info.get("research_field", "未知")}')activity.save()return userdef _simulate_user_history(self, user):"""模擬用戶歷史活動"""# 模擬過去幾天的登錄days_back = random.randint(1, 7)for i in range(days_back):if random.random() < 0.7:  # 70%概率有活動activity_date = datetime.now() - timedelta(days=i)# 創建歷史活動記錄activity = UserActivity(user_id=user.id,action='daily_login',details='虛擬用戶歷史登錄',created_at=activity_date)activity.save()# 有概率進行其他活動if random.random() < 0.3:  # 30%概率有其他活動actions = ['share_content', 'upload_document', 'write_review']action = random.choice(actions)activity = UserActivity(user_id=user.id,action=action,details=f'虛擬用戶歷史{action}',created_at=activity_date + timedelta(hours=random.randint(1, 10)))activity.save()def create_batch_virtual_users(self, count=5):"""批量創建虛擬用戶"""user_types = ['student', 'researcher', 'academic', 'graduate']created_users = []for i in range(count):try:user_type = random.choice(user_types)user = self.create_virtual_user(user_type)created_users.append(user)# 隨機延遲,模擬真實注冊間隔import timetime.sleep(random.uniform(0.5, 2.0))except Exception as e:print(f"創建虛擬用戶失敗: {e}")continuereturn created_usersdef simulate_user_activities(self, days=1):"""模擬用戶活動"""# 獲取虛擬用戶(用戶名包含特定模式的用戶)virtual_users = User.query.filter(db.or_(User.username.like('student_%'),User.username.like('scholar_%'),User.username.like('researcher_%'),User.username.like('academic_%'))).order_by(db.func.random()).limit(20).all()activities = [('share_content', 0.2),      # 20%概率('upload_document', 0.15),   # 15%概率('write_review', 0.1),       # 10%概率('daily_login', 0.8)         # 80%概率]activity_count = 0for user in virtual_users:for action, probability in activities:if random.random() < probability:result = PointsService.award_points(user, action)if result['success']:activity_count += 1return {'simulated_users': len(virtual_users),'activities_created': activity_count,'timestamp': datetime.now().isoformat()}

🎨 前端用戶界面設計

📝 用戶注冊頁面

在這里插入圖片描述

<!-- app/templates/auth/register.html - 用戶注冊頁面 -->
{% extends "base.html" %}{% block title %}用戶注冊 - Academic Platform{% endblock %}{% block extra_css %}
<style>
.auth-container {max-width: 500px;margin: 2rem auto;padding: 2rem;background: white;border-radius: 10px;box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}.form-floating {margin-bottom: 1rem;
}.password-strength {height: 4px;background: #e9ecef;border-radius: 2px;overflow: hidden;margin-top: 5px;
}.password-strength-bar {height: 100%;transition: all 0.3s ease;border-radius: 2px;
}.strength-weak { background: #dc3545; width: 25%; }
.strength-fair { background: #ffc107; width: 50%; }
.strength-good { background: #198754; width: 75%; }
.strength-strong { background: #198754; width: 100%; }.feature-highlight {background: #f8f9fa;border-radius: 8px;padding: 1rem;margin: 1rem 0;
}
</style>
{% endblock %}{% block content %}
<div class="container"><div class="auth-container"><div class="text-center mb-4"><h2 class="fw-bold text-primary"><i class="fas fa-user-plus me-2"></i>加入Academic Platform</h2><p class="text-muted">開啟您的學術研究之旅</p></div><!-- 注冊表單 --><form id="registerForm" method="POST"><!-- 用戶名 --><div class="form-floating"><input type="text" class="form-control" id="username" name="username" placeholder="用戶名" required minlength="3" maxlength="20"><label for="username"><i class="fas fa-user me-1"></i>用戶名</label><div class="form-text">3-20個字符,支持字母、數字、下劃線</div></div><!-- 郵箱 --><div class="form-floating"><input type="email" class="form-control" id="email" name="email" placeholder="郵箱地址" required><label for="email"><i class="fas fa-envelope me-1"></i>郵箱地址</label><div class="form-text">用于賬戶驗證和重要通知</div></div><!-- 密碼 --><div class="form-floating"><input type="password" class="form-control" id="password" name="password" placeholder="密碼" required minlength="6"><label for="password"><i class="fas fa-lock me-1"></i>密碼</label><div class="password-strength"><div class="password-strength-bar" id="strengthBar"></div></div><div class="form-text"><span id="strengthText">至少6個字符</span></div></div><!-- 確認密碼 --><div class="form-floating"><input type="password" class="form-control" id="confirmPassword" name="confirm_password" placeholder="確認密碼" required><label for="confirmPassword"><i class="fas fa-lock me-1"></i>確認密碼</label><div class="invalid-feedback" id="passwordMismatch">兩次輸入的密碼不一致</div></div><!-- 同意條款 --><div class="form-check mb-3"><input class="form-check-input" type="checkbox" id="agreeTerms" required><label class="form-check-label" for="agreeTerms">我已閱讀并同意 <a href="#" class="text-decoration-none">用戶協議</a><a href="#" class="text-decoration-none">隱私政策</a></label></div><!-- 注冊按鈕 --><button type="submit" class="btn btn-primary w-100 py-2" id="submitBtn"><i class="fas fa-user-plus me-2"></i>立即注冊</button></form><!-- 登錄鏈接 --><div class="text-center mt-3"><span class="text-muted">已有賬戶?</span><a href="{{ url_for('auth.login') }}" class="text-decoration-none">立即登錄</a></div><!-- 功能亮點 --><div class="feature-highlight mt-4"><h6 class="fw-bold mb-2"><i class="fas fa-star text-warning me-1"></i>注冊即享特權</h6><ul class="list-unstyled mb-0 small"><li><i class="fas fa-check text-success me-2"></i>每日登錄獎勵 10 積分</li><li><i class="fas fa-check text-success me-2"></i>完善資料獎勵 50 積分</li><li><i class="fas fa-check text-success me-2"></i>免費使用 AI 分析功能</li><li><i class="fas fa-check text-success me-2"></i>優先獲得新功能體驗</li></ul></div></div>
</div>
{% endblock %}{% block extra_js %}
<script>
$(document).ready(function() {// 密碼強度檢測$('#password').on('input', function() {const password = $(this).val();const strength = calculatePasswordStrength(password);updatePasswordStrength(strength);});// 確認密碼檢查$('#confirmPassword').on('input', function() {const password = $('#password').val();const confirmPassword = $(this).val();if (confirmPassword && password !== confirmPassword) {$(this).addClass('is-invalid');} else {$(this).removeClass('is-invalid');}});// 表單提交$('#registerForm').on('submit', function(e) {e.preventDefault();const formData = {username: $('#username').val(),email: $('#email').val(),password: $('#password').val()};// 驗證密碼匹配if ($('#password').val() !== $('#confirmPassword').val()) {showAlert('兩次輸入的密碼不一致', 'error');return;}// 提交注冊submitRegistration(formData);});
});function calculatePasswordStrength(password) {let strength = 0;if (password.length >= 6) strength += 1;if (password.length >= 8) strength += 1;if (/[a-z]/.test(password)) strength += 1;if (/[A-Z]/.test(password)) strength += 1;if (/[0-9]/.test(password)) strength += 1;if (/[^A-Za-z0-9]/.test(password)) strength += 1;return Math.min(strength, 4);
}function updatePasswordStrength(strength) {const strengthBar = $('#strengthBar');const strengthText = $('#strengthText');const levels = [{ class: '', text: '至少6個字符' },{ class: 'strength-weak', text: '密碼強度:弱' },{ class: 'strength-fair', text: '密碼強度:一般' },{ class: 'strength-good', text: '密碼強度:良好' },{ class: 'strength-strong', text: '密碼強度:強' }];const level = levels[strength];strengthBar.attr('class', 'password-strength-bar ' + level.class);strengthText.text(level.text);
}function submitRegistration(formData) {const submitBtn = $('#submitBtn');const originalText = submitBtn.html();// 顯示加載狀態submitBtn.html('<i class="fas fa-spinner fa-spin me-2"></i>注冊中...').prop('disabled', true);$.ajax({url: '{{ url_for("auth.register") }}',method: 'POST',contentType: 'application/json',data: JSON.stringify(formData),success: function(response) {if (response.success) {showAlert('注冊成功!請檢查郵箱驗證鏈接', 'success');setTimeout(() => {window.location.href = '{{ url_for("auth.login") }}';}, 2000);} else {showAlert(response.message || '注冊失敗', 'error');submitBtn.html(originalText).prop('disabled', false);}},error: function(xhr) {const response = xhr.responseJSON;if (response && response.errors) {showAlert(response.errors.join('<br>'), 'error');} else {showAlert('注冊失敗,請重試', 'error');}submitBtn.html(originalText).prop('disabled', false);}});
}function showAlert(message, type) {const alertClass = type === 'error' ? 'alert-danger' : 'alert-success';const alertHtml = `<div class="alert ${alertClass} alert-dismissible fade show" role="alert">${message}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>`;$('.auth-container').prepend(alertHtml);// 自動消失setTimeout(() => {$('.alert').fadeOut();}, 5000);
}
</script>
{% endblock %}

👤 用戶中心頁面

在這里插入圖片描述

<!-- app/templates/user/profile.html - 用戶中心頁面 -->
{% extends "base.html" %}{% block title %}個人中心 - {{ current_user.get_display_name() }}{% endblock %}{% block extra_css %}
<style>
.profile-header {background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);color: white;border-radius: 15px;padding: 2rem;margin-bottom: 2rem;
}.avatar-container {position: relative;display: inline-block;
}.avatar-large {width: 120px;height: 120px;border: 4px solid rgba(255,255,255,0.3);border-radius: 50%;
}.level-badge {position: absolute;bottom: -5px;right: -5px;padding: 0.25rem 0.5rem;border-radius: 15px;font-size: 0.7rem;font-weight: bold;
}.level-bronze { background: #cd7f32; }
.level-silver { background: #c0c0c0; }
.level-gold { background: #ffd700; }
.level-platinum { background: #e5e4e2; color: #333; }.stats-card {background: white;border-radius: 10px;padding: 1.5rem;box-shadow: 0 2px 4px rgba(0,0,0,0.1);text-align: center;transition: transform 0.2s;
}.stats-card:hover {transform: translateY(-2px);
}.progress-ring {width: 80px;height: 80px;margin: 0 auto 1rem;
}.progress-ring circle {fill: none;stroke-width: 4;stroke-linecap: round;
}.progress-bg {stroke: #e9ecef;
}.progress-bar {stroke: #007bff;stroke-dasharray: 0 251.2;transform: rotate(-90deg);transform-origin: 50% 50%;transition: stroke-dasharray 1s ease;
}.activity-timeline {position: relative;padding-left: 2rem;
}.activity-timeline::before {content: '';position: absolute;left: 0.5rem;top: 0;bottom: 0;width: 2px;background: #e9ecef;
}.activity-item {position: relative;margin-bottom: 1.5rem;background: white;border-radius: 8px;padding: 1rem;box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}.activity-item::before {content: '';position: absolute;left: -1.75rem;top: 1rem;width: 10px;height: 10px;background: #007bff;border-radius: 50%;border: 2px solid white;
}
</style>
{% endblock %}{% block content %}
<div class="container"><!-- 用戶資料頭部 --><div class="profile-header"><div class="row align-items-center"><div class="col-md-3 text-center"><div class="avatar-container"><img src="{{ current_user.avatar_url or url_for('static', filename='images/default-avatar.png') }}" alt="頭像" class="avatar-large"><span class="level-badge level-{{ current_user.member_level }}">{{ current_user.member_level.upper() }}</span></div></div><div class="col-md-6"><h2 class="mb-2">{{ current_user.get_display_name() }}</h2><p class="mb-1"><i class="fas fa-user me-2"></i>@{{ current_user.username }}</p>{% if current_user.location %}<p class="mb-1"><i class="fas fa-map-marker-alt me-2"></i>{{ current_user.location }}</p>{% endif %}{% if current_user.bio %}<p class="mb-0 opacity-75">{{ current_user.bio }}</p>{% endif %}</div><div class="col-md-3 text-center"><div class="mb-2"><i class="fas fa-coins fs-1 text-warning"></i></div><h3 class="mb-0">{{ current_user.available_points }}</h3><small class="opacity-75">可用積分</small></div></div></div><div class="row"><!-- 左側:統計信息 --><div class="col-lg-8"><!-- 積分統計 --><div class="row mb-4"><div class="col-md-3 mb-3"><div class="stats-card"><div class="progress-ring"><svg width="80" height="80"><circle class="progress-bg" cx="40" cy="40" r="36"></circle><circle class="progress-bar" cx="40" cy="40" r="36" id="totalPointsProgress"></circle></svg></div><h4 class="text-primary">{{ current_user.total_points }}</h4><small class="text-muted">總積分</small></div></div><div class="col-md-3 mb-3"><div class="stats-card"><i class="fas fa-gift fs-1 text-success mb-3"></i><h4 class="text-success">{{ current_user.available_points }}</h4><small class="text-muted">可用積分</small></div></div><div class="col-md-3 mb-3"><div class="stats-card"><i class="fas fa-shopping-cart fs-1 text-warning mb-3"></i><h4 class="text-warning">{{ current_user.points_used }}</h4><small class="text-muted">已消費</small></div></div><div class="col-md-3 mb-3"><div class="stats-card"><i class="fas fa-robot fs-1 text-info mb-3"></i><h4 class="text-info">{{ current_user.ai_calls_total }}</h4><small class="text-muted">AI調用</small></div></div></div><!-- 等級進度 --><div class="card mb-4"><div class="card-body"><h5 class="card-title"><i class="fas fa-trophy me-2"></i>會員等級</h5><div class="row align-items-center"><div class="col-md-8"><div class="d-flex justify-content-between mb-2"><span>當前等級:<strong class="text-{{ current_user.member_level }}">{{ current_user.member_level.upper() }}</strong></span><span id="nextLevelInfo">加載中...</span></div><div class="progress" style="height: 8px;"><div class="progress-bar" role="progressbar" id="levelProgress" style="width: 0%"></div></div></div><div class="col-md-4 text-end"><a href="{{ url_for('user.upgrade') }}" class="btn btn-outline-primary"><i class="fas fa-arrow-up me-1"></i>升級會員</a></div></div></div></div><!-- 最近活動 --><div class="card"><div class="card-header"><h5 class="mb-0"><i class="fas fa-history me-2"></i>最近活動</h5></div><div class="card-body"><div class="activity-timeline" id="activityTimeline"><div class="text-center py-4"><i class="fas fa-spinner fa-spin fs-1 text-muted"></i><p class="text-muted mt-2">加載活動記錄...</p></div></div></div></div></div><!-- 右側:快速操作 --><div class="col-lg-4"><!-- 今日任務 --><div class="card mb-4"><div class="card-header"><h6 class="mb-0"><i class="fas fa-tasks me-2"></i>今日任務</h6></div><div class="card-body"><div class="task-item d-flex justify-content-between align-items-center mb-2"><span><i class="fas fa-sign-in-alt text-success me-2"></i>每日登錄</span><span class="badge bg-success">+10</span></div><div class="task-item d-flex justify-content-between align-items-center mb-2"><span><i class="fas fa-share text-primary me-2"></i>分享內容 (0/3)</span><span class="badge bg-primary">+5</span></div><div class="task-item d-flex justify-content-between align-items-center mb-2"><span><i class="fas fa-upload text-warning me-2"></i>上傳文檔 (0/10)</span><span class="badge bg-warning">+8</span></div></div></div><!-- 積分排行 --><div class="card mb-4"><div class="card-header"><h6 class="mb-0"><i class="fas fa-medal me-2"></i>積分排行榜</h6></div><div class="card-body" id="pointsRanking"><div class="text-center py-3"><i class="fas fa-spinner fa-spin"></i><small class="text-muted d-block mt-1">加載排行榜...</small></div></div></div><!-- 快速操作 --><div class="card"><div class="card-header"><h6 class="mb-0"><i class="fas fa-bolt me-2"></i>快速操作</h6></div><div class="card-body"><div class="d-grid gap-2"><a href="{{ url_for('user.edit_profile') }}" class="btn btn-outline-primary btn-sm"><i class="fas fa-edit me-2"></i>編輯資料</a><a href="{{ url_for('user.points_history') }}" class="btn btn-outline-success btn-sm"><i class="fas fa-history me-2"></i>積分記錄</a><a href="{{ url_for('user.security') }}" class="btn btn-outline-warning btn-sm"><i class="fas fa-shield-alt me-2"></i>安全設置</a><button class="btn btn-outline-danger btn-sm" id="dailyCheckin"><i class="fas fa-calendar-check me-2"></i>每日簽到</button></div></div></div></div></div>
</div>
{% endblock %}{% block extra_js %}
<script>
$(document).ready(function() {// 加載用戶數據loadUserStats();loadRecentActivities();loadPointsRanking();// 每日簽到$('#dailyCheckin').on('click', function() {dailyCheckin();});
});function loadUserStats() {$.get('/api/user/stats').done(function(data) {// 更新等級進度if (data.next_level) {const progress = data.next_level.progress_percent || 0;$('#levelProgress').css('width', progress + '%');$('#nextLevelInfo').text(`距離 ${data.next_level.level.toUpperCase()} 還需 ${data.next_level.remaining_points} 積分`);} else {$('#nextLevelInfo').text('已達最高等級');$('#levelProgress').css('width', '100%');}// 更新積分環形進度updateCircularProgress('totalPointsProgress', data.total_points, 5000);}).fail(function() {console.error('Failed to load user stats');});
}function loadRecentActivities() {$.get('/api/user/activities').done(function(data) {const timeline = $('#activityTimeline');timeline.empty();if (data.activities && data.activities.length > 0) {data.activities.forEach(function(activity) {const activityHtml = `<div class="activity-item"><div class="d-flex justify-content-between align-items-start"><div><h6 class="mb-1">${getActivityIcon(activity.action)} ${activity.details}</h6><small class="text-muted">${formatDateTime(activity.created_at)}</small></div>${activity.points ? `<span class="badge bg-success">+${activity.points}</span>` : ''}</div></div>`;timeline.append(activityHtml);});} else {timeline.html(`<div class="text-center py-4"><i class="fas fa-inbox fs-1 text-muted"></i><p class="text-muted mt-2">暫無活動記錄</p></div>`);}}).fail(function() {$('#activityTimeline').html(`<div class="text-center py-4"><i class="fas fa-exclamation-triangle fs-1 text-warning"></i><p class="text-muted mt-2">加載活動記錄失敗</p></div>`);});
}function loadPointsRanking() {$.get('/api/user/points-ranking').done(function(data) {const ranking = $('#pointsRanking');ranking.empty();if (data.ranking && data.ranking.length > 0) {data.ranking.forEach(function(user, index) {const medal = ['🥇', '🥈', '🥉'][index] || `${index + 1}.`;const isCurrentUser = user.username === '{{ current_user.username }}';const userHtml = `<div class="d-flex justify-content-between align-items-center mb-2 ${isCurrentUser ? 'bg-light rounded p-2' : ''}"><span><span class="me-2">${medal}</span><strong>${user.display_name}</strong>${isCurrentUser ? '<small class="text-primary">(我)</small>' : ''}</span><span class="badge bg-primary">${user.total_points}</span></div>`;ranking.append(userHtml);});} else {ranking.html('<p class="text-muted text-center">暫無排行數據</p>');}}).fail(function() {$('#pointsRanking').html('<p class="text-danger text-center">加載失敗</p>');});
}function dailyCheckin() {const btn = $('#dailyCheckin');const originalText = btn.html();btn.html('<i class="fas fa-spinner fa-spin me-2"></i>簽到中...').prop('disabled', true);$.post('/api/user/daily-checkin').done(function(response) {if (response.success) {showToast('簽到成功!獲得 ' + response.points + ' 積分', 'success');// 刷新頁面數據loadUserStats();loadRecentActivities();} else {showToast(response.message || '簽到失敗', 'warning');}}).fail(function(xhr) {const response = xhr.responseJSON;showToast(response ? response.message : '簽到失敗,請重試', 'error');}).always(function() {btn.html(originalText).prop('disabled', false);});
}function updateCircularProgress(elementId, value, max) {const circle = document.getElementById(elementId);const radius = 36;const circumference = 2 * Math.PI * radius;const progress = (value / max) * 100;const strokeDasharray = (progress / 100) * circumference;circle.style.strokeDasharray = `${strokeDasharray} ${circumference}`;
}function getActivityIcon(action) {const icons = {'daily_login': '<i class="fas fa-sign-in-alt text-success"></i>','share_content': '<i class="fas fa-share text-primary"></i>','upload_document': '<i class="fas fa-upload text-info"></i>','use_ai_analysis': '<i class="fas fa-robot text-warning"></i>','complete_profile': '<i class="fas fa-user-edit text-success"></i>','level_upgrade': '<i class="fas fa-arrow-up text-gold"></i>'};return icons[action] || '<i class="fas fa-star text-muted"></i>';
}function formatDateTime(dateString) {const date = new Date(dateString);const now = new Date();const diff = now - date;if (diff < 60000) return '剛剛';if (diff < 3600000) return Math.floor(diff / 60000) + ' 分鐘前';if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小時前';if (diff < 604800000) return Math.floor(diff / 86400000) + ' 天前';return date.toLocaleDateString();
}function showToast(message, type) {const toastClass = {'success': 'bg-success','warning': 'bg-warning','error': 'bg-danger'}[type] || 'bg-info';const toastHtml = `<div class="toast align-items-center text-white ${toastClass} border-0" role="alert"><div class="d-flex"><div class="toast-body">${message}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div></div>`;// 添加到頁面并顯示$('body').append(`<div class="toast-container position-fixed top-0 end-0 p-3">${toastHtml}</div>`);$('.toast').toast('show');// 自動清理setTimeout(() => {$('.toast-container').remove();}, 5000);
}
</script>
{% endblock %}

🚀 部署與測試

🧪 單元測試

# tests/test_user_system.py - 用戶系統測試
import unittest
from datetime import datetime, timedelta
from app import create_app
from app.models import db
from app.models.user import User
from app.models.points import PointsHistory, UserActivity
from app.services.points_service import PointsService
from app.services.virtual_user_service import VirtualUserServiceclass UserSystemTestCase(unittest.TestCase):def setUp(self):self.app = create_app('testing')self.app_context = self.app.app_context()self.app_context.push()db.create_all()self.client = self.app.test_client()def tearDown(self):db.session.remove()db.drop_all()self.app_context.pop()def test_user_registration_and_verification(self):"""測試用戶注冊和郵箱驗證"""# 注冊用戶response = self.client.post('/auth/register', json={'username': 'testuser','email': 'test@example.com','password': 'TestPass123'})self.assertEqual(response.status_code, 200)data = response.get_json()self.assertTrue(data['success'])# 驗證用戶已創建user = User.query.filter_by(username='testuser').first()self.assertIsNotNone(user)self.assertEqual(user.email, 'test@example.com')self.assertFalse(user.is_verified)# 測試郵箱驗證token = user.generate_confirmation_token()self.assertTrue(user.confirm_email(token))self.assertTrue(user.is_verified)def test_points_system(self):"""測試積分系統"""user = User(username='testuser', email='test@example.com')user.set_password('testpass123')user.save()# 測試積分獎勵result = PointsService.award_points(user, 'daily_login')self.assertTrue(result['success'])self.assertEqual(result['points'], 10)self.assertEqual(user.total_points, 10)# 測試重復獎勵限制result = PointsService.award_points(user, 'daily_login')self.assertFalse(result['success'])  # 每日限制# 測試一次性獎勵result = PointsService.award_points(user, 'complete_profile')self.assertTrue(result['success'])self.assertEqual(user.total_points, 60)  # 10 + 50# 測試重復一次性獎勵result = PointsService.award_points(user, 'complete_profile')self.assertFalse(result['success'])def test_member_level_upgrade(self):"""測試會員等級升級"""user = User(username='testuser', email='test@example.com')user.set_password('testpass123')user.save()# 初始等級self.assertEqual(user.member_level, 'bronze')# 升級到銀牌user.add_points(600, '測試積分')self.assertEqual(user.member_level, 'silver')# 升級到金牌user.add_points(1500, '測試積分')self.assertEqual(user.member_level, 'gold')# 升級到白金user.add_points(3000, '測試積分')self.assertEqual(user.member_level, 'platinum')def test_ai_usage_limits(self):"""測試AI使用限制"""user = User(username='testuser', email='test@example.com', member_level='bronze')user.set_password('testpass123')user.save()# 測試青銅用戶限制(10次)for i in range(10):self.assertTrue(user.can_use_ai())user.use_ai_call()# 第11次應該被拒絕self.assertFalse(user.can_use_ai())# 升級到銀牌后應該可以繼續使用user.member_level = 'silver'user.save()self.assertTrue(user.can_use_ai())def test_virtual_user_creation(self):"""測試虛擬用戶創建"""service = VirtualUserService()# 創建單個虛擬用戶user = service.create_virtual_user('student')self.assertIsNotNone(user)self.assertTrue(user.is_verified)self.assertGreater(user.total_points, 0)self.assertIn(user.member_level, ['bronze', 'silver', 'gold'])# 批量創建虛擬用戶users = service.create_batch_virtual_users(3)self.assertEqual(len(users), 3)# 驗證用戶名唯一性usernames = [user.username for user in users]self.assertEqual(len(usernames), len(set(usernames)))def test_points_consumption(self):"""測試積分消費"""user = User(username='testuser', email='test@example.com')user.set_password('testpass123')user.save()# 添加積分user.add_points(100, '測試積分')# 消費積分result = PointsService.consume_points(user, 30, '測試消費')self.assertTrue(result['success'])self.assertEqual(user.available_points, 70)self.assertEqual(user.points_used, 30)# 積分不足時消費result = PointsService.consume_points(user, 100, '測試消費')self.assertFalse(result['success'])def test_continuous_login_bonus(self):"""測試連續登錄獎勵"""user = User(username='testuser', email='test@example.com')user.set_password('testpass123')user.save()# 模擬連續3天登錄for i in range(3):activity = UserActivity(user_id=user.id,action='earn_points_daily_login',details='每日登錄獎勵',created_at=datetime.now() - timedelta(days=2-i))activity.save()# 檢查連續登錄獎勵bonus = PointsService.check_continuous_login(user)self.assertGreater(bonus, 0)  # 應該有連續登錄獎勵if __name__ == '__main__':unittest.main()

🎉 總結與展望

? 我們完成了什么?

通過這篇文章,我們構建了一個完整的用戶系統:

  • 🔐 完善的認證系統:注冊、登錄、郵箱驗證、密碼重置
  • 🏆 靈活的權限控制:基于角色和等級的多層權限管理
  • 🎯 激勵性積分系統:多樣化積分獲取,自動等級升級
  • 🤖 AI驅動增長:智能虛擬用戶生成,提升平臺活躍度
  • 🛡? 安全防護機制:頻率限制、會話管理、行為監控
  • 🎨 現代化UI界面:響應式設計,優秀的用戶體驗

📊 系統特色

功能模塊特色亮點技術實現
認證系統郵箱驗證、密碼強度檢測Flask-Login + itsdangerous
權限控制裝飾器模式、緩存優化RBAC + Redis緩存
積分系統智能規則、防刷機制數據庫事務 + 業務邏輯
AI增長智能用戶生成、行為模擬GLM-4 API + 算法優化
安全防護多層防護、實時監控Redis限流 + 行為分析

🔮 下一步計劃

在下一篇文章中,我們將深入前端界面設計:

🎨 第三篇預告:《現代化前端界面:Bootstrap5 + jQuery打造響應式UI》
  • 📱 完美的響應式設計和移動端適配
  • ? jQuery實現的動態交互效果
  • 🌙 深色/淺色主題切換功能
  • 🔄 AJAX異步數據加載優化
  • 🎭 組件化設計和復用策略

💡 實戰建議

  1. 安全優先:用戶系統是整個應用的安全基礎,不能有絲毫馬虎
  2. 體驗至上:積分和等級設計要有足夠的激勵性,但不能過于復雜
  3. 性能考慮:權限檢查要使用緩存,避免頻繁數據庫查詢
  4. 擴展性:設計時要考慮未來可能的功能擴展需求
  5. 監控完善:用戶行為數據是產品優化的重要依據

基于Madechango的真實項目經驗,這套用戶系統已經在生產環境中穩定運行,支撐了數萬用戶的使用。希望這篇文章能幫助你構建出色的用戶系統!


📌 重要提醒:本文基于真實項目Madechango的用戶系統設計,但為了教學目的,對某些敏感實現進行了簡化處理。在生產環境中,還需要考慮更多的安全性、合規性和性能優化措施。

🔗 相關鏈接

  • 項目原型:https://madechango.com
  • Flask-Login文檔:https://flask-login.readthedocs.io/
  • Bootstrap文檔:https://getbootstrap.com/docs/5.3/
  • Redis文檔:https://redis.io/documentation

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/100400.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/100400.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/100400.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Java 大視界 -- Java 大數據在智能安防視頻監控系統中的視頻內容理解與智能預警升級

Java 大視界 -- Java 大數據在智能安防視頻監控系統中的視頻內容理解與智能預警升級引言&#xff1a;正文&#xff1a;一、傳統安防監控的 “三重困局”&#xff1a;看不全、看不懂、反應慢1.1 人工盯屏 “力不從心”1.1.1 攝像頭密度與人力的矛盾1.1.2 錄像調閱 “馬后炮”1.2…

OpenHarmony包管理子系統核心源碼深度解讀:從BundleManager到AMS,徹底打通應用安裝、卸載與沙箱機制全鏈路

目錄 架構概覽 核心組件詳解 包安裝流程分析 包卸載流程分析 包更新流程分析 包信息存儲機制 Launcher界面管控 開機默認系統應用安裝機制<

簡單聊聊神經網絡中的反向傳播

參考文章&#xff1a; 一文弄懂神經網絡中的反向傳播法——BackPropagation - Charlotte77 - 博客園 反向傳播求偏導原理簡單理解_反向傳播偏導-CSDN博客 這篇文章是筆者在讀完上述兩篇參考文章后的整理或者說按照自己的理解進行的一些補充&#xff0c;強烈推薦先閱讀上述兩篇文…

JSP自駕游管理系統46u2v--(程序+源碼+數據庫+調試部署+開發環境)

本系統&#xff08;程序源碼數據庫調試部署開發環境&#xff09;帶論文文檔1萬字以上&#xff0c;文末可獲取&#xff0c;系統界面在最后面。系統程序文件列表開題報告內容一、研究背景與意義 近年來&#xff0c;自駕游因自由度高、個性化強成為國內旅游市場增長最快的領域&…

通過 SQL 快速使用 OceanBase 向量檢索學習筆記

背景 AI時代離不開向量數據庫&#xff0c;向量數據庫簡單說就是在數據庫中用多維向量存儲某類事物的特征&#xff0c;通過公式計算各個向量在空間坐標系中的位置關系&#xff0c;以此來判斷事物之間的相似性。相關基礎概念如下: ● Embedding ● 距離/相似性度量 ○ Cosine dis…

PromptAD:首次引入提示學習,實現精準工業異常檢測,1張正常樣本即可超越現有方法

近年來&#xff0c;工業異常檢測&#xff08;Anomaly Detection&#xff09;在智能制造、質量監控等領域扮演著越來越重要的角色。傳統方法通常依賴大量正常樣本進行訓練&#xff0c;而在實際生產中&#xff0c;異常樣本稀少甚至不存在&#xff0c;能否僅憑少量正常樣本就實現精…

算法 --- 字符串

字符串 字符串算法題目主要處理文本的查找、匹配、比較、變換和統計問題&#xff0c;其核心特點是輸入數據為字符序列&#xff0c;解題關鍵在于利用其連續性、前綴性、字典序等特性&#xff0c;并常借助哈希、自動機、指針滑動、動態規劃等技巧高效處理。 詳細分類型與適用場景…

SpringBoot中 Gzip 壓縮的兩種開啟方式:GeoJSON 瘦身實戰

目錄 前言 一、GZIP壓縮知識簡介 1、什么是Gzip 2、Gzip特點 3、Gzip在GIS方面的應用 二、SpringBoot中開啟Gzip的方式 1、在SpringBoot中開啟Gzip的知識簡介 2、SpringBoot中GeoJSON的實例 三、全局開啟Gzip實現 1、實現原理 2、實現效果 四、局部約定配置 1、實現…

PPTist+cpolar:開源演示文稿的遠程創作方案

文章目錄前言【視頻教程】1. 本地安裝PPTist2. PPTist 使用介紹3. 安裝Cpolar內網穿透4. 配置公網地址6. 配置固定公網地址前言 PPTist作為開源在線演示文稿工具&#xff0c;提供媲美PowerPoint的核心功能&#xff0c;支持多頁面編輯、圖表插入、音視頻嵌入和動畫效果設置。特…

服務注冊/服務發現-Eureka

目的&#xff1a;解決微服務在調用遠程服務時URL寫死的問題注冊中心服務提供者&#xff08;Server&#xff09;&#xff1a;一次業務中&#xff0c;被其他微服務調用的服務&#xff0c;也就是提供接口給其他微服務。服務消費者&#xff08;Client&#xff09;:一次業務中&#…

cuda stream

基本概念 cuda stream表示GPU的一個操作隊列&#xff0c;操作在隊列中按照一定的順序執行&#xff0c;也可以向流中添加一定的操作如核函數的啟動、內存的復制、事件的啟動和結束等 一個流中的不同操作有著嚴格的順序&#xff0c;但是不同流之間沒有任何限制 cuda stream中排隊…

數據結構:完全二叉樹

完全二叉樹 定義&#xff1a; 按層序遍歷&#xff08;從上到下&#xff0c;從左到右&#xff09;填充節點。 除了最后一層外&#xff0c;其余各層必須全滿。 最后一層的節點必須 連續靠左。 完全二叉樹不一定是滿二叉樹。 滿二叉樹 (Full Binary Tree)&#xff1a;每個節點都有…

【Java初學基礎】?Object()頂級父類與它的重要方法equals()

object類常見方法/*** native 方法&#xff0c;用于返回當前運行時對象的 Class 對象&#xff0c;使用了 final 關鍵字修飾&#xff0c;故不允許子類重寫。*/ public final native Class<?> getClass() /*** native 方法&#xff0c;用于返回對象的哈希碼&#xff0c;主…

用深度學習(LSTM)實現時間序列預測:從數據到閉環預測全解析

用深度學習&#xff08;LSTM&#xff09;實現時間序列預測&#xff1a;從數據到閉環預測全解析 時間序列預測是工業、金融、環境等領域的核心需求——小到預測設備溫度波動&#xff0c;大到預測股價走勢&#xff0c;都需要從歷史數據中挖掘時序規律。長短期記憶網絡&#xff08…

gpu-z功能介紹,安裝與使用方法

GPU-Z 功能介紹、安裝與使用方法 一、核心功能 硬件信息檢測 識別顯卡型號、制造商、核心架構&#xff08;如NVIDIA Ada Lovelace、AMD RDNA 3&#xff09;、制造工藝&#xff08;如5nm、7nm&#xff09;。顯示顯存類型&#xff08;GDDR6X、HBM2e&#xff09;、容量、帶寬及顯…

數據搬家后如何處理舊 iPhone

每年&#xff0c;蘋果都會推出新款 iPhone&#xff0c;激發了人們升級到 iPhone 17、iPhone 17 Pro、iPhone 17 Pro Max 或 iPhone Air 等新機型的熱情。但在獲得新 iPhone 之前&#xff0c;有一件重要的事情要做&#xff1a;將數據從舊 iPhone 轉移到新設備。雖然許多用戶都能…

Java關鍵字深度解析(上)

這是一份全面的Java關鍵字實戰指南 目錄 1.數據類型關鍵字:內存布局與性能優化 1.1 基礎類型的內存密碼 byte-內存的極簡主義者 int-Java世界的萬能鑰匙 long - 時間與ID的守護者 1.2 引用類型的架構設計 String-不是關鍵字但勝于關鍵字 2.訪問修飾符:企業級權限控制 …

C語言深度解析:指針數組與數組指針的區別與應用

目錄 1 引言&#xff1a;從名字理解本質區別 2 指針數組&#xff1a;靈活管理多個指針 2.1 基本概念與聲明方式 2.2 內存布局與特性 2.3 典型應用場景&#xff1a;字符串數組與多維度數據管理 2.3.1 靜態分配示例&#xff1a;字符串數組 2.3.2 動態分配示例&#xff1a;…

Node.js 高級應用:負載均衡與流量限制

在當今高并發的網絡應用環境中&#xff0c;如何有效地分配服務器資源并保護系統免受惡意攻擊是開發者必須面對的重要問題。Node.js 作為一款廣受歡迎的服務器端 JavaScript 運行時環境&#xff0c;提供了豐富的工具和模塊來應對這些挑戰。本文將深入探討如何在 Node.js 中實現負…

信任鏈驗證流程

信任鏈驗證流程 (The Chain of Trust)整個過程就像一場嚴格的接力賽&#xff0c;每一棒都必須從可信的上一位手中接過接力棒&#xff08;信任&#xff09;&#xff0c;驗證無誤后&#xff0c;再跑自己的那段路&#xff0c;并把信任傳遞給下一棒現在&#xff0c;我們來詳細解讀圖…