Initial commit: 电动车租赁系统后端

This commit is contained in:
notyclaw 2026-03-27 21:10:55 +08:00
commit 6e1378c812
62 changed files with 7516 additions and 0 deletions

70
.gitignore vendored Normal file
View File

@ -0,0 +1,70 @@
# Dependencies
node_modules/
# Environment variables
.env
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

60
README.md Normal file
View File

@ -0,0 +1,60 @@
# 两轮电动车租赁管理系统
## 🚀 项目简介
一个完整的两轮电动车租赁管理系统,包含车辆管理、订单管理、客户管理、财务管理等功能。
## 📁 项目结构
```
e-scooter-rental-system/
├── server/ # 后端服务
│ ├── models/ # 数据模型
│ ├── routes/ # 路由
│ ├── middleware/ # 中间件
│ └── index.js # 入口文件
├── client/ # 前端页面
│ ├── src/
│ │ ├── components/ # 组件
│ │ ├── pages/ # 页面
│ │ └── App.jsx
│ └── index.html
├── database/ # 数据库脚本
├── docs/ # 文档
└── package.json
```
## 🛠️ 技术栈
- **后端**: Node.js + Express + MongoDB
- **前端**: Vue.js / React (待定)
- **数据库**: MongoDB
## 📋 功能模块
1. **车辆管理** - 车辆信息、状态追踪、GPS定位
2. **订单管理** - 租赁订单、计费、逾期提醒
3. **客户管理** - 客户信息、租赁历史
4. **财务管理** - 收入统计、报表生成
5. **运维管理** - 维修记录、保养提醒
## 🚀 快速开始
```bash
# 安装依赖
npm install
# 启动后端
npm run server
# 启动前端 (待实现)
npm run client
```
## 📝 开发计划
- [ ] 初始化项目结构
- [ ] 设计数据库模型
- [ ] 实现后端API
- [ ] 开发前端界面
- [ ] 集成测试

39
add-finance.js Normal file
View File

@ -0,0 +1,39 @@
const http = require('http');
// 添加收支记录
const payments = [
{ type: 'income', party: '张三', amount: 1500, method: '微信', category: '租金收入', remark: '租车费' },
{ type: 'income', party: '李四', amount: 2000, method: '支付宝', category: '租金收入', remark: '租车费' },
{ type: 'income', party: '王五', amount: 500, method: '现金', category: '押金退还', remark: '还车退押金' },
{ type: 'expense', party: '赵六', amount: 300, method: '微信', category: '退款', remark: '提前还车退款' },
{ type: 'expense', party: '房东', amount: 1500, method: '银行卡', category: '房租', remark: '门店房租' },
{ type: 'expense', party: '电工', amount: 200, method: '现金', category: '水电', remark: '门店电费' }
];
let i = 0;
const addPayment = () => {
if (i >= payments.length) {
console.log('Done!');
return;
}
const data = JSON.stringify(payments[i]);
const req = http.request({
hostname: 'localhost',
port: 3000,
path: '/api/finance',
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': data.length }
}, res => {
let b = '';
res.on('data', c => b += c);
res.on('end', () => {
console.log('Added:', payments[i].party, payments[i].amount);
i++;
addPayment();
});
});
req.on('error', e => console.error(e));
req.write(data);
req.end();
};
addPayment();

190
docs/API.md Normal file
View File

@ -0,0 +1,190 @@
# API 文档
## 基础 URL
`http://localhost:3000/api`
---
## 车辆管理 (Vehicles)
### 获取所有车辆
```
GET /vehicles
```
### 获取单个车辆
```
GET /vehicles/:id
```
### 创建车辆
```
POST /vehicles
```
请求体:
```json
{
"vehicleId": "SCOOTER001",
"model": "黑骑士",
"brand": "小牛",
"color": "黑色",
"batteryType": "锂电池",
"batteryCapacity": 20,
"status": "空闲"
}
```
### 更新车辆
```
PUT /vehicles/:id
```
### 删除车辆
```
DELETE /vehicles/:id
```
### 按状态筛选车辆
```
GET /vehicles/status/:status
```
状态: 空闲, 在租, 维修中, 已报废, 待回收
### 更新车辆位置
```
PATCH /vehicles/:id/location
```
请求体:
```json
{
"longitude": 116.397,
"latitude": 39.909
}
```
---
## 订单管理 (Orders)
### 获取所有订单
```
GET /orders
```
### 获取单个订单
```
GET /orders/:id
```
### 创建订单
```
POST /orders
```
请求体:
```json
{
"customer": "客户ID",
"vehicle": "车辆ID",
"startDate": "2026-03-05",
"endDate": "2026-03-12",
"rentalFee": 50,
"deposit": 200
}
```
### 结束订单
```
PATCH /orders/:id/complete
```
### 获取逾期订单
```
GET /orders/status/overdue
```
### 按状态筛选订单
```
GET /orders/status/:status
```
状态: 待支付, 进行中, 已完成, 逾期, 已取消, 已退款
---
## 客户管理 (Customers)
### 获取所有客户
```
GET /customers
```
### 获取单个客户
```
GET /customers/:id
```
### 创建客户
```
POST /customers
```
请求体:
```json
{
"name": "张三",
"phone": "13800138000",
"idCard": "110101199001011234"
}
```
### 更新客户
```
PUT /customers/:id
```
### 删除客户
```
DELETE /customers/:id
```
### 搜索客户
```
GET /customers/search/:keyword
```
### 更新客户信用评分
```
PATCH /customers/:id/credit
```
请求体:
```json
{
"creditScore": 85
}
```
---
## 财务管理 (Finance)
### 获取财务统计
```
GET /finance/stats
```
### 获取收入趋势
```
GET /finance/trend?period=month
```
参数: week, month, quarter, year
### 获取逾期账款列表
```
GET /finance/overdue
```
---
## 健康检查
```
GET /health
```
返回: `{ "status": "ok", "timestamp": "..." }`

149
docs/MongoDB安装指南.md Normal file
View File

@ -0,0 +1,149 @@
# MongoDB 安装指南
## 为什么需要 MongoDB
两轮电动车租赁管理系统使用 MongoDB 作为数据库,用于存储车辆、订单、客户等数据。
## 安装方法
### 方法一:下载安装包 (推荐)
#### 1. 下载 MongoDB
- 访问: https://www.mongodb.com/try/download/community
- 选择 **Windows** 版本
- 下载 **MongoDB Community Server**
#### 2. 安装 MongoDB
- 运行下载的安装程序
- 选择 **Complete** 安装类型
- 勾选 **Install MongoDB as a Service**
- 点击 **Install** 开始安装
#### 3. 配置数据目录
- 默认路径: `C:\data\db`
- 如果目录不存在,手动创建:
```bash
mkdir C:\data\db
```
#### 4. 验证安装
打开命令提示符,输入:
```bash
mongod --version
```
如果显示版本信息,说明安装成功。
### 方法二:使用 Docker (推荐)
#### 1. 安装 Docker Desktop
- 访问: https://www.docker.com/products/docker-desktop/
- 下载并安装 Docker Desktop for Windows
- 启动 Docker Desktop
#### 2. 拉取 MongoDB 镜像
```bash
docker pull mongo
```
#### 3. 启动 MongoDB 容器
```bash
docker run -d -p 27017:27017 --name mongodb mongo
```
#### 4. 验证运行
```bash
docker ps
```
应该看到 mongodb 容器正在运行。
### 方法三:使用 MongoDB Compass (图形界面)
#### 1. 下载 MongoDB Compass
- 访问: https://www.mongodb.com/try/download/compass
- 下载 Windows 版本
#### 2. 安装 MongoDB Compass
- 运行安装程序
- 按照提示完成安装
#### 3. 连接到本地 MongoDB
- 打开 MongoDB Compass
- 连接字符串: `mongodb://localhost:27017`
- 点击 **Connect**
## 启动 MongoDB
### Windows 服务方式
如果安装时选择了 "Install MongoDB as a Service"MongoDB 会自动启动。
### 手动启动
```bash
mongod --dbpath C:\data\db
```
### Docker 方式
```bash
docker start mongodb
```
## 验证 MongoDB 运行
### 方法一:使用命令行
```bash
mongosh
```
如果进入 MongoDB Shell说明运行正常。
### 方法二:使用 MongoDB Compass
- 打开 MongoDB Compass
- 连接到 `mongodb://localhost:27017`
- 如果连接成功,说明运行正常
## 常见问题
### 1. 端口被占用
如果 27017 端口被占用,可以修改端口:
```bash
mongod --port 27018 --dbpath C:\data\db
```
同时需要修改项目中的 `.env` 文件:
```
MONGODB_URI=mongodb://localhost:27018/e-scooter-rental
```
### 2. 数据目录权限问题
如果无法创建数据目录,尝试以管理员身份运行命令提示符。
### 3. Docker 连接失败
确保 Docker Desktop 正在运行,并且没有其他容器占用 27017 端口。
## 启动项目
### 1. 启动 MongoDB
确保 MongoDB 正在运行。
### 2. 启动后端服务
```bash
cd E:\code\e-scooter-rental-system
npm run dev
```
### 3. 生成测试数据
```bash
npm run seed
```
### 4. 测试 API
```bash
curl http://localhost:3000/health
```
## 下一步
安装完成后,按照以下顺序操作:
1. ✅ 安装 MongoDB
2. ✅ 启动 MongoDB
3. ✅ 启动后端服务
4. ✅ 生成测试数据
5. ✅ 测试 API 接口
6. ⏳ 开发前端页面

142
docs/README.md Normal file
View File

@ -0,0 +1,142 @@
# 两轮电动车租赁管理系统
## 🚀 项目简介
一个完整的两轮电动车租赁管理系统,包含车辆管理、订单管理、客户管理、财务管理等功能。
## 📁 项目结构
```
e-scooter-rental-system/
├── server/ # 后端服务
│ ├── models/ # 数据模型
│ │ ├── Vehicle.js # 车辆模型
│ │ ├── Order.js # 订单模型
│ │ ├── Customer.js # 客户模型
│ │ └── index.js
│ ├── routes/ # 路由
│ │ ├── vehicles.js # 车辆路由
│ │ ├── orders.js # 订单路由
│ │ ├── customers.js # 客户路由
│ │ ├── finance.js # 财务路由
│ │ └── index.js
│ ├── middleware/ # 中间件
│ │ └── errorHandler.js
│ ├── seed.js # 测试数据脚本
│ └── index.js # 入口文件
├── docs/ # 文档
│ ├── README.md # 项目说明
│ ├── API.md # API 文档
│ ├── 数据库设计.md # 数据库设计
│ ├── 开发计划.md # 开发计划
│ ├── 快速开始.md # 快速开始指南
│ ├── 测试数据.md # 测试数据说明
│ ├── MongoDB安装指南.md # MongoDB 安装指南
│ ├── 开发进度.md # 开发进度
│ ├── 下一步行动.md # 下一步行动
│ ├── 项目总结.md # 项目总结
│ ├── 文件清单.md # 文件清单
│ ├── 常见问题.md # 常见问题
│ ├── 项目里程碑.md # 项目里程碑
│ └── 项目状态.md # 项目状态
├── .env # 环境配置
├── .gitignore
└── package.json
```
## 🛠️ 技术栈
- **后端**: Node.js + Express + MongoDB
- **前端**: Vue.js / React (待定)
- **数据库**: MongoDB
## 📋 功能模块
1. **车辆管理** - 车辆信息、状态追踪、GPS定位
2. **订单管理** - 租赁订单、计费、逾期提醒
3. **客户管理** - 客户信息、租赁历史
4. **财务管理** - 收入统计、报表生成
5. **运维管理** - 维修记录、保养提醒
## 🚀 快速开始
### 1. 安装 MongoDB
详细步骤请查看: [MongoDB安装指南.md](MongoDB安装指南.md)
### 2. 启动后端服务
```bash
cd E:\code\e-scooter-rental-system
npm install
npm run dev
```
### 3. 生成测试数据
```bash
npm run seed
```
### 4. 测试 API
访问: http://localhost:3000/health
## 📖 文档导航
- [API 文档](API.md) - 接口说明
- [数据库设计](数据库设计.md) - 数据模型设计
- [开发计划](开发计划.md) - 项目开发计划
- [快速开始](快速开始.md) - 快速上手指南
- [测试数据](测试数据.md) - 测试数据说明
- [MongoDB安装指南](MongoDB安装指南.md) - 数据库安装步骤
- [项目状态](项目状态.md) - 当前状态
## 🎯 功能优先级
### P0 (必须实现)
1. 车辆信息管理 (增删改查)
2. 订单创建和管理
3. 客户信息管理
4. 订单状态流转
5. 财务统计
### P1 (重要功能)
1. 车辆位置追踪
2. 逾期订单提醒
3. 客户信用评分
4. 数据搜索和筛选
### P2 (锦上添花)
1. 数据导出 (Excel)
2. 报表图表展示
3. 短信通知
4. 移动端适配
## 📝 开发规范
### 代码规范
- 使用 ESLint 代码检查
- 统一代码风格
- 添加必要注释
### Git 规范
- 使用 feature 分支开发
- 提交信息格式: `类型: 描述`
- 定期合并到 main 分支
### API 规范
- 统一返回格式: `{ success, data, message }`
- 使用 HTTP 状态码
- 添加错误处理
## 🚀 当前状态
- ✅ 后端服务框架搭建完成
- ✅ 数据模型设计完成
- ✅ API 路由实现完成
- ✅ 文档编写完成
- ❌ MongoDB 未安装
- ⏳ 需要安装 MongoDB 数据库
- ⏳ 需要生成测试数据
- ⏳ 需要开发前端页面
## 📞 联系方式
如有问题,请联系项目负责人。

141
docs/下一步行动.md Normal file
View File

@ -0,0 +1,141 @@
# 下一步行动
## 🎯 当前首要任务
### 1. 安装 MongoDB 数据库
由于当前环境未安装 MongoDB需要手动安装。
**推荐安装方式Docker (最简单)**
```bash
# 1. 安装 Docker Desktop
# 访问 https://www.docker.com/products/docker-desktop/
# 下载并安装 Docker Desktop for Windows
# 2. 拉取 MongoDB 镜像
docker pull mongo
# 3. 启动 MongoDB 容器
docker run -d -p 27017:27017 --name mongodb mongo
# 4. 验证运行
docker ps
```
**备选安装方式:下载安装包**
1. 访问 https://www.mongodb.com/try/download/community
2. 下载 Windows 版本
3. 运行安装程序
4. 创建数据目录: `C:\data\db`
5. 启动 MongoDB
详细步骤请查看: `docs/MongoDB安装指南.md`
## 📋 安装完成后操作
### 1. 生成测试数据
```bash
cd E:\code\e-scooter-rental-system
npm run seed
```
### 2. 测试 API 接口
#### 健康检查
```bash
curl http://localhost:3000/health
```
#### 查询车辆
```bash
curl http://localhost:3000/api/vehicles
```
#### 查询客户
```bash
curl http://localhost:3000/api/customers
```
#### 查询订单
```bash
curl http://localhost:3000/api/orders
```
#### 查询财务统计
```bash
curl http://localhost:3000/api/finance/stats
```
### 3. 使用 Postman 测试
1. 打开 Postman
2. 创建新请求
3. 设置 URL: `http://localhost:3000/api/vehicles`
4. 设置方法: GET
5. 发送请求
## 🚀 开发前端页面
### 选择技术栈
推荐使用 **Vue.js + Element Plus**
### 创建前端项目
```bash
# 使用 Vue CLI
vue create e-scooter-rental-client
# 或使用 Vite
npm create vite@latest e-scooter-rental-client -- --template vue
```
### 前端页面规划
1. **车辆管理页面**
- 车辆列表
- 车辆详情
- 添加/编辑车辆
- 车辆位置地图
2. **订单管理页面**
- 订单列表
- 创建订单
- 订单详情
- 结束订单
3. **客户管理页面**
- 客户列表
- 客户详情
- 添加/编辑客户
- 客户信用评分
4. **财务统计页面**
- 收入统计
- 逾期账款
- 财务报表
## 📝 开发计划
### 第一阶段:后端完善 (1天)
- [ ] 安装 MongoDB
- [ ] 生成测试数据
- [ ] 测试所有 API 接口
### 第二阶段:前端开发 (2-3天)
- [ ] 创建前端项目
- [ ] 开发车辆管理页面
- [ ] 开发订单管理页面
- [ ] 开发客户管理页面
### 第三阶段:功能完善 (1-2天)
- [ ] 开发财务统计页面
- [ ] 实现逾期提醒功能
- [ ] 完善用户权限管理
### 第四阶段:测试部署 (1天)
- [ ] 功能测试
- [ ] Bug 修复
- [ ] 部署上线
## 📞 需要帮助?
如果在安装 MongoDB 或开发过程中遇到问题,请随时联系!

487
docs/前端开发指南.md Normal file
View File

@ -0,0 +1,487 @@
# 前端开发指南
## 技术栈选择
### 推荐方案Vue.js + Element Plus
**理由:**
- 学习曲线平缓,适合快速开发
- Element Plus 组件丰富,适合管理后台
- 社区活跃,文档完善
### 备选方案React + Ant Design
**理由:**
- 生态系统庞大
- Ant Design 企业级组件库
- 适合大型项目
## 项目结构
```
e-scooter-rental-system/
├── frontend/ # 前端项目
│ ├── public/ # 静态资源
│ ├── src/
│ │ ├── components/ # 公共组件
│ │ ├── views/ # 页面组件
│ │ ├── router/ # 路由配置
│ │ ├── store/ # 状态管理
│ │ ├── api/ # API 接口
│ │ ├── utils/ # 工具函数
│ │ └── App.vue # 根组件
│ ├── package.json
│ └── vue.config.js
└── server/ # 后端项目 (已存在)
```
## 页面设计
### 1. 车辆管理页面
**功能:**
- 车辆列表展示(表格)
- 车辆搜索和筛选
- 车辆详情查看
- 车辆添加/编辑/删除
- 车辆状态更新
**页面布局:**
```
┌─────────────────────────────────────┐
│ 顶部导航栏 │
├─────────────────────────────────────┤
│ 搜索栏 + 筛选条件 │
├─────────────────────────────────────┤
│ 车辆列表(表格) │
│ - 车辆编号 │
│ - 车辆类型 │
│ - 状态(空闲/使用中/维修中) │
│ - 位置 │
│ - 操作(查看/编辑/删除) │
└─────────────────────────────────────┘
```
### 2. 订单管理页面
**功能:**
- 订单列表展示
- 订单搜索和筛选(按状态、时间等)
- 订单详情查看
- 创建新订单
- 结束订单
- 逾期订单提醒
**页面布局:**
```
┌─────────────────────────────────────┐
│ 顶部导航栏 │
├─────────────────────────────────────┤
│ 搜索栏 + 筛选条件(状态、时间范围) │
├─────────────────────────────────────┤
│ 订单列表(表格) │
│ - 订单号 │
│ - 客户名称 │
│ - 车辆信息 │
│ - 租期 │
│ - 费用 │
│ - 状态 │
│ - 操作(查看/结束) │
└─────────────────────────────────────┘
```
### 3. 客户管理页面
**功能:**
- 客户列表展示
- 客户搜索
- 客户详情查看
- 添加/编辑/删除客户
- 信用评分管理
**页面布局:**
```
┌─────────────────────────────────────┐
│ 顶部导航栏 │
├─────────────────────────────────────┤
│ 搜索栏 │
├─────────────────────────────────────┤
│ 客户列表(表格) │
│ - 客户姓名 │
│ - 联系方式 │
│ - 信用评分 │
│ - 租赁历史 │
│ - 操作(查看/编辑/删除) │
└─────────────────────────────────────┘
```
### 4. 财务统计页面
**功能:**
- 收入统计图表
- 逾期账款统计
- 订单收入趋势
- 导出报表
**页面布局:**
```
┌─────────────────────────────────────┐
│ 顶部导航栏 │
├─────────────────────────────────────┤
│ 统计卡片 │
│ - 总收入 │
│ - 本月收入 │
│ - 逾期账款 │
│ - 订单数量 │
├─────────────────────────────────────┤
│ 图表区域 │
│ - 收入趋势图 │
│ - 订单状态分布 │
│ - 车辆使用率 │
└─────────────────────────────────────┘
```
## 组件设计
### 公共组件
1. **导航栏组件**
- 顶部导航
- 侧边栏菜单
2. **表格组件**
- 支持分页
- 支持搜索
- 支持排序
3. **表单组件**
- 输入框
- 下拉选择
- 日期选择
- 表单验证
4. **弹窗组件**
- 确认弹窗
- 详情弹窗
- 编辑弹窗
### 页面组件
1. **车辆管理**
- VehicleList.vue
- VehicleDetail.vue
- VehicleForm.vue
2. **订单管理**
- OrderList.vue
- OrderDetail.vue
- OrderForm.vue
3. **客户管理**
- CustomerList.vue
- CustomerDetail.vue
- CustomerForm.vue
4. **财务统计**
- FinanceDashboard.vue
- StatisticsChart.vue
## API 接口调用
### 基础配置
```javascript
// src/api/request.js
import axios from 'axios'
const request = axios.create({
baseURL: 'http://localhost:3000/api',
timeout: 10000
})
// 请求拦截器
request.interceptors.request.use(config => {
// 添加认证 token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
request.interceptors.response.use(
response => response.data,
error => {
console.error('API Error:', error)
return Promise.reject(error)
}
)
export default request
```
### 车辆管理 API
```javascript
// src/api/vehicle.js
import request from './request'
export const getVehicles = (params) => request.get('/vehicles', { params })
export const getVehicle = (id) => request.get(`/vehicles/${id}`)
export const createVehicle = (data) => request.post('/vehicles', data)
export const updateVehicle = (id, data) => request.put(`/vehicles/${id}`, data)
export const deleteVehicle = (id) => request.delete(`/vehicles/${id}`)
export const getVehiclesByStatus = (status) => request.get(`/vehicles/status/${status}`)
```
### 订单管理 API
```javascript
// src/api/order.js
import request from './request'
export const getOrders = (params) => request.get('/orders', { params })
export const getOrder = (id) => request.get(`/orders/${id}`)
export const createOrder = (data) => request.post('/orders', data)
export const updateOrder = (id, data) => request.put(`/orders/${id}`, data)
export const endOrder = (id) => request.post(`/orders/${id}/end`)
export const getOverdueOrders = () => request.get('/orders/overdue')
```
### 客户管理 API
```javascript
// src/api/customer.js
import request from './request'
export const getCustomers = (params) => request.get('/customers', { params })
export const getCustomer = (id) => request.get(`/customers/${id}`)
export const createCustomer = (data) => request.post('/customers', data)
export const updateCustomer = (id, data) => request.put(`/customers/${id}`, data)
export const deleteCustomer = (id) => request.delete(`/customers/${id}`)
export const searchCustomers = (keyword) => request.get(`/customers/search/${keyword}`)
```
## 状态管理
### 使用 Vuex
```javascript
// src/store/index.js
import { createStore } from 'vuex'
export default createStore({
state: {
vehicles: [],
orders: [],
customers: [],
currentUser: null
},
mutations: {
SET_VEHICLES(state, vehicles) {
state.vehicles = vehicles
},
SET_ORDERS(state, orders) {
state.orders = orders
},
SET_CUSTOMERS(state, customers) {
state.customers = customers
}
},
actions: {
async fetchVehicles({ commit }) {
const response = await getVehicles()
commit('SET_VEHICLES', response.data)
}
}
})
```
## 路由配置
```javascript
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
redirect: '/vehicles'
},
{
path: '/vehicles',
name: 'VehicleList',
component: () => import('@/views/VehicleList.vue')
},
{
path: '/vehicles/:id',
name: 'VehicleDetail',
component: () => import('@/views/VehicleDetail.vue')
},
{
path: '/orders',
name: 'OrderList',
component: () => import('@/views/OrderList.vue')
},
{
path: '/customers',
name: 'CustomerList',
component: () => import('@/views/CustomerList.vue')
},
{
path: '/finance',
name: 'FinanceDashboard',
component: () => import('@/views/FinanceDashboard.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
```
## 样式设计
### 使用 Element Plus 主题
```javascript
// src/main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import store from './store'
const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.use(store)
app.mount('#app')
```
### 自定义样式
```scss
// src/assets/styles/main.scss
// 主题色
$primary-color: #409EFF;
$success-color: #67C23A;
$warning-color: #E6A23C;
$danger-color: #F56C6C;
$info-color: #909399;
// 布局
.container {
padding: 20px;
}
.card {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
// 表格样式
.table-container {
.el-table {
.cell {
text-align: center;
}
}
}
// 表单样式
.form-container {
max-width: 600px;
margin: 0 auto;
}
```
## 开发步骤
### 1. 创建前端项目
```bash
# 进入项目目录
cd E:\code\e-scooter-rental-system
# 创建前端项目
npm create vue@latest frontend
# 或使用 Vue CLI
# vue create frontend
# 进入前端目录
cd frontend
# 安装依赖
npm install
# 安装 Element Plus
npm install element-plus
# 安装 Vue Router
npm install vue-router@4
# 安装 Vuex (可选)
npm install vuex@next
```
### 2. 配置开发环境
```javascript
// frontend/vue.config.js
module.exports = {
devServer: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
}
```
### 3. 启动开发服务器
```bash
npm run dev
```
访问http://localhost:8080
## 注意事项
1. **跨域问题**
- 开发环境使用 proxy 解决跨域
- 生产环境需要配置 CORS
2. **API 认证**
- 使用 JWT token 认证
- token 存储在 localStorage
3. **错误处理**
- 统一错误提示
- 网络错误处理
4. **性能优化**
- 按需加载组件
- 图片懒加载
- 数据缓存
## 下一步
1. 安装 MongoDB首要任务
2. 生成测试数据
3. 创建前端项目
4. 开发车辆管理页面
5. 开发订单管理页面
6. 开发客户管理页面
7. 开发财务统计页面

132
docs/常见问题.md Normal file
View File

@ -0,0 +1,132 @@
# 常见问题
## MongoDB 相关
### 1. MongoDB 连接失败
**错误信息:**
```
connect ECONNREFUSED ::1:27017
connect ECONNREFUSED 127.0.0.1:27017
```
**原因:** MongoDB 未安装或未启动
**解决方案:**
1. 安装 MongoDB (推荐使用 Docker)
2. 启动 MongoDB
3. 重新启动后端服务
### 2. 端口被占用
**错误信息:**
```
EADDRINUSE: address already in use :::27017
```
**解决方案:**
1. 修改 .env 文件中的 MONGODB_URI
2. 或停止占用端口的服务
### 3. Docker 安装失败
**可能原因:**
- Docker Desktop 未启动
- 端口冲突
- 权限不足
**解决方案:**
1. 确保 Docker Desktop 正在运行
2. 检查端口是否被占用
3. 以管理员身份运行命令
## 后端服务相关
### 1. 端口被占用
**错误信息:**
```
Error: listen EADDRINUSE: address already in use :::3000
```
**解决方案:**
1. 修改 .env 文件中的 PORT
2. 或停止占用 3000 端口的服务
### 2. nodemon 未找到
**错误信息:**
```
'nodemon' is not recognized as an internal or external command
```
**解决方案:**
```bash
npm install nodemon --save-dev
```
### 3. 依赖安装失败
**解决方案:**
1. 清除 npm 缓存: `npm cache clean --force`
2. 删除 node_modules 文件夹
3. 重新安装: `npm install`
## API 相关
### 1. API 返回错误
**检查步骤:**
1. 确保 MongoDB 正在运行
2. 检查 API URL 是否正确
3. 查看后端日志
### 2. 跨域问题
**解决方案:**
- 已配置 CORS 中间件
- 确保前端正确配置代理
## 开发相关
### 1. 如何添加新功能
**步骤:**
1. 设计数据模型
2. 创建模型文件
3. 创建路由文件
4. 更新 API 文档
5. 测试接口
### 2. 如何修改现有功能
**步骤:**
1. 修改模型文件
2. 修改路由文件
3. 更新 API 文档
4. 测试接口
## 部署相关
### 1. 生产环境配置
**注意事项:**
1. 修改 .env 文件
2. 配置 MongoDB 远程连接
3. 配置 CORS 白名单
4. 启用 HTTPS
### 2. 性能优化
**建议:**
1. 使用 Redis 缓存
2. 优化数据库查询
3. 启用 gzip 压缩
4. 使用 CDN
## 联系方式
如果遇到其他问题,请查看:
- `docs/MongoDB安装指南.md` - 数据库安装
- `docs/快速开始.md` - 快速上手
- `docs/API.md` - 接口文档

120
docs/开发计划.md Normal file
View File

@ -0,0 +1,120 @@
# 开发计划
## 📅 项目时间线
### 第一阶段:后端开发 (1-2天)
- [x] 项目初始化
- [x] 数据库模型设计
- [x] API 路由实现
- [ ] 数据库连接测试
- [ ] API 接口测试
### 第二阶段:前端开发 (2-3天)
- [ ] 前端项目初始化
- [ ] 页面布局设计
- [ ] 车辆管理页面
- [ ] 订单管理页面
- [ ] 客户管理页面
- [ ] 财务统计页面
### 第三阶段:功能完善 (1-2天)
- [ ] 用户登录/权限管理
- [ ] 数据导入导出
- [ ] 报表生成
- [ ] 逾期提醒功能
### 第四阶段:测试部署 (1天)
- [ ] 功能测试
- [ ] Bug 修复
- [ ] 部署上线
---
## 🎯 功能优先级
### P0 (必须实现)
1. 车辆信息管理 (增删改查)
2. 订单创建和管理
3. 客户信息管理
4. 订单状态流转
5. 财务统计
### P1 (重要功能)
1. 车辆位置追踪
2. 逾期订单提醒
3. 客户信用评分
4. 数据搜索和筛选
### P2 (锦上添花)
1. 数据导出 (Excel)
2. 报表图表展示
3. 短信通知
4. 移动端适配
---
## 🔧 技术栈选择
### 后端
- Node.js + Express
- MongoDB (数据库)
- JWT (认证)
### 前端 (待定)
- Vue.js + Element Plus
- 或 React + Ant Design
### 部署
- 本地开发: Node.js
- 生产环境: 云服务器 + Docker
---
## 📝 开发规范
### 代码规范
- 使用 ESLint 代码检查
- 统一代码风格
- 添加必要注释
### Git 规范
- 使用 feature 分支开发
- 提交信息格式: `类型: 描述`
- 定期合并到 main 分支
### API 规范
- 统一返回格式: `{ success, data, message }`
- 使用 HTTP 状态码
- 添加错误处理
---
## 🚀 快速开始
### 1. 安装依赖
```bash
npm install
```
### 2. 启动 MongoDB
```bash
# Windows
mongod --dbpath E:\data\db
# 或使用 Docker
docker run -d -p 27017:27017 --name mongodb mongo
```
### 3. 启动后端
```bash
npm run dev
```
### 4. 测试 API
访问: http://localhost:3000/health
---
## 📞 联系方式
如有问题,请联系项目负责人。

98
docs/开发进度.md Normal file
View File

@ -0,0 +1,98 @@
# 开发进度
## 📅 2026-03-05
### ✅ 已完成
#### 1. 项目初始化
- 创建项目目录 `E:\code\e-scooter-rental-system`
- 初始化 npm 项目
- 安装依赖: express, mongoose, cors, dotenv
#### 2. 后端服务
- ✅ 创建 Express 服务器
- ✅ 配置 MongoDB 连接
- ✅ 实现错误处理中间件
- ✅ 实现 404 处理
- ✅ 健康检查接口
#### 3. 数据模型
- ✅ 车辆模型 (Vehicle)
- ✅ 订单模型 (Order)
- ✅ 客户模型 (Customer)
#### 4. API 路由
- ✅ 车辆管理路由
- ✅ 订单管理路由
- ✅ 客户管理路由
- ✅ 财务管理路由
#### 5. 文档编写
- ✅ API 文档
- ✅ 数据库设计文档
- ✅ 开发计划文档
- ✅ 快速开始指南
- ✅ 测试数据说明
- ✅ MongoDB 安装指南
- ✅ 项目说明文档
#### 6. 工具脚本
- ✅ 测试数据生成脚本 (seed.js)
### ⏳ 进行中
1. **安装 MongoDB 数据库**
- 需要手动安装 MongoDB
- 详细步骤见 `MongoDB安装指南.md`
2. **生成测试数据**
- 等待 MongoDB 安装完成后运行
3. **测试 API 接口**
- 等待 MongoDB 安装完成后测试
### 📋 待办事项
#### 立即执行
- [ ] 安装 MongoDB 数据库
- [ ] 运行种子脚本生成测试数据
- [ ] 测试所有 API 接口
#### 短期目标 (1-2天)
- [ ] 选择前端技术栈 (Vue.js 或 React)
- [ ] 创建前端项目
- [ ] 开发车辆管理页面
- [ ] 开发订单管理页面
- [ ] 开发客户管理页面
#### 中期目标 (3-5天)
- [ ] 开发财务统计页面
- [ ] 实现逾期提醒功能
- [ ] 完善用户权限管理
- [ ] 集成测试
#### 长期目标
- [ ] 部署上线
- [ ] 性能优化
- [ ] 用户反馈收集
## 📊 项目统计
- **代码文件**: 15 个
- **文档文件**: 7 个
- **API 接口**: 4 个模块
- **数据模型**: 3 个
## 🎯 当前状态
- ✅ 后端服务框架完成
- ✅ 数据模型设计完成
- ⏳ 需要安装 MongoDB
- ⏳ 需要生成测试数据
- ⏳ 需要开发前端
## 📝 注意事项
1. MongoDB 安装是当前首要任务
2. 安装完成后运行 `npm run seed` 生成测试数据
3. 前端开发需要选择技术栈

103
docs/快速开始.md Normal file
View File

@ -0,0 +1,103 @@
# 快速开始指南
## 1. 安装 MongoDB
### Windows 安装步骤
1. **下载 MongoDB**
- 访问: https://www.mongodb.com/try/download/community
- 选择 Windows 版本下载
2. **安装 MongoDB**
- 运行安装程序
- 选择 "Complete" 安装类型
- 勾选 "Install MongoDB as a Service"
3. **配置数据目录**
- 默认路径: `C:\data\db`
- 如果目录不存在,手动创建:
```bash
mkdir C:\data\db
```
4. **启动 MongoDB**
- 如果安装为服务,会自动启动
- 或手动启动:
```bash
mongod --dbpath C:\data\db
```
### 使用 Docker 安装 (推荐)
```bash
# 拉取 MongoDB 镜像
docker pull mongo
# 启动 MongoDB 容器
docker run -d -p 27017:27017 --name mongodb mongo
```
## 2. 启动后端服务
### 安装依赖
```bash
cd E:\code\e-scooter-rental-system
npm install
```
### 启动开发服务器
```bash
npm run dev
```
### 验证服务
访问: http://localhost:3000/health
预期返回:
```json
{
"status": "ok",
"timestamp": "2026-03-05T06:06:20.281Z"
}
```
## 3. 测试 API 接口
### 使用 curl 测试
```bash
# 测试健康检查
curl http://localhost:3000/health
# 测试车辆列表 (需要先启动 MongoDB)
curl http://localhost:3000/api/vehicles
```
### 使用 Postman 测试
1. 打开 Postman
2. 创建新请求
3. 设置 URL: `http://localhost:3000/api/vehicles`
4. 设置方法: GET
5. 发送请求
## 4. 常见问题
### MongoDB 连接失败
- 检查 MongoDB 是否启动
- 检查端口 27017 是否被占用
- 检查 .env 文件中的 MONGODB_URI 配置
### 端口被占用
- 修改 .env 文件中的 PORT 配置
- 或停止占用端口的其他服务
### nodemon 命令未找到
- 运行: `npm install nodemon --save-dev`
## 5. 下一步
1. ✅ 后端服务已启动
2. ⏳ 安装并启动 MongoDB
3. ⏳ 测试 API 接口
4. ⏳ 开发前端页面

137
docs/数据库设计.md Normal file
View File

@ -0,0 +1,137 @@
# 数据库设计文档
## 数据库: MongoDB
## 集合: vehicles, orders, customers
---
## 1. 车辆集合 (vehicles)
### 字段说明
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| vehicleId | String | ✅ | 车架号/车辆编号 (唯一) |
| model | String | ✅ | 车型 |
| brand | String | ✅ | 品牌 |
| color | String | ❌ | 颜色 |
| batteryType | String | ❌ | 电池类型 |
| batteryCapacity | Number | ❌ | 电池容量 (Ah) |
| batteryStatus | String | ❌ | 电池状态: 正常/老化/待更换 |
| status | String | ✅ | 车辆状态: 空闲/在租/维修中/已报废/待回收 |
| location | Object | ❌ | GPS位置 {type, coordinates} |
| lastLocationUpdate | Date | ❌ | 最后位置更新时间 |
| currentOrderId | ObjectId | ❌ | 当前订单ID (关联orders) |
| totalRentDays | Number | ❌ | 累计租赁天数 |
| lastMaintenanceDate | Date | ❌ | 最后保养日期 |
| nextMaintenanceDate | Date | ❌ | 下次保养日期 |
| maintenanceHistory | Array | ❌ | 维修历史记录 |
| purchaseDate | Date | ❌ | 购买日期 |
| purchasePrice | Number | ❌ | 购买价格 |
| purchaseSupplier | String | ❌ | 供应商 |
| notes | String | ❌ | 备注 |
| createdAt | Date | ✅ | 创建时间 |
| updatedAt | Date | ✅ | 更新时间 |
### 索引
- vehicleId: 唯一索引
- location: 2dsphere 索引 (用于地理查询)
---
## 2. 订单集合 (orders)
### 字段说明
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| orderNumber | String | ✅ | 订单号 (唯一) |
| customer | ObjectId | ✅ | 客户ID (关联customers) |
| vehicle | ObjectId | ✅ | 车辆ID (关联vehicles) |
| startDate | Date | ✅ | 开始日期 |
| endDate | Date | ✅ | 结束日期 |
| actualEndDate | Date | ❌ | 实际结束日期 |
| rentalFee | Number | ✅ | 租金 (元/天) |
| deposit | Number | ❌ | 押金 |
| totalAmount | Number | ✅ | 总金额 |
| paidAmount | Number | ❌ | 已支付金额 |
| status | String | ✅ | 订单状态: 待支付/进行中/已完成/逾期/已取消/已退款 |
| overdueDays | Number | ❌ | 逾期天数 |
| overdueFee | Number | ❌ | 逾期费用 |
| paymentMethod | String | ❌ | 支付方式: 微信/支付宝/现金/银行卡 |
| paymentDate | Date | ❌ | 支付日期 |
| contractUrl | String | ❌ | 合同文件路径 |
| contractSigned | Boolean | ❌ | 合同是否签署 |
| notes | String | ❌ | 备注 |
| createdAt | Date | ✅ | 创建时间 |
| updatedAt | Date | ✅ | 更新时间 |
### 索引
- orderNumber: 唯一索引
- customer: 普通索引
- vehicle: 普通索引
- status: 普通索引
- endDate: 普通索引 (用于查询逾期订单)
---
## 3. 客户集合 (customers)
### 字段说明
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| customerId | String | ✅ | 客户编号 (唯一) |
| name | String | ✅ | 姓名 |
| phone | String | ✅ | 手机号 |
| idCard | String | ❌ | 身份证号 |
| address | String | ❌ | 地址 |
| email | String | ❌ | 邮箱 |
| totalRentals | Number | ❌ | 总租赁次数 |
| totalSpent | Number | ❌ | 总消费金额 |
| currentRentals | Number | ❌ | 当前租赁数量 |
| creditScore | Number | ❌ | 信用评分 (0-100) |
| creditLevel | String | ❌ | 信用等级: 优秀/良好/一般/较差 |
| accountStatus | String | ✅ | 账户状态: 正常/冻结/注销 |
| notes | String | ❌ | 备注 |
| createdAt | Date | ✅ | 创建时间 |
| updatedAt | Date | ✅ | 更新时间 |
### 索引
- customerId: 唯一索引
- phone: 唯一索引
- name: 普通索引 (用于搜索)
---
## 4. 关系说明
### 车辆 ↔ 订单
- 一辆车同一时间只能有一个进行中的订单
- 车辆状态会随订单状态变化
### 客户 ↔ 订单
- 一个客户可以有多个订单
- 客户的租赁次数和消费金额会随订单更新
### 订单 ↔ 车辆/客户
- 订单必须关联一辆车和一个客户
- 订单完成后,车辆状态变为空闲
---
## 5. 数据验证规则
### 车辆
- vehicleId 必须唯一
- status 必须在预定义枚举中
- location 必须是有效的经纬度坐标
### 订单
- orderNumber 必须唯一
- startDate 必须早于 endDate
- rentalFee 必须大于 0
- status 必须在预定义枚举中
### 客户
- customerId 必须唯一
- phone 必须唯一
- creditScore 必须在 0-100 之间
- creditLevel 由 creditScore 自动计算

200
docs/文件清单.md Normal file
View File

@ -0,0 +1,200 @@
# 文件清单
## 项目结构
```
e-scooter-rental-system/
├── server/ # 后端代码
│ ├── models/ # 数据模型
│ │ ├── Vehicle.js # 车辆模型
│ │ ├── Order.js # 订单模型
│ │ ├── Customer.js # 客户模型
│ │ └── index.js # 模型导出
│ ├── routes/ # 路由
│ │ ├── vehicles.js # 车辆路由
│ │ ├── orders.js # 订单路由
│ │ ├── customers.js # 客户路由
│ │ ├── finance.js # 财务路由
│ │ └── index.js # 路由导出
│ ├── middleware/ # 中间件
│ │ └── errorHandler.js # 错误处理
│ ├── seed.js # 测试数据脚本
│ └── index.js # 入口文件
├── docs/ # 文档
│ ├── API.md # API 文档
│ ├── 数据库设计.md # 数据库设计
│ ├── 开发计划.md # 开发计划
│ ├── 快速开始.md # 快速开始指南
│ ├── 测试数据.md # 测试数据说明
│ ├── MongoDB安装指南.md # MongoDB 安装指南
│ ├── README.md # 项目说明
│ ├── 开发进度.md # 开发进度
│ ├── 下一步行动.md # 下一步行动
│ ├── 项目总结.md # 项目总结
│ ├── 文件清单.md # 文件清单
│ ├── 常见问题.md # 常见问题
│ ├── 项目里程碑.md # 项目里程碑
│ ├── 项目状态.md # 项目状态
│ ├── 项目完成清单.md # 项目完成清单
│ ├── 前端开发指南.md # 前端开发指南
│ └── 页面设计.md # 页面设计
├── .env # 环境配置
├── .gitignore # Git 忽略文件
├── package.json # 项目配置
└── README.md # 项目说明
```
## 代码文件 (15个)
### 数据模型 (4个)
1. `server/models/Vehicle.js` - 车辆模型
2. `server/models/Order.js` - 订单模型
3. `server/models/Customer.js` - 客户模型
4. `server/models/index.js` - 模型导出
### 路由 (5个)
5. `server/routes/vehicles.js` - 车辆路由
6. `server/routes/orders.js` - 订单路由
7. `server/routes/customers.js` - 客户路由
8. `server/routes/finance.js` - 财务路由
9. `server/routes/index.js` - 路由导出
### 中间件 (1个)
10. `server/middleware/errorHandler.js` - 错误处理
### 入口文件 (1个)
11. `server/index.js` - 入口文件
### 工具脚本 (1个)
12. `server/seed.js` - 测试数据脚本
### 配置文件 (3个)
13. `.env` - 环境配置
14. `.gitignore` - Git 忽略文件
15. `package.json` - 项目配置
## 文档文件 (16个)
### 项目文档 (16个)
1. `docs/API.md` - API 接口文档
2. `docs/数据库设计.md` - 数据库设计文档
3. `docs/开发计划.md` - 开发计划文档
4. `docs/快速开始.md` - 快速开始指南
5. `docs/测试数据.md` - 测试数据说明
6. `docs/MongoDB安装指南.md` - MongoDB 安装指南
7. `docs/README.md` - 项目说明文档
8. `docs/开发进度.md` - 开发进度文档
9. `docs/下一步行动.md` - 下一步行动文档
10. `docs/项目总结.md` - 项目总结文档
11. `docs/文件清单.md` - 文件清单文档
12. `docs/常见问题.md` - 常见问题文档
13. `docs/项目里程碑.md` - 项目里程碑文档
14. `docs/项目状态.md` - 项目状态文档
15. `docs/项目完成清单.md` - 项目完成清单文档
16. `docs/前端开发指南.md` - 前端开发指南
17. `docs/页面设计.md` - 页面设计文档
## 统计信息
### 文件总数
- 代码文件: 15 个
- 文档文件: 17 个
- 配置文件: 3 个
- **总计: 35 个文件** (不含 node_modules)
### API 接口统计
- 车辆管理: 7 个接口
- 订单管理: 7 个接口
- 客户管理: 7 个接口
- 财务管理: 3 个接口
- **总计: 24 个接口**
### 文档统计
- 项目文档: 17 个
- 涵盖: 安装、开发、测试、部署、前端开发全流程
## 文件说明
### 核心代码文件
#### 数据模型
- **Vehicle.js**: 车辆信息、状态、位置、维护记录
- **Order.js**: 订单号、客户、车辆、租期、费用、状态
- **Customer.js**: 客户信息、信用评分、租赁历史
#### 路由文件
- **vehicles.js**: 车辆管理接口 (7个)
- **orders.js**: 订单管理接口 (7个)
- **customers.js**: 客户管理接口 (7个)
- **finance.js**: 财务管理接口 (3个)
#### 中间件
- **errorHandler.js**: 统一错误处理
### 文档文件
#### 安装部署
- **MongoDB安装指南.md**: MongoDB 安装步骤
- **快速开始.md**: 项目快速启动指南
#### 开发文档
- **API.md**: 所有 API 接口说明
- **数据库设计.md**: 数据库表结构设计
- **开发计划.md**: 项目开发时间线
- **前端开发指南.md**: 前端开发规范
- **页面设计.md**: 页面布局和组件设计
#### 项目管理
- **开发进度.md**: 当前开发进度
- **下一步行动.md**: 待办事项清单
- **项目总结.md**: 项目概述和总结
- **项目状态.md**: 项目当前状态
- **项目里程碑.md**: 项目里程碑节点
- **项目完成清单.md**: 完成事项清单
#### 维护文档
- **常见问题.md**: 常见问题和解决方案
- **文件清单.md**: 项目文件清单
## 文件依赖关系
### 代码文件依赖
```
server/index.js
├── server/models/index.js
│ ├── server/models/Vehicle.js
│ ├── server/models/Order.js
│ └── server/models/Customer.js
├── server/routes/index.js
│ ├── server/routes/vehicles.js
│ ├── server/routes/orders.js
│ ├── server/routes/customers.js
│ └── server/routes/finance.js
└── server/middleware/errorHandler.js
```
### 文档文件依赖
```
README.md (项目入口)
├── 快速开始.md (启动指南)
├── MongoDB安装指南.md (数据库安装)
├── API.md (接口文档)
├── 开发计划.md (开发路线)
└── 前端开发指南.md (前端开发)
```
## 更新记录
### 2026-03-05
- 新增: 前端开发指南.md
- 新增: 页面设计.md
- 更新: 文件清单.md (增加前端文档)
- 更新: USER.md (增加文档引用)
## 下一步
1. 安装 MongoDB (首要任务)
2. 生成测试数据
3. 测试 API 接口
4. 创建前端项目
5. 实现页面设计

90
docs/测试数据.md Normal file
View File

@ -0,0 +1,90 @@
# 测试数据说明
## 生成测试数据
### 1. 启动 MongoDB
确保 MongoDB 已启动并运行在默认端口 27017。
### 2. 运行种子脚本
```bash
cd E:\code\e-scooter-rental-system
npm run seed
```
### 3. 预期输出
```
🧹 清空数据...
✅ 数据已清空
🚗 创建示例车辆...
✅ 创建了 5 辆车
👥 创建示例客户...
✅ 创建了 5 个客户
📋 创建示例订单...
✅ 创建了 3 个订单
🎉 示例数据创建完成!
车辆: 5 辆
客户: 5 个
订单: 3 个
```
## 测试数据详情
### 车辆数据 (5辆)
| 车辆编号 | 车型 | 品牌 | 颜色 | 状态 |
|---------|------|------|------|------|
| SCOOTER001 | 黑骑士 | 小牛 | 黑色 | 空闲 |
| SCOOTER002 | 黑骑士 | 小牛 | 白色 | 空闲 |
| SCOOTER003 | 电动车 | 雅迪 | 蓝色 | 空闲 |
| SCOOTER004 | 高端豪车 | 特斯拉 | 红色 | 空闲 |
| SCOOTER005 | 普通标准套餐 | 爱玛 | 绿色 | 空闲 |
### 客户数据 (5个)
| 姓名 | 手机号 | 地址 |
|------|--------|------|
| 张三 | 13800138000 | 北京市朝阳区 |
| 李四 | 13800138001 | 北京市海淀区 |
| 王五 | 13800138002 | 北京市西城区 |
| 赵六 | 13800138003 | 北京市东城区 |
| 钱七 | 13800138004 | 北京市丰台区 |
### 订单数据 (3个)
| 客户 | 车辆 | 租期 | 租金 | 状态 |
|------|------|------|------|------|
| 张三 | SCOOTER001 | 2026-02-20 至 2026-03-20 | 50元/天 | 进行中 |
| 李四 | SCOOTER002 | 2026-02-15 至 2026-03-15 | 50元/天 | 进行中 |
| 王五 | SCOOTER003 | 2026-01-10 至 2026-02-10 | 40元/天 | 已完成 |
## API 测试示例
### 1. 查询所有车辆
```bash
curl http://localhost:3000/api/vehicles
```
### 2. 查询所有客户
```bash
curl http://localhost:3000/api/customers
```
### 3. 查询所有订单
```bash
curl http://localhost:3000/api/orders
```
### 4. 查询财务统计
```bash
curl http://localhost:3000/api/finance/stats
```
### 5. 查询逾期订单
```bash
curl http://localhost:3000/api/orders/status/overdue
```
## 注意事项
1. 运行种子脚本前,请确保 MongoDB 已启动
2. 种子脚本会清空现有数据,请谨慎使用
3. 测试数据仅供开发和测试使用
4. 生产环境请使用真实数据

405
docs/页面设计.md Normal file
View File

@ -0,0 +1,405 @@
# 页面设计
## 设计原则
### 1. 用户体验优先
- 简洁明了的界面布局
- 直观的操作流程
- 及时的反馈提示
- ⚠️ **注意**:确保选中状态、悬停状态等交互状态有足够的对比度,避免用户看不清当前选中的项目
### 2. 响应式设计
- 适配不同屏幕尺寸
- 移动端友好
- 桌面端优化
### 3. 一致性
- 统一的配色方案
- 统一的组件样式
- 统一的交互方式
## 配色方案
### 主色调
- 主色:#409EFF蓝色
- 成功:#67C23A绿色
- 警告:#E6A23C橙色
- 危险:#F56C6C红色
- 信息:#909399灰色
### 背景色
- 页面背景:#F5F7FA
- 卡片背景:#FFFFFF
- 表格斑马纹:#FAFAFA
## 布局设计
### 顶部导航栏
```
┌─────────────────────────────────────────────────────────────┐
│ logo │ 车辆管理 │ 订单管理 │ 客户管理 │ 财务统计 │ 用户名 │
└─────────────────────────────────────────────────────────────┘
```
### 侧边栏菜单
```
┌──────────────┬──────────────────────────────────────────────┐
│ │ 顶部导航栏 │
│ 侧边栏 ├──────────────────────────────────────────────┤
│ - 车辆管理 │ │
│ - 订单管理 │ 内容区域 │
│ - 客户管理 │ │
│ - 财务统计 │ │
│ │ │
└──────────────┴──────────────────────────────────────────────┘
```
#### 侧边栏菜单样式(重要!)
⚠️ **常见问题**Element Plus 默认的菜单选中样式是深蓝色背景 + 白色文字,在某些主题下对比度不足,导致选中项看不清。
**解决方案:**
```css
/* 侧边栏菜单自定义样式 */
.el-menu {
border-right: none !important;
}
/* 选中项样式 - 使用主色调但调整对比度 */
.el-menu-item.is-active {
background-color: rgba(64, 158, 255, 0.15) !important;
color: #409EFF !important;
font-weight: 600;
}
/* 悬停效果 */
.el-menu-item:hover {
background-color: rgba(64, 158, 255, 0.08) !important;
}
/* 选中项左侧蓝色指示条(替代全覆盖背景) */
.el-menu-item.is-active::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 3px;
background-color: #409EFF;
border-radius: 0 2px 2px 0;
}
/* 展开/折叠图标颜色 */
.el-sub-menu__title {
color: #303133;
}
.el-menu-item, .el-sub-menu__title {
transition: all 0.3s ease;
}
```
**设计要点:**
- 不要用全覆盖的深色背景
- 使用左侧 3px 蓝色指示条 + 浅蓝色背景
- 保持文字清晰可读
- 添加过渡动画提升体验
## 页面设计详情
### 1. 车辆管理页面
#### 车辆列表页
**布局:**
```
┌─────────────────────────────────────────────────────────────┐
│ 车辆管理 > 车辆列表 │
├─────────────────────────────────────────────────────────────┤
│ [搜索框] [状态筛选] [车辆类型筛选] [添加车辆] │
├─────────────────────────────────────────────────────────────┤
│ ┌────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │编号│类型 │状态 │位置 │续航 │价格 │操作 │ │ │
│ ├────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ │
│ │001 │电动车│空闲 │A区 │50km │¥50/天│查看 编辑 删除│ │
│ │002 │电动车│使用中│B区 │45km │¥50/天│查看 编辑 删除│ │
│ │003 │电动车│维修中│C区 │- │¥50/天│查看 编辑 删除│ │
│ └────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
│ │
│ [分页1 2 3 4 5 ... 共 50 条] │
└─────────────────────────────────────────────────────────────┘
```
#### 车辆详情页
**布局:**
```
┌─────────────────────────────────────────────────────────────┐
│ 车辆管理 > 车辆详情 > 001号车辆 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 车辆信息 │ │ 位置信息 │ │
│ │ 编号001 │ │ 当前位置A区 │ │
│ │ 类型:电动车 │ │ 最后更新:... │ │
│ │ 状态:空闲 │ │ │ │
│ │ 续航50km │ └─────────────────┘ │
│ │ 价格¥50/天 │ │
│ └─────────────────┘ ┌─────────────────┐ │
│ │ 维护记录 │ │
│ ┌─────────────────┐ │ 2026-03-01 ... │ │
│ │ 车辆图片 │ │ 2026-02-15 ... │ │
│ │ │ └─────────────────┘ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 2. 订单管理页面
#### 订单列表页
**布局:**
```
┌─────────────────────────────────────────────────────────────┐
│ 订单管理 > 订单列表 │
├─────────────────────────────────────────────────────────────┤
│ [搜索框] [状态筛选] [时间筛选] [创建订单] │
├─────────────────────────────────────────────────────────────┤
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │订单号│客户 │车辆 │租期 │费用 │状态 │操作 │ │ │
│ ├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ │
│ │202603│张三 │001 │7天 │¥350 │进行中│查看 结束│ │
│ │202602│李四 │002 │30天 │¥1500 │已完成│查看 │ │
│ │202601│王五 │003 │逾期 │¥500 │逾期 │查看 结束│ │
│ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
│ │
│ [分页1 2 3 4 5 ... 共 100 条] │
└─────────────────────────────────────────────────────────────┘
```
#### 订单详情页
**布局:**
```
┌─────────────────────────────────────────────────────────────┐
│ 订单管理 > 订单详情 > 202603001 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 订单信息 │ │ 客户信息 │ │
│ │ 订单号202603001│ │ 姓名:张三 │ │
│ │ 创建时间:... │ │ 电话138... │ │
│ │ 租期7天 │ │ 信用分85 │ │
│ │ 费用¥350 │ └─────────────────┘ │
│ │ 状态:进行中 │ │
│ └─────────────────┘ ┌─────────────────┐ │
│ │ 车辆信息 │ │
│ ┌─────────────────┐ │ 编号001 │ │
│ │ 订单备注 │ │ 类型:电动车 │ │
│ │ ... │ └─────────────────┘ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 3. 客户管理页面
#### 客户列表页
**布局:**
```
┌─────────────────────────────────────────────────────────────┐
│ 客户管理 > 客户列表 │
├─────────────────────────────────────────────────────────────┤
│ [搜索框] [添加客户] │
├─────────────────────────────────────────────────────────────┤
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │姓名 │电话 │信用分│租赁次数│状态 │操作 │ │ │ │
│ ├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ │
│ │张三 │138...│85 │5 │正常 │查看 编辑 删除│ │
│ │李四 │139...│72 │3 │正常 │查看 编辑 删除│ │
│ │王五 │137...│60 │1 │异常 │查看 编辑 删除│ │
│ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
│ │
│ [分页1 2 3 4 5 ... 共 50 条] │
└─────────────────────────────────────────────────────────────┘
```
### 4. 财务统计页面
#### 财务仪表盘
**布局:**
```
┌─────────────────────────────────────────────────────────────┐
│ 财务统计 > 仪表盘 │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 总收入 │ │ 本月收入 │ │ 逾期账款 │ │ 订单数量 │ │
│ │ ¥50,000 │ │ ¥8,500 │ │ ¥2,000 │ │ 150 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────┐ ┌─────────────────────┐ │
│ │ 收入趋势图 │ │ 订单状态分布 │ │
│ │ │ │ │ │
│ │ ▲ │ │ ┌─────┐ │ │
│ │ │ ╭───╮ │ │ │完成 │ 60% │ │
│ │ │ ╭─╯ ╰─╮ │ │ ├─────┤ │ │
│ │ │─╯ ╰──╮ │ │ │进行中│ 30% │ │
│ │ │ ╰──╮ │ │ ├─────┤ │ │
│ │ └────────────────╯ │ │ │逾期 │ 10% │ │
│ │ 1月 2月 3月 4月 │ │ └─────┘ │ │
│ └─────────────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## 组件设计
### 1. 搜索框组件
```vue
<template>
<div class="search-box">
<el-input
v-model="keyword"
placeholder="搜索..."
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button @click="handleSearch">搜索</el-button>
</div>
</template>
```
### 2. 表格组件
```vue
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="编号" width="80" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" @click="handleView(row)">查看</el-button>
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</template>
```
### 3. 表单组件
```vue
<template>
<el-form :model="form" label-width="100px">
<el-form-item label="车辆编号">
<el-input v-model="form.id" />
</el-form-item>
<el-form-item label="车辆类型">
<el-select v-model="form.type" placeholder="请选择">
<el-option label="电动车" value="electric" />
<el-option label="摩托车" value="motorcycle" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio label="空闲">空闲</el-radio>
<el-radio label="使用中">使用中</el-radio>
<el-radio label="维修中">维修中</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
<el-button @click="handleCancel">取消</el-button>
</el-form-item>
</el-form>
</template>
```
## 交互设计
### 1. 操作反馈
- **成功提示**:绿色 toast 提示
- **错误提示**:红色 toast 提示
- **确认弹窗**:删除操作前确认
### 2. 加载状态
- **表格加载**:骨架屏
- **页面加载**:全屏 loading
- **按钮加载**:按钮 loading 状态
### 3. 表单验证
- **必填项**:红色星号标记
- **格式验证**:实时验证
- **错误提示**:下方红色文字
## 响应式设计
### 桌面端(> 1200px
- 侧边栏固定宽度 200px
- 内容区域自适应
- 表格显示完整列
### 平板端768px - 1200px
- 侧边栏可折叠
- 表格部分列隐藏
- 卡片布局调整
### 移动端(< 768px
- 侧边栏隐藏,使用汉堡菜单
- 表格转为卡片列表
- 表单单列布局
## 开发建议
### 1. 组件复用
- 公共组件放在 `components/common/`
- 业务组件放在 `components/business/`
- 工具函数放在 `utils/`
### 2. 状态管理
- 简单状态使用 Vuex
- 复杂状态考虑 Pinia
- 本地存储使用 localStorage
### 3. 性能优化
- 图片懒加载
- 组件按需加载
- 数据缓存策略
### 4. 代码规范
- 使用 ESLint + Prettier
- 组件文件大小不超过 300 行
- 注释重要逻辑
## 下一步
1. 安装 MongoDB首要任务
2. 生成测试数据
3. 创建前端项目
4. 实现页面设计
5. 测试和优化

220
docs/项目完成清单.md Normal file
View File

@ -0,0 +1,220 @@
# 项目完成清单
## ✅ 已完成工作 (2026-03-05)
### 1. 项目初始化
- [x] 创建项目目录 `E:\code\e-scooter-rental-system`
- [x] 初始化 npm 项目
- [x] 安装依赖包 (express, mongoose, cors, dotenv, nodemon)
- [x] 配置环境变量 (.env)
### 2. 后端服务
- [x] 搭建 Express 服务器
- [x] 配置 MongoDB 连接
- [x] 添加 CORS 中间件
- [x] 添加 JSON 解析中间件
- [x] 实现错误处理中间件
- [x] 实现 404 处理
- [x] 健康检查接口 (`/health`)
### 3. 数据模型
- [x] 车辆模型 (Vehicle.js)
- 车辆信息、状态、位置、维护记录
- [x] 订单模型 (Order.js)
- 订单号、客户、车辆、租期、费用、状态
- [x] 客户模型 (Customer.js)
- 客户信息、信用评分、租赁历史
- [x] 模型导出文件 (index.js)
### 4. API 路由
- [x] 车辆管理路由 (`/api/vehicles`)
- 查询所有车辆
- 查询单个车辆
- 创建车辆
- 更新车辆
- 删除车辆
- 按状态筛选
- 更新车辆位置
- [x] 订单管理路由 (`/api/orders`)
- 查询所有订单
- 查询单个订单
- 创建订单
- 更新订单
- 结束订单
- 查询逾期订单
- 按状态筛选
- [x] 客户管理路由 (`/api/customers`)
- 查询所有客户
- 查询单个客户
- 创建客户
- 更新客户
- 删除客户
- 搜索客户
- 更新信用评分
- [x] 财务管理路由 (`/api/finance`)
- 财务统计
- 收入趋势
- 逾期账款
### 5. 工具脚本
- [x] 测试数据生成脚本 (seed.js)
- [x] .gitignore 配置
### 6. 文档编写 (17个文档)
- [x] API 文档 (docs/API.md)
- [x] 数据库设计文档 (docs/数据库设计.md)
- [x] 开发计划文档 (docs/开发计划.md)
- [x] 快速开始指南 (docs/快速开始.md)
- [x] 测试数据说明 (docs/测试数据.md)
- [x] MongoDB 安装指南 (docs/MongoDB安装指南.md)
- [x] 项目说明文档 (docs/README.md)
- [x] 开发进度文档 (docs/开发进度.md)
- [x] 下一步行动文档 (docs/下一步行动.md)
- [x] 项目总结文档 (docs/项目总结.md)
- [x] 文件清单文档 (docs/文件清单.md)
- [x] 常见问题文档 (docs/常见问题.md)
- [x] 项目里程碑文档 (docs/项目里程碑.md)
- [x] 项目状态文档 (docs/项目状态.md)
- [x] 项目完成清单 (docs/项目完成清单.md)
- [x] 前端开发指南 (docs/前端开发指南.md)
- [x] 页面设计文档 (docs/页面设计.md)
## ⏳ 进行中工作
### 1. MongoDB 安装
- [ ] 安装 MongoDB 数据库
- [ ] 启动 MongoDB 服务
- [ ] 验证 MongoDB 连接
### 2. 测试数据
- [ ] 运行种子脚本生成测试数据
- [ ] 验证数据是否正确插入
### 3. API 测试
- [ ] 测试健康检查接口
- [ ] 测试车辆管理接口
- [ ] 测试订单管理接口
- [ ] 测试客户管理接口
- [ ] 测试财务管理接口
## 📋 待办事项
### 立即执行
- [ ] 安装 MongoDB (首要任务)
- [ ] 生成测试数据
- [ ] 测试所有 API 接口
### 短期目标 (1-2天)
- [ ] 选择前端技术栈
- [ ] 创建前端项目
- [ ] 开发车辆管理页面
### 中期目标 (3-5天)
- [ ] 开发订单管理页面
- [ ] 开发客户管理页面
- [ ] 开发财务统计页面
### 长期目标 (1周内)
- [ ] 实现逾期提醒功能
- [ ] 完善用户权限管理
- [ ] 部署上线
## 📊 项目统计
### 文件统计
- 代码文件: 15 个
- 文档文件: 17 个
- 配置文件: 3 个
- 总计: 35 个文件 (不含 node_modules)
### API 接口统计
- 车辆管理: 7 个接口
- 订单管理: 7 个接口
- 客户管理: 7 个接口
- 财务管理: 3 个接口
- 总计: 24 个接口
### 文档统计
- 项目文档: 17 个
- 涵盖: 安装、开发、测试、部署、前端开发全流程
### 项目进度
- **整体进度: 40%**
| 模块 | 进度 | 状态 |
|------|------|------|
| 后端框架 | 100% | ✅ 完成 |
| 数据模型 | 100% | ✅ 完成 |
| API 路由 | 100% | ✅ 完成 |
| 文档编写 | 100% | ✅ 完成 |
| MongoDB 安装 | 0% | ❌ 未开始 |
| 测试数据 | 0% | ⏳ 等待 MongoDB |
| 前端开发 | 0% | ⏳ 等待后端 |
| 功能完善 | 0% | ⏳ 等待前端 |
| 测试部署 | 0% | ⏳ 等待功能完成 |
## 🎯 当前状态
### ✅ 已完成
- 后端服务框架
- 数据模型设计
- API 路由实现
- 错误处理中间件
- 文档编写 (17个文档)
- 前端开发指南
- 页面设计文档
### ❌ 问题
- MongoDB 未安装
- 数据库连接失败
### ⏳ 等待
- 安装 MongoDB
- 生成测试数据
- 测试 API 接口
## 📝 下一步操作
1. **安装 MongoDB** (首要任务)
- 推荐使用 Docker 安装
- 详细步骤见 `docs/MongoDB安装指南.md`
2. **生成测试数据**
```bash
npm run seed
```
3. **测试 API 接口**
```bash
curl http://localhost:3000/health
curl http://localhost:3000/api/vehicles
```
4. **开发前端页面**
- 选择 Vue.js 或 React
- 创建前端项目
- 开发车辆管理页面
## 🎉 项目亮点
1. **完整的后端框架**
- Express + MongoDB
- 错误处理完善
- API 接口规范
2. **详细的文档**
- 17 个文档文件
- 涵盖安装、开发、测试、前端开发全流程
3. **测试数据支持**
- 种子脚本一键生成测试数据
- 便于前端开发和测试
4. **模块化设计**
- 数据模型、路由、中间件分离
- 易于维护和扩展
5. **前端开发指南**
- 详细的前端开发规范
- 页面设计文档
- 组件设计指南

206
docs/项目总结.md Normal file
View File

@ -0,0 +1,206 @@
# 两轮电动车租赁管理系统 - 项目总结
## 📋 项目概述
**项目名称**: 两轮电动车租赁管理系统
**项目位置**: `E:\code\e-scooter-rental-system`
**技术栈**: Node.js + Express + MongoDB
**开发时间**: 2026-03-05
## ✅ 已完成工作
### 1. 项目初始化
- ✅ 创建项目目录
- ✅ 初始化 npm 项目
- ✅ 安装依赖包
- ✅ 配置环境变量
### 2. 后端服务
- ✅ Express 服务器搭建
- ✅ MongoDB 连接配置
- ✅ 错误处理中间件
- ✅ 404 处理
- ✅ 健康检查接口
### 3. 数据模型
- ✅ 车辆模型 (Vehicle)
- ✅ 订单模型 (Order)
- ✅ 客户模型 (Customer)
### 4. API 路由
- ✅ 车辆管理路由
- ✅ 订单管理路由
- ✅ 客户管理路由
- ✅ 财务管理路由
### 5. 文档编写
- ✅ API 文档
- ✅ 数据库设计文档
- ✅ 开发计划文档
- ✅ 快速开始指南
- ✅ 测试数据说明
- ✅ MongoDB 安装指南
- ✅ 项目说明文档
- ✅ 开发进度文档
- ✅ 下一步行动文档
### 6. 工具脚本
- ✅ 测试数据生成脚本
## 📁 项目结构
```
e-scooter-rental-system/
├── server/
│ ├── models/ # 数据模型 (3个)
│ │ ├── Vehicle.js
│ │ ├── Order.js
│ │ ├── Customer.js
│ │ └── index.js
│ ├── routes/ # 路由 (4个)
│ │ ├── vehicles.js
│ │ ├── orders.js
│ │ ├── customers.js
│ │ ├── finance.js
│ │ └── index.js
│ ├── middleware/ # 中间件
│ │ └── errorHandler.js
│ ├── seed.js # 测试数据脚本
│ └── index.js # 入口文件
├── docs/ # 文档 (9个)
│ ├── API.md
│ ├── 数据库设计.md
│ ├── 开发计划.md
│ ├── 快速开始.md
│ ├── 测试数据.md
│ ├── MongoDB安装指南.md
│ ├── README.md
│ ├── 开发进度.md
│ └── 下一步行动.md
├── .env # 环境配置
├── .gitignore
└── package.json
```
## 🎯 功能模块
### 1. 车辆管理
- 车辆信息录入
- 车辆状态追踪
- GPS 位置更新
- 车辆筛选查询
### 2. 订单管理
- 订单创建
- 订单状态管理
- 逾期订单处理
- 订单查询筛选
### 3. 客户管理
- 客户信息登记
- 客户搜索
- 信用评分管理
- 租赁历史查询
### 4. 财务管理
- 收入统计
- 逾期账款管理
- 财务报表生成
- 收入趋势分析
## 📊 API 接口统计
| 模块 | 接口数量 | 功能 |
|------|---------|------|
| 车辆管理 | 7 个 | 查询、创建、更新、删除、位置更新、状态筛选 |
| 订单管理 | 6 个 | 查询、创建、结束订单、逾期订单、状态筛选 |
| 客户管理 | 6 个 | 查询、创建、更新、删除、搜索、信用评分 |
| 财务管理 | 3 个 | 统计、趋势、逾期账款 |
**总计**: 22 个 API 接口
## 🚀 当前状态
### ✅ 已完成
- 后端服务框架
- 数据模型设计
- API 路由实现
- 文档编写
- 错误处理
### ⏳ 进行中
- **安装 MongoDB 数据库** (首要任务)
### 📋 待办事项
1. 安装 MongoDB
2. 生成测试数据
3. 测试 API 接口
4. 开发前端页面
## 📝 技术要点
### 数据库连接
- 数据库名: e-scooter-rental
- 默认端口: 27017
- 连接字符串: `mongodb://localhost:27017/e-scooter-rental`
### API 基础 URL
- 本地: http://localhost:3000/api
- 健康检查: http://localhost:3000/health
### API 返回格式
```json
{
"success": true,
"data": {...},
"message": "成功"
}
```
## 🎯 下一步行动
### 1. 安装 MongoDB (首要任务)
详细步骤请查看: `docs/MongoDB安装指南.md`
推荐使用 Docker 安装:
```bash
docker pull mongo
docker run -d -p 27017:27017 --name mongodb mongo
```
### 2. 生成测试数据
```bash
npm run seed
```
### 3. 测试 API 接口
- 健康检查: `curl http://localhost:3000/health`
- 查询车辆: `curl http://localhost:3000/api/vehicles`
- 查询客户: `curl http://localhost:3000/api/customers`
- 查询订单: `curl http://localhost:3000/api/orders`
### 4. 开发前端页面
推荐使用 Vue.js + Element Plus
## 📞 需要帮助?
如果在安装 MongoDB 或开发过程中遇到问题,请随时联系!
## 🎉 项目亮点
1. **完整的后端框架**
- Express + MongoDB
- 错误处理完善
- API 接口规范
2. **详细的文档**
- 9 个文档文件
- 涵盖安装、开发、测试全流程
3. **测试数据支持**
- 种子脚本一键生成测试数据
- 便于前端开发和测试
4. **模块化设计**
- 数据模型、路由、中间件分离
- 易于维护和扩展

135
docs/项目状态.md Normal file
View File

@ -0,0 +1,135 @@
# 项目状态
## 📊 当前状态 (2026-03-05)
### ✅ 已完成
- 后端服务框架搭建
- 数据模型设计 (车辆、订单、客户)
- API 路由实现 (4个模块)
- 错误处理中间件
- 文档编写 (11个文档)
- 测试数据脚本
### ❌ 问题
- MongoDB 未安装
- 数据库连接失败
### ⏳ 进行中
- 安装 MongoDB 数据库
## 🎯 下一步行动
### 立即执行
1. **安装 MongoDB** (首要任务)
- 推荐使用 Docker 安装
- 详细步骤见 `docs/MongoDB安装指南.md`
2. **生成测试数据**
```bash
npm run seed
```
3. **测试 API 接口**
- 健康检查: `curl http://localhost:3000/health`
- 查询车辆: `curl http://localhost:3000/api/vehicles`
### 短期目标 (1-2天)
1. 选择前端技术栈 (Vue.js 或 React)
2. 创建前端项目
3. 开发车辆管理页面
### 中期目标 (3-5天)
1. 开发订单管理页面
2. 开发客户管理页面
3. 开发财务统计页面
### 长期目标 (1周内)
1. 实现逾期提醒功能
2. 完善用户权限管理
3. 部署上线
## 📈 项目进度
### 整体进度: 40%
| 模块 | 进度 | 状态 |
|------|------|------|
| 后端框架 | 100% | ✅ 完成 |
| 数据模型 | 100% | ✅ 完成 |
| API 路由 | 100% | ✅ 完成 |
| 文档编写 | 100% | ✅ 完成 |
| MongoDB 安装 | 0% | ❌ 未开始 |
| 测试数据 | 0% | ⏳ 等待 MongoDB |
| 前端开发 | 0% | ⏳ 等待后端 |
| 功能完善 | 0% | ⏳ 等待前端 |
| 测试部署 | 0% | ⏳ 等待功能完成 |
## 📋 任务清单
### 已完成任务
- [x] 创建项目目录
- [x] 初始化 npm 项目
- [x] 安装依赖包
- [x] 搭建 Express 服务器
- [x] 配置 MongoDB 连接
- [x] 设计数据模型
- [x] 实现 API 路由
- [x] 编写文档
- [x] 添加错误处理
### 进行中任务
- [ ] 安装 MongoDB 数据库
### 待办任务
- [ ] 生成测试数据
- [ ] 测试 API 接口
- [ ] 选择前端技术栈
- [ ] 创建前端项目
- [ ] 开发车辆管理页面
- [ ] 开发订单管理页面
- [ ] 开发客户管理页面
- [ ] 开发财务统计页面
- [ ] 实现逾期提醒功能
- [ ] 完善用户权限管理
- [ ] 功能测试
- [ ] 部署上线
## 🎯 成功标准
### 后端服务
- ✅ 服务正常启动
- ✅ API 接口可用
- ✅ 数据库连接正常
- ✅ 错误处理完善
### 前端页面
- [ ] 页面加载正常
- [ ] 数据展示正确
- [ ] 操作响应及时
- [ ] 用户体验良好
### 功能完整性
- [ ] 车辆管理功能完整
- [ ] 订单管理功能完整
- [ ] 客户管理功能完整
- [ ] 财务管理功能完整
## 📞 沟通计划
### 每日更新
- 项目进度更新
- 遇到的问题
- 下一步计划
### 里程碑更新
- 完成里程碑时更新
- 调整时间线时通知
- 遇到重大问题时沟通
## 🎉 项目成功标准
1. **功能完整**: 所有计划功能都已实现
2. **代码质量**: 代码规范、注释完整
3. **文档齐全**: 技术文档、用户文档完整
4. **测试通过**: 功能测试、集成测试通过
5. **部署成功**: 生产环境正常运行

145
docs/项目里程碑.md Normal file
View File

@ -0,0 +1,145 @@
# 项目里程碑
## 🎯 项目目标
开发一个完整的两轮电动车租赁管理系统,包含车辆管理、订单管理、客户管理、财务管理等功能。
## 📅 时间线
### 第一阶段:后端开发 (2026-03-05)
#### ✅ 已完成
- [x] 项目初始化
- [x] 后端服务搭建
- [x] 数据模型设计
- [x] API 路由实现
- [x] 文档编写
- [x] 错误处理中间件
#### ⏳ 进行中
- [ ] 安装 MongoDB 数据库
- [ ] 生成测试数据
- [ ] 测试 API 接口
### 第二阶段:前端开发 (预计 2026-03-06 ~ 2026-03-08)
#### 计划完成
- [ ] 选择前端技术栈
- [ ] 创建前端项目
- [ ] 开发车辆管理页面
- [ ] 开发订单管理页面
- [ ] 开发客户管理页面
### 第三阶段:功能完善 (预计 2026-03-09 ~ 2026-03-10)
#### 计划完成
- [ ] 开发财务统计页面
- [ ] 实现逾期提醒功能
- [ ] 完善用户权限管理
- [ ] 集成测试
### 第四阶段:测试部署 (预计 2026-03-11)
#### 计划完成
- [ ] 功能测试
- [ ] Bug 修复
- [ ] 部署上线
## 📊 项目进度
### 当前进度: 40%
- ✅ 后端框架: 100%
- ✅ 数据模型: 100%
- ✅ API 路由: 100%
- ✅ 文档编写: 100%
- ⏳ MongoDB 安装: 0%
- ⏳ 测试数据: 0%
- ⏳ 前端开发: 0%
- ⏳ 功能完善: 0%
- ⏳ 测试部署: 0%
## 🎯 里程碑节点
### 里程碑 1: 后端服务就绪
- ✅ 后端服务启动
- ✅ 数据库连接配置
- ✅ API 接口完成
- ⏳ MongoDB 安装
### 里程碑 2: 前端页面开发
- [ ] 车辆管理页面
- [ ] 订单管理页面
- [ ] 客户管理页面
### 里程碑 3: 功能完善
- [ ] 财务统计页面
- [ ] 逾期提醒功能
- [ ] 用户权限管理
### 里程碑 4: 测试部署
- [ ] 功能测试完成
- [ ] Bug 修复完成
- [ ] 部署上线
## 📝 开发任务清单
### 立即执行 (今天)
- [ ] 安装 MongoDB 数据库
- [ ] 生成测试数据
- [ ] 测试 API 接口
### 短期任务 (1-2天)
- [ ] 选择前端技术栈
- [ ] 创建前端项目
- [ ] 开发车辆管理页面
### 中期任务 (3-5天)
- [ ] 开发订单管理页面
- [ ] 开发客户管理页面
- [ ] 开发财务统计页面
### 长期任务 (1周内)
- [ ] 实现逾期提醒功能
- [ ] 完善用户权限管理
- [ ] 部署上线
## 🎯 成功标准
### 后端服务
- ✅ 服务正常启动
- ✅ API 接口可用
- ✅ 数据库连接正常
- ✅ 错误处理完善
### 前端页面
- [ ] 页面加载正常
- [ ] 数据展示正确
- [ ] 操作响应及时
- [ ] 用户体验良好
### 功能完整性
- [ ] 车辆管理功能完整
- [ ] 订单管理功能完整
- [ ] 客户管理功能完整
- [ ] 财务管理功能完整
## 📞 沟通计划
### 每日更新
- 项目进度更新
- 遇到的问题
- 下一步计划
### 里程碑更新
- 完成里程碑时更新
- 调整时间线时通知
- 遇到重大问题时沟通
## 🎉 项目成功标准
1. **功能完整**: 所有计划功能都已实现
2. **代码质量**: 代码规范、注释完整
3. **文档齐全**: 技术文档、用户文档完整
4. **测试通过**: 功能测试、集成测试通过
5. **部署成功**: 生产环境正常运行

1572
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "e-scooter-rental-system",
"version": "1.0.0",
"description": "两轮电动车租赁管理系统",
"main": "server/index.js",
"scripts": {
"start": "node server/index.js",
"dev": "nodemon server/index.js",
"seed": "node server/seed.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"e-scooter",
"rental",
"management",
"system"
],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.13.6",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"mongoose": "^8.10.0"
},
"devDependencies": {
"nodemon": "^3.1.14"
}
}

55
reset-finance.js Normal file
View File

@ -0,0 +1,55 @@
const http = require('http');
// 先删除所有旧数据
const req1 = http.request({
hostname: 'localhost',
port: 3000,
path: '/api/finance/delete-all',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, res => {
let b = '';
res.on('data', c => b += c);
res.on('end', () => {
console.log('Deleted all:', b);
// 添加新的收支数据
const payments = [
{ type: 'income', party: '张三', amount: 1500, method: '微信', category: '租金收入', remark: '租车费' },
{ type: 'income', party: '李四', amount: 2000, method: '支付宝', category: '租金收入', remark: '租车费' },
{ type: 'income', party: '王五', amount: 500, method: '现金', category: '押金退还', remark: '还车退押金' },
{ type: 'expense', party: '赵六', amount: 300, method: '微信', category: '退款', remark: '提前还车退款' },
{ type: 'expense', party: '房东', amount: 1500, method: '银行卡', category: '房租', remark: '门店房租' },
{ type: 'expense', party: '电工', amount: 200, method: '现金', category: '水电', remark: '门店电费' }
];
let i = 0;
const addPayment = () => {
if (i >= payments.length) {
console.log('Done!');
return;
}
const data = JSON.stringify(payments[i]);
const req = http.request({
hostname: 'localhost',
port: 3000,
path: '/api/finance',
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': data.length }
}, res => {
let b = '';
res.on('data', c => b += c);
res.on('end', () => {
console.log('Added:', payments[i].type, payments[i].party, payments[i].amount);
i++;
addPayment();
});
});
req.on('error', e => console.error(e));
req.write(data);
req.end();
};
addPayment();
});
});
req1.on('error', e => console.error(e));
req1.end();

64
seed-all.js Normal file
View File

@ -0,0 +1,64 @@
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/e-scooter-rental')
.then(async () => {
console.log('✅ MongoDB 连接成功');
const Store = require('./server/models/Store');
const Payment = require('./server/models/Payment');
const Complaint = require('./server/models/Complaint');
const Customer = require('./server/models/Customer');
const Order = require('./server/models/Order');
// 清空数据
await Store.deleteMany({});
await Payment.deleteMany({});
await Complaint.deleteMany({});
console.log('🧹 已清空门店、财务、投诉数据');
// 门店数据
await Store.insertMany([
{ storeId: 'STORE001', name: '朝阳区总店', address: '北京市朝阳区建国路88号', phone: '010-12345678', manager: '王店长', status: '营业中', approvalStatus: '已通过' },
{ storeId: 'STORE002', name: '海淀区一分店', address: '北京市海淀区中关村大街1号', phone: '010-23456789', manager: '李店长', status: '营业中', approvalStatus: '已通过' },
{ storeId: 'STORE003', name: '西城区二分店', address: '北京市西城区西单北大街120号', phone: '010-34567890', manager: '张店长', status: '营业中', approvalStatus: '已通过' },
{ storeId: 'STORE004', name: '东城区三分店', address: '北京市东城区王府井大街138号', phone: '010-45678901', manager: '赵店长', status: '装修中', approvalStatus: '已通过' },
{ storeId: 'STORE005', name: '丰台区四分店', address: '北京市丰台区南三环西路16号', phone: '010-56789012', manager: '钱店长', status: '营业中', approvalStatus: '待审批' }
]);
console.log('✅ 门店: 5 个');
// 支付数据 (财务)
await Payment.insertMany([
{ paymentId: 'PAY001', type: '收入', party: '张三', amount: 300, method: '微信', category: '租金收入', remark: 'ORDER001租金', operator: '系统' },
{ paymentId: 'PAY002', type: '收入', party: '李四', amount: 300, method: '支付宝', category: '租金收入', remark: 'ORDER002租金', operator: '系统' },
{ paymentId: 'PAY003', type: '收入', party: '王五', amount: 200, method: '微信', category: '租金收入', remark: 'ORDER003租金', operator: '系统' },
{ paymentId: 'PAY004', type: '支出', party: '张三', amount: 200, method: '微信', category: '押金退还', remark: 'ORDER003押金退还', operator: '系统' },
{ paymentId: 'PAY005', type: '支出', party: '员工A', amount: 5000, method: '银行卡', category: '工资', remark: '2月工资', operator: '财务' }
]);
console.log('✅ 财务记录: 5 条');
// 投诉数据
const customers = await Customer.find();
const orders = await Order.find();
if (customers.length > 0 && orders.length > 0) {
await Complaint.insertMany([
{ complaintId: 'COMP001', customer: customers[0]._id, order: orders[0]._id, type: '车辆问题', content: '电动车刹车不灵', status: '处理中', handler: '王店长' },
{ complaintId: 'COMP002', customer: customers[1]._id, order: orders[1]._id, type: '服务态度', content: '店员服务态度差', status: '已解决', response: '已道歉并整改', handler: '李店长' },
{ complaintId: 'COMP003', customer: customers[2]?._id || customers[0]._id, order: orders[2]?._id || orders[0]._id, type: '费用问题', content: '费用计算有误', status: '待处理' }
]);
console.log('✅ 投诉: 3 条');
}
console.log('\n🎉 测试数据创建完成!');
console.log('📊 数据汇总:');
console.log(' - 车辆: 5 辆');
console.log(' - 客户: 5 个');
console.log(' - 订单: 3 个');
console.log(' - 门店: 5 个');
console.log(' - 财务: 5 条');
console.log(' - 投诉: 3 条');
mongoose.disconnect();
process.exit(0);
})
.catch(err => console.error('❌ 错误:', err));

122
seed-dispute.js Normal file
View File

@ -0,0 +1,122 @@
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/e-scooter-rental')
.then(async () => {
console.log('✅ MongoDB 连接成功');
const Dispute = require('./server/models/Dispute');
const Conflict = require('./server/models/Conflict');
const Store = require('./server/models/Store');
const Payment = require('./server/models/Payment');
// 清空纠纷和冲突数据
await Dispute.deleteMany({});
await Conflict.deleteMany({});
console.log('🧹 已清空纠纷协调数据');
const stores = await Store.find();
// 纠纷数据 (门店之间)
if (stores.length >= 2) {
await Dispute.insertMany([
{
disputeId: 'DISP001',
storeA: stores[0]._id, storeAName: stores[0].name,
storeB: stores[1]._id, storeBName: stores[1].name,
type: '订单纠纷',
title: '跨区订单归属争议',
content: '客户在A门店租车在B门店还车订单归属产生争议',
status: '处理中',
handler: '总部协调员'
},
{
disputeId: 'DISP002',
storeA: stores[2]._id, storeAName: stores[2].name,
storeB: stores[3]._id, storeBName: stores[3].name,
type: '费用纠纷',
title: '车辆维修费用分摊',
content: '跨区车辆维修费用分摊比例未达成一致',
status: '待处理'
},
{
disputeId: 'DISP003',
storeA: stores[0]._id, storeAName: stores[0].name,
storeB: stores[4]._id, storeBName: stores[4].name,
type: '区域纠纷',
title: '经营范围重叠',
content: '两家门店服务区域划分不明确',
status: '已解决',
result: '已重新划分服务区域',
handler: '区域经理'
}
]);
console.log('✅ 门店纠纷: 3 条');
}
// 冲突数据 (骑手门店/门店公司)
await Conflict.insertMany([
{
conflictId: 'CONF001',
type: '骑手门店',
title: '骑手租金拖欠',
content: '骑手张某拖欠门店租车租金共计500元',
partyA: '骑手张某',
partyB: '朝阳区总店',
status: '处理中',
handler: '王店长'
},
{
conflictId: 'CONF002',
type: '骑手门店',
title: '车辆损坏赔偿',
content: '骑手归还车辆时发现车身刮擦,双方对赔偿金额有争议',
partyA: '骑手李某',
partyB: '海淀区一分店',
status: '已解决',
result: '骑手承担200元维修费用',
handler: '李店长'
},
{
conflictId: 'CONF003',
type: '门店公司',
title: '总部扣款异议',
content: '门店对公司月度扣款有异议,认为计算有误',
partyA: '西城区二分店',
partyB: '公司总部',
status: '待处理'
},
{
conflictId: 'CONF004',
type: '骑手门店',
title: '头盔丢失',
content: '骑手归还车辆时遗失头盔一个',
partyA: '骑手赵某',
partyB: '东城区三分店',
status: '处理中',
handler: '赵店长'
}
]);
console.log('✅ 骑手/门店冲突: 4 条');
// 打款/财务数据 (使用正确的枚举值)
await Payment.insertMany([
{ paymentId: 'PAY006', type: '支出', party: '门店A', amount: 12000, method: '银行卡', category: '房租', remark: '3月房租', operator: '财务' },
{ paymentId: 'PAY007', type: '支出', party: '物业', amount: 1500, method: '银行卡', category: '水电', remark: '2月水电费', operator: '行政' },
{ paymentId: 'PAY008', type: '支出', party: '维修店', amount: 800, method: '微信', category: '维修', remark: '车辆维修', operator: '王店长' },
{ paymentId: 'PAY009', type: '收入', party: '骑手张某', amount: 500, method: '微信', category: '租金收入', remark: '骑手租金', operator: '系统' },
{ paymentId: 'PAY010', type: '支出', party: '员工B', amount: 4500, method: '银行卡', category: '工资', remark: '2月工资', operator: '财务' },
{ paymentId: 'PAY011', type: '收入', party: '骑手李某', amount: 300, method: '支付宝', category: '租金收入', remark: '骑手租金', operator: '系统' },
{ paymentId: 'PAY012', type: '支出', party: '供应商', amount: 3500, method: '银行卡', category: '其他', remark: '采购电池', operator: '采购' }
]);
console.log('✅ 打款/财务记录: 7 条');
console.log('\n🎉 纠纷协调和打款数据创建完成!');
console.log('📊 数据汇总:');
console.log(' - 门店纠纷: 3 条');
console.log(' - 骑手/门店冲突: 4 条');
console.log(' - 打款记录: 7 条');
mongoose.disconnect();
process.exit(0);
})
.catch(err => console.error('❌ 错误:', err));

30
seed-payment.js Normal file
View File

@ -0,0 +1,30 @@
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/e-scooter-rental')
.then(async () => {
console.log('✅ MongoDB 连接成功');
const Payment = require('./server/models/Payment');
// 添加更多打款数据
await Payment.insertMany([
{ paymentId: 'PAY013', type: '收入', party: '骑手王某', amount: 800, method: '微信', category: '租金收入', remark: '3月租金', operator: '系统' },
{ paymentId: 'PAY014', type: '支出', party: '员工C', amount: 5200, method: '银行卡', category: '工资', remark: '3月工资', operator: '财务' },
{ paymentId: 'PAY015', type: '支出', party: '房东', amount: 15000, method: '银行卡', category: '房租', remark: '4月房租', operator: '财务' },
{ paymentId: 'PAY016', type: '支出', party: '电力公司', amount: 2800, method: '银行卡', category: '水电', remark: '3月电费', operator: '行政' },
{ paymentId: 'PAY017', type: '支出', party: '自来水公司', amount: 600, method: '银行卡', category: '水电', remark: '3月水费', operator: '行政' },
{ paymentId: 'PAY018', type: '支出', party: '修理厂', amount: 1200, method: '微信', category: '维修', remark: '车辆大修', operator: '王店长' },
{ paymentId: 'PAY019', type: '收入', party: '骑手孙某', amount: 450, method: '支付宝', category: '租金收入', remark: '租金', operator: '系统' },
{ paymentId: 'PAY020', type: '支出', party: '保险公司', amount: 3500, method: '银行卡', category: '其他', remark: '车辆保险', operator: '财务' },
{ paymentId: 'PAY021', type: '收入', party: '骑手周某', amount: 600, method: '微信', category: '租金收入', remark: '租金', operator: '系统' },
{ paymentId: 'PAY022', type: '支出', party: '广告公司', amount: 2000, method: '银行卡', category: '其他', remark: '营销推广', operator: '市场' }
]);
console.log('✅ 添加了 10 条打款记录');
const total = await Payment.countDocuments();
console.log('📊 打款记录总数:', total);
mongoose.disconnect();
process.exit(0);
})
.catch(err => console.error('❌ 错误:', err));

47
seed-payments.js Normal file
View File

@ -0,0 +1,47 @@
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/e-scooter-rental')
.then(async () => {
console.log('Connected to MongoDB');
// 定义新的 Payment 模型
const paymentSchema = new mongoose.Schema({
paymentId: String,
type: String,
party: String,
amount: Number,
method: String,
category: String,
remark: String,
createdAt: Date
});
const Payment = mongoose.model('Payment', paymentSchema, 'payments');
// 删除旧数据
await Payment.deleteMany({});
console.log('Deleted old data');
// 添加新数据
const payments = [
{ paymentId: 'PAY001', type: 'income', party: '张三', amount: 1500, method: '微信', category: '租金收入', remark: '租车费' },
{ paymentId: 'PAY002', type: 'income', party: '李四', amount: 2000, method: '支付宝', category: '租金收入', remark: '租车费' },
{ paymentId: 'PAY003', type: 'income', party: '王五', amount: 500, method: '现金', category: '押金退还', remark: '还车退押金' },
{ paymentId: 'PAY004', type: 'expense', party: '赵六', amount: 300, method: '微信', category: '退款', remark: '提前还车退款' },
{ paymentId: 'PAY005', type: 'expense', party: '房东', amount: 1500, method: '银行卡', category: '房租', remark: '门店房租' },
{ paymentId: 'PAY006', type: 'expense', party: '电工', amount: 200, method: '现金', category: '水电', remark: '门店电费' }
];
await Payment.insertMany(payments);
console.log('Added new payments');
// 验证
const count = await Payment.countDocuments();
console.log('Total payments:', count);
process.exit(0);
})
.catch(e => {
console.error(e);
process.exit(1);
});

257
seed-test-data.js Normal file
View File

@ -0,0 +1,257 @@
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/e-scooter-rental')
.then(async () => {
console.log('✅ MongoDB 连接成功');
const Store = require('./server/models/Store');
const Vehicle = require('./server/models/Vehicle');
const Customer = require('./server/models/Customer');
const Order = require('./server/models/Order');
const Payment = require('./server/models/Payment');
// 清空数据
await Vehicle.deleteMany({});
await Customer.deleteMany({});
await Order.deleteMany({});
console.log('🧹 已清空车辆、客户、订单数据');
// ===== 1. 门店数据 (10个) =====
const stores = await Store.insertMany([
{ storeId: 'STORE001', name: '朝阳区总店', address: '北京市朝阳区建国路88号', phone: '010-12345678', manager: '王店长', status: '营业中', approvalStatus: '已通过' },
{ storeId: 'STORE002', name: '海淀区一分店', address: '北京市海淀区中关村大街1号', phone: '010-23456789', manager: '李店长', status: '营业中', approvalStatus: '已通过' },
{ storeId: 'STORE003', name: '西城区二分店', address: '北京市西城区西单北大街120号', phone: '010-34567890', manager: '张店长', status: '营业中', approvalStatus: '已通过' },
{ storeId: 'STORE004', name: '东城区三分店', address: '北京市东城区王府井大街138号', phone: '010-45678901', manager: '赵店长', status: '装修中', approvalStatus: '已通过' },
{ storeId: 'STORE005', name: '丰台区四分店', address: '北京市丰台区南三环西路16号', phone: '010-56789012', manager: '钱店长', status: '营业中', approvalStatus: '已通过' },
{ storeId: 'STORE006', name: '通州区分店', address: '北京市通州区运河西大街128号', phone: '010-67890123', manager: '孙店长', status: '营业中', approvalStatus: '已通过' },
{ storeId: 'STORE007', name: '昌平区分店', address: '北京市昌平区西关环岛附近', phone: '010-78901234', manager: '周店长', status: '营业中', approvalStatus: '已通过' },
{ storeId: 'STORE008', name: '大兴区分店', address: '北京市大兴区黄村镇兴政街', phone: '010-89012345', manager: '吴店长', status: '已停业', approvalStatus: '已通过' },
{ storeId: 'STORE009', name: '顺义区分店', address: '北京市顺义区府前中街', phone: '010-90123456', manager: '郑店长', status: '营业中', approvalStatus: '已通过' },
{ storeId: 'STORE010', name: '房山区分店', address: '北京市房山区良乡镇拱辰大街', phone: '010-01234567', manager: '冯店长', status: '营业中', approvalStatus: '待审批' }
]);
console.log('✅ 门店: 10 个');
// ===== 2. 客户/骑手数据 (15个) =====
const customers = await Customer.insertMany([
{ customerId: 'CUST001', name: '张三', phone: '13800138001', idCard: '110101199001011234', address: '北京市朝阳区', email: 'zhangsan@email.com', creditScore: 95, creditLevel: '优秀', accountStatus: '正常' },
{ customerId: 'CUST002', name: '李四', phone: '13800138002', idCard: '110102199002022345', address: '北京市海淀区', email: 'lisi@email.com', creditScore: 88, creditLevel: '优秀', accountStatus: '正常' },
{ customerId: 'CUST003', name: '王五', phone: '13800138003', idCard: '110103199003033456', address: '北京市西城区', email: 'wangwu@email.com', creditScore: 75, creditLevel: '良好', accountStatus: '正常' },
{ customerId: 'CUST004', name: '赵六', phone: '13800138004', idCard: '110104199004044567', address: '北京市东城区', email: 'zhaoliu@email.com', creditScore: 92, creditLevel: '优秀', accountStatus: '正常' },
{ customerId: 'CUST005', name: '钱七', phone: '13800138005', idCard: '110105199005055678', address: '北京市丰台区', email: 'qianqi@email.com', creditScore: 65, creditLevel: '一般', accountStatus: '正常' },
{ customerId: 'CUST006', name: '孙八', phone: '13800138006', idCard: '110106199006066789', address: '北京市通州区', email: 'sunba@email.com', creditScore: 80, creditLevel: '良好', accountStatus: '正常' },
{ customerId: 'CUST007', name: '周九', phone: '13800138007', idCard: '110107199007077890', address: '北京市昌平区', email: 'zhoujiu@email.com', creditScore: 85, creditLevel: '优秀', accountStatus: '正常' },
{ customerId: 'CUST008', name: '吴十', phone: '13800138008', idCard: '110108199008088901', address: '北京市大兴区', email: 'wushi@email.com', creditScore: 70, creditLevel: '良好', accountStatus: '正常' },
{ customerId: 'CUST009', name: '郑十一', phone: '13800138009', idCard: '110109199009099012', address: '北京市顺义区', email: 'zhengshiyi@email.com', creditScore: 90, creditLevel: '优秀', accountStatus: '正常' },
{ customerId: 'CUST010', name: '冯十二', phone: '13800138010', idCard: '110110199010101123', address: '北京市房山区', email: 'fengershi@email.com', creditScore: 55, creditLevel: '较差', accountStatus: '正常' },
{ customerId: 'CUST011', name: '陈十三', phone: '13800138011', idCard: '110111199011112234', address: '北京市朝阳区', email: 'chenshisan@email.com', creditScore: 82, creditLevel: '良好', accountStatus: '正常' },
{ customerId: 'CUST012', name: '刘十四', phone: '13800138012', idCard: '110112199012123345', address: '北京市海淀区', email: 'liushisi@email.com', creditScore: 78, creditLevel: '良好', accountStatus: '正常' },
{ customerId: 'CUST013', name: '杨十五', phone: '13800138013', idCard: '110113199101011456', address: '北京市西城区', email: 'yangshiwu@email.com', creditScore: 95, creditLevel: '优秀', accountStatus: '正常' },
{ customerId: 'CUST014', name: '黄十六', phone: '13800138014', idCard: '110114199102022567', address: '北京市东城区', email: 'huangshiliu@email.com', creditScore: 60, creditLevel: '一般', accountStatus: '冻结' },
{ customerId: 'CUST015', name: '林十七', phone: '13800138015', idCard: '110115199103033678', address: '北京市丰台区', email: 'linshiqi@email.com', creditScore: 88, creditLevel: '优秀', accountStatus: '正常' }
]);
console.log('✅ 客户/骑手: 15 个');
// ===== 3. 车辆数据 (25辆) =====
const vehicleData = [];
const models = ['雅迪DT3', '爱玛A500', '台铃TL1', '新日XC1', '绿源L6', '小牛MQi2', '九号E25', '雅马哈E01'];
const colors = ['黑色', '白色', '红色', '蓝色', '绿色', '灰色', '金色'];
const storeIds = stores.map(s => s._id);
for (let i = 1; i <= 25; i++) {
const storeIndex = (i - 1) % storeIds.length;
const statusOptions = ['空闲', '空闲', '空闲', '在租', '在租', '维修中', '空闲'];
const status = statusOptions[Math.floor(Math.random() * statusOptions.length)];
vehicleData.push({
vehicleId: `VEH${String(i).padStart(4, '0')}`,
model: models[Math.floor(Math.random() * models.length)],
color: colors[Math.floor(Math.random() * colors.length)],
batteryType: '锂电池',
batteryCapacity: [48, 60, 72][Math.floor(Math.random() * 3)],
batteryStatus: ['正常', '正常', '正常', '老化'][Math.floor(Math.random() * 4)],
status: status,
location: {
type: 'Point',
coordinates: [116.2 + Math.random() * 0.5, 39.8 + Math.random() * 0.5]
},
totalRentDays: Math.floor(Math.random() * 500),
purchaseDate: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
purchasePrice: [2999, 3999, 4999, 5999][Math.floor(Math.random() * 4)],
purchaseSupplier: ['雅迪集团', '爱玛集团', '台铃集团', '新日集团'][Math.floor(Math.random() * 4)],
notes: i <= 2 ? '新车,暂无记录' : ''
});
}
const vehicles = await Vehicle.insertMany(vehicleData);
console.log('✅ 车辆: 25 辆');
// ===== 4. 订单数据 (20个) - 不同状态 =====
const orderData = [];
const now = new Date();
const oneDay = 24 * 60 * 60 * 1000;
// 已完成订单 (8个)
for (let i = 1; i <= 8; i++) {
const customer = customers[Math.floor(Math.random() * customers.length)];
const vehicle = vehicles[Math.floor(Math.random() * vehicles.length)];
const days = Math.floor(Math.random() * 10) + 1;
const startDate = new Date(now.getTime() - (Math.floor(Math.random() * 30) + days) * oneDay);
const endDate = new Date(startDate.getTime() + days * oneDay);
const dailyRate = [30, 50, 80, 100][Math.floor(Math.random() * 4)];
const total = dailyRate * days;
orderData.push({
orderNumber: `ORD${String(now.getFullYear())}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(i).padStart(4, '0')}`,
customer: customer._id,
vehicle: vehicle._id,
startDate: startDate,
endDate: endDate,
actualEndDate: endDate,
rentalFee: dailyRate * days,
deposit: 200,
totalAmount: total + 200,
paidAmount: total + 200,
status: '已完成',
paymentMethod: ['微信', '支付宝', '现金'][Math.floor(Math.random() * 3)],
paymentDate: startDate,
contractSigned: true,
notes: ''
});
}
// 进行中订单 (6个)
for (let i = 9; i <= 14; i++) {
const customer = customers[Math.floor(Math.random() * customers.length)];
const vehicle = vehicles[Math.floor(Math.random() * vehicles.length)];
const days = Math.floor(Math.random() * 15) + 1;
const startDate = new Date(now.getTime() - Math.floor(Math.random() * 5) * oneDay);
const endDate = new Date(startDate.getTime() + days * oneDay);
const dailyRate = [30, 50, 80, 100][Math.floor(Math.random() * 4)];
orderData.push({
orderNumber: `ORD${String(now.getFullYear())}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(i).padStart(4, '0')}`,
customer: customer._id,
vehicle: vehicle._id,
startDate: startDate,
endDate: endDate,
rentalFee: dailyRate * days,
deposit: 200,
totalAmount: dailyRate * days + 200,
paidAmount: dailyRate * days + 200,
status: '进行中',
paymentMethod: ['微信', '支付宝', '现金'][Math.floor(Math.random() * 3)],
paymentDate: startDate,
contractSigned: true,
notes: ''
});
}
// 待支付订单 (2个)
for (let i = 15; i <= 16; i++) {
const customer = customers[Math.floor(Math.random() * customers.length)];
const vehicle = vehicles[Math.floor(Math.random() * vehicles.length)];
const days = Math.floor(Math.random() * 7) + 1;
const startDate = new Date(now.getTime() + oneDay);
const endDate = new Date(startDate.getTime() + days * oneDay);
const dailyRate = [30, 50, 80, 100][Math.floor(Math.random() * 4)];
orderData.push({
orderNumber: `ORD${String(now.getFullYear())}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(i).padStart(4, '0')}`,
customer: customer._id,
vehicle: vehicle._id,
startDate: startDate,
endDate: endDate,
rentalFee: dailyRate * days,
deposit: 200,
totalAmount: dailyRate * days + 200,
paidAmount: 0,
status: '待支付',
contractSigned: false,
notes: '订单已创建,等待支付'
});
}
// 已取消订单 (2个)
for (let i = 17; i <= 18; i++) {
const customer = customers[Math.floor(Math.random() * customers.length)];
const vehicle = vehicles[Math.floor(Math.random() * vehicles.length)];
const days = Math.floor(Math.random() * 5) + 1;
const startDate = new Date(now.getTime() - Math.floor(Math.random() * 10) * oneDay);
const endDate = new Date(startDate.getTime() + days * oneDay);
const dailyRate = [30, 50, 80, 100][Math.floor(Math.random() * 4)];
orderData.push({
orderNumber: `ORD${String(now.getFullYear())}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(i).padStart(4, '0')}`,
customer: customer._id,
vehicle: vehicle._id,
startDate: startDate,
endDate: endDate,
rentalFee: dailyRate * days,
deposit: 200,
totalAmount: dailyRate * days + 200,
paidAmount: 200,
status: '已取消',
paymentMethod: ['微信', '支付宝'][Math.floor(Math.random() * 2)],
paymentDate: startDate,
contractSigned: true,
notes: '客户主动取消订单'
});
}
// 逾期订单 (2个)
for (let i = 19; i <= 20; i++) {
const customer = customers[Math.floor(Math.random() * customers.length)];
const vehicle = vehicles[Math.floor(Math.random() * vehicles.length)];
const days = 3;
const startDate = new Date(now.getTime() - 10 * oneDay);
const endDate = new Date(startDate.getTime() + days * oneDay);
const dailyRate = [30, 50, 80, 100][Math.floor(Math.random() * 4)];
const overdueDays = Math.floor((now.getTime() - endDate.getTime()) / oneDay);
orderData.push({
orderNumber: `ORD${String(now.getFullYear())}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(i).padStart(4, '0')}`,
customer: customer._id,
vehicle: vehicle._id,
startDate: startDate,
endDate: endDate,
actualEndDate: now,
rentalFee: dailyRate * days,
deposit: 200,
totalAmount: dailyRate * days + 200 + overdueDays * 20,
paidAmount: dailyRate * days + 200,
status: '逾期',
overdueDays: overdueDays,
overdueFee: overdueDays * 20,
paymentMethod: ['微信', '支付宝'][Math.floor(Math.random() * 2)],
paymentDate: startDate,
contractSigned: true,
notes: '逾期未归还,需缴纳逾期费用'
});
}
const orders = await Order.insertMany(orderData);
console.log('✅ 订单: 20 个 (已完成: 8, 进行中: 6, 待支付: 2, 已取消: 2, 逾期: 2)');
// 更新客户的总租赁次数和消费
for (const customer of customers) {
const customerOrders = await Order.find({ customer: customer._id, status: '已完成' });
await Customer.findByIdAndUpdate(customer._id, {
totalRentals: customerOrders.length,
totalSpent: customerOrders.reduce((sum, o) => sum + o.totalAmount, 0),
currentRentals: await Order.countDocuments({ customer: customer._id, status: '进行中' })
});
}
console.log('✅ 客户数据已更新');
console.log('\n🎉 测试数据创建完成!');
console.log('📊 数据汇总:');
console.log(' - 门店: 10 个');
console.log(' - 车辆: 25 辆');
console.log(' - 客户/骑手: 15 个');
console.log(' - 订单: 20 个');
console.log(' - 订单状态分布: 已完成(8), 进行中(6), 待支付(2), 已取消(2), 逾期(2)');
mongoose.disconnect();
process.exit(0);
})
.catch(err => console.error('❌ 错误:', err));

55
server/add-data.js Normal file
View File

@ -0,0 +1,55 @@
const http = require('http');
function postData(path, data) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify(data);
const options = {
hostname: 'localhost',
port: 3000,
path: path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
});
req.on('error', reject);
req.write(postData);
req.end();
});
}
async function main() {
// 客服投诉
await postData('/api/complaints', { complaintId: 'COMP001', type: '服务态度', content: '客户投诉店员态度不好', handler: '张三', status: '待处理' });
console.log('✅ 投诉1');
await postData('/api/complaints', { complaintId: 'COMP002', type: '车辆问题', content: '电动车电量不足', handler: '李四', status: '处理中' });
console.log('✅ 投诉2');
await postData('/api/complaints', { complaintId: 'COMP003', type: '费用问题', content: '费用计算有误', handler: '王五', status: '已解决' });
console.log('✅ 投诉3');
// 审批
await postData('/api/approvals', { approvalId: 'APPR001', type: '订单退款', title: '客户申请退款', applicant: '张三', status: '待审批' });
console.log('✅ 审批1');
await postData('/api/approvals', { approvalId: 'APPR002', type: '车辆报废', title: '旧车报废申请', applicant: '李四', status: '已通过' });
console.log('✅ 审批2');
await postData('/api/approvals', { approvalId: 'APPR003', type: '押金退还', title: '押金退还审批', applicant: '王五', status: '待审批' });
console.log('✅ 审批3');
// 打款
await postData('/api/payments', { paymentId: 'PAY001', type: '退款', amount: 200, method: '微信', account: 'wx123456', status: '待打款' });
console.log('✅ 打款1');
await postData('/api/payments', { paymentId: 'PAY002', type: '押金退还', amount: 500, method: '支付宝', account: 'ali@xx.com', status: '已打款' });
console.log('✅ 打款2');
await postData('/api/payments', { paymentId: 'PAY003', type: '分成', amount: 300, method: '银行卡', account: '6222xxx1234', status: '打款中' });
console.log('✅ 打款3');
console.log('🎉 完成!');
}
main().catch(console.error);

View File

@ -0,0 +1,18 @@
// 清理 vehicle-types 集合中所有 pricePerDay 字段
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/e-scooter-rental')
.then(async () => {
const db = mongoose.connection.db;
const result = await db.collection('vehicle-types').updateMany(
{ pricePerDay: { $exists: true } },
{ $unset: { pricePerDay: "" } }
);
console.log(`已从 ${result.modifiedCount} 条 vehicle-types 文档中删除 pricePerDay 字段`);
await mongoose.disconnect();
process.exit(0);
})
.catch(err => {
console.error('执行失败:', err);
process.exit(1);
});

54
server/index.js Normal file
View File

@ -0,0 +1,54 @@
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(express.json());
// 连接 MongoDB
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/e-scooter-rental')
.then(() => console.log('✅ MongoDB 连接成功'))
.catch(err => console.error('❌ MongoDB 连接失败:', err.message));
// 路由
app.use('/api/vehicles', require('./routes/vehicles'));
app.use('/api/orders', require('./routes/orders'));
app.use('/api/customers', require('./routes/customers'));
app.use('/api/finance', require('./routes/finance'));
app.use('/api/stores', require('./routes/stores'));
app.use('/api/complaints', require('./routes/complaints'));
app.use('/api/approvals', require('./routes/approvals'));
app.use('/api/payments', require('./routes/payments'));
app.use('/api/conflicts', require('./routes/conflicts'));
app.use('/api/applications', require('./routes/applications'));
app.use('/api/disputes', require('./routes/disputes'));
app.use('/api/riders', require('./routes/riders'));
app.use('/api/vehicle-types', require('./routes/vehicleTypes'));
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 404 处理
app.use((req, res) => {
res.status(404).json({
success: false,
message: '接口不存在',
path: req.path
});
});
// 错误处理中间件
const errorHandler = require('./middleware/errorHandler');
app.use(errorHandler);
// 启动服务器
app.listen(PORT, () => {
console.log(`🚀 服务器运行在 http://localhost:${PORT}`);
});

View File

@ -0,0 +1,40 @@
// 错误处理中间件
const errorHandler = (err, req, res, next) => {
console.error('错误:', err.message);
// MongoDB 连接错误
if (err.name === 'MongoError' || err.name === 'MongooseError') {
return res.status(500).json({
success: false,
message: '数据库错误',
error: err.message
});
}
// 验证错误
if (err.name === 'ValidationError') {
return res.status(400).json({
success: false,
message: '数据验证失败',
error: err.message
});
}
// 404 错误
if (err.name === 'CastError') {
return res.status(404).json({
success: false,
message: '数据不存在',
error: err.message
});
}
// 默认错误
res.status(err.status || 500).json({
success: false,
message: err.message || '服务器内部错误',
error: process.env.NODE_ENV === 'development' ? err.stack : undefined
});
};
module.exports = errorHandler;

View File

@ -0,0 +1,25 @@
const mongoose = require('mongoose');
const applicationSchema = new mongoose.Schema({
appId: { type: String, required: true, unique: true },
store: { type: mongoose.Schema.Types.ObjectId, ref: 'Store' },
storeName: { type: String },
type: { type: String, enum: ['注册申请', '活动申请', '促销活动', '设备申请', '其他'], required: true },
title: { type: String, content: { type: String },
required: true },
status: { type: String, enum: ['待审批', '已通过', '已拒绝'], default: '待审批' },
rejectReason: { type: String },
handler: { type: String },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
applicationSchema.pre('save', function(next) {
if (!this.appId) {
this.appId = `APP${Date.now()}`;
}
this.updatedAt = new Date();
next();
});
module.exports = mongoose.model('Application', applicationSchema);

24
server/models/Approval.js Normal file
View File

@ -0,0 +1,24 @@
const mongoose = require('mongoose');
const approvalSchema = new mongoose.Schema({
approvalId: { type: String, required: true, unique: true },
type: { type: String, enum: ['订单退款', '车辆报废', '押金退还', '价格修改', '其他'], required: true },
title: { type: String, required: true },
content: { type: String },
applicant: { type: String },
approver: { type: String },
status: { type: String, enum: ['待审批', '已通过', '已拒绝'], default: '待审批' },
remark: { type: String },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
approvalSchema.pre('save', function(next) {
if (!this.approvalId) {
this.approvalId = `APPR${Date.now()}`;
}
this.updatedAt = new Date();
next();
});
module.exports = mongoose.model('Approval', approvalSchema);

View File

@ -0,0 +1,24 @@
const mongoose = require('mongoose');
const complaintSchema = new mongoose.Schema({
complaintId: { type: String, required: true, unique: true },
customer: { type: mongoose.Schema.Types.ObjectId, ref: 'Customer' },
order: { type: mongoose.Schema.Types.ObjectId, ref: 'Order' },
type: { type: String, enum: ['服务态度', '车辆问题', '费用问题', '其他'], required: true },
content: { type: String, required: true },
status: { type: String, enum: ['待处理', '处理中', '已解决', '已关闭'], default: '待处理' },
response: { type: String },
handler: { type: String },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
complaintSchema.pre('save', function(next) {
if (!this.complaintId) {
this.complaintId = `COMP${Date.now()}`;
}
this.updatedAt = new Date();
next();
});
module.exports = mongoose.model('Complaint', complaintSchema);

25
server/models/Conflict.js Normal file
View File

@ -0,0 +1,25 @@
const mongoose = require('mongoose');
const conflictSchema = new mongoose.Schema({
conflictId: { type: String, required: true, unique: true },
type: { type: String, enum: ['骑手门店', '门店公司'], required: true },
title: { type: String, required: true },
content: { type: String },
partyA: { type: String }, // 骑手/门店
partyB: { type: String }, // 门店/公司
status: { type: String, enum: ['待处理', '处理中', '已解决', '已关闭'], default: '待处理' },
handler: { type: String },
result: { type: String },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
conflictSchema.pre('save', function(next) {
if (!this.conflictId) {
this.conflictId = `CONF${Date.now()}`;
}
this.updatedAt = new Date();
next();
});
module.exports = mongoose.model('Conflict', conflictSchema);

57
server/models/Customer.js Normal file
View File

@ -0,0 +1,57 @@
const mongoose = require('mongoose');
const customerSchema = new mongoose.Schema({
// 基本信息
customerId: { type: String, required: true, unique: true }, // 客户编号
name: { type: String, required: true }, // 姓名
phone: { type: String, required: true }, // 手机号
idCard: { type: String }, // 身份证号
// 联系信息
address: { type: String }, // 地址
email: { type: String }, // 邮箱
// 租赁信息
totalRentals: { type: Number, default: 0 }, // 总租赁次数
totalSpent: { type: Number, default: 0 }, // 总消费金额
currentRentals: { type: Number, default: 0 }, // 当前租赁数量
// 信用信息
creditScore: { type: Number, default: 100 }, // 信用评分 (0-100)
creditLevel: { type: String, enum: ['优秀', '良好', '一般', '较差'], default: '优秀' },
// 账户信息
accountStatus: {
type: String,
enum: ['正常', '冻结', '注销'],
default: '正常'
},
// 备注
notes: { type: String },
// 时间戳
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
// 更新时自动更新 updatedAt
customerSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
// 生成客户编号
customerSchema.pre('save', function(next) {
if (!this.customerId) {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
this.customerId = `CUST${year}${month}${day}${random}`;
}
next();
});
module.exports = mongoose.model('Customer', customerSchema);

27
server/models/Dispute.js Normal file
View File

@ -0,0 +1,27 @@
const mongoose = require('mongoose');
const disputeSchema = new mongoose.Schema({
disputeId: { type: String, required: true, unique: true },
storeA: { type: mongoose.Schema.Types.ObjectId, ref: 'Store' },
storeAName: { type: String },
storeB: { type: mongoose.Schema.Types.ObjectId, ref: 'Store' },
storeBName: { type: String },
type: { type: String, enum: ['订单纠纷', '区域纠纷', '费用纠纷', '其他'], required: true },
title: { type: String, required: true },
content: { type: String },
status: { type: String, enum: ['待处理', '处理中', '已解决'], default: '待处理' },
result: { type: String },
handler: { type: String },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
disputeSchema.pre('save', function(next) {
if (!this.disputeId) {
this.disputeId = `DISP${Date.now()}`;
}
this.updatedAt = new Date();
next();
});
module.exports = mongoose.model('Dispute', disputeSchema);

66
server/models/Order.js Normal file
View File

@ -0,0 +1,66 @@
const mongoose = require('mongoose');
const orderSchema = new mongoose.Schema({
// 订单基本信息
orderNumber: { type: String, unique: true }, // 订单号(由 pre-save hook 自动生成)
customer: { type: mongoose.Schema.Types.ObjectId, ref: 'Customer', required: true }, // 客户
vehicle: { type: mongoose.Schema.Types.ObjectId, ref: 'Vehicle', required: true }, // 车辆
// 租赁信息
startDate: { type: Date, required: true }, // 开始日期
endDate: { type: Date, required: true }, // 结束日期
actualEndDate: { type: Date }, // 实际结束日期
// 费用信息
rentalFee: { type: Number, required: true }, // 租金
deposit: { type: Number, default: 0 }, // 押金
totalAmount: { type: Number, required: true }, // 总金额
paidAmount: { type: Number, default: 0 }, // 已支付金额
// 订单状态
status: {
type: String,
enum: ['待支付', '进行中', '已完成', '逾期', '已取消', '已退款'],
default: '待支付'
},
// 逾期信息
overdueDays: { type: Number, default: 0 }, // 逾期天数
overdueFee: { type: Number, default: 0 }, // 逾期费用
// 支付信息
paymentMethod: { type: String, enum: ['微信', '支付宝', '现金', '银行卡'] },
paymentDate: { type: Date },
// 合同信息
contractUrl: { type: String }, // 合同文件路径
contractSigned: { type: Boolean, default: false }, // 合同是否签署
// 备注
notes: { type: String },
// 时间戳
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
// 更新时自动更新 updatedAt
orderSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
// 生成订单号
orderSchema.pre('save', function(next) {
if (!this.orderNumber) {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
this.orderNumber = `ORD${year}${month}${day}${random}`;
}
next();
});
module.exports = mongoose.model('Order', orderSchema);

38
server/models/Payment.js Normal file
View File

@ -0,0 +1,38 @@
const mongoose = require('mongoose');
const paymentSchema = new mongoose.Schema({
paymentId: { type: String, required: true, unique: true },
// 收支类型:收入(客户付款)/ 支出(退款、工资、房租等)
type: {
type: String,
enum: ['收入', '支出'],
required: true
},
// 对方名称(谁给钱/我们给谁)
party: { type: String, required: true },
// 金额
amount: { type: Number, required: true },
// 收支方式
method: { type: String, enum: ['微信', '支付宝', '银行卡', '现金'] },
// 分类
category: {
type: String,
enum: ['租金收入', '押金退还', '退款', '工资', '房租', '水电', '维修', '其他']
},
// 备注
remark: { type: String },
// 经手人
operator: { type: String },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
paymentSchema.pre('save', function(next) {
if (!this.paymentId) {
this.paymentId = `PAY${Date.now()}`;
}
this.updatedAt = new Date();
next();
});
module.exports = mongoose.model('Payment', paymentSchema);

39
server/models/Rider.js Normal file
View File

@ -0,0 +1,39 @@
const mongoose = require('mongoose');
const riderSchema = new mongoose.Schema({
// 基本信息
riderId: { type: String, required: true, unique: true }, // 骑手编号
name: { type: String, required: true }, // 姓名
phone: { type: String, required: true }, // 手机号
password: { type: String, required: true }, // 密码(简单位符串存储)
// 接单状态
status: {
type: String,
enum: ['active', 'inactive'],
default: 'inactive'
},
// 统计数据
rating: { type: Number, default: 5.0 }, // 评分
totalOrders: { type: Number, default: 0 }, // 总订单数
totalIncome: { type: Number, default: 0 }, // 总收入
// 时间戳
createdAt: { type: Date, default: Date.now }
});
// 生成骑手编号
riderSchema.pre('save', function(next) {
if (!this.riderId) {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
this.riderId = `RIDER${year}${month}${day}${random}`;
}
next();
});
module.exports = mongoose.model('Rider', riderSchema);

26
server/models/Store.js Normal file
View File

@ -0,0 +1,26 @@
const mongoose = require('mongoose');
const storeSchema = new mongoose.Schema({
storeId: { type: String, required: true, unique: true },
name: { type: String, required: true },
address: { type: String },
phone: { type: String },
manager: { type: String },
status: { type: String, enum: ['营业中', '已停业', '装修中'], default: '营业中' },
approvalStatus: { type: String, enum: ['待审批', '已通过', '已拒绝'], default: '待审批' },
rejectReason: { type: String },
images: [{
type: { type: String }, // 门店照片/营业执照/身份证
url: { type: String }
}],
createdAt: { type: Date, default: Date.now }
});
storeSchema.pre('save', function(next) {
if (!this.storeId) {
this.storeId = `STORE${Date.now()}`;
}
next();
});
module.exports = mongoose.model('Store', storeSchema);

74
server/models/Vehicle.js Normal file
View File

@ -0,0 +1,74 @@
const mongoose = require('mongoose');
const vehicleSchema = new mongoose.Schema({
// 门店关联
storeId: { type: String, required: true, index: true },
// 车辆基本信息
frameNumber: { type: String, required: true, unique: true }, // 车架号
plateNumber: { type: String }, // 车牌号
// 品牌与车型
brand: { type: String }, // 品牌
vehicleType: { type: String }, // 车型(对应 vehicleTypes 的 name
color: { type: String }, // 颜色
// 电池信息
batteryType: { type: String }, // 电池类型
batteryCapacity: { type: Number }, // 电池容量 (Ah)
batteryStatus: { type: String, enum: ['正常', '老化', '待更换'], default: '正常' },
// 状态信息
status: {
type: String,
enum: ['空闲', '在租', '维修中', '已报废', '待回收'],
default: '空闲'
},
isRented: { type: Boolean, default: false }, // 是否在租(冗余字段,方便查询)
// 位置信息
location: {
type: { type: String, default: 'Point' },
coordinates: { type: [Number], default: [0, 0] } // [经度, 纬度]
},
lastLocationUpdate: { type: Date },
// 租赁信息
currentOrderId: { type: mongoose.Schema.Types.ObjectId, ref: 'Order' },
totalRentDays: { type: Number, default: 0 }, // 累计租赁天数
// 维护信息
lastMaintenanceDate: { type: Date },
nextMaintenanceDate: { type: Date },
maintenanceHistory: [{
date: { type: Date },
type: { type: String }, // 保养类型
description: { type: String },
cost: { type: Number }
}],
// 购买信息
purchaseDate: { type: Date },
purchasePrice: { type: Number },
purchaseSupplier: { type: String },
// 备注
notes: { type: String },
// 时间戳
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
// 更新时自动更新 updatedAt
vehicleSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
// 创建地理位置索引
vehicleSchema.index({ location: '2dsphere' });
// 按门店索引,方便查询某门店的车辆
vehicleSchema.index({ storeId: 1 });
module.exports = mongoose.model('Vehicle', vehicleSchema);

View File

@ -0,0 +1,12 @@
const mongoose = require('mongoose');
const vehicleTypeSchema = new mongoose.Schema({
storeId: { type: String, required: true, index: true },
brand: { type: String, required: true },
name: { type: String, required: true },
cover: { type: String, default: '' },
}, {
timestamps: true
});
module.exports = mongoose.model('VehicleType', vehicleTypeSchema);

6
server/models/index.js Normal file
View File

@ -0,0 +1,6 @@
// 导出所有模型
module.exports = {
Vehicle: require('./Vehicle'),
Order: require('./Order'),
Customer: require('./Customer')
};

View File

@ -0,0 +1,42 @@
const express = require('express');
const router = express.Router();
const Application = require('../models/Application');
router.get('/', async (req, res) => {
try {
const apps = await Application.find().populate('store');
res.json({ success: true, data: apps });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.post('/', async (req, res) => {
try {
const app = new Application(req.body);
await app.save();
res.json({ success: true, data: app });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
router.put('/:id', async (req, res) => {
try {
const app = await Application.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json({ success: true, data: app });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
router.delete('/:id', async (req, res) => {
try {
await Application.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
module.exports = router;

View File

@ -0,0 +1,46 @@
const express = require('express');
const router = express.Router();
const Approval = require('../models/Approval');
// 获取所有审批
router.get('/', async (req, res) => {
try {
const approvals = await Approval.find();
res.json({ success: true, data: approvals });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 创建审批
router.post('/', async (req, res) => {
try {
const approval = new Approval(req.body);
await approval.save();
res.json({ success: true, data: approval });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 更新审批
router.put('/:id', async (req, res) => {
try {
const approval = await Approval.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json({ success: true, data: approval });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 删除审批
router.delete('/:id', async (req, res) => {
try {
await Approval.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
module.exports = router;

View File

@ -0,0 +1,46 @@
const express = require('express');
const router = express.Router();
const Complaint = require('../models/Complaint');
// 获取所有投诉
router.get('/', async (req, res) => {
try {
const complaints = await Complaint.find().populate('customer order');
res.json({ success: true, data: complaints });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 创建投诉
router.post('/', async (req, res) => {
try {
const complaint = new Complaint(req.body);
await complaint.save();
res.json({ success: true, data: complaint });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 更新投诉
router.put('/:id', async (req, res) => {
try {
const complaint = await Complaint.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json({ success: true, data: complaint });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 删除投诉
router.delete('/:id', async (req, res) => {
try {
await Complaint.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
module.exports = router;

View File

@ -0,0 +1,46 @@
const express = require('express');
const router = express.Router();
const Conflict = require('../models/Conflict');
// 获取所有矛盾记录
router.get('/', async (req, res) => {
try {
const conflicts = await Conflict.find();
res.json({ success: true, data: conflicts });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 创建矛盾记录
router.post('/', async (req, res) => {
try {
const conflict = new Conflict(req.body);
await conflict.save();
res.json({ success: true, data: conflict });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 更新矛盾记录
router.put('/:id', async (req, res) => {
try {
const conflict = await Conflict.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json({ success: true, data: conflict });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 删除矛盾记录
router.delete('/:id', async (req, res) => {
try {
await Conflict.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
module.exports = router;

109
server/routes/customers.js Normal file
View File

@ -0,0 +1,109 @@
const express = require('express');
const router = express.Router();
const Customer = require('../models/Customer');
// 获取所有客户
router.get('/', async (req, res) => {
try {
const customers = await Customer.find();
res.json({ success: true, data: customers });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 获取单个客户
router.get('/:id', async (req, res) => {
try {
const customer = await Customer.findById(req.params.id);
if (!customer) {
return res.status(404).json({ success: false, message: '客户不存在' });
}
res.json({ success: true, data: customer });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 创建客户
router.post('/', async (req, res) => {
try {
const customer = new Customer(req.body);
await customer.save();
res.status(201).json({ success: true, data: customer });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 更新客户
router.put('/:id', async (req, res) => {
try {
const customer = await Customer.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!customer) {
return res.status(404).json({ success: false, message: '客户不存在' });
}
res.json({ success: true, data: customer });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 删除客户
router.delete('/:id', async (req, res) => {
try {
const customer = await Customer.findByIdAndDelete(req.params.id);
if (!customer) {
return res.status(404).json({ success: false, message: '客户不存在' });
}
res.json({ success: true, message: '客户已删除' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 搜索客户
router.get('/search/:keyword', async (req, res) => {
try {
const keyword = req.params.keyword;
const customers = await Customer.find({
$or: [
{ name: { $regex: keyword, $options: 'i' } },
{ phone: { $regex: keyword, $options: 'i' } },
{ customerId: { $regex: keyword, $options: 'i' } }
]
});
res.json({ success: true, data: customers });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 更新客户信用评分
router.patch('/:id/credit', async (req, res) => {
try {
const { creditScore } = req.body;
let creditLevel = '优秀';
if (creditScore < 60) creditLevel = '较差';
else if (creditScore < 80) creditLevel = '一般';
else if (creditScore < 90) creditLevel = '良好';
const customer = await Customer.findByIdAndUpdate(
req.params.id,
{ creditScore, creditLevel },
{ new: true }
);
if (!customer) {
return res.status(404).json({ success: false, message: '客户不存在' });
}
res.json({ success: true, data: customer });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
module.exports = router;

42
server/routes/disputes.js Normal file
View File

@ -0,0 +1,42 @@
const express = require('express');
const router = express.Router();
const Dispute = require('../models/Dispute');
router.get('/', async (req, res) => {
try {
const disputes = await Dispute.find();
res.json({ success: true, data: disputes });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.post('/', async (req, res) => {
try {
const dispute = new Dispute(req.body);
await dispute.save();
res.json({ success: true, data: dispute });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
router.put('/:id', async (req, res) => {
try {
const dispute = await Dispute.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json({ success: true, data: dispute });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
router.delete('/:id', async (req, res) => {
try {
await Dispute.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
module.exports = router;

164
server/routes/finance.js Normal file
View File

@ -0,0 +1,164 @@
const express = require('express');
const router = express.Router();
const Payment = require('../models/Payment');
// 获取收支明细列表
router.get('/', async (req, res) => {
try {
const { type, startDate, endDate, party } = req.query;
const filter = {};
if (type) filter.type = type;
if (party) filter.party = new RegExp(party, 'i');
if (startDate || endDate) {
filter.createdAt = {};
if (startDate) filter.createdAt.$gte = new Date(startDate);
if (endDate) filter.createdAt.$lte = new Date(endDate);
}
const payments = await Payment.find(filter)
.sort('-createdAt')
.limit(100);
// 计算汇总 - 支持中英文
const income = await Payment.aggregate([
{ $match: { type: { $in: ['income', 'expense', '收入', '支出', '租金收入', '押金退还', '租金'] } } },
{ $group: { _id: '$type', total: { $sum: '$amount' } } }
]);
// 分开计算收入和支出
let totalIncome = 0;
let totalExpense = 0;
for (const item of income) {
if (item._id === 'income' || item._id === '收入' || item._id === '租金收入' || item._id === '押金退还' || item._id === '租金') {
totalIncome += item.total;
} else {
totalExpense += item.total;
}
}
res.json({
success: true,
data: {
list: payments,
summary: {
totalIncome: totalIncome,
totalExpense: totalExpense,
balance: totalIncome - totalExpense
}
}
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 创建收支记录
router.post('/', async (req, res) => {
try {
const payment = new Payment(req.body);
await payment.save();
res.json({ success: true, data: payment });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 更新收支记录
router.put('/:id', async (req, res) => {
try {
const payment = await Payment.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json({ success: true, data: payment });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 删除收支记录
router.delete('/:id', async (req, res) => {
try {
await Payment.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 清空所有收支记录
router.post('/delete-all', async (req, res) => {
try {
await Payment.deleteMany({});
res.json({ success: true, message: 'All payments deleted' });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 获取收支统计
router.get('/stats', async (req, res) => {
try {
const { period = 'month' } = req.query;
let startDate;
const now = new Date();
switch (period) {
case 'week':
startDate = new Date(now - 7 * 24 * 60 * 60 * 1000);
break;
case 'month':
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
break;
case 'quarter':
startDate = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1);
break;
case 'year':
startDate = new Date(now.getFullYear(), 0, 1);
break;
default:
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
}
const income = await Payment.aggregate([
{ $match: { type: '收入', createdAt: { $gte: startDate } } },
{ $group: { _id: null, total: { $sum: '$amount' } } }
]);
const expense = await Payment.aggregate([
{ $match: { type: '支出', createdAt: { $gte: startDate } } },
{ $group: { _id: null, total: { $sum: '$amount' } } }
]);
// 按分类统计
const incomeByCategory = await Payment.aggregate([
{ $match: { type: '收入', createdAt: { $gte: startDate } } },
{ $group: { _id: '$category', total: { $sum: '$amount' } } }
]);
const expenseByCategory = await Payment.aggregate([
{ $match: { type: '支出', createdAt: { $gte: startDate } } },
{ $group: { _id: '$category', total: { $sum: '$amount' } } }
]);
res.json({
success: true,
data: {
income: income[0]?.total || 0,
expense: expense[0]?.total || 0,
balance: (income[0]?.total || 0) - (expense[0]?.total || 0),
incomeByCategory: incomeByCategory.reduce((acc, item) => {
acc[item._id || '其他'] = item.total;
return acc;
}, {}),
expenseByCategory: expenseByCategory.reduce((acc, item) => {
acc[item._id || '其他'] = item.total;
return acc;
}, {})
}
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;

7
server/routes/index.js Normal file
View File

@ -0,0 +1,7 @@
// 导出所有路由
module.exports = {
vehicles: require('./vehicles'),
orders: require('./orders'),
customers: require('./customers'),
finance: require('./finance')
};

251
server/routes/orders.js Normal file
View File

@ -0,0 +1,251 @@
const express = require('express');
const router = express.Router();
const Order = require('../models/Order');
const Vehicle = require('../models/Vehicle');
const Customer = require('../models/Customer');
// 获取所有订单
router.get('/', async (req, res) => {
try {
const orders = await Order.find()
.populate('customer', 'name phone')
.populate('vehicle', 'model vehicleId');
res.json({ success: true, data: orders });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 获取单个订单
router.get('/:id', async (req, res) => {
try {
const order = await Order.findById(req.params.id)
.populate('customer')
.populate('vehicle');
if (!order) {
return res.status(404).json({ success: false, message: '订单不存在' });
}
res.json({ success: true, data: order });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 创建订单(支持门店端新客创建订单)
router.post('/', async (req, res) => {
try {
const {
customerName, customer, // customerName=新客姓名字符串customer=已有客户ID
vehicleId, vehicle, // vehicleId=前端字段名vehicle=后端字段名
contractMonths, startDate, endDate, rentalFee, deposit, orderType
} = req.body;
// 解析车辆 ID兼容前端 vehicleId 字段名)
const vehicleObjectId = vehicle || vehicleId;
// 解析客户优先用已有客户ID否则用姓名查找或创建新客户
let customerObjectId = customer;
if (!customerObjectId && customerName) {
// 查找同名客户
let customerDoc = await Customer.findOne({ name: customerName });
if (!customerDoc) {
// 创建新客户phone 必填,设为默认值)
customerDoc = await Customer.create({
name: customerName,
phone: '00000000000',
customerId: `CUST${Date.now()}`
});
}
customerObjectId = customerDoc._id;
}
// 检查车辆是否存在
const vehicleDoc = await Vehicle.findById(vehicleObjectId);
if (!vehicleDoc) {
return res.status(404).json({ success: false, message: '车辆不存在' });
}
if (vehicleDoc.status !== '空闲') {
return res.status(400).json({ success: false, message: '车辆不可用,当前状态:' + vehicleDoc.status });
}
// 日期处理startDate/endDate 或根据 contractMonths 计算
const start = startDate ? new Date(startDate) : new Date();
let end = endDate ? new Date(endDate) : null;
let months = Number(contractMonths) || 0;
if (!end && months > 0) {
end = new Date(start);
end.setMonth(end.getMonth() + months);
}
// 租金计算:使用传入的 rentalFee若未传则为 0
let fee = Number(rentalFee) || 0;
const depositAmt = Number(deposit) || 0;
const totalAmount = fee + depositAmt;
// 创建订单
const order = new Order({
customer: customerObjectId,
vehicle: vehicleObjectId,
startDate: start,
endDate: end || new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000),
rentalFee: fee,
deposit: depositAmt,
totalAmount,
status: '进行中'
});
await order.save();
// 更新车辆状态
vehicleDoc.status = '在租';
vehicleDoc.isRented = true;
vehicleDoc.currentOrderId = order._id;
await vehicleDoc.save();
// 更新客户统计(如客户已存在)
if (customerObjectId) {
const cust = await Customer.findById(customerObjectId);
if (cust) {
cust.totalRentals += 1;
cust.currentRentals = (cust.currentRentals || 0) + 1;
cust.totalSpent = (cust.totalSpent || 0) + totalAmount;
await cust.save();
}
}
res.status(201).json({ success: true, data: order });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 更新订单
router.put('/:id', async (req, res) => {
try {
const order = await Order.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!order) {
return res.status(404).json({ success: false, message: '订单不存在' });
}
res.json({ success: true, data: order });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 结束订单
router.patch('/:id/complete', async (req, res) => {
try {
const order = await Order.findById(req.params.id);
if (!order) {
return res.status(404).json({ success: false, message: '订单不存在' });
}
order.status = '已完成';
order.actualEndDate = new Date();
await order.save();
// 更新车辆状态
const vehicle = await Vehicle.findById(order.vehicle);
if (vehicle) {
vehicle.status = '空闲';
vehicle.isRented = false;
vehicle.currentOrderId = null;
vehicle.totalRentDays += Math.ceil((new Date(order.endDate) - new Date(order.startDate)) / (1000 * 60 * 60 * 24));
await vehicle.save();
}
// 更新客户信息
const customer = await Customer.findById(order.customer);
if (customer) {
customer.currentRentals -= 1;
await customer.save();
}
res.json({ success: true, data: order });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 取消订单
router.patch('/:id/cancel', async (req, res) => {
try {
const order = await Order.findById(req.params.id);
if (!order) {
return res.status(404).json({ success: false, message: '订单不存在' });
}
if (order.status === '已完成' || order.status === '已取消') {
return res.status(400).json({ success: false, message: '当前状态无法取消' });
}
order.status = '已取消';
order.actualEndDate = new Date();
await order.save();
// 更新车辆状态
const vehicle = await Vehicle.findById(order.vehicle);
if (vehicle) {
vehicle.status = '空闲';
vehicle.isRented = false;
vehicle.currentOrderId = null;
await vehicle.save();
}
// 更新客户信息
const customer = await Customer.findById(order.customer);
if (customer) {
customer.currentRentals = Math.max((customer.currentRentals || 0) - 1, 0);
await customer.save();
}
res.json({ success: true, data: order });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 获取逾期订单
router.get('/status/overdue', async (req, res) => {
try {
const now = new Date();
const orders = await Order.find({
status: '进行中',
endDate: { $lt: now }
})
.populate('customer', 'name phone')
.populate('vehicle', 'model vehicleId');
// 更新逾期天数和费用
for (const order of orders) {
const overdueDays = Math.ceil((now - order.endDate) / (1000 * 60 * 60 * 24));
order.overdueDays = overdueDays;
order.overdueFee = overdueDays * order.rentalFee * 0.1; // 10% 滞纳金
order.status = '逾期';
await order.save();
}
res.json({ success: true, data: orders });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 按状态筛选订单
router.get('/status/:status', async (req, res) => {
try {
const orders = await Order.find({ status: req.params.status })
.populate('customer', 'name phone')
.populate('vehicle', 'model vehicleId');
res.json({ success: true, data: orders });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;

46
server/routes/payments.js Normal file
View File

@ -0,0 +1,46 @@
const express = require('express');
const router = express.Router();
const Payment = require('../models/Payment');
// 获取所有打款记录
router.get('/', async (req, res) => {
try {
const payments = await Payment.find();
res.json({ success: true, data: payments });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 创建打款记录
router.post('/', async (req, res) => {
try {
const payment = new Payment(req.body);
await payment.save();
res.json({ success: true, data: payment });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 更新打款记录
router.put('/:id', async (req, res) => {
try {
const payment = await Payment.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json({ success: true, data: payment });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 删除打款记录
router.delete('/:id', async (req, res) => {
try {
await Payment.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
module.exports = router;

84
server/routes/riders.js Normal file
View File

@ -0,0 +1,84 @@
const express = require('express');
const router = express.Router();
const Rider = require('../models/Rider');
const Order = require('../models/Order');
// 骑手登录
router.post('/login', async (req, res) => {
try {
const { phone, password } = req.body;
if (!phone || !password) {
return res.status(400).json({ success: false, message: '手机号和密码不能为空' });
}
const rider = await Rider.findOne({ phone, password });
if (!rider) {
return res.status(401).json({ success: false, message: '手机号或密码错误' });
}
// 简单生成一个token实际生产应使用 JWT
const token = Buffer.from(`${rider._id}:${Date.now()}`).toString('base64');
res.json({
success: true,
data: {
id: rider._id,
riderId: rider.riderId,
name: rider.name,
phone: rider.phone,
status: rider.status,
rating: rider.rating,
totalOrders: rider.totalOrders,
totalIncome: rider.totalIncome,
token
}
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 获取骑手信息
router.get('/:id', async (req, res) => {
try {
const rider = await Rider.findById(req.params.id);
if (!rider) {
return res.status(404).json({ success: false, message: '骑手不存在' });
}
res.json({ success: true, data: rider });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 更新骑手信息(如状态切换)
router.put('/:id', async (req, res) => {
try {
const rider = await Rider.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!rider) {
return res.status(404).json({ success: false, message: '骑手不存在' });
}
res.json({ success: true, data: rider });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 获取骑手的订单
router.get('/:id/orders', async (req, res) => {
try {
const orders = await Order.find({ rider: req.params.id })
.populate('customer', 'name phone')
.populate('vehicle', 'vehicleId model color')
.sort({ createdAt: -1 });
res.json({ success: true, data: orders });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;

46
server/routes/stores.js Normal file
View File

@ -0,0 +1,46 @@
const express = require('express');
const router = express.Router();
const Store = require('../models/Store');
// 获取所有门店
router.get('/', async (req, res) => {
try {
const stores = await Store.find();
res.json({ success: true, data: stores });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 创建门店
router.post('/', async (req, res) => {
try {
const store = new Store(req.body);
await store.save();
res.json({ success: true, data: store });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 更新门店
router.put('/:id', async (req, res) => {
try {
const store = await Store.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json({ success: true, data: store });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 删除门店
router.delete('/:id', async (req, res) => {
try {
await Store.findByIdAndDelete(req.params.id);
res.json({ success: true });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
module.exports = router;

View File

@ -0,0 +1,58 @@
const express = require('express');
const router = express.Router();
const VehicleType = require('../models/VehicleType');
// 获取所有车型(支持按 storeId 筛选)
router.get('/', async (req, res) => {
try {
const filter = {};
if (req.query.storeId) filter.storeId = req.query.storeId;
const vehicleTypes = await VehicleType.find(filter).sort({ createdAt: -1 });
res.json({ success: true, data: vehicleTypes });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 创建车型
router.post('/', async (req, res) => {
try {
const vehicleType = new VehicleType(req.body);
await vehicleType.save();
res.status(201).json({ success: true, data: vehicleType });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 更新车型
router.put('/:id', async (req, res) => {
try {
const vehicleType = await VehicleType.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!vehicleType) {
return res.status(404).json({ success: false, message: '车型不存在' });
}
res.json({ success: true, data: vehicleType });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 删除车型
router.delete('/:id', async (req, res) => {
try {
const vehicleType = await VehicleType.findByIdAndDelete(req.params.id);
if (!vehicleType) {
return res.status(404).json({ success: false, message: '车型不存在' });
}
res.json({ success: true, message: '车型已删除' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;

117
server/routes/vehicles.js Normal file
View File

@ -0,0 +1,117 @@
const express = require('express');
const router = express.Router();
const Vehicle = require('../models/Vehicle');
// 获取所有车辆(支持按 storeId 过滤)
router.get('/', async (req, res) => {
try {
const filter = {};
if (req.query.storeId) {
filter.storeId = req.query.storeId;
}
const vehicles = await Vehicle.find(filter).populate('currentOrderId');
res.json({ success: true, data: vehicles });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 获取单个车辆
router.get('/:id', async (req, res) => {
try {
const vehicle = await Vehicle.findById(req.params.id).populate('currentOrderId');
if (!vehicle) {
return res.status(404).json({ success: false, message: '车辆不存在' });
}
res.json({ success: true, data: vehicle });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 创建车辆
router.post('/', async (req, res) => {
try {
const data = { ...req.body };
// status 为"在租"时自动设置 isRented
if (data.status === '在租') {
data.isRented = true;
}
const vehicle = new Vehicle(data);
await vehicle.save();
res.status(201).json({ success: true, data: vehicle });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 更新车辆
router.put('/:id', async (req, res) => {
try {
const data = { ...req.body };
// status 变化时同步 isRented
if (data.status !== undefined) {
data.isRented = data.status === '在租';
}
const vehicle = await Vehicle.findByIdAndUpdate(
req.params.id,
data,
{ new: true, runValidators: true }
);
if (!vehicle) {
return res.status(404).json({ success: false, message: '车辆不存在' });
}
res.json({ success: true, data: vehicle });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// 删除车辆
router.delete('/:id', async (req, res) => {
try {
const vehicle = await Vehicle.findByIdAndDelete(req.params.id);
if (!vehicle) {
return res.status(404).json({ success: false, message: '车辆不存在' });
}
res.json({ success: true, message: '车辆已删除' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 按状态筛选车辆
router.get('/status/:status', async (req, res) => {
try {
const vehicles = await Vehicle.find({ status: req.params.status });
res.json({ success: true, data: vehicles });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// 更新车辆位置
router.patch('/:id/location', async (req, res) => {
try {
const { longitude, latitude } = req.body;
const vehicle = await Vehicle.findByIdAndUpdate(
req.params.id,
{
location: {
type: 'Point',
coordinates: [longitude, latitude]
},
lastLocationUpdate: new Date()
},
{ new: true }
);
if (!vehicle) {
return res.status(404).json({ success: false, message: '车辆不存在' });
}
res.json({ success: true, data: vehicle });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
module.exports = router;

51
server/seed-new.js Normal file
View File

@ -0,0 +1,51 @@
const axios = require('axios');
async function seed() {
// 门店数据
const stores = [
{ storeId: 'STORE001', name: '旗舰店A', address: '北京市朝阳区', phone: '010-12345678', manager: '张三', status: '营业中' },
{ storeId: 'STORE002', name: '旗舰店B', address: '上海市浦东新区', phone: '021-87654321', manager: '李四', status: '营业中' },
{ storeId: 'STORE003', name: '加盟店C', address: '广州市天河区', phone: '020-11112222', manager: '王五', status: '装修中' }
];
for (const s of stores) {
await axios.post('http://localhost:3000/api/stores', s);
}
console.log('✅ 门店数据');
// 客服投诉
const complaints = [
{ complaintId: 'COMP001', type: '服务态度', content: '客户投诉店员态度不好', handler: '张三', status: '处理中' },
{ complaintId: 'COMP002', type: '车辆问题', content: '电动车电量不足', handler: '李四', status: '已解决' },
{ complaintId: 'COMP003', type: '费用问题', content: '费用计算有误', handler: '王五', status: '待处理' }
];
for (const c of complaints) {
await axios.post('http://localhost:3000/api/complaints', c);
}
console.log('✅ 客服投诉');
// 审批
const approvals = [
{ approvalId: 'APPR001', type: '订单退款', title: '客户申请退款', applicant: '张三', status: '待审批' },
{ approvalId: 'APPR002', type: '车辆报废', title: '旧车报废申请', applicant: '李四', status: '已通过' },
{ approvalId: 'APPR003', type: '押金退还', title: '押金退还审批', applicant: '王五', status: '待审批' }
];
for (const a of approvals) {
await axios.post('http://localhost:3000/api/approvals', a);
}
console.log('✅ 审批数据');
// 打款
const payments = [
{ paymentId: 'PAY001', type: '退款', amount: 200, method: '微信', account: 'wx123456', status: '待打款' },
{ paymentId: 'PAY002', type: '押金退还', amount: 500, method: '支付宝', account: 'ali@xx.com', status: '已打款' },
{ paymentId: 'PAY003', type: '分成', amount: 300, method: '银行卡', account: '6222***1234', status: '打款中' }
];
for (const p of payments) {
await axios.post('http://localhost:3000/api/payments', p);
}
console.log('✅ 打款数据');
console.log('🎉 测试数据添加完成!');
}
seed().catch(console.error);

295
server/seed.js Normal file
View File

@ -0,0 +1,295 @@
const mongoose = require('mongoose');
const Vehicle = require('./models/Vehicle');
const Customer = require('./models/Customer');
const Order = require('./models/Order');
const Store = require('./models/Store');
const Rider = require('./models/Rider');
// 连接 MongoDB
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/e-scooter-rental')
.then(() => console.log('✅ MongoDB 连接成功'))
.catch(err => console.error('❌ MongoDB 连接失败:', err));
// 清空数据
async function clearData() {
console.log('🧹 清空数据...');
await Vehicle.deleteMany({});
await Customer.deleteMany({});
await Order.deleteMany({});
await Store.deleteMany({});
await Rider.deleteMany({});
console.log('✅ 数据已清空');
}
// 创建示例车辆
async function createVehicles() {
const vehicles = [
{
vehicleId: 'SCOOTER001',
model: '黑骑士',
color: '黑色',
batteryType: '锂电池',
batteryCapacity: 20,
batteryStatus: '正常',
status: '空闲',
purchaseDate: new Date('2024-01-15'),
purchasePrice: 3500,
purchaseSupplier: '小牛电动车'
},
{
vehicleId: 'SCOOTER002',
model: '黑骑士',
color: '白色',
batteryType: '锂电池',
batteryCapacity: 20,
batteryStatus: '正常',
status: '空闲',
purchaseDate: new Date('2024-02-20'),
purchasePrice: 3500,
purchaseSupplier: '小牛电动车'
},
{
vehicleId: 'SCOOTER003',
model: '电动车',
color: '蓝色',
batteryType: '铅酸电池',
batteryCapacity: 24,
batteryStatus: '正常',
status: '空闲',
purchaseDate: new Date('2024-03-10'),
purchasePrice: 2800,
purchaseSupplier: '雅迪电动车'
},
{
vehicleId: 'SCOOTER004',
model: '高端豪车',
color: '红色',
batteryType: '锂电池',
batteryCapacity: 30,
batteryStatus: '正常',
status: '空闲',
purchaseDate: new Date('2024-04-05'),
purchasePrice: 8000,
purchaseSupplier: '特斯拉'
},
{
vehicleId: 'SCOOTER005',
model: '普通标准套餐',
color: '绿色',
batteryType: '铅酸电池',
batteryCapacity: 20,
batteryStatus: '正常',
status: '空闲',
purchaseDate: new Date('2024-05-01'),
purchasePrice: 2500,
purchaseSupplier: '爱玛电动车'
}
];
console.log('🚗 创建示例车辆...');
await Vehicle.insertMany(vehicles);
console.log(`✅ 创建了 ${vehicles.length} 辆车`);
}
// 创建示例客户
async function createCustomers() {
const customers = [
{
customerId: 'CUST001',
name: '张三',
phone: '13800138000',
idCard: '110101199001011234',
address: '北京市朝阳区',
email: 'zhangsan@example.com'
},
{
customerId: 'CUST002',
name: '李四',
phone: '13800138001',
idCard: '110101199002022345',
address: '北京市海淀区',
email: 'lisi@example.com'
},
{
customerId: 'CUST003',
name: '王五',
phone: '13800138002',
idCard: '110101199003033456',
address: '北京市西城区',
email: 'wangwu@example.com'
},
{
customerId: 'CUST004',
name: '赵六',
phone: '13800138003',
idCard: '110101199004044567',
address: '北京市东城区',
email: 'zhaoliu@example.com'
},
{
customerId: 'CUST005',
name: '钱七',
phone: '13800138004',
idCard: '110101199005055678',
address: '北京市丰台区',
email: 'qianqi@example.com'
}
];
console.log('👥 创建示例客户...');
await Customer.insertMany(customers);
console.log(`✅ 创建了 ${customers.length} 个客户`);
}
// 创建示例订单
async function createOrders() {
const vehicles = await Vehicle.find().limit(3);
const customers = await Customer.find().limit(3);
const orders = [
{
orderNumber: 'ORDER001',
customer: customers[0]._id,
vehicle: vehicles[0]._id,
startDate: new Date('2026-02-20'),
endDate: new Date('2026-03-20'),
rentalFee: 50,
deposit: 200,
totalAmount: 300,
status: '进行中'
},
{
orderNumber: 'ORDER002',
customer: customers[1]._id,
vehicle: vehicles[1]._id,
startDate: new Date('2026-02-15'),
endDate: new Date('2026-03-15'),
rentalFee: 50,
deposit: 200,
totalAmount: 300,
status: '进行中'
},
{
orderNumber: 'ORDER003',
customer: customers[2]._id,
vehicle: vehicles[2]._id,
startDate: new Date('2026-01-10'),
endDate: new Date('2026-02-10'),
actualEndDate: new Date('2026-02-10'),
rentalFee: 40,
deposit: 150,
totalAmount: 200,
paidAmount: 200,
status: '已完成'
}
];
console.log('📋 创建示例订单...');
await Order.insertMany(orders);
console.log(`✅ 创建了 ${orders.length} 个订单`);
}
// 创建示例门店
async function createStores() {
const stores = [
{
storeId: 'STORE001',
name: '朝阳电动车门店',
address: '北京市朝阳区建国路88号',
phone: '010-12345678',
manager: '张三',
status: '营业中',
approvalStatus: '已通过',
images: []
},
{
storeId: 'STORE002',
name: '海淀电动车门店',
address: '北京市海淀区中关村大街100号',
phone: '010-87654321',
manager: '李四',
status: '营业中',
approvalStatus: '已通过',
images: []
},
{
storeId: 'STORE003',
name: '西城电动车门店',
address: '北京市西城区西单北大街120号',
phone: '010-11223344',
manager: '王五',
status: '营业中',
approvalStatus: '已通过',
images: []
}
];
console.log('🏪 创建示例门店...');
await Store.insertMany(stores);
console.log(`✅ 创建了 ${stores.length} 个门店`);
}
// 创建示例骑手
async function createRiders() {
const riders = [
{
riderId: 'RIDER001',
name: '张骑手',
phone: '13900139000',
password: '123456',
status: 'active',
rating: 4.8,
totalOrders: 120,
totalIncome: 15000
},
{
riderId: 'RIDER002',
name: '李骑手',
phone: '13900139001',
password: '123456',
status: 'active',
rating: 4.5,
totalOrders: 80,
totalIncome: 10000
},
{
riderId: 'RIDER003',
name: '王骑手',
phone: '13900139002',
password: '123456',
status: 'inactive',
rating: 4.9,
totalOrders: 200,
totalIncome: 25000
}
];
console.log('🛵 创建示例骑手...');
await Rider.insertMany(riders);
console.log(`✅ 创建了 ${riders.length} 个骑手`);
}
// 主函数
async function main() {
try {
await clearData();
await createVehicles();
await createCustomers();
await createOrders();
await createStores();
await createRiders();
console.log('\n🎉 示例数据创建完成!');
console.log('车辆: 5 辆');
console.log('客户: 5 个');
console.log('订单: 3 个');
console.log('门店: 3 个');
console.log('骑手: 3 个');
} catch (error) {
console.error('❌ 创建示例数据失败:', error);
} finally {
mongoose.disconnect();
process.exit(0);
}
}
main();

10
test-mongo.js Normal file
View File

@ -0,0 +1,10 @@
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/e-scooter-rental')
.then(() => {
console.log('MongoDB OK');
process.exit(0);
})
.catch(e => {
console.log('MongoDB Error:', e.message);
process.exit(1);
});