commit 6e1378c812633c703995f3110bf04b7bc1c3cfad Author: notyclaw Date: Fri Mar 27 21:10:55 2026 +0800 Initial commit: 电动车租赁系统后端 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea020c4 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..612ccf0 --- /dev/null +++ b/README.md @@ -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 +- [ ] 开发前端界面 +- [ ] 集成测试 diff --git a/add-finance.js b/add-finance.js new file mode 100644 index 0000000..5f3c198 --- /dev/null +++ b/add-finance.js @@ -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(); diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..8b7639a --- /dev/null +++ b/docs/API.md @@ -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": "..." }` diff --git a/docs/MongoDB安装指南.md b/docs/MongoDB安装指南.md new file mode 100644 index 0000000..e47a9fa --- /dev/null +++ b/docs/MongoDB安装指南.md @@ -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. ⏳ 开发前端页面 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4c4b56f --- /dev/null +++ b/docs/README.md @@ -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 数据库 +- ⏳ 需要生成测试数据 +- ⏳ 需要开发前端页面 + +## 📞 联系方式 + +如有问题,请联系项目负责人。 diff --git a/docs/下一步行动.md b/docs/下一步行动.md new file mode 100644 index 0000000..d7ed03f --- /dev/null +++ b/docs/下一步行动.md @@ -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 或开发过程中遇到问题,请随时联系! diff --git a/docs/前端开发指南.md b/docs/前端开发指南.md new file mode 100644 index 0000000..edc7fe8 --- /dev/null +++ b/docs/前端开发指南.md @@ -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. 开发财务统计页面 diff --git a/docs/常见问题.md b/docs/常见问题.md new file mode 100644 index 0000000..8dea095 --- /dev/null +++ b/docs/常见问题.md @@ -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` - 接口文档 diff --git a/docs/开发计划.md b/docs/开发计划.md new file mode 100644 index 0000000..5522170 --- /dev/null +++ b/docs/开发计划.md @@ -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 + +--- + +## 📞 联系方式 + +如有问题,请联系项目负责人。 diff --git a/docs/开发进度.md b/docs/开发进度.md new file mode 100644 index 0000000..f73806d --- /dev/null +++ b/docs/开发进度.md @@ -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. 前端开发需要选择技术栈 diff --git a/docs/快速开始.md b/docs/快速开始.md new file mode 100644 index 0000000..6479c65 --- /dev/null +++ b/docs/快速开始.md @@ -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. ⏳ 开发前端页面 diff --git a/docs/数据库设计.md b/docs/数据库设计.md new file mode 100644 index 0000000..d51b454 --- /dev/null +++ b/docs/数据库设计.md @@ -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 自动计算 diff --git a/docs/文件清单.md b/docs/文件清单.md new file mode 100644 index 0000000..ef77843 --- /dev/null +++ b/docs/文件清单.md @@ -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. 实现页面设计 diff --git a/docs/测试数据.md b/docs/测试数据.md new file mode 100644 index 0000000..314e442 --- /dev/null +++ b/docs/测试数据.md @@ -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. 生产环境请使用真实数据 diff --git a/docs/页面设计.md b/docs/页面设计.md new file mode 100644 index 0000000..b8091f0 --- /dev/null +++ b/docs/页面设计.md @@ -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 + +``` + +### 2. 表格组件 + +```vue + +``` + +### 3. 表单组件 + +```vue + +``` + +## 交互设计 + +### 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. 测试和优化 diff --git a/docs/项目完成清单.md b/docs/项目完成清单.md new file mode 100644 index 0000000..47dedfe --- /dev/null +++ b/docs/项目完成清单.md @@ -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. **前端开发指南** + - 详细的前端开发规范 + - 页面设计文档 + - 组件设计指南 diff --git a/docs/项目总结.md b/docs/项目总结.md new file mode 100644 index 0000000..d0bf19c --- /dev/null +++ b/docs/项目总结.md @@ -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. **模块化设计** + - 数据模型、路由、中间件分离 + - 易于维护和扩展 diff --git a/docs/项目状态.md b/docs/项目状态.md new file mode 100644 index 0000000..4c4ac5b --- /dev/null +++ b/docs/项目状态.md @@ -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. **部署成功**: 生产环境正常运行 diff --git a/docs/项目里程碑.md b/docs/项目里程碑.md new file mode 100644 index 0000000..4ffe301 --- /dev/null +++ b/docs/项目里程碑.md @@ -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. **部署成功**: 生产环境正常运行 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d98b88c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1572 @@ +{ + "name": "e-scooter-rental-system", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e-scooter-rental-system", + "version": "1.0.0", + "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" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.6", + "resolved": "https://registry.npmmirror.com/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmmirror.com/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmmirror.com/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmmirror.com/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmmirror.com/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.23.0", + "resolved": "https://registry.npmmirror.com/mongoose/-/mongoose-8.23.0.tgz", + "integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmmirror.com/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..27e6886 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/reset-finance.js b/reset-finance.js new file mode 100644 index 0000000..fa86905 --- /dev/null +++ b/reset-finance.js @@ -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(); diff --git a/seed-all.js b/seed-all.js new file mode 100644 index 0000000..4e34355 --- /dev/null +++ b/seed-all.js @@ -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)); diff --git a/seed-dispute.js b/seed-dispute.js new file mode 100644 index 0000000..2059521 --- /dev/null +++ b/seed-dispute.js @@ -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)); diff --git a/seed-payment.js b/seed-payment.js new file mode 100644 index 0000000..53d9c18 --- /dev/null +++ b/seed-payment.js @@ -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)); diff --git a/seed-payments.js b/seed-payments.js new file mode 100644 index 0000000..5092e50 --- /dev/null +++ b/seed-payments.js @@ -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); + }); diff --git a/seed-test-data.js b/seed-test-data.js new file mode 100644 index 0000000..73b902d --- /dev/null +++ b/seed-test-data.js @@ -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)); diff --git a/server/add-data.js b/server/add-data.js new file mode 100644 index 0000000..e12f4a4 --- /dev/null +++ b/server/add-data.js @@ -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); diff --git a/server/clearPricePerDay.js b/server/clearPricePerDay.js new file mode 100644 index 0000000..370db6e --- /dev/null +++ b/server/clearPricePerDay.js @@ -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); + }); diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..35bb440 --- /dev/null +++ b/server/index.js @@ -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}`); +}); diff --git a/server/middleware/errorHandler.js b/server/middleware/errorHandler.js new file mode 100644 index 0000000..6c18bb3 --- /dev/null +++ b/server/middleware/errorHandler.js @@ -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; diff --git a/server/models/Application.js b/server/models/Application.js new file mode 100644 index 0000000..88e84d0 --- /dev/null +++ b/server/models/Application.js @@ -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); diff --git a/server/models/Approval.js b/server/models/Approval.js new file mode 100644 index 0000000..1132179 --- /dev/null +++ b/server/models/Approval.js @@ -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); diff --git a/server/models/Complaint.js b/server/models/Complaint.js new file mode 100644 index 0000000..46604d3 --- /dev/null +++ b/server/models/Complaint.js @@ -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); diff --git a/server/models/Conflict.js b/server/models/Conflict.js new file mode 100644 index 0000000..fbaec8f --- /dev/null +++ b/server/models/Conflict.js @@ -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); diff --git a/server/models/Customer.js b/server/models/Customer.js new file mode 100644 index 0000000..dbf410f --- /dev/null +++ b/server/models/Customer.js @@ -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); diff --git a/server/models/Dispute.js b/server/models/Dispute.js new file mode 100644 index 0000000..f73c05f --- /dev/null +++ b/server/models/Dispute.js @@ -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); diff --git a/server/models/Order.js b/server/models/Order.js new file mode 100644 index 0000000..c023916 --- /dev/null +++ b/server/models/Order.js @@ -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); diff --git a/server/models/Payment.js b/server/models/Payment.js new file mode 100644 index 0000000..dab6944 --- /dev/null +++ b/server/models/Payment.js @@ -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); diff --git a/server/models/Rider.js b/server/models/Rider.js new file mode 100644 index 0000000..9fb67b7 --- /dev/null +++ b/server/models/Rider.js @@ -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); diff --git a/server/models/Store.js b/server/models/Store.js new file mode 100644 index 0000000..cd4a6fc --- /dev/null +++ b/server/models/Store.js @@ -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); diff --git a/server/models/Vehicle.js b/server/models/Vehicle.js new file mode 100644 index 0000000..7d3161a --- /dev/null +++ b/server/models/Vehicle.js @@ -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); diff --git a/server/models/VehicleType.js b/server/models/VehicleType.js new file mode 100644 index 0000000..7a33fe8 --- /dev/null +++ b/server/models/VehicleType.js @@ -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); diff --git a/server/models/index.js b/server/models/index.js new file mode 100644 index 0000000..013d8fa --- /dev/null +++ b/server/models/index.js @@ -0,0 +1,6 @@ +// 导出所有模型 +module.exports = { + Vehicle: require('./Vehicle'), + Order: require('./Order'), + Customer: require('./Customer') +}; diff --git a/server/routes/applications.js b/server/routes/applications.js new file mode 100644 index 0000000..ba08d81 --- /dev/null +++ b/server/routes/applications.js @@ -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; diff --git a/server/routes/approvals.js b/server/routes/approvals.js new file mode 100644 index 0000000..82de76d --- /dev/null +++ b/server/routes/approvals.js @@ -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; diff --git a/server/routes/complaints.js b/server/routes/complaints.js new file mode 100644 index 0000000..a6d617e --- /dev/null +++ b/server/routes/complaints.js @@ -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; diff --git a/server/routes/conflicts.js b/server/routes/conflicts.js new file mode 100644 index 0000000..d03fdd8 --- /dev/null +++ b/server/routes/conflicts.js @@ -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; diff --git a/server/routes/customers.js b/server/routes/customers.js new file mode 100644 index 0000000..ff5fac2 --- /dev/null +++ b/server/routes/customers.js @@ -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; diff --git a/server/routes/disputes.js b/server/routes/disputes.js new file mode 100644 index 0000000..4d97175 --- /dev/null +++ b/server/routes/disputes.js @@ -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; diff --git a/server/routes/finance.js b/server/routes/finance.js new file mode 100644 index 0000000..0a07cd4 --- /dev/null +++ b/server/routes/finance.js @@ -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; diff --git a/server/routes/index.js b/server/routes/index.js new file mode 100644 index 0000000..5d353b1 --- /dev/null +++ b/server/routes/index.js @@ -0,0 +1,7 @@ +// 导出所有路由 +module.exports = { + vehicles: require('./vehicles'), + orders: require('./orders'), + customers: require('./customers'), + finance: require('./finance') +}; diff --git a/server/routes/orders.js b/server/routes/orders.js new file mode 100644 index 0000000..0e87b0d --- /dev/null +++ b/server/routes/orders.js @@ -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; diff --git a/server/routes/payments.js b/server/routes/payments.js new file mode 100644 index 0000000..f8922f6 --- /dev/null +++ b/server/routes/payments.js @@ -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; diff --git a/server/routes/riders.js b/server/routes/riders.js new file mode 100644 index 0000000..5b6df20 --- /dev/null +++ b/server/routes/riders.js @@ -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; diff --git a/server/routes/stores.js b/server/routes/stores.js new file mode 100644 index 0000000..dc1c3e9 --- /dev/null +++ b/server/routes/stores.js @@ -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; diff --git a/server/routes/vehicleTypes.js b/server/routes/vehicleTypes.js new file mode 100644 index 0000000..bc5bfe5 --- /dev/null +++ b/server/routes/vehicleTypes.js @@ -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; diff --git a/server/routes/vehicles.js b/server/routes/vehicles.js new file mode 100644 index 0000000..0d51aea --- /dev/null +++ b/server/routes/vehicles.js @@ -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; diff --git a/server/seed-new.js b/server/seed-new.js new file mode 100644 index 0000000..bde1181 --- /dev/null +++ b/server/seed-new.js @@ -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); diff --git a/server/seed.js b/server/seed.js new file mode 100644 index 0000000..47f41e3 --- /dev/null +++ b/server/seed.js @@ -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(); diff --git a/test-mongo.js b/test-mongo.js new file mode 100644 index 0000000..e749e2c --- /dev/null +++ b/test-mongo.js @@ -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); + });