|
|
@@ -0,0 +1,455 @@
|
|
|
+const pool = require('../config/database');
|
|
|
+const bcrypt = require('bcryptjs');
|
|
|
+const crypto = require('crypto');
|
|
|
+const { logOperation } = require('../middleware/logger');
|
|
|
+
|
|
|
+// 获取用户列表
|
|
|
+exports.getUsers = async (req, res) => {
|
|
|
+ try {
|
|
|
+ const { page = 1, limit = 20, role, team, status, keyword } = req.query;
|
|
|
+ const offset = (page - 1) * limit;
|
|
|
+ const currentUser = req.user;
|
|
|
+
|
|
|
+ // 构建 WHERE 条件
|
|
|
+ let whereClause = 'WHERE 1=1';
|
|
|
+ const params = [];
|
|
|
+
|
|
|
+ // 权限控制:销售经理只能看自己团队的
|
|
|
+ if (currentUser.role === 'sales_manager') {
|
|
|
+ whereClause += ' AND u.team = ?';
|
|
|
+ params.push(currentUser.team);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 角色过滤
|
|
|
+ if (role) {
|
|
|
+ whereClause += ' AND u.role = ?';
|
|
|
+ params.push(role);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 团队过滤
|
|
|
+ if (team) {
|
|
|
+ whereClause += ' AND u.team = ?';
|
|
|
+ params.push(team);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 状态过滤
|
|
|
+ if (status) {
|
|
|
+ whereClause += ' AND u.status = ?';
|
|
|
+ params.push(status);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关键词搜索
|
|
|
+ if (keyword) {
|
|
|
+ whereClause += ' AND (u.username LIKE ? OR u.real_name LIKE ? OR u.email LIKE ? OR u.phone LIKE ?)';
|
|
|
+ const searchPattern = `%${keyword}%`;
|
|
|
+ params.push(searchPattern, searchPattern, searchPattern, searchPattern);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取总数
|
|
|
+ const countQuery = `
|
|
|
+ SELECT COUNT(*) as total
|
|
|
+ FROM users u
|
|
|
+ ${whereClause}
|
|
|
+ `;
|
|
|
+ const [countResult] = await pool.query(countQuery, params);
|
|
|
+ const total = countResult && countResult[0] ? countResult[0].total : 0;
|
|
|
+
|
|
|
+ // 获取数据(不返回密码)
|
|
|
+ const query = `
|
|
|
+ SELECT u.id, u.username, u.real_name, u.role, u.department, u.team,
|
|
|
+ u.email, u.phone, u.status, u.last_login, u.created_at, u.updated_at,
|
|
|
+ (SELECT COUNT(*) FROM customers WHERE sales_owner = u.id) as customer_count,
|
|
|
+ (SELECT COUNT(*) FROM customers WHERE sales_owner = u.id AND status = 'closed') as closed_count
|
|
|
+ FROM users u
|
|
|
+ ${whereClause}
|
|
|
+ ORDER BY u.created_at DESC
|
|
|
+ LIMIT ? OFFSET ?
|
|
|
+ `;
|
|
|
+ params.push(parseInt(limit), parseInt(offset));
|
|
|
+
|
|
|
+ const [users] = await pool.query(query, params);
|
|
|
+
|
|
|
+ res.json({
|
|
|
+ success: true,
|
|
|
+ data: {
|
|
|
+ users,
|
|
|
+ pagination: {
|
|
|
+ page: parseInt(page),
|
|
|
+ limit: parseInt(limit),
|
|
|
+ total,
|
|
|
+ total_pages: Math.ceil(total / limit)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取用户列表失败:', error);
|
|
|
+ res.status(500).json({ success: false, message: '获取用户列表失败' });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取用户详情
|
|
|
+exports.getUserDetail = async (req, res) => {
|
|
|
+ try {
|
|
|
+ const { id } = req.params;
|
|
|
+ const currentUser = req.user;
|
|
|
+
|
|
|
+ // 权限检查
|
|
|
+ if (currentUser.role === 'sales_manager') {
|
|
|
+ const [checkUser] = await pool.query(
|
|
|
+ 'SELECT team FROM users WHERE id = ?',
|
|
|
+ [id]
|
|
|
+ );
|
|
|
+ if (!checkUser[0] || checkUser[0].team !== currentUser.team) {
|
|
|
+ return res.status(403).json({ success: false, message: '无权查看该用户' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const [users] = await pool.query(
|
|
|
+ `SELECT u.id, u.username, u.real_name, u.role, u.department, u.team,
|
|
|
+ u.email, u.phone, u.status, u.last_login, u.created_at, u.updated_at,
|
|
|
+ (SELECT COUNT(*) FROM customers WHERE sales_owner = u.id) as customer_count,
|
|
|
+ (SELECT COUNT(*) FROM customers WHERE sales_owner = u.id AND status = 'closed') as closed_count,
|
|
|
+ (SELECT COUNT(*) FROM customers WHERE sales_owner = u.id AND status = 'following') as following_count
|
|
|
+ FROM users u
|
|
|
+ WHERE u.id = ?`,
|
|
|
+ [id]
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!users || !users[0]) {
|
|
|
+ return res.status(404).json({ success: false, message: '用户不存在' });
|
|
|
+ }
|
|
|
+
|
|
|
+ res.json({ success: true, data: users[0] });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取用户详情失败:', error);
|
|
|
+ res.status(500).json({ success: false, message: '获取用户详情失败' });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 创建用户
|
|
|
+exports.createUser = async (req, res) => {
|
|
|
+ try {
|
|
|
+ const { username, password, real_name, role, department, team, email, phone } = req.body;
|
|
|
+ const currentUser = req.user;
|
|
|
+
|
|
|
+ // 验证必填字段
|
|
|
+ if (!username || !password || !real_name || !role) {
|
|
|
+ return res.status(400).json({ success: false, message: '请填写完整信息' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 权限检查:只有管理员和销售总监可以创建用户
|
|
|
+ if (!['admin', 'sales_director'].includes(currentUser.role)) {
|
|
|
+ return res.status(403).json({ success: false, message: '无权创建用户' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查用户名是否已存在
|
|
|
+ const [existingUsers] = await pool.query(
|
|
|
+ 'SELECT id FROM users WHERE username = ?',
|
|
|
+ [username]
|
|
|
+ );
|
|
|
+ if (existingUsers && existingUsers.length > 0) {
|
|
|
+ return res.status(400).json({ success: false, message: '用户名已存在' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加密密码
|
|
|
+ const hashedPassword = await bcrypt.hash(password, 10);
|
|
|
+ const userId = crypto.randomUUID();
|
|
|
+
|
|
|
+ // 插入用户
|
|
|
+ await pool.query(
|
|
|
+ `INSERT INTO users (id, username, password, real_name, role, department, team, email, phone, status)
|
|
|
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`,
|
|
|
+ [userId, username, hashedPassword, real_name, role, department, team, email, phone]
|
|
|
+ );
|
|
|
+
|
|
|
+ // 记录日志
|
|
|
+ await logOperation(
|
|
|
+ currentUser.id,
|
|
|
+ 'create_user',
|
|
|
+ 'user',
|
|
|
+ userId,
|
|
|
+ { username, real_name, role },
|
|
|
+ req.ip
|
|
|
+ );
|
|
|
+
|
|
|
+ res.json({ success: true, message: '用户创建成功', data: { id: userId } });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('创建用户失败:', error);
|
|
|
+ res.status(500).json({ success: false, message: '创建用户失败' });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 更新用户信息
|
|
|
+exports.updateUser = async (req, res) => {
|
|
|
+ try {
|
|
|
+ const { id } = req.params;
|
|
|
+ const { real_name, role, department, team, email, phone, status } = req.body;
|
|
|
+ const currentUser = req.user;
|
|
|
+
|
|
|
+ // 权限检查
|
|
|
+ if (!['admin', 'sales_director'].includes(currentUser.role)) {
|
|
|
+ return res.status(403).json({ success: false, message: '无权修改用户信息' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查用户是否存在
|
|
|
+ const [users] = await pool.query('SELECT id FROM users WHERE id = ?', [id]);
|
|
|
+ if (!users || !users[0]) {
|
|
|
+ return res.status(404).json({ success: false, message: '用户不存在' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新用户信息
|
|
|
+ const updates = [];
|
|
|
+ const params = [];
|
|
|
+
|
|
|
+ if (real_name) {
|
|
|
+ updates.push('real_name = ?');
|
|
|
+ params.push(real_name);
|
|
|
+ }
|
|
|
+ if (role) {
|
|
|
+ updates.push('role = ?');
|
|
|
+ params.push(role);
|
|
|
+ }
|
|
|
+ if (department) {
|
|
|
+ updates.push('department = ?');
|
|
|
+ params.push(department);
|
|
|
+ }
|
|
|
+ if (team !== undefined) {
|
|
|
+ updates.push('team = ?');
|
|
|
+ params.push(team);
|
|
|
+ }
|
|
|
+ if (email !== undefined) {
|
|
|
+ updates.push('email = ?');
|
|
|
+ params.push(email);
|
|
|
+ }
|
|
|
+ if (phone !== undefined) {
|
|
|
+ updates.push('phone = ?');
|
|
|
+ params.push(phone);
|
|
|
+ }
|
|
|
+ if (status) {
|
|
|
+ updates.push('status = ?');
|
|
|
+ params.push(status);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (updates.length === 0) {
|
|
|
+ return res.status(400).json({ success: false, message: '没有需要更新的信息' });
|
|
|
+ }
|
|
|
+
|
|
|
+ params.push(id);
|
|
|
+ await pool.query(
|
|
|
+ `UPDATE users SET ${updates.join(', ')}, updated_at = NOW() WHERE id = ?`,
|
|
|
+ params
|
|
|
+ );
|
|
|
+
|
|
|
+ // 记录日志
|
|
|
+ await logOperation(
|
|
|
+ currentUser.id,
|
|
|
+ 'update_user',
|
|
|
+ 'user',
|
|
|
+ id,
|
|
|
+ { real_name, role, department, team, email, phone, status },
|
|
|
+ req.ip
|
|
|
+ );
|
|
|
+
|
|
|
+ res.json({ success: true, message: '用户信息更新成功' });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('更新用户信息失败:', error);
|
|
|
+ res.status(500).json({ success: false, message: '更新用户信息失败' });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 重置用户密码
|
|
|
+exports.resetPassword = async (req, res) => {
|
|
|
+ try {
|
|
|
+ const { id } = req.params;
|
|
|
+ const { new_password } = req.body;
|
|
|
+ const currentUser = req.user;
|
|
|
+
|
|
|
+ // 权限检查
|
|
|
+ if (!['admin', 'sales_director'].includes(currentUser.role)) {
|
|
|
+ return res.status(403).json({ success: false, message: '无权重置密码' });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!new_password || new_password.length < 6) {
|
|
|
+ return res.status(400).json({ success: false, message: '密码长度至少6位' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加密新密码
|
|
|
+ const hashedPassword = await bcrypt.hash(new_password, 10);
|
|
|
+
|
|
|
+ await pool.query(
|
|
|
+ 'UPDATE users SET password = ?, updated_at = NOW() WHERE id = ?',
|
|
|
+ [hashedPassword, id]
|
|
|
+ );
|
|
|
+
|
|
|
+ // 记录日志
|
|
|
+ await logOperation(
|
|
|
+ currentUser.id,
|
|
|
+ 'reset_password',
|
|
|
+ 'user',
|
|
|
+ id,
|
|
|
+ { operator: currentUser.username },
|
|
|
+ req.ip
|
|
|
+ );
|
|
|
+
|
|
|
+ res.json({ success: true, message: '密码重置成功' });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('重置密码失败:', error);
|
|
|
+ res.status(500).json({ success: false, message: '重置密码失败' });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 删除用户(软删除)
|
|
|
+exports.deleteUser = async (req, res) => {
|
|
|
+ try {
|
|
|
+ const { id } = req.params;
|
|
|
+ const currentUser = req.user;
|
|
|
+
|
|
|
+ // 权限检查:只有管理员可以删除用户
|
|
|
+ if (currentUser.role !== 'admin') {
|
|
|
+ return res.status(403).json({ success: false, message: '无权删除用户' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 不能删除自己
|
|
|
+ if (id === currentUser.id) {
|
|
|
+ return res.status(400).json({ success: false, message: '不能删除自己' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查用户是否有关联客户
|
|
|
+ const [customers] = await pool.query(
|
|
|
+ 'SELECT COUNT(*) as count FROM customers WHERE sales_owner = ? AND status = "following"',
|
|
|
+ [id]
|
|
|
+ );
|
|
|
+
|
|
|
+ if (customers[0].count > 0) {
|
|
|
+ return res.status(400).json({
|
|
|
+ success: false,
|
|
|
+ message: `该用户还有 ${customers[0].count} 个跟进中的客户,请先转移客户`
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 软删除:将状态设为 inactive
|
|
|
+ await pool.query(
|
|
|
+ 'UPDATE users SET status = "inactive", updated_at = NOW() WHERE id = ?',
|
|
|
+ [id]
|
|
|
+ );
|
|
|
+
|
|
|
+ // 记录日志
|
|
|
+ await logOperation(
|
|
|
+ currentUser.id,
|
|
|
+ 'delete_user',
|
|
|
+ 'user',
|
|
|
+ id,
|
|
|
+ { operator: currentUser.username },
|
|
|
+ req.ip
|
|
|
+ );
|
|
|
+
|
|
|
+ res.json({ success: true, message: '用户已禁用' });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('删除用户失败:', error);
|
|
|
+ res.status(500).json({ success: false, message: '删除用户失败' });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取团队列表
|
|
|
+exports.getTeams = async (req, res) => {
|
|
|
+ try {
|
|
|
+ const [teams] = await pool.query(
|
|
|
+ `SELECT team, COUNT(*) as member_count,
|
|
|
+ SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_count
|
|
|
+ FROM users
|
|
|
+ WHERE team IS NOT NULL AND team != ''
|
|
|
+ GROUP BY team
|
|
|
+ ORDER BY team`
|
|
|
+ );
|
|
|
+
|
|
|
+ res.json({ success: true, data: teams || [] });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取团队列表失败:', error);
|
|
|
+ res.status(500).json({ success: false, message: '获取团队列表失败' });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 转移客户(用于用户离职等场景)
|
|
|
+exports.transferCustomers = async (req, res) => {
|
|
|
+ try {
|
|
|
+ const { from_user_id, to_user_id } = req.body;
|
|
|
+ const currentUser = req.user;
|
|
|
+
|
|
|
+ // 权限检查
|
|
|
+ if (!['admin', 'sales_director'].includes(currentUser.role)) {
|
|
|
+ return res.status(403).json({ success: false, message: '无权转移客户' });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!from_user_id || !to_user_id) {
|
|
|
+ return res.status(400).json({ success: false, message: '请指定源用户和目标用户' });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (from_user_id === to_user_id) {
|
|
|
+ return res.status(400).json({ success: false, message: '源用户和目标用户不能相同' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查两个用户是否存在
|
|
|
+ const [users] = await pool.query(
|
|
|
+ 'SELECT id, real_name FROM users WHERE id IN (?, ?)',
|
|
|
+ [from_user_id, to_user_id]
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!users || users.length !== 2) {
|
|
|
+ return res.status(400).json({ success: false, message: '用户不存在' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转移客户
|
|
|
+ const [result] = await pool.query(
|
|
|
+ 'UPDATE customers SET sales_owner = ?, updated_at = NOW() WHERE sales_owner = ? AND status = "following"',
|
|
|
+ [to_user_id, from_user_id]
|
|
|
+ );
|
|
|
+
|
|
|
+ // 记录日志
|
|
|
+ await logOperation(
|
|
|
+ currentUser.id,
|
|
|
+ 'transfer_customers',
|
|
|
+ 'user',
|
|
|
+ from_user_id,
|
|
|
+ { from_user_id, to_user_id, count: result.affectedRows },
|
|
|
+ req.ip
|
|
|
+ );
|
|
|
+
|
|
|
+ res.json({
|
|
|
+ success: true,
|
|
|
+ message: `成功转移 ${result.affectedRows} 个客户`,
|
|
|
+ data: { count: result.affectedRows }
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('转移客户失败:', error);
|
|
|
+ res.status(500).json({ success: false, message: '转移客户失败' });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取用户统计信息
|
|
|
+exports.getUserStats = async (req, res) => {
|
|
|
+ try {
|
|
|
+ const { id } = req.params;
|
|
|
+ const { start_date, end_date } = req.query;
|
|
|
+
|
|
|
+ const [stats] = await pool.query(
|
|
|
+ `SELECT
|
|
|
+ COUNT(*) as total_customers,
|
|
|
+ SUM(CASE WHEN status = 'following' THEN 1 ELSE 0 END) as following_count,
|
|
|
+ SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) as closed_count,
|
|
|
+ SUM(CASE WHEN status = 'lost' THEN 1 ELSE 0 END) as lost_count,
|
|
|
+ SUM(CASE WHEN status = 'released' THEN 1 ELSE 0 END) as released_count
|
|
|
+ FROM customers
|
|
|
+ WHERE sales_owner = ?
|
|
|
+ ${start_date ? 'AND created_at >= ?' : ''}
|
|
|
+ ${end_date ? 'AND created_at <= ?' : ''}`,
|
|
|
+ [id, start_date, end_date].filter(Boolean)
|
|
|
+ );
|
|
|
+
|
|
|
+ res.json({ success: true, data: stats[0] || {} });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取用户统计失败:', error);
|
|
|
+ res.status(500).json({ success: false, message: '获取用户统计失败' });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+module.exports = exports;
|