ly 4 ay önce
işleme
36ef82096c

+ 23 - 0
.env.example

@@ -0,0 +1,23 @@
+# 服务器配置
+PORT=3000
+NODE_ENV=development
+
+# 数据库配置
+DB_HOST=localhost
+DB_PORT=3306
+DB_USER=root
+DB_PASSWORD=LIYIdeqq
+DB_NAME=customer_crm
+
+# JWT配置
+JWT_SECRET=your_jwt_secret_key_change_this_in_production
+JWT_EXPIRES_IN=7d
+
+# 业务配置
+DEFAULT_PROTECTION_DAYS=30
+MAX_DAILY_LEADS=5
+SIMILARITY_THRESHOLD=0.8
+
+# 文件上传
+UPLOAD_PATH=./uploads
+MAX_FILE_SIZE=5242880

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+node_modules/
+.env
+uploads/
+*.log
+.DS_Store

+ 686 - 0
API_EXAMPLES.md

@@ -0,0 +1,686 @@
+# API 接口调用示例
+
+本文档提供常见业务场景的 API 调用示例。
+
+## 前置说明
+
+所有需要认证的接口都需要在 Header 中携带 JWT Token:
+```
+Authorization: Bearer {your_token}
+```
+
+基础 URL:`http://localhost:3000/api`
+
+## 场景 1:用户登录并查看仪表盘
+
+### 1.1 用户登录
+
+```bash
+curl -X POST http://localhost:3000/api/auth/login \
+  -H "Content-Type: application/json" \
+  -d '{
+    "username": "admin",
+    "password": "admin123"
+  }'
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "data": {
+    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+    "user": {
+      "id": "550e8400-e29b-41d4-a716-446655440000",
+      "username": "admin",
+      "real_name": "系统管理员",
+      "role": "admin",
+      "department": "管理部",
+      "team": null
+    }
+  }
+}
+```
+
+### 1.2 查看个人仪表盘
+
+```bash
+# 使用上一步获得的 token
+TOKEN="your_token_here"
+
+curl -X GET http://localhost:3000/api/stats/dashboard \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "data": {
+    "summary": {
+      "total": 15,
+      "following": 10,
+      "won": 3,
+      "lost": 2,
+      "conversion_rate": "60.00%",
+      "month_reports": 8,
+      "week_followups": 12
+    },
+    "expiring_customers": [
+      {
+        "id": "...",
+        "customer_name": "XX科技有限公司",
+        "protected_end_date": "2024-01-23 10:30:00",
+        "industry": "互联网/软件",
+        "region": "北京"
+      }
+    ]
+  }
+}
+```
+
+## 场景 2:报备新客户
+
+### 2.1 查重检查
+
+```bash
+curl -X GET "http://localhost:3000/api/customers/check-duplicate?customer_name=腾讯科技" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+**响应示例(无冲突)**:
+```json
+{
+  "success": true,
+  "has_conflict": false,
+  "message": "未发现重复客户,可以报备"
+}
+```
+
+**响应示例(有冲突)**:
+```json
+{
+  "success": true,
+  "has_conflict": true,
+  "conflict_type": "exact",
+  "data": {
+    "id": "...",
+    "customer_name": "腾讯科技",
+    "owner_name": "张三",
+    "team": "华北团队",
+    "report_time": "2024-01-15 09:30:00"
+  },
+  "message": "发现完全重复的客户"
+}
+```
+
+### 2.2 创建客户报备
+
+```bash
+curl -X POST http://localhost:3000/api/customers \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "customer_name": "阿里云计算有限公司",
+    "industry": "互联网/软件",
+    "region": "杭州",
+    "contact_person": "李先生",
+    "contact_phone": "13800138000",
+    "demand_description": "需要企业级CRM系统,预计预算50万元,决策周期2个月",
+    "source": "展会"
+  }'
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "message": "客户报备成功",
+  "data": {
+    "id": "customer-uuid-here",
+    "customer_name": "阿里云计算有限公司",
+    "protected_end_date": "2024-02-20 10:30:00"
+  }
+}
+```
+
+## 场景 3:客户跟进
+
+### 3.1 查看客户列表
+
+```bash
+# 查看我的跟进中客户
+curl -X GET "http://localhost:3000/api/customers?status=following&page=1&limit=10" \
+  -H "Authorization: Bearer $TOKEN"
+
+# 搜索特定客户
+curl -X GET "http://localhost:3000/api/customers?keyword=阿里&page=1&limit=10" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "data": {
+    "customers": [
+      {
+        "id": "...",
+        "customer_name": "阿里云计算有限公司",
+        "industry": "互联网/软件",
+        "region": "杭州",
+        "contact_person": "李先生",
+        "contact_phone": "13800138000",
+        "status": "following",
+        "owner_name": "系统管理员",
+        "protected_end_date": "2024-02-20 10:30:00",
+        "is_expired": 0,
+        "report_time": "2024-01-20 10:30:00"
+      }
+    ],
+    "pagination": {
+      "page": 1,
+      "limit": 10,
+      "total": 15,
+      "total_pages": 2
+    }
+  }
+}
+```
+
+### 3.2 查看客户详情
+
+```bash
+CUSTOMER_ID="customer-uuid-here"
+
+curl -X GET "http://localhost:3000/api/customers/$CUSTOMER_ID" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "data": {
+    "customer": {
+      "id": "...",
+      "customer_name": "阿里云计算有限公司",
+      "industry": "互联网/软件",
+      "region": "杭州",
+      "contact_person": "李先生",
+      "contact_phone": "13800138000",
+      "demand_description": "需要企业级CRM系统...",
+      "source": "展会",
+      "status": "following",
+      "owner_name": "系统管理员",
+      "protected_end_date": "2024-02-20 10:30:00",
+      "last_followup": "2024-01-19 15:00:00"
+    },
+    "followups": [
+      {
+        "id": "...",
+        "followup_type": "call",
+        "content": "电话沟通,客户表示有兴趣",
+        "next_plan": "下周上门拜访",
+        "user_name": "系统管理员",
+        "created_at": "2024-01-19 15:00:00"
+      }
+    ],
+    "attachments": []
+  }
+}
+```
+
+### 3.3 添加跟进记录
+
+```bash
+curl -X POST http://localhost:3000/api/customers/followup \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "customer_id": "customer-uuid-here",
+    "followup_type": "visit",
+    "content": "上门拜访,与技术负责人进行了深入沟通,客户对产品功能很满意",
+    "next_plan": "下周提交方案和报价"
+  }'
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "message": "跟进记录添加成功",
+  "data": {
+    "id": "followup-uuid-here"
+  }
+}
+```
+
+### 3.4 更新客户状态
+
+```bash
+# 标记为已成交
+curl -X PATCH "http://localhost:3000/api/customers/$CUSTOMER_ID/status" \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "status": "won"
+  }'
+
+# 标记为已丢单
+curl -X PATCH "http://localhost:3000/api/customers/$CUSTOMER_ID/status" \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "status": "lost"
+  }'
+```
+
+## 场景 4:公海池管理
+
+### 4.1 查看公海池客户
+
+```bash
+curl -X GET "http://localhost:3000/api/pool/customers?page=1&limit=20" \
+  -H "Authorization: Bearer $TOKEN"
+
+# 按行业筛选
+curl -X GET "http://localhost:3000/api/pool/customers?industry=互联网/软件" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "data": {
+    "customers": [
+      {
+        "id": "...",
+        "customer_name": "公海客户001有限公司",
+        "industry": "制造业",
+        "region": "上海",
+        "last_owner_name": "张三",
+        "claim_count": 2,
+        "release_reason": "保护期自动到期",
+        "updated_at": "2024-01-20 10:00:00"
+      }
+    ],
+    "pagination": {
+      "page": 1,
+      "limit": 20,
+      "total": 5,
+      "total_pages": 1
+    }
+  }
+}
+```
+
+### 4.2 领取客户
+
+```bash
+curl -X POST http://localhost:3000/api/pool/claim \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "customer_id": "pool-customer-uuid-here"
+  }'
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "message": "客户领取成功",
+  "data": {
+    "customer_id": "...",
+    "protected_end_date": "2024-02-20 10:30:00"
+  }
+}
+```
+
+### 4.3 释放客户到公海池
+
+```bash
+curl -X POST http://localhost:3000/api/pool/release \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "customer_id": "customer-uuid-here",
+    "release_reason": "客户暂时没有采购计划,延期到明年"
+  }'
+```
+
+### 4.4 查看我的领取记录
+
+```bash
+curl -X GET "http://localhost:3000/api/pool/my-claims?page=1&limit=20" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+## 场景 5:审批流程
+
+### 5.1 提交延期申请
+
+```bash
+curl -X POST http://localhost:3000/api/approvals \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "type": "extension",
+    "customer_id": "customer-uuid-here",
+    "reason": "客户决策流程较长,需要经过董事会审批,希望延长保护期",
+    "extension_days": 15
+  }'
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "message": "审批申请已提交",
+  "data": {
+    "id": "approval-uuid-here"
+  }
+}
+```
+
+### 5.2 查看我的审批申请
+
+```bash
+curl -X GET "http://localhost:3000/api/approvals/my?page=1&limit=20" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+### 5.3 查看待审批列表(经理权限)
+
+```bash
+curl -X GET "http://localhost:3000/api/approvals/pending?page=1&limit=20" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "data": {
+    "approvals": [
+      {
+        "id": "...",
+        "type": "extension",
+        "applicant_name": "张三",
+        "applicant_team": "华北团队",
+        "customer_name": "阿里云计算有限公司",
+        "reason": "客户决策流程较长...",
+        "extension_days": 15,
+        "status": "pending",
+        "created_at": "2024-01-20 10:00:00"
+      }
+    ],
+    "pagination": {
+      "page": 1,
+      "limit": 20,
+      "total": 3,
+      "total_pages": 1
+    }
+  }
+}
+```
+
+### 5.4 处理审批(经理权限)
+
+```bash
+APPROVAL_ID="approval-uuid-here"
+
+# 批准
+curl -X POST "http://localhost:3000/api/approvals/$APPROVAL_ID/process" \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "status": "approved",
+    "result_comment": "同意延期15天,请继续跟进"
+  }'
+
+# 拒绝
+curl -X POST "http://localhost:3000/api/approvals/$APPROVAL_ID/process" \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "status": "rejected",
+    "result_comment": "已经延期过一次,建议尽快推进或释放"
+  }'
+```
+
+## 场景 6:统计报表
+
+### 6.1 团队统计(经理权限)
+
+```bash
+# 按天统计
+curl -X GET "http://localhost:3000/api/stats/team?start_date=2024-01-01&end_date=2024-01-31&period=day" \
+  -H "Authorization: Bearer $TOKEN"
+
+# 按周统计
+curl -X GET "http://localhost:3000/api/stats/team?period=week" \
+  -H "Authorization: Bearer $TOKEN"
+
+# 按月统计
+curl -X GET "http://localhost:3000/api/stats/team?period=month" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "data": {
+    "team_summary": {
+      "total": 45,
+      "following": 30,
+      "won": 10,
+      "lost": 5
+    },
+    "member_stats": [
+      {
+        "id": "...",
+        "real_name": "张三",
+        "total_customers": 20,
+        "following": 15,
+        "won": 4,
+        "lost": 1,
+        "active_days": 22,
+        "conversion_rate": "80.00%"
+      },
+      {
+        "id": "...",
+        "real_name": "李四",
+        "total_customers": 15,
+        "following": 10,
+        "won": 3,
+        "lost": 2,
+        "active_days": 18,
+        "conversion_rate": "60.00%"
+      }
+    ],
+    "timeline": [
+      {
+        "period": "2024-01-01",
+        "count": 5,
+        "won_count": 2
+      },
+      {
+        "period": "2024-01-02",
+        "count": 3,
+        "won_count": 1
+      }
+    ]
+  }
+}
+```
+
+### 6.2 客户来源分析
+
+```bash
+curl -X GET "http://localhost:3000/api/stats/source-analysis?start_date=2024-01-01&end_date=2024-01-31" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "data": [
+    {
+      "source": "转介绍",
+      "count": 15,
+      "won_count": 8,
+      "lost_count": 2,
+      "conversion_rate": "80.00%"
+    },
+    {
+      "source": "展会",
+      "count": 10,
+      "won_count": 3,
+      "lost_count": 3,
+      "conversion_rate": "50.00%"
+    }
+  ]
+}
+```
+
+### 6.3 行业分布分析
+
+```bash
+curl -X GET "http://localhost:3000/api/stats/industry-analysis" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+### 6.4 公海池利用情况(经理权限)
+
+```bash
+curl -X GET "http://localhost:3000/api/stats/pool-utilization" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+**响应示例**:
+```json
+{
+  "success": true,
+  "data": {
+    "pool_total": 25,
+    "week_claims": 8,
+    "month_claims": 32,
+    "top_claimers": [
+      {
+        "real_name": "张三",
+        "claim_count": 12
+      },
+      {
+        "real_name": "李四",
+        "claim_count": 10
+      }
+    ]
+  }
+}
+```
+
+## 场景 7:用户管理
+
+### 7.1 创建新用户(管理员权限)
+
+```bash
+curl -X POST http://localhost:3000/api/auth/register \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "username": "sales003",
+    "password": "123456",
+    "real_name": "王五",
+    "email": "wangwu@example.com",
+    "phone": "13800138003",
+    "role": "sales",
+    "department": "销售部",
+    "team": "华南团队"
+  }'
+```
+
+### 7.2 修改密码
+
+```bash
+curl -X POST http://localhost:3000/api/auth/change-password \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "old_password": "admin123",
+    "new_password": "newpassword123"
+  }'
+```
+
+## 错误处理示例
+
+### 未认证
+```json
+{
+  "success": false,
+  "message": "未提供认证令牌"
+}
+```
+
+### 权限不足
+```json
+{
+  "success": false,
+  "message": "权限不足,需要以下角色之一: sales_manager, admin"
+}
+```
+
+### 业务错误
+```json
+{
+  "success": false,
+  "message": "今日领取次数已达上限(5次)"
+}
+```
+
+## 使用 JavaScript fetch
+
+```javascript
+const API_BASE = 'http://localhost:3000/api';
+const token = 'your_token_here';
+
+// 登录
+async function login(username, password) {
+  const response = await fetch(`${API_BASE}/auth/login`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ username, password })
+  });
+  return await response.json();
+}
+
+// 创建客户报备
+async function createCustomer(data) {
+  const response = await fetch(`${API_BASE}/customers`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      'Authorization': `Bearer ${token}`
+    },
+    body: JSON.stringify(data)
+  });
+  return await response.json();
+}
+
+// 获取客户列表
+async function getCustomers(params = {}) {
+  const queryString = new URLSearchParams(params).toString();
+  const response = await fetch(`${API_BASE}/customers?${queryString}`, {
+    headers: { 'Authorization': `Bearer ${token}` }
+  });
+  return await response.json();
+}
+```
+
+---
+
+更多接口详情请参考 `README.md` 文档。

+ 303 - 0
INSTALL.md

@@ -0,0 +1,303 @@
+# 安装指南
+
+## 📋 前置要求
+
+在开始安装之前,请确保您的系统已安装:
+
+1. **Node.js** (版本 >= 14.x)
+   ```bash
+   node --version  # 检查版本
+   ```
+
+2. **MySQL** (版本 >= 5.7)
+   ```bash
+   mysql --version  # 检查版本
+   ```
+
+3. **npm** 或 **yarn**
+   ```bash
+   npm --version  # 检查版本
+   ```
+
+## 🚀 安装步骤
+
+### 1. 准备项目目录
+
+确认您在项目目录中:
+```bash
+cd /Users/ly/CodeBuddy/20260114134435
+```
+
+### 2. 安装依赖包
+
+```bash
+npm install
+```
+
+这将安装所有必需的 Node.js 依赖包,包括:
+- express: Web 框架
+- mysql2: MySQL 数据库驱动
+- jsonwebtoken: JWT 认证
+- bcryptjs: 密码加密
+- 等等...
+
+### 3. 配置数据库
+
+#### 3.1 创建 MySQL 数据库
+
+登录 MySQL:
+```bash
+mysql -u root -p
+```
+
+创建数据库(可选,初始化脚本会自动创建):
+```sql
+CREATE DATABASE customer_crm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+EXIT;
+```
+
+#### 3.2 配置环境变量
+
+复制环境变量模板:
+```bash
+cp .env.example .env
+```
+
+编辑 `.env` 文件:
+```bash
+# macOS/Linux
+nano .env
+
+# 或使用其他编辑器
+vim .env
+code .env
+```
+
+修改以下配置:
+```env
+# 数据库配置(必须修改)
+DB_HOST=localhost
+DB_PORT=3306
+DB_USER=root
+DB_PASSWORD=你的MySQL密码
+DB_NAME=customer_crm
+
+# JWT密钥(建议修改为随机字符串)
+JWT_SECRET=your_jwt_secret_key_change_this_in_production
+
+# 其他配置可保持默认
+PORT=3000
+DEFAULT_PROTECTION_DAYS=30
+MAX_DAILY_LEADS=5
+```
+
+### 4. 初始化数据库
+
+运行初始化脚本:
+```bash
+npm run init-db
+```
+
+此脚本将:
+- ✅ 创建数据库(如果不存在)
+- ✅ 创建所有必需的数据表
+- ✅ 创建默认管理员账号(admin / admin123)
+
+如果看到以下输出,说明初始化成功:
+```
+数据库连接成功
+数据库 customer_crm 创建成功
+用户表创建成功
+客户报备表创建成功
+...
+默认管理员账号创建成功 (用户名: admin, 密码: admin123)
+数据库初始化完成!
+```
+
+### 5. 创建测试数据(可选)
+
+如果您想要一些测试数据来体验系统:
+```bash
+node scripts/createTestData.js
+```
+
+这将创建:
+- 3个测试用户(2个销售 + 1个经理)
+- 25个测试客户(20个正在跟进,5个在公海池)
+- 若干跟进记录
+
+### 6. 启动服务
+
+开发环境(推荐,支持热重载):
+```bash
+npm run dev
+```
+
+生产环境:
+```bash
+npm start
+```
+
+看到以下输出说明启动成功:
+```
+==================================================
+客户报备管理系统 (Customer Registration CRM)
+==================================================
+服务器运行在: http://localhost:3000
+环境: development
+API 文档: http://localhost:3000/api
+==================================================
+定时任务已启动
+- 保护期检查: 每小时执行一次
+- 到期提醒: 每天 09:00 执行
+```
+
+### 7. 访问系统
+
+打开浏览器访问:
+```
+http://localhost:3000
+```
+
+使用默认账号登录:
+- 用户名:`admin`
+- 密码:`admin123`
+
+## 🧪 测试 API
+
+### 使用 curl 测试
+
+1. 登录获取 Token:
+```bash
+curl -X POST http://localhost:3000/api/auth/login \
+  -H "Content-Type: application/json" \
+  -d '{"username":"admin","password":"admin123"}'
+```
+
+2. 使用 Token 访问 API:
+```bash
+# 替换 YOUR_TOKEN 为上一步获得的 token
+curl -X GET http://localhost:3000/api/stats/dashboard \
+  -H "Authorization: Bearer YOUR_TOKEN"
+```
+
+### 使用 Postman 测试
+
+1. 导入环境变量:
+   - URL: `http://localhost:3000`
+   - 创建变量 `token`
+
+2. 测试登录接口:
+   - POST `/api/auth/login`
+   - Body: `{"username":"admin","password":"admin123"}`
+   - 保存返回的 token
+
+3. 测试其他接口时,在 Headers 中添加:
+   - Key: `Authorization`
+   - Value: `Bearer {{token}}`
+
+## 🔧 常见问题
+
+### 问题1:数据库连接失败
+
+**错误信息**:
+```
+ER_ACCESS_DENIED_ERROR: Access denied for user 'root'@'localhost'
+```
+
+**解决方案**:
+- 检查 `.env` 文件中的数据库密码是否正确
+- 确认 MySQL 服务已启动:`mysql.server status`
+- 测试数据库连接:`mysql -u root -p`
+
+### 问题2:端口被占用
+
+**错误信息**:
+```
+Error: listen EADDRINUSE: address already in use :::3000
+```
+
+**解决方案**:
+1. 更改端口:修改 `.env` 文件中的 `PORT=3000` 为其他端口
+2. 或杀掉占用端口的进程:
+```bash
+# 查找占用端口的进程
+lsof -i :3000
+
+# 杀掉进程(替换 PID)
+kill -9 PID
+```
+
+### 问题3:npm install 失败
+
+**解决方案**:
+1. 清理 npm 缓存:
+```bash
+npm cache clean --force
+```
+
+2. 删除 node_modules 重新安装:
+```bash
+rm -rf node_modules
+npm install
+```
+
+3. 使用国内镜像(如果网络慢):
+```bash
+npm config set registry https://registry.npmmirror.com
+npm install
+```
+
+### 问题4:初始化数据库时出错
+
+**解决方案**:
+1. 确保 MySQL 用户有创建数据库的权限:
+```sql
+GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost';
+FLUSH PRIVILEGES;
+```
+
+2. 手动创建数据库后再运行初始化脚本:
+```sql
+CREATE DATABASE customer_crm DEFAULT CHARACTER SET utf8mb4;
+```
+
+## 🎯 下一步
+
+安装完成后,您可以:
+
+1. **阅读 API 文档**:查看 `README.md` 了解所有 API 接口
+2. **创建测试数据**:运行 `node scripts/createTestData.js`
+3. **修改配置**:根据需要调整 `.env` 中的业务配置
+4. **开始开发**:基于现有代码进行二次开发
+
+## 📦 生产环境部署
+
+生产环境部署建议:
+
+1. 使用 PM2 进程管理器:
+```bash
+npm install -g pm2
+pm2 start src/server.js --name customer-crm
+pm2 save
+pm2 startup
+```
+
+2. 使用 Nginx 反向代理
+3. 配置 HTTPS 证书
+4. 定期备份数据库
+5. 设置日志轮转
+
+详细部署指南请参考生产环境文档。
+
+## 💬 获取帮助
+
+如遇到问题:
+1. 查看日志输出
+2. 检查 `.env` 配置
+3. 参考 `README.md`
+4. 提交 Issue
+
+---
+
+**祝您安装顺利!** 🎉

+ 303 - 0
QUICKSTART.md

@@ -0,0 +1,303 @@
+# 快速启动指南 ⚡
+
+> 5分钟快速上手客户报备管理系统
+
+## 🎯 快速启动(3步)
+
+### 1️⃣ 安装依赖
+```bash
+npm install
+```
+
+### 2️⃣ 配置数据库
+```bash
+# 复制配置文件
+cp .env.example .env
+
+# 编辑 .env 文件,修改数据库密码
+# DB_PASSWORD=你的MySQL密码
+```
+
+### 3️⃣ 初始化并启动
+```bash
+# 初始化数据库
+npm run init-db
+
+# 启动服务(开发模式)
+npm run dev
+```
+
+**访问系统**:http://localhost:3000
+
+**默认账号**:`admin` / `admin123`
+
+## 🎮 可选:创建测试数据
+
+```bash
+npm run test-data
+```
+
+这将创建:
+- ✅ 3个测试用户
+- ✅ 25个测试客户
+- ✅ 多条跟进记录
+
+测试账号:
+- `sales001` / `123456` (销售-张三)
+- `sales002` / `123456` (销售-李四)
+- `manager001` / `123456` (经理-王经理)
+
+## 📝 核心功能体验
+
+### 1. 报备新客户
+
+```bash
+# 获取 Token(替换为你的实际密码)
+TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
+  -H "Content-Type: application/json" \
+  -d '{"username":"admin","password":"admin123"}' \
+  | grep -o '"token":"[^"]*"' \
+  | cut -d'"' -f4)
+
+# 创建客户报备
+curl -X POST http://localhost:3000/api/customers \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "customer_name": "测试科技有限公司",
+    "industry": "互联网/软件",
+    "region": "北京",
+    "demand_description": "需要CRM系统",
+    "source": "展会"
+  }'
+```
+
+### 2. 查看我的客户
+
+```bash
+curl -X GET "http://localhost:3000/api/customers?status=following" \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+### 3. 查看个人仪表盘
+
+```bash
+curl -X GET http://localhost:3000/api/stats/dashboard \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+### 4. 查看公海池
+
+```bash
+curl -X GET http://localhost:3000/api/pool/customers \
+  -H "Authorization: Bearer $TOKEN"
+```
+
+## 🌐 使用浏览器测试
+
+打开浏览器访问:http://localhost:3000
+
+系统提供了一个简单的 Web 界面,包含:
+- ✅ 用户登录
+- ✅ 个人仪表盘
+- ✅ API 文档展示
+
+## 📚 下一步
+
+1. **阅读完整文档**
+   - 📖 [README.md](README.md) - 完整功能说明
+   - 🔧 [INSTALL.md](INSTALL.md) - 详细安装指南
+   - 📡 [API_EXAMPLES.md](API_EXAMPLES.md) - API 调用示例
+
+2. **了解业务流程**
+   ```
+   报备客户 → 查重验证 → 进入保护期(30天) → 
+   跟进客户 → 更新状态 → 到期释放 → 公海池
+   ```
+
+3. **熟悉角色权限**
+   - 👤 销售:报备、跟进自己的客户
+   - 👔 经理:管理团队、审批申请
+   - 👑 总监:查看部门数据
+   - ⚙️ 管理员:系统管理
+
+## 🛠️ 常用命令
+
+```bash
+# 开发模式(热重载)
+npm run dev
+
+# 生产模式
+npm start
+
+# 初始化数据库
+npm run init-db
+
+# 创建测试数据
+npm run test-data
+
+# 清空数据库(保留管理员)
+npm run clean-db
+```
+
+## ⚙️ 核心配置
+
+在 `.env` 文件中配置:
+
+```env
+# 服务器端口
+PORT=3000
+
+# 数据库连接
+DB_HOST=localhost
+DB_PORT=3306
+DB_USER=root
+DB_PASSWORD=your_password
+DB_NAME=customer_crm
+
+# 业务规则
+DEFAULT_PROTECTION_DAYS=30    # 保护期天数
+MAX_DAILY_LEADS=5             # 每日最大领取数
+SIMILARITY_THRESHOLD=0.8      # 查重相似度阈值
+```
+
+## 🔥 常见问题
+
+### Q1: 数据库连接失败?
+**A:** 检查 `.env` 文件中的数据库密码是否正确,确保 MySQL 已启动。
+
+### Q2: 端口被占用?
+**A:** 修改 `.env` 中的 `PORT=3000` 为其他端口。
+
+### Q3: npm install 很慢?
+**A:** 使用国内镜像:
+```bash
+npm config set registry https://registry.npmmirror.com
+npm install
+```
+
+### Q4: 忘记管理员密码?
+**A:** 重新运行初始化脚本:
+```bash
+npm run clean-db
+npm run init-db
+```
+密码将重置为 `admin123`
+
+## 📊 系统监控
+
+查看系统日志:
+```bash
+# 实时查看日志
+npm run dev
+
+# 日志中会显示:
+# - 请求日志
+# - 数据库操作
+# - 定时任务执行
+# - 错误信息
+```
+
+## 🎯 业务场景演示
+
+### 场景1:销售报备客户
+1. 登录系统(sales001)
+2. 查重检查
+3. 创建客户报备
+4. 添加跟进记录
+
+### 场景2:客户成交
+1. 查看客户列表
+2. 选择客户添加跟进
+3. 更新状态为"已成交"
+
+### 场景3:公海池领取
+1. 查看公海池客户
+2. 筛选感兴趣的客户
+3. 领取客户(获得新的30天保护期)
+
+### 场景4:延期申请
+1. 客户保护期即将到期
+2. 提交延期申请
+3. 经理审批通过
+4. 保护期自动延长
+
+## 🎨 自定义开发
+
+### 修改保护期天数
+在 `.env` 中修改:
+```env
+DEFAULT_PROTECTION_DAYS=45
+```
+
+### 修改每日领取限制
+```env
+MAX_DAILY_LEADS=10
+```
+
+### 修改定时任务
+编辑 `src/utils/scheduler.js`:
+```javascript
+// 每2小时检查一次
+cron.schedule('0 */2 * * *', checkProtectionExpiry);
+
+// 每天下午3点提醒
+cron.schedule('0 15 * * *', sendExpiryReminders);
+```
+
+## 🚀 生产环境部署
+
+### 使用 PM2
+```bash
+# 安装 PM2
+npm install -g pm2
+
+# 启动服务
+pm2 start src/server.js --name customer-crm
+
+# 保存配置
+pm2 save
+
+# 开机自启
+pm2 startup
+```
+
+### Nginx 配置示例
+```nginx
+server {
+    listen 80;
+    server_name your-domain.com;
+
+    location / {
+        proxy_pass http://localhost:3000;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection 'upgrade';
+        proxy_set_header Host $host;
+        proxy_cache_bypass $http_upgrade;
+    }
+}
+```
+
+## 📞 获取帮助
+
+- 📖 查看完整文档:[README.md](README.md)
+- 💡 查看 API 示例:[API_EXAMPLES.md](API_EXAMPLES.md)
+- 🐛 遇到问题?提交 Issue
+- 💬 需要定制?联系开发团队
+
+## ✅ 检查清单
+
+启动前确认:
+- [ ] Node.js >= 14.x 已安装
+- [ ] MySQL >= 5.7 已安装并运行
+- [ ] 已执行 `npm install`
+- [ ] 已配置 `.env` 文件
+- [ ] 已执行 `npm run init-db`
+- [ ] 能访问 http://localhost:3000
+
+---
+
+**现在开始使用吧!** 🎉
+
+有问题随时查看文档或提交 Issue。

+ 426 - 0
README.md

@@ -0,0 +1,426 @@
+# 客户报备管理系统 (Customer Registration CRM)
+
+## 📋 项目简介
+
+基于 Node.js + Express + MySQL 的企业级客户报备管理系统,实现标准化客户报备流程,防止销售撞单抢单,实现客户资源公司化、流程规范化、管理可视化。
+
+### 核心功能
+
+- ✅ **客户报备与查重** - 支持精确匹配和模糊匹配,防止重复报备
+- ✅ **保护期管理** - 默认30天保护期,自动到期提醒
+- ✅ **公海池管理** - 客户释放、领取,限制每日领取次数
+- ✅ **审批流程** - 延期申请、强制释放、协同跟进
+- ✅ **数据报表** - 个人仪表盘、团队统计、来源分析
+- ✅ **角色权限** - 销售、销售经理、销售总监、系统管理员
+- ✅ **自动化任务** - 定时检查保护期到期、自动提醒
+
+## 🚀 快速开始
+
+### 环境要求
+
+- Node.js >= 14.x
+- MySQL >= 5.7
+- npm 或 yarn
+
+### 安装步骤
+
+1. **克隆项目**
+```bash
+cd /Users/ly/CodeBuddy/20260114134435
+```
+
+2. **安装依赖**
+```bash
+npm install
+```
+
+3. **配置环境变量**
+```bash
+cp .env.example .env
+```
+
+编辑 `.env` 文件,配置数据库连接信息:
+```env
+PORT=3000
+NODE_ENV=development
+
+DB_HOST=localhost
+DB_PORT=3306
+DB_USER=root
+DB_PASSWORD=your_password
+DB_NAME=customer_crm
+
+JWT_SECRET=your_jwt_secret_key_change_this_in_production
+JWT_EXPIRES_IN=7d
+
+DEFAULT_PROTECTION_DAYS=30
+MAX_DAILY_LEADS=5
+SIMILARITY_THRESHOLD=0.8
+```
+
+4. **初始化数据库**
+```bash
+npm run init-db
+```
+
+这将自动创建数据库表结构,并生成默认管理员账号:
+- 用户名:`admin`
+- 密码:`admin123`
+
+5. **启动服务**
+
+开发环境(带热重载):
+```bash
+npm run dev
+```
+
+生产环境:
+```bash
+npm start
+```
+
+6. **访问系统**
+
+打开浏览器访问:http://localhost:3000
+
+API 基础路径:http://localhost:3000/api
+
+## 📚 API 文档
+
+### 认证接口
+
+#### 用户登录
+```http
+POST /api/auth/login
+Content-Type: application/json
+
+{
+  "username": "admin",
+  "password": "admin123"
+}
+```
+
+响应:
+```json
+{
+  "success": true,
+  "data": {
+    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+    "user": {
+      "id": "uuid",
+      "username": "admin",
+      "real_name": "系统管理员",
+      "role": "admin"
+    }
+  }
+}
+```
+
+#### 获取当前用户信息
+```http
+GET /api/auth/me
+Authorization: Bearer {token}
+```
+
+### 客户管理接口
+
+#### 客户查重
+```http
+GET /api/customers/check-duplicate?customer_name=测试公司
+Authorization: Bearer {token}
+```
+
+#### 创建客户报备
+```http
+POST /api/customers
+Authorization: Bearer {token}
+Content-Type: application/json
+
+{
+  "customer_name": "XX有限公司",
+  "industry": "互联网/软件",
+  "region": "北京",
+  "contact_person": "张三",
+  "contact_phone": "13800138000",
+  "demand_description": "需要CRM系统",
+  "source": "转介绍"
+}
+```
+
+#### 获取客户列表
+```http
+GET /api/customers?page=1&limit=20&status=following&keyword=测试
+Authorization: Bearer {token}
+```
+
+参数说明:
+- `page`: 页码(默认1)
+- `limit`: 每页数量(默认20)
+- `status`: 客户状态(following/won/lost/released)
+- `keyword`: 搜索关键词
+
+#### 获取客户详情
+```http
+GET /api/customers/{customer_id}
+Authorization: Bearer {token}
+```
+
+#### 添加跟进记录
+```http
+POST /api/customers/followup
+Authorization: Bearer {token}
+Content-Type: application/json
+
+{
+  "customer_id": "uuid",
+  "followup_type": "call",
+  "content": "电话沟通,客户表示有兴趣",
+  "next_plan": "下周上门拜访"
+}
+```
+
+### 公海池接口
+
+#### 获取公海池客户
+```http
+GET /api/pool/customers?page=1&limit=20&industry=互联网
+Authorization: Bearer {token}
+```
+
+#### 领取客户
+```http
+POST /api/pool/claim
+Authorization: Bearer {token}
+Content-Type: application/json
+
+{
+  "customer_id": "uuid"
+}
+```
+
+#### 释放客户到公海池
+```http
+POST /api/pool/release
+Authorization: Bearer {token}
+Content-Type: application/json
+
+{
+  "customer_id": "uuid",
+  "release_reason": "客户暂无需求"
+}
+```
+
+### 审批流程接口
+
+#### 创建审批申请
+```http
+POST /api/approvals
+Authorization: Bearer {token}
+Content-Type: application/json
+
+{
+  "type": "extension",
+  "customer_id": "uuid",
+  "reason": "客户决策周期较长,需要延长跟进时间",
+  "extension_days": 15
+}
+```
+
+审批类型:
+- `extension`: 延期申请
+- `force_release`: 强制释放
+- `collaboration`: 协同跟进
+
+#### 获取待审批列表
+```http
+GET /api/approvals/pending?page=1&limit=20
+Authorization: Bearer {token}
+```
+
+#### 处理审批
+```http
+POST /api/approvals/{approval_id}/process
+Authorization: Bearer {token}
+Content-Type: application/json
+
+{
+  "status": "approved",
+  "result_comment": "同意延期"
+}
+```
+
+### 统计报表接口
+
+#### 个人仪表盘
+```http
+GET /api/stats/dashboard
+Authorization: Bearer {token}
+```
+
+#### 团队统计
+```http
+GET /api/stats/team?start_date=2024-01-01&end_date=2024-01-31&period=day
+Authorization: Bearer {token}
+```
+
+#### 客户来源分析
+```http
+GET /api/stats/source-analysis
+Authorization: Bearer {token}
+```
+
+## 🎭 角色权限说明
+
+### 销售(sales)
+- 报备客户
+- 查看和管理自己的客户
+- 添加跟进记录
+- 从公海池领取客户
+- 提交延期申请
+
+### 销售经理(sales_manager)
+- 销售的所有权限
+- 查看团队所有客户
+- 审批延期申请
+- 强制释放客户
+- 查看团队统计报表
+
+### 销售总监(sales_director)
+- 销售经理的所有权限
+- 查看部门所有客户
+- 查看更高级别的统计报表
+
+### 系统管理员(admin)
+- 所有权限
+- 用户管理
+- 系统配置
+
+## 🗄️ 数据库设计
+
+### 主要数据表
+
+- **users** - 用户表
+- **customers** - 客户报备表
+- **followup_records** - 跟进记录表
+- **pool_claim_records** - 公海池领取记录表
+- **approvals** - 审批流程表
+- **operation_logs** - 操作日志表
+- **attachments** - 附件表
+
+## ⏰ 定时任务
+
+系统包含以下自动化任务:
+
+1. **保护期检查** - 每小时执行
+   - 自动释放到期的客户到公海池
+
+2. **到期提醒** - 每天 09:00 执行
+   - 提醒3天后到期的客户
+   - 提醒1天后到期的客户
+
+## 🔒 安全性
+
+- JWT Token 认证
+- 密码 bcrypt 加密
+- 角色权限控制
+- 数据权限隔离
+- 操作日志完整记录
+- SQL 注入防护(参数化查询)
+
+## 🛠️ 技术栈
+
+- **后端框架**: Express.js
+- **数据库**: MySQL
+- **认证**: JWT (jsonwebtoken)
+- **密码加密**: bcryptjs
+- **定时任务**: node-cron
+- **日期处理**: moment
+- **字符串相似度**: string-similarity
+
+## 📦 项目结构
+
+```
+customer-crm/
+├── src/
+│   ├── config/
+│   │   └── database.js          # 数据库配置
+│   ├── controllers/
+│   │   ├── authController.js    # 认证控制器
+│   │   ├── customerController.js # 客户管理控制器
+│   │   ├── poolController.js    # 公海池控制器
+│   │   ├── approvalController.js # 审批流程控制器
+│   │   └── statsController.js   # 统计报表控制器
+│   ├── middleware/
+│   │   ├── auth.js              # 认证中间件
+│   │   └── logger.js            # 日志中间件
+│   ├── routes/
+│   │   └── index.js             # 路由配置
+│   ├── utils/
+│   │   └── scheduler.js         # 定时任务
+│   └── server.js                # 服务器入口
+├── scripts/
+│   └── initDatabase.js          # 数据库初始化脚本
+├── public/
+│   └── index.html               # 前端页面
+├── package.json
+├── .env.example
+└── README.md
+```
+
+## 🔧 配置说明
+
+### 业务配置
+
+在 `.env` 文件中可配置:
+
+- `DEFAULT_PROTECTION_DAYS`: 默认保护期天数(默认30天)
+- `MAX_DAILY_LEADS`: 每日最大领取客户数(默认5个)
+- `SIMILARITY_THRESHOLD`: 客户名称相似度阈值(默认0.8)
+
+### 定时任务配置
+
+在 `src/utils/scheduler.js` 中可修改定时任务执行时间。
+
+## 🐛 常见问题
+
+### 数据库连接失败
+检查 `.env` 文件中的数据库配置是否正确,确保 MySQL 服务已启动。
+
+### JWT Token 过期
+重新登录获取新的 Token。
+
+### 领取客户失败
+检查是否达到每日领取上限,或客户是否已被其他人领取。
+
+## 📝 开发计划
+
+### 第一期(MVP)✅
+- [x] 客户报备与查重
+- [x] 30天保护期管理
+- [x] 基础公海池
+- [x] 个人客户列表
+- [x] 团队报备统计
+
+### 第二期规划
+- [ ] 微信/钉钉通知集成
+- [ ] 更丰富的数据报表
+- [ ] 移动端 App
+- [ ] 与主流 CRM 系统集成
+- [ ] 客户标签系统
+- [ ] 智能客户分配
+
+## 📄 许可证
+
+MIT License
+
+## 👥 贡献
+
+欢迎提交 Issue 和 Pull Request!
+
+## 📧 联系方式
+
+如有问题,请提交 Issue 或联系开发团队。
+
+---
+
+**祝您使用愉快!** 🎉

+ 36 - 0
package.json

@@ -0,0 +1,36 @@
+{
+  "name": "customer-registration-crm",
+  "version": "1.0.0",
+  "description": "客户报备管理系统",
+  "main": "src/server.js",
+  "scripts": {
+    "start": "node src/server.js",
+    "dev": "nodemon src/server.js",
+    "init-db": "node scripts/initDatabase.js",
+    "test-data": "node scripts/createTestData.js",
+    "clean-db": "node scripts/cleanDatabase.js"
+  },
+  "keywords": [
+    "crm",
+    "customer",
+    "registration"
+  ],
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "bcryptjs": "^2.4.3",
+    "cors": "^2.8.5",
+    "dotenv": "^16.3.1",
+    "express": "^4.18.2",
+    "express-validator": "^7.0.1",
+    "jsonwebtoken": "^9.0.2",
+    "moment": "^2.29.4",
+    "multer": "^1.4.5-lts.1",
+    "mysql2": "^3.6.5",
+    "node-cron": "^3.0.3",
+    "string-similarity": "^4.0.4"
+  },
+  "devDependencies": {
+    "nodemon": "^3.0.2"
+  }
+}

+ 468 - 0
public/index.html

@@ -0,0 +1,468 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>客户报备管理系统</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            min-height: 100vh;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+
+        .container {
+            width: 90%;
+            max-width: 1200px;
+            background: white;
+            border-radius: 20px;
+            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+            padding: 40px;
+        }
+
+        .header {
+            text-align: center;
+            margin-bottom: 40px;
+        }
+
+        .header h1 {
+            color: #333;
+            font-size: 32px;
+            margin-bottom: 10px;
+        }
+
+        .header p {
+            color: #666;
+            font-size: 16px;
+        }
+
+        .login-form {
+            max-width: 400px;
+            margin: 0 auto;
+            display: none;
+        }
+
+        .login-form.active {
+            display: block;
+        }
+
+        .form-group {
+            margin-bottom: 20px;
+        }
+
+        .form-group label {
+            display: block;
+            margin-bottom: 8px;
+            color: #333;
+            font-weight: 500;
+        }
+
+        .form-group input {
+            width: 100%;
+            padding: 12px 15px;
+            border: 2px solid #e0e0e0;
+            border-radius: 8px;
+            font-size: 14px;
+            transition: border-color 0.3s;
+        }
+
+        .form-group input:focus {
+            outline: none;
+            border-color: #667eea;
+        }
+
+        .btn {
+            width: 100%;
+            padding: 14px;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            border: none;
+            border-radius: 8px;
+            font-size: 16px;
+            font-weight: 500;
+            cursor: pointer;
+            transition: transform 0.2s, box-shadow 0.2s;
+        }
+
+        .btn:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
+        }
+
+        .btn:active {
+            transform: translateY(0);
+        }
+
+        .dashboard {
+            display: none;
+        }
+
+        .dashboard.active {
+            display: block;
+        }
+
+        .user-info {
+            background: #f5f5f5;
+            padding: 20px;
+            border-radius: 10px;
+            margin-bottom: 30px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+
+        .user-info h3 {
+            color: #333;
+            font-size: 18px;
+        }
+
+        .user-info .role {
+            display: inline-block;
+            padding: 4px 12px;
+            background: #667eea;
+            color: white;
+            border-radius: 20px;
+            font-size: 12px;
+            margin-left: 10px;
+        }
+
+        .btn-logout {
+            padding: 8px 20px;
+            background: #ff4757;
+            color: white;
+            border: none;
+            border-radius: 6px;
+            cursor: pointer;
+            font-size: 14px;
+        }
+
+        .stats-grid {
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+            gap: 20px;
+            margin-bottom: 30px;
+        }
+
+        .stat-card {
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            padding: 25px;
+            border-radius: 10px;
+            box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
+        }
+
+        .stat-card h4 {
+            font-size: 14px;
+            margin-bottom: 10px;
+            opacity: 0.9;
+        }
+
+        .stat-card .value {
+            font-size: 32px;
+            font-weight: bold;
+        }
+
+        .api-docs {
+            background: #f9f9f9;
+            padding: 30px;
+            border-radius: 10px;
+            margin-top: 30px;
+        }
+
+        .api-docs h3 {
+            color: #333;
+            margin-bottom: 20px;
+        }
+
+        .api-item {
+            background: white;
+            padding: 15px;
+            margin-bottom: 10px;
+            border-radius: 8px;
+            border-left: 4px solid #667eea;
+        }
+
+        .api-item .method {
+            display: inline-block;
+            padding: 4px 10px;
+            background: #667eea;
+            color: white;
+            border-radius: 4px;
+            font-size: 12px;
+            font-weight: bold;
+            margin-right: 10px;
+        }
+
+        .api-item .method.post {
+            background: #28a745;
+        }
+
+        .api-item .method.put {
+            background: #ffc107;
+        }
+
+        .api-item .method.delete {
+            background: #dc3545;
+        }
+
+        .alert {
+            padding: 12px 20px;
+            border-radius: 8px;
+            margin-bottom: 20px;
+            display: none;
+        }
+
+        .alert.success {
+            background: #d4edda;
+            color: #155724;
+            border: 1px solid #c3e6cb;
+        }
+
+        .alert.error {
+            background: #f8d7da;
+            color: #721c24;
+            border: 1px solid #f5c6cb;
+        }
+
+        .alert.active {
+            display: block;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <div class="header">
+            <h1>🎯 客户报备管理系统</h1>
+            <p>Customer Registration CRM System</p>
+        </div>
+
+        <div id="alert" class="alert"></div>
+
+        <!-- 登录表单 -->
+        <div id="loginForm" class="login-form active">
+            <form onsubmit="handleLogin(event)">
+                <div class="form-group">
+                    <label for="username">用户名</label>
+                    <input type="text" id="username" name="username" value="admin" required>
+                </div>
+                <div class="form-group">
+                    <label for="password">密码</label>
+                    <input type="password" id="password" name="password" value="admin123" required>
+                </div>
+                <button type="submit" class="btn">登录</button>
+            </form>
+            <div style="margin-top: 20px; text-align: center; color: #666; font-size: 14px;">
+                <p>默认账号: admin / admin123</p>
+            </div>
+        </div>
+
+        <!-- 仪表盘 -->
+        <div id="dashboard" class="dashboard">
+            <div class="user-info">
+                <div>
+                    <h3>欢迎回来,<span id="userName"></span> <span id="userRole" class="role"></span></h3>
+                </div>
+                <button class="btn-logout" onclick="handleLogout()">退出登录</button>
+            </div>
+
+            <div class="stats-grid" id="statsGrid">
+                <div class="stat-card">
+                    <h4>我的报备</h4>
+                    <div class="value" id="totalCustomers">-</div>
+                </div>
+                <div class="stat-card">
+                    <h4>跟进中</h4>
+                    <div class="value" id="followingCustomers">-</div>
+                </div>
+                <div class="stat-card">
+                    <h4>已成交</h4>
+                    <div class="value" id="wonCustomers">-</div>
+                </div>
+                <div class="stat-card">
+                    <h4>转化率</h4>
+                    <div class="value" id="conversionRate">-</div>
+                </div>
+            </div>
+
+            <div class="api-docs">
+                <h3>📚 API 接口文档</h3>
+                
+                <h4 style="margin: 20px 0 10px; color: #555;">认证接口</h4>
+                <div class="api-item">
+                    <span class="method post">POST</span>
+                    <code>/api/auth/login</code>
+                    <p style="margin-top: 8px; color: #666; font-size: 14px;">用户登录</p>
+                </div>
+                <div class="api-item">
+                    <span class="method">GET</span>
+                    <code>/api/auth/me</code>
+                    <p style="margin-top: 8px; color: #666; font-size: 14px;">获取当前用户信息</p>
+                </div>
+
+                <h4 style="margin: 20px 0 10px; color: #555;">客户管理</h4>
+                <div class="api-item">
+                    <span class="method">GET</span>
+                    <code>/api/customers/check-duplicate?customer_name=xxx</code>
+                    <p style="margin-top: 8px; color: #666; font-size: 14px;">客户查重</p>
+                </div>
+                <div class="api-item">
+                    <span class="method post">POST</span>
+                    <code>/api/customers</code>
+                    <p style="margin-top: 8px; color: #666; font-size: 14px;">创建客户报备</p>
+                </div>
+                <div class="api-item">
+                    <span class="method">GET</span>
+                    <code>/api/customers</code>
+                    <p style="margin-top: 8px; color: #666; font-size: 14px;">获取客户列表</p>
+                </div>
+
+                <h4 style="margin: 20px 0 10px; color: #555;">公海池管理</h4>
+                <div class="api-item">
+                    <span class="method">GET</span>
+                    <code>/api/pool/customers</code>
+                    <p style="margin-top: 8px; color: #666; font-size: 14px;">获取公海池客户</p>
+                </div>
+                <div class="api-item">
+                    <span class="method post">POST</span>
+                    <code>/api/pool/claim</code>
+                    <p style="margin-top: 8px; color: #666; font-size: 14px;">领取公海池客户</p>
+                </div>
+
+                <h4 style="margin: 20px 0 10px; color: #555;">统计报表</h4>
+                <div class="api-item">
+                    <span class="method">GET</span>
+                    <code>/api/stats/dashboard</code>
+                    <p style="margin-top: 8px; color: #666; font-size: 14px;">个人仪表盘</p>
+                </div>
+                <div class="api-item">
+                    <span class="method">GET</span>
+                    <code>/api/stats/team</code>
+                    <p style="margin-top: 8px; color: #666; font-size: 14px;">团队统计</p>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        const API_BASE = '/api';
+        let token = localStorage.getItem('token');
+
+        // 页面加载时检查登录状态
+        if (token) {
+            checkAuth();
+        }
+
+        function showAlert(message, type = 'success') {
+            const alert = document.getElementById('alert');
+            alert.textContent = message;
+            alert.className = `alert ${type} active`;
+            setTimeout(() => {
+                alert.className = 'alert';
+            }, 3000);
+        }
+
+        async function handleLogin(event) {
+            event.preventDefault();
+            const username = document.getElementById('username').value;
+            const password = document.getElementById('password').value;
+
+            try {
+                const response = await fetch(`${API_BASE}/auth/login`, {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({ username, password })
+                });
+
+                const data = await response.json();
+
+                if (data.success) {
+                    token = data.data.token;
+                    localStorage.setItem('token', token);
+                    showAlert('登录成功!', 'success');
+                    showDashboard(data.data.user);
+                    loadDashboardData();
+                } else {
+                    showAlert(data.message || '登录失败', 'error');
+                }
+            } catch (error) {
+                showAlert('网络错误,请稍后重试', 'error');
+                console.error(error);
+            }
+        }
+
+        async function checkAuth() {
+            try {
+                const response = await fetch(`${API_BASE}/auth/me`, {
+                    headers: { 'Authorization': `Bearer ${token}` }
+                });
+
+                const data = await response.json();
+
+                if (data.success) {
+                    showDashboard(data.data);
+                    loadDashboardData();
+                } else {
+                    handleLogout();
+                }
+            } catch (error) {
+                console.error(error);
+                handleLogout();
+            }
+        }
+
+        function showDashboard(user) {
+            document.getElementById('loginForm').classList.remove('active');
+            document.getElementById('dashboard').classList.add('active');
+            document.getElementById('userName').textContent = user.real_name;
+            document.getElementById('userRole').textContent = getRoleName(user.role);
+        }
+
+        async function loadDashboardData() {
+            try {
+                const response = await fetch(`${API_BASE}/stats/dashboard`, {
+                    headers: { 'Authorization': `Bearer ${token}` }
+                });
+
+                const data = await response.json();
+
+                if (data.success) {
+                    const summary = data.data.summary;
+                    document.getElementById('totalCustomers').textContent = summary.total;
+                    document.getElementById('followingCustomers').textContent = summary.following;
+                    document.getElementById('wonCustomers').textContent = summary.won;
+                    document.getElementById('conversionRate').textContent = summary.conversion_rate;
+                }
+            } catch (error) {
+                console.error('加载仪表盘数据失败:', error);
+            }
+        }
+
+        function handleLogout() {
+            localStorage.removeItem('token');
+            token = null;
+            document.getElementById('loginForm').classList.add('active');
+            document.getElementById('dashboard').classList.remove('active');
+            showAlert('已退出登录', 'success');
+        }
+
+        function getRoleName(role) {
+            const roles = {
+                'sales': '销售',
+                'sales_manager': '销售经理',
+                'sales_director': '销售总监',
+                'admin': '管理员'
+            };
+            return roles[role] || role;
+        }
+    </script>
+</body>
+</html>

+ 48 - 0
scripts/cleanDatabase.js

@@ -0,0 +1,48 @@
+require('dotenv').config();
+const pool = require('../src/config/database');
+
+async function cleanDatabase() {
+  const connection = await pool.getConnection();
+  
+  try {
+    console.log('⚠️  警告:此操作将清空所有数据(保留管理员账号)\n');
+    
+    // 清空表(保留结构)
+    await connection.query('SET FOREIGN_KEY_CHECKS = 0');
+    
+    await connection.query('DELETE FROM operation_logs');
+    console.log('✅ 清空操作日志表');
+    
+    await connection.query('DELETE FROM attachments');
+    console.log('✅ 清空附件表');
+    
+    await connection.query('DELETE FROM approvals');
+    console.log('✅ 清空审批表');
+    
+    await connection.query('DELETE FROM pool_claim_records');
+    console.log('✅ 清空公海池领取记录表');
+    
+    await connection.query('DELETE FROM followup_records');
+    console.log('✅ 清空跟进记录表');
+    
+    await connection.query('DELETE FROM customers');
+    console.log('✅ 清空客户表');
+    
+    await connection.query("DELETE FROM users WHERE username != 'admin'");
+    console.log('✅ 清空用户表(保留管理员)');
+    
+    await connection.query('SET FOREIGN_KEY_CHECKS = 1');
+    
+    console.log('\n🎉 数据库清理完成!\n');
+    console.log('管理员账号已保留: admin / admin123\n');
+    
+  } catch (error) {
+    console.error('清理数据库失败:', error);
+    process.exit(1);
+  } finally {
+    connection.release();
+    await pool.end();
+  }
+}
+
+cleanDatabase();

+ 173 - 0
scripts/createTestData.js

@@ -0,0 +1,173 @@
+require('dotenv').config();
+const bcrypt = require('bcryptjs');
+const pool = require('../src/config/database');
+const crypto = require('crypto');
+const moment = require('moment');
+
+async function createTestData() {
+  const connection = await pool.getConnection();
+  
+  try {
+    console.log('开始创建测试数据...\n');
+
+    // 创建测试用户
+    const users = [
+      {
+        id: crypto.randomUUID(),
+        username: 'sales001',
+        password: await bcrypt.hash('123456', 10),
+        real_name: '张三',
+        email: 'zhangsan@example.com',
+        phone: '13800138001',
+        role: 'sales',
+        department: '销售部',
+        team: '华北团队'
+      },
+      {
+        id: crypto.randomUUID(),
+        username: 'sales002',
+        password: await bcrypt.hash('123456', 10),
+        real_name: '李四',
+        email: 'lisi@example.com',
+        phone: '13800138002',
+        role: 'sales',
+        department: '销售部',
+        team: '华北团队'
+      },
+      {
+        id: crypto.randomUUID(),
+        username: 'manager001',
+        password: await bcrypt.hash('123456', 10),
+        real_name: '王经理',
+        email: 'wangmanager@example.com',
+        phone: '13800138003',
+        role: 'sales_manager',
+        department: '销售部',
+        team: '华北团队'
+      }
+    ];
+
+    for (const user of users) {
+      await connection.query(
+        `INSERT INTO users (id, username, password, real_name, email, phone, role, department, team) 
+         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+         ON DUPLICATE KEY UPDATE username=username`,
+        [user.id, user.username, user.password, user.real_name, user.email, user.phone, user.role, user.department, user.team]
+      );
+      console.log(`✅ 创建用户: ${user.real_name} (${user.username}) - 密码: 123456`);
+    }
+
+    // 创建测试客户
+    const industries = ['互联网/软件', '制造业', '金融', '教育', '医疗', '零售'];
+    const regions = ['北京', '上海', '广州', '深圳', '杭州', '成都'];
+    const sources = ['转介绍', '展会', '自主开发', '电话营销', '网络推广'];
+    const statuses = ['following', 'following', 'following', 'won', 'lost'];
+
+    const customers = [];
+    for (let i = 1; i <= 20; i++) {
+      const status = statuses[Math.floor(Math.random() * statuses.length)];
+      const salesOwner = users[Math.floor(Math.random() * 2)]; // 随机分配给销售
+      const daysAgo = Math.floor(Math.random() * 60);
+      const reportTime = moment().subtract(daysAgo, 'days');
+      
+      customers.push({
+        id: crypto.randomUUID(),
+        customer_name: `测试客户${String(i).padStart(3, '0')}有限公司`,
+        industry: industries[Math.floor(Math.random() * industries.length)],
+        region: regions[Math.floor(Math.random() * regions.length)],
+        contact_person: `联系人${i}`,
+        contact_phone: `138${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`,
+        demand_description: `需求描述${i}:希望采购CRM系统,预算${Math.floor(Math.random() * 50) + 10}万元`,
+        source: sources[Math.floor(Math.random() * sources.length)],
+        sales_owner: salesOwner.id,
+        report_time: reportTime.format('YYYY-MM-DD HH:mm:ss'),
+        protected_end_date: moment(reportTime).add(30, 'days').format('YYYY-MM-DD HH:mm:ss'),
+        status: status,
+        last_followup: status === 'following' ? moment().subtract(Math.floor(Math.random() * 7), 'days').format('YYYY-MM-DD HH:mm:ss') : null,
+        is_in_pool: false
+      });
+    }
+
+    // 创建一些公海池客户
+    for (let i = 21; i <= 25; i++) {
+      customers.push({
+        id: crypto.randomUUID(),
+        customer_name: `公海客户${String(i).padStart(3, '0')}有限公司`,
+        industry: industries[Math.floor(Math.random() * industries.length)],
+        region: regions[Math.floor(Math.random() * regions.length)],
+        contact_person: `联系人${i}`,
+        contact_phone: `138${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`,
+        demand_description: `需求描述${i}`,
+        source: sources[Math.floor(Math.random() * sources.length)],
+        sales_owner: users[0].id,
+        report_time: moment().subtract(60, 'days').format('YYYY-MM-DD HH:mm:ss'),
+        protected_end_date: moment().subtract(10, 'days').format('YYYY-MM-DD HH:mm:ss'),
+        status: 'released',
+        release_reason: '保护期自动到期',
+        is_in_pool: true
+      });
+    }
+
+    for (const customer of customers) {
+      await connection.query(
+        `INSERT INTO customers 
+         (id, customer_name, industry, region, contact_person, contact_phone, 
+          demand_description, source, sales_owner, report_time, protected_end_date, 
+          status, last_followup, release_reason, is_in_pool) 
+         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+        [
+          customer.id, customer.customer_name, customer.industry, customer.region,
+          customer.contact_person, customer.contact_phone, customer.demand_description,
+          customer.source, customer.sales_owner, customer.report_time, customer.protected_end_date,
+          customer.status, customer.last_followup, customer.release_reason, customer.is_in_pool
+        ]
+      );
+    }
+    console.log(`\n✅ 创建 ${customers.length} 个测试客户`);
+
+    // 创建跟进记录
+    const followupTypes = ['call', 'visit', 'email', 'wechat', 'other'];
+    let followupCount = 0;
+    
+    for (const customer of customers.slice(0, 15)) {
+      if (customer.status === 'following') {
+        const recordCount = Math.floor(Math.random() * 5) + 1;
+        for (let i = 0; i < recordCount; i++) {
+          await connection.query(
+            `INSERT INTO followup_records (id, customer_id, user_id, followup_type, content, next_plan, created_at)
+             VALUES (?, ?, ?, ?, ?, ?, ?)`,
+            [
+              crypto.randomUUID(),
+              customer.id,
+              customer.sales_owner,
+              followupTypes[Math.floor(Math.random() * followupTypes.length)],
+              `跟进记录${i + 1}:与客户进行了沟通`,
+              i < recordCount - 1 ? '继续跟进' : '下周再联系',
+              moment().subtract(Math.floor(Math.random() * 30), 'days').format('YYYY-MM-DD HH:mm:ss')
+            ]
+          );
+          followupCount++;
+        }
+      }
+    }
+    console.log(`✅ 创建 ${followupCount} 条跟进记录`);
+
+    console.log('\n🎉 测试数据创建完成!\n');
+    console.log('测试账号信息:');
+    console.log('====================================');
+    console.log('管理员: admin / admin123');
+    console.log('销售1: sales001 / 123456 (张三)');
+    console.log('销售2: sales002 / 123456 (李四)');
+    console.log('经理: manager001 / 123456 (王经理)');
+    console.log('====================================\n');
+
+  } catch (error) {
+    console.error('创建测试数据失败:', error);
+    process.exit(1);
+  } finally {
+    connection.release();
+    await pool.end();
+  }
+}
+
+createTestData();

+ 199 - 0
scripts/initDatabase.js

@@ -0,0 +1,199 @@
+require('dotenv').config();
+const mysql = require('mysql2/promise');
+
+const dbConfig = {
+  host: process.env.DB_HOST,
+  port: process.env.DB_PORT,
+  user: process.env.DB_USER,
+  password: process.env.DB_PASSWORD,
+};
+
+async function initDatabase() {
+  let connection;
+  
+  try {
+    connection = await mysql.createConnection(dbConfig);
+    console.log('数据库连接成功');
+
+    // 创建数据库
+    await connection.query(`CREATE DATABASE IF NOT EXISTS ${process.env.DB_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
+    console.log(`数据库 ${process.env.DB_NAME} 创建成功`);
+
+    await connection.query(`USE ${process.env.DB_NAME}`);
+
+    // 用户表
+    await connection.query(`
+      CREATE TABLE IF NOT EXISTS users (
+        id VARCHAR(36) PRIMARY KEY,
+        username VARCHAR(50) UNIQUE NOT NULL,
+        password VARCHAR(255) NOT NULL,
+        real_name VARCHAR(50) NOT NULL,
+        email VARCHAR(100),
+        phone VARCHAR(20),
+        role ENUM('sales', 'sales_manager', 'sales_director', 'admin') NOT NULL DEFAULT 'sales',
+        department VARCHAR(50),
+        team VARCHAR(50),
+        status ENUM('active', 'inactive') DEFAULT 'active',
+        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+        INDEX idx_role (role),
+        INDEX idx_department (department),
+        INDEX idx_team (team)
+      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+    `);
+    console.log('用户表创建成功');
+
+    // 客户报备表
+    await connection.query(`
+      CREATE TABLE IF NOT EXISTS customers (
+        id VARCHAR(36) PRIMARY KEY,
+        customer_name VARCHAR(200) NOT NULL,
+        industry VARCHAR(100),
+        region VARCHAR(100),
+        contact_person VARCHAR(50),
+        contact_phone VARCHAR(20),
+        demand_description TEXT,
+        source VARCHAR(50),
+        sales_owner VARCHAR(36) NOT NULL,
+        report_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        protected_end_date TIMESTAMP,
+        status ENUM('following', 'won', 'lost', 'released') DEFAULT 'following',
+        last_followup TIMESTAMP,
+        release_reason VARCHAR(200),
+        is_in_pool BOOLEAN DEFAULT FALSE,
+        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+        FOREIGN KEY (sales_owner) REFERENCES users(id) ON DELETE RESTRICT,
+        INDEX idx_customer_name (customer_name),
+        INDEX idx_sales_owner (sales_owner),
+        INDEX idx_status (status),
+        INDEX idx_protected_end_date (protected_end_date),
+        INDEX idx_is_in_pool (is_in_pool),
+        FULLTEXT idx_ft_customer_name (customer_name)
+      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+    `);
+    console.log('客户报备表创建成功');
+
+    // 跟进记录表
+    await connection.query(`
+      CREATE TABLE IF NOT EXISTS followup_records (
+        id VARCHAR(36) PRIMARY KEY,
+        customer_id VARCHAR(36) NOT NULL,
+        user_id VARCHAR(36) NOT NULL,
+        followup_type ENUM('call', 'visit', 'email', 'wechat', 'other') DEFAULT 'call',
+        content TEXT,
+        next_plan TEXT,
+        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
+        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+        INDEX idx_customer_id (customer_id),
+        INDEX idx_user_id (user_id),
+        INDEX idx_created_at (created_at)
+      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+    `);
+    console.log('跟进记录表创建成功');
+
+    // 公海池领取记录表
+    await connection.query(`
+      CREATE TABLE IF NOT EXISTS pool_claim_records (
+        id VARCHAR(36) PRIMARY KEY,
+        customer_id VARCHAR(36) NOT NULL,
+        user_id VARCHAR(36) NOT NULL,
+        claim_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
+        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+        INDEX idx_customer_id (customer_id),
+        INDEX idx_user_id (user_id),
+        INDEX idx_claim_time (claim_time)
+      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+    `);
+    console.log('公海池领取记录表创建成功');
+
+    // 审批流程表
+    await connection.query(`
+      CREATE TABLE IF NOT EXISTS approvals (
+        id VARCHAR(36) PRIMARY KEY,
+        type ENUM('extension', 'force_release', 'collaboration') NOT NULL,
+        applicant_id VARCHAR(36) NOT NULL,
+        approver_id VARCHAR(36),
+        customer_id VARCHAR(36),
+        reason TEXT,
+        extension_days INT,
+        status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
+        result_comment TEXT,
+        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        processed_at TIMESTAMP,
+        FOREIGN KEY (applicant_id) REFERENCES users(id) ON DELETE CASCADE,
+        FOREIGN KEY (approver_id) REFERENCES users(id) ON DELETE SET NULL,
+        FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
+        INDEX idx_applicant_id (applicant_id),
+        INDEX idx_approver_id (approver_id),
+        INDEX idx_status (status),
+        INDEX idx_type (type)
+      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+    `);
+    console.log('审批流程表创建成功');
+
+    // 操作日志表
+    await connection.query(`
+      CREATE TABLE IF NOT EXISTS operation_logs (
+        id VARCHAR(36) PRIMARY KEY,
+        user_id VARCHAR(36),
+        action VARCHAR(50) NOT NULL,
+        target_type VARCHAR(50),
+        target_id VARCHAR(36),
+        details TEXT,
+        ip_address VARCHAR(45),
+        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
+        INDEX idx_user_id (user_id),
+        INDEX idx_action (action),
+        INDEX idx_created_at (created_at)
+      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+    `);
+    console.log('操作日志表创建成功');
+
+    // 附件表
+    await connection.query(`
+      CREATE TABLE IF NOT EXISTS attachments (
+        id VARCHAR(36) PRIMARY KEY,
+        customer_id VARCHAR(36) NOT NULL,
+        filename VARCHAR(255) NOT NULL,
+        original_name VARCHAR(255) NOT NULL,
+        file_path VARCHAR(500) NOT NULL,
+        file_size INT,
+        mime_type VARCHAR(100),
+        uploaded_by VARCHAR(36) NOT NULL,
+        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
+        FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE CASCADE,
+        INDEX idx_customer_id (customer_id)
+      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+    `);
+    console.log('附件表创建成功');
+
+    // 插入默认管理员账号 (密码: admin123)
+    const bcrypt = require('bcryptjs');
+    const crypto = require('crypto');
+    const adminPassword = await bcrypt.hash('admin123', 10);
+    const adminId = crypto.randomUUID();
+    
+    await connection.query(`
+      INSERT INTO users (id, username, password, real_name, role, department) 
+      VALUES (?, 'admin', ?, '系统管理员', 'admin', '管理部')
+      ON DUPLICATE KEY UPDATE username=username
+    `, [adminId, adminPassword]);
+    console.log('默认管理员账号创建成功 (用户名: admin, 密码: admin123)');
+
+    console.log('\n数据库初始化完成!');
+  } catch (error) {
+    console.error('数据库初始化失败:', error);
+    process.exit(1);
+  } finally {
+    if (connection) {
+      await connection.end();
+    }
+  }
+}
+
+initDatabase();

+ 16 - 0
src/config/database.js

@@ -0,0 +1,16 @@
+const mysql = require('mysql2/promise');
+
+const pool = mysql.createPool({
+  host: process.env.DB_HOST,
+  port: process.env.DB_PORT,
+  user: process.env.DB_USER,
+  password: process.env.DB_PASSWORD,
+  database: process.env.DB_NAME,
+  waitForConnections: true,
+  connectionLimit: 10,
+  queueLimit: 0,
+  enableKeepAlive: true,
+  keepAliveInitialDelay: 0
+});
+
+module.exports = pool;

+ 262 - 0
src/controllers/approvalController.js

@@ -0,0 +1,262 @@
+const pool = require('../config/database');
+const { logOperation } = require('../middleware/logger');
+const crypto = require('crypto');
+const moment = require('moment');
+
+// 创建审批申请
+exports.createApproval = async (req, res) => {
+  try {
+    const { type, customer_id, reason, extension_days } = req.body;
+
+    const validTypes = ['extension', 'force_release', 'collaboration'];
+    if (!validTypes.includes(type)) {
+      return res.status(400).json({ success: false, message: '无效的审批类型' });
+    }
+
+    if (!customer_id || !reason) {
+      return res.status(400).json({ success: false, message: '客户ID和申请原因不能为空' });
+    }
+
+    // 延期申请需要指定延期天数
+    if (type === 'extension' && !extension_days) {
+      return res.status(400).json({ success: false, message: '延期申请需要指定延期天数' });
+    }
+
+    // 获取审批人(销售经理或更高级别)
+    let approverId = null;
+    if (type === 'extension' || type === 'collaboration') {
+      const [managers] = await pool.query(
+        `SELECT id FROM users 
+         WHERE team = ? AND role IN ('sales_manager', 'sales_director', 'admin') 
+         LIMIT 1`,
+        [req.user.team]
+      );
+      
+      if (managers.length > 0) {
+        approverId = managers[0].id;
+      }
+    }
+
+    const approvalId = crypto.randomUUID();
+    
+    await pool.query(
+      `INSERT INTO approvals (id, type, applicant_id, approver_id, customer_id, reason, extension_days, status)
+       VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')`,
+      [approvalId, type, req.user.id, approverId, customer_id, reason, extension_days || null]
+    );
+
+    // 记录操作日志
+    await logOperation(req.user.id, 'create_approval', 'approval', approvalId, { type, customer_id }, req.ip);
+
+    res.status(201).json({
+      success: true,
+      message: '审批申请已提交',
+      data: { id: approvalId }
+    });
+  } catch (error) {
+    console.error('创建审批申请失败:', error);
+    res.status(500).json({ success: false, message: '创建审批申请失败' });
+  }
+};
+
+// 获取待审批列表
+exports.getPendingApprovals = async (req, res) => {
+  try {
+    const { page = 1, limit = 20, type } = req.query;
+    const offset = (page - 1) * limit;
+    const userId = req.user.id;
+
+    let query = `
+      SELECT a.*, 
+        u1.real_name as applicant_name, u1.team as applicant_team,
+        c.customer_name
+      FROM approvals a
+      LEFT JOIN users u1 ON a.applicant_id = u1.id
+      LEFT JOIN customers c ON a.customer_id = c.id
+      WHERE a.approver_id = ? AND a.status = 'pending'
+    `;
+    const params = [userId];
+
+    if (type) {
+      query += ' AND a.type = ?';
+      params.push(type);
+    }
+
+    query += ' ORDER BY a.created_at DESC LIMIT ? OFFSET ?';
+    params.push(parseInt(limit), parseInt(offset));
+
+    const [approvals] = await pool.query(query, params);
+
+    // 获取总数
+    const countQuery = `
+      SELECT COUNT(*) as total FROM approvals 
+      WHERE approver_id = ? AND status = 'pending'
+      ${type ? 'AND type = ?' : ''}
+    `;
+    const countParams = type ? [userId, type] : [userId];
+    const [countResult] = await pool.query(countQuery, countParams);
+
+    res.json({
+      success: true,
+      data: {
+        approvals,
+        pagination: {
+          page: parseInt(page),
+          limit: parseInt(limit),
+          total: countResult[0].total,
+          total_pages: Math.ceil(countResult[0].total / limit)
+        }
+      }
+    });
+  } catch (error) {
+    console.error('获取待审批列表失败:', error);
+    res.status(500).json({ success: false, message: '获取待审批列表失败' });
+  }
+};
+
+// 获取我的审批申请
+exports.getMyApprovals = async (req, res) => {
+  try {
+    const { page = 1, limit = 20, status } = req.query;
+    const offset = (page - 1) * limit;
+    const userId = req.user.id;
+
+    let query = `
+      SELECT a.*, 
+        u1.real_name as approver_name,
+        c.customer_name
+      FROM approvals a
+      LEFT JOIN users u1 ON a.approver_id = u1.id
+      LEFT JOIN customers c ON a.customer_id = c.id
+      WHERE a.applicant_id = ?
+    `;
+    const params = [userId];
+
+    if (status) {
+      query += ' AND a.status = ?';
+      params.push(status);
+    }
+
+    query += ' ORDER BY a.created_at DESC LIMIT ? OFFSET ?';
+    params.push(parseInt(limit), parseInt(offset));
+
+    const [approvals] = await pool.query(query, params);
+
+    // 获取总数
+    const countQuery = `
+      SELECT COUNT(*) as total FROM approvals 
+      WHERE applicant_id = ?
+      ${status ? 'AND status = ?' : ''}
+    `;
+    const countParams = status ? [userId, status] : [userId];
+    const [countResult] = await pool.query(countQuery, countParams);
+
+    res.json({
+      success: true,
+      data: {
+        approvals,
+        pagination: {
+          page: parseInt(page),
+          limit: parseInt(limit),
+          total: countResult[0].total,
+          total_pages: Math.ceil(countResult[0].total / limit)
+        }
+      }
+    });
+  } catch (error) {
+    console.error('获取我的审批申请失败:', error);
+    res.status(500).json({ success: false, message: '获取我的审批申请失败' });
+  }
+};
+
+// 处理审批
+exports.processApproval = async (req, res) => {
+  const connection = await pool.getConnection();
+  
+  try {
+    const { id } = req.params;
+    const { status, result_comment } = req.body;
+
+    if (!['approved', 'rejected'].includes(status)) {
+      return res.status(400).json({ success: false, message: '无效的审批结果' });
+    }
+
+    await connection.beginTransaction();
+
+    // 获取审批信息
+    const [approvals] = await connection.query(
+      'SELECT * FROM approvals WHERE id = ? AND status = ?',
+      [id, 'pending']
+    );
+
+    if (approvals.length === 0) {
+      await connection.rollback();
+      return res.status(404).json({ success: false, message: '审批不存在或已处理' });
+    }
+
+    const approval = approvals[0];
+
+    // 检查审批权限
+    if (approval.approver_id !== req.user.id) {
+      await connection.rollback();
+      return res.status(403).json({ success: false, message: '无权处理该审批' });
+    }
+
+    // 更新审批状态
+    await connection.query(
+      'UPDATE approvals SET status = ?, result_comment = ?, processed_at = NOW() WHERE id = ?',
+      [status, result_comment, id]
+    );
+
+    // 如果批准,执行相应操作
+    if (status === 'approved') {
+      switch (approval.type) {
+        case 'extension':
+          // 延长保护期
+          const [customer] = await connection.query(
+            'SELECT protected_end_date FROM customers WHERE id = ?',
+            [approval.customer_id]
+          );
+          
+          if (customer.length > 0) {
+            const newEndDate = moment(customer[0].protected_end_date)
+              .add(approval.extension_days, 'days')
+              .format('YYYY-MM-DD HH:mm:ss');
+            
+            await connection.query(
+              'UPDATE customers SET protected_end_date = ? WHERE id = ?',
+              [newEndDate, approval.customer_id]
+            );
+          }
+          break;
+
+        case 'force_release':
+          // 强制释放到公海池
+          await connection.query(
+            `UPDATE customers 
+             SET status = 'released', is_in_pool = true, release_reason = ?
+             WHERE id = ?`,
+            ['管理员强制释放', approval.customer_id]
+          );
+          break;
+
+        case 'collaboration':
+          // 协同跟进 - 可以在这里添加协同逻辑
+          break;
+      }
+    }
+
+    await connection.commit();
+
+    // 记录操作日志
+    await logOperation(req.user.id, 'process_approval', 'approval', id, { status, result_comment }, req.ip);
+
+    res.json({ success: true, message: '审批处理成功' });
+  } catch (error) {
+    await connection.rollback();
+    console.error('处理审批失败:', error);
+    res.status(500).json({ success: false, message: '处理审批失败' });
+  } finally {
+    connection.release();
+  }
+};

+ 161 - 0
src/controllers/authController.js

@@ -0,0 +1,161 @@
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+const pool = require('../config/database');
+const { logOperation } = require('../middleware/logger');
+const crypto = require('crypto');
+
+// 用户登录
+exports.login = async (req, res) => {
+  try {
+    const { username, password } = req.body;
+
+    if (!username || !password) {
+      return res.status(400).json({ success: false, message: '用户名和密码不能为空' });
+    }
+
+    // 查询用户
+    const [users] = await pool.query(
+      'SELECT * FROM users WHERE username = ? AND status = ?',
+      [username, 'active']
+    );
+
+    if (users.length === 0) {
+      return res.status(401).json({ success: false, message: '用户名或密码错误' });
+    }
+
+    const user = users[0];
+
+    // 验证密码
+    const isValidPassword = await bcrypt.compare(password, user.password);
+    if (!isValidPassword) {
+      return res.status(401).json({ success: false, message: '用户名或密码错误' });
+    }
+
+    // 生成 JWT
+    const token = jwt.sign(
+      { userId: user.id, role: user.role },
+      process.env.JWT_SECRET,
+      { expiresIn: process.env.JWT_EXPIRES_IN }
+    );
+
+    // 记录登录日志
+    await logOperation(user.id, 'login', 'user', user.id, { username }, req.ip);
+
+    res.json({
+      success: true,
+      data: {
+        token,
+        user: {
+          id: user.id,
+          username: user.username,
+          real_name: user.real_name,
+          role: user.role,
+          department: user.department,
+          team: user.team
+        }
+      }
+    });
+  } catch (error) {
+    console.error('登录失败:', error);
+    res.status(500).json({ success: false, message: '登录失败' });
+  }
+};
+
+// 用户注册(仅管理员可用)
+exports.register = async (req, res) => {
+  try {
+    const { username, password, real_name, email, phone, role, department, team } = req.body;
+
+    if (!username || !password || !real_name) {
+      return res.status(400).json({ success: false, message: '用户名、密码和真实姓名不能为空' });
+    }
+
+    // 检查用户名是否已存在
+    const [existing] = await pool.query('SELECT id FROM users WHERE username = ?', [username]);
+    if (existing.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, email, phone, role, department, team) 
+       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+      [userId, username, hashedPassword, real_name, email, phone, role || 'sales', department, team]
+    );
+
+    // 记录操作日志
+    await logOperation(req.user.id, 'create_user', 'user', userId, { username, real_name, role }, req.ip);
+
+    res.status(201).json({
+      success: true,
+      message: '用户创建成功',
+      data: { id: userId, username, real_name, role }
+    });
+  } catch (error) {
+    console.error('用户注册失败:', error);
+    res.status(500).json({ success: false, message: '用户注册失败' });
+  }
+};
+
+// 获取当前用户信息
+exports.getCurrentUser = async (req, res) => {
+  try {
+    const [users] = await pool.query(
+      'SELECT id, username, real_name, email, phone, role, department, team, created_at FROM users WHERE id = ?',
+      [req.user.id]
+    );
+
+    if (users.length === 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.changePassword = async (req, res) => {
+  try {
+    const { old_password, new_password } = req.body;
+
+    if (!old_password || !new_password) {
+      return res.status(400).json({ success: false, message: '旧密码和新密码不能为空' });
+    }
+
+    if (new_password.length < 6) {
+      return res.status(400).json({ success: false, message: '新密码长度不能少于6位' });
+    }
+
+    // 获取当前用户密码
+    const [users] = await pool.query('SELECT password FROM users WHERE id = ?', [req.user.id]);
+    
+    if (users.length === 0) {
+      return res.status(404).json({ success: false, message: '用户不存在' });
+    }
+
+    // 验证旧密码
+    const isValidPassword = await bcrypt.compare(old_password, users[0].password);
+    if (!isValidPassword) {
+      return res.status(401).json({ success: false, message: '旧密码错误' });
+    }
+
+    // 加密新密码并更新
+    const hashedPassword = await bcrypt.hash(new_password, 10);
+    await pool.query('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, req.user.id]);
+
+    // 记录操作日志
+    await logOperation(req.user.id, 'change_password', 'user', req.user.id, {}, req.ip);
+
+    res.json({ success: true, message: '密码修改成功' });
+  } catch (error) {
+    console.error('修改密码失败:', error);
+    res.status(500).json({ success: false, message: '修改密码失败' });
+  }
+};

+ 381 - 0
src/controllers/customerController.js

@@ -0,0 +1,381 @@
+const pool = require('../config/database');
+const { logOperation } = require('../middleware/logger');
+const stringSimilarity = require('string-similarity');
+const crypto = require('crypto');
+const moment = require('moment');
+
+// 客户查重
+exports.checkDuplicate = async (req, res) => {
+  try {
+    const { customer_name } = req.query;
+
+    if (!customer_name) {
+      return res.status(400).json({ success: false, message: '客户名称不能为空' });
+    }
+
+    // 精确匹配
+    const [exactMatches] = await pool.query(
+      `SELECT c.*, u.real_name as owner_name, u.team 
+       FROM customers c 
+       LEFT JOIN users u ON c.sales_owner = u.id 
+       WHERE c.customer_name = ? AND c.status != 'released'`,
+      [customer_name]
+    );
+
+    if (exactMatches.length > 0) {
+      return res.json({
+        success: true,
+        has_conflict: true,
+        conflict_type: 'exact',
+        data: exactMatches[0],
+        message: '发现完全重复的客户'
+      });
+    }
+
+    // 模糊匹配
+    const [allCustomers] = await pool.query(
+      `SELECT c.*, u.real_name as owner_name, u.team 
+       FROM customers c 
+       LEFT JOIN users u ON c.sales_owner = u.id 
+       WHERE c.status != 'released'`
+    );
+
+    const similarities = allCustomers.map(customer => ({
+      ...customer,
+      similarity: stringSimilarity.compareTwoStrings(customer_name, customer.customer_name)
+    }));
+
+    const threshold = parseFloat(process.env.SIMILARITY_THRESHOLD) || 0.8;
+    const similarCustomers = similarities.filter(c => c.similarity >= threshold);
+
+    if (similarCustomers.length > 0) {
+      return res.json({
+        success: true,
+        has_conflict: true,
+        conflict_type: 'similar',
+        data: similarCustomers[0],
+        similarity: similarCustomers[0].similarity,
+        message: `发现相似度${(similarCustomers[0].similarity * 100).toFixed(1)}%的客户`
+      });
+    }
+
+    res.json({
+      success: true,
+      has_conflict: false,
+      message: '未发现重复客户,可以报备'
+    });
+  } catch (error) {
+    console.error('客户查重失败:', error);
+    res.status(500).json({ success: false, message: '客户查重失败' });
+  }
+};
+
+// 创建客户报备
+exports.createCustomer = async (req, res) => {
+  try {
+    const {
+      customer_name,
+      industry,
+      region,
+      contact_person,
+      contact_phone,
+      demand_description,
+      source
+    } = req.body;
+
+    if (!customer_name || !industry || !region || !demand_description || !source) {
+      return res.status(400).json({ 
+        success: false, 
+        message: '客户名称、行业、地区、需求概况、来源渠道为必填项' 
+      });
+    }
+
+    // 再次检查重复
+    const [existing] = await pool.query(
+      'SELECT id FROM customers WHERE customer_name = ? AND status != ?',
+      [customer_name, 'released']
+    );
+
+    if (existing.length > 0) {
+      return res.status(400).json({ 
+        success: false, 
+        message: '该客户已被报备,无法重复报备' 
+      });
+    }
+
+    const customerId = crypto.randomUUID();
+    const protectionDays = parseInt(process.env.DEFAULT_PROTECTION_DAYS) || 30;
+    const protectedEndDate = moment().add(protectionDays, 'days').format('YYYY-MM-DD HH:mm:ss');
+
+    await pool.query(
+      `INSERT INTO customers 
+       (id, customer_name, industry, region, contact_person, contact_phone, 
+        demand_description, source, sales_owner, protected_end_date, status) 
+       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+      [
+        customerId, customer_name, industry, region, contact_person, contact_phone,
+        demand_description, source, req.user.id, protectedEndDate, 'following'
+      ]
+    );
+
+    // 记录操作日志
+    await logOperation(
+      req.user.id, 
+      'create_customer', 
+      'customer', 
+      customerId, 
+      { customer_name, industry, region }, 
+      req.ip
+    );
+
+    res.status(201).json({
+      success: true,
+      message: '客户报备成功',
+      data: {
+        id: customerId,
+        customer_name,
+        protected_end_date: protectedEndDate
+      }
+    });
+  } catch (error) {
+    console.error('客户报备失败:', error);
+    res.status(500).json({ success: false, message: '客户报备失败' });
+  }
+};
+
+// 获取客户列表
+exports.getCustomers = async (req, res) => {
+  try {
+    const { status, page = 1, limit = 20, keyword } = req.query;
+    const offset = (page - 1) * limit;
+    const userId = req.user.id;
+    const userRole = req.user.role;
+
+    let query = `
+      SELECT c.*, u.real_name as owner_name, u.team,
+        CASE 
+          WHEN c.protected_end_date < NOW() THEN 1
+          ELSE 0
+        END as is_expired
+      FROM customers c
+      LEFT JOIN users u ON c.sales_owner = u.id
+      WHERE 1=1
+    `;
+    const params = [];
+
+    // 根据角色过滤数据
+    if (userRole === 'sales') {
+      query += ' AND c.sales_owner = ?';
+      params.push(userId);
+    } else if (userRole === 'sales_manager') {
+      query += ' AND u.team = ?';
+      params.push(req.user.team);
+    }
+    // admin 和 sales_director 可以查看所有客户
+
+    // 状态过滤
+    if (status) {
+      query += ' AND c.status = ?';
+      params.push(status);
+    }
+
+    // 关键词搜索
+    if (keyword) {
+      query += ' AND (c.customer_name LIKE ? OR c.contact_person LIKE ? OR c.contact_phone LIKE ?)';
+      const searchPattern = `%${keyword}%`;
+      params.push(searchPattern, searchPattern, searchPattern);
+    }
+
+    // 获取总数
+    const countQuery = query.replace(/SELECT.*FROM/, 'SELECT COUNT(*) as total FROM');
+    const [countResult] = await pool.query(countQuery, params);
+    const total = countResult[0].total;
+
+    // 获取数据
+    query += ' ORDER BY c.created_at DESC LIMIT ? OFFSET ?';
+    params.push(parseInt(limit), parseInt(offset));
+    
+    const [customers] = await pool.query(query, params);
+
+    res.json({
+      success: true,
+      data: {
+        customers,
+        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.getCustomerById = async (req, res) => {
+  try {
+    const { id } = req.params;
+
+    const [customers] = await pool.query(
+      `SELECT c.*, u.real_name as owner_name, u.team, u.phone as owner_phone,
+        CASE 
+          WHEN c.protected_end_date < NOW() THEN 1
+          ELSE 0
+        END as is_expired
+       FROM customers c
+       LEFT JOIN users u ON c.sales_owner = u.id
+       WHERE c.id = ?`,
+      [id]
+    );
+
+    if (customers.length === 0) {
+      return res.status(404).json({ success: false, message: '客户不存在' });
+    }
+
+    // 获取跟进记录
+    const [followups] = await pool.query(
+      `SELECT f.*, u.real_name as user_name
+       FROM followup_records f
+       LEFT JOIN users u ON f.user_id = u.id
+       WHERE f.customer_id = ?
+       ORDER BY f.created_at DESC
+       LIMIT 10`,
+      [id]
+    );
+
+    // 获取附件
+    const [attachments] = await pool.query(
+      `SELECT id, original_name, file_size, mime_type, created_at
+       FROM attachments
+       WHERE customer_id = ?
+       ORDER BY created_at DESC`,
+      [id]
+    );
+
+    res.json({
+      success: true,
+      data: {
+        customer: customers[0],
+        followups,
+        attachments
+      }
+    });
+  } catch (error) {
+    console.error('获取客户详情失败:', error);
+    res.status(500).json({ success: false, message: '获取客户详情失败' });
+  }
+};
+
+// 更新客户信息
+exports.updateCustomer = async (req, res) => {
+  try {
+    const { id } = req.params;
+    const allowedFields = [
+      'industry', 'region', 'contact_person', 'contact_phone', 
+      'demand_description', 'source'
+    ];
+
+    const updates = {};
+    allowedFields.forEach(field => {
+      if (req.body[field] !== undefined) {
+        updates[field] = req.body[field];
+      }
+    });
+
+    if (Object.keys(updates).length === 0) {
+      return res.status(400).json({ success: false, message: '没有要更新的字段' });
+    }
+
+    const setClause = Object.keys(updates).map(key => `${key} = ?`).join(', ');
+    const values = [...Object.values(updates), id];
+
+    await pool.query(
+      `UPDATE customers SET ${setClause} WHERE id = ?`,
+      values
+    );
+
+    // 记录操作日志
+    await logOperation(req.user.id, 'update_customer', 'customer', id, updates, req.ip);
+
+    res.json({ success: true, message: '客户信息更新成功' });
+  } catch (error) {
+    console.error('更新客户信息失败:', error);
+    res.status(500).json({ success: false, message: '更新客户信息失败' });
+  }
+};
+
+// 更新客户状态
+exports.updateCustomerStatus = async (req, res) => {
+  try {
+    const { id } = req.params;
+    const { status, release_reason } = req.body;
+
+    const validStatuses = ['following', 'won', 'lost', 'released'];
+    if (!validStatuses.includes(status)) {
+      return res.status(400).json({ success: false, message: '无效的状态值' });
+    }
+
+    const updates = { status };
+    if (status === 'released' && release_reason) {
+      updates.release_reason = release_reason;
+      updates.is_in_pool = true;
+    }
+
+    const setClause = Object.keys(updates).map(key => `${key} = ?`).join(', ');
+    const values = [...Object.values(updates), id];
+
+    await pool.query(
+      `UPDATE customers SET ${setClause} WHERE id = ?`,
+      values
+    );
+
+    // 记录操作日志
+    await logOperation(req.user.id, 'update_customer_status', 'customer', id, { status, release_reason }, req.ip);
+
+    res.json({ success: true, message: '客户状态更新成功' });
+  } catch (error) {
+    console.error('更新客户状态失败:', error);
+    res.status(500).json({ success: false, message: '更新客户状态失败' });
+  }
+};
+
+// 添加跟进记录
+exports.addFollowup = async (req, res) => {
+  try {
+    const { customer_id, followup_type, content, next_plan } = req.body;
+
+    if (!customer_id || !content) {
+      return res.status(400).json({ success: false, message: '客户ID和跟进内容不能为空' });
+    }
+
+    const followupId = crypto.randomUUID();
+    
+    await pool.query(
+      `INSERT INTO followup_records (id, customer_id, user_id, followup_type, content, next_plan)
+       VALUES (?, ?, ?, ?, ?, ?)`,
+      [followupId, customer_id, req.user.id, followup_type || 'call', content, next_plan]
+    );
+
+    // 更新客户最后跟进时间
+    await pool.query(
+      'UPDATE customers SET last_followup = NOW() WHERE id = ?',
+      [customer_id]
+    );
+
+    // 记录操作日志
+    await logOperation(req.user.id, 'add_followup', 'followup', followupId, { customer_id, followup_type }, req.ip);
+
+    res.status(201).json({
+      success: true,
+      message: '跟进记录添加成功',
+      data: { id: followupId }
+    });
+  } catch (error) {
+    console.error('添加跟进记录失败:', error);
+    res.status(500).json({ success: false, message: '添加跟进记录失败' });
+  }
+};

+ 234 - 0
src/controllers/poolController.js

@@ -0,0 +1,234 @@
+const pool = require('../config/database');
+const { logOperation } = require('../middleware/logger');
+const crypto = require('crypto');
+const moment = require('moment');
+
+// 获取公海池客户列表
+exports.getPoolCustomers = async (req, res) => {
+  try {
+    const { page = 1, limit = 20, industry, region, keyword } = req.query;
+    const offset = (page - 1) * limit;
+
+    let query = `
+      SELECT c.*, u.real_name as last_owner_name,
+        (SELECT COUNT(*) FROM pool_claim_records WHERE customer_id = c.id) as claim_count
+      FROM customers c
+      LEFT JOIN users u ON c.sales_owner = u.id
+      WHERE c.is_in_pool = true AND c.status = 'released'
+    `;
+    const params = [];
+
+    // 行业过滤
+    if (industry) {
+      query += ' AND c.industry = ?';
+      params.push(industry);
+    }
+
+    // 地区过滤
+    if (region) {
+      query += ' AND c.region = ?';
+      params.push(region);
+    }
+
+    // 关键词搜索
+    if (keyword) {
+      query += ' AND (c.customer_name LIKE ? OR c.demand_description LIKE ?)';
+      const searchPattern = `%${keyword}%`;
+      params.push(searchPattern, searchPattern);
+    }
+
+    // 获取总数
+    const countQuery = query.replace(/SELECT.*FROM/, 'SELECT COUNT(*) as total FROM');
+    const [countResult] = await pool.query(countQuery, params);
+    const total = countResult[0].total;
+
+    // 获取数据
+    query += ' ORDER BY c.updated_at DESC LIMIT ? OFFSET ?';
+    params.push(parseInt(limit), parseInt(offset));
+    
+    const [customers] = await pool.query(query, params);
+
+    res.json({
+      success: true,
+      data: {
+        customers,
+        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.claimCustomer = async (req, res) => {
+  const connection = await pool.getConnection();
+  
+  try {
+    const { customer_id } = req.body;
+    const userId = req.user.id;
+
+    if (!customer_id) {
+      return res.status(400).json({ success: false, message: '客户ID不能为空' });
+    }
+
+    await connection.beginTransaction();
+
+    // 检查客户是否在公海池
+    const [customers] = await connection.query(
+      'SELECT * FROM customers WHERE id = ? AND is_in_pool = true AND status = ?',
+      [customer_id, 'released']
+    );
+
+    if (customers.length === 0) {
+      await connection.rollback();
+      return res.status(400).json({ success: false, message: '该客户不在公海池或已被领取' });
+    }
+
+    // 检查今日领取次数
+    const maxDailyLeads = parseInt(process.env.MAX_DAILY_LEADS) || 5;
+    const todayStart = moment().startOf('day').format('YYYY-MM-DD HH:mm:ss');
+    
+    const [claimCount] = await connection.query(
+      'SELECT COUNT(*) as count FROM pool_claim_records WHERE user_id = ? AND claim_time >= ?',
+      [userId, todayStart]
+    );
+
+    if (claimCount[0].count >= maxDailyLeads) {
+      await connection.rollback();
+      return res.status(400).json({ 
+        success: false, 
+        message: `今日领取次数已达上限(${maxDailyLeads}次)` 
+      });
+    }
+
+    // 更新客户信息
+    const protectionDays = parseInt(process.env.DEFAULT_PROTECTION_DAYS) || 30;
+    const protectedEndDate = moment().add(protectionDays, 'days').format('YYYY-MM-DD HH:mm:ss');
+
+    await connection.query(
+      `UPDATE customers 
+       SET sales_owner = ?, status = 'following', is_in_pool = false, 
+           protected_end_date = ?, release_reason = NULL
+       WHERE id = ?`,
+      [userId, protectedEndDate, customer_id]
+    );
+
+    // 记录领取记录
+    const recordId = crypto.randomUUID();
+    await connection.query(
+      'INSERT INTO pool_claim_records (id, customer_id, user_id) VALUES (?, ?, ?)',
+      [recordId, customer_id, userId]
+    );
+
+    await connection.commit();
+
+    // 记录操作日志
+    await logOperation(userId, 'claim_customer', 'customer', customer_id, {}, req.ip);
+
+    res.json({
+      success: true,
+      message: '客户领取成功',
+      data: {
+        customer_id,
+        protected_end_date: protectedEndDate
+      }
+    });
+  } catch (error) {
+    await connection.rollback();
+    console.error('领取客户失败:', error);
+    res.status(500).json({ success: false, message: '领取客户失败' });
+  } finally {
+    connection.release();
+  }
+};
+
+// 手动释放客户到公海池
+exports.releaseCustomer = async (req, res) => {
+  try {
+    const { customer_id, release_reason } = req.body;
+
+    if (!customer_id || !release_reason) {
+      return res.status(400).json({ success: false, message: '客户ID和释放原因不能为空' });
+    }
+
+    // 检查客户所有权
+    const [customers] = await pool.query(
+      'SELECT sales_owner FROM customers WHERE id = ?',
+      [customer_id]
+    );
+
+    if (customers.length === 0) {
+      return res.status(404).json({ success: false, message: '客户不存在' });
+    }
+
+    // 只有客户负责人或经理以上可以释放
+    const isOwner = customers[0].sales_owner === req.user.id;
+    const isManager = ['sales_manager', 'sales_director', 'admin'].includes(req.user.role);
+
+    if (!isOwner && !isManager) {
+      return res.status(403).json({ success: false, message: '无权释放该客户' });
+    }
+
+    await pool.query(
+      `UPDATE customers 
+       SET status = 'released', is_in_pool = true, release_reason = ?
+       WHERE id = ?`,
+      [release_reason, customer_id]
+    );
+
+    // 记录操作日志
+    await logOperation(req.user.id, 'release_customer', 'customer', customer_id, { release_reason }, req.ip);
+
+    res.json({ success: true, message: '客户已释放到公海池' });
+  } catch (error) {
+    console.error('释放客户失败:', error);
+    res.status(500).json({ success: false, message: '释放客户失败' });
+  }
+};
+
+// 获取我的领取记录
+exports.getMyClaimRecords = async (req, res) => {
+  try {
+    const { page = 1, limit = 20 } = req.query;
+    const offset = (page - 1) * limit;
+    const userId = req.user.id;
+
+    const [records] = await pool.query(
+      `SELECT pcr.*, c.customer_name, c.industry, c.region, c.status
+       FROM pool_claim_records pcr
+       LEFT JOIN customers c ON pcr.customer_id = c.id
+       WHERE pcr.user_id = ?
+       ORDER BY pcr.claim_time DESC
+       LIMIT ? OFFSET ?`,
+      [userId, parseInt(limit), parseInt(offset)]
+    );
+
+    const [countResult] = await pool.query(
+      'SELECT COUNT(*) as total FROM pool_claim_records WHERE user_id = ?',
+      [userId]
+    );
+
+    res.json({
+      success: true,
+      data: {
+        records,
+        pagination: {
+          page: parseInt(page),
+          limit: parseInt(limit),
+          total: countResult[0].total,
+          total_pages: Math.ceil(countResult[0].total / limit)
+        }
+      }
+    });
+  } catch (error) {
+    console.error('获取领取记录失败:', error);
+    res.status(500).json({ success: false, message: '获取领取记录失败' });
+  }
+};

+ 310 - 0
src/controllers/statsController.js

@@ -0,0 +1,310 @@
+const pool = require('../config/database');
+const moment = require('moment');
+
+// 个人仪表盘
+exports.getPersonalDashboard = async (req, res) => {
+  try {
+    const userId = req.user.id;
+
+    // 我的报备统计
+    const [stats] = await pool.query(
+      `SELECT 
+        COUNT(*) as total,
+        SUM(CASE WHEN status = 'following' THEN 1 ELSE 0 END) as following,
+        SUM(CASE WHEN status = 'won' THEN 1 ELSE 0 END) as won,
+        SUM(CASE WHEN status = 'lost' THEN 1 ELSE 0 END) as lost
+       FROM customers
+       WHERE sales_owner = ?`,
+      [userId]
+    );
+
+    // 即将到期的客户(3天内)
+    const threeDaysLater = moment().add(3, 'days').format('YYYY-MM-DD HH:mm:ss');
+    const [expiring] = await pool.query(
+      `SELECT id, customer_name, protected_end_date, industry, region
+       FROM customers
+       WHERE sales_owner = ? AND status = 'following' 
+         AND protected_end_date <= ? AND protected_end_date > NOW()
+       ORDER BY protected_end_date ASC
+       LIMIT 10`,
+      [userId, threeDaysLater]
+    );
+
+    // 个人转化率
+    const wonCount = stats[0].won || 0;
+    const totalClosed = wonCount + (stats[0].lost || 0);
+    const conversionRate = totalClosed > 0 ? ((wonCount / totalClosed) * 100).toFixed(2) : 0;
+
+    // 本月报备数
+    const monthStart = moment().startOf('month').format('YYYY-MM-DD HH:mm:ss');
+    const [monthStats] = await pool.query(
+      `SELECT COUNT(*) as count FROM customers 
+       WHERE sales_owner = ? AND report_time >= ?`,
+      [userId, monthStart]
+    );
+
+    // 本周跟进次数
+    const weekStart = moment().startOf('week').format('YYYY-MM-DD HH:mm:ss');
+    const [followupStats] = await pool.query(
+      `SELECT COUNT(*) as count FROM followup_records 
+       WHERE user_id = ? AND created_at >= ?`,
+      [userId, weekStart]
+    );
+
+    res.json({
+      success: true,
+      data: {
+        summary: {
+          total: stats[0].total,
+          following: stats[0].following,
+          won: stats[0].won,
+          lost: stats[0].lost,
+          conversion_rate: conversionRate + '%',
+          month_reports: monthStats[0].count,
+          week_followups: followupStats[0].count
+        },
+        expiring_customers: expiring
+      }
+    });
+  } catch (error) {
+    console.error('获取个人仪表盘失败:', error);
+    res.status(500).json({ success: false, message: '获取个人仪表盘失败' });
+  }
+};
+
+// 团队报备统计
+exports.getTeamStats = async (req, res) => {
+  try {
+    const { start_date, end_date, period = 'day' } = req.query;
+    const userTeam = req.user.team;
+    const userRole = req.user.role;
+
+    if (userRole === 'sales') {
+      return res.status(403).json({ success: false, message: '权限不足' });
+    }
+
+    let startDate = start_date || moment().subtract(30, 'days').format('YYYY-MM-DD');
+    let endDate = end_date || moment().format('YYYY-MM-DD');
+
+    // 团队成员统计
+    const [memberStats] = await pool.query(
+      `SELECT 
+        u.id, u.real_name,
+        COUNT(c.id) as total_customers,
+        SUM(CASE WHEN c.status = 'following' THEN 1 ELSE 0 END) as following,
+        SUM(CASE WHEN c.status = 'won' THEN 1 ELSE 0 END) as won,
+        SUM(CASE WHEN c.status = 'lost' THEN 1 ELSE 0 END) as lost,
+        COUNT(DISTINCT DATE(c.report_time)) as active_days
+       FROM users u
+       LEFT JOIN customers c ON u.id = c.sales_owner 
+         AND c.report_time BETWEEN ? AND ?
+       WHERE u.team = ? AND u.role IN ('sales', 'sales_manager')
+       GROUP BY u.id, u.real_name
+       ORDER BY total_customers DESC`,
+      [startDate, endDate, userTeam]
+    );
+
+    // 计算转化率
+    memberStats.forEach(member => {
+      const totalClosed = (member.won || 0) + (member.lost || 0);
+      member.conversion_rate = totalClosed > 0 
+        ? ((member.won / totalClosed) * 100).toFixed(2) + '%' 
+        : '0%';
+    });
+
+    // 团队总体统计
+    const [teamTotal] = await pool.query(
+      `SELECT 
+        COUNT(c.id) as total,
+        SUM(CASE WHEN c.status = 'following' THEN 1 ELSE 0 END) as following,
+        SUM(CASE WHEN c.status = 'won' THEN 1 ELSE 0 END) as won,
+        SUM(CASE WHEN c.status = 'lost' THEN 1 ELSE 0 END) as lost
+       FROM customers c
+       LEFT JOIN users u ON c.sales_owner = u.id
+       WHERE u.team = ? AND c.report_time BETWEEN ? AND ?`,
+      [userTeam, startDate, endDate]
+    );
+
+    // 按时间段统计
+    let dateFormat = '%Y-%m-%d';
+    if (period === 'week') {
+      dateFormat = '%Y-%u';
+    } else if (period === 'month') {
+      dateFormat = '%Y-%m';
+    }
+
+    const [timeline] = await pool.query(
+      `SELECT 
+        DATE_FORMAT(c.report_time, ?) as period,
+        COUNT(*) as count,
+        SUM(CASE WHEN c.status = 'won' THEN 1 ELSE 0 END) as won_count
+       FROM customers c
+       LEFT JOIN users u ON c.sales_owner = u.id
+       WHERE u.team = ? AND c.report_time BETWEEN ? AND ?
+       GROUP BY period
+       ORDER BY period`,
+      [dateFormat, userTeam, startDate, endDate]
+    );
+
+    res.json({
+      success: true,
+      data: {
+        team_summary: teamTotal[0],
+        member_stats: memberStats,
+        timeline
+      }
+    });
+  } catch (error) {
+    console.error('获取团队统计失败:', error);
+    res.status(500).json({ success: false, message: '获取团队统计失败' });
+  }
+};
+
+// 客户来源分析
+exports.getSourceAnalysis = async (req, res) => {
+  try {
+    const { start_date, end_date } = req.query;
+    const userId = req.user.id;
+    const userRole = req.user.role;
+
+    let startDate = start_date || moment().subtract(90, 'days').format('YYYY-MM-DD');
+    let endDate = end_date || moment().format('YYYY-MM-DD');
+
+    let query = `
+      SELECT 
+        c.source,
+        COUNT(*) as count,
+        SUM(CASE WHEN c.status = 'won' THEN 1 ELSE 0 END) as won_count,
+        SUM(CASE WHEN c.status = 'lost' THEN 1 ELSE 0 END) as lost_count
+      FROM customers c
+    `;
+
+    const params = [startDate, endDate];
+
+    // 根据角色限制数据范围
+    if (userRole === 'sales') {
+      query += ' WHERE c.sales_owner = ? AND c.report_time BETWEEN ? AND ?';
+      params.unshift(userId);
+    } else if (userRole === 'sales_manager') {
+      query += ` 
+        LEFT JOIN users u ON c.sales_owner = u.id
+        WHERE u.team = ? AND c.report_time BETWEEN ? AND ?
+      `;
+      params.unshift(req.user.team);
+    } else {
+      query += ' WHERE c.report_time BETWEEN ? AND ?';
+    }
+
+    query += ' GROUP BY c.source ORDER BY count DESC';
+
+    const [sources] = await pool.query(query, params);
+
+    // 计算转化率
+    sources.forEach(source => {
+      const totalClosed = (source.won_count || 0) + (source.lost_count || 0);
+      source.conversion_rate = totalClosed > 0 
+        ? ((source.won_count / totalClosed) * 100).toFixed(2) + '%' 
+        : '0%';
+    });
+
+    res.json({
+      success: true,
+      data: sources
+    });
+  } catch (error) {
+    console.error('获取客户来源分析失败:', error);
+    res.status(500).json({ success: false, message: '获取客户来源分析失败' });
+  }
+};
+
+// 行业分布分析
+exports.getIndustryAnalysis = async (req, res) => {
+  try {
+    const userId = req.user.id;
+    const userRole = req.user.role;
+
+    let query = `
+      SELECT 
+        c.industry,
+        COUNT(*) as count,
+        SUM(CASE WHEN c.status = 'won' THEN 1 ELSE 0 END) as won_count
+      FROM customers c
+    `;
+
+    const params = [];
+
+    if (userRole === 'sales') {
+      query += ' WHERE c.sales_owner = ?';
+      params.push(userId);
+    } else if (userRole === 'sales_manager') {
+      query += ' LEFT JOIN users u ON c.sales_owner = u.id WHERE u.team = ?';
+      params.push(req.user.team);
+    }
+
+    query += ' GROUP BY c.industry ORDER BY count DESC LIMIT 20';
+
+    const [industries] = await pool.query(query, params);
+
+    res.json({
+      success: true,
+      data: industries
+    });
+  } catch (error) {
+    console.error('获取行业分布分析失败:', error);
+    res.status(500).json({ success: false, message: '获取行业分布分析失败' });
+  }
+};
+
+// 公海池利用情况
+exports.getPoolUtilization = async (req, res) => {
+  try {
+    if (req.user.role === 'sales') {
+      return res.status(403).json({ success: false, message: '权限不足' });
+    }
+
+    // 公海池总数
+    const [poolTotal] = await pool.query(
+      'SELECT COUNT(*) as total FROM customers WHERE is_in_pool = true AND status = ?',
+      ['released']
+    );
+
+    // 本周领取统计
+    const weekStart = moment().startOf('week').format('YYYY-MM-DD HH:mm:ss');
+    const [weekClaims] = await pool.query(
+      'SELECT COUNT(*) as count FROM pool_claim_records WHERE claim_time >= ?',
+      [weekStart]
+    );
+
+    // 本月领取统计
+    const monthStart = moment().startOf('month').format('YYYY-MM-DD HH:mm:ss');
+    const [monthClaims] = await pool.query(
+      'SELECT COUNT(*) as count FROM pool_claim_records WHERE claim_time >= ?',
+      [monthStart]
+    );
+
+    // 领取排行
+    const [topClaimers] = await pool.query(
+      `SELECT u.real_name, COUNT(*) as claim_count
+       FROM pool_claim_records pcr
+       LEFT JOIN users u ON pcr.user_id = u.id
+       WHERE pcr.claim_time >= ?
+       GROUP BY u.id, u.real_name
+       ORDER BY claim_count DESC
+       LIMIT 10`,
+      [monthStart]
+    );
+
+    res.json({
+      success: true,
+      data: {
+        pool_total: poolTotal[0].total,
+        week_claims: weekClaims[0].count,
+        month_claims: monthClaims[0].count,
+        top_claimers: topClaimers
+      }
+    });
+  } catch (error) {
+    console.error('获取公海池利用情况失败:', error);
+    res.status(500).json({ success: false, message: '获取公海池利用情况失败' });
+  }
+};

+ 112 - 0
src/middleware/auth.js

@@ -0,0 +1,112 @@
+const jwt = require('jsonwebtoken');
+const pool = require('../config/database');
+
+// 验证 JWT Token
+const authenticateToken = async (req, res, next) => {
+  try {
+    const authHeader = req.headers['authorization'];
+    const token = authHeader && authHeader.split(' ')[1];
+
+    if (!token) {
+      return res.status(401).json({ success: false, message: '未提供认证令牌' });
+    }
+
+    const decoded = jwt.verify(token, process.env.JWT_SECRET);
+    
+    // 验证用户是否存在且状态为活跃
+    const [users] = await pool.query(
+      'SELECT id, username, real_name, role, department, team, status FROM users WHERE id = ? AND status = ?',
+      [decoded.userId, 'active']
+    );
+
+    if (users.length === 0) {
+      return res.status(401).json({ success: false, message: '用户不存在或已被禁用' });
+    }
+
+    req.user = users[0];
+    next();
+  } catch (error) {
+    if (error.name === 'JsonWebTokenError') {
+      return res.status(403).json({ success: false, message: '无效的认证令牌' });
+    }
+    if (error.name === 'TokenExpiredError') {
+      return res.status(403).json({ success: false, message: '认证令牌已过期' });
+    }
+    return res.status(500).json({ success: false, message: '认证失败' });
+  }
+};
+
+// 角色权限验证
+const authorizeRoles = (...roles) => {
+  return (req, res, next) => {
+    if (!req.user) {
+      return res.status(401).json({ success: false, message: '未认证' });
+    }
+
+    if (!roles.includes(req.user.role)) {
+      return res.status(403).json({ 
+        success: false, 
+        message: '权限不足,需要以下角色之一: ' + roles.join(', ') 
+      });
+    }
+
+    next();
+  };
+};
+
+// 数据权限验证 - 检查用户是否有权访问特定客户
+const checkCustomerAccess = async (req, res, next) => {
+  try {
+    const customerId = req.params.id || req.body.customer_id;
+    const userId = req.user.id;
+    const userRole = req.user.role;
+
+    if (!customerId) {
+      return res.status(400).json({ success: false, message: '缺少客户ID' });
+    }
+
+    const [customers] = await pool.query(
+      'SELECT sales_owner FROM customers WHERE id = ?',
+      [customerId]
+    );
+
+    if (customers.length === 0) {
+      return res.status(404).json({ success: false, message: '客户不存在' });
+    }
+
+    const customer = customers[0];
+
+    // 管理员和总监可以访问所有客户
+    if (userRole === 'admin' || userRole === 'sales_director') {
+      return next();
+    }
+
+    // 销售经理可以访问团队客户
+    if (userRole === 'sales_manager') {
+      const [ownerInfo] = await pool.query(
+        'SELECT team FROM users WHERE id = ?',
+        [customer.sales_owner]
+      );
+      
+      if (ownerInfo.length > 0 && ownerInfo[0].team === req.user.team) {
+        return next();
+      }
+    }
+
+    // 销售只能访问自己的客户
+    if (customer.sales_owner === userId) {
+      return next();
+    }
+
+    return res.status(403).json({ success: false, message: '无权访问该客户' });
+  } catch (error) {
+    console.error('权限验证失败:', error);
+    return res.status(500).json({ success: false, message: '权限验证失败' });
+  }
+};
+
+module.exports = {
+  authenticateToken,
+  authorizeRoles,
+  checkCustomerAccess
+};

+ 32 - 0
src/middleware/logger.js

@@ -0,0 +1,32 @@
+const pool = require('../config/database');
+const crypto = require('crypto');
+
+// 记录操作日志
+const logOperation = async (userId, action, targetType, targetId, details, ipAddress) => {
+  try {
+    await pool.query(
+      `INSERT INTO operation_logs (id, user_id, action, target_type, target_id, details, ip_address) 
+       VALUES (?, ?, ?, ?, ?, ?, ?)`,
+      [crypto.randomUUID(), userId, action, targetType, targetId, JSON.stringify(details), ipAddress]
+    );
+  } catch (error) {
+    console.error('记录操作日志失败:', error);
+  }
+};
+
+// 中间件:自动记录请求日志
+const requestLogger = (req, res, next) => {
+  const start = Date.now();
+  
+  res.on('finish', () => {
+    const duration = Date.now() - start;
+    console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`);
+  });
+  
+  next();
+};
+
+module.exports = {
+  logOperation,
+  requestLogger
+};

+ 84 - 0
src/routes/index.js

@@ -0,0 +1,84 @@
+const express = require('express');
+const router = express.Router();
+
+// 导入控制器
+const authController = require('../controllers/authController');
+const customerController = require('../controllers/customerController');
+const poolController = require('../controllers/poolController');
+const approvalController = require('../controllers/approvalController');
+const statsController = require('../controllers/statsController');
+
+// 导入中间件
+const { authenticateToken, authorizeRoles, checkCustomerAccess } = require('../middleware/auth');
+
+// ==================== 认证路由 ====================
+router.post('/auth/login', authController.login);
+router.get('/auth/me', authenticateToken, authController.getCurrentUser);
+router.post('/auth/change-password', authenticateToken, authController.changePassword);
+router.post('/auth/register', authenticateToken, authorizeRoles('admin'), authController.register);
+
+// ==================== 客户管理路由 ====================
+// 客户查重
+router.get('/customers/check-duplicate', authenticateToken, customerController.checkDuplicate);
+
+// 客户报备
+router.post('/customers', authenticateToken, customerController.createCustomer);
+
+// 获取客户列表
+router.get('/customers', authenticateToken, customerController.getCustomers);
+
+// 获取客户详情
+router.get('/customers/:id', authenticateToken, checkCustomerAccess, customerController.getCustomerById);
+
+// 更新客户信息
+router.put('/customers/:id', authenticateToken, checkCustomerAccess, customerController.updateCustomer);
+
+// 更新客户状态
+router.patch('/customers/:id/status', authenticateToken, checkCustomerAccess, customerController.updateCustomerStatus);
+
+// 添加跟进记录
+router.post('/customers/followup', authenticateToken, customerController.addFollowup);
+
+// ==================== 公海池路由 ====================
+// 获取公海池客户列表
+router.get('/pool/customers', authenticateToken, poolController.getPoolCustomers);
+
+// 领取客户
+router.post('/pool/claim', authenticateToken, poolController.claimCustomer);
+
+// 释放客户到公海池
+router.post('/pool/release', authenticateToken, poolController.releaseCustomer);
+
+// 我的领取记录
+router.get('/pool/my-claims', authenticateToken, poolController.getMyClaimRecords);
+
+// ==================== 审批流程路由 ====================
+// 创建审批申请
+router.post('/approvals', authenticateToken, approvalController.createApproval);
+
+// 获取待审批列表
+router.get('/approvals/pending', authenticateToken, authorizeRoles('sales_manager', 'sales_director', 'admin'), approvalController.getPendingApprovals);
+
+// 获取我的审批申请
+router.get('/approvals/my', authenticateToken, approvalController.getMyApprovals);
+
+// 处理审批
+router.post('/approvals/:id/process', authenticateToken, authorizeRoles('sales_manager', 'sales_director', 'admin'), approvalController.processApproval);
+
+// ==================== 统计报表路由 ====================
+// 个人仪表盘
+router.get('/stats/dashboard', authenticateToken, statsController.getPersonalDashboard);
+
+// 团队统计
+router.get('/stats/team', authenticateToken, authorizeRoles('sales_manager', 'sales_director', 'admin'), statsController.getTeamStats);
+
+// 客户来源分析
+router.get('/stats/source-analysis', authenticateToken, statsController.getSourceAnalysis);
+
+// 行业分布分析
+router.get('/stats/industry-analysis', authenticateToken, statsController.getIndustryAnalysis);
+
+// 公海池利用情况
+router.get('/stats/pool-utilization', authenticateToken, authorizeRoles('sales_manager', 'sales_director', 'admin'), statsController.getPoolUtilization);
+
+module.exports = router;

+ 85 - 0
src/server.js

@@ -0,0 +1,85 @@
+require('dotenv').config();
+const express = require('express');
+const cors = require('cors');
+const routes = require('./routes');
+const { requestLogger } = require('./middleware/logger');
+const { startScheduler } = require('./utils/scheduler');
+
+const app = express();
+const PORT = process.env.PORT || 3000;
+const path = require('path');
+
+// 中间件
+app.use(cors());
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+app.use(requestLogger);
+
+// 静态文件服务 - 提供前端页面
+app.use(express.static(path.join(__dirname, '../public')));
+
+// API 路由
+app.use('/api', routes);
+
+// 健康检查
+app.get('/health', (req, res) => {
+  res.json({ 
+    success: true, 
+    message: '客户报备管理系统运行正常',
+    timestamp: new Date().toISOString()
+  });
+});
+
+// 根路径重定向到 index.html
+app.get('/', (req, res) => {
+  res.sendFile(path.join(__dirname, '../public/index.html'));
+});
+
+// 404 处理 - 只对 API 请求返回 JSON
+app.use((req, res) => {
+  if (req.path.startsWith('/api')) {
+    res.status(404).json({ 
+      success: false, 
+      message: '请求的资源不存在' 
+    });
+  } else {
+    res.status(404).sendFile(path.join(__dirname, '../public/index.html'));
+  }
+});
+
+// 错误处理
+app.use((err, req, res, next) => {
+  console.error('服务器错误:', err);
+  res.status(500).json({ 
+    success: false, 
+    message: '服务器内部错误',
+    error: process.env.NODE_ENV === 'development' ? err.message : undefined
+  });
+});
+
+// 启动服务器
+app.listen(PORT, () => {
+  console.log('='.repeat(50));
+  console.log('客户报备管理系统 (Customer Registration CRM)');
+  console.log('='.repeat(50));
+  console.log(`服务器运行在: http://localhost:${PORT}`);
+  console.log(`环境: ${process.env.NODE_ENV || 'development'}`);
+  console.log(`API 文档: http://localhost:${PORT}/api`);
+  console.log('='.repeat(50));
+  
+  // 启动定时任务
+  startScheduler();
+});
+
+// 优雅退出
+process.on('SIGTERM', () => {
+  console.log('收到 SIGTERM 信号,准备关闭服务器...');
+  process.exit(0);
+});
+
+process.on('SIGINT', () => {
+  console.log('\n收到 SIGINT 信号,准备关闭服务器...');
+  process.exit(0);
+});
+
+module.exports = app;

+ 89 - 0
src/utils/scheduler.js

@@ -0,0 +1,89 @@
+const cron = require('node-cron');
+const pool = require('../config/database');
+const moment = require('moment');
+
+// 定时任务:检查保护期到期
+const checkProtectionExpiry = async () => {
+  try {
+    const now = moment().format('YYYY-MM-DD HH:mm:ss');
+    
+    // 自动释放到期的客户
+    const [result] = await pool.query(
+      `UPDATE customers 
+       SET status = 'released', is_in_pool = true, release_reason = '保护期自动到期'
+       WHERE protected_end_date < ? AND status = 'following'`,
+      [now]
+    );
+
+    if (result.affectedRows > 0) {
+      console.log(`[${new Date().toISOString()}] 自动释放 ${result.affectedRows} 个到期客户到公海池`);
+    }
+  } catch (error) {
+    console.error('检查保护期到期失败:', error);
+  }
+};
+
+// 定时任务:保护期到期提醒
+const sendExpiryReminders = async () => {
+  try {
+    // 3天后到期的客户
+    const threeDaysLater = moment().add(3, 'days').format('YYYY-MM-DD');
+    const threeDaysEnd = moment().add(3, 'days').endOf('day').format('YYYY-MM-DD HH:mm:ss');
+    
+    const [reminders3Days] = await pool.query(
+      `SELECT c.id, c.customer_name, c.protected_end_date, u.real_name, u.email
+       FROM customers c
+       LEFT JOIN users u ON c.sales_owner = u.id
+       WHERE DATE(c.protected_end_date) = ? AND c.status = 'following'`,
+      [threeDaysLater]
+    );
+
+    if (reminders3Days.length > 0) {
+      console.log(`[${new Date().toISOString()}] ${reminders3Days.length} 个客户将在3天后到期`);
+      // 这里可以集成邮件或消息推送服务
+      // 例如: await sendEmailReminder(reminders3Days);
+    }
+
+    // 1天后到期的客户
+    const oneDayLater = moment().add(1, 'days').format('YYYY-MM-DD');
+    
+    const [reminders1Day] = await pool.query(
+      `SELECT c.id, c.customer_name, c.protected_end_date, u.real_name, u.email
+       FROM customers c
+       LEFT JOIN users u ON c.sales_owner = u.id
+       WHERE DATE(c.protected_end_date) = ? AND c.status = 'following'`,
+      [oneDayLater]
+    );
+
+    if (reminders1Day.length > 0) {
+      console.log(`[${new Date().toISOString()}] ${reminders1Day.length} 个客户将在1天后到期`);
+    }
+  } catch (error) {
+    console.error('发送到期提醒失败:', error);
+  }
+};
+
+// 启动定时任务
+const startScheduler = () => {
+  // 每小时检查一次保护期到期
+  cron.schedule('0 * * * *', () => {
+    console.log('[定时任务] 开始检查保护期到期客户...');
+    checkProtectionExpiry();
+  });
+
+  // 每天早上9点发送到期提醒
+  cron.schedule('0 9 * * *', () => {
+    console.log('[定时任务] 开始发送保护期到期提醒...');
+    sendExpiryReminders();
+  });
+
+  console.log('定时任务已启动');
+  console.log('- 保护期检查: 每小时执行一次');
+  console.log('- 到期提醒: 每天 09:00 执行');
+};
+
+module.exports = {
+  startScheduler,
+  checkProtectionExpiry,
+  sendExpiryReminders
+};