refactor: 项目架构重构 - 目录标准化 + 清理冗余
- 目录重命名: backend/wdkj-server/ → server/, frontend/ai-dimension/ → client/ - 删除 20+ 冗余文件(WDKJ 旧脚本、Windows 脚本、过时文档、设计稿) - 更新 package.json 元数据(移除 wdkj 命名) - 完善三级 .gitignore(根 + server + client) - 重写 README.md 和 CHANGELOG.md - 工具脚本移至 scripts/
This commit is contained in:
Executable
+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
+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
+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
+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)
|
||||
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
+25
@@ -0,0 +1,25 @@
|
||||
const User = require('./User')
|
||||
const Order = require('./Order')
|
||||
const Knowledge = require('./Knowledge')
|
||||
const ShopItem = require('./ShopItem')
|
||||
const Admin = require('./Admin')
|
||||
const AdminLog = require('./AdminLog')
|
||||
const AIChatQuota = require('./AIChatQuota')
|
||||
const AIModel = require('./AIModel')
|
||||
const LoginLog = require('./LoginLog')
|
||||
const Feedback = require('./Feedback')
|
||||
const Trend = require('./Trend')
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Order,
|
||||
Knowledge,
|
||||
ShopItem,
|
||||
Admin,
|
||||
AdminLog,
|
||||
AIChatQuota,
|
||||
AIModel,
|
||||
LoginLog,
|
||||
Feedback,
|
||||
Trend
|
||||
}
|
||||
Reference in New Issue
Block a user