Prechádzať zdrojové kódy

增加客户管理配置,支持设置系统管理员

ly 4 mesiacov pred
rodič
commit
c985483db9

+ 292 - 0
USER_MANAGEMENT_GUIDE.md

@@ -0,0 +1,292 @@
+# 用户管理功能说明
+
+## 功能概览
+
+系统新增了完整的用户管理功能,管理员和销售总监可以对业务员进行全面管理。
+
+## 功能特性
+
+### 1. 用户列表管理
+
+**访问路径**: `/users`
+
+**功能**:
+- 查看所有用户信息
+- 多维度筛选(角色、状态、关键词)
+- 查看用户业绩统计(客户数、成交数)
+- 支持分页浏览
+
+**权限**:
+- 管理员:查看所有用户
+- 销售总监:查看所有用户
+- 销售经理:只能查看自己团队的用户
+
+### 2. 创建新用户
+
+**功能**:
+- 创建新的业务员账号
+- 设置用户基本信息(用户名、密码、姓名)
+- 分配角色和权限
+- 设置部门和团队
+
+**必填字段**:
+- 用户名(用于登录)
+- 密码(至少6位)
+- 姓名
+- 角色
+
+**可选字段**:
+- 部门
+- 团队
+- 邮箱
+- 手机号
+
+**权限**: 仅限管理员和销售总监
+
+### 3. 编辑用户信息
+
+**功能**:
+- 修改用户基本信息
+- 调整角色和权限
+- 更改部门团队
+- 启用/禁用账号
+
+**不可修改**: 用户名(登录账号)
+
+**权限**: 仅限管理员和销售总监
+
+### 4. 重置密码
+
+**功能**:
+- 为用户重置登录密码
+- 设置新密码(至少6位)
+
+**使用场景**:
+- 用户忘记密码
+- 账号安全需要
+
+**权限**: 仅限管理员和销售总监
+
+### 5. 禁用用户
+
+**功能**:
+- 将用户状态设为禁用
+- 软删除,不删除历史数据
+- 自动检查用户是否有跟进中的客户
+
+**限制**:
+- 不能禁用自己
+- 如果用户有跟进中的客户,需先转移客户
+
+**权限**: 仅限管理员
+
+### 6. 转移客户
+
+**功能**:
+- 将一个用户的所有跟进中客户转移给另一个用户
+- 批量转移,自动记录
+
+**使用场景**:
+- 员工离职
+- 团队调整
+- 业务重组
+
+**权限**: 仅限管理员和销售总监
+
+### 7. 查看用户详情
+
+**访问路径**: `/users/:id`
+
+**功能**:
+- 查看用户完整信息
+- 查看业绩统计
+- 查看客户状态分布
+
+**统计数据**:
+- 总客户数
+- 成交客户数
+- 跟进中客户数
+- 成交率
+- 已丢单数
+- 已释放数
+
+## API 接口
+
+### 用户管理相关接口
+
+```javascript
+// 获取用户列表
+GET /api/users
+参数: page, limit, role, team, status, keyword
+
+// 获取用户详情
+GET /api/users/:id
+
+// 创建用户
+POST /api/users
+Body: { username, password, real_name, role, department, team, email, phone }
+
+// 更新用户信息
+PUT /api/users/:id
+Body: { real_name, role, department, team, email, phone, status }
+
+// 重置密码
+POST /api/users/:id/reset-password
+Body: { new_password }
+
+// 禁用用户
+DELETE /api/users/:id
+
+// 转移客户
+POST /api/users/transfer-customers
+Body: { from_user_id, to_user_id }
+
+// 获取团队列表
+GET /api/teams
+
+// 获取用户统计
+GET /api/users/:id/stats
+参数: start_date, end_date
+```
+
+## 角色权限说明
+
+### 管理员 (admin)
+- 创建/编辑/禁用所有用户
+- 查看所有用户信息
+- 重置任何用户密码
+- 转移任何用户的客户
+- 全系统最高权限
+
+### 销售总监 (sales_director)
+- 创建/编辑用户(除管理员外)
+- 查看所有用户信息
+- 重置密码
+- 转移客户
+- 查看所有统计报表
+
+### 销售经理 (sales_manager)
+- 查看自己团队的用户
+- 查看团队统计
+- 无法创建/编辑用户
+- 无法转移客户
+
+### 销售 (sales)
+- 无法访问用户管理功能
+- 只能查看和管理自己的客户
+
+## 使用示例
+
+### 示例1: 新员工入职
+
+1. 管理员登录系统
+2. 进入"用户管理"页面
+3. 点击"新建用户"按钮
+4. 填写信息:
+   - 用户名: `zhangsan`
+   - 密码: `123456`
+   - 姓名: `张三`
+   - 角色: `销售`
+   - 团队: `华东团队`
+5. 点击确定,创建成功
+6. 新员工可使用账号登录
+
+### 示例2: 员工离职客户转移
+
+1. 管理员登录系统
+2. 进入"用户管理"页面
+3. 点击"转移客户"按钮
+4. 选择源用户(离职员工)
+5. 选择目标用户(接手员工)
+6. 确认转移,系统自动处理
+7. 将离职员工账号禁用
+
+### 示例3: 重置员工密码
+
+1. 管理员或销售总监登录
+2. 在用户列表找到目标用户
+3. 点击"重置密码"
+4. 输入新密码(至少6位)
+5. 确认后通知员工新密码
+
+### 示例4: 调整员工角色
+
+1. 管理员登录系统
+2. 在用户列表找到目标用户
+3. 点击"编辑"按钮
+4. 修改角色字段(如:销售 → 销售经理)
+5. 保存更新
+6. 员工重新登录后生效
+
+## 注意事项
+
+1. **密码安全**
+   - 创建用户时密码至少6位
+   - 建议首次登录后修改密码
+   - 定期更新密码
+
+2. **客户转移**
+   - 转移前请确认目标用户
+   - 转移操作不可撤销
+   - 系统会记录转移日志
+
+3. **禁用用户**
+   - 禁用前必须转移其跟进中的客户
+   - 禁用后用户无法登录
+   - 历史数据不会被删除
+
+4. **权限管理**
+   - 合理分配用户角色
+   - 避免过多管理员账号
+   - 定期审核用户权限
+
+5. **数据安全**
+   - 所有操作都有日志记录
+   - 用户信息加密存储
+   - 密码使用 bcrypt 加密
+
+## 数据统计
+
+用户列表中显示的统计数据:
+- **客户数**: 该用户报备的所有客户总数
+- **成交数**: 已成交的客户数量
+- **跟进中**: 正在跟进的客户数量
+
+用户详情页显示的统计数据:
+- 总客户数
+- 成交客户数(绿色)
+- 跟进中客户数(橙色)
+- 成交率(百分比)
+- 已丢单数
+- 已释放到公海池的数量
+
+## 常见问题
+
+**Q: 忘记管理员密码怎么办?**
+A: 需要通过数据库直接重置,或联系技术支持
+
+**Q: 如何批量导入用户?**
+A: 当前版本不支持批量导入,需逐个创建
+
+**Q: 禁用的用户可以恢复吗?**
+A: 可以,管理员编辑用户将状态改为"正常"即可
+
+**Q: 转移客户后原用户还能看到吗?**
+A: 不能,客户所有权已转移,原用户无法查看
+
+**Q: 销售经理可以管理其他团队的用户吗?**
+A: 不可以,销售经理只能查看自己团队的用户
+
+## 后续计划
+
+- [ ] 批量导入用户功能
+- [ ] 用户活跃度分析
+- [ ] 权限更细粒度控制
+- [ ] 用户操作审计报告
+- [ ] 移动端用户管理
+
+---
+
+**文档版本**: v1.0  
+**更新时间**: 2024-01-14  
+**维护者**: 系统管理员

+ 4 - 0
client/src/App.jsx

@@ -11,6 +11,8 @@ import ApprovalList from './pages/Approval/ApprovalList'
 import MyApprovals from './pages/Approval/MyApprovals'
 import TeamStats from './pages/Stats/TeamStats'
 import SourceAnalysis from './pages/Stats/SourceAnalysis'
+import UserList from './pages/User/UserList'
+import UserDetail from './pages/User/UserDetail'
 
 function App() {
   const [user, setUser] = useState(null)
@@ -58,6 +60,8 @@ function App() {
           <Route path="my-approvals" element={<MyApprovals />} />
           <Route path="stats/team" element={<TeamStats />} />
           <Route path="stats/source" element={<SourceAnalysis />} />
+          <Route path="users" element={<UserList />} />
+          <Route path="users/:id" element={<UserDetail />} />
         </Route>
       </Routes>
     </Router>

+ 11 - 3
client/src/components/Layout/index.jsx

@@ -7,7 +7,8 @@ import {
   BarChartOutlined,
   UserOutlined,
   LogoutOutlined,
-  FileTextOutlined
+  FileTextOutlined,
+  SettingOutlined
 } from '@ant-design/icons'
 import { Outlet, useNavigate, useLocation } from 'react-router-dom'
 import { USER_ROLES } from '../../utils/constants'
@@ -56,6 +57,11 @@ const Layout = ({ user, onLogout }) => {
         { key: '/stats/team', label: '团队统计' },
         { key: '/stats/source', label: '来源分析' },
       ]
+    }] : []),
+    ...(['admin', 'sales_director', 'sales_manager'].includes(user.role) ? [{
+      key: '/users',
+      icon: <SettingOutlined />,
+      label: '用户管理',
     }] : [])
   ]
 
@@ -92,7 +98,9 @@ const Layout = ({ user, onLogout }) => {
             <div className="user-info">
               <Avatar icon={<UserOutlined />} />
               <span className="user-name">{user.real_name}</span>
-              <span className="user-role">{USER_ROLES[user.role]}</span>
+              <span className="user-role">
+                {USER_ROLES.find(r => r.value === user.role)?.label || user.role}
+              </span>
             </div>
           </Dropdown>
         </div>
@@ -103,7 +111,7 @@ const Layout = ({ user, onLogout }) => {
           <Menu
             mode="inline"
             selectedKeys={[location.pathname]}
-            defaultOpenKeys={['/customers', '/approvals-menu', '/stats']}
+            defaultOpenKeys={['/customers', '/approvals-menu', '/stats', '/users']}
             items={menuItems}
             onClick={({ key }) => navigate(key)}
           />

+ 167 - 0
client/src/pages/User/UserDetail.jsx

@@ -0,0 +1,167 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { Card, Descriptions, Button, Space, Tag, Statistic, Row, Col, Spin, message } from 'antd';
+import { ArrowLeftOutlined, EditOutlined } from '@ant-design/icons';
+import request from '../../utils/request';
+import { USER_ROLES, USER_STATUS } from '../../utils/constants';
+
+const UserDetail = () => {
+  const { id } = useParams();
+  const navigate = useNavigate();
+  const [loading, setLoading] = useState(false);
+  const [userData, setUserData] = useState(null);
+  const [stats, setStats] = useState(null);
+
+  useEffect(() => {
+    fetchUserDetail();
+    fetchUserStats();
+  }, [id]);
+
+  const fetchUserDetail = async () => {
+    setLoading(true);
+    try {
+      const response = await request.get(`/users/${id}`);
+      if (response.success) {
+        setUserData(response.data);
+      }
+    } catch (error) {
+      message.error('获取用户详情失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchUserStats = async () => {
+    try {
+      const response = await request.get(`/users/${id}/stats`);
+      if (response.success) {
+        setStats(response.data);
+      }
+    } catch (error) {
+      console.error('获取统计失败');
+    }
+  };
+
+  if (loading) {
+    return (
+      <div style={{ textAlign: 'center', padding: '100px' }}>
+        <Spin size="large" />
+      </div>
+    );
+  }
+
+  if (!userData) {
+    return null;
+  }
+
+  const roleInfo = USER_ROLES.find(r => r.value === userData.role);
+  const statusInfo = USER_STATUS.find(s => s.value === userData.status);
+
+  return (
+    <div>
+      <Space direction="vertical" style={{ width: '100%' }} size="large">
+        <Card>
+          <Space style={{ width: '100%', justifyContent: 'space-between' }}>
+            <Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/users')}>
+              返回列表
+            </Button>
+            <Button type="primary" icon={<EditOutlined />} onClick={() => navigate(`/users/${id}/edit`)}>
+              编辑
+            </Button>
+          </Space>
+        </Card>
+
+        {/* 用户基本信息 */}
+        <Card title="基本信息">
+          <Descriptions column={2}>
+            <Descriptions.Item label="用户名">{userData.username}</Descriptions.Item>
+            <Descriptions.Item label="姓名">{userData.real_name}</Descriptions.Item>
+            <Descriptions.Item label="角色">
+              <Tag color={roleInfo?.color}>{roleInfo?.label || userData.role}</Tag>
+            </Descriptions.Item>
+            <Descriptions.Item label="状态">
+              <Tag color={statusInfo?.color}>{statusInfo?.label || userData.status}</Tag>
+            </Descriptions.Item>
+            <Descriptions.Item label="部门">{userData.department || '-'}</Descriptions.Item>
+            <Descriptions.Item label="团队">{userData.team || '-'}</Descriptions.Item>
+            <Descriptions.Item label="邮箱">{userData.email || '-'}</Descriptions.Item>
+            <Descriptions.Item label="手机">{userData.phone || '-'}</Descriptions.Item>
+            <Descriptions.Item label="最后登录">
+              {userData.last_login || '从未登录'}
+            </Descriptions.Item>
+            <Descriptions.Item label="创建时间">{userData.created_at}</Descriptions.Item>
+          </Descriptions>
+        </Card>
+
+        {/* 业绩统计 */}
+        <Card title="业绩统计">
+          <Row gutter={16}>
+            <Col span={6}>
+              <Card>
+                <Statistic
+                  title="总客户数"
+                  value={userData.customer_count || 0}
+                  valueStyle={{ color: '#1890ff' }}
+                />
+              </Card>
+            </Col>
+            <Col span={6}>
+              <Card>
+                <Statistic
+                  title="成交客户数"
+                  value={userData.closed_count || 0}
+                  valueStyle={{ color: '#52c41a' }}
+                />
+              </Card>
+            </Col>
+            <Col span={6}>
+              <Card>
+                <Statistic
+                  title="跟进中"
+                  value={stats?.following_count || 0}
+                  valueStyle={{ color: '#faad14' }}
+                />
+              </Card>
+            </Col>
+            <Col span={6}>
+              <Card>
+                <Statistic
+                  title="成交率"
+                  value={
+                    userData.customer_count > 0
+                      ? ((userData.closed_count / userData.customer_count) * 100).toFixed(1)
+                      : 0
+                  }
+                  suffix="%"
+                  valueStyle={{ color: '#13c2c2' }}
+                />
+              </Card>
+            </Col>
+          </Row>
+        </Card>
+
+        {/* 详细统计 */}
+        {stats && (
+          <Card title="客户状态分布">
+            <Row gutter={16}>
+              <Col span={6}>
+                <Statistic title="跟进中" value={stats.following_count || 0} />
+              </Col>
+              <Col span={6}>
+                <Statistic title="已成交" value={stats.closed_count || 0} />
+              </Col>
+              <Col span={6}>
+                <Statistic title="已丢单" value={stats.lost_count || 0} />
+              </Col>
+              <Col span={6}>
+                <Statistic title="已释放" value={stats.released_count || 0} />
+              </Col>
+            </Row>
+          </Card>
+        )}
+      </Space>
+    </div>
+  );
+};
+
+export default UserDetail;

+ 539 - 0
client/src/pages/User/UserList.jsx

@@ -0,0 +1,539 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Space, Tag, Input, Select, message, Modal, Form, Popconfirm, Card } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, SwapOutlined, EyeOutlined } from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
+import request from '../../utils/request';
+import { USER_ROLES, USER_STATUS } from '../../utils/constants';
+
+const { Search } = Input;
+const { Option } = Select;
+
+const UserList = () => {
+  const navigate = useNavigate();
+  const [loading, setLoading] = useState(false);
+  const [dataSource, setDataSource] = useState([]);
+  const [pagination, setPagination] = useState({ page: 1, limit: 20, total: 0 });
+  const [filters, setFilters] = useState({});
+  const [createModalVisible, setCreateModalVisible] = useState(false);
+  const [editModalVisible, setEditModalVisible] = useState(false);
+  const [resetPasswordModalVisible, setResetPasswordModalVisible] = useState(false);
+  const [transferModalVisible, setTransferModalVisible] = useState(false);
+  const [currentUser, setCurrentUser] = useState(null);
+  const [allUsers, setAllUsers] = useState([]);
+  const [createForm] = Form.useForm();
+  const [editForm] = Form.useForm();
+  const [resetPasswordForm] = Form.useForm();
+  const [transferForm] = Form.useForm();
+
+  useEffect(() => {
+    fetchData();
+  }, [pagination.page, pagination.limit, filters]);
+
+  useEffect(() => {
+    fetchAllUsers();
+  }, []);
+
+  const fetchData = async () => {
+    setLoading(true);
+    try {
+      const params = {
+        page: pagination.page,
+        limit: pagination.limit,
+        ...filters
+      };
+      const response = await request.get('/users', { params });
+      if (response.success) {
+        setDataSource(response.data.users || []);
+        setPagination(prev => ({ ...prev, total: response.data.pagination.total }));
+      }
+    } catch (error) {
+      message.error('获取用户列表失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchAllUsers = async () => {
+    try {
+      const response = await request.get('/users', { params: { limit: 1000 } });
+      if (response.success) {
+        setAllUsers(response.data.users || []);
+      }
+    } catch (error) {
+      console.error('获取用户列表失败');
+    }
+  };
+
+  const handleSearch = (value) => {
+    setFilters(prev => ({ ...prev, keyword: value }));
+    setPagination(prev => ({ ...prev, page: 1 }));
+  };
+
+  const handleRoleChange = (value) => {
+    setFilters(prev => ({ ...prev, role: value }));
+    setPagination(prev => ({ ...prev, page: 1 }));
+  };
+
+  const handleStatusChange = (value) => {
+    setFilters(prev => ({ ...prev, status: value }));
+    setPagination(prev => ({ ...prev, page: 1 }));
+  };
+
+  const handleCreate = async (values) => {
+    try {
+      const response = await request.post('/users', values);
+      if (response.success) {
+        message.success('用户创建成功');
+        setCreateModalVisible(false);
+        createForm.resetFields();
+        fetchData();
+        fetchAllUsers();
+      }
+    } catch (error) {
+      message.error(error.message || '创建用户失败');
+    }
+  };
+
+  const handleEdit = async (values) => {
+    try {
+      const response = await request.put(`/users/${currentUser.id}`, values);
+      if (response.success) {
+        message.success('用户信息更新成功');
+        setEditModalVisible(false);
+        editForm.resetFields();
+        setCurrentUser(null);
+        fetchData();
+        fetchAllUsers();
+      }
+    } catch (error) {
+      message.error(error.message || '更新用户失败');
+    }
+  };
+
+  const handleResetPassword = async (values) => {
+    try {
+      const response = await request.post(`/users/${currentUser.id}/reset-password`, values);
+      if (response.success) {
+        message.success('密码重置成功');
+        setResetPasswordModalVisible(false);
+        resetPasswordForm.resetFields();
+        setCurrentUser(null);
+      }
+    } catch (error) {
+      message.error(error.message || '重置密码失败');
+    }
+  };
+
+  const handleDelete = async (id) => {
+    try {
+      const response = await request.delete(`/users/${id}`);
+      if (response.success) {
+        message.success('用户已禁用');
+        fetchData();
+      }
+    } catch (error) {
+      message.error(error.message || '禁用用户失败');
+    }
+  };
+
+  const handleTransfer = async (values) => {
+    try {
+      const response = await request.post('/users/transfer-customers', values);
+      if (response.success) {
+        message.success(response.message);
+        setTransferModalVisible(false);
+        transferForm.resetFields();
+        fetchData();
+      }
+    } catch (error) {
+      message.error(error.message || '转移客户失败');
+    }
+  };
+
+  const showEditModal = (record) => {
+    setCurrentUser(record);
+    editForm.setFieldsValue(record);
+    setEditModalVisible(true);
+  };
+
+  const showResetPasswordModal = (record) => {
+    setCurrentUser(record);
+    setResetPasswordModalVisible(true);
+  };
+
+  const columns = [
+    {
+      title: '用户名',
+      dataIndex: 'username',
+      key: 'username',
+      fixed: 'left',
+      width: 120,
+    },
+    {
+      title: '姓名',
+      dataIndex: 'real_name',
+      key: 'real_name',
+      width: 100,
+    },
+    {
+      title: '角色',
+      dataIndex: 'role',
+      key: 'role',
+      width: 120,
+      render: (role) => {
+        const roleInfo = USER_ROLES.find(r => r.value === role);
+        return <Tag color={roleInfo?.color || 'default'}>{roleInfo?.label || role}</Tag>;
+      },
+    },
+    {
+      title: '部门',
+      dataIndex: 'department',
+      key: 'department',
+      width: 100,
+    },
+    {
+      title: '团队',
+      dataIndex: 'team',
+      key: 'team',
+      width: 100,
+    },
+    {
+      title: '邮箱',
+      dataIndex: 'email',
+      key: 'email',
+      width: 180,
+    },
+    {
+      title: '手机',
+      dataIndex: 'phone',
+      key: 'phone',
+      width: 120,
+    },
+    {
+      title: '客户数',
+      dataIndex: 'customer_count',
+      key: 'customer_count',
+      width: 80,
+      render: (count) => count || 0,
+    },
+    {
+      title: '成交数',
+      dataIndex: 'closed_count',
+      key: 'closed_count',
+      width: 80,
+      render: (count) => <span style={{ color: '#52c41a' }}>{count || 0}</span>,
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      width: 80,
+      render: (status) => {
+        const statusInfo = USER_STATUS.find(s => s.value === status);
+        return <Tag color={statusInfo?.color || 'default'}>{statusInfo?.label || status}</Tag>;
+      },
+    },
+    {
+      title: '最后登录',
+      dataIndex: 'last_login',
+      key: 'last_login',
+      width: 160,
+      render: (time) => time || '从未登录',
+    },
+    {
+      title: '操作',
+      key: 'action',
+      fixed: 'right',
+      width: 200,
+      render: (_, record) => (
+        <Space size="small">
+          <Button 
+            type="link" 
+            size="small" 
+            icon={<EyeOutlined />}
+            onClick={() => navigate(`/users/${record.id}`)}
+          >
+            详情
+          </Button>
+          <Button 
+            type="link" 
+            size="small" 
+            icon={<EditOutlined />}
+            onClick={() => showEditModal(record)}
+          >
+            编辑
+          </Button>
+          <Button 
+            type="link" 
+            size="small" 
+            icon={<KeyOutlined />}
+            onClick={() => showResetPasswordModal(record)}
+          >
+            重置密码
+          </Button>
+          <Popconfirm
+            title="确定要禁用该用户吗?"
+            onConfirm={() => handleDelete(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button type="link" size="small" danger icon={<DeleteOutlined />}>
+              禁用
+            </Button>
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <Card>
+      <Space direction="vertical" style={{ width: '100%' }} size="large">
+        <Space style={{ width: '100%', justifyContent: 'space-between' }}>
+          <Space>
+            <Search
+              placeholder="搜索用户名、姓名、邮箱、手机"
+              onSearch={handleSearch}
+              style={{ width: 300 }}
+              allowClear
+            />
+            <Select
+              placeholder="角色"
+              style={{ width: 120 }}
+              onChange={handleRoleChange}
+              allowClear
+            >
+              {USER_ROLES.map(role => (
+                <Option key={role.value} value={role.value}>{role.label}</Option>
+              ))}
+            </Select>
+            <Select
+              placeholder="状态"
+              style={{ width: 100 }}
+              onChange={handleStatusChange}
+              allowClear
+            >
+              {USER_STATUS.map(status => (
+                <Option key={status.value} value={status.value}>{status.label}</Option>
+              ))}
+            </Select>
+          </Space>
+          <Space>
+            <Button
+              type="default"
+              icon={<SwapOutlined />}
+              onClick={() => setTransferModalVisible(true)}
+            >
+              转移客户
+            </Button>
+            <Button
+              type="primary"
+              icon={<PlusOutlined />}
+              onClick={() => setCreateModalVisible(true)}
+            >
+              新建用户
+            </Button>
+          </Space>
+        </Space>
+
+        <Table
+          columns={columns}
+          dataSource={dataSource}
+          rowKey="id"
+          loading={loading}
+          scroll={{ x: 1600 }}
+          pagination={{
+            current: pagination.page,
+            pageSize: pagination.limit,
+            total: pagination.total,
+            showSizeChanger: true,
+            showTotal: (total) => `共 ${total} 条`,
+            onChange: (page, limit) => setPagination({ ...pagination, page, limit }),
+          }}
+        />
+      </Space>
+
+      {/* 创建用户 Modal */}
+      <Modal
+        title="新建用户"
+        open={createModalVisible}
+        onOk={() => createForm.submit()}
+        onCancel={() => {
+          setCreateModalVisible(false);
+          createForm.resetFields();
+        }}
+        width={600}
+      >
+        <Form form={createForm} onFinish={handleCreate} layout="vertical">
+          <Form.Item
+            label="用户名"
+            name="username"
+            rules={[{ required: true, message: '请输入用户名' }]}
+          >
+            <Input placeholder="用于登录" />
+          </Form.Item>
+          <Form.Item
+            label="密码"
+            name="password"
+            rules={[
+              { required: true, message: '请输入密码' },
+              { min: 6, message: '密码至少6位' }
+            ]}
+          >
+            <Input.Password placeholder="至少6位" />
+          </Form.Item>
+          <Form.Item
+            label="姓名"
+            name="real_name"
+            rules={[{ required: true, message: '请输入姓名' }]}
+          >
+            <Input />
+          </Form.Item>
+          <Form.Item
+            label="角色"
+            name="role"
+            rules={[{ required: true, message: '请选择角色' }]}
+          >
+            <Select>
+              {USER_ROLES.map(role => (
+                <Option key={role.value} value={role.value}>{role.label}</Option>
+              ))}
+            </Select>
+          </Form.Item>
+          <Form.Item label="部门" name="department">
+            <Input />
+          </Form.Item>
+          <Form.Item label="团队" name="team">
+            <Input />
+          </Form.Item>
+          <Form.Item label="邮箱" name="email">
+            <Input type="email" />
+          </Form.Item>
+          <Form.Item label="手机" name="phone">
+            <Input />
+          </Form.Item>
+        </Form>
+      </Modal>
+
+      {/* 编辑用户 Modal */}
+      <Modal
+        title="编辑用户"
+        open={editModalVisible}
+        onOk={() => editForm.submit()}
+        onCancel={() => {
+          setEditModalVisible(false);
+          editForm.resetFields();
+          setCurrentUser(null);
+        }}
+        width={600}
+      >
+        <Form form={editForm} onFinish={handleEdit} layout="vertical">
+          <Form.Item label="姓名" name="real_name">
+            <Input />
+          </Form.Item>
+          <Form.Item label="角色" name="role">
+            <Select>
+              {USER_ROLES.map(role => (
+                <Option key={role.value} value={role.value}>{role.label}</Option>
+              ))}
+            </Select>
+          </Form.Item>
+          <Form.Item label="部门" name="department">
+            <Input />
+          </Form.Item>
+          <Form.Item label="团队" name="team">
+            <Input />
+          </Form.Item>
+          <Form.Item label="邮箱" name="email">
+            <Input type="email" />
+          </Form.Item>
+          <Form.Item label="手机" name="phone">
+            <Input />
+          </Form.Item>
+          <Form.Item label="状态" name="status">
+            <Select>
+              {USER_STATUS.map(status => (
+                <Option key={status.value} value={status.value}>{status.label}</Option>
+              ))}
+            </Select>
+          </Form.Item>
+        </Form>
+      </Modal>
+
+      {/* 重置密码 Modal */}
+      <Modal
+        title="重置密码"
+        open={resetPasswordModalVisible}
+        onOk={() => resetPasswordForm.submit()}
+        onCancel={() => {
+          setResetPasswordModalVisible(false);
+          resetPasswordForm.resetFields();
+          setCurrentUser(null);
+        }}
+      >
+        <Form form={resetPasswordForm} onFinish={handleResetPassword} layout="vertical">
+          <Form.Item
+            label="新密码"
+            name="new_password"
+            rules={[
+              { required: true, message: '请输入新密码' },
+              { min: 6, message: '密码至少6位' }
+            ]}
+          >
+            <Input.Password />
+          </Form.Item>
+        </Form>
+      </Modal>
+
+      {/* 转移客户 Modal */}
+      <Modal
+        title="转移客户"
+        open={transferModalVisible}
+        onOk={() => transferForm.submit()}
+        onCancel={() => {
+          setTransferModalVisible(false);
+          transferForm.resetFields();
+        }}
+      >
+        <Form form={transferForm} onFinish={handleTransfer} layout="vertical">
+          <Form.Item
+            label="源用户"
+            name="from_user_id"
+            rules={[{ required: true, message: '请选择源用户' }]}
+          >
+            <Select
+              showSearch
+              placeholder="选择要转移客户的用户"
+              optionFilterProp="children"
+            >
+              {allUsers.map(user => (
+                <Option key={user.id} value={user.id}>
+                  {user.real_name} ({user.username}) - {user.customer_count || 0}个客户
+                </Option>
+              ))}
+            </Select>
+          </Form.Item>
+          <Form.Item
+            label="目标用户"
+            name="to_user_id"
+            rules={[{ required: true, message: '请选择目标用户' }]}
+          >
+            <Select
+              showSearch
+              placeholder="选择接收客户的用户"
+              optionFilterProp="children"
+            >
+              {allUsers.map(user => (
+                <Option key={user.id} value={user.id}>
+                  {user.real_name} ({user.username})
+                </Option>
+              ))}
+            </Select>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </Card>
+  );
+};
+
+export default UserList;

+ 14 - 8
client/src/utils/constants.js

@@ -1,3 +1,17 @@
+// 用户角色
+export const USER_ROLES = [
+  { label: '销售', value: 'sales', color: 'blue' },
+  { label: '销售经理', value: 'sales_manager', color: 'green' },
+  { label: '销售总监', value: 'sales_director', color: 'purple' },
+  { label: '系统管理员', value: 'admin', color: 'red' },
+];
+
+// 用户状态
+export const USER_STATUS = [
+  { label: '正常', value: 'active', color: 'success' },
+  { label: '禁用', value: 'inactive', color: 'default' },
+];
+
 // 客户状态
 export const CUSTOMER_STATUS = {
   following: { text: '跟进中', color: 'processing' },
@@ -29,14 +43,6 @@ export const APPROVAL_STATUS = {
   rejected: { text: '已拒绝', color: 'error' }
 }
 
-// 用户角色
-export const USER_ROLES = {
-  sales: '销售',
-  sales_manager: '销售经理',
-  sales_director: '销售总监',
-  admin: '系统管理员'
-}
-
 // 客户来源
 export const CUSTOMER_SOURCES = [
   '转介绍',

+ 2 - 1
package.json

@@ -8,7 +8,8 @@
     "dev": "nodemon src/server.js",
     "init-db": "node scripts/initDatabase.js",
     "test-data": "node scripts/createTestData.js",
-    "clean-db": "node scripts/cleanDatabase.js"
+    "clean-db": "node scripts/cleanDatabase.js",
+    "migrate": "node scripts/addLastLoginField.js"
   },
   "keywords": [
     "crm",

+ 40 - 0
scripts/addLastLoginField.js

@@ -0,0 +1,40 @@
+require('dotenv').config();
+const pool = require('../src/config/database');
+
+async function addLastLoginField() {
+  let connection;
+  
+  try {
+    connection = await pool.getConnection();
+    console.log('开始添加 last_login 字段...\n');
+
+    // 检查字段是否已存在
+    const [columns] = await connection.query(`
+      SHOW COLUMNS FROM users LIKE 'last_login'
+    `);
+
+    if (columns && columns.length > 0) {
+      console.log('last_login 字段已存在,无需添加');
+      return;
+    }
+
+    // 添加 last_login 字段
+    await connection.query(`
+      ALTER TABLE users 
+      ADD COLUMN last_login TIMESTAMP NULL AFTER status
+    `);
+    
+    console.log('✓ last_login 字段添加成功');
+    console.log('\n数据库更新完成!');
+  } catch (error) {
+    console.error('添加字段失败:', error);
+    process.exit(1);
+  } finally {
+    if (connection) {
+      await connection.release();
+    }
+    process.exit(0);
+  }
+}
+
+addLastLoginField();

+ 1 - 0
scripts/initDatabase.js

@@ -34,6 +34,7 @@ async function initDatabase() {
         department VARCHAR(50),
         team VARCHAR(50),
         status ENUM('active', 'inactive') DEFAULT 'active',
+        last_login TIMESTAMP NULL,
         created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
         updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
         INDEX idx_role (role),

+ 6 - 0
src/controllers/authController.js

@@ -38,6 +38,12 @@ exports.login = async (req, res) => {
       { expiresIn: process.env.JWT_EXPIRES_IN }
     );
 
+    // 更新最后登录时间
+    await pool.query(
+      'UPDATE users SET last_login = NOW() WHERE id = ?',
+      [user.id]
+    );
+
     // 记录登录日志
     await logOperation(user.id, 'login', 'user', user.id, { username }, req.ip);
 

+ 455 - 0
src/controllers/userController.js

@@ -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;

+ 29 - 0
src/routes/index.js

@@ -7,6 +7,7 @@ const customerController = require('../controllers/customerController');
 const poolController = require('../controllers/poolController');
 const approvalController = require('../controllers/approvalController');
 const statsController = require('../controllers/statsController');
+const userController = require('../controllers/userController');
 
 // 导入中间件
 const { authenticateToken, authorizeRoles, checkCustomerAccess } = require('../middleware/auth');
@@ -81,4 +82,32 @@ router.get('/stats/industry-analysis', authenticateToken, statsController.getInd
 // 公海池利用情况
 router.get('/stats/pool-utilization', authenticateToken, authorizeRoles('sales_manager', 'sales_director', 'admin'), statsController.getPoolUtilization);
 
+// ==================== 用户管理路由 ====================
+// 获取用户列表
+router.get('/users', authenticateToken, authorizeRoles('sales_manager', 'sales_director', 'admin'), userController.getUsers);
+
+// 获取用户详情
+router.get('/users/:id', authenticateToken, authorizeRoles('sales_manager', 'sales_director', 'admin'), userController.getUserDetail);
+
+// 创建用户
+router.post('/users', authenticateToken, authorizeRoles('sales_director', 'admin'), userController.createUser);
+
+// 更新用户信息
+router.put('/users/:id', authenticateToken, authorizeRoles('sales_director', 'admin'), userController.updateUser);
+
+// 重置用户密码
+router.post('/users/:id/reset-password', authenticateToken, authorizeRoles('sales_director', 'admin'), userController.resetPassword);
+
+// 删除用户
+router.delete('/users/:id', authenticateToken, authorizeRoles('admin'), userController.deleteUser);
+
+// 转移客户
+router.post('/users/transfer-customers', authenticateToken, authorizeRoles('sales_director', 'admin'), userController.transferCustomers);
+
+// 获取团队列表
+router.get('/teams', authenticateToken, authorizeRoles('sales_manager', 'sales_director', 'admin'), userController.getTeams);
+
+// 获取用户统计
+router.get('/users/:id/stats', authenticateToken, authorizeRoles('sales_manager', 'sales_director', 'admin'), userController.getUserStats);
+
 module.exports = router;