Generating commit message...
This commit is contained in:
212
backend/src/models/User.js
Normal file
212
backend/src/models/User.js
Normal file
@@ -0,0 +1,212 @@
|
||||
const mongoose = require('mongoose')
|
||||
const bcrypt = require('bcryptjs')
|
||||
|
||||
// 用户等级枚举
|
||||
const UserLevel = {
|
||||
NORMAL: 1, // 普通用户
|
||||
VIP: 2, // VIP用户
|
||||
SUPER_VIP: 3 // 超级VIP
|
||||
}
|
||||
|
||||
// 用户状态枚举
|
||||
const UserStatus = {
|
||||
ACTIVE: 'active', // 活跃
|
||||
INACTIVE: 'inactive', // 非活跃
|
||||
BANNED: 'banned' // 封禁
|
||||
}
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
// 基础信息
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
minlength: 3,
|
||||
maxlength: 20,
|
||||
match: /^[a-zA-Z0-9_]+$/
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 6
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
sparse: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
match: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
sparse: true,
|
||||
trim: true,
|
||||
match: /^1[3-9]\d{9}$/
|
||||
},
|
||||
|
||||
// 个人信息
|
||||
nickname: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 20
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
enum: ['male', 'female', 'unknown'],
|
||||
default: 'unknown'
|
||||
},
|
||||
birthday: Date,
|
||||
|
||||
// 账户信息
|
||||
points: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
enum: Object.values(UserLevel),
|
||||
default: UserLevel.NORMAL
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: Object.values(UserStatus),
|
||||
default: UserStatus.ACTIVE
|
||||
},
|
||||
balance: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0
|
||||
},
|
||||
|
||||
// 第三方登录
|
||||
wechatOpenid: {
|
||||
type: String,
|
||||
sparse: true
|
||||
},
|
||||
wechatUnionid: {
|
||||
type: String,
|
||||
sparse: true
|
||||
},
|
||||
|
||||
// 统计信息
|
||||
travelCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
animalAdoptCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
flowerOrderCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 时间戳
|
||||
lastLoginAt: Date,
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: {
|
||||
transform: function(doc, ret) {
|
||||
delete ret.password
|
||||
delete ret.wechatOpenid
|
||||
delete ret.wechatUnionid
|
||||
return ret
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 索引 (移除与字段定义中 unique: true 和 sparse: true 重复的索引)
|
||||
userSchema.index({ createdAt: -1 })
|
||||
userSchema.index({ points: -1 })
|
||||
|
||||
// 密码加密中间件
|
||||
userSchema.pre('save', async function(next) {
|
||||
if (!this.isModified('password')) return next()
|
||||
|
||||
try {
|
||||
const salt = await bcrypt.genSalt(12)
|
||||
this.password = await bcrypt.hash(this.password, salt)
|
||||
next()
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// 比较密码方法
|
||||
userSchema.methods.comparePassword = async function(candidatePassword) {
|
||||
return bcrypt.compare(candidatePassword, this.password)
|
||||
}
|
||||
|
||||
// 检查用户状态是否活跃
|
||||
userSchema.methods.isActive = function() {
|
||||
return this.status === UserStatus.ACTIVE
|
||||
}
|
||||
|
||||
// 检查是否为VIP用户
|
||||
userSchema.methods.isVip = function() {
|
||||
return this.level >= UserLevel.VIP
|
||||
}
|
||||
|
||||
// 添加积分
|
||||
userSchema.methods.addPoints = function(points) {
|
||||
this.points += points
|
||||
return this.save()
|
||||
}
|
||||
|
||||
// 扣除积分
|
||||
userSchema.methods.deductPoints = function(points) {
|
||||
if (this.points < points) {
|
||||
throw new Error('积分不足')
|
||||
}
|
||||
this.points -= points
|
||||
return this.save()
|
||||
}
|
||||
|
||||
// 静态方法:根据用户名查找用户
|
||||
userSchema.statics.findByUsername = function(username) {
|
||||
return this.findOne({ username })
|
||||
}
|
||||
|
||||
// 静态方法:根据邮箱查找用户
|
||||
userSchema.statics.findByEmail = function(email) {
|
||||
return this.findOne({ email })
|
||||
}
|
||||
|
||||
// 静态方法:根据手机号查找用户
|
||||
userSchema.statics.findByPhone = function(phone) {
|
||||
return this.findOne({ phone })
|
||||
}
|
||||
|
||||
// 虚拟字段:用户等级名称
|
||||
userSchema.virtual('levelName').get(function() {
|
||||
const levelNames = {
|
||||
[UserLevel.NORMAL]: '普通用户',
|
||||
[UserLevel.VIP]: 'VIP用户',
|
||||
[UserLevel.SUPER_VIP]: '超级VIP'
|
||||
}
|
||||
return levelNames[this.level] || '未知'
|
||||
})
|
||||
|
||||
const User = mongoose.model('User', userSchema)
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
UserLevel,
|
||||
UserStatus
|
||||
}
|
||||
160
backend/src/models/UserMySQL.js
Normal file
160
backend/src/models/UserMySQL.js
Normal file
@@ -0,0 +1,160 @@
|
||||
const { query, transaction } = require('../config/database');
|
||||
|
||||
class UserMySQL {
|
||||
// 创建用户
|
||||
static async create(userData) {
|
||||
const {
|
||||
openid,
|
||||
nickname,
|
||||
avatar = '',
|
||||
gender = 'other',
|
||||
birthday = null,
|
||||
phone = null,
|
||||
email = null
|
||||
} = userData;
|
||||
|
||||
const sql = `
|
||||
INSERT INTO users (
|
||||
openid, nickname, avatar, gender, birthday, phone, email,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`;
|
||||
|
||||
const params = [
|
||||
openid,
|
||||
nickname,
|
||||
avatar,
|
||||
gender,
|
||||
birthday,
|
||||
phone,
|
||||
email
|
||||
];
|
||||
|
||||
const result = await query(sql, params);
|
||||
return result.insertId;
|
||||
}
|
||||
|
||||
// 根据ID查找用户
|
||||
static async findById(id) {
|
||||
const sql = 'SELECT * FROM users WHERE id = ?';
|
||||
const rows = await query(sql, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 根据openid查找用户
|
||||
static async findByOpenid(openid) {
|
||||
const sql = 'SELECT * FROM users WHERE openid = ?';
|
||||
const rows = await query(sql, [openid]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 根据手机号查找用户
|
||||
static async findByPhone(phone) {
|
||||
const sql = 'SELECT * FROM users WHERE phone = ?';
|
||||
const rows = await query(sql, [phone]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 根据邮箱查找用户
|
||||
static async findByEmail(email) {
|
||||
const sql = 'SELECT * FROM users WHERE email = ?';
|
||||
const rows = await query(sql, [email]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
static async update(id, updates) {
|
||||
const allowedFields = ['nickname', 'avatar', 'gender', 'birthday', 'phone', 'email'];
|
||||
const setClauses = [];
|
||||
const params = [];
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (allowedFields.includes(key) && value !== undefined) {
|
||||
setClauses.push(`${key} = ?`);
|
||||
params.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (setClauses.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setClauses.push('updated_at = NOW()');
|
||||
params.push(id);
|
||||
|
||||
const sql = `UPDATE users SET ${setClauses.join(', ')} WHERE id = ?`;
|
||||
const result = await query(sql, params);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
static async updatePassword(id, newPassword) {
|
||||
const sql = 'UPDATE users SET password = ?, updated_at = NOW() WHERE id = ?';
|
||||
const result = await query(sql, [newPassword, id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
static async updateLastLogin(id) {
|
||||
const sql = 'UPDATE users SET updated_at = NOW() WHERE id = ?';
|
||||
const result = await query(sql, [id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 检查openid是否已存在
|
||||
static async isOpenidExists(openid, excludeId = null) {
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE openid = ?';
|
||||
const params = [openid];
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
static async isEmailExists(email, excludeId = null) {
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE email = ?';
|
||||
const params = [email];
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
static async isPhoneExists(phone, excludeId = null) {
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE phone = ?';
|
||||
const params = [phone];
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查用户名是否已已存在 (根据openid检查)
|
||||
static async isUsernameExists(username, excludeId = null) {
|
||||
return await this.isOpenidExists(username, excludeId);
|
||||
}
|
||||
|
||||
// 安全返回用户信息(去除敏感信息)
|
||||
static sanitize(user) {
|
||||
if (!user) return null;
|
||||
|
||||
const { password, ...safeUser } = user;
|
||||
return safeUser;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserMySQL;
|
||||
Reference in New Issue
Block a user