const bcrypt = require('bcryptjs'); const User = require('../models/User'); const Role = require('../models/Role'); const Menu = require('../models/Menu'); const Tenant = require('../models/Tenant'); const Category = require('../models/Category'); const Setting = require('../models/Setting'); const { SubscriptionPlan } = require('../models'); const { generateToken } = require('../middleware/auth'); const { logLogin, getClientIp } = require('../utils/logger'); // 登录 exports.login = async (req, res) => { const clientIp = getClientIp(req); const userAgent = req.headers['user-agent']; let username = req.body.username; try { const { password } = req.body; // 参数验证 if (!username || !password) { await logLogin({ username, loginType: 'login', ip: clientIp, userAgent, status: 'fail', message: '用户名和密码不能为空' }); return res.status(400).json({ code: 400, message: '用户名和密码不能为空' }); } // 查找用户 const user = await User.findOne({ where: { username }, include: [ { model: Role, as: 'role' }, { model: Tenant, as: 'tenant' } ] }); if (!user) { await logLogin({ username, loginType: 'login', ip: clientIp, userAgent, status: 'fail', message: '用户名或密码错误' }); return res.status(401).json({ code: 401, message: '用户名或密码错误' }); } // 检查用户状态 if (user.status === 'disabled') { await logLogin({ userId: user.id, username: user.username, loginType: 'login', ip: clientIp, userAgent, status: 'fail', message: '账号已被禁用' }); return res.status(401).json({ code: 401, message: '账号已被禁用' }); } // 验证密码 const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { await logLogin({ userId: user.id, username: user.username, loginType: 'login', ip: clientIp, userAgent, status: 'fail', message: '用户名或密码错误' }); return res.status(401).json({ code: 401, message: '用户名或密码错误' }); } // 检查租户计费状态(仅非系统管理员需要检查) if (user.tenant && user.userType !== 'super_admin') { const billingStatus = user.tenant.billingStatus; if (billingStatus === 'trial_expired' || billingStatus === 'paid_expired') { await logLogin({ userId: user.id, username: user.username, loginType: 'login', ip: clientIp, userAgent, status: 'fail', message: '账户已过期,请续费' }); return res.status(403).json({ code: 403, message: '您的账户已过期,请前往续费', data: { expired: true, billingStatus: billingStatus, trialEndDate: user.tenant.trialEndDate, paidEndDate: user.tenant.paidEndDate } }); } } // 生成 Token const token = generateToken(user); // 获取用户菜单权限 let menus = []; // 系统管理员返回所有菜单 if (user.userType === 'super_admin') { const allMenus = await Menu.findAll({ where: { isDeleted: 0, status: 'active' }, order: [['sort', 'ASC']] }); menus = buildMenuTree(allMenus); } else if (user.roleId) { // 租户管理员和普通用户返回角色分配的菜单 const roleData = await Role.findByPk(user.roleId, { include: [{ model: Menu, as: 'menus', where: { isDeleted: 0, status: 'active' }, through: { attributes: [] } }] }); if (roleData && roleData.menus) { const sortedMenus = roleData.menus.sort((a, b) => a.sort - b.sort); menus = buildMenuTree(sortedMenus); } } // 记录登录成功日志 await logLogin({ userId: user.id, username: user.username, loginType: 'login', ip: clientIp, userAgent, status: 'success', message: '登录成功' }); // 返回用户信息和 Token res.json({ code: 200, message: '登录成功', data: { token, userInfo: { id: user.id, username: user.username, nickname: user.nickname, role: user.role, tenantId: user.tenantId, userType: user.userType }, tenant: user.tenant, menus } }); } catch (error) { console.error('登录错误:', error); await logLogin({ username, loginType: 'login', ip: clientIp, userAgent, status: 'fail', message: error.message }); res.status(500).json({ code: 500, message: '登录失败', error: error.message }); } }; // 登出 exports.logout = async (req, res) => { try { // 记录登出日志 const user = req.user || {}; await logLogin({ userId: user.id, username: user.username, loginType: 'logout', ip: getClientIp(req), userAgent: req.headers['user-agent'], status: 'success', message: '登出成功' }); res.json({ code: 200, message: '登出成功' }); } catch (error) { console.error('登出错误:', error); res.json({ code: 200, message: '登出成功' }); } }; // 构建菜单树 function buildMenuTree(menus, parentId = null) { return menus .filter(menu => menu.parentId === parentId) .map(menu => { // 获取菜单的原始数据(处理 Sequelize 模型对象) const menuData = menu.toJSON ? menu.toJSON() : menu; return { id: menuData.id, name: menuData.name, code: menuData.code, type: menuData.type, path: menuData.path, component: menuData.component, icon: menuData.icon, sort: menuData.sort, visible: menuData.visible, status: menuData.status, isDeleted: menuData.isDeleted, parentId: menuData.parentId, children: buildMenuTree(menus, menuData.id) }; }); } // 获取当前用户信息 exports.getCurrentUser = async (req, res) => { try { // req.user 由 authMiddleware 附加 res.json({ code: 200, message: '获取成功', data: req.user }); } catch (error) { console.error('获取用户信息错误:', error); res.status(500).json({ code: 500, message: '获取用户信息失败', error: error.message }); } }; // 修改密码 exports.changePassword = async (req, res) => { try { const { oldPassword, newPassword } = req.body; const userId = req.user.id; // 参数验证 if (!oldPassword || !newPassword) { return res.status(400).json({ code: 400, message: '原密码和新密码不能为空' }); } if (newPassword.length < 6) { return res.status(400).json({ code: 400, message: '新密码长度不能少于6位' }); } // 查找用户 const user = await User.findByPk(userId); if (!user) { return res.status(404).json({ code: 404, message: '用户不存在' }); } // 验证原密码 const isPasswordValid = await bcrypt.compare(oldPassword, user.password); if (!isPasswordValid) { return res.status(400).json({ code: 400, message: '原密码错误' }); } // 加密新密码 const hashedPassword = await bcrypt.hash(newPassword, 10); // 更新密码 await user.update({ password: hashedPassword }); res.json({ code: 200, message: '密码修改成功' }); } catch (error) { console.error('修改密码错误:', error); res.status(500).json({ code: 500, message: '修改密码失败', error: error.message }); } }; // 租户自助注册 exports.registerTenant = async (req, res) => { const { contactName, contactPhone, contactEmail, adminUsername, adminPassword } = req.body; try { // 参数验证 if (!contactName || !contactPhone || !contactEmail || !adminUsername || !adminPassword) { return res.status(400).json({ code: 400, message: '请填写所有必填项' }); } // 检查管理员账号是否已存在 const existingUser = await User.findOne({ where: { username: adminUsername } }); if (existingUser) { return res.status(400).json({ code: 400, message: '管理员账号已存在' }); } // 查询默认套餐 const defaultPlan = await SubscriptionPlan.findOne({ where: { isDefault: true, isDeleted: 0 } }); if (!defaultPlan) { return res.status(500).json({ code: 500, message: '系统未配置默认套餐,请联系管理员' }); } // 生成租户编码:时间戳 + 随机数 const timestamp = Date.now().toString(36).toUpperCase(); const random = Math.random().toString(36).substring(2, 6).toUpperCase(); const code = `T${timestamp}${random}`; const now = new Date(); const trialEndDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 创建租户(使用默认套餐配置) const tenant = await Tenant.create({ code, contactName, contactPhone, contactEmail, maxUsers: defaultPlan.maxUsers, maxApartments: defaultPlan.maxApartments, maxRooms: defaultPlan.maxRooms, status: 'active', billingStatus: 'trial_active', planId: defaultPlan.id, trialStartDate: now, trialEndDate: trialEndDate, currentPeriodStart: now, currentPeriodEnd: trialEndDate }); // 创建租户管理员账号 const hashedPassword = await bcrypt.hash(adminPassword, 10); const user = await User.create({ username: adminUsername, password: hashedPassword, nickname: adminUsername, tenantId: tenant.id, userType: 'tenant_admin', status: 'active' }); // 创建默认管理员角色并分配所有菜单权限 const adminRole = await Role.create({ name: '管理员', code: 'admin', description: '租户默认管理员角色,拥有所有权限', tenantId: tenant.id, status: 'active' }); // 获取基础菜单并分配给角色(新租户只分配基础功能菜单) const basicMenus = await Menu.findAll({ where: { isDeleted: 0, status: 'active', isBasic: 1 } }); if (basicMenus.length > 0) { const menuIds = basicMenus.map(menu => menu.id); await adminRole.setMenus(menuIds); } // 将用户关联到管理员角色 await user.update({ roleId: adminRole.id }); // 初始化默认收支类目 const defaultCategories = [ // 收入类目 { name: '租金', code: 'rent', type: 'income', sort: 1 }, { name: '水费', code: 'water', type: 'income', sort: 2 }, { name: '电费', code: 'electricity', type: 'income', sort: 3 }, { name: '燃气费', code: 'gas', type: 'income', sort: 4 }, { name: '押金', code: 'deposit', type: 'income', sort: 5 }, { name: '物业费', code: 'property_fee', type: 'income', sort: 6 }, { name: '违约金', code: 'penalty', type: 'income', sort: 7 }, { name: '其他收入', code: 'other_income', type: 'income', sort: 8 }, // 支出类目 { name: '维修费', code: 'maintenance', type: 'expense', sort: 1 }, { name: '中介费', code: 'agency_fee', type: 'expense', sort: 2 }, { name: '物业费', code: 'property_fee_expense', type: 'expense', sort: 3 }, { name: '水费', code: 'water_expense', type: 'expense', sort: 4 }, { name: '电费', code: 'electricity_expense', type: 'expense', sort: 5 }, { name: '燃气费', code: 'gas_expense', type: 'expense', sort: 6 }, { name: '装修费', code: 'renovation', type: 'expense', sort: 7 }, { name: '其他支出', code: 'other_expense', type: 'expense', sort: 8 } ]; for (const category of defaultCategories) { await Category.create({ ...category, tenantId: tenant.id, status: 'active', isDefault: 1, createBy: user.id }); } // 初始化默认系统设置 const defaultSettings = [ { key: 'expireReminderDays', value: '30', description: '房间到期提前提醒天数' } ]; for (const setting of defaultSettings) { await Setting.create({ ...setting, tenantId: tenant.id, createBy: user.id }); } res.json({ code: 200, message: '注册成功,欢迎试用', data: { tenantId: tenant.id, code: tenant.code, status: tenant.status } }); } catch (error) { console.error('租户注册失败:', error); res.status(500).json({ code: 500, message: '注册失败', error: error.message }); } };