Files
ai-dimension/server/src/models/User.js
T
Yuzhiran Dev c1d6dd3b29 refactor: 项目架构重构 - 目录标准化 + 清理冗余
- 目录重命名: backend/wdkj-server/ → server/, frontend/ai-dimension/ → client/
- 删除 20+ 冗余文件(WDKJ 旧脚本、Windows 脚本、过时文档、设计稿)
- 更新 package.json 元数据(移除 wdkj 命名)
- 完善三级 .gitignore(根 + server + client)
- 重写 README.md 和 CHANGELOG.md
- 工具脚本移至 scripts/
2026-07-11 12:45:37 +08:00

306 lines
7.9 KiB
JavaScript
Executable File

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)