refactor: 项目架构重构 - 目录标准化 + 清理冗余

- 目录重命名: backend/wdkj-server/ → server/, frontend/ai-dimension/ → client/
- 删除 20+ 冗余文件(WDKJ 旧脚本、Windows 脚本、过时文档、设计稿)
- 更新 package.json 元数据(移除 wdkj 命名)
- 完善三级 .gitignore(根 + server + client)
- 重写 README.md 和 CHANGELOG.md
- 工具脚本移至 scripts/
This commit is contained in:
Yuzhiran Dev
2026-07-11 12:45:37 +08:00
parent 66ac576082
commit c1d6dd3b29
106 changed files with 17436 additions and 2311 deletions
+113
View File
@@ -0,0 +1,113 @@
const mongoose = require('mongoose')
/**
* 用户AI问答次数记录模型
*/
const AIChatQuotaSchema = new mongoose.Schema({
openid: {
type: String,
required: true,
unique: true,
index: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
index: true
},
// 剩余免费问答次数
freeQuota: {
type: Number,
default: 5
},
// 通过分享获得的次数
sharedQuota: {
type: Number,
default: 0
},
// 已使用的总次数
usedQuota: {
type: Number,
default: 0
},
// 今日已使用次数
dailyUsed: {
type: Number,
default: 0
},
// 最后使用日期
lastUsedDate: {
type: Date,
default: null
},
// 分享获得次数的记录
shareRecords: [{
sharedAt: { type: Date, default: Date.now },
gainedQuota: { type: Number, default: 5 }
}],
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
})
// 更新时自动更新 updatedAt
AIChatQuotaSchema.pre('save', function(next) {
this.updatedAt = Date.now()
next()
})
// 获取总可用次数
AIChatQuotaSchema.methods.getTotalQuota = function() {
return this.freeQuota + this.sharedQuota
}
// 获取剩余可用次数
AIChatQuotaSchema.methods.getRemainingQuota = function() {
return this.getTotalQuota() - this.usedQuota
}
// 检查是否有可用次数
AIChatQuotaSchema.methods.hasQuota = function() {
return this.getRemainingQuota() > 0
}
// 使用一次问答机会
AIChatQuotaSchema.methods.useQuota = async function() {
if (!this.hasQuota()) {
return false
}
this.usedQuota += 1
this.dailyUsed += 1
this.lastUsedDate = new Date()
await this.save()
return true
}
// 通过分享增加次数
AIChatQuotaSchema.methods.addQuotaByShare = async function(gainedQuota = 5) {
this.sharedQuota += gainedQuota
this.shareRecords.push({
sharedAt: new Date(),
gainedQuota
})
await this.save()
return this.getRemainingQuota()
}
// 重置每日使用次数(可定时任务调用)
AIChatQuotaSchema.methods.resetDailyUsed = function() {
const today = new Date().toDateString()
const lastUsed = this.lastUsedDate ? new Date(this.lastUsedDate).toDateString() : null
if (lastUsed !== today) {
this.dailyUsed = 0
}
}
module.exports = mongoose.model('AIChatQuota', AIChatQuotaSchema)
+117
View File
@@ -0,0 +1,117 @@
const mongoose = require('mongoose')
/**
* AI模型配置模型
*/
const AIModelSchema = new mongoose.Schema({
// 模型名称
name: {
type: String,
required: true,
trim: true
},
// 模型ID
modelId: {
type: String,
required: true,
unique: true,
trim: true
},
// API地址
apiUrl: {
type: String,
required: true,
trim: true
},
// API Key
apiKey: {
type: String,
required: true
},
// 模型描述
description: {
type: String,
default: ''
},
// 是否启用
isActive: {
type: Boolean,
default: true
},
// 是否为默认模型
isDefault: {
type: Boolean,
default: false
},
// 模型参数配置
config: {
temperature: {
type: Number,
default: 0.7,
min: 0,
max: 2
},
maxTokens: {
type: Number,
default: 800,
min: 1,
max: 4096
},
topP: {
type: Number,
default: 1,
min: 0,
max: 1
}
},
// 优先级(数字越小优先级越高)
priority: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
})
// 更新时自动更新 updatedAt
AIModelSchema.pre('save', function(next) {
this.updatedAt = Date.now()
next()
})
// 设置默认模型时,取消其他模型的默认状态
AIModelSchema.pre('save', async function(next) {
if (this.isDefault && this.isModified('isDefault')) {
await this.constructor.updateMany(
{ _id: { $ne: this._id } },
{ isDefault: false }
)
}
next()
})
// 获取当前使用的模型(优先返回默认模型,如果没有则返回第一个启用的模型)
AIModelSchema.statics.getCurrentModel = async function() {
// 先查找默认模型
let model = await this.findOne({ isDefault: true, isActive: true })
// 如果没有默认模型,返回第一个启用的模型
if (!model) {
model = await this.findOne({ isActive: true }).sort({ priority: 1, createdAt: -1 })
}
return model
}
// 获取所有启用的模型列表
AIModelSchema.statics.getActiveModels = async function() {
return await this.find({ isActive: true }).sort({ priority: 1, createdAt: -1 })
}
module.exports = mongoose.model('AIModel', AIModelSchema)
+130
View File
@@ -0,0 +1,130 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
const bcrypt = require('bcryptjs')
/**
* 管理员模型
*/
const AdminSchema = new Schema({
// 登录信息
username: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
minlength: 3,
maxlength: 30
},
password: {
type: String,
required: true,
minlength: 6
},
email: {
type: String,
trim: true,
lowercase: true
},
// 基本信息
realName: String,
phone: String,
avatar: String,
// 角色
role: {
type: String,
enum: ['super_admin', 'content_manager', 'shop_manager', 'viewer'],
default: 'viewer'
},
// 权限
permissions: [{
type: String
}],
// 状态
status: {
type: String,
enum: ['active', 'inactive', 'suspended'],
default: 'active'
},
// 登录信息
lastLogin: Date,
lastLoginIp: String,
loginCount: {
type: Number,
default: 0
},
// 时间戳
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
})
// 索引(username 已通过 unique: true 自动创建索引)
AdminSchema.index({ role: 1 })
// 中间件:保存前加密密码
AdminSchema.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)
}
})
// 实例方法:验证密码
AdminSchema.methods.validatePassword = async function(password) {
return bcrypt.compare(password, this.password)
}
// 实例方法:检查权限
AdminSchema.methods.hasPermission = function(permission) {
if (this.permissions.includes('*')) return true
return this.permissions.includes(permission)
}
// 静态方法:根据角色获取权限
AdminSchema.statics.getPermissionsByRole = function(role) {
const permissionMap = {
super_admin: ['*'],
content_manager: [
'knowledge:read', 'knowledge:write', 'knowledge:delete',
'gallery:read', 'gallery:approve', 'gallery:delete',
'users:read'
],
shop_manager: [
'shop:read', 'shop:write', 'shop:delete',
'orders:read', 'orders:process', 'orders:refund',
'users:read'
],
viewer: [
'dashboard:read',
'analytics:read',
'users:read'
]
}
return permissionMap[role] || []
}
module.exports = mongoose.model('Admin', AdminSchema)
+54
View File
@@ -0,0 +1,54 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 管理员操作日志模型
*/
const AdminLogSchema = new Schema({
// 管理员信息
adminId: {
type: Schema.Types.ObjectId,
ref: 'Admin',
required: true,
index: true
},
adminName: {
type: String,
required: true
},
// 操作信息
action: {
type: String,
required: true,
enum: ['login', 'create', 'read', 'update', 'delete', 'review', 'export']
},
resource: {
type: String,
required: true
},
resourceId: String,
// 详细信息
details: Schema.Types.Mixed,
// IP 地址
ip: String,
userAgent: String,
// 时间戳
timestamp: {
type: Date,
default: Date.now,
index: true
}
}, {
timestamps: false
})
// 索引
AdminLogSchema.index({ adminId: 1, timestamp: -1 })
AdminLogSchema.index({ resource: 1, timestamp: -1 })
AdminLogSchema.index({ action: 1 })
module.exports = mongoose.model('AdminLog', AdminLogSchema)
+191
View File
@@ -0,0 +1,191 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 反馈模型
* 存储用户的建议、问题和反馈
*/
const FeedbackSchema = new Schema({
// 反馈者信息
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
index: true
},
openid: {
type: String,
index: true
},
userName: {
type: String,
default: ''
},
userContact: {
type: String,
default: ''
},
// 反馈类型
type: {
type: String,
enum: ['suggestion', 'bug', 'complaint', 'praise', 'other'],
required: true,
index: true
},
// 反馈标题
title: {
type: String,
required: true,
maxlength: 100
},
// 反馈内容
content: {
type: String,
required: true,
maxlength: 2000
},
// 图片附件
images: [{
type: String
}],
// 相关页面/功能
relatedPage: {
type: String,
default: ''
},
// 设备信息
deviceInfo: {
device: { type: String, default: '' },
os: { type: String, default: '' },
osVersion: { type: String, default: '' },
browser: { type: String, default: '' },
browserVersion: { type: String, default: '' },
screenResolution: { type: String, default: '' },
appVersion: { type: String, default: '' }
},
// 网络信息
networkInfo: {
type: { type: String, default: '' },
ip: { type: String, default: '' }
},
// 处理状态
status: {
type: String,
enum: ['pending', 'processing', 'resolved', 'rejected', 'closed'],
default: 'pending',
index: true
},
// 优先级
priority: {
type: String,
enum: ['low', 'normal', 'high', 'urgent'],
default: 'normal'
},
// 处理记录
processLog: [{
operator: { type: String, required: true },
action: { type: String, required: true },
comment: { type: String, default: '' },
createdAt: { type: Date, default: Date.now }
}],
// 处理结果
result: {
type: String,
default: ''
},
// 处理人
handler: {
type: String,
default: ''
},
// 处理时间
handledAt: {
type: Date
},
// 用户评分(对处理结果的满意度)
rating: {
type: Number,
min: 1,
max: 5
},
// 用户备注
userRemark: {
type: String,
default: ''
},
// 创建时间
createdAt: {
type: Date,
default: Date.now,
index: true
},
// 更新时间
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: { createdAt: false, updatedAt: true }
})
// 索引
FeedbackSchema.index({ status: 1, createdAt: -1 })
FeedbackSchema.index({ type: 1, createdAt: -1 })
FeedbackSchema.index({ priority: 1, createdAt: -1 })
// 静态方法:获取待处理的反馈数量
FeedbackSchema.statics.getPendingCount = function() {
return this.countDocuments({ status: 'pending' })
}
// 静态方法:获取反馈统计
FeedbackSchema.statics.getStats = function(startDate, endDate) {
return this.aggregate([
{
$match: {
createdAt: { $gte: startDate, $lte: endDate }
}
},
{
$group: {
_id: '$status',
count: { $sum: 1 }
}
}
])
}
// 静态方法:按类型统计
FeedbackSchema.statics.getTypeStats = function(startDate, endDate) {
return this.aggregate([
{
$match: {
createdAt: { $gte: startDate, $lte: endDate }
}
},
{
$group: {
_id: '$type',
count: { $sum: 1 }
}
}
])
}
module.exports = mongoose.model('Feedback', FeedbackSchema)
+125
View File
@@ -0,0 +1,125 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 知识库模型
*/
const KnowledgeSchema = new Schema({
// 基本信息
title: {
type: String,
required: true,
trim: true,
maxlength: 100
},
content: {
type: String,
required: true
},
// 分类
dim: {
type: Number,
enum: [1, 2, 3, 4, 5],
required: true,
index: true
},
category: {
type: String,
enum: ['concept', 'history', 'application', 'question'],
required: true
},
tags: [{
type: String,
maxlength: 20
}],
// 多层级内容
sections: [{
title: String,
content: String,
images: [String]
}],
// 问答系统
question: String,
answer: String,
options: [{
text: String,
isCorrect: Boolean
}],
// 统计
viewCount: {
type: Number,
default: 0
},
collectionCount: {
type: Number,
default: 0
},
// 付费与订阅
isPremium: {
type: Boolean,
default: false
},
requiredPoints: {
type: Number,
default: 0
},
price: {
type: Number,
default: 0
},
currency: {
type: String,
default: 'CNY'
},
// 状态
status: {
type: String,
enum: ['draft', 'pending', 'approved', 'rejected'],
default: 'draft',
index: true
},
// 审核信息
reviewComment: String,
reviewedAt: Date,
reviewerId: String,
// 排序
sortOrder: {
type: Number,
default: 0
},
// 时间戳
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
})
// 索引
KnowledgeSchema.index({ dim: 1, sortOrder: 1 })
KnowledgeSchema.index({ status: 1, createdAt: -1 })
KnowledgeSchema.index({ category: 1 })
// 中间件:保存前更新 updatedAt
KnowledgeSchema.pre('save', function(next) {
this.updatedAt = Date.now()
next()
})
module.exports = mongoose.model('Knowledge', KnowledgeSchema)
+142
View File
@@ -0,0 +1,142 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 登录日志模型
* 记录用户的登录/注册行为
*/
const LoginLogSchema = new Schema({
// 用户标识
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
openid: {
type: String,
required: true,
index: true
},
// 登录类型
type: {
type: String,
enum: ['register', 'login', 'auto_login'],
required: true
},
// 登录方式
method: {
type: String,
enum: ['wechat_miniapp', 'wechat_webview', 'wechat_oauth', 'username', 'other'],
required: true
},
// IP地址
ip: {
type: String,
default: ''
},
// 终端信息
userAgent: {
type: String,
default: ''
},
// 设备信息
device: {
type: String,
default: ''
},
// 操作系统
os: {
type: String,
default: ''
},
// 浏览器
browser: {
type: String,
default: ''
},
// 登录来源
source: {
type: String,
enum: ['miniapp', 'webview', 'h5', 'admin', 'other'],
default: 'other'
},
// 登录结果
success: {
type: Boolean,
default: true
},
// 失败原因
failReason: {
type: String,
default: ''
},
// 地理位置(可选)
location: {
country: { type: String, default: '' },
province: { type: String, default: '' },
city: { type: String, default: '' }
},
// 登录时间
createdAt: {
type: Date,
default: Date.now,
index: true
}
}, {
timestamps: false
})
// 索引
LoginLogSchema.index({ userId: 1, createdAt: -1 })
LoginLogSchema.index({ openid: 1, createdAt: -1 })
LoginLogSchema.index({ type: 1, createdAt: -1 })
LoginLogSchema.index({ method: 1, createdAt: -1 })
// 静态方法:获取用户的登录历史
LoginLogSchema.statics.getUserLoginHistory = function(userId, limit = 10) {
return this.find({ userId })
.sort({ createdAt: -1 })
.limit(limit)
.lean()
}
// 静态方法:获取最近的登录日志
LoginLogSchema.statics.getRecentLogs = function(limit = 50) {
return this.find()
.sort({ createdAt: -1 })
.limit(limit)
.populate('userId', 'nickName avatarUrl')
.lean()
}
// 静态方法:统计登录数据
LoginLogSchema.statics.getLoginStats = function(startDate, endDate) {
return this.aggregate([
{
$match: {
createdAt: { $gte: startDate, $lte: endDate }
}
},
{
$group: {
_id: '$type',
count: { $sum: 1 }
}
}
])
}
module.exports = mongoose.model('LoginLog', LoginLogSchema)
+120
View File
@@ -0,0 +1,120 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 订单模型
*/
const OrderSchema = new Schema({
// 订单基本信息
orderNo: {
type: String,
required: true,
unique: true,
index: true
},
// 用户信息
openid: {
type: String,
required: true,
index: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
index: true
},
// 商品信息
productId: {
type: String,
required: true
},
productName: {
type: String,
required: true
},
amount: {
type: Number,
required: true
},
// 支付信息
status: {
type: String,
enum: ['pending', 'paid', 'completed', 'failed', 'refunded'],
default: 'pending',
index: true
},
prepayId: String,
transactionId: String,
// 微信支付参数(用于调试)
paymentParams: Schema.Types.Mixed,
rawResult: Schema.Types.Mixed,
// 审核信息(管理后台)
reviewComment: String,
reviewedAt: Date,
reviewerId: String,
// 时间戳
createdAt: {
type: Date,
default: Date.now,
index: true
},
paidAt: Date,
completedAt: Date,
refundedAt: Date
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
})
// 索引
OrderSchema.index({ openid: 1, createdAt: -1 })
OrderSchema.index({ status: 1, createdAt: -1 })
OrderSchema.index({ userId: 1, createdAt: -1 }) // 优化用户订单查询
// 静态方法:获取今日订单数
OrderSchema.statics.getTodayCount = async function() {
const today = new Date()
today.setHours(0, 0, 0, 0)
return this.countDocuments({
createdAt: { $gte: today },
status: 'paid'
})
}
// 静态方法:获取订单趋势(7天)
OrderSchema.statics.getWeekTrend = async function() {
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
weekAgo.setHours(0, 0, 0, 0)
return this.aggregate([
{
$match: {
createdAt: { $gte: weekAgo },
status: 'paid'
}
},
{
$group: {
_id: {
$dateToString: { format: '%Y-%m-%d', date: '$createdAt' }
},
count: { $sum: 1 },
revenue: { $sum: '$amount' }
}
},
{
$sort: { '_id': 1 }
}
])
}
module.exports = mongoose.model('Order', OrderSchema)
+109
View File
@@ -0,0 +1,109 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 商品模型
*/
const ShopItemSchema = new Schema({
// 基本信息
itemId: {
type: String,
required: true,
unique: true,
index: true
},
name: {
type: String,
required: true,
trim: true
},
description: {
type: String,
trim: true
},
// 价格(单位:分)
price: {
type: Number,
required: true,
min: 1
},
originalPrice: {
type: Number
},
// 商品类型
type: {
type: String,
enum: ['skin', 'noad', 'subscription'],
required: true
},
// 订阅天数(仅订阅商品)
duration: {
type: Number,
default: 30
},
// 图片
imageUrl: String,
previewUrl: String,
// 统计
purchaseCount: {
type: Number,
default: 0
},
// 状态
status: {
type: String,
enum: ['active', 'inactive', 'discontinued'],
default: 'active',
index: true
},
// 排序
sortOrder: {
type: Number,
default: 0
},
// 促销
isOnSale: {
type: Boolean,
default: false
},
saleEndTime: Date,
// 时间戳
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
})
// 索引
ShopItemSchema.index({ status: 1, sortOrder: 1 })
ShopItemSchema.index({ type: 1 })
// 虚拟字段:是否在促销中
ShopItemSchema.virtual('isOnSaleActive').get(function() {
return this.isOnSale && this.saleEndTime && new Date() < this.saleEndTime
})
// 中间件:保存前更新 updatedAt
ShopItemSchema.pre('save', function(next) {
this.updatedAt = Date.now()
next()
})
module.exports = mongoose.model('ShopItem', ShopItemSchema)
+33
View File
@@ -0,0 +1,33 @@
const mongoose = require('mongoose');
const { Schema } = mongoose;
const TrendSchema = new Schema({
title: { type: String, required: true, trim: true },
summary: { type: String },
source: { type: String, default: '未知来源' },
sourceUrl: { type: String },
category: {
type: String,
enum: ['产品', '论文', '工具', '公司', '政策', '其他'],
default: '其他'
},
tags: [String],
hot: { type: Boolean, default: false },
picked: { type: Boolean, default: false },
newsDate: { type: Date, default: Date.now, index: true },
viewCount: { type: Number, default: 0 },
status: {
type: String,
enum: ['draft', 'published'],
default: 'published'
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
TrendSchema.index({ status: 1, newsDate: -1 });
TrendSchema.index({ category: 1, newsDate: -1 });
module.exports = mongoose.model('Trend', TrendSchema);
+305
View File
@@ -0,0 +1,305 @@
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)
+25
View File
@@ -0,0 +1,25 @@
const User = require('./User')
const Order = require('./Order')
const Knowledge = require('./Knowledge')
const ShopItem = require('./ShopItem')
const Admin = require('./Admin')
const AdminLog = require('./AdminLog')
const AIChatQuota = require('./AIChatQuota')
const AIModel = require('./AIModel')
const LoginLog = require('./LoginLog')
const Feedback = require('./Feedback')
const Trend = require('./Trend')
module.exports = {
User,
Order,
Knowledge,
ShopItem,
Admin,
AdminLog,
AIChatQuota,
AIModel,
LoginLog,
Feedback,
Trend
}