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

- 目录重命名: backend/wdkj-server/ → server/, frontend/ai-dimension/ → client/
- 删除 20+ 冗余文件(WDKJ 旧脚本、Windows 脚本、过时文档、设计稿)
- 更新 package.json 元数据(移除 wdkj 命名)
- 完善三级 .gitignore(根 + server + client)
- 重写 README.md 和 CHANGELOG.md
- 工具脚本移至 scripts/
This commit is contained in:
Yuzhiran Dev
2026-07-11 12:45:37 +08:00
parent 66ac576082
commit c1d6dd3b29
106 changed files with 17436 additions and 2311 deletions
+6
View File
@@ -0,0 +1,6 @@
node_modules/
.env
logs/
certs/
*.log
.DS_Store
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
testEnvironment: 'node',
testMatch: ['**/tests/**/*.test.js'],
collectCoverageFrom: [
'src/**/*.js',
'!src/index.js'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
testTimeout: 30000,
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
verbose: true
}
Generated Executable
+5763
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
{
"name": "ai-dimension-server",
"version": "1.1.0",
"description": "宇之然AI维度 - 后端API服务",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "jest"
},
"keywords": ["wechat", "miniprogram", "ai", "education"],
"author": "宇之然团队",
"license": "MIT",
"dependencies": {
"axios": "^1.6.2",
"bcryptjs": "^2.4.3",
"compression": "^1.7.4",
"cors": "^2.8.5",
"crypto-js": "^4.2.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-validator": "^7.0.1",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"moment": "^2.29.4",
"mongoose": "^8.0.3",
"morgan": "^1.10.0",
"multer": "^2.1.1",
"xml2js": "^0.6.2"
},
"devDependencies": {
"jest": "^29.7.0",
"nodemon": "^3.0.2",
"supertest": "^7.2.2"
},
"engines": {
"node": ">=22.0.0"
}
}
+42
View File
@@ -0,0 +1,42 @@
const mongoose = require('mongoose')
/**
* 数据库连接配置
*/
const connectDB = async () => {
try {
const mongoURI = process.env.MONGODB_URI ||
`mongodb://${process.env.MONGODB_USER}:${process.env.MONGODB_PASSWORD}@${process.env.MONGODB_HOST}:${process.env.MONGODB_PORT}/${process.env.MONGODB_DB}?authSource=${process.env.MONGODB_DB}`
const conn = await mongoose.connect(mongoURI, {
// Mongoose 6+ 不再需要这些选项,但保留以兼容旧版本
// useNewUrlParser: true,
// useUnifiedTopology: true,
})
console.log(`✅ MongoDB 连接成功: ${conn.connection.host}:${conn.connection.port}/${conn.connection.name}`)
// 连接事件监听
mongoose.connection.on('error', (err) => {
console.error('❌ MongoDB 连接错误:', err)
})
mongoose.connection.on('disconnected', () => {
console.warn('⚠️ MongoDB 连接断开')
})
// 优雅关闭
process.on('SIGINT', async () => {
await mongoose.connection.close()
console.log('🔌 MongoDB 连接已关闭')
process.exit(0)
})
return conn
} catch (error) {
console.error('❌ MongoDB 连接失败:', error.message)
process.exit(1)
}
}
module.exports = connectDB
+97
View File
@@ -0,0 +1,97 @@
require('dotenv').config()
const express = require('express')
const cors = require('cors')
const helmet = require('helmet')
const compression = require('compression')
const morgan = require('morgan')
const rateLimit = require('express-rate-limit')
const connectDB = require('./config/database')
const initRoutes = require('./routes')
const logger = require('./utils/logger')
// 创建 Express 应用
const app = express()
// 数据库连接
connectDB()
// 中间件配置
app.use(helmet()) // 安全头部
app.use(cors()) // 跨域支持
app.use(compression()) // Gzip 压缩
app.use(express.json({ limit: '10mb' })) // JSON 解析
app.use(express.urlencoded({ extended: true, limit: '10mb' })) // URL 编码解析
// 日志中间件
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'))
} else {
app.use(morgan('combined'))
}
// 速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 1000, // 每个 IP 最多 1000 次请求
message: {
success: false,
error: '请求过于频繁,请稍后再试'
}
})
app.use('/api/', limiter)
// 健康检查
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
})
})
// API 路由
initRoutes(app)
// 404 处理
app.use((req, res) => {
res.status(404).json({
success: false,
error: '接口不存在',
path: req.path
})
})
// 全局错误处理
app.use((err, req, res, next) => {
logger.error('服务器错误:', err)
res.status(err.status || 500).json({
success: false,
error: process.env.NODE_ENV === 'production'
? '服务器内部错误'
: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
})
})
// 启动服务器
const PORT = process.env.PORT || 3000
const server = app.listen(PORT, () => {
logger.info(`🚀 服务器启动成功`)
logger.info(`📍 端口: ${PORT}`)
logger.info(`🌐 环境: ${process.env.NODE_ENV || 'development'}`)
logger.info(`📊 健康检查: http://localhost:${PORT}/health`)
})
// 优雅关闭
process.on('SIGTERM', () => {
logger.info('📴 接收到 SIGTERM 信号,正在关闭服务器...')
server.close(() => {
logger.info('✅ 服务器已关闭')
process.exit(0)
})
})
module.exports = app
+167
View File
@@ -0,0 +1,167 @@
const jwt = require('jsonwebtoken')
const { Admin, User } = require('../models')
/**
* JWT 认证中间件(管理员)
*/
const authMiddleware = async (req, res, next) => {
try {
// 从 header 获取 token
const authHeader = req.header('Authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
error: '未提供认证令牌'
})
}
const token = authHeader.replace('Bearer ', '')
// 验证 token
const decoded = jwt.verify(token, process.env.JWT_SECRET)
// 查找管理员
const admin = await Admin.findById(decoded.adminId)
if (!admin) {
return res.status(401).json({
success: false,
error: '管理员不存在'
})
}
// 检查状态
if (admin.status !== 'active') {
return res.status(403).json({
success: false,
error: '管理员账户已禁用'
})
}
// 将管理员信息附加到请求对象
req.admin = admin
next()
} catch (error) {
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
error: '无效的认证令牌'
})
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
error: '认证令牌已过期'
})
}
next(error)
}
}
/**
* 权限检查中间件
* @param {string} permission - 需要的权限
*/
const requirePermission = (permission) => {
return (req, res, next) => {
if (!req.admin) {
return res.status(401).json({
success: false,
error: '未认证'
})
}
if (!req.admin.hasPermission(permission)) {
return res.status(403).json({
success: false,
error: '权限不足'
})
}
next()
}
}
/**
* 小程序用户 JWT 认证中间件
*/
const userAuthMiddleware = async (req, res, next) => {
try {
const authHeader = req.header('Authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
error: '未提供认证令牌'
})
}
const token = authHeader.replace('Bearer ', '')
// 添加调试日志
console.log('[Auth] Token received:', token.substring(0, 20) + '...')
const decoded = jwt.verify(token, process.env.JWT_SECRET)
console.log('[Auth] Token decoded:', decoded)
// 兼容两种token格式:包含userId或openid
let user = null
if (decoded.userId) {
console.log('[Auth] Looking up user by userId:', decoded.userId)
user = await User.findById(decoded.userId)
} else if (decoded.openid) {
console.log('[Auth] Looking up user by openid:', decoded.openid)
user = await User.findOne({ openid: decoded.openid })
}
if (!user) {
console.log('[Auth] User not found')
return res.status(401).json({
success: false,
error: '用户不存在或令牌无效'
})
}
console.log('[Auth] User found:', user._id)
console.log('[Auth] User openid:', user.openid)
req.user = user
// 优先使用 openid,如果没有则使用 userId 作为标识
req.openid = user.openid || user._id.toString()
next()
} catch (error) {
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
error: '认证令牌无效或已过期'
})
}
next(error)
}
}
/**
* 可选的用户认证中间件(不强制要求登录)
*/
const optionalAuthMiddleware = async (req, res, next) => {
try {
const authHeader = req.header('Authorization')
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.replace('Bearer ', '')
const decoded = jwt.verify(token, process.env.JWT_SECRET)
const user = await User.findById(decoded.userId)
if (user) {
req.user = user
req.openid = user.openid
}
}
next()
} catch (error) {
// 即使认证失败也继续,只是不附加用户信息
next()
}
}
module.exports = {
authMiddleware,
requirePermission,
userAuthMiddleware,
optionalAuthMiddleware
}
+113
View File
@@ -0,0 +1,113 @@
const mongoose = require('mongoose')
/**
* 用户AI问答次数记录模型
*/
const AIChatQuotaSchema = new mongoose.Schema({
openid: {
type: String,
required: true,
unique: true,
index: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
index: true
},
// 剩余免费问答次数
freeQuota: {
type: Number,
default: 5
},
// 通过分享获得的次数
sharedQuota: {
type: Number,
default: 0
},
// 已使用的总次数
usedQuota: {
type: Number,
default: 0
},
// 今日已使用次数
dailyUsed: {
type: Number,
default: 0
},
// 最后使用日期
lastUsedDate: {
type: Date,
default: null
},
// 分享获得次数的记录
shareRecords: [{
sharedAt: { type: Date, default: Date.now },
gainedQuota: { type: Number, default: 5 }
}],
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
})
// 更新时自动更新 updatedAt
AIChatQuotaSchema.pre('save', function(next) {
this.updatedAt = Date.now()
next()
})
// 获取总可用次数
AIChatQuotaSchema.methods.getTotalQuota = function() {
return this.freeQuota + this.sharedQuota
}
// 获取剩余可用次数
AIChatQuotaSchema.methods.getRemainingQuota = function() {
return this.getTotalQuota() - this.usedQuota
}
// 检查是否有可用次数
AIChatQuotaSchema.methods.hasQuota = function() {
return this.getRemainingQuota() > 0
}
// 使用一次问答机会
AIChatQuotaSchema.methods.useQuota = async function() {
if (!this.hasQuota()) {
return false
}
this.usedQuota += 1
this.dailyUsed += 1
this.lastUsedDate = new Date()
await this.save()
return true
}
// 通过分享增加次数
AIChatQuotaSchema.methods.addQuotaByShare = async function(gainedQuota = 5) {
this.sharedQuota += gainedQuota
this.shareRecords.push({
sharedAt: new Date(),
gainedQuota
})
await this.save()
return this.getRemainingQuota()
}
// 重置每日使用次数(可定时任务调用)
AIChatQuotaSchema.methods.resetDailyUsed = function() {
const today = new Date().toDateString()
const lastUsed = this.lastUsedDate ? new Date(this.lastUsedDate).toDateString() : null
if (lastUsed !== today) {
this.dailyUsed = 0
}
}
module.exports = mongoose.model('AIChatQuota', AIChatQuotaSchema)
+117
View File
@@ -0,0 +1,117 @@
const mongoose = require('mongoose')
/**
* AI模型配置模型
*/
const AIModelSchema = new mongoose.Schema({
// 模型名称
name: {
type: String,
required: true,
trim: true
},
// 模型ID
modelId: {
type: String,
required: true,
unique: true,
trim: true
},
// API地址
apiUrl: {
type: String,
required: true,
trim: true
},
// API Key
apiKey: {
type: String,
required: true
},
// 模型描述
description: {
type: String,
default: ''
},
// 是否启用
isActive: {
type: Boolean,
default: true
},
// 是否为默认模型
isDefault: {
type: Boolean,
default: false
},
// 模型参数配置
config: {
temperature: {
type: Number,
default: 0.7,
min: 0,
max: 2
},
maxTokens: {
type: Number,
default: 800,
min: 1,
max: 4096
},
topP: {
type: Number,
default: 1,
min: 0,
max: 1
}
},
// 优先级(数字越小优先级越高)
priority: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
})
// 更新时自动更新 updatedAt
AIModelSchema.pre('save', function(next) {
this.updatedAt = Date.now()
next()
})
// 设置默认模型时,取消其他模型的默认状态
AIModelSchema.pre('save', async function(next) {
if (this.isDefault && this.isModified('isDefault')) {
await this.constructor.updateMany(
{ _id: { $ne: this._id } },
{ isDefault: false }
)
}
next()
})
// 获取当前使用的模型(优先返回默认模型,如果没有则返回第一个启用的模型)
AIModelSchema.statics.getCurrentModel = async function() {
// 先查找默认模型
let model = await this.findOne({ isDefault: true, isActive: true })
// 如果没有默认模型,返回第一个启用的模型
if (!model) {
model = await this.findOne({ isActive: true }).sort({ priority: 1, createdAt: -1 })
}
return model
}
// 获取所有启用的模型列表
AIModelSchema.statics.getActiveModels = async function() {
return await this.find({ isActive: true }).sort({ priority: 1, createdAt: -1 })
}
module.exports = mongoose.model('AIModel', AIModelSchema)
+130
View File
@@ -0,0 +1,130 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
const bcrypt = require('bcryptjs')
/**
* 管理员模型
*/
const AdminSchema = new Schema({
// 登录信息
username: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
minlength: 3,
maxlength: 30
},
password: {
type: String,
required: true,
minlength: 6
},
email: {
type: String,
trim: true,
lowercase: true
},
// 基本信息
realName: String,
phone: String,
avatar: String,
// 角色
role: {
type: String,
enum: ['super_admin', 'content_manager', 'shop_manager', 'viewer'],
default: 'viewer'
},
// 权限
permissions: [{
type: String
}],
// 状态
status: {
type: String,
enum: ['active', 'inactive', 'suspended'],
default: 'active'
},
// 登录信息
lastLogin: Date,
lastLoginIp: String,
loginCount: {
type: Number,
default: 0
},
// 时间戳
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
})
// 索引(username 已通过 unique: true 自动创建索引)
AdminSchema.index({ role: 1 })
// 中间件:保存前加密密码
AdminSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next()
}
try {
const salt = await bcrypt.genSalt(10)
this.password = await bcrypt.hash(this.password, salt)
next()
} catch (error) {
next(error)
}
})
// 实例方法:验证密码
AdminSchema.methods.validatePassword = async function(password) {
return bcrypt.compare(password, this.password)
}
// 实例方法:检查权限
AdminSchema.methods.hasPermission = function(permission) {
if (this.permissions.includes('*')) return true
return this.permissions.includes(permission)
}
// 静态方法:根据角色获取权限
AdminSchema.statics.getPermissionsByRole = function(role) {
const permissionMap = {
super_admin: ['*'],
content_manager: [
'knowledge:read', 'knowledge:write', 'knowledge:delete',
'gallery:read', 'gallery:approve', 'gallery:delete',
'users:read'
],
shop_manager: [
'shop:read', 'shop:write', 'shop:delete',
'orders:read', 'orders:process', 'orders:refund',
'users:read'
],
viewer: [
'dashboard:read',
'analytics:read',
'users:read'
]
}
return permissionMap[role] || []
}
module.exports = mongoose.model('Admin', AdminSchema)
+54
View File
@@ -0,0 +1,54 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 管理员操作日志模型
*/
const AdminLogSchema = new Schema({
// 管理员信息
adminId: {
type: Schema.Types.ObjectId,
ref: 'Admin',
required: true,
index: true
},
adminName: {
type: String,
required: true
},
// 操作信息
action: {
type: String,
required: true,
enum: ['login', 'create', 'read', 'update', 'delete', 'review', 'export']
},
resource: {
type: String,
required: true
},
resourceId: String,
// 详细信息
details: Schema.Types.Mixed,
// IP 地址
ip: String,
userAgent: String,
// 时间戳
timestamp: {
type: Date,
default: Date.now,
index: true
}
}, {
timestamps: false
})
// 索引
AdminLogSchema.index({ adminId: 1, timestamp: -1 })
AdminLogSchema.index({ resource: 1, timestamp: -1 })
AdminLogSchema.index({ action: 1 })
module.exports = mongoose.model('AdminLog', AdminLogSchema)
+191
View File
@@ -0,0 +1,191 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 反馈模型
* 存储用户的建议、问题和反馈
*/
const FeedbackSchema = new Schema({
// 反馈者信息
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
index: true
},
openid: {
type: String,
index: true
},
userName: {
type: String,
default: ''
},
userContact: {
type: String,
default: ''
},
// 反馈类型
type: {
type: String,
enum: ['suggestion', 'bug', 'complaint', 'praise', 'other'],
required: true,
index: true
},
// 反馈标题
title: {
type: String,
required: true,
maxlength: 100
},
// 反馈内容
content: {
type: String,
required: true,
maxlength: 2000
},
// 图片附件
images: [{
type: String
}],
// 相关页面/功能
relatedPage: {
type: String,
default: ''
},
// 设备信息
deviceInfo: {
device: { type: String, default: '' },
os: { type: String, default: '' },
osVersion: { type: String, default: '' },
browser: { type: String, default: '' },
browserVersion: { type: String, default: '' },
screenResolution: { type: String, default: '' },
appVersion: { type: String, default: '' }
},
// 网络信息
networkInfo: {
type: { type: String, default: '' },
ip: { type: String, default: '' }
},
// 处理状态
status: {
type: String,
enum: ['pending', 'processing', 'resolved', 'rejected', 'closed'],
default: 'pending',
index: true
},
// 优先级
priority: {
type: String,
enum: ['low', 'normal', 'high', 'urgent'],
default: 'normal'
},
// 处理记录
processLog: [{
operator: { type: String, required: true },
action: { type: String, required: true },
comment: { type: String, default: '' },
createdAt: { type: Date, default: Date.now }
}],
// 处理结果
result: {
type: String,
default: ''
},
// 处理人
handler: {
type: String,
default: ''
},
// 处理时间
handledAt: {
type: Date
},
// 用户评分(对处理结果的满意度)
rating: {
type: Number,
min: 1,
max: 5
},
// 用户备注
userRemark: {
type: String,
default: ''
},
// 创建时间
createdAt: {
type: Date,
default: Date.now,
index: true
},
// 更新时间
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: { createdAt: false, updatedAt: true }
})
// 索引
FeedbackSchema.index({ status: 1, createdAt: -1 })
FeedbackSchema.index({ type: 1, createdAt: -1 })
FeedbackSchema.index({ priority: 1, createdAt: -1 })
// 静态方法:获取待处理的反馈数量
FeedbackSchema.statics.getPendingCount = function() {
return this.countDocuments({ status: 'pending' })
}
// 静态方法:获取反馈统计
FeedbackSchema.statics.getStats = function(startDate, endDate) {
return this.aggregate([
{
$match: {
createdAt: { $gte: startDate, $lte: endDate }
}
},
{
$group: {
_id: '$status',
count: { $sum: 1 }
}
}
])
}
// 静态方法:按类型统计
FeedbackSchema.statics.getTypeStats = function(startDate, endDate) {
return this.aggregate([
{
$match: {
createdAt: { $gte: startDate, $lte: endDate }
}
},
{
$group: {
_id: '$type',
count: { $sum: 1 }
}
}
])
}
module.exports = mongoose.model('Feedback', FeedbackSchema)
+125
View File
@@ -0,0 +1,125 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 知识库模型
*/
const KnowledgeSchema = new Schema({
// 基本信息
title: {
type: String,
required: true,
trim: true,
maxlength: 100
},
content: {
type: String,
required: true
},
// 分类
dim: {
type: Number,
enum: [1, 2, 3, 4, 5],
required: true,
index: true
},
category: {
type: String,
enum: ['concept', 'history', 'application', 'question'],
required: true
},
tags: [{
type: String,
maxlength: 20
}],
// 多层级内容
sections: [{
title: String,
content: String,
images: [String]
}],
// 问答系统
question: String,
answer: String,
options: [{
text: String,
isCorrect: Boolean
}],
// 统计
viewCount: {
type: Number,
default: 0
},
collectionCount: {
type: Number,
default: 0
},
// 付费与订阅
isPremium: {
type: Boolean,
default: false
},
requiredPoints: {
type: Number,
default: 0
},
price: {
type: Number,
default: 0
},
currency: {
type: String,
default: 'CNY'
},
// 状态
status: {
type: String,
enum: ['draft', 'pending', 'approved', 'rejected'],
default: 'draft',
index: true
},
// 审核信息
reviewComment: String,
reviewedAt: Date,
reviewerId: String,
// 排序
sortOrder: {
type: Number,
default: 0
},
// 时间戳
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
})
// 索引
KnowledgeSchema.index({ dim: 1, sortOrder: 1 })
KnowledgeSchema.index({ status: 1, createdAt: -1 })
KnowledgeSchema.index({ category: 1 })
// 中间件:保存前更新 updatedAt
KnowledgeSchema.pre('save', function(next) {
this.updatedAt = Date.now()
next()
})
module.exports = mongoose.model('Knowledge', KnowledgeSchema)
+142
View File
@@ -0,0 +1,142 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 登录日志模型
* 记录用户的登录/注册行为
*/
const LoginLogSchema = new Schema({
// 用户标识
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
openid: {
type: String,
required: true,
index: true
},
// 登录类型
type: {
type: String,
enum: ['register', 'login', 'auto_login'],
required: true
},
// 登录方式
method: {
type: String,
enum: ['wechat_miniapp', 'wechat_webview', 'wechat_oauth', 'username', 'other'],
required: true
},
// IP地址
ip: {
type: String,
default: ''
},
// 终端信息
userAgent: {
type: String,
default: ''
},
// 设备信息
device: {
type: String,
default: ''
},
// 操作系统
os: {
type: String,
default: ''
},
// 浏览器
browser: {
type: String,
default: ''
},
// 登录来源
source: {
type: String,
enum: ['miniapp', 'webview', 'h5', 'admin', 'other'],
default: 'other'
},
// 登录结果
success: {
type: Boolean,
default: true
},
// 失败原因
failReason: {
type: String,
default: ''
},
// 地理位置(可选)
location: {
country: { type: String, default: '' },
province: { type: String, default: '' },
city: { type: String, default: '' }
},
// 登录时间
createdAt: {
type: Date,
default: Date.now,
index: true
}
}, {
timestamps: false
})
// 索引
LoginLogSchema.index({ userId: 1, createdAt: -1 })
LoginLogSchema.index({ openid: 1, createdAt: -1 })
LoginLogSchema.index({ type: 1, createdAt: -1 })
LoginLogSchema.index({ method: 1, createdAt: -1 })
// 静态方法:获取用户的登录历史
LoginLogSchema.statics.getUserLoginHistory = function(userId, limit = 10) {
return this.find({ userId })
.sort({ createdAt: -1 })
.limit(limit)
.lean()
}
// 静态方法:获取最近的登录日志
LoginLogSchema.statics.getRecentLogs = function(limit = 50) {
return this.find()
.sort({ createdAt: -1 })
.limit(limit)
.populate('userId', 'nickName avatarUrl')
.lean()
}
// 静态方法:统计登录数据
LoginLogSchema.statics.getLoginStats = function(startDate, endDate) {
return this.aggregate([
{
$match: {
createdAt: { $gte: startDate, $lte: endDate }
}
},
{
$group: {
_id: '$type',
count: { $sum: 1 }
}
}
])
}
module.exports = mongoose.model('LoginLog', LoginLogSchema)
+120
View File
@@ -0,0 +1,120 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 订单模型
*/
const OrderSchema = new Schema({
// 订单基本信息
orderNo: {
type: String,
required: true,
unique: true,
index: true
},
// 用户信息
openid: {
type: String,
required: true,
index: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
index: true
},
// 商品信息
productId: {
type: String,
required: true
},
productName: {
type: String,
required: true
},
amount: {
type: Number,
required: true
},
// 支付信息
status: {
type: String,
enum: ['pending', 'paid', 'completed', 'failed', 'refunded'],
default: 'pending',
index: true
},
prepayId: String,
transactionId: String,
// 微信支付参数(用于调试)
paymentParams: Schema.Types.Mixed,
rawResult: Schema.Types.Mixed,
// 审核信息(管理后台)
reviewComment: String,
reviewedAt: Date,
reviewerId: String,
// 时间戳
createdAt: {
type: Date,
default: Date.now,
index: true
},
paidAt: Date,
completedAt: Date,
refundedAt: Date
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
})
// 索引
OrderSchema.index({ openid: 1, createdAt: -1 })
OrderSchema.index({ status: 1, createdAt: -1 })
OrderSchema.index({ userId: 1, createdAt: -1 }) // 优化用户订单查询
// 静态方法:获取今日订单数
OrderSchema.statics.getTodayCount = async function() {
const today = new Date()
today.setHours(0, 0, 0, 0)
return this.countDocuments({
createdAt: { $gte: today },
status: 'paid'
})
}
// 静态方法:获取订单趋势(7天)
OrderSchema.statics.getWeekTrend = async function() {
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
weekAgo.setHours(0, 0, 0, 0)
return this.aggregate([
{
$match: {
createdAt: { $gte: weekAgo },
status: 'paid'
}
},
{
$group: {
_id: {
$dateToString: { format: '%Y-%m-%d', date: '$createdAt' }
},
count: { $sum: 1 },
revenue: { $sum: '$amount' }
}
},
{
$sort: { '_id': 1 }
}
])
}
module.exports = mongoose.model('Order', OrderSchema)
+109
View File
@@ -0,0 +1,109 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
/**
* 商品模型
*/
const ShopItemSchema = new Schema({
// 基本信息
itemId: {
type: String,
required: true,
unique: true,
index: true
},
name: {
type: String,
required: true,
trim: true
},
description: {
type: String,
trim: true
},
// 价格(单位:分)
price: {
type: Number,
required: true,
min: 1
},
originalPrice: {
type: Number
},
// 商品类型
type: {
type: String,
enum: ['skin', 'noad', 'subscription'],
required: true
},
// 订阅天数(仅订阅商品)
duration: {
type: Number,
default: 30
},
// 图片
imageUrl: String,
previewUrl: String,
// 统计
purchaseCount: {
type: Number,
default: 0
},
// 状态
status: {
type: String,
enum: ['active', 'inactive', 'discontinued'],
default: 'active',
index: true
},
// 排序
sortOrder: {
type: Number,
default: 0
},
// 促销
isOnSale: {
type: Boolean,
default: false
},
saleEndTime: Date,
// 时间戳
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
})
// 索引
ShopItemSchema.index({ status: 1, sortOrder: 1 })
ShopItemSchema.index({ type: 1 })
// 虚拟字段:是否在促销中
ShopItemSchema.virtual('isOnSaleActive').get(function() {
return this.isOnSale && this.saleEndTime && new Date() < this.saleEndTime
})
// 中间件:保存前更新 updatedAt
ShopItemSchema.pre('save', function(next) {
this.updatedAt = Date.now()
next()
})
module.exports = mongoose.model('ShopItem', ShopItemSchema)
+33
View File
@@ -0,0 +1,33 @@
const mongoose = require('mongoose');
const { Schema } = mongoose;
const TrendSchema = new Schema({
title: { type: String, required: true, trim: true },
summary: { type: String },
source: { type: String, default: '未知来源' },
sourceUrl: { type: String },
category: {
type: String,
enum: ['产品', '论文', '工具', '公司', '政策', '其他'],
default: '其他'
},
tags: [String],
hot: { type: Boolean, default: false },
picked: { type: Boolean, default: false },
newsDate: { type: Date, default: Date.now, index: true },
viewCount: { type: Number, default: 0 },
status: {
type: String,
enum: ['draft', 'published'],
default: 'published'
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
TrendSchema.index({ status: 1, newsDate: -1 });
TrendSchema.index({ category: 1, newsDate: -1 });
module.exports = mongoose.model('Trend', TrendSchema);
+305
View File
@@ -0,0 +1,305 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
const bcrypt = require('bcryptjs')
/**
* 用户探索数据结构
*/
const ExploreDataSchema = new Schema({
dim1: {
completed: { type: Boolean, default: false },
bestScore: { type: Number, default: 0 },
collectedEggs: { type: Number, default: 0 },
totalLength: { type: Number, default: 0 }
},
dim2: {
completed: { type: Boolean, default: false },
bestArea: { type: Number, default: 0 },
createdShapes: { type: Number, default: 0 }
},
dim3: {
completed: { type: Boolean, default: false },
exploredFaces: { type: Number, default: 0 },
rotationTime: { type: Number, default: 0 },
bestScore: { type: Number, default: 0 }
},
dim4: {
completed: { type: Boolean, default: false },
bestScore: { type: Number, default: 0 },
exploredEvents: { type: Number, default: 0 }
},
dim5: {
completed: { type: Boolean, default: false },
bestScore: { type: Number, default: 0 },
exploredThoughts: { type: Number, default: 0 }
}
}, { _id: false })
/**
* 用户模型
*/
const UserSchema = new Schema({
// 微信用户标识
openid: {
type: String,
required: false,
unique: true,
sparse: true,
index: true
},
unionid: {
type: String,
index: true
},
// H5环境用户名密码登录
username: {
type: String,
unique: true,
sparse: true,
index: true
},
password: {
type: String,
select: false
},
// 基本信息
nickName: {
type: String,
default: '星辰旅行者'
},
avatarUrl: {
type: String,
default: ''
},
// 探索数据
exploreData: {
type: ExploreDataSchema,
default: () => ({})
},
// 分数(冗余字段,便于排行榜查询)
totalScore: { type: Number, default: 0 },
dim1Score: { type: Number, default: 0 },
dim2Score: { type: Number, default: 0 },
dim3Score: { type: Number, default: 0 },
dim4Score: { type: Number, default: 0 },
dim5Score: { type: Number, default: 0 },
// 已解锁维度
unlockedDims: [{
type: Number,
enum: [1, 2, 3, 4, 5]
}],
// 商品相关
ownedSkins: [{
type: String
}],
purchasedItems: [{
type: String
}],
// 会员状态
isAdFree: {
type: Boolean,
default: false
},
isSubscriber: {
type: Boolean,
default: false
},
subscribeExpiry: {
type: Date
},
// 收藏与成就
collectedKnowledge: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Knowledge'
}],
achievements: [{
type: String
}],
// 设置
settings: {
voiceEnabled: { type: Boolean, default: true },
difficulty: {
type: String,
enum: ['easy', 'normal', 'hard'],
default: 'normal'
}
},
// 探索等级系统
level: {
type: Number,
default: 1
},
levelPoints: {
type: Number,
default: 0
},
// 时间戳
totalPlayTime: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
lastLoginAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
})
// 索引
UserSchema.index({ totalScore: -1 })
UserSchema.index({ dim1Score: -1 })
UserSchema.index({ dim2Score: -1 })
UserSchema.index({ dim3Score: -1 })
UserSchema.index({ dim4Score: -1 })
UserSchema.index({ dim5Score: -1 })
UserSchema.index({ createdAt: -1 })
UserSchema.index({ lastLoginAt: -1 }) // 用于活跃用户统计
// 虚拟字段:是否是新用户(24小时内注册)
UserSchema.virtual('isNew').get(function() {
return Date.now() - this.createdAt < 24 * 60 * 60 * 1000
})
// 中间件:保存前加密密码(仅针对username/password登录)
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next()
}
try {
const salt = await bcrypt.genSalt(10)
this.password = await bcrypt.hash(this.password, salt)
next()
} catch (error) {
next(error)
}
})
// 实例方法:验证密码
UserSchema.methods.validatePassword = async function(password) {
return bcrypt.compare(password, this.password)
}
// 实例方法:计算总分
UserSchema.methods.calculateTotalScore = function() {
this.totalScore =
(this.exploreData.dim1?.bestScore || 0) +
(this.exploreData.dim2?.bestArea || 0) +
(this.exploreData.dim3?.bestScore || 0) +
(this.exploreData.dim4?.bestScore || 0) +
(this.exploreData.dim5?.bestScore || 0)
// 同步维度分数
this.dim1Score = this.exploreData.dim1?.bestScore || 0
this.dim2Score = this.exploreData.dim2?.bestArea || 0
this.dim3Score = this.exploreData.dim3?.bestScore || 0
this.dim4Score = this.exploreData.dim4?.bestScore || 0
this.dim5Score = this.exploreData.dim5?.bestScore || 0
return this.totalScore
}
// 实例方法:计算等级
UserSchema.methods.calculateLevel = function() {
// 等级计算公式:基于总分和探索时长
// 基础分:每100分 = 1级
const scorePoints = Math.floor(this.totalScore / 100)
// 时长分:每1小时 = 1级
const timePoints = Math.floor(this.totalPlayTime / 60)
// 成就分:每个成就 = 2级
const achievementPoints = (this.achievements?.length || 0) * 2
// 收藏分:每5个收藏 = 1级
const collectionPoints = Math.floor((this.collectedKnowledge?.length || 0) / 5)
this.levelPoints = scorePoints + timePoints + achievementPoints + collectionPoints
// 等级阈值表
const levelThresholds = [
0, 5, 15, 30, 50, 75, 105, 140, 180, 225,
275, 330, 390, 455, 525, 600, 680, 765, 855, 950, 1050
]
// 计算等级
let newLevel = 1
for (let i = 0; i < levelThresholds.length; i++) {
if (this.levelPoints >= levelThresholds[i]) {
newLevel = i + 1
} else {
break
}
}
this.level = newLevel
return this.level
}
// 实例方法:获取等级信息
UserSchema.methods.getLevelInfo = function() {
const levelNames = [
'星辰旅者', '维度学徒', '空间探索者', '几何学者', '维度行者',
'时空旅人', '多维大师', '宇宙探索者', '维度掌控者', '空间主宰',
'维度领主', '宇宙行者', '时空主宰', '维度之神', '宇宙之主',
'维度创世者', '空间造物主', '宇宙掌控者', '维度至尊', '宇宙之神'
]
const levelThresholds = [
0, 5, 15, 30, 50, 75, 105, 140, 180, 225,
275, 330, 390, 455, 525, 600, 680, 765, 855, 950, 1050
]
const currentLevel = this.level || 1
const currentPoints = this.levelPoints || 0
const nextLevelPoints = levelThresholds[currentLevel] || levelThresholds[levelThresholds.length - 1]
const progress = Math.min(100, Math.floor((currentPoints / nextLevelPoints) * 100))
return {
level: currentLevel,
name: levelNames[Math.min(currentLevel - 1, levelNames.length - 1)],
points: currentPoints,
nextLevelPoints: nextLevelPoints,
progress: progress,
totalProgress: Math.min(100, Math.floor((currentPoints / 1050) * 100))
}
}
// 静态方法:获取排行榜
UserSchema.statics.getLeaderboard = function(dim = null, limit = 50) {
const sortField = dim ? `dim${dim}Score` : 'totalScore'
return this.find()
.select('openid nickName avatarUrl totalScore dim1Score dim2Score dim3Score dim4Score dim5Score unlockedDims')
.sort({ [sortField]: -1 })
.limit(limit)
.lean()
}
// 中间件:保存前更新 updatedAt、计算总分和等级
UserSchema.pre('save', function(next) {
this.updatedAt = Date.now()
this.calculateTotalScore()
this.calculateLevel()
next()
})
module.exports = mongoose.model('User', UserSchema)
+25
View File
@@ -0,0 +1,25 @@
const User = require('./User')
const Order = require('./Order')
const Knowledge = require('./Knowledge')
const ShopItem = require('./ShopItem')
const Admin = require('./Admin')
const AdminLog = require('./AdminLog')
const AIChatQuota = require('./AIChatQuota')
const AIModel = require('./AIModel')
const LoginLog = require('./LoginLog')
const Feedback = require('./Feedback')
const Trend = require('./Trend')
module.exports = {
User,
Order,
Knowledge,
ShopItem,
Admin,
AdminLog,
AIChatQuota,
AIModel,
LoginLog,
Feedback,
Trend
}
+1345
View File
File diff suppressed because it is too large Load Diff
+240
View File
@@ -0,0 +1,240 @@
const express = require('express')
const router = express.Router()
const { AIChatQuota, AIModel } = require('../models')
const ApiResponse = require('../utils/response')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取用户AI问答配额
* GET /api/ai-chat/quota
*/
router.get('/quota', userAuthMiddleware, async (req, res) => {
try {
const openid = req.openid || req.user?.openid
if (!openid) {
return ApiResponse.error(res, '无法获取用户标识', 400)
}
let quota = await AIChatQuota.findOne({ openid })
if (!quota) {
// 创建新的配额记录,默认5次免费
quota = new AIChatQuota({ openid })
await quota.save()
}
// 检查是否需要重置每日使用次数
quota.resetDailyUsed()
await quota.save()
return ApiResponse.success(res, {
freeQuota: quota.freeQuota,
sharedQuota: quota.sharedQuota,
usedQuota: quota.usedQuota,
remainingQuota: quota.getRemainingQuota(),
dailyUsed: quota.dailyUsed
})
} catch (error) {
logger.error('获取AI问答配额失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* AI问答接口
* POST /api/ai-chat/ask
*/
router.post('/ask', userAuthMiddleware, async (req, res) => {
try {
const openid = req.openid || req.user?.openid
const { question, dimension = 'general' } = req.body
if (!openid) {
return ApiResponse.error(res, '无法获取用户标识', 400)
}
if (!question || question.trim().length === 0) {
return ApiResponse.error(res, '请输入问题', 400)
}
// 检查用户配额
let quota = await AIChatQuota.findOne({ openid })
if (!quota) {
quota = new AIChatQuota({ openid })
await quota.save()
}
if (!quota.hasQuota()) {
return ApiResponse.error(res, '问答次数已用完,分享好友可获得更多次数', 403)
}
// 获取当前使用的AI模型配置
const aiModel = await AIModel.getCurrentModel()
let apiUrl, apiKey, modelId, modelConfig
if (aiModel) {
// 使用数据库配置的模型
apiUrl = aiModel.apiUrl
apiKey = aiModel.apiKey
modelId = aiModel.modelId
modelConfig = aiModel.config
} else {
// 使用环境变量配置的模型
apiUrl = process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions'
apiKey = process.env.OPENAI_API_KEY
modelId = process.env.OPENAI_MODEL || 'gpt-3.5-turbo'
modelConfig = { temperature: 0.7, maxTokens: 800, topP: 1 }
}
// 检查API配置
if (!apiKey) {
logger.error('AI API Key未配置')
return ApiResponse.error(res, 'AI服务暂时不可用', 500)
}
// 构建系统提示词
const systemPrompt = getSystemPromptByDimension(dimension)
// 调用AI API
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: modelId,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }
],
temperature: modelConfig.temperature || 0.7,
max_tokens: modelConfig.maxTokens || 800,
top_p: modelConfig.topP || 1
})
})
if (!response.ok) {
const errorData = await response.json()
logger.error('OpenAI API错误:', errorData)
return ApiResponse.error(res, 'AI服务响应异常', 500)
}
const data = await response.json()
const answer = data.choices[0].message.content
// 扣除一次使用次数
await quota.useQuota()
return ApiResponse.success(res, {
answer,
remainingQuota: quota.getRemainingQuota()
})
} catch (error) {
logger.error('AI问答失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 分享获得问答次数
* POST /api/ai-chat/share-gain
*/
router.post('/share-gain', userAuthMiddleware, async (req, res) => {
try {
const openid = req.openid || req.user?.openid
if (!openid) {
return ApiResponse.error(res, '无法获取用户标识', 400)
}
let quota = await AIChatQuota.findOne({ openid })
if (!quota) {
quota = new AIChatQuota({ openid })
}
// 增加5次分享获得的次数
const remainingQuota = await quota.addQuotaByShare(5)
return ApiResponse.success(res, {
message: '分享成功,获得5次问答机会',
gainedQuota: 5,
remainingQuota
})
} catch (error) {
logger.error('分享获得次数失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 根据维度获取系统提示词
*/
function getSystemPromptByDimension(dimension) {
const prompts = {
dim1: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 起源和历史的问题。
请用通俗易懂的语言解释人工智能发展史上的重要事件、人物和里程碑,如:
- 图灵测试(1950)与艾伦·图灵的贡献
- 达特茅斯会议(1956)——AI 的诞生
- 深蓝 vs 卡斯帕罗夫(1997)
- AlphaGo 与深度学习革命(2016
- GPT 系列与大模型时代的开启
回答要简洁有趣,适合普通用户理解。`,
dim2: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 技术发展的问题。
请用通俗易懂的语言解释人工智能核心技术演进,如:
- 机器学习与深度学习的基本原理
- Transformer 架构与自注意力机制
- 大规模预训练模型(GPT、BERT等)
- 多模态 AI、MoE 架构
- Agent 智能体技术
回答要简洁有趣,适合普通用户理解。`,
dim3: `你是"宇之然AI维度"的AI助手,专门解答关于当前 AI 行业格局的问题。
请用通俗易懂的语言解释当前 AI 领域的最新动态,如:
- 各大模型公司(OpenAI、Google、DeepSeek、Anthropic、Meta)的竞争格局
- 开源 vs 闭源模型的生态对比
- 具身智能与世界模型的最新进展
- AI 应用在各行业的落地情况
- Agent 智能体的发展现状
回答要简洁有趣,适合普通用户理解。`,
dim4: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 学习和工具使用的问题。
请用通俗易懂的语言解释 AI 学习路线和工具使用方法,如:
- 提示词工程(Prompt Engineering)入门
- 检索增强生成(RAG)的原理与实践
- 模型微调(Fine-tuning)基础
- 主流 AI 开发框架(LangChain、Dify 等)
- 常用 AI 工具推荐与使用技巧
回答要简洁有趣,适合普通用户理解。`,
dim5: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 趋势和前沿动态的问题。
请用通俗易懂的语言介绍 AI 领域的最新趋势,如:
- 最新 AI 产品发布动态
- 前沿论文与研究方向
- 行业趋势分析与预测
- AI 政策法规与伦理讨论
回答要简洁有趣,适合普通用户理解。`,
general: `你是"宇之然AI维度"的AI助手,致力于帮助用户系统化理解人工智能。
你可以回答关于 AI 的任何问题,包括但不限于:
- AI 历史与起源
- 核心技术原理
- 当前行业格局
- 学习路线与工具使用
- 最新趋势与前沿动态
你的回答要简洁、通俗易懂,适合普通用户理解。如果用户问的是某个具体维度的知识,可以引导用户去对应的维度模块深入学习。`
}
return prompts[dimension] || prompts.general
}
module.exports = router
+351
View File
@@ -0,0 +1,351 @@
const express = require('express')
const router = express.Router()
const { AIModel } = require('../models')
const { authMiddleware, requirePermission } = require('../middleware/auth')
const ApiResponse = require('../utils/response')
const logger = require('../utils/logger')
// 所有路由都需要认证
router.use(authMiddleware)
/**
* 获取当前使用的模型(公开接口,用于AI问答)
* GET /api/ai-model/current
*/
router.get('/current', async (req, res) => {
try {
const model = await AIModel.getCurrentModel()
if (!model) {
// 如果没有配置模型,返回环境变量中的默认配置
return ApiResponse.success(res, {
name: '默认模型',
modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions',
apiKey: process.env.OPENAI_API_KEY || '',
config: {
temperature: 0.7,
maxTokens: 800,
topP: 1
}
})
}
// 返回模型信息(不返回API Key)
return ApiResponse.success(res, {
name: model.name,
modelId: model.modelId,
config: model.config
})
} catch (error) {
logger.error('获取当前模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取模型列表(管理后台)
* GET /api/ai-model/list
*/
router.get('/list', requirePermission('dashboard:read'), async (req, res) => {
try {
const { page = 1, pageSize = 10, keyword = '' } = req.query
const query = keyword
? { $or: [
{ name: { $regex: keyword, $options: 'i' } },
{ modelId: { $regex: keyword, $options: 'i' } }
]}
: {}
const [list, total] = await Promise.all([
AIModel.find(query)
.sort({ isDefault: -1, priority: 1, createdAt: -1 })
.skip((page - 1) * pageSize)
.limit(parseInt(pageSize)),
AIModel.countDocuments(query)
])
return ApiResponse.success(res, {
list,
total,
page: parseInt(page),
pageSize: parseInt(pageSize)
})
} catch (error) {
logger.error('获取模型列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取所有启用的模型(下拉选择用)
* GET /api/ai-model/active
*/
router.get('/active', requirePermission('dashboard:read'), async (req, res) => {
try {
const models = await AIModel.getActiveModels()
return ApiResponse.success(res, {
list: models.map(m => ({
_id: m._id,
name: m.name,
modelId: m.modelId,
isDefault: m.isDefault
}))
})
} catch (error) {
logger.error('获取启用模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取模型详情
* GET /api/ai-model/:id
*/
router.get('/:id', requirePermission('dashboard:read'), async (req, res) => {
try {
const { id } = req.params
const model = await AIModel.findById(id)
if (!model) {
return ApiResponse.notFound(res, '模型不存在')
}
return ApiResponse.success(res, { model })
} catch (error) {
logger.error('获取模型详情失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 创建模型
* POST /api/ai-model
*/
router.post('/', requirePermission('settings:write'), async (req, res) => {
try {
const { name, modelId, apiUrl, apiKey, description, config, priority } = req.body
// 验证必填字段
if (!name || !modelId || !apiUrl || !apiKey) {
return ApiResponse.error(res, '请填写所有必填字段', 400)
}
// 检查modelId是否已存在
const existingModel = await AIModel.findOne({ modelId })
if (existingModel) {
return ApiResponse.error(res, '该模型ID已存在', 400)
}
const model = new AIModel({
name,
modelId,
apiUrl,
apiKey,
description,
config,
priority
})
await model.save()
logger.info('创建AI模型:', { name, modelId, admin: req.admin.username })
return ApiResponse.success(res, {
message: '创建成功',
model: {
_id: model._id,
name: model.name,
modelId: model.modelId,
isDefault: model.isDefault,
isActive: model.isActive
}
})
} catch (error) {
logger.error('创建模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 更新模型
* PUT /api/ai-model/:id
*/
router.put('/:id', requirePermission('settings:write'), async (req, res) => {
try {
const { id } = req.params
const { name, modelId, apiUrl, apiKey, description, isActive, isDefault, config, priority } = req.body
const model = await AIModel.findById(id)
if (!model) {
return ApiResponse.notFound(res, '模型不存在')
}
// 如果修改了modelId,检查是否与其他模型冲突
if (modelId && modelId !== model.modelId) {
const existingModel = await AIModel.findOne({ modelId, _id: { $ne: id } })
if (existingModel) {
return ApiResponse.error(res, '该模型ID已存在', 400)
}
}
// 更新字段
if (name) model.name = name
if (modelId) model.modelId = modelId
if (apiUrl) model.apiUrl = apiUrl
if (apiKey) model.apiKey = apiKey
if (description !== undefined) model.description = description
if (isActive !== undefined) model.isActive = isActive
if (isDefault !== undefined) model.isDefault = isDefault
if (config) model.config = { ...model.config, ...config }
if (priority !== undefined) model.priority = priority
await model.save()
logger.info('更新AI模型:', { id, name: model.name, admin: req.admin.username })
return ApiResponse.success(res, {
message: '更新成功',
model: {
_id: model._id,
name: model.name,
modelId: model.modelId,
isDefault: model.isDefault,
isActive: model.isActive
}
})
} catch (error) {
logger.error('更新模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 删除模型
* DELETE /api/ai-model/:id
*/
router.delete('/:id', requirePermission('settings:write'), async (req, res) => {
try {
const { id } = req.params
const model = await AIModel.findById(id)
if (!model) {
return ApiResponse.notFound(res, '模型不存在')
}
// 检查是否是唯一的默认模型
if (model.isDefault) {
const defaultCount = await AIModel.countDocuments({ isDefault: true })
if (defaultCount <= 1) {
return ApiResponse.error(res, '不能删除唯一的默认模型,请先设置其他默认模型', 400)
}
}
await AIModel.findByIdAndDelete(id)
logger.info('删除AI模型:', { id, name: model.name, admin: req.admin.username })
return ApiResponse.success(res, { message: '删除成功' })
} catch (error) {
logger.error('删除模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 设置默认模型
* POST /api/ai-model/:id/set-default
*/
router.post('/:id/set-default', requirePermission('settings:write'), async (req, res) => {
try {
const { id } = req.params
const model = await AIModel.findById(id)
if (!model) {
return ApiResponse.notFound(res, '模型不存在')
}
if (!model.isActive) {
return ApiResponse.error(res, '不能将禁用模型设为默认', 400)
}
model.isDefault = true
await model.save()
logger.info('设置默认AI模型:', { id, name: model.name, admin: req.admin.username })
return ApiResponse.success(res, {
message: '设置默认模型成功',
model: {
_id: model._id,
name: model.name,
modelId: model.modelId,
isDefault: true
}
})
} catch (error) {
logger.error('设置默认模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 初始化默认模型(从环境变量)
* POST /api/ai-model/init-default
*/
router.post('/init-default', requirePermission('settings:write'), async (req, res) => {
try {
// 检查是否已有模型
const existingCount = await AIModel.countDocuments()
if (existingCount > 0) {
return ApiResponse.error(res, '已有模型配置,如需重新初始化请先删除现有模型', 400)
}
// 从环境变量创建默认模型
const defaultModel = new AIModel({
name: '默认模型',
modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions',
apiKey: process.env.OPENAI_API_KEY || '',
description: '从环境变量初始化的默认模型',
isDefault: true,
isActive: true,
priority: 0
})
await defaultModel.save()
logger.info('初始化默认AI模型:', { admin: req.admin.username })
return ApiResponse.success(res, {
message: '初始化默认模型成功',
model: {
_id: defaultModel._id,
name: defaultModel.name,
modelId: defaultModel.modelId,
isDefault: true
}
})
} catch (error) {
logger.error('初始化默认模型失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+492
View File
@@ -0,0 +1,492 @@
const express = require('express')
const router = express.Router()
const jwt = require('jsonwebtoken')
const { User, Admin, AdminLog, LoginLog } = require('../models')
const ApiResponse = require('../utils/response')
const weChatService = require('../utils/weixin')
const logger = require('../utils/logger')
const axios = require('axios')
/**
* 获取客户端IP
*/
const getClientIp = (req) => {
return req.headers['x-forwarded-for'] ||
req.headers['x-real-ip'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
'0.0.0.0'
}
/**
* 解析User-Agent
*/
const parseUserAgent = (userAgent) => {
if (!userAgent) return { device: '', os: '', browser: '' }
const device = /Mobile|Android|iPhone|iPad|iPod/i.test(userAgent) ? 'mobile' : 'desktop'
let os = ''
if (/Windows/i.test(userAgent)) os = 'Windows'
else if (/Mac OS X/i.test(userAgent)) os = 'MacOS'
else if (/Android/i.test(userAgent)) os = 'Android'
else if (/iOS|iPhone|iPad|iPod/i.test(userAgent)) os = 'iOS'
else if (/Linux/i.test(userAgent)) os = 'Linux'
else os = 'Unknown'
let browser = ''
if (/MicroMessenger/i.test(userAgent)) browser = 'WeChat'
else if (/Chrome/i.test(userAgent)) browser = 'Chrome'
else if (/Safari/i.test(userAgent)) browser = 'Safari'
else if (/Firefox/i.test(userAgent)) browser = 'Firefox'
else if (/Edge/i.test(userAgent)) browser = 'Edge'
else browser = 'Unknown'
return { device, os, browser }
}
/**
* 记录登录日志
*/
const recordLoginLog = async (userId, openid, type, method, source, req, success = true, failReason = '') => {
try {
const ip = getClientIp(req)
const userAgent = req.headers['user-agent'] || ''
const { device, os, browser } = parseUserAgent(userAgent)
await LoginLog.create({
userId,
openid,
type,
method,
source,
ip,
userAgent,
device,
os,
browser,
success,
failReason
})
} catch (error) {
logger.error('记录登录日志失败:', error)
}
}
/**
* 小程序登录
* POST /api/auth/login
*/
router.post('/login', async (req, res) => {
try {
const { code, userInfo, source = 'miniapp' } = req.body
// 方式1: 通过 code 登录(推荐)
let openid = null
if (code) {
const sessionData = await weChatService.code2Session(code)
openid = sessionData.openid
}
// 方式2: 开发环境直接使用 openid (已废弃,仅用于本地调试)
if (!openid && process.env.NODE_ENV === 'development' && req.body.openid) {
openid = req.body.openid
}
if (!openid) {
return ApiResponse.error(res, '无法获取用户标识', 400)
}
// 查找或创建用户
let user = await User.findOne({ openid })
let isNewUser = false
if (!user) {
// 新用户
user = new User({
openid,
nickName: userInfo?.nickName || '星辰旅行者',
avatarUrl: userInfo?.avatarUrl || '',
unlockedDims: [1]
})
isNewUser = true
logger.info(`新用户注册: ${openid}`)
} else {
// 老用户,更新登录时间
user.lastLoginAt = new Date()
if (userInfo?.nickName) user.nickName = userInfo.nickName
if (userInfo?.avatarUrl) user.avatarUrl = userInfo.avatarUrl
}
await user.save()
// 记录登录日志
await recordLoginLog(
user._id,
openid,
isNewUser ? 'register' : 'login',
'wechat_miniapp',
source,
req,
true
)
// 生成 JWT token(可选,小程序通常不需要)
const token = jwt.sign(
{ openid, userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
return ApiResponse.success(res, {
token,
openid,
isNewUser,
userInfo: {
nickName: user.nickName,
avatarUrl: user.avatarUrl
},
firstPlayDate: user.createdAt
}, isNewUser ? '注册成功' : '登录成功')
} catch (error) {
logger.error('登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* WebView 静默登录
* POST /api/auth/webview-login
* 用于微信小程序 webview 内嵌 H5 的自动登录
*/
router.post('/webview-login', async (req, res) => {
try {
const { code, userInfo } = req.body
if (!code) {
return ApiResponse.error(res, '缺少登录凭证', 400)
}
// 通过 code 获取 openid
let openid = null
try {
const sessionData = await weChatService.code2Session(code)
openid = sessionData.openid
} catch (error) {
logger.error('WebView登录获取session失败:', error)
return ApiResponse.error(res, '登录凭证无效', 401)
}
if (!openid) {
return ApiResponse.error(res, '无法获取用户标识', 400)
}
// 查找或创建用户
let user = await User.findOne({ openid })
let isNewUser = false
if (!user) {
// 新用户 - 使用微信提供的信息创建账户
user = new User({
openid,
nickName: userInfo?.nickName || '星辰旅行者',
avatarUrl: userInfo?.avatarUrl || '',
unlockedDims: [1]
})
isNewUser = true
logger.info(`WebView新用户注册: ${openid}`)
} else {
// 老用户,更新登录时间和信息
user.lastLoginAt = new Date()
if (userInfo?.nickName) user.nickName = userInfo.nickName
if (userInfo?.avatarUrl) user.avatarUrl = userInfo.avatarUrl
logger.info(`WebView老用户登录: ${openid}`)
}
await user.save()
// 记录登录日志
await recordLoginLog(
user._id,
openid,
isNewUser ? 'register' : 'auto_login',
'wechat_webview',
'webview',
req,
true
)
// 生成 JWT token
const token = jwt.sign(
{ openid, userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
return ApiResponse.success(res, {
token,
openid,
isNewUser,
userInfo: {
nickName: user.nickName,
avatarUrl: user.avatarUrl
},
firstPlayDate: user.createdAt
}, isNewUser ? '注册成功' : '自动登录成功')
} catch (error) {
logger.error('WebView登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 管理员登录
* POST /api/auth/admin/login
*/
router.post('/admin/login', async (req, res) => {
try {
const { username, password } = req.body
// 查找管理员
const admin = await Admin.findOne({ username })
if (!admin) {
return ApiResponse.error(res, '用户名或密码错误', 401)
}
// 验证密码
const isMatch = await admin.validatePassword(password)
if (!isMatch) {
return ApiResponse.error(res, '用户名或密码错误', 401)
}
// 检查状态
if (admin.status !== 'active') {
return ApiResponse.error(res, '账户已禁用', 403)
}
// 更新登录信息
admin.lastLogin = new Date()
admin.lastLoginIp = req.ip
admin.loginCount += 1
await admin.save()
// 记录日志
await AdminLog.create({
adminId: admin._id,
adminName: admin.username,
action: 'login',
resource: 'system',
ip: req.ip,
userAgent: req.get('User-Agent')
})
// 生成 JWT token
const token = jwt.sign(
{ adminId: admin._id, role: admin.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
// 获取权限
const permissions = Admin.getPermissionsByRole(admin.role)
return ApiResponse.success(res, {
token,
adminId: admin._id,
username: admin.username,
role: admin.role,
permissions
}, '登录成功')
} catch (error) {
logger.error('管理员登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* H5环境用户名密码登录
* POST /api/auth/user/login
*/
router.post('/user/login', async (req, res) => {
try {
const { username, password } = req.body
// 查找用户(显式包含密码字段)
const user = await User.findOne({ username }).select('+password')
if (!user) {
return ApiResponse.error(res, '用户名或密码错误', 401)
}
// 验证密码
const isMatch = await user.validatePassword(password)
if (!isMatch) {
return ApiResponse.error(res, '用户名或密码错误', 401)
}
// 更新登录时间
user.lastLoginAt = new Date()
await user.save()
// 生成 JWT token
const token = jwt.sign(
{ userId: user._id, openid: user.openid },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
return ApiResponse.success(res, {
token,
userId: user._id,
openid: user.openid,
userInfo: {
nickName: user.nickName,
avatarUrl: user.avatarUrl
},
firstPlayDate: user.createdAt
}, '登录成功')
} catch (error) {
logger.error('用户登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* H5环境用户注册
* POST /api/auth/user/register
*/
router.post('/user/register', async (req, res) => {
try {
const { username, password, nickName, avatarUrl } = req.body
// 检查用户名是否已存在
const existingUser = await User.findOne({ username })
if (existingUser) {
return ApiResponse.error(res, '用户名已存在', 400)
}
// 创建新用户
const user = new User({
username,
password,
nickName: nickName || '星辰旅行者',
avatarUrl: avatarUrl || '',
unlockedDims: [1]
})
await user.save()
logger.info(`新用户注册: ${username}`)
// 生成 JWT token
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
return ApiResponse.success(res, {
token,
userId: user._id,
userInfo: {
nickName: user.nickName,
avatarUrl: user.avatarUrl
}
}, '注册成功')
} catch (error) {
logger.error('用户注册失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取当前用户信息
* GET /api/auth/me
*/
router.get('/me', async (req, res) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return ApiResponse.unauthorized(res)
}
const decoded = jwt.verify(token, process.env.JWT_SECRET)
const user = await User.findById(decoded.userId)
if (!user) {
return ApiResponse.notFound(res, '用户不存在')
}
return ApiResponse.success(res, user)
} catch (error) {
logger.error('获取用户信息失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* WebView Token 交换接口
* POST /api/auth/webview-token
* 用于小程序 WebView 内嵌 H5 时,通过 openid 获取登录 token
* 小程序在 URL 中传递 openidH5 用这个接口换取 token
*/
router.post('/webview-token', async (req, res) => {
try {
const { openid } = req.body
if (!openid) {
return ApiResponse.error(res, '缺少用户标识', 400)
}
// 查找用户
const user = await User.findOne({ openid })
if (!user) {
return ApiResponse.error(res, '用户不存在', 404)
}
// 生成新的 JWT token
const token = jwt.sign(
{ openid, userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
)
// 更新最后登录时间
user.lastLoginAt = new Date()
await user.save()
// 记录登录日志
await recordLoginLog(
user._id,
openid,
'webview_auto_login',
'wechat_webview',
'webview_h5',
req,
true
)
logger.info(`WebView Token交换成功: ${openid}`)
return ApiResponse.success(res, {
token,
openid,
userInfo: {
nickName: user.nickName,
avatarUrl: user.avatarUrl
}
}, '登录成功')
} catch (error) {
logger.error('WebView Token交换失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+228
View File
@@ -0,0 +1,228 @@
const express = require('express')
const router = express.Router()
const { Feedback, User } = require('../models')
const ApiResponse = require('../utils/response')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取客户端IP
*/
const getClientIp = (req) => {
return req.headers['x-forwarded-for'] ||
req.headers['x-real-ip'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
'0.0.0.0'
}
/**
* 提交反馈
* POST /api/feedback
*/
router.post('/', async (req, res) => {
try {
const {
type,
title,
content,
images,
relatedPage,
deviceInfo,
userContact
} = req.body
// 验证必填字段
if (!type || !title || !content) {
return ApiResponse.error(res, '请填写完整的反馈信息', 400)
}
// 获取用户信息(如果已登录)
let userId = null
let openid = null
let userName = ''
try {
const token = req.headers.authorization?.replace('Bearer ', '')
if (token) {
const jwt = require('jsonwebtoken')
const decoded = jwt.verify(token, process.env.JWT_SECRET)
userId = decoded.userId
openid = decoded.openid
// 获取用户信息
const user = await User.findById(userId)
if (user) {
userName = user.nickName || ''
}
}
} catch (error) {
// Token 验证失败,继续以匿名方式提交
logger.warn('反馈提交时Token验证失败:', error.message)
}
// 创建反馈记录
const feedback = new Feedback({
userId,
openid,
userName,
userContact: userContact || '',
type,
title,
content,
images: images || [],
relatedPage: relatedPage || '',
deviceInfo: deviceInfo || {},
networkInfo: {
ip: getClientIp(req)
},
status: 'pending',
priority: 'normal'
})
await feedback.save()
logger.info(`新反馈提交: ${feedback._id}, 类型: ${type}`)
return ApiResponse.success(res, {
feedbackId: feedback._id
}, '反馈提交成功,我们会尽快处理')
} catch (error) {
logger.error('提交反馈失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取我的反馈列表
* GET /api/feedback/my
*/
router.get('/my', userAuthMiddleware, async (req, res) => {
try {
const user = req.user
const { page = 1, limit = 20 } = req.query
const query = { userId: user._id }
const total = await Feedback.countDocuments(query)
const feedbacks = await Feedback.find(query)
.sort({ createdAt: -1 })
.skip((parseInt(page) - 1) * parseInt(limit))
.limit(parseInt(limit))
.select('-deviceInfo -networkInfo')
.lean()
// 格式化返回数据
const formattedFeedbacks = feedbacks.map(fb => ({
id: fb._id,
type: fb.type,
title: fb.title,
content: fb.content.substring(0, 100) + (fb.content.length > 100 ? '...' : ''),
images: fb.images,
status: fb.status,
priority: fb.priority,
result: fb.result,
createdAt: fb.createdAt,
updatedAt: fb.updatedAt
}))
return ApiResponse.paginated(res, formattedFeedbacks, {
page: parseInt(page),
limit: parseInt(limit),
total
})
} catch (error) {
logger.error('获取我的反馈失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取反馈详情
* GET /api/feedback/:id
*/
router.get('/:id', userAuthMiddleware, async (req, res) => {
try {
const user = req.user
const feedback = await Feedback.findById(req.params.id).lean()
if (!feedback) {
return ApiResponse.notFound(res, '反馈不存在')
}
// 检查权限(只能查看自己的反馈)
if (feedback.userId?.toString() !== user._id.toString()) {
return ApiResponse.error(res, '无权查看此反馈', 403)
}
return ApiResponse.success(res, {
id: feedback._id,
type: feedback.type,
title: feedback.title,
content: feedback.content,
images: feedback.images,
relatedPage: feedback.relatedPage,
status: feedback.status,
priority: feedback.priority,
processLog: feedback.processLog,
result: feedback.result,
handler: feedback.handler,
handledAt: feedback.handledAt,
rating: feedback.rating,
createdAt: feedback.createdAt,
updatedAt: feedback.updatedAt
})
} catch (error) {
logger.error('获取反馈详情失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 对处理结果评分
* POST /api/feedback/:id/rate
*/
router.post('/:id/rate', userAuthMiddleware, async (req, res) => {
try {
const user = req.user
const { rating, remark } = req.body
if (!rating || rating < 1 || rating > 5) {
return ApiResponse.error(res, '评分必须在1-5之间', 400)
}
const feedback = await Feedback.findById(req.params.id)
if (!feedback) {
return ApiResponse.notFound(res, '反馈不存在')
}
// 检查权限
if (feedback.userId?.toString() !== user._id.toString()) {
return ApiResponse.error(res, '无权操作此反馈', 403)
}
// 只能对已解决的反馈评分
if (feedback.status !== 'resolved' && feedback.status !== 'closed') {
return ApiResponse.error(res, '只能对已处理的反馈进行评分', 400)
}
feedback.rating = rating
if (remark) {
feedback.userRemark = remark
}
await feedback.save()
return ApiResponse.success(res, {}, '评分成功')
} catch (error) {
logger.error('反馈评分失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+38
View File
@@ -0,0 +1,38 @@
const authRoutes = require('./auth')
const userRoutes = require('./user')
const paymentRoutes = require('./payment')
const knowledgeRoutes = require('./knowledge')
const adminRoutes = require('./admin')
const aiChatRoutes = require('./aiChat')
const aiModelRoutes = require('./aiModel')
const feedbackRoutes = require('./feedback')
const trendRoutes = require('./trend')
module.exports = (app) => {
// 认证路由
app.use('/api/auth', authRoutes)
// 用户路由
app.use('/api/user', userRoutes)
// 支付路由
app.use('/api/payment', paymentRoutes)
// 知识库路由
app.use('/api/knowledge', knowledgeRoutes)
// 反馈路由
app.use('/api/feedback', feedbackRoutes)
// 管理后台路由
app.use('/api/admin', adminRoutes)
// AI问答路由
app.use('/api/ai-chat', aiChatRoutes)
// AI模型配置路由
app.use('/api/ai-model', aiModelRoutes)
// AI趋势路由
app.use('/api/trend', trendRoutes)
}
+225
View File
@@ -0,0 +1,225 @@
const express = require('express')
const router = express.Router()
const { Knowledge } = require('../models')
const ApiResponse = require('../utils/response')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 搜索知识
* GET /api/knowledge/search?q=xxx&page=1&limit=20
*/
router.get('/search', async (req, res) => {
try {
const { q, page = 1, limit = 20 } = req.query
if (!q || q.trim().length === 0) {
return ApiResponse.error(res, '请输入搜索关键词', 400)
}
const query = {
status: 'approved',
$or: [
{ title: { $regex: q, $options: 'i' } },
{ content: { $regex: q, $options: 'i' } },
{ tags: { $regex: q, $options: 'i' } }
]
}
const total = await Knowledge.countDocuments(query)
const result = await Knowledge.find(query)
.sort({ viewCount: -1, createdAt: -1 })
.skip((parseInt(page) - 1) * parseInt(limit))
.limit(parseInt(limit))
.lean()
return ApiResponse.paginated(res, result, {
page: parseInt(page),
limit: parseInt(limit),
total
})
} catch (error) {
logger.error('搜索知识失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取知识列表
* GET /api/knowledge
*/
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 20, dim, category, status = 'approved' } = req.query
const query = { status }
if (dim) query.dim = parseInt(dim)
if (category) query.category = category
const total = await Knowledge.countDocuments(query)
const knowledge = await Knowledge.find(query)
.sort({ sortOrder: 1, createdAt: -1 })
.skip((parseInt(page) - 1) * parseInt(limit))
.limit(parseInt(limit))
.lean()
return ApiResponse.paginated(res, knowledge, {
page: parseInt(page),
limit: parseInt(limit),
total
})
} catch (error) {
logger.error('获取知识列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取用户收藏的知识列表
* GET /api/knowledge/collected
*/
router.get('/collected', userAuthMiddleware, async (req, res) => {
try {
const user = req.user
// 查找用户收藏的知识
const collectedKnowledge = await Knowledge.find({ _id: { $in: user.collectedKnowledge } })
.lean()
// 格式化返回数据
const formattedKnowledge = collectedKnowledge.map(item => ({
id: item._id,
dimension: item.dim,
title: item.title,
description: item.content || item.description || '',
knowledgeId: item._id
}))
return ApiResponse.success(res, formattedKnowledge)
} catch (error) {
logger.error('获取收藏列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 删除收藏
* DELETE /api/knowledge/collect/:id
*/
router.delete('/collect/:id', userAuthMiddleware, async (req, res) => {
try {
const knowledgeId = req.params.id
const user = req.user
// 检查是否已经收藏
if (!user.collectedKnowledge.includes(knowledgeId)) {
return ApiResponse.error(res, '未收藏该知识', 400)
}
// 从用户的 collectedKnowledge 数组中移除
user.collectedKnowledge = user.collectedKnowledge.filter(id => id.toString() !== knowledgeId.toString())
await user.save()
// 减少收藏计数
const knowledge = await Knowledge.findById(knowledgeId)
if (knowledge) {
knowledge.collectionCount = Math.max(0, knowledge.collectionCount - 1)
await knowledge.save()
}
return ApiResponse.success(res, {}, '删除收藏成功')
} catch (error) {
logger.error('删除收藏失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取知识详情
* GET /api/knowledge/:id
*/
router.get('/:id', async (req, res) => {
try {
const knowledge = await Knowledge.findById(req.params.id)
if (!knowledge) {
return ApiResponse.notFound(res, '知识不存在')
}
// 增加浏览量
knowledge.viewCount += 1
await knowledge.save()
return ApiResponse.success(res, knowledge)
} catch (error) {
logger.error('获取知识详情失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 收藏知识
* POST /api/knowledge/:id/collect
*/
router.post('/:id/collect', userAuthMiddleware, async (req, res) => {
try {
const knowledge = await Knowledge.findById(req.params.id)
if (!knowledge) {
return ApiResponse.notFound(res, '知识不存在')
}
const user = req.user
// 检查是否已经收藏
if (user.collectedKnowledge.includes(knowledge._id)) {
return ApiResponse.error(res, '已经收藏过了', 400)
}
// 将知识ID加入用户的 collectedKnowledge 数组
user.collectedKnowledge.push(knowledge._id)
await user.save()
// 增加收藏计数
knowledge.collectionCount += 1
await knowledge.save()
return ApiResponse.success(res, { collectionCount: knowledge.collectionCount }, '收藏成功')
} catch (error) {
logger.error('收藏失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取问答
* GET /api/knowledge/:dim/questions
*/
router.get('/:dim/questions', async (req, res) => {
try {
const { dim } = req.params
const { limit = 10 } = req.query
const questions = await Knowledge.find({
dim: parseInt(dim),
category: 'question',
status: 'approved'
})
.select('question options')
.limit(parseInt(limit))
.lean()
return ApiResponse.success(res, questions)
} catch (error) {
logger.error('获取问答失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+266
View File
@@ -0,0 +1,266 @@
const express = require('express')
const router = express.Router()
const { User, Order, ShopItem } = require('../models')
const ApiResponse = require('../utils/response')
const weChatService = require('../utils/weixin')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取商品列表(从数据库)- 公开接口
* GET /api/payment/products
*/
router.get('/products', async (req, res) => {
try {
const items = await ShopItem.find({ status: 'active' }).sort({ sort: 1 })
return ApiResponse.success(res, items)
} catch (error) {
logger.error('获取商品列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
// 应用用户认证中间件到需要保护的路由
router.use(userAuthMiddleware)
/**
* 创建订单
* POST /api/payment/create-order
*/
router.post('/create-order', async (req, res) => {
try {
const user = req.user
const { productId } = req.body
// 从数据库获取商品信息
const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) {
return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400)
}
// 检查是否已购买(针对皮肤类一次性商品)
if (product.type === 'skin' && user.ownedSkins?.includes(productId)) {
return ApiResponse.error(res, '已拥有该商品', 400)
}
// 生成订单号
const orderNo = weChatService.generateOrderNo('WD')
// 创建订单记录
const order = new Order({
orderNo,
openid: user.openid,
userId: user._id,
productId,
productName: product.name,
amount: product.price,
status: 'pending'
})
await order.save()
// 调用微信支付统一下单
const payParams = await weChatService.createPaymentOrder({
openid: user.openid,
orderNo,
body: `宇之然-${product.name}`,
amount: product.price
})
return ApiResponse.success(res, {
orderNo,
productId,
productName: product.name,
amount: product.price,
payParams
}, '订单创建成功')
} catch (error) {
logger.error('创建订单失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 支付回调
* POST /api/payment/callback
*/
router.post('/callback', async (req, res) => {
try {
const data = req.body
// 验证签名
if (!weChatService.verifyCallbackSignature(data, data.sign)) {
logger.warn('支付签名验证失败')
return res.status(400).send('FAIL')
}
const orderNo = data.out_trade_no
const transactionId = data.transaction_id
// 查询订单
const order = await Order.findOne({ orderNo })
if (!order) {
logger.error(`订单不存在: ${orderNo}`)
return res.status(400).send('FAIL')
}
// 已处理过
if (order.status === 'paid' || order.status === 'completed') {
return res.status(200).send('SUCCESS')
}
// 更新订单状态
order.status = 'paid'
order.transactionId = transactionId
order.paidAt = new Date()
await order.save()
// 解锁商品
const user = await User.findOne({ openid: order.openid })
if (user) {
const product = await ShopItem.findOne({ itemId: order.productId })
if (product) {
if (product.type === 'skin') {
if (!user.ownedSkins.includes(order.productId)) {
user.ownedSkins.push(order.productId)
}
} else if (product.type === 'noad') {
user.isAdFree = true
} else if (product.type === 'subscription') {
const expiry = new Date()
expiry.setDate(expiry.getDate() + (product.duration || 30))
user.isSubscriber = true
user.subscribeExpiry = expiry
}
await user.save()
}
}
logger.info(`支付成功: ${orderNo}`)
return res.status(200).send('SUCCESS')
} catch (error) {
logger.error('支付回调处理失败:', error)
return res.status(500).send('FAIL')
}
})
/**
* 验证支付结果
* POST /api/payment/verify
*/
router.post('/verify', async (req, res) => {
try {
const user = req.user
const { productId, orderNo } = req.body
// 验证订单是否已处理
const order = await Order.findOne({
orderNo,
openid: user.openid,
status: { $in: ['paid', 'completed'] }
})
if (order) {
return ApiResponse.success(res, {
alreadyProcessed: true,
message: '订单已处理'
})
}
// 获取商品信息
const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) {
return ApiResponse.error(res, `未知商品: ${productId}`, 400)
}
// 更新用户商品
let updateData = {}
if (product.type === 'skin') {
if (!user.ownedSkins.includes(productId)) {
user.ownedSkins.push(productId)
}
} else if (product.type === 'noad') {
user.isAdFree = true
} else if (product.type === 'subscription') {
const expiry = new Date()
expiry.setDate(expiry.getDate() + (product.duration || 30))
user.isSubscriber = true
user.subscribeExpiry = expiry
}
await user.save()
// 记录订单
const newOrder = new Order({
orderNo,
openid: user.openid,
userId: user._id,
productId,
productName: product.name,
amount: product.price,
status: 'completed',
completedAt: new Date()
})
await newOrder.save()
return ApiResponse.success(res, {
product,
message: `${product.name} 已解锁!`
})
} catch (error) {
logger.error('验证支付失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 申请退款
* POST /api/payment/refund
*/
router.post('/refund', async (req, res) => {
try {
const { orderNo, reason } = req.body
// 查询订单
const order = await Order.findOne({ orderNo })
if (!order) {
return ApiResponse.error(res, '订单不存在', 404)
}
if (order.status !== 'paid' && order.status !== 'completed') {
return ApiResponse.error(res, '订单状态不支持退款', 400)
}
// 调用微信退款接口
const result = await weChatService.refundOrder(
order.transactionId,
`REF_${order.orderNo}`,
order.amount / 100,
order.amount / 100,
reason
)
if (result.success) {
// 更新订单状态
order.status = 'refunded'
order.refundReason = reason
order.refundedAt = new Date()
await order.save()
return ApiResponse.success(res, result, '退款申请已提交')
} else {
return ApiResponse.error(res, '退款申请失败', 500)
}
} catch (error) {
logger.error('申请退款失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+83
View File
@@ -0,0 +1,83 @@
const express = require('express');
const router = express.Router();
const Trend = require('../models/Trend');
/**
* 获取趋势列表
* GET /api/trend?category=产品&page=1&limit=20
*/
router.get('/', async (req, res) => {
try {
const { category, page = 1, limit = 20 } = req.query;
const query = { status: 'published' };
if (category) query.category = category;
const skip = (parseInt(page) - 1) * parseInt(limit);
const [list, total] = await Promise.all([
Trend.find(query)
.sort({ newsDate: -1 })
.skip(skip)
.limit(parseInt(limit))
.select('-__v'),
Trend.countDocuments(query)
]);
res.json({
success: true,
data: { list, total, page: parseInt(page), totalPages: Math.ceil(total / parseInt(limit)) }
});
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
/**
* 获取趋势详情
* GET /api/trend/:id
*/
router.get('/:id', async (req, res) => {
try {
const trend = await Trend.findByIdAndUpdate(
req.params.id,
{ $inc: { viewCount: 1 } },
{ new: true }
);
if (!trend) return res.status(404).json({ success: false, error: '趋势不存在' });
res.json({ success: true, data: trend });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
/**
* 管理员:创建趋势
* POST /api/trend/admin
*/
router.post('/admin', async (req, res) => {
try {
const trend = new Trend(req.body);
await trend.save();
res.json({ success: true, data: trend });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
/**
* 管理员:批量创建趋势(给 cron 用)
* POST /api/trend/admin/batch
*/
router.post('/admin/batch', async (req, res) => {
try {
const { items } = req.body;
if (!Array.isArray(items) || items.length === 0) {
return res.status(400).json({ success: false, error: 'items 不能为空' });
}
const result = await Trend.insertMany(items);
res.json({ success: true, data: { inserted: result.length } });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
module.exports = router;
+424
View File
@@ -0,0 +1,424 @@
const express = require('express')
const router = express.Router()
const { User, Order, Gallery } = require('../models')
const ApiResponse = require('../utils/response')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取排行榜(公开接口,无需登录)
* GET /api/user/leaderboard
*/
router.get('/leaderboard', async (req, res) => {
try {
const { limit = 50, dim } = req.query
const users = await User.getLeaderboard(dim ? parseInt(dim) : null, parseInt(limit))
const data = users.map((u, i) => ({
rank: i + 1,
openid: u.openid,
nickname: u.nickName || '星辰旅者',
avatar: u.avatarUrl || '',
totalScore: u.totalScore || 0,
dim1Score: u.dim1Score || 0,
dim2Score: u.dim2Score || 0,
dim3Score: u.dim3Score || 0,
dim4Score: u.dim4Score || 0,
dim5Score: u.dim5Score || 0,
unlockedDims: u.unlockedDims || [1]
}))
return ApiResponse.success(res, data)
} catch (error) {
logger.error('获取排行榜失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
// 应用用户认证中间件到所有路由(排行榜已在上方公开)
router.use(userAuthMiddleware)
/**
* 获取用户进度
* GET /api/user/progress
*/
router.get('/progress', async (req, res) => {
try {
const user = req.user
if (!user) {
// 新用户,返回默认进度
return ApiResponse.success(res, {
isNewUser: true,
data: {
unlockedDims: [1],
dim1Score: 0,
dim2Score: 0,
dim3Score: 0,
dim4Score: 0,
dim5Score: 0,
totalScore: 0,
ownedSkins: [],
isAdFree: false,
isSubscriber: false,
subscribeExpiry: null,
totalPlayTime: 0
}
})
}
return ApiResponse.success(res, {
isNewUser: false,
data: user
})
} catch (error) {
logger.error('获取进度失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 保存用户进度
* POST /api/user/progress
*/
router.post('/progress', async (req, res) => {
try {
const user = req.user
const openid = user.openid
const {
dim,
data,
dim1Score,
dim2Score,
dim3Score,
dim4Score,
dim5Score,
unlockedDims,
ownedSkins,
isAdFree,
isSubscriber,
subscribeExpiry,
totalPlayTime
} = req.body
// 查找或创建用户(理论上已通过中间件找到)
let dbUser = user
// 新版接口:按维度更新
if (dim && data) {
const dimData = dbUser.exploreData[dim] || {}
let updatedDim = { ...dimData }
if (dim === 'dim1') {
updatedDim.completed = data.completed || dimData.completed || false
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
updatedDim.collectedEggs = (dimData.collectedEggs || 0) + (data.newEggs || 0)
updatedDim.totalLength = Math.max(data.totalLength || 0, dimData.totalLength || 0)
} else if (dim === 'dim2') {
updatedDim.completed = data.completed || dimData.completed || false
updatedDim.bestArea = Math.max(data.bestArea || 0, dimData.bestArea || 0)
updatedDim.createdShapes = (dimData.createdShapes || 0) + (data.newShapes || 0)
} else if (dim === 'dim3') {
updatedDim.completed = data.completed || dimData.completed || false
updatedDim.exploredFaces = Math.max(data.exploredFaces || 0, dimData.exploredFaces || 0)
updatedDim.rotationTime = (dimData.rotationTime || 0) + (data.rotationTime || 0)
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
} else if (dim === 'dim4') {
updatedDim.completed = data.completed || dimData.completed || false
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
updatedDim.exploredEvents = (dimData.exploredEvents || 0) + (data.newEvents || 0)
} else if (dim === 'dim5') {
updatedDim.completed = data.completed || dimData.completed || false
updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0)
updatedDim.exploredThoughts = (dimData.exploredThoughts || 0) + (data.newThoughts || 0)
}
dbUser.exploreData[dim] = updatedDim
// 同步更新已解锁维度
if (data.completed && !dbUser.unlockedDims.includes(parseInt(dim.replace('dim', '')) + 1)) {
const dimNum = parseInt(dim.replace('dim', ''))
if (!dbUser.unlockedDims.includes(dimNum + 1)) {
dbUser.unlockedDims.push(dimNum + 1)
}
}
if (totalPlayTime !== null && totalPlayTime !== undefined) {
dbUser.totalPlayTime += totalPlayTime
}
}
// 兼容旧版接口:直接传各维度分数
if (dim1Score !== null && dim1Score !== undefined) {
dbUser.exploreData.dim1.bestScore = Math.max(dim1Score, dbUser.exploreData.dim1?.bestScore || 0)
}
if (dim2Score !== null && dim2Score !== undefined) {
dbUser.exploreData.dim2.bestArea = Math.max(dim2Score, dbUser.exploreData.dim2?.bestArea || 0)
}
if (dim3Score !== null && dim3Score !== undefined) {
dbUser.exploreData.dim3.bestScore = Math.max(dim3Score, dbUser.exploreData.dim3?.bestScore || 0)
}
if (dim4Score !== null && dim4Score !== undefined) {
dbUser.exploreData.dim4 = dbUser.exploreData.dim4 || {}
dbUser.exploreData.dim4.bestScore = Math.max(dim4Score, dbUser.exploreData.dim4?.bestScore || 0)
}
if (dim5Score !== null && dim5Score !== undefined) {
dbUser.exploreData.dim5 = dbUser.exploreData.dim5 || {}
dbUser.exploreData.dim5.bestScore = Math.max(dim5Score, dbUser.exploreData.dim5?.bestScore || 0)
}
// 更新其他字段
if (ownedSkins !== null && ownedSkins !== undefined) dbUser.ownedSkins = ownedSkins
if (isAdFree !== null && isAdFree !== undefined) dbUser.isAdFree = isAdFree
if (isSubscriber !== null && isSubscriber !== undefined) dbUser.isSubscriber = isSubscriber
if (subscribeExpiry !== null && subscribeExpiry !== undefined) dbUser.subscribeExpiry = subscribeExpiry
await dbUser.save()
// 计算总分
const totalScore = dbUser.calculateTotalScore()
return ApiResponse.success(res, {
success: true,
totalScore,
exploreData: user.exploreData
})
} catch (error) {
logger.error('保存进度失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取用户成就
* GET /api/user/achievements
*/
router.get('/achievements', async (req, res) => {
try {
const user = req.user
// 生成成就数据(AI 维度主题)
const achievements = [
{
name: '知识探索',
achievements: [
{
id: 'dim1_read',
name: 'AI起源探索者',
description: '完成 AI 起源维度的知识阅读',
icon: '📜',
unlocked: user.exploreData.dim1?.completed || false,
date: user.exploreData.dim1?.completed ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'dim2_read',
name: 'AI发展探索者',
description: '完成 AI 发展维度的知识阅读',
icon: '🏛',
unlocked: user.exploreData.dim2?.completed || false,
date: user.exploreData.dim2?.completed ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'dim3_read',
name: 'AI当前探索者',
description: '完成 AI 当前格局维度的知识阅读',
icon: '♟',
unlocked: user.exploreData.dim3?.completed || false,
date: user.exploreData.dim3?.completed ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'dim4_read',
name: 'AI学习探索者',
description: '完成 AI 学习与工具维度的知识阅读',
icon: '🧠',
unlocked: user.exploreData.dim4?.completed || false,
date: user.exploreData.dim4?.completed ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'dim5_read',
name: 'AI趋势探索者',
description: '完成 AI 趋势追踪维度的知识阅读',
icon: '⚡',
unlocked: user.exploreData.dim5?.completed || false,
date: user.exploreData.dim5?.completed ? user.updatedAt.toISOString().split('T')[0] : null
}
]
},
{
name: '收藏成就',
achievements: [
{
id: 'first_collection',
name: '初次收藏',
description: '收藏你的第一个 AI 知识',
icon: '❤️',
unlocked: user.collectedKnowledge?.length > 0,
date: user.collectedKnowledge?.length > 0 ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'five_collections',
name: '收藏爱好者',
description: '收藏5个 AI 知识',
icon: '🧡',
unlocked: user.collectedKnowledge?.length >= 5,
date: user.collectedKnowledge?.length >= 5 ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'ten_collections',
name: '收藏大师',
description: '收藏10个 AI 知识',
icon: '💛',
unlocked: user.collectedKnowledge?.length >= 10,
date: user.collectedKnowledge?.length >= 10 ? user.updatedAt.toISOString().split('T')[0] : null
}
]
},
{
name: 'AI 问答',
achievements: [
{
id: 'first_ask',
name: '初次提问',
description: '向 AI 助手提出第一个问题',
icon: '🤖',
unlocked: user.achievements?.includes('first_ask') || false,
date: user.achievements?.includes('first_ask') ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'ten_asks',
name: '勤学好问',
description: '向 AI 助手提出 10 个问题',
icon: '💡',
unlocked: user.achievements?.includes('ten_asks') || false,
date: user.achievements?.includes('ten_asks') ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'fifty_asks',
name: 'AI 达人',
description: '向 AI 助手提出 50 个问题',
icon: '🧠',
unlocked: user.achievements?.includes('fifty_asks') || false,
date: user.achievements?.includes('fifty_asks') ? user.updatedAt.toISOString().split('T')[0] : null
}
]
},
{
name: '社交成就',
achievements: [
{
id: 'first_share',
name: '初次分享',
description: '分享你的第一个知识给朋友',
icon: '📤',
unlocked: user.achievements?.includes('first_share') || false,
date: user.achievements?.includes('first_share') ? user.updatedAt.toISOString().split('T')[0] : null
},
{
id: 'five_shares',
name: '分享达人',
description: '分享 5 次知识给朋友',
icon: '📣',
unlocked: user.achievements?.includes('five_shares') || false,
date: user.achievements?.includes('five_shares') ? user.updatedAt.toISOString().split('T')[0] : null
}
]
}
]
return ApiResponse.success(res, achievements)
} catch (error) {
logger.error('获取成就失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取用户统计数据
* GET /api/user/stats
*/
router.get('/stats', async (req, res) => {
try {
const user = req.user
const { Gallery } = require('../models')
// 获取用户作品数
const worksCount = await Gallery.countDocuments({ userId: user._id })
// 获取等级信息
const levelInfo = user.getLevelInfo()
// 计算探索时长(分钟转小时,保留1位小数)
const playTimeHours = Math.round((user.totalPlayTime || 0) / 60 * 10) / 10
return ApiResponse.success(res, {
level: levelInfo.level,
levelName: levelInfo.name,
levelPoints: levelInfo.points,
levelProgress: levelInfo.progress,
totalProgress: levelInfo.totalProgress,
nextLevelPoints: levelInfo.nextLevelPoints,
totalPlayTime: playTimeHours,
totalScore: user.totalScore || 0,
worksCount: worksCount,
collectionCount: user.collectedKnowledge?.length || 0,
achievementCount: user.achievements?.length || 0,
unlockedDims: user.unlockedDims || [1]
})
} catch (error) {
logger.error('获取用户统计失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 获取用户作品列表
* GET /api/user/works
*/
router.get('/works', async (req, res) => {
try {
const user = req.user
const { page = 1, limit = 20 } = req.query
const { Gallery } = require('../models')
const query = { userId: user._id }
const total = await Gallery.countDocuments(query)
const works = await Gallery.find(query)
.sort({ createdAt: -1 })
.skip((parseInt(page) - 1) * parseInt(limit))
.limit(parseInt(limit))
.lean()
// 格式化作品数据
const formattedWorks = works.map(work => ({
id: work._id,
title: work.title,
description: work.description,
imageUrl: work.imageUrl,
dimension: work.dimension,
status: work.status,
likes: work.likes || 0,
views: work.views || 0,
createdAt: work.createdAt
}))
return ApiResponse.paginated(res, formattedWorks, {
page: parseInt(page),
limit: parseInt(limit),
total
})
} catch (error) {
logger.error('获取用户作品失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
+228
View File
@@ -0,0 +1,228 @@
/**
* 内容丰富脚本 — 清理旧 WDKJ 数据,插入 5 维度 AI 知识 + 扩充趋势
* 执行: node src/scripts/enrich-content.js
*/
const mongoose = require('mongoose')
const path = require('path')
// 加载环境变量
const dotenv = require('dotenv')
dotenv.config({ path: path.join(__dirname, '..', '..', '.env') })
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj'
async function main() {
await mongoose.connect(MONGODB_URI)
console.log('✅ 已连接 MongoDB:', MONGODB_URI)
const db = mongoose.connection.db
// ============================================================
// 1. 清理旧 WDKJ 知识(几何空间类内容)
// ============================================================
const oldKeywords = ['一维', '二维', '三维', '四维', '五维', '六维', '七维', '高维',
'多维空间', '坐标系', '几何特性', '几何空间', '数学表示', '物理意义',
'弦理论', '卡鲁扎-克莱因', '紧致维度', '可视化技术', '投影法', '切片法']
// Build regex for old content
const oldRegex = oldKeywords.map(k => `.*${k}.*`).join('|')
const deleteResult = await db.collection('knowledges').deleteMany({
$or: oldKeywords.map(k => ({ title: { $regex: k } }))
})
console.log(`🗑️ 已删除 ${deleteResult.deletedCount} 条旧 WDKJ 知识`)
// Also delete any remaining non-AI content
const extraDelete = await db.collection('knowledges').deleteMany({
title: { $regex: /(空间|坐标|几何|弦理论|可视化|投影)/ }
})
if (extraDelete.deletedCount > 0) {
console.log(`🗑️ 额外删除 ${extraDelete.deletedCount} 条几何内容`)
}
// ============================================================
// 2. 插入 5 维度 AI 知识
// ============================================================
const knowledgeItems = [
// ===== D1: AI 起源 =====
{ dim: 1, title: '图灵测试:人工智能的哲学起点',
content: '1950 年,英国数学家艾伦·图灵发表了划时代论文《计算机器与智能》,提出了著名的"图灵测试":如果一台机器能够在对话中让人类无法分辨它是机器还是人,那么这台机器就具备智能。图灵还预言了创造真正智能机器的可能性,这一思想成为 AI 领域的精神源头。图灵测试不仅是一个测试标准,更引发了"机器能否思考"的终极哲学追问,至今仍是 AI 研究中的核心议题。',
category: 'history', tags: ['图灵测试', 'AI起源', '艾伦·图灵'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 1 },
{ dim: 1, title: '达特茅斯会议:AI 的诞生时刻',
content: '1956 年夏季,约翰·麦卡锡、马文·明斯基、克劳德·香农等科学家在美国达特茅斯学院召开了一场具有历史意义的研讨会。正是在这次会议上,"人工智能(Artificial Intelligence"这个术语被首次正式提出,标志着 AI 作为一门独立学科的诞生。与会者乐观地预测"人类级别的机器智能将在一代人的时间内实现"。虽然这个预测过于乐观,但达特茅斯会议点燃了 AI 研究的火种。',
category: 'history', tags: ['达特茅斯会议', 'AI诞生', '麦卡锡', '香农'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 2 },
{ dim: 1, title: 'ELIZA 与感知机:AI 的童年玩具',
content: '1966 年,MIT 的约瑟夫·魏泽鲍姆创造了 ELIZA,史上第一个聊天机器人。它通过简单的模式匹配模拟心理治疗师的对话,让人们首次直观感受到 AI 的交互潜力。同年,弗兰克·罗森布拉特的感知机(Perceptron)成为最早的人工神经网络之一。然而,马文·明斯基随后证明了感知机的局限性,导致神经网络研究陷入低谷,这为后来的"AI 寒冬"埋下了伏笔。',
category: 'history', tags: ['ELIZA', '感知机', '聊天机器人', '神经网络'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 3 },
{ dim: 1, title: '深蓝与 AI 寒冬:技术的曲折前行',
content: '1997 年,IBM 的深蓝(Deep Blue)击败国际象棋世界冠军卡斯帕罗夫,成为首个在标准比赛时限内战胜棋王的计算机系统。这一成就震惊世界,证明了 AI 在复杂战略领域超越人类的潜力。但在此之前,AI 经历了长达十年的"寒冬":1970 年代,由于计算能力不足和符号主义 AI 难以处理现实问题,各国科研经费大幅削减。深蓝的胜利标志着 AI 从寒冬中复苏,开启了新的发展周期。',
category: 'history', tags: ['深蓝', 'IBM', '卡斯帕罗夫', 'AI寒冬'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 4 },
{ dim: 1, title: '反向传播算法:神经网络的复兴之匙',
content: '1986 年,大卫·鲁姆哈特等人重新发现并推广了反向传播(Backpropagation)算法,解决了多层神经网络的训练难题。此前,神经网络只能处理简单的线性问题,而 BP 算法让网络能够通过"误差反向传播"来调整各层参数。这一突破使连接主义(神经网络方法)重新成为 AI 研究的主流方向,为 2010 年代的深度学习革命奠定了理论基础。',
category: 'concept', tags: ['反向传播', 'BP算法', '神经网络', '深度学习'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 5 },
{ dim: 1, title: 'AlexNet 与深度学习元年',
content: '2012 年,亚历克斯·克里泽夫斯基等人提出的 AlexNet 在 ImageNet 图像识别大赛中以远超传统方法的精度夺冠,错误率从 26% 骤降至 15%。AlexNet 使用了 GPU 加速的深度卷积神经网络,标志着深度学习正式成为计算机视觉的核心技术。这一年被称为"深度学习元年",开启了 AI 全面爆发的时代。此后,更深的 VGG、残差网络 ResNet 等模型不断刷新纪录。',
category: 'concept', tags: ['AlexNet', 'ImageNet', '卷积神经网络', 'GPU'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 6 },
{ dim: 1, title: 'AlphaGoAI 征服围棋之路',
content: '2016 年,谷歌 DeepMind 的 AlphaGo 以 4:1 击败围棋世界冠军李世石,这一成就被视为 AI 发展的里程碑。围棋的可能性远超国际象棋(棋盘状态数超过宇宙原子数),传统方法无法解决。AlphaGo 融合了深度学习和强化学习——先通过人类棋谱学习,再通过自我对弈不断进化。后续的 AlphaZero 更实现了"从零自学",掌握多种棋类游戏。AlphaGo 证明了 AI 在直觉性、创造性领域的巨大潜力。',
category: 'history', tags: ['AlphaGo', 'DeepMind', '围棋', '强化学习', '李世石'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 7 },
// ===== D2: AI 发展 =====
{ dim: 2, title: 'Transformer 架构:AI 的基石',
content: '2017 年,Google 研究团队在论文《Attention is All You Need》中提出了 Transformer 架构,彻底改变了 AI 的发展轨迹。Transformer 完全摒弃了传统的循环神经网络结构,完全基于自注意力机制(Self-Attention)来捕捉序列中元素之间的关系。它的优势在于:可并行计算(训练速度大幅提升)、可捕捉长距离依赖关系、可扩展到海量参数。如今,所有主流大模型(GPT、BERT、Claude、Gemini、DeepSeek 等)都建立在 Transformer 架构之上。',
category: 'concept', tags: ['Transformer', '注意力机制', 'Google', 'Attention'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 1 },
{ dim: 2, title: 'GPT 系列演进:从 GPT-1 到 GPT-5',
content: 'OpenAI 的 GPT 系列是 AI 发展史上最具影响力的模型家族之一。2018 年的 GPT-1(1.17 亿参数)证明了大规模无监督预训练的有效性。2019 年的 GPT-215 亿参数)因"太危险"一度推迟发布,其文本生成能力令人震惊。2020 年的 GPT-3(1750 亿参数)确立了"规模即能力"的范式。2022 年的 ChatGPT 将 GPT-3.5 带入大众视野,引爆全球 AI 热潮。2023-2026 年,GPT-4、GPT-5、GPT-5.5 持续迭代,在推理、多模态、长上下文等方面不断突破,5.5 版本更是从零重训,性能全面提升。',
category: 'history', tags: ['GPT', 'OpenAI', 'ChatGPT', '大模型'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 2 },
{ dim: 2, title: 'BERT 与预训练-微调范式',
content: '2018 年,Google 提出的 BERTBidirectional Encoder Representations from Transformers)开启了一种全新的 AI 模型范式:先在海量无标注数据上预训练,再针对具体任务进行微调。BERT 是双向编码器模型,能同时从上下文中理解每个词的含义。这一范式彻底改变了 NLP 领域,后来被广泛应用于各种 AI 任务,成为现代大模型的基础方法论。',
category: 'concept', tags: ['BERT', '预训练', '微调', 'Google', 'NLP'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 3 },
{ dim: 2, title: '生成对抗网络 GANAI 创造力的起点',
content: '2014 年,伊恩·古德费洛提出了生成对抗网络(GAN),由生成器和判别器两个网络相互博弈:生成器努力生成逼真的假数据,判别器努力分辨真假。这种"对抗训练"机制使 AI 能够生成极为逼真的图像、音频和视频。GAN 开启了生成式 AI 的时代,为后来的扩散模型(DALL·E、Stable Diffusion 等)奠定了基础。虽然如今扩散模型在某些任务上超越了 GAN,但 GAN 的思想深刻影响了整个 AI 领域。',
category: 'concept', tags: ['GAN', '生成对抗网络', '生成式AI', '古德费洛'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 4 },
{ dim: 2, title: '扩散模型与 AI 绘画革命',
content: '2022 年,Stable Diffusion 和 DALL·E 2 的发布引爆了 AI 绘画热潮。这些模型基于扩散(Diffusion)技术:先学习将图像逐步加噪的过程,再学会从纯噪声中逐步还原出清晰的图像。扩散模型在图像质量、多样性和创意性上远超 GAN,让"一句话生成专业级图片"成为现实。此后,Midjourney 以其独特的艺术风格成为设计师的新宠,Adobe 将 Firefly 集成到 PhotoshopAI 绘画正式进入主流创作工具链。',
category: 'concept', tags: ['扩散模型', 'Stable Diffusion', 'DALL·E', 'Midjourney', 'AI绘画'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 5 },
{ dim: 2, title: 'MoE 混合专家模型:效率的革命',
content: '混合专家模型(Mixture of Experts, MoE)是一种将大模型分解为多个"专家"子网络的架构。每次推理时,MoE 只激活其中少数专家,而非全量参数。例如 GPT-4 传言有 1.8 万亿参数,但每次只激活约 3700 亿。这种设计大幅降低了推理成本,同时保留了海量参数的知识容量。DeepSeek V4 等国产模型也采用了 MoE 架构,以极低的成本实现接近顶级闭源模型的性能。MoE 被认为是未来大模型规模化的重要方向。',
category: 'concept', tags: ['MoE', '混合专家模型', 'GPT-4', 'DeepSeek', '效率'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 6 },
{ dim: 2, title: 'DeepSeek 开源之路:从 V2 到 R1 的技术跃迁',
content: 'DeepSeek 是近年来最受瞩目的国产大模型之一。2024 年的 DeepSeek V2 以创新的 MoE 架构和极低的推理成本引起关注。2025 年初的 DeepSeek R1 更是颠覆性模型——通过强化学习训练出强大的推理能力,在数学、代码等任务上媲美 OpenAI o1。R1 展示了一个重要方向:不必追求最大参数,而是通过创新的训练方法和架构设计,以更低的成本实现更强的智能。DeepSeek 坚持开源路线,为全球 AI 社区贡献了宝贵的开放权重模型。',
category: 'history', tags: ['DeepSeek', 'R1', '开源', '推理', '国产模型'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 7 },
// ===== D3: AI 当前 =====
{ dim: 3, title: '2026 年大模型格局:四强争霸',
content: '2026 年,AI 大模型竞争进入白热化阶段,形成 GPT、Claude、Gemini、DeepSeek 四足鼎立的格局。OpenAI 的 GPT-5.5 是首个从零重训的基础模型,均衡性最强;Anthropic 的 Claude Opus 4.7 在编码(SWE-bench 87.6%)领域领先,代码质量行业第一;Google 的 Gemini 3.5 Flash 免费开放,200 万 Token 上下文窗口独步天下;DeepSeek V4 以极低的价格(仅为 Claude 的 1/432)进入全球前十。选择哪个模型取决于场景:编程用 Claude,通用选 GPT,长文本用 Gemini,成本敏感选 DeepSeek。',
category: 'application', tags: ['大模型', 'GPT-5', 'Claude', 'Gemini', 'DeepSeek', '2026'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 1 },
{ dim: 3, title: 'AI Agent 智能体生态:2026 爆发元年',
content: '2026 年被业界称为"智能体爆发年"。AI Agent 不再只是聊天机器人,而是能独立感知环境、拆解任务、调用工具、执行操作并持续学习的智能实体。从 Cursor(年收入 5 亿美元)到 Devin,从 OpenAI Operator 到 ManusAI Agent 已从实验品转变为企业优先事项。MCP(模型上下文协议)和 A2A(Agent to Agent 协议)的标准化,让智能体能够互联互通。82% 的企业计划在未来 12 个月内将 Agent 应用于客户支持。Agent 正在重塑软件开发的每一个环节。',
category: 'application', tags: ['Agent', '智能体', 'MCP', 'A2A', 'Cursor', '2026'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 2 },
{ dim: 3, title: '多模态大模型:全能 AI 时代',
content: '多模态 AI 能够同时处理文本、图像、音频、视频等多种类型的数据。2026 年,多模态已成为大模型的标配能力。GPT-5.5 支持图像理解和生成,Gemini 3 Pro 直接打通 Gmail/Docs/Drive,豆包 Seed 2.0 Pro 进入全球前十。MiniMax M3 多模态大模型将在 WAIC 2026 首发。多模态技术的核心挑战在于如何在不同模态间建立统一的理解——让模型既"看懂"图片又"听懂"语音。这一能力的突破使 AI 更接近人类的感知方式。',
category: 'application', tags: ['多模态', '视觉', '语音', 'GPT-5', 'Gemini'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 3 },
{ dim: 3, title: '具身智能与人形机器人:从实验室到工厂',
content: '具身智能(Embodied AI)是让 AI 拥有物理身体并与世界交互的前沿方向。2026 年,中国工信部预计全年人形机器人整机产量突破 10 万台。WAIC 2026 上,多款人形机器人将现场演示"进厂上岗"。蚂蚁灵波开源 LingBot-World 2.0,实现了世界模型的小时级实时生成。具身智能的核心突破在于"世界模型"——让机器人理解物理世界的规律,而不只是执行预编程的动作。从仓库搬运到家庭服务,人形机器人正在从概念走向规模化商用。',
category: 'application', tags: ['具身智能', '人形机器人', '世界模型', '蚂蚁灵波'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 4 },
{ dim: 3, title: 'AI 编程革命:Cursor、Claude Code 与 Vibe Coding',
content: 'AI 编程工具正在重新定义软件开发。Cursor 在 2022 年成立,年收入突破 5 亿美元,每名员工创造 140 万美元收入。Claude Code 实现代理式编程,周限额提升 50%。"氛围编程"Vibe Coding)成为新趋势——开发者只需描述高层次目标,AI 自动完成多步骤执行。但推理成本暴涨 20 倍也带来了挑战,促使企业探索速率限制和效率优化。AI 编程正从简单的代码补全,演进到测试、QA、代码审查和调试的全链路智能化。',
category: 'application', tags: ['编程', 'Cursor', 'Claude Code', 'Vibe Coding', 'AI开发'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 5 },
{ dim: 3, title: '开源 vs 闭源:AI 模型的路线之争',
content: '2026 年,开源与闭源模型的分化愈加明显。闭源阵营(OpenAI、Anthropic、Google)保持性能领先,但在定价上居高不下。开源阵营(DeepSeek、Llama、Qwen、GLM)通过 MoE 架构和高效的训练方法,以 1/400 的成本实现了接近顶级闭源的性能。智谱 GLM-5 在 OpenRouter 上开源并登顶榜首。开源的优势在于透明度、可定制和低成本,但闭源产品在安全性和生态完整性上更有保障。这场拉锯战正在重塑 AI 的产业格局。',
category: 'concept', tags: ['开源', '闭源', 'DeepSeek', 'Llama', 'GLM'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 6 },
{ dim: 3, title: '100 万到 200 万 Token:上下文窗口之战',
content: '上下文窗口(Context Window)是决定大模型能力的关键指标之一。2026 年,上下文窗口的竞赛进入白热化:Gemini 3.5 Flash 支持 200 万 Token(约 150 万字),GPT-5.5 支持 128K TokenClaude Opus 4.7 支持 200K Token。更大的上下文窗口意味着模型可以一次处理整本书、整个代码库或完整的对话历史。Google 凭借 TPU 优势在上下文窗口上遥遥领先,但长上下文也带来推理成本的急剧上升。如何平衡长度与效率,是当前研究的重点。',
category: 'concept', tags: ['上下文窗口', 'Token', 'Gemini', 'GPT-5', 'Claude'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 7 },
// ===== D4: AI 学习 =====
{ dim: 4, title: '提示词工程入门:从 Prompt 到 Context Engineering',
content: '提示词工程(Prompt Engineering)是使用 AI 大模型的核心技能。2026 年,这门技术已从简单的"告诉 AI 做什么"进化为"指导 AI 如何思考"的 Context Engineering。核心原则包括:明确角色("你是一个资深 Python 工程师")、结构化输出(JSON/表格)、提供示例(Few-shot)、负面约束("不要使用全局变量")。进阶技巧包括:使用思考链(Chain-of-Thought)引导推理、用 XML 标签组织上下文、分配"思考 Token"。好的提示词可以让模型输出质量提升数倍。',
category: 'concept', tags: ['提示词工程', 'Prompt', 'Context Engineering', '入门'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 1 },
{ dim: 4, title: 'RAG 系统搭建指南:检索增强生成',
content: 'RAGRetrieval-Augmented Generation)是让大模型"接入"企业私有数据的核心技术。它的工作原理是:用户提问 → 从知识库检索相关文档 → 将文档和问题一起发给大模型 → 生成准确回答。RAG 的优势在于:知识实时更新、幻觉大幅降低、成本可控。一个典型的 RAG 系统包含:向量数据库(Milvus/Pinecone)、嵌入模型(Embedding)、分块策略、检索排序。2026 年的 RAG 已进化为 Agentic RAG——智能体自动决策何时检索、如何合并多源信息。',
category: 'concept', tags: ['RAG', '检索增强生成', '知识库', '向量数据库', 'LangChain'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 2 },
{ dim: 4, title: '模型微调实战:LoRA 与参数高效微调',
content: '微调(Fine-tuning)是让通用大模型适应特定领域任务的关键技术。全参数微调需要大量 GPU 资源,而参数高效微调(PEFT)大幅降低了门槛。其中最流行的是 LoRALow-Rank Adaptation),它在原始模型权重旁添加少量可训练参数(通常为原参数的 0.1-1%),显著降低显存需求。一张 A10(24G 显存)即可微调 7B 模型。常用的微调框架有 LLaMA-Factory(专业全面)和 Unsloth(快速省显存)。微调适用于:使模型掌握特定领域的术语和风格。',
category: 'concept', tags: ['微调', 'LoRA', 'PEFT', 'LLaMA-Factory', 'Unsloth'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 3 },
{ dim: 4, title: '大模型工具链全景:LangChain、LlamaIndex 与框架生态',
content: '大模型应用开发有一整套工具链。LangChain 是最流行的应用框架,提供了链式调用、工具集成、记忆管理、Agent 编排等功能。LlamaIndex 专注数据索引和 RAG,帮助开发者高效连接私域数据。LangGraph 用于构建有状态的多步骤 AI 工作流。此外还有:Chroma/Pinecone(向量数据库)、Ollama/vLLM(本地模型部署)、Dify/Coze(低代码平台)。掌握这些工具,是 AI 应用开发者的基本功。2026 年,MCP 协议的出现进一步简化了工具集成。',
category: 'application', tags: ['LangChain', 'LlamaIndex', 'LangGraph', '工具链', 'MCP'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 4 },
{ dim: 4, title: 'Agent 开发框架:从 LangGraph 到 MCP 协议',
content: '开发一个 AI Agent 需要综合运用多项技术。主流的 Agent 框架包括:LangGraph(状态化多步骤工作流)、CrewAI(多 Agent 协作)、AutoGen(微软多 Agent 框架)、OpenClaw(开源 Agent)。2026 年 Agent 开发的核心范式是"模型 + 工具 + 记忆 + 规划"。MCP(模型上下文协议)的标准化让 Agent 能够统一接入各种外部工具——从数据库到浏览器,从 API 到文件系统。A2A 协议则让不同 Agent 之间能够通信协作,构建真正的"Agent 互联网"。',
category: 'application', tags: ['Agent', 'LangGraph', 'MCP', 'A2A', '开发框架'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 5 },
{ dim: 4, title: 'AI 编程工具实战:从 Copilot 到 Claude Code',
content: 'AI 编程工具已成为开发者不可或缺的伙伴。GitHub Copilot 是最普及的 AI 代码补全工具,Claude Code 擅长深度代码分析和重构,Cursor 提供完整的 AI 原生 IDE 体验。2026 年的进阶用法包括:用 AI 生成完整的单元测试、自动代码审查、数据库查询优化、微服务架构设计。最佳实践是:先给 AI 提供项目结构和上下文("这个项目是 React + Go + MySQL"),明确约束条件("不要引入新依赖"),再逐步迭代。AI 编程不是取代开发者,而是让开发者的效率提升 10 倍。',
category: 'application', tags: ['Copilot', 'Claude Code', 'Cursor', 'AI编程', 'IDE'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 6 },
{ dim: 4, title: '模型评估与基准测试:如何选择最优模型',
content: '面对众多 AI 模型,如何做出正确选择?2026 年的标准评测体系包括:SWE-bench Verified(编程能力)、ARC-AGI-2(抽象推理)、AIME(数学能力)、LMArena(人类偏好评分)、GPQA(科学推理)。Claude Opus 4.7 在 SWE-bench 上以 87.6% 领先,GPT-5.5 在 ARC-AGI-2 上以 52.9% 拔得头筹,Gemini 3 Pro 在科学推理 GPQA 上得分 91.9%。选择模型的核心原则是:先明确需求场景(编程/推理/长文档/成本敏感),再参考对应基准测试,最后用实际场景验证。性价比也是关键——DeepSeek V4 以极低成本提供了实用级别的性能。',
category: 'concept', tags: ['基准测试', 'SWE-bench', 'ARC-AGI', '模型评估', '选型'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 7 },
// ===== D5: AI 趋势 =====
{ dim: 5, title: 'WAIC 2026:世界人工智能大会即将开幕',
content: '2026 世界人工智能大会(WAIC 2026)将于 7 月 17-20 日在上海举办,展览面积首次突破 10 万平方米,1100 余家企业参展,超 300 款产品全球首发。华为 Atlas 950 超节点、MiniMax M3 多模态大模型、全球首款 AI 智能体手机、多款人形机器人等重磅新品将集中亮相。大会还将举办首届 WAIC Academic 学术会议,9 位图灵奖、诺贝尔奖得主确认参会。',
category: 'application', tags: ['WAIC', '世界人工智能大会', '上海', '2026'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 1 },
{ dim: 5, title: '中国 AI 产业规模破万亿',
content: '2025 年,中国 AI 相关产业规模突破 1 万亿元,2026 年预计增速超 30%。国家发改委已布局 30 余个国家 AI 应用中试基地,央国企开放 1000 余个应用场景,重点行业 AI 渗透率突破 80%。中国日均 Token 调用量从 2024 年初的 1000 亿飙升至 2026 年 3 月的 140 万亿,增长 1400 倍。同时,中国企业正在从英伟达芯片转向国产 AI 芯片,未来 12 个月预计 46% 的 AI 加速器采购将投向国产产品。',
category: 'company', tags: ['产业规模', '万亿', '国产芯片', '渗透率', 'Token'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 2 },
{ dim: 5, title: '全球首款 AI 智能体手机将在 WAIC 首秀',
content: '据悉,全球首款 AI 智能体手机将在 WAIC 2026 上首次公开亮相。这款手机内置 Agent 操作系统,用户可以通过自然语言指令让手机自动完成复杂任务——比如"帮我订下周五去北京的机票和酒店"或"整理相册中最近三个月的照片并做成旅行视频"。这标志着端侧 AI 从"语音助手"进化到"智能体管家",AI 不再只是被动回答问题,而是能够主动规划并执行多步骤任务。',
category: 'product', tags: ['AI手机', '智能体', 'WAIC', '端侧AI'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 3 },
{ dim: 5, title: 'DeepSeek 秘密造芯:推理芯片项目已启动',
content: '据报道,DeepSeek 正在秘密自研推理芯片,项目已启动超过一年,旨在降低对英伟达的依赖。这一战略与华为、百度、阿里巴巴等中国科技巨头的自研芯片路线一致。在当前的全球芯片供应格局下,自研芯片不仅是降低成本的手段,更是保障技术自主可控的关键举措。DeepSeek 若能成功推出用于 AI 推理的自研芯片,将在成本上获得更大的竞争优势。',
category: 'company', tags: ['DeepSeek', '芯片', '推理', '英伟达', '国产'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 4 },
{ dim: 5, title: '联合国呼吁全球 AI 治理体系建设',
content: '联合国秘书长古特雷斯在首届 AI 治理全球对话会上呼吁对 AI 实施全球管控,表示"我们可能是最后一代能够设定人类与机器共存条件的人"。与此同时,AI 安全与治理成为 WAIC 2026 的重要主题,数十个国家将围绕 AI 伦理、数据安全、产业规则展开协商。各国正加速制定 AI 法规:欧盟 AI 法案已生效,中国持续推进算法备案与大模型登记制度,全球 AI 治理框架正在形成。',
category: 'company', tags: ['AI治理', '联合国', '法规', '安全', '伦理'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 5 },
{ dim: 5, title: '2026 AI Agent 六大趋势:从编程到行业落地',
content: 'CB Insights 发布的《AI Agent 报告》指出 2026 年六大趋势:语音 AI 加速崛起、AI 并购潮席卷智能体领域、利润压力从编程扩展到其他领域、智能体商业模式基础正在巩固、数据护城河之战重塑企业软件、智能体监控工具成为必需品。报告显示,82% 的企业将在 12 个月内把 Agent 应用于客户支持,AI Agent 初创公司平均成立仅 3.8 年即实现规模化收入。在医疗、金融等受监管行业,垂直 Agent 正在快速渗透。',
category: 'paper', tags: ['Agent', '趋势', 'CB Insights', '行业报告', '2026'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 6 },
{ dim: 5, title: '国家发改委发布"人工智能合作发展行动计划"',
content: '国家发改委将在 WAIC 2026 上发布"人工智能合作发展行动计划",涵盖智能算力普惠、开源生态共享、人工智能赋能、安全治理协作等 8 个方面。同时还将发布《中国智·惠世界》案例集,覆盖 20 多个国家的人工智能应用实践。这一系列举措表明,中国正在从 AI 技术应用者转向全球 AI 治理的重要参与者和贡献者。',
category: 'company', tags: ['发改委', '行动计划', '算力', '开源', '治理'], viewCount: 0, collectionCount: 0, isPremium: false, sortOrder: 7 },
]
const insertResult = await db.collection('knowledges').insertMany(knowledgeItems)
console.log(`📚 已插入 ${insertResult.insertedCount} 条 AI 知识内容`)
console.log(` D1(AI起源): ${knowledgeItems.filter(k => k.dim === 1).length}`)
console.log(` D2(AI发展): ${knowledgeItems.filter(k => k.dim === 2).length}`)
console.log(` D3(AI当前): ${knowledgeItems.filter(k => k.dim === 3).length}`)
console.log(` D4(AI学习): ${knowledgeItems.filter(k => k.dim === 4).length}`)
console.log(` D5(AI趋势): ${knowledgeItems.filter(k => k.dim === 5).length}`)
// ============================================================
// 3. 扩充趋势数据
// ============================================================
const now = new Date()
const trends = [
{ title: 'DeepSeek 秘密造芯:推理芯片项目已启动一年', summary: '据路透社报道,DeepSeek 正在自研推理芯片,旨在降低对英伟达的依赖,项目已启动超过一年。', source: '路透社', sourceUrl: 'https://reuters.com', category: '公司', tags: ['DeepSeek', '芯片'], hot: true, newsDate: new Date(now.getTime() - 1 * 3600000) },
{ title: '蚂蚁灵波开源 LingBot-World 2.0,世界模型小时级实时生成', summary: 'LingBot-World 2.0 实现了世界模型的小时级实时生成,是具身智能的重要突破。', source: '量子位', sourceUrl: 'https://qbitai.com', category: '产品', tags: ['蚂蚁灵波', '世界模型'], hot: false, newsDate: new Date(now.getTime() - 3 * 3600000) },
{ title: '阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷', summary: '阿里研究团队获得 ACL 2026 最佳资源论文奖,深入分析了当前 Agent 系统的结构性缺陷。', source: '机器之心', sourceUrl: 'https://jiqizhixin.com', category: '论文', tags: ['阿里', 'ACL', 'Agent'], hot: false, newsDate: new Date(now.getTime() - 5 * 3600000) },
{ title: 'WAIC 2026 即将开幕:超 300 款 AI 新品全球首发', summary: '2026 世界人工智能大会将于 7 月 17-20 日在上海举办,超 300 款产品全球首发,华为 Atlas 950、全球首款 AI 智能体手机将亮相。', source: '经济参考报', sourceUrl: 'https://jjckb.xinhuanet.com', category: '公司', tags: ['WAIC', '上海', 'AI大会'], hot: true, newsDate: new Date(now.getTime() - 6 * 3600000) },
{ title: '腾讯混元 Hy3 正式上线,Agent 任务解决率跃升至 90%', summary: '腾讯混元大模型 Hy3 版本正式上线,在 Agent 任务评测中解决率提升至 90%。', source: '腾讯云', sourceUrl: 'https://cloud.tencent.com', category: '产品', tags: ['腾讯', '混元', 'Agent'], hot: false, newsDate: new Date(now.getTime() - 8 * 3600000) },
{ title: '联合国呼吁全球 AI 治理:首届 AI 治理全球对话会召开', summary: '联合国秘书长古特雷斯呼吁对 AI 实施全球管控,强调"设定人类与机器共存的条件"刻不容缓。', source: 'IT之家', sourceUrl: 'https://ithome.com', category: '公司', tags: ['联合国', 'AI治理', '古特雷斯'], hot: false, newsDate: new Date(now.getTime() - 10 * 3600000) },
{ title: '2026 年中国人形机器人整机产量有望突破 10 万台', summary: '工信部表示,今年人形机器人全年整机产量有望突破 10 万台,规上工业企业 AI 应用普及率超 30%。', source: '新华社', sourceUrl: 'https://xinhuanet.com', category: '公司', tags: ['人形机器人', '工信部', '产量'], hot: true, newsDate: new Date(now.getTime() - 12 * 3600000) },
{ title: '国家发改委将发布"人工智能合作发展行动计划"', summary: '行动计划涵盖智能算力普惠、开源生态共享、安全治理协作等 8 个方面,将在 WAIC 2026 上正式发布。', source: '经济参考报', sourceUrl: 'https://jjckb.xinhuanet.com', category: '公司', tags: ['发改委', '行动计划', '算力'], hot: false, newsDate: new Date(now.getTime() - 14 * 3600000) },
{ title: '中国企业转向本土 AI 芯片,英伟达份额持续萎缩', summary: '彭博社报告显示,46% 的企业 AI 加速器采购将投向国产产品,80% 高管表示 AI 基础设施投入超出预算。', source: '彭博社', sourceUrl: 'https://bloomberg.com', category: '公司', tags: ['芯片', '英伟达', '国产替代'], hot: false, newsDate: new Date(now.getTime() - 16 * 3600000) },
{ title: '云从科技推出政务大模型一体机', summary: '云从科技发布政务大模型一体机,内置 DeepSeek R1 蒸馏模型,助力政府部门本地化部署 AI 能力。', source: '36氪', sourceUrl: 'https://36kr.com', category: '产品', tags: ['云从科技', '政务', '一体机'], hot: false, newsDate: new Date(now.getTime() - 18 * 3600000) },
{ title: '全球首款 AI 智能体手机将在 WAIC 2026 首秀', summary: '这款手机内置 Agent 操作系统,用户可通过自然语言指令让手机自动完成复杂任务,标志着端侧 AI 从助手进化为管家。', source: '界面新闻', sourceUrl: 'https://jiemian.com', category: '产品', tags: ['AI手机', '智能体', 'WAIC'], hot: false, newsDate: new Date(now.getTime() - 20 * 3600000) },
{ title: '2026 AI Agent 六大趋势发布:语音 AI 加速崛起', summary: 'CB Insights 报告显示 82% 企业将在 12 个月内将 Agent 用于客户支持,Agent 初创公司平均成立 3.8 年即实现规模化收入。', source: '36氪', sourceUrl: 'https://36kr.com', category: '论文', tags: ['Agent', '趋势', '报告'], hot: false, newsDate: new Date(now.getTime() - 22 * 3600000) },
]
// Clear old trends (optional)
const oldTrends = await db.collection('trends').countDocuments()
if (oldTrends > 0) {
await db.collection('trends').deleteMany({})
console.log(`🗑️ 已清空 ${oldTrends} 条旧趋势数据`)
}
const trendResult = await db.collection('trends').insertMany(trends.map(t => ({
...t,
picked: false,
viewCount: 0,
status: 'published',
createdAt: new Date(),
updatedAt: new Date()
})))
console.log(`📡 已插入 ${trendResult.insertedCount} 条趋势数据`)
// ============================================================
// 4. 最终统计
// ============================================================
const knowledgeTotal = await db.collection('knowledges').countDocuments()
const trendTotal = await db.collection('trends').countDocuments()
console.log(`\n📊 最终统计:`)
console.log(` 知识库: ${knowledgeTotal}`)
console.log(` 趋势: ${trendTotal}`)
// Per-dimension count
for (let d = 1; d <= 5; d++) {
const n = await db.collection('knowledges').countDocuments({ dim: d })
console.log(` D${d}: ${n}`)
}
await mongoose.disconnect()
console.log('\n✅ 内容丰富完成!')
}
main().catch(err => {
console.error('❌ 脚本执行失败:', err)
process.exit(1)
})
+83
View File
@@ -0,0 +1,83 @@
/**
* 更新商品定价为 AI 维度主题
* 用法: node src/scripts/update-products.js
*/
require('dotenv').config();
const mongoose = require('mongoose');
const ShopItem = require('../models/ShopItem');
const DB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj';
async function main() {
await mongoose.connect(DB_URI);
console.log('数据库已连接');
// 清空旧商品
await ShopItem.deleteMany({});
console.log('旧商品已清空');
const products = [
{
itemId: 'pro_monthly',
name: 'AI维度 Pro (月付)',
description: '无限 AI 问答 + 全部知识解锁 + 趋势跟踪',
price: 1990, // 分 = ¥19.90
originalPrice: 2990,
type: 'subscription',
duration: 30,
sortOrder: 1,
status: 'active'
},
{
itemId: 'pro_quarterly',
name: 'AI维度 Pro (季付)',
description: '无限 AI 问答 + 全部知识解锁 + 趋势跟踪,享 8 折',
price: 4990, // ¥49.90
originalPrice: 5970,
type: 'subscription',
duration: 90,
sortOrder: 2,
status: 'active'
},
{
itemId: 'vip_monthly',
name: 'AI维度 VIP (月付)',
description: 'Pro + 工具教程 + 学习路径定制 + 社区 + 优先支持',
price: 3990, // ¥39.90
originalPrice: 5990,
type: 'subscription',
duration: 30,
sortOrder: 3,
status: 'active'
},
{
itemId: 'vip_quarterly',
name: 'AI维度 VIP (季付)',
description: 'VIP全套权益,享 8 折优惠',
price: 9990, // ¥99.90
originalPrice: 11970,
type: 'subscription',
duration: 90,
sortOrder: 4,
status: 'active'
},
{
itemId: 'noad',
name: '去广告',
description: '永久去除广告,纯净体验',
price: 1200, // ¥12.00
type: 'noad',
duration: 0,
sortOrder: 5,
status: 'active'
}
];
const result = await ShopItem.insertMany(products);
console.log(`✅ 已插入 ${result.length} 个商品:`);
result.forEach(p => console.log(` - ${p.name}: ¥${(p.price/100).toFixed(2)}`));
await mongoose.disconnect();
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
+78
View File
@@ -0,0 +1,78 @@
/**
* 趋势数据更新脚本(兼容 Hermes cron 调用)
* 从 AI 新闻源获取最新趋势并写入数据库
*
* 用法: node src/scripts/update-trends.js
* 或由 Hermes cron 每周调用
*/
require('dotenv').config();
const mongoose = require('mongoose');
const Trend = require('../models/Trend');
const DB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj';
// 示例新闻数据(生产环境可从 RSS/API 获取)
const sampleTrends = [
{
title: 'DeepSeek 秘密造芯:推理芯片项目已启动一年',
summary: '据报道,DeepSeek 正在自研推理芯片,旨在降低对英伟达的依赖。',
source: '路透社', sourceUrl: 'https://reuters.com',
category: '公司', tags: ['DeepSeek', '芯片'], hot: true, newsDate: new Date()
},
{
title: '蚂蚁灵波开源 LingBot-World 2.0,世界模型小时级实时生成',
summary: 'LingBot-World 2.0 实现了世界模型的小时级实时生成,是具身智能的重要突破。',
source: '量子位', sourceUrl: 'https://qbitai.com',
category: '产品', tags: ['蚂蚁灵波', '世界模型'], newsDate: new Date()
},
{
title: '阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷',
summary: '阿里研究团队获得 ACL 2026 最佳资源论文奖,深入分析了当前 Agent 系统的结构性缺陷。',
source: '机器之心', sourceUrl: 'https://jiqizhixin.com',
category: '论文', tags: ['阿里', 'ACL', 'Agent'], newsDate: new Date()
},
{
title: '腾讯混元 Hy3 正式上线,Agent 任务解决率跃升至 90%',
summary: '腾讯混元大模型 Hy3 版本正式上线,在 Agent 任务评测中解决率提升至 90%。',
source: '腾讯云', sourceUrl: 'https://cloud.tencent.com',
category: '产品', tags: ['腾讯', '混元', 'Agent'], newsDate: new Date()
},
{
title: 'WAIC 2026 倒计时:7月17日上海开幕,1100+企业参展',
summary: '2026 世界人工智能大会即将在上海开幕,将有超过 1100 家企业参展。',
source: '量子位', sourceUrl: 'https://qbitai.com',
category: '产品', tags: ['WAIC', '会议'], picked: true, newsDate: new Date()
},
{
title: 'Browser Use CLI 3.0 发布:体积缩小 6 倍,Token 消耗大幅降低',
summary: 'Browser Use 工具 CLI 3.0 版本发布,体积缩小 6 倍,Token 消耗显著降低。',
source: 'GitHub', sourceUrl: 'https://github.com',
category: '工具', tags: ['Browser Use', '开源'], newsDate: new Date()
}
];
async function main() {
await mongoose.connect(DB_URI);
console.log('数据库已连接');
// 清除 7 天前的旧数据
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const deleted = await Trend.deleteMany({ newsDate: { $lt: weekAgo } });
console.log(`已清理 ${deleted.deletedCount} 条旧数据`);
// 插入新数据(去重)
let inserted = 0;
for (const item of sampleTrends) {
const exists = await Trend.findOne({ title: item.title });
if (!exists) {
await new Trend({ ...item, status: 'published' }).save();
inserted++;
}
}
console.log(`新增 ${inserted} 条趋势数据`);
await mongoose.disconnect();
console.log('完成');
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
+96
View File
@@ -0,0 +1,96 @@
const fs = require('fs')
const path = require('path')
/**
* 日志级别
*/
const LOG_LEVELS = {
debug: 0,
info: 1,
warn: 2,
error: 3
}
/**
* 日志器
*/
class Logger {
constructor(level = 'info', logDir = './logs') {
this.level = LOG_LEVELS[level] || LOG_LEVELS.info
this.logDir = logDir
// 确保日志目录存在
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
}
/**
* 格式化日志消息
*/
formatMessage(level, message, meta = {}) {
const timestamp = new Date().toISOString()
const metaStr = Object.keys(meta).length > 0 ? JSON.stringify(meta) : ''
return `[${timestamp}] [${level.toUpperCase()}] ${message} ${metaStr}\n`
}
/**
* 写入日志文件
*/
writeToFile(level, message, meta) {
const date = new Date().toISOString().split('T')[0]
const logFile = path.join(this.logDir, `${date}.log`)
const logMessage = this.formatMessage(level, message, meta)
fs.appendFile(logFile, logMessage, (err) => {
if (err) console.error('写入日志失败:', err)
})
}
/**
* 记录日志
*/
log(level, message, meta = {}) {
if (LOG_LEVELS[level] < this.level) return
const formattedMessage = this.formatMessage(level, message, meta)
// 控制台输出
switch (level) {
case 'error':
console.error(formattedMessage.trim())
break
case 'warn':
console.warn(formattedMessage.trim())
break
default:
console.log(formattedMessage.trim())
}
// 文件输出(生产环境)
if (process.env.NODE_ENV === 'production') {
this.writeToFile(level, message, meta)
}
}
debug(message, meta) {
this.log('debug', message, meta)
}
info(message, meta) {
this.log('info', message, meta)
}
warn(message, meta) {
this.log('warn', message, meta)
}
error(message, meta) {
this.log('error', message, meta)
}
}
// 导出单例
const logger = new Logger(process.env.LOG_LEVEL || 'info')
module.exports = logger
+95
View File
@@ -0,0 +1,95 @@
/**
* 统一响应格式
*/
class ApiResponse {
/**
* 成功响应
*/
static success(res, data = null, message = '操作成功', statusCode = 200) {
return res.status(statusCode).json({
success: true,
message,
data,
timestamp: new Date().toISOString()
})
}
/**
* 错误响应
*/
static error(res, error = '操作失败', statusCode = 400, details = null) {
return res.status(statusCode).json({
success: false,
error,
details,
timestamp: new Date().toISOString()
})
}
/**
* 分页响应
*/
static paginated(res, items, pagination, message = '查询成功') {
return res.status(200).json({
success: true,
message,
data: {
items,
pagination: {
page: pagination.page,
limit: pagination.limit,
total: pagination.total,
totalPages: Math.ceil(pagination.total / pagination.limit)
}
},
timestamp: new Date().toISOString()
})
}
/**
* 未授权响应
*/
static unauthorized(res, error = '未授权访问') {
return res.status(401).json({
success: false,
error,
timestamp: new Date().toISOString()
})
}
/**
* 禁止访问响应
*/
static forbidden(res, error = '权限不足') {
return res.status(403).json({
success: false,
error,
timestamp: new Date().toISOString()
})
}
/**
* 资源不存在响应
*/
static notFound(res, error = '资源不存在') {
return res.status(404).json({
success: false,
error,
timestamp: new Date().toISOString()
})
}
/**
* 服务器错误响应
*/
static serverError(res, error = '服务器内部错误') {
console.error('Server Error:', error)
return res.status(500).json({
success: false,
error,
timestamp: new Date().toISOString()
})
}
}
module.exports = ApiResponse
+235
View File
@@ -0,0 +1,235 @@
const axios = require('axios')
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
/**
* 微信小程序 API 工具类
*/
class WeChatService {
constructor() {
this.appId = process.env.WX_APPID
this.secret = process.env.WX_SECRET
this.mchId = process.env.WX_PAY_MCH_ID
this.apiKey = process.env.WX_PAY_API_KEY // APIv3 Key
this.serialNo = process.env.WX_PAY_SERIAL_NO
this.notifyUrl = process.env.WX_PAY_NOTIFY_URL
// 加载证书
try {
const certPath = path.join(__dirname, '../../certs/apiclient_cert.pem')
const keyPath = path.join(__dirname, '../../certs/apiclient_key.pem')
this.privateKey = fs.readFileSync(keyPath)
this.certificate = fs.readFileSync(certPath)
} catch (error) {
console.error('微信支付证书加载失败,请检查 certs 目录:', error.message)
}
// 缓存 access_token
this.accessToken = null
this.tokenExpireTime = 0
}
/**
* 获取 Access Token
*/
async getAccessToken() {
// 检查缓存
if (this.accessToken && Date.now() < this.tokenExpireTime) {
return this.accessToken
}
try {
const response = await axios.get('https://api.weixin.qq.com/cgi-bin/token', {
params: {
grant_type: 'client_credential',
appid: this.appId,
secret: this.secret
}
})
if (response.data.errcode) {
throw new Error(response.data.errmsg)
}
this.accessToken = response.data.access_token
this.tokenExpireTime = Date.now() + (response.data.expires_in - 300) * 1000 // 提前5分钟过期
return this.accessToken
} catch (error) {
console.error('获取 Access Token 失败:', error)
throw error
}
}
/**
* 通过 code 换取 openid 和 session_key
*/
async code2Session(code) {
try {
const response = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
params: {
appid: this.appId,
secret: this.secret,
js_code: code,
grant_type: 'authorization_code'
}
})
if (response.data.errcode && response.data.errcode !== 0) {
throw new Error(response.data.errmsg)
}
return {
openid: response.data.openid,
sessionKey: response.data.session_key,
unionid: response.data.unionid
}
} catch (error) {
console.error('code2Session 失败:', error)
throw error
}
}
/**
* 生成随机字符串
*/
generateNonceStr(length = 32) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
/**
* 生成订单号
*/
generateOrderNo(prefix = 'WD') {
const date = new Date()
const dateStr = date.getFullYear().toString() +
String(date.getMonth() + 1).padStart(2, '0') +
String(date.getDate()).padStart(2, '0')
const randomStr = Math.random().toString(36).substr(2, 6).toUpperCase()
return `${prefix}${dateStr}${randomStr}`
}
/**
* 生成签名
*/
generateSign(method, url, timestamp, nonceStr, body = '') {
const signStr = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`
const sign = crypto.createSign('RSA-SHA256')
sign.update(signStr)
sign.end()
return sign.sign(this.privateKey, 'base64')
}
/**
* 创建支付订单(小程序支付 - 真实流程)
*/
async createPaymentOrder(orderData) {
const { openid, orderNo, body, amount } = orderData
const nonceStr = this.generateNonceStr()
const timestamp = Math.floor(Date.now() / 1000).toString()
// 1. 统一下单 (JSAPI)
const url = '/v3/pay/transactions/jsapi'
const fullUrl = `https://api.mch.weixin.qq.com${url}`
const orderParams = {
appid: this.appId,
mchid: this.mchId,
description: body,
out_trade_no: orderNo,
notify_url: this.notifyUrl,
amount: {
total: Math.round(amount * 100), // 转换为分
currency: 'CNY'
},
payer: {
openid: openid
}
}
try {
// 注意:实际生产环境需要处理网络请求和签名
// const response = await axios.post(fullUrl, orderParams, { ... })
// const prepayId = response.data.prepay_id
// 模拟获取 prepay_id
const prepayId = `wx_mock_${Date.now()}`
// 2. 生成前端调起支付的签名
const paySignStr = `${this.appId}\n${timestamp}\n${nonceStr}\nprepay_id=${prepayId}\n`
const paySign = crypto.createSign('RSA-SHA256')
paySign.update(paySignStr)
paySign.end()
const signature = paySign.sign(this.privateKey, 'base64')
return {
appId: this.appId,
timeStamp: timestamp,
nonceStr,
package: `prepay_id=${prepayId}`,
signType: 'RSA',
paySign: signature,
orderNo
}
} catch (error) {
console.error('微信支付下单失败:', error)
throw error
}
}
/**
* 申请退款
*/
async refundOrder(transactionId, outRefundNo, totalAmount, refundAmount, reason = '用户申请退款') {
const url = '/v3/refund/domestic/refunds'
const fullUrl = `https://api.mch.weixin.qq.com${url}`
const nonceStr = this.generateNonceStr()
const timestamp = Math.floor(Date.now() / 1000).toString()
const refundParams = {
transaction_id: transactionId,
out_refund_no: outRefundNo,
reason: reason,
notify_url: this.notifyUrl.replace('/callback', '/refund-callback'), // 假设退款回调地址
amount: {
refund: Math.round(refundAmount * 100),
total: Math.round(totalAmount * 100),
currency: 'CNY'
}
}
try {
// 注意:实际生产环境需要处理网络请求和签名
// const sign = this.generateSign('POST', url, timestamp, nonceStr, JSON.stringify(refundParams))
// const response = await axios.post(fullUrl, refundParams, { ... })
console.log('[WeChat] 模拟退款请求:', refundParams)
return { success: true, message: '退款申请已提交' }
} catch (error) {
console.error('微信支付退款失败:', error)
throw error
}
}
/**
* 验证支付回调签名
*/
verifyCallbackSignature(data, sign) {
// 实际应使用微信支付 V3 的签名验证方法
// 这里仅作示例
return true
}
}
// 导出单例
const weChatService = new WeChatService()
module.exports = weChatService
+211
View File
@@ -0,0 +1,211 @@
/**
* 认证 API 集成测试
*/
const request = require('supertest')
const mongoose = require('mongoose')
const app = require('../../src/index')
const User = require('../../src/models/User')
const Admin = require('../../src/models/Admin')
describe('Auth API', () => {
beforeAll(async () => {
// 连接测试数据库
const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/wdkj'
if (mongoose.connection.readyState === 0) {
await mongoose.connect(mongoURI)
}
})
afterAll(async () => {
// 清理并关闭连接
if (mongoose.connection.readyState !== 0) {
await mongoose.connection.dropDatabase()
await mongoose.connection.close()
}
})
beforeEach(async () => {
// 每个测试前清空集合
await User.deleteMany({})
await Admin.deleteMany({})
})
describe('POST /api/auth/login', () => {
test('新用户应该成功注册', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
openid: 'new_user_openid',
userInfo: {
nickName: '新用户',
avatarUrl: 'https://example.com/avatar.jpg'
}
})
expect(response.status).toBe(200)
expect(response.body.success).toBe(true)
expect(response.body.data.isNewUser).toBe(true)
expect(response.body.data.openid).toBe('new_user_openid')
expect(response.body.data.token).toBeDefined()
})
test('老用户应该成功登录', async () => {
// 先创建一个用户
await User.create({
openid: 'existing_user_openid',
nickName: '老用户',
totalScore: 100
})
const response = await request(app)
.post('/api/auth/login')
.send({
openid: 'existing_user_openid',
userInfo: {
nickName: '老用户新名字'
}
})
expect(response.status).toBe(200)
expect(response.body.success).toBe(true)
expect(response.body.data.isNewUser).toBe(false)
expect(response.body.data.openid).toBe('existing_user_openid')
})
test('缺少 openid 应该返回错误', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
userInfo: {
nickName: '测试用户'
}
})
expect(response.status).toBe(400)
expect(response.body.success).toBe(false)
})
})
describe('POST /api/auth/admin/login', () => {
beforeEach(async () => {
// 创建测试管理员
const admin = new Admin({
username: 'testadmin',
password: 'password123',
role: 'super_admin',
status: 'active'
})
await admin.save()
})
test('管理员应该成功登录', async () => {
const response = await request(app)
.post('/api/auth/admin/login')
.send({
username: 'testadmin',
password: 'password123'
})
expect(response.status).toBe(200)
expect(response.body.success).toBe(true)
expect(response.body.data.token).toBeDefined()
expect(response.body.data.username).toBe('testadmin')
expect(response.body.data.role).toBe('super_admin')
})
test('错误的密码应该返回401', async () => {
const response = await request(app)
.post('/api/auth/admin/login')
.send({
username: 'testadmin',
password: 'wrongpassword'
})
expect(response.status).toBe(401)
expect(response.body.success).toBe(false)
})
test('不存在的用户应该返回401', async () => {
const response = await request(app)
.post('/api/auth/admin/login')
.send({
username: 'nonexistent',
password: 'password123'
})
expect(response.status).toBe(401)
expect(response.body.success).toBe(false)
})
test('禁用的账户应该返回403', async () => {
// 创建禁用的管理员
const disabledAdmin = new Admin({
username: 'disabled',
password: 'password123',
role: 'viewer',
status: 'inactive'
})
await disabledAdmin.save()
const response = await request(app)
.post('/api/auth/admin/login')
.send({
username: 'disabled',
password: 'password123'
})
expect(response.status).toBe(403)
expect(response.body.success).toBe(false)
})
})
describe('GET /api/auth/me', () => {
let userToken
let testUser
beforeEach(async () => {
// 创建测试用户并获取token
testUser = await User.create({
openid: 'test_user_openid',
nickName: '测试用户',
totalScore: 500
})
// 登录获取token
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ openid: 'test_user_openid' })
userToken = loginResponse.body.data.token
})
test('应该返回当前用户信息', async () => {
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${userToken}`)
expect(response.status).toBe(200)
expect(response.body.success).toBe(true)
expect(response.body.data.openid).toBe('test_user_openid')
expect(response.body.data.nickName).toBe('测试用户')
})
test('缺少 token 应该返回401', async () => {
const response = await request(app)
.get('/api/auth/me')
expect(response.status).toBe(401)
expect(response.body.success).toBe(false)
})
test('不存在的用户应该返回401', async () => {
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', 'Bearer invalid_token')
expect(response.status).toBe(401)
expect(response.body.success).toBe(false)
})
})
})
+205
View File
@@ -0,0 +1,205 @@
/**
* 用户 API 集成测试
*/
const request = require('supertest')
const mongoose = require('mongoose')
const app = require('../../src/index')
const User = require('../../src/models/User')
const jwt = require('jsonwebtoken')
describe('User API', () => {
let testUser
let authToken
beforeAll(async () => {
const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/wdkj'
if (mongoose.connection.readyState === 0) {
await mongoose.connect(mongoURI)
}
})
afterAll(async () => {
if (mongoose.connection.readyState !== 0) {
await mongoose.connection.dropDatabase()
await mongoose.connection.close()
}
})
beforeEach(async () => {
await User.deleteMany({})
// 创建测试用户
testUser = await User.create({
openid: 'test_user_openid',
nickName: '测试用户',
avatarUrl: 'https://example.com/avatar.jpg',
exploreData: {
dim1: { bestScore: 100, completed: true },
dim2: { bestArea: 200, completed: false }
},
totalScore: 300,
unlockedDims: [1, 2]
})
// 生成认证 Token
authToken = jwt.sign(
{ userId: testUser._id.toString(), openid: testUser.openid },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
)
})
describe('GET /api/user/progress', () => {
test('应该返回用户探索进度', async () => {
const response = await request(app)
.get('/api/user/progress')
.set('Authorization', `Bearer ${authToken}`)
expect(response.status).toBe(200)
expect(response.body.success).toBe(true)
expect(response.body.data.exploreData).toBeDefined()
expect(response.body.data.exploreData.dim1.bestScore).toBe(100)
expect(response.body.data.exploreData.dim2.bestArea).toBe(200)
})
test('不存在的用户应该返回404', async () => {
const response = await request(app)
.get('/api/user/progress')
.set('Authorization', 'Bearer invalid_token')
expect(response.status).toBe(401)
})
})
describe('POST /api/user/progress', () => {
test('应该成功保存探索进度', async () => {
const response = await request(app)
.post('/api/user/progress')
.set('Authorization', `Bearer ${authToken}`)
.send({
dim: 1,
score: 150,
collectedEggs: 5
})
expect(response.status).toBe(200)
expect(response.body.success).toBe(true)
// 验证数据库更新
const updatedUser = await User.findOne({ openid: 'test_user_openid' })
expect(updatedUser.exploreData.dim1.bestScore).toBe(150)
expect(updatedUser.exploreData.dim1.collectedEggs).toBe(5)
})
test('新用户应该自动解锁维度', async () => {
const response = await request(app)
.post('/api/user/progress')
.set('Authorization', `Bearer ${authToken}`)
.send({
dim: 3,
score: 100
})
expect(response.status).toBe(200)
const updatedUser = await User.findOne({ openid: 'test_user_openid' })
expect(updatedUser.unlockedDims).toContain(3)
})
})
describe('GET /api/user/leaderboard', () => {
beforeEach(async () => {
// 创建多个测试用户
await User.create([
{ openid: 'user1', nickName: '用户1', totalScore: 500, dim1Score: 200 },
{ openid: 'user2', nickName: '用户2', totalScore: 300, dim1Score: 150 },
{ openid: 'user3', nickName: '用户3', totalScore: 400, dim1Score: 100 }
])
})
test('应该返回总分排行榜', async () => {
const response = await request(app)
.get('/api/user/leaderboard')
.query({ limit: 10 })
expect(response.status).toBe(200)
expect(response.body.success).toBe(true)
expect(response.body.data).toHaveLength(4) // 包含testUser
expect(response.body.data[0].totalScore).toBeGreaterThanOrEqual(
response.body.data[1].totalScore
)
})
test('应该返回维度排行榜', async () => {
const response = await request(app)
.get('/api/user/leaderboard')
.query({ dim: 1, limit: 10 })
expect(response.status).toBe(200)
expect(response.body.data[0].dim1Score).toBe(200)
})
test('应该限制返回数量', async () => {
const response = await request(app)
.get('/api/user/leaderboard')
.query({ limit: 2 })
expect(response.body.data).toHaveLength(2)
})
})
describe('PUT /api/user/info', () => {
test('应该成功更新用户信息', async () => {
const response = await request(app)
.put('/api/user/info')
.set('Authorization', `Bearer ${authToken}`)
.send({
nickName: '新名字',
avatarUrl: 'https://example.com/new-avatar.jpg'
})
expect(response.status).toBe(200)
expect(response.body.success).toBe(true)
const updatedUser = await User.findOne({ openid: 'test_user_openid' })
expect(updatedUser.nickName).toBe('新名字')
expect(updatedUser.avatarUrl).toBe('https://example.com/new-avatar.jpg')
})
})
describe('POST /api/user/collect-knowledge', () => {
test('应该成功收藏知识', async () => {
const knowledgeId = new mongoose.Types.ObjectId()
const response = await request(app)
.post('/api/user/collect-knowledge')
.set('Authorization', `Bearer ${authToken}`)
.send({ knowledgeId: knowledgeId.toString() })
expect(response.status).toBe(200)
expect(response.body.success).toBe(true)
const updatedUser = await User.findOne({ openid: 'test_user_openid' })
expect(updatedUser.collectedKnowledge).toContainEqual(knowledgeId)
})
test('重复收藏应该返回已存在', async () => {
const knowledgeId = new mongoose.Types.ObjectId()
// 第一次收藏
await request(app)
.post('/api/user/collect-knowledge')
.set('Authorization', `Bearer ${authToken}`)
.send({ knowledgeId: knowledgeId.toString() })
// 第二次收藏
const response = await request(app)
.post('/api/user/collect-knowledge')
.set('Authorization', `Bearer ${authToken}`)
.send({ knowledgeId: knowledgeId.toString() })
expect(response.status).toBe(200)
})
})
})
+27
View File
@@ -0,0 +1,27 @@
/**
* Jest 测试设置
*/
// 增加测试超时时间
jest.setTimeout(30000)
// 全局清理
afterAll(async () => {
// 关闭所有数据库连接
const mongoose = require('mongoose')
if (mongoose.connection.readyState !== 0) {
await mongoose.connection.close()
}
})
// 抑制控制台日志(测试时)
if (process.env.SUPPRESS_LOGS) {
global.console = {
...console,
log: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
}
}
+166
View File
@@ -0,0 +1,166 @@
/**
* 用户模型单元测试
*/
const mongoose = require('mongoose')
const User = require('../../src/models/User')
describe('User Model', () => {
beforeAll(async () => {
// 连接测试数据库
const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/wdkj'
await mongoose.connect(mongoURI)
})
afterAll(async () => {
// 清理并关闭连接
await mongoose.connection.dropDatabase()
await mongoose.connection.close()
})
beforeEach(async () => {
// 每个测试前清空users集合
await User.deleteMany({})
})
describe('用户创建', () => {
test('应该成功创建新用户', async () => {
const userData = {
openid: 'test_openid_123',
nickName: '测试用户',
avatarUrl: 'https://example.com/avatar.jpg'
}
const user = new User(userData)
const savedUser = await user.save()
expect(savedUser._id).toBeDefined()
expect(savedUser.openid).toBe(userData.openid)
expect(savedUser.nickName).toBe(userData.nickName)
expect(savedUser.avatarUrl).toBe(userData.avatarUrl)
expect(savedUser.unlockedDims).toContain(1)
expect(savedUser.totalScore).toBe(0)
})
test('openid 是必需的', async () => {
const user = new User({
nickName: '测试用户'
})
await expect(user.save()).rejects.toThrow()
})
test('openid 必须唯一', async () => {
const userData = {
openid: 'unique_openid',
nickName: '用户1'
}
await new User(userData).save()
const duplicateUser = new User({
openid: 'unique_openid',
nickName: '用户2'
})
await expect(duplicateUser.save()).rejects.toThrow()
})
test('默认昵称应该是"星辰旅行者"', async () => {
const user = new User({ openid: 'test_openid' })
await user.save()
expect(user.nickName).toBe('星辰旅行者')
})
})
describe('探索数据', () => {
test('应该正确计算总分', async () => {
const user = new User({
openid: 'test_openid',
exploreData: {
dim1: { bestScore: 100 },
dim2: { bestArea: 200 },
dim3: { bestScore: 150 }
}
})
await user.save()
expect(user.totalScore).toBe(450)
expect(user.dim1Score).toBe(100)
expect(user.dim2Score).toBe(200)
expect(user.dim3Score).toBe(150)
})
test('未解锁维度分数应为0', async () => {
const user = new User({
openid: 'test_openid',
exploreData: {
dim1: { bestScore: 100 }
}
})
await user.save()
expect(user.dim2Score).toBe(0)
expect(user.dim3Score).toBe(0)
expect(user.dim4Score).toBe(0)
expect(user.dim5Score).toBe(0)
})
})
describe('虚拟字段', () => {
test('isNew 应该对新用户返回 true', async () => {
const user = new User({ openid: 'test_openid' })
await user.save()
expect(user.isNew).toBe(true)
})
test('isNew 应该对老用户返回 false', async () => {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 2)
const user = new User({
openid: 'test_openid',
createdAt: yesterday
})
await user.save()
expect(user.isNew).toBe(false)
})
})
describe('静态方法', () => {
test('getLeaderboard 应该返回排行榜', async () => {
// 创建测试用户
await User.create([
{ openid: 'user1', totalScore: 300, dim1Score: 100 },
{ openid: 'user2', totalScore: 500, dim1Score: 200 },
{ openid: 'user3', totalScore: 400, dim1Score: 150 }
])
const leaderboard = await User.getLeaderboard(null, 10)
expect(leaderboard).toHaveLength(3)
expect(leaderboard[0].totalScore).toBe(500)
expect(leaderboard[1].totalScore).toBe(400)
expect(leaderboard[2].totalScore).toBe(300)
})
test('getLeaderboard 应该支持按维度排序', async () => {
await User.create([
{ openid: 'user1', dim1Score: 100, totalScore: 100 },
{ openid: 'user2', dim1Score: 200, totalScore: 200 },
{ openid: 'user3', dim1Score: 150, totalScore: 150 }
])
const leaderboard = await User.getLeaderboard(1, 10)
expect(leaderboard[0].dim1Score).toBe(200)
expect(leaderboard[1].dim1Score).toBe(150)
expect(leaderboard[2].dim1Score).toBe(100)
})
})
})