c1d6dd3b29
- 目录重命名: backend/wdkj-server/ → server/, frontend/ai-dimension/ → client/ - 删除 20+ 冗余文件(WDKJ 旧脚本、Windows 脚本、过时文档、设计稿) - 更新 package.json 元数据(移除 wdkj 命名) - 完善三级 .gitignore(根 + server + client) - 重写 README.md 和 CHANGELOG.md - 工具脚本移至 scripts/
306 lines
7.9 KiB
JavaScript
Executable File
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)
|