🎉 feat: initialize 宇之然AI维度 project
- uni-app Vue3 frontend with dark glassmorphism theme - 5 AI dimensions: Origin / Development / Current / Learning / Trend - AI Chat system prompt updated from geometry to AI - 23 AI knowledge articles initialized in DB - Trend API + cron job for daily news - Product pricing (Pro ¥19.9, VIP ¥39.9) - Pinia stores + API utilities - AGENTS.md documentation
This commit is contained in:
+113
@@ -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)
|
||||
Executable
+117
@@ -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)
|
||||
Executable
+130
@@ -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)
|
||||
Executable
+54
@@ -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)
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const BGMschema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
dimension: {
|
||||
type: Number,
|
||||
required: true,
|
||||
enum: [1, 2, 3, 4, 5],
|
||||
index: true
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
loop: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
volume: {
|
||||
type: Number,
|
||||
default: 0.5,
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
index: true
|
||||
},
|
||||
sortOrder: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
})
|
||||
|
||||
// 更新时自动修改 updatedAt
|
||||
BGMschema.pre('save', function(next) {
|
||||
this.updatedAt = Date.now()
|
||||
next()
|
||||
})
|
||||
|
||||
// 复合索引:维度 + 激活状态 + 排序
|
||||
BGMschema.index({ dimension: 1, isActive: 1, sortOrder: 1 })
|
||||
|
||||
module.exports = mongoose.model('BGM', BGMschema)
|
||||
Executable
+191
@@ -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)
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
const mongoose = require('mongoose')
|
||||
const { Schema } = mongoose
|
||||
|
||||
/**
|
||||
* 画廊作品模型
|
||||
*/
|
||||
const GallerySchema = new Schema({
|
||||
// 用户信息
|
||||
openid: {
|
||||
type: String,
|
||||
required: false,
|
||||
index: true
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
index: true
|
||||
},
|
||||
authorName: {
|
||||
type: String,
|
||||
default: '星辰旅者'
|
||||
},
|
||||
authorAvatar: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 作品信息
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 100
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500
|
||||
},
|
||||
|
||||
// 图片数据(Base64 或 URL)
|
||||
imageData: String,
|
||||
imageUrl: String,
|
||||
|
||||
// 作品属性
|
||||
tags: [{
|
||||
type: String,
|
||||
maxlength: 20
|
||||
}],
|
||||
dim: {
|
||||
type: Number,
|
||||
enum: [1, 2, 3, 4, 5],
|
||||
default: 1
|
||||
},
|
||||
|
||||
// 统计
|
||||
likeCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
viewCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 审核状态
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'approved', 'rejected'],
|
||||
default: 'pending',
|
||||
index: true
|
||||
},
|
||||
reviewComment: String,
|
||||
reviewedAt: Date,
|
||||
reviewerId: String,
|
||||
|
||||
// 时间戳
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
index: true
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
})
|
||||
|
||||
// 索引
|
||||
GallerySchema.index({ openid: 1, createdAt: -1 })
|
||||
GallerySchema.index({ status: 1, createdAt: -1 })
|
||||
GallerySchema.index({ dim: 1, createdAt: -1 })
|
||||
GallerySchema.index({ likeCount: -1 })
|
||||
|
||||
// 静态方法:获取今日作品数
|
||||
GallerySchema.statics.getTodayCount = async function() {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
return this.countDocuments({
|
||||
createdAt: { $gte: today },
|
||||
status: 'approved'
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = mongoose.model('Gallery', GallerySchema)
|
||||
Executable
+125
@@ -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)
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
const mongoose = require('mongoose')
|
||||
const { Schema } = mongoose
|
||||
|
||||
/**
|
||||
* 关卡配置模型
|
||||
*/
|
||||
const LevelSchema = new Schema({
|
||||
// 关卡基本信息
|
||||
dimension: {
|
||||
type: Number,
|
||||
required: true,
|
||||
enum: [1, 2, 3, 4, 5],
|
||||
index: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
|
||||
// 关卡难度配置
|
||||
difficulty: {
|
||||
type: String,
|
||||
enum: ['easy', 'normal', 'hard', 'expert'],
|
||||
default: 'normal'
|
||||
},
|
||||
|
||||
// 通关条件
|
||||
requirements: {
|
||||
score: { type: Number, default: 0 },
|
||||
timeLimit: { type: Number, default: 0 }, // 秒,0表示无限制
|
||||
collectibles: { type: Number, default: 0 } // 需要收集的物品数量
|
||||
},
|
||||
|
||||
// 奖励配置
|
||||
rewards: {
|
||||
exp: { type: Number, default: 100 },
|
||||
coins: { type: Number, default: 50 },
|
||||
unlockNext: { type: Boolean, default: true }
|
||||
},
|
||||
|
||||
// 关卡描述
|
||||
title: String,
|
||||
description: String,
|
||||
tips: [String], // 游戏提示
|
||||
|
||||
// 状态
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isLocked: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 排序
|
||||
sortOrder: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 时间戳
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
})
|
||||
|
||||
// 复合索引:维度 + 关卡等级
|
||||
LevelSchema.index({ dimension: 1, level: 1 }, { unique: true })
|
||||
LevelSchema.index({ dimension: 1, isActive: 1, sortOrder: 1 })
|
||||
|
||||
module.exports = mongoose.model('Level', LevelSchema)
|
||||
Executable
+142
@@ -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)
|
||||
Executable
+120
@@ -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)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const ShareRecordSchema = new mongoose.Schema({
|
||||
openid: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
index: true
|
||||
},
|
||||
shareType: {
|
||||
type: String,
|
||||
enum: ['app', 'dim1', 'dim2', 'dim3', 'dim4', 'dim5'],
|
||||
default: 'app'
|
||||
},
|
||||
score: { type: Number, default: 0 },
|
||||
sharedAt: { type: Date, default: Date.now }
|
||||
})
|
||||
|
||||
ShareRecordSchema.index({ sharedAt: -1 })
|
||||
|
||||
module.exports = mongoose.model('ShareRecord', ShareRecordSchema)
|
||||
Executable
+109
@@ -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)
|
||||
@@ -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);
|
||||
Executable
+305
@@ -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)
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
const User = require('./User')
|
||||
const Order = require('./Order')
|
||||
const Gallery = require('./Gallery')
|
||||
const Knowledge = require('./Knowledge')
|
||||
const ShopItem = require('./ShopItem')
|
||||
const Admin = require('./Admin')
|
||||
const AdminLog = require('./AdminLog')
|
||||
const ShareRecord = require('./ShareRecord')
|
||||
const BGM = require('./BGM')
|
||||
const Level = require('./Level')
|
||||
const AIChatQuota = require('./AIChatQuota')
|
||||
const AIModel = require('./AIModel')
|
||||
const LoginLog = require('./LoginLog')
|
||||
const Feedback = require('./Feedback')
|
||||
const Trend = require('./Trend')
|
||||
|
||||
// 拼音探索模块模型
|
||||
const pinyinModels = require('./pinyin')
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Order,
|
||||
Gallery,
|
||||
Knowledge,
|
||||
ShopItem,
|
||||
Admin,
|
||||
AdminLog,
|
||||
ShareRecord,
|
||||
BGM,
|
||||
Level,
|
||||
AIChatQuota,
|
||||
AIModel,
|
||||
LoginLog,
|
||||
Feedback,
|
||||
Trend,
|
||||
// 拼音探索模型
|
||||
...pinyinModels
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* 拼音探索成就模型
|
||||
* 定义可获得的成就及其条件
|
||||
*/
|
||||
const pinyinAchievementSchema = new mongoose.Schema({
|
||||
// 成就代码
|
||||
code: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true
|
||||
},
|
||||
|
||||
// 成就名称
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
|
||||
// 成就描述
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
// 成就图标
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 成就类型
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['explore', 'game', 'collection', 'streak', 'special'],
|
||||
default: 'explore'
|
||||
},
|
||||
|
||||
// 达成条件
|
||||
condition: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: [
|
||||
'explore_count', // 探索数量
|
||||
'collect_count', // 收集数量
|
||||
'game_count', // 游戏次数
|
||||
'game_score', // 游戏分数
|
||||
'streak_days', // 连续天数
|
||||
'complete_symbol', // 完成指定拼音
|
||||
'complete_type', // 完成某类型所有拼音
|
||||
'explore_all' // 探索所有内容
|
||||
]
|
||||
},
|
||||
value: { type: Number, required: true }, // 条件值
|
||||
symbol: { type: String }, // 特定拼音(可选)
|
||||
symbolType: { type: String } // 特定类型(可选)
|
||||
},
|
||||
|
||||
// 奖励
|
||||
reward: {
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['stone', 'badge', 'theme', 'none'],
|
||||
default: 'none'
|
||||
},
|
||||
value: { type: Number, default: 0 },
|
||||
item: { type: String, default: '' }
|
||||
},
|
||||
|
||||
// 排序
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 是否启用
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 创建时间
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
|
||||
// 更新时间
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
// 索引
|
||||
pinyinAchievementSchema.index({ type: 1, order: 1 });
|
||||
pinyinAchievementSchema.index({ isActive: 1 });
|
||||
pinyinAchievementSchema.index({ code: 1 });
|
||||
|
||||
// 更新中间件
|
||||
pinyinAchievementSchema.pre('save', function(next) {
|
||||
this.updatedAt = Date.now();
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('PinyinAchievement', pinyinAchievementSchema);
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* 拼音内容模型
|
||||
* 存储拼音字母的基础信息、音频、口型图、关联词语等
|
||||
*/
|
||||
const pinyinContentSchema = new mongoose.Schema({
|
||||
// 拼音符号,如 "b"
|
||||
symbol: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true
|
||||
},
|
||||
|
||||
// 拼音类型:initial(声母)/final(韵母)/overall(整体认读)
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['initial', 'final', 'overall']
|
||||
},
|
||||
|
||||
// 拼音名称,如 "玻"
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
|
||||
// 发音音频URL
|
||||
audioUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 口型示意图URL
|
||||
mouthImage: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 发音方法描述
|
||||
pronunciation: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 显示顺序
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 是否免费
|
||||
isFree: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 状态:active(启用)/inactive(禁用)
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['active', 'inactive'],
|
||||
default: 'active'
|
||||
},
|
||||
|
||||
// 相关词语
|
||||
words: [{
|
||||
word: { type: String, required: true }, // 汉字
|
||||
pinyin: { type: String, required: true }, // 拼音
|
||||
image: { type: String, default: '' }, // 图片URL
|
||||
audioUrl: { type: String, default: '' }, // 音频URL
|
||||
meaning: { type: String, default: '' } // 释义
|
||||
}],
|
||||
|
||||
// 关联游戏配置
|
||||
games: [{
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['match', 'tone', 'find', 'puzzle', 'mimic', 'runner']
|
||||
},
|
||||
config: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
isEnabled: { type: Boolean, default: true }
|
||||
}],
|
||||
|
||||
// 探索区域配置
|
||||
exploreAreas: {
|
||||
audio: { type: Boolean, default: true }, // 声音发现
|
||||
mouth: { type: Boolean, default: true }, // 口型观察
|
||||
speak: { type: Boolean, default: true }, // 语音互动
|
||||
write: { type: Boolean, default: true }, // 书写体验
|
||||
game: { type: Boolean, default: true }, // 趣味互动
|
||||
words: { type: Boolean, default: true } // 词语发现
|
||||
},
|
||||
|
||||
// 创建时间
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
|
||||
// 更新时间
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
// 索引
|
||||
pinyinContentSchema.index({ type: 1, order: 1 });
|
||||
pinyinContentSchema.index({ status: 1 });
|
||||
pinyinContentSchema.index({ isFree: 1 });
|
||||
|
||||
// 更新中间件
|
||||
pinyinContentSchema.pre('save', function(next) {
|
||||
this.updatedAt = Date.now();
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('PinyinContent', pinyinContentSchema);
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* 拼音游戏记录模型
|
||||
* 记录用户的游戏行为和成绩
|
||||
*/
|
||||
const pinyinGameRecordSchema = new mongoose.Schema({
|
||||
// 用户ID
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
|
||||
// 游戏类型
|
||||
gameType: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['match', 'tone', 'find', 'puzzle', 'mimic', 'runner']
|
||||
},
|
||||
|
||||
// 游戏难度
|
||||
difficulty: {
|
||||
type: String,
|
||||
enum: ['easy', 'normal', 'hard'],
|
||||
default: 'normal'
|
||||
},
|
||||
|
||||
// 得分
|
||||
score: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 游戏时长(秒)
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 正确数
|
||||
correctCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 错误数
|
||||
wrongCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 准确率
|
||||
accuracy: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 游戏详情
|
||||
details: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: {}
|
||||
},
|
||||
|
||||
// 是否完成
|
||||
isCompleted: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 游戏时间
|
||||
playedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
|
||||
// 创建时间
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
// 索引
|
||||
pinyinGameRecordSchema.index({ userId: 1, gameType: 1 });
|
||||
pinyinGameRecordSchema.index({ userId: 1, playedAt: -1 });
|
||||
pinyinGameRecordSchema.index({ gameType: 1, score: -1 });
|
||||
|
||||
// 静态方法:获取用户游戏统计
|
||||
pinyinGameRecordSchema.statics.getUserStats = async function(userId) {
|
||||
const stats = await this.aggregate([
|
||||
{ $match: { userId: mongoose.Types.ObjectId(userId) } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$gameType',
|
||||
totalGames: { $sum: 1 },
|
||||
totalScore: { $sum: '$score' },
|
||||
avgScore: { $avg: '$score' },
|
||||
maxScore: { $max: '$score' },
|
||||
totalDuration: { $sum: '$duration' },
|
||||
avgAccuracy: { $avg: '$accuracy' }
|
||||
}
|
||||
}
|
||||
]);
|
||||
return stats;
|
||||
};
|
||||
|
||||
// 静态方法:获取排行榜
|
||||
pinyinGameRecordSchema.statics.getLeaderboard = async function(gameType, limit = 10) {
|
||||
const leaderboard = await this.aggregate([
|
||||
{ $match: { gameType: gameType } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$userId',
|
||||
maxScore: { $max: '$score' },
|
||||
totalGames: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{ $sort: { maxScore: -1 } },
|
||||
{ $limit: limit },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'users',
|
||||
localField: '_id',
|
||||
foreignField: '_id',
|
||||
as: 'user'
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
userId: '$_id',
|
||||
maxScore: 1,
|
||||
totalGames: 1,
|
||||
nickname: { $arrayElemAt: ['$user.nickname', 0] },
|
||||
avatar: { $arrayElemAt: ['$user.avatar', 0] }
|
||||
}
|
||||
}
|
||||
]);
|
||||
return leaderboard;
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('PinyinGameRecord', pinyinGameRecordSchema);
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* 用户拼音探索进度模型
|
||||
* 记录用户的探索进度、收集的能量石、成就等
|
||||
*/
|
||||
const pinyinProgressSchema = new mongoose.Schema({
|
||||
// 用户ID
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
|
||||
// 已探索的拼音
|
||||
exploredSymbols: [{
|
||||
symbol: { type: String, required: true }, // 拼音符号
|
||||
exploredAt: { type: Date, default: Date.now }, // 首次探索时间
|
||||
completedAreas: [{ type: String }], // 完成的探索区域
|
||||
isCollected: { type: Boolean, default: false }, // 是否收集能量石
|
||||
collectedAt: { type: Date }, // 收集时间
|
||||
lastExploredAt: { type: Date } // 最后探索时间
|
||||
}],
|
||||
|
||||
// 当前正在探索的拼音
|
||||
currentSymbol: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 总探索数
|
||||
totalExplored: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 收集的能量石数量
|
||||
totalStones: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 获得的成就
|
||||
achievements: [{
|
||||
code: { type: String, required: true }, // 成就代码
|
||||
obtainedAt: { type: Date, default: Date.now } // 获得时间
|
||||
}],
|
||||
|
||||
// 每日统计
|
||||
dailyStats: [{
|
||||
date: { type: Date, required: true }, // 日期
|
||||
exploreCount: { type: Number, default: 0 }, // 探索次数
|
||||
gameCount: { type: Number, default: 0 }, // 游戏次数
|
||||
duration: { type: Number, default: 0 } // 时长(分钟)
|
||||
}],
|
||||
|
||||
// 连续探索天数
|
||||
streakDays: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 最后探索日期
|
||||
lastExploreDate: {
|
||||
type: Date
|
||||
},
|
||||
|
||||
// 创建时间
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
|
||||
// 更新时间
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
// 索引
|
||||
pinyinProgressSchema.index({ userId: 1 });
|
||||
pinyinProgressSchema.index({ totalExplored: -1 });
|
||||
|
||||
// 更新中间件
|
||||
pinyinProgressSchema.pre('save', function(next) {
|
||||
this.updatedAt = Date.now();
|
||||
next();
|
||||
});
|
||||
|
||||
// 方法:获取指定拼音的探索记录
|
||||
pinyinProgressSchema.methods.getSymbolProgress = function(symbol) {
|
||||
return this.exploredSymbols.find(s => s.symbol === symbol);
|
||||
};
|
||||
|
||||
// 方法:检查是否已探索指定拼音
|
||||
pinyinProgressSchema.methods.hasExplored = function(symbol) {
|
||||
return this.exploredSymbols.some(s => s.symbol === symbol);
|
||||
};
|
||||
|
||||
// 方法:检查是否完成指定拼音的所有区域
|
||||
pinyinProgressSchema.methods.isSymbolCompleted = function(symbol) {
|
||||
const record = this.getSymbolProgress(symbol);
|
||||
if (!record) return false;
|
||||
return record.completedAreas.length >= 6; // 6个探索区域
|
||||
};
|
||||
|
||||
// 方法:添加探索记录
|
||||
pinyinProgressSchema.methods.addExplore = function(symbol, area) {
|
||||
let record = this.getSymbolProgress(symbol);
|
||||
|
||||
if (!record) {
|
||||
record = {
|
||||
symbol: symbol,
|
||||
exploredAt: new Date(),
|
||||
completedAreas: [],
|
||||
isCollected: false
|
||||
};
|
||||
this.exploredSymbols.push(record);
|
||||
this.totalExplored += 1;
|
||||
}
|
||||
|
||||
if (!record.completedAreas.includes(area)) {
|
||||
record.completedAreas.push(area);
|
||||
}
|
||||
|
||||
record.lastExploredAt = new Date();
|
||||
this.currentSymbol = symbol;
|
||||
this.lastExploreDate = new Date();
|
||||
};
|
||||
|
||||
// 方法:收集能量石
|
||||
pinyinProgressSchema.methods.collectStone = function(symbol) {
|
||||
const record = this.getSymbolProgress(symbol);
|
||||
if (record && !record.isCollected) {
|
||||
record.isCollected = true;
|
||||
record.collectedAt = new Date();
|
||||
this.totalStones += 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 方法:添加成就
|
||||
pinyinProgressSchema.methods.addAchievement = function(code) {
|
||||
if (!this.achievements.some(a => a.code === code)) {
|
||||
this.achievements.push({
|
||||
code: code,
|
||||
obtainedAt: new Date()
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 方法:更新每日统计
|
||||
pinyinProgressSchema.methods.updateDailyStats = function(type, duration = 0) {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
let dailyStat = this.dailyStats.find(d => {
|
||||
const statDate = new Date(d.date);
|
||||
statDate.setHours(0, 0, 0, 0);
|
||||
return statDate.getTime() === today.getTime();
|
||||
});
|
||||
|
||||
if (!dailyStat) {
|
||||
dailyStat = {
|
||||
date: today,
|
||||
exploreCount: 0,
|
||||
gameCount: 0,
|
||||
duration: 0
|
||||
};
|
||||
this.dailyStats.push(dailyStat);
|
||||
}
|
||||
|
||||
if (type === 'explore') {
|
||||
dailyStat.exploreCount += 1;
|
||||
} else if (type === 'game') {
|
||||
dailyStat.gameCount += 1;
|
||||
}
|
||||
|
||||
if (duration > 0) {
|
||||
dailyStat.duration += duration;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('PinyinProgress', pinyinProgressSchema);
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 拼音探索模块模型导出
|
||||
*/
|
||||
module.exports = {
|
||||
PinyinContent: require('./PinyinContent'),
|
||||
PinyinProgress: require('./PinyinProgress'),
|
||||
PinyinAchievement: require('./PinyinAchievement'),
|
||||
PinyinGameRecord: require('./PinyinGameRecord')
|
||||
};
|
||||
Reference in New Issue
Block a user