新功能: - 创建AI聊天助手组件(AIChatWidget) - 支持文字+图片多模态对话 - 集成Gemini AI API - 支持图片编辑和内容生成 - 实时对话历史管理 - 优雅的UI设计,支持黑夜模式 技术实现: - 后端API代理Gemini API(/api/ai-chat) - 支持最多5张图片上传 - 流式响应处理 - 对话历史管理 - Base64图片处理 配置: - 在.env中添加Gemini API配置 - GEMINI_API_KEY - GEMINI_API_BASE_URL - GEMINI_MODEL 替换原弹幕功能,提供更强大的AI助手体验!
248 lines
5.1 KiB
JavaScript
Executable File
248 lines
5.1 KiB
JavaScript
Executable File
const mongoose = require('mongoose');
|
||
const bcrypt = require('bcryptjs');
|
||
const jwt = require('jsonwebtoken');
|
||
const validator = require('validator');
|
||
|
||
// 检查模型是否已经存在,如果存在则直接返回
|
||
if (mongoose.models.User) {
|
||
module.exports = mongoose.models.User;
|
||
return;
|
||
}
|
||
|
||
const userSchema = new mongoose.Schema({
|
||
username: {
|
||
type: String,
|
||
required: true,
|
||
trim: true,
|
||
unique: true,
|
||
minlength: 2,
|
||
maxlength: 30,
|
||
validate: {
|
||
validator: function(v) {
|
||
// 允许中文、英文字母、数字和下划线
|
||
// 中文字符范围:\u4e00-\u9fa5
|
||
return /^[\u4e00-\u9fa5a-zA-Z0-9_]+$/.test(v);
|
||
},
|
||
message: props => `${props.value} 不是有效的用户名。用户名只能包含中文、英文字母、数字和下划线。`
|
||
}
|
||
},
|
||
email: {
|
||
type: String,
|
||
required: true,
|
||
unique: true,
|
||
trim: true,
|
||
lowercase: true,
|
||
validate(value) {
|
||
if (!validator.isEmail(value)) {
|
||
throw new Error('邮箱格式不正确');
|
||
}
|
||
}
|
||
},
|
||
password: {
|
||
type: String,
|
||
required: true,
|
||
minlength: 6,
|
||
trim: true
|
||
},
|
||
role: {
|
||
type: String,
|
||
enum: ['user', 'admin', 'vip', 'disabled'],
|
||
default: 'user'
|
||
},
|
||
status: {
|
||
type: String,
|
||
enum: ['pending', 'active', 'suspended'],
|
||
default: 'pending'
|
||
},
|
||
avatar: {
|
||
type: String,
|
||
default: '/images/default-avatar.png'
|
||
},
|
||
credits: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
usageHistory: [{
|
||
feature: {
|
||
type: String,
|
||
required: true
|
||
},
|
||
timestamp: {
|
||
type: Date,
|
||
default: Date.now
|
||
},
|
||
creditsUsed: {
|
||
type: Number,
|
||
default: 1
|
||
}
|
||
}],
|
||
tokens: [{
|
||
token: {
|
||
type: String,
|
||
required: true
|
||
}
|
||
}],
|
||
createdAt: {
|
||
type: Date,
|
||
default: Date.now
|
||
},
|
||
lastLogin: {
|
||
type: Date,
|
||
default: Date.now
|
||
},
|
||
resetPasswordToken: String,
|
||
resetPasswordExpires: Date,
|
||
isEmailVerified: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
isManuallyAdded: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
emailVerificationToken: String,
|
||
emailVerificationExpires: Date,
|
||
settings: {
|
||
type: mongoose.Schema.Types.Mixed,
|
||
default: {}
|
||
},
|
||
// 钉钉登录相关字段
|
||
dingtalk_userid: {
|
||
type: String,
|
||
sparse: true, // 允许多个null值,但不允许重复的非null值
|
||
unique: true
|
||
},
|
||
loginType: {
|
||
type: String,
|
||
enum: ['normal', 'dingtalk'],
|
||
default: 'normal'
|
||
},
|
||
phone: {
|
||
type: String,
|
||
sparse: true,
|
||
unique: true,
|
||
validate: {
|
||
validator: function(v) {
|
||
// 如果提供了手机号,验证格式
|
||
return !v || /^1[3-9]\d{9}$/.test(v);
|
||
},
|
||
message: props => `${props.value} 不是有效的手机号格式`
|
||
}
|
||
},
|
||
realName: {
|
||
type: String,
|
||
trim: true
|
||
},
|
||
nickname: {
|
||
type: String,
|
||
trim: true
|
||
}
|
||
}, {
|
||
timestamps: true
|
||
});
|
||
|
||
// 隐藏敏感信息
|
||
userSchema.methods.toJSON = function() {
|
||
const user = this;
|
||
const userObject = user.toObject();
|
||
|
||
delete userObject.password;
|
||
delete userObject.tokens;
|
||
|
||
return userObject;
|
||
};
|
||
|
||
// 生成认证令牌
|
||
userSchema.methods.generateAuthToken = function() {
|
||
try {
|
||
const user = this;
|
||
const token = jwt.sign(
|
||
{
|
||
userId: user._id.toString(),
|
||
username: user.username,
|
||
email: user.email,
|
||
role: user.role
|
||
},
|
||
process.env.JWT_SECRET || 'your-secret-key',
|
||
{ expiresIn: '7d' }
|
||
);
|
||
|
||
// 保存令牌到用户的tokens数组
|
||
user.tokens = user.tokens || [];
|
||
user.tokens.push({ token });
|
||
|
||
return token;
|
||
} catch (error) {
|
||
console.error('生成令牌错误:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// 查找用户凭据
|
||
userSchema.statics.findByCredentials = async (email, password) => {
|
||
try {
|
||
const user = await User.findOne({ email });
|
||
|
||
if (!user) {
|
||
throw new Error('用户不存在');
|
||
}
|
||
|
||
const isMatch = await bcrypt.compare(password, user.password);
|
||
|
||
if (!isMatch) {
|
||
throw new Error('密码错误');
|
||
}
|
||
|
||
return user;
|
||
} catch (error) {
|
||
console.error('查找用户凭据错误:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// 保存前加密密码
|
||
userSchema.pre('save', async function(next) {
|
||
try {
|
||
const user = this;
|
||
|
||
if (user.isModified('password')) {
|
||
user.password = await bcrypt.hash(user.password, 8);
|
||
}
|
||
|
||
next();
|
||
} catch (error) {
|
||
console.error('保存前加密密码错误:', error);
|
||
next(error);
|
||
}
|
||
});
|
||
|
||
// 记录功能使用
|
||
userSchema.methods.recordUsage = async function(feature, creditsUsed = 1) {
|
||
try {
|
||
const user = this;
|
||
|
||
// 检查积分是否足够
|
||
if (user.credits < creditsUsed) {
|
||
throw new Error('积分不足');
|
||
}
|
||
|
||
// 扣除积分并记录使用历史
|
||
user.credits -= creditsUsed;
|
||
user.usageHistory = user.usageHistory || [];
|
||
user.usageHistory.push({
|
||
feature,
|
||
creditsUsed,
|
||
timestamp: new Date()
|
||
});
|
||
|
||
await user.save();
|
||
return user;
|
||
} catch (error) {
|
||
console.error('记录功能使用错误:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
const User = mongoose.model('User', userSchema);
|
||
|
||
module.exports = User;
|