const mongoose = require('mongoose') const { Schema } = mongoose const bcrypt = require('bcryptjs') /** * 用户探索数据结构 */ const ExploreDataSchema = new Schema({ dim1: { completed: { type: Boolean, default: false }, bestScore: { type: Number, default: 0 }, collectedEggs: { type: Number, default: 0 }, totalLength: { type: Number, default: 0 } }, dim2: { completed: { type: Boolean, default: false }, bestArea: { type: Number, default: 0 }, createdShapes: { type: Number, default: 0 } }, dim3: { completed: { type: Boolean, default: false }, exploredFaces: { type: Number, default: 0 }, rotationTime: { type: Number, default: 0 }, bestScore: { type: Number, default: 0 } }, dim4: { completed: { type: Boolean, default: false }, bestScore: { type: Number, default: 0 }, exploredEvents: { type: Number, default: 0 } }, dim5: { completed: { type: Boolean, default: false }, bestScore: { type: Number, default: 0 }, exploredThoughts: { type: Number, default: 0 } } }, { _id: false }) /** * 用户模型 */ const UserSchema = new Schema({ // 微信用户标识 openid: { type: String, required: false, unique: true, sparse: true, index: true }, unionid: { type: String, index: true }, // H5环境用户名密码登录 username: { type: String, unique: true, sparse: true, index: true }, password: { type: String, select: false }, // 基本信息 nickName: { type: String, default: '星辰旅行者' }, avatarUrl: { type: String, default: '' }, // 探索数据 exploreData: { type: ExploreDataSchema, default: () => ({}) }, // 分数(冗余字段,便于排行榜查询) totalScore: { type: Number, default: 0 }, dim1Score: { type: Number, default: 0 }, dim2Score: { type: Number, default: 0 }, dim3Score: { type: Number, default: 0 }, dim4Score: { type: Number, default: 0 }, dim5Score: { type: Number, default: 0 }, // 已解锁维度 unlockedDims: [{ type: Number, enum: [1, 2, 3, 4, 5] }], // 商品相关 ownedSkins: [{ type: String }], purchasedItems: [{ type: String }], // 会员状态 isAdFree: { type: Boolean, default: false }, isSubscriber: { type: Boolean, default: false }, subscribeExpiry: { type: Date }, // 收藏与成就 collectedKnowledge: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Knowledge' }], achievements: [{ type: String }], // 设置 settings: { voiceEnabled: { type: Boolean, default: true }, difficulty: { type: String, enum: ['easy', 'normal', 'hard'], default: 'normal' } }, // 探索等级系统 level: { type: Number, default: 1 }, levelPoints: { type: Number, default: 0 }, // 时间戳 totalPlayTime: { type: Number, default: 0 }, createdAt: { type: Date, default: Date.now }, lastLoginAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }) // 索引 UserSchema.index({ totalScore: -1 }) UserSchema.index({ dim1Score: -1 }) UserSchema.index({ dim2Score: -1 }) UserSchema.index({ dim3Score: -1 }) UserSchema.index({ dim4Score: -1 }) UserSchema.index({ dim5Score: -1 }) UserSchema.index({ createdAt: -1 }) UserSchema.index({ lastLoginAt: -1 }) // 用于活跃用户统计 // 虚拟字段:是否是新用户(24小时内注册) UserSchema.virtual('isNew').get(function() { return Date.now() - this.createdAt < 24 * 60 * 60 * 1000 }) // 中间件:保存前加密密码(仅针对username/password登录) UserSchema.pre('save', async function(next) { if (!this.isModified('password')) { return next() } try { const salt = await bcrypt.genSalt(10) this.password = await bcrypt.hash(this.password, salt) next() } catch (error) { next(error) } }) // 实例方法:验证密码 UserSchema.methods.validatePassword = async function(password) { return bcrypt.compare(password, this.password) } // 实例方法:计算总分 UserSchema.methods.calculateTotalScore = function() { this.totalScore = (this.exploreData.dim1?.bestScore || 0) + (this.exploreData.dim2?.bestArea || 0) + (this.exploreData.dim3?.bestScore || 0) + (this.exploreData.dim4?.bestScore || 0) + (this.exploreData.dim5?.bestScore || 0) // 同步维度分数 this.dim1Score = this.exploreData.dim1?.bestScore || 0 this.dim2Score = this.exploreData.dim2?.bestArea || 0 this.dim3Score = this.exploreData.dim3?.bestScore || 0 this.dim4Score = this.exploreData.dim4?.bestScore || 0 this.dim5Score = this.exploreData.dim5?.bestScore || 0 return this.totalScore } // 实例方法:计算等级 UserSchema.methods.calculateLevel = function() { // 等级计算公式:基于总分和探索时长 // 基础分:每100分 = 1级 const scorePoints = Math.floor(this.totalScore / 100) // 时长分:每1小时 = 1级 const timePoints = Math.floor(this.totalPlayTime / 60) // 成就分:每个成就 = 2级 const achievementPoints = (this.achievements?.length || 0) * 2 // 收藏分:每5个收藏 = 1级 const collectionPoints = Math.floor((this.collectedKnowledge?.length || 0) / 5) this.levelPoints = scorePoints + timePoints + achievementPoints + collectionPoints // 等级阈值表 const levelThresholds = [ 0, 5, 15, 30, 50, 75, 105, 140, 180, 225, 275, 330, 390, 455, 525, 600, 680, 765, 855, 950, 1050 ] // 计算等级 let newLevel = 1 for (let i = 0; i < levelThresholds.length; i++) { if (this.levelPoints >= levelThresholds[i]) { newLevel = i + 1 } else { break } } this.level = newLevel return this.level } // 实例方法:获取等级信息 UserSchema.methods.getLevelInfo = function() { const levelNames = [ '星辰旅者', '维度学徒', '空间探索者', '几何学者', '维度行者', '时空旅人', '多维大师', '宇宙探索者', '维度掌控者', '空间主宰', '维度领主', '宇宙行者', '时空主宰', '维度之神', '宇宙之主', '维度创世者', '空间造物主', '宇宙掌控者', '维度至尊', '宇宙之神' ] const levelThresholds = [ 0, 5, 15, 30, 50, 75, 105, 140, 180, 225, 275, 330, 390, 455, 525, 600, 680, 765, 855, 950, 1050 ] const currentLevel = this.level || 1 const currentPoints = this.levelPoints || 0 const nextLevelPoints = levelThresholds[currentLevel] || levelThresholds[levelThresholds.length - 1] const progress = Math.min(100, Math.floor((currentPoints / nextLevelPoints) * 100)) return { level: currentLevel, name: levelNames[Math.min(currentLevel - 1, levelNames.length - 1)], points: currentPoints, nextLevelPoints: nextLevelPoints, progress: progress, totalProgress: Math.min(100, Math.floor((currentPoints / 1050) * 100)) } } // 静态方法:获取排行榜 UserSchema.statics.getLeaderboard = function(dim = null, limit = 50) { const sortField = dim ? `dim${dim}Score` : 'totalScore' return this.find() .select('openid nickName avatarUrl totalScore dim1Score dim2Score dim3Score dim4Score dim5Score unlockedDims') .sort({ [sortField]: -1 }) .limit(limit) .lean() } // 中间件:保存前更新 updatedAt、计算总分和等级 UserSchema.pre('save', function(next) { this.updatedAt = Date.now() this.calculateTotalScore() this.calculateLevel() next() }) module.exports = mongoose.model('User', UserSchema)