🎉 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:
Yuzhiran Dev
2026-07-10 13:26:04 +08:00
commit 24cd9cef53
107 changed files with 20665 additions and 0 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)
+66
View File
@@ -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)
+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)
+106
View File
@@ -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)
+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)
+79
View File
@@ -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)
+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)
+25
View File
@@ -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)
+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)
+38
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,9 @@
/**
* 拼音探索模块模型导出
*/
module.exports = {
PinyinContent: require('./PinyinContent'),
PinyinProgress: require('./PinyinProgress'),
PinyinAchievement: require('./PinyinAchievement'),
PinyinGameRecord: require('./PinyinGameRecord')
};