From c48e2c2961255f09b74910d9a243535c667cd3dd Mon Sep 17 00:00:00 2001 From: notyclaw Date: Fri, 27 Mar 2026 23:43:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=A8=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E4=BD=93=E7=B3=BB=20-=20JWT=E9=89=B4=E6=9D=83=E3=80=81bcrypt?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E5=8A=A0=E5=AF=86=E3=80=81RBAC=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=8E=A7=E5=88=B6=E3=80=81=E9=80=9F=E7=8E=87=E9=99=90?= =?UTF-8?q?=E5=88=B6=E3=80=81CORS=E7=99=BD=E5=90=8D=E5=8D=95=E3=80=81?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 8 + package-lock.json | 226 +++++++++++++++++++++++++++- package.json | 7 +- seed-full-data.js | 269 ++++++++++++++++++++++++++++++++++ server/index.js | 55 +++++-- server/middleware/auth.js | 34 +++++ server/middleware/validate.js | 75 ++++++++++ server/models/Rider.js | 9 +- server/routes/customers.js | 47 +++--- server/routes/finance.js | 103 ++++++------- server/routes/orders.js | 107 ++++++-------- server/routes/riders.js | 45 ++++-- server/routes/stores.js | 20 ++- server/routes/vehicles.js | 73 ++++----- server/seed.js | 215 ++++----------------------- server/utils/password.js | 17 +++ 16 files changed, 897 insertions(+), 413 deletions(-) create mode 100644 .env.example create mode 100644 seed-full-data.js create mode 100644 server/middleware/auth.js create mode 100644 server/middleware/validate.js create mode 100644 server/utils/password.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0a8e32c --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +NODE_ENV=production +PORT=3000 +MONGODB_URI=mongodb://localhost:27017/e-scooter-rental +JWT_SECRET=your-super-secret-jwt-key-change-in-production +JWT_EXPIRES_IN=7d +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX_REQUESTS=100 +ALLOWED_ORIGINS=https://51bike.online,https://rider.51bike.online,https://store.51bike.online diff --git a/package-lock.json b/package-lock.json index d98b88c..b7a1986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,68 @@ "license": "ISC", "dependencies": { "axios": "^1.13.6", - "cors": "^2.8.5", + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-rate-limit": "^8.3.1", + "helmet": "^8.1.0", + "joi": "^18.1.1", + "jsonwebtoken": "^9.0.3", "mongoose": "^8.10.0" }, "devDependencies": { "nodemon": "^3.1.14" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.4.6", "resolved": "https://registry.npmmirror.com/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", @@ -28,6 +81,12 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "http://mirrors.tencentyun.com/npm/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -103,6 +162,15 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "http://mirrors.tencentyun.com/npm/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -175,6 +243,12 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "http://mirrors.tencentyun.com/npm/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", @@ -288,7 +362,7 @@ }, "node_modules/cors": { "version": "2.8.6", - "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "resolved": "http://mirrors.tencentyun.com/npm/cors/-/cors-2.8.6.tgz", "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { @@ -372,6 +446,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "http://mirrors.tencentyun.com/npm/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", @@ -493,6 +576,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "http://mirrors.tencentyun.com/npm/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", @@ -713,6 +814,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "http://mirrors.tencentyun.com/npm/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", @@ -758,6 +868,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "http://mirrors.tencentyun.com/npm/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -813,6 +932,66 @@ "node": ">=0.12.0" } }, + "node_modules/joi": { + "version": "18.1.1", + "resolved": "http://mirrors.tencentyun.com/npm/joi/-/joi-18.1.1.tgz", + "integrity": "sha512-pJkBiPtNo+o0h19LfSvUN46Y5zY+ck99AtHwch9n2HqVLNRgP0ZMyIH8FRMoP+HV8hy/+AG99dXFfwpf83iZfQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "http://mirrors.tencentyun.com/npm/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "http://mirrors.tencentyun.com/npm/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "http://mirrors.tencentyun.com/npm/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmmirror.com/kareem/-/kareem-2.6.3.tgz", @@ -822,6 +1001,48 @@ "node": ">=12.0.0" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "http://mirrors.tencentyun.com/npm/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "http://mirrors.tencentyun.com/npm/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "http://mirrors.tencentyun.com/npm/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "http://mirrors.tencentyun.com/npm/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "http://mirrors.tencentyun.com/npm/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "http://mirrors.tencentyun.com/npm/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "http://mirrors.tencentyun.com/npm/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1279,7 +1500,6 @@ "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" diff --git a/package.json b/package.json index 27e6886..135bda7 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,14 @@ "license": "ISC", "dependencies": { "axios": "^1.13.6", - "cors": "^2.8.5", + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-rate-limit": "^8.3.1", + "helmet": "^8.1.0", + "joi": "^18.1.1", + "jsonwebtoken": "^9.0.3", "mongoose": "^8.10.0" }, "devDependencies": { diff --git a/seed-full-data.js b/seed-full-data.js new file mode 100644 index 0000000..998b9c7 --- /dev/null +++ b/seed-full-data.js @@ -0,0 +1,269 @@ +/** + * 完整测试数据填充脚本 + * 用法: node seed-full-data.js + */ +const mongoose = require('mongoose'); + +const MONGO_URI = 'mongodb://localhost:27017/e-scooter-rental'; + +// 加载所有模型 +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'); +const Complaint = require('./server/models/Complaint'); +const Application = require('./server/models/Application'); +const Dispute = require('./server/models/Dispute'); + +const COLLECTIONS = [Store, Vehicle, Customer, Order, Payment, Complaint, Application, Dispute]; + +async function clearAll() { + console.log('🗑️ 清空所有集合...'); + for (const Model of COLLECTIONS) { + await Model.deleteMany({}); + } + console.log(' 所有集合已清空\n'); +} + +async function seedStores() { + console.log('📍 插入门店数据...'); + const stores = [ + { storeId: 'STORE001', name: '朝阳区总店', address: '北京市朝阳区建国路88号', phone: '010-12345678', manager: '王店长', status: '营业中', approvalStatus: '已通过' }, + { storeId: 'STORE002', name: '海淀区中关村店', address: '北京市海淀区中关村大街12号', phone: '010-23456789', manager: '李店长', status: '营业中', approvalStatus: '已通过' }, + { storeId: 'STORE003', name: '西城区金融街店', address: '北京市西城区金融大街28号', phone: '010-34567890', manager: '赵店长', status: '营业中', approvalStatus: '已通过' }, + { storeId: 'STORE004', name: '东城区王府井店', address: '北京市东城区王府井大街66号', phone: '010-45678901', manager: '孙店长', status: '装修中', approvalStatus: '已通过' }, + { storeId: 'STORE005', name: '丰台区南站店', address: '北京市丰台区南站西路88号', phone: '010-56789012', manager: '周店长', status: '营业中', approvalStatus: '已通过' }, + ]; + const result = await Store.insertMany(stores); + console.log(` 插入 ${result.length} 条门店记录`); + return result; +} + +async function seedVehicles(stores) { + console.log('🚗 插入车辆数据...'); + const storeIds = stores.map(s => s.storeId); + const vehicles = [ + { vehicleId: 'VEH0001', storeId: storeIds[0], frameNumber: 'FN0001', plateNumber: '京A12345', brand: '雅迪', vehicleType: 'DT3', color: '黑色', batteryType: '锂电池', batteryCapacity: 48, batteryStatus: '正常', status: '空闲', isRented: false, purchaseDate: new Date('2023-06-15'), purchasePrice: 3999, purchaseSupplier: '雅迪集团' }, + { vehicleId: 'VEH0002', storeId: storeIds[0], frameNumber: 'FN0002', plateNumber: '京A12346', brand: '爱玛', vehicleType: 'TDN3', color: '白色', batteryType: '铅酸电池', batteryCapacity: 60, batteryStatus: '正常', status: '在租', isRented: true, purchaseDate: new Date('2023-07-20'), purchasePrice: 3599, purchaseSupplier: '爱玛科技' }, + { vehicleId: 'VEH0003', storeId: storeIds[1], frameNumber: 'FN0003', plateNumber: '京B23456', brand: '台铃', vehicleType: 'TL5', color: '银色', batteryType: '锂电池', batteryCapacity: 48, batteryStatus: '正常', status: '空闲', isRented: false, purchaseDate: new Date('2023-08-10'), purchasePrice: 4200, purchaseSupplier: '台铃集团' }, + { vehicleId: 'VEH0004', storeId: storeIds[1], frameNumber: 'FN0004', plateNumber: '京B23457', brand: '小牛', vehicleType: 'MQi2', color: '红色', batteryType: '锂电池', batteryCapacity: 48, batteryStatus: '老化', status: '维修中', isRented: false, purchaseDate: new Date('2023-09-01'), purchasePrice: 4999, purchaseSupplier: '小牛电动' }, + { vehicleId: 'VEH0005', storeId: storeIds[2], frameNumber: 'FN0005', plateNumber: '京C34567', brand: '雅迪', vehicleType: 'DT5', color: '蓝色', batteryType: '锂电池', batteryCapacity: 52, batteryStatus: '正常', status: '空闲', isRented: false, purchaseDate: new Date('2023-10-15'), purchasePrice: 4299, purchaseSupplier: '雅迪集团' }, + { vehicleId: 'VEH0006', storeId: storeIds[2], frameNumber: 'FN0006', plateNumber: '京C34568', brand: '绿源', vehicleType: 'LY6', color: '绿色', batteryType: '铅酸电池', batteryCapacity: 60, batteryStatus: '正常', status: '在租', isRented: true, purchaseDate: new Date('2023-11-01'), purchasePrice: 3699, purchaseSupplier: '绿源电动车' }, + { vehicleId: 'VEH0007', storeId: storeIds[3], frameNumber: 'FN0007', plateNumber: '京D45678', brand: '爱玛', vehicleType: 'TDN5', color: '黑色', batteryType: '锂电池', batteryCapacity: 48, batteryStatus: '正常', status: '空闲', isRented: false, purchaseDate: new Date('2024-01-10'), purchasePrice: 3899, purchaseSupplier: '爱玛科技' }, + { vehicleId: 'VEH0008', storeId: storeIds[3], frameNumber: 'FN0008', plateNumber: '京D45679', brand: '台铃', vehicleType: 'TL3', color: '白色', batteryType: '铅酸电池', batteryCapacity: 60, batteryStatus: '待更换', status: '待回收', isRented: false, purchaseDate: new Date('2023-05-20'), purchasePrice: 3299, purchaseSupplier: '台铃集团' }, + { vehicleId: 'VEH0009', storeId: storeIds[4], frameNumber: 'FN0009', plateNumber: '京E56789', brand: '小牛', vehicleType: 'MQi3', color: '灰色', batteryType: '锂电池', batteryCapacity: 52, batteryStatus: '正常', status: '空闲', isRented: false, purchaseDate: new Date('2024-02-15'), purchasePrice: 5199, purchaseSupplier: '小牛电动' }, + { vehicleId: 'VEH0010', storeId: storeIds[4], frameNumber: 'FN0010', plateNumber: '京E56790', brand: '雅迪', vehicleType: 'DT3', color: '红色', batteryType: '锂电池', batteryCapacity: 48, batteryStatus: '正常', status: '空闲', isRented: false, purchaseDate: new Date('2024-03-01'), purchasePrice: 3999, purchaseSupplier: '雅迪集团' }, + ]; + const result = await Vehicle.insertMany(vehicles); + console.log(` 插入 ${result.length} 条车辆记录`); + return result; +} + +async function seedCustomers() { + console.log('👤 插入客户数据...'); + const customers = [ + { customerId: 'CUST001', name: '张三', phone: '13800138001', idCard: '110101199001011234', address: '北京市朝阳区', email: 'zhangsan@email.com', creditScore: 95, creditLevel: '优秀', accountStatus: '正常' }, + { customerId: 'CUST002', name: '李四', phone: '13800138002', idCard: '110102199203122345', address: '北京市海淀区', email: 'lisi@email.com', creditScore: 88, creditLevel: '良好', accountStatus: '正常' }, + { customerId: 'CUST003', name: '王五', phone: '13800138003', idCard: '110105198805051234', address: '北京市西城区', email: 'wangwu@email.com', creditScore: 92, creditLevel: '优秀', accountStatus: '正常' }, + { customerId: 'CUST004', name: '赵六', phone: '13800138004', idCard: '110106199107081234', address: '北京市东城区', email: 'zhaoliu@email.com', creditScore: 75, creditLevel: '一般', accountStatus: '正常' }, + { customerId: 'CUST005', name: '孙七', phone: '13800138005', idCard: '110107199306151234', address: '北京市丰台区', email: 'sunqi@email.com', creditScore: 80, creditLevel: '良好', accountStatus: '正常' }, + { customerId: 'CUST006', name: '周八', phone: '13800138006', idCard: '110108199501201234', address: '北京市朝阳区', email: 'zhouba@email.com', creditScore: 90, creditLevel: '优秀', accountStatus: '正常' }, + { customerId: 'CUST007', name: '吴九', phone: '13800138007', idCard: '110109198912301234', address: '北京市海淀区', email: 'wujiu@email.com', creditScore: 60, creditLevel: '较差', accountStatus: '冻结' }, + { customerId: 'CUST008', name: '郑十', phone: '13800138008', idCard: '110111199708101234', address: '北京市通州区', email: 'zhengshi@email.com', creditScore: 85, creditLevel: '良好', accountStatus: '正常' }, + ]; + const result = await Customer.insertMany(customers); + console.log(` 插入 ${result.length} 条客户记录`); + return result; +} + +async function seedOrders(customers, vehicles, stores) { + console.log('📋 插入订单数据...'); + // Order 使用 customer 和 vehicle 的 ObjectId + // 注意: insertMany 不会触发 pre-save hook,需要手动生成 orderNumber + const ordersData = [ + { + orderNumber: 'ORD202603200001', + customer: customers[0]._id, + vehicle: vehicles[1]._id, // VEH0002 在租 + startDate: new Date('2026-03-20'), + endDate: new Date('2026-03-27'), + status: '进行中', + rentalFee: 50, + deposit: 200, + totalAmount: 550, + paidAmount: 200, + paymentMethod: '微信', + }, + { + orderNumber: 'ORD202603180002', + customer: customers[1]._id, + vehicle: vehicles[5]._id, // VEH0006 在租 + startDate: new Date('2026-03-18'), + endDate: new Date('2026-03-28'), + status: '进行中', + rentalFee: 45, + deposit: 200, + totalAmount: 650, + paidAmount: 200, + paymentMethod: '支付宝', + }, + { + orderNumber: 'ORD202603100002', + customer: customers[2]._id, + vehicle: vehicles[0]._id, // VEH0001 已完成 + startDate: new Date('2026-03-10'), + endDate: new Date('2026-03-15'), + actualEndDate: new Date('2026-03-15'), + status: '已完成', + rentalFee: 50, + deposit: 200, + totalAmount: 450, + paidAmount: 450, + paymentMethod: '微信', + }, + { + orderNumber: 'ORD202603050003', + customer: customers[3]._id, + vehicle: vehicles[2]._id, + startDate: new Date('2026-03-05'), + endDate: new Date('2026-03-10'), + actualEndDate: new Date('2026-03-12'), // 逾期2天 + status: '逾期', + rentalFee: 50, + deposit: 200, + totalAmount: 600, + paidAmount: 200, + paymentMethod: '银行卡', + overdueDays: 2, + overdueFee: 50, + }, + { + orderNumber: 'ORD202603010004', + customer: customers[4]._id, + vehicle: vehicles[6]._id, + startDate: new Date('2026-03-01'), + endDate: new Date('2026-03-03'), + actualEndDate: new Date('2026-03-03'), + status: '已完成', + rentalFee: 40, + deposit: 200, + totalAmount: 280, + paidAmount: 280, + paymentMethod: '现金', + }, + ]; + const result = await Order.insertMany(ordersData); + console.log(` 插入 ${result.length} 条订单记录`); + return result; +} + +async function seedPayments(orders) { + console.log('💰 插入财务数据...'); + const payments = [ + { paymentId: 'PAY001', type: '收入', party: '张三', amount: 200, method: '微信', category: '其他', remark: '租车押金收取', createdAt: new Date('2026-03-20') }, + { paymentId: 'PAY002', type: '收入', party: '张三', amount: 550, method: '微信', category: '租金收入', remark: '租车7天租金', createdAt: new Date('2026-03-27') }, + { paymentId: 'PAY003', type: '收入', party: '李四', amount: 200, method: '支付宝', category: '其他', remark: '租车押金收取', createdAt: new Date('2026-03-18') }, + { paymentId: 'PAY004', type: '收入', party: '王五', amount: 450, method: '微信', category: '租金收入', remark: '租车5天完成', createdAt: new Date('2026-03-15') }, + { paymentId: 'PAY005', type: '支出', party: '张客户', amount: 200, method: '微信', category: '押金退还', remark: '王五订单押金退还', createdAt: new Date('2026-03-15') }, + { paymentId: 'PAY006', type: '支出', party: '维修员老李', amount: 300, method: '现金', category: '工资', remark: '3月维修工费', createdAt: new Date('2026-03-20') }, + { paymentId: 'PAY007', type: '支出', party: '房东王先生', amount: 5000, method: '银行卡', category: '房租', remark: '3月门店房租', createdAt: new Date('2026-03-01') }, + { paymentId: 'PAY008', type: '收入', party: '赵六', amount: 200, method: '银行卡', category: '其他', remark: '租车押金收取', createdAt: new Date('2026-03-05') }, + ]; + const result = await Payment.insertMany(payments); + console.log(` 插入 ${result.length} 条财务记录`); + return result; +} + +async function seedComplaints(customers, orders) { + console.log('📝 插入投诉数据...'); + const complaints = [ + { 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: '待处理', handler: '李店长' }, + { complaintId: 'COMP003', customer: customers[3]._id, order: orders[3]._id, type: '费用问题', content: '认为逾期费用计算不合理', status: '已解决', handler: '赵店长', response: '已减免逾期费用50元' }, + ]; + const result = await Complaint.insertMany(complaints); + console.log(` 插入 ${result.length} 条投诉记录`); + return result; +} + +async function seedApplications(stores) { + console.log('📄 插入申请数据...'); + const applications = [ + { appId: 'APP001', store: stores[0]._id, storeName: '朝阳区总店', type: '促销活动', title: '五一假期租车优惠活动', content: '申请在五一假期期间推出租车8折优惠活动', status: '待审批' }, + { appId: 'APP002', store: stores[1]._id, storeName: '海淀区中关村店', type: '设备申请', title: '新增10辆电动车', content: '申请采购10辆雅迪DT3型号电动车用于扩充库存', status: '已通过', handler: '总部审批员' }, + { appId: 'APP003', store: stores[3]._id, storeName: '东城区王府井店', type: '注册申请', title: '门店重装开业申请', content: '门店装修完成,申请重新开业', status: '已拒绝', rejectReason: '消防验收未通过' }, + ]; + const result = await Application.insertMany(applications); + console.log(` 插入 ${result.length} 条申请记录`); + return result; +} + +async function seedDisputes(stores) { + console.log('⚖️ 插入争议数据...'); + const disputes = [ + { + disputeId: 'DIS001', + storeA: stores[0]._id, + storeAName: '朝阳区总店', + storeB: stores[1]._id, + storeBName: '海淀区中关村店', + type: '区域纠纷', + title: '客户跨区还车纠纷', + content: '客户在A店租车后跨区还至B店,产生区域管理归属争议', + status: '待处理', + }, + { + disputeId: 'DIS002', + storeA: stores[2]._id, + storeAName: '西城区金融街店', + storeB: stores[4]._id, + storeBName: '丰台区南站店', + type: '费用纠纷', + title: '订单费用分成争议', + content: '跨店订单的费用收入分成存在分歧', + status: '处理中', + handler: '总部调解员', + }, + ]; + const result = await Dispute.insertMany(disputes); + console.log(` 插入 ${result.length} 条争议记录`); + return result; +} + +async function main() { + try { + console.log('🚀 连接 MongoDB...\n'); + await mongoose.connect(MONGO_URI); + console.log(` 已连接: ${MONGO_URI}\n`); + + await clearAll(); + + const stores = await seedStores(); + const vehicles = await seedVehicles(stores); + const customers = await seedCustomers(); + const orders = await seedOrders(customers, vehicles, stores); + await seedPayments(orders); + await seedComplaints(customers, orders); + await seedApplications(stores); + await seedDisputes(stores); + + console.log('\n✅ 数据填充完成!\n'); + console.log('📊 各集合数据条数:'); + + for (const Model of COLLECTIONS) { + const count = await Model.countDocuments(); + console.log(` ${Model.modelName}: ${count} 条`); + } + + console.log('\n🕐 关闭连接...\n'); + await mongoose.disconnect(); + console.log('👋 完成!'); + } catch (err) { + console.error('❌ 错误:', err.message); + await mongoose.disconnect(); + process.exit(1); + } +} + +main(); diff --git a/server/index.js b/server/index.js index 35bb440..122caa5 100644 --- a/server/index.js +++ b/server/index.js @@ -1,21 +1,56 @@ +require('dotenv').config(); const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); -require('dotenv').config(); +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); const app = express(); const PORT = process.env.PORT || 3000; -// 中间件 -app.use(cors()); -app.use(express.json()); +// ─── 安全中间件 ────────────────────────────────────────────── -// 连接 MongoDB +// helmet: 安全响应头 +app.use(helmet()); + +// CORS: 白名单域名 +const allowedOrigins = (process.env.ALLOWED_ORIGINS || 'http://localhost:5173') + .split(',') + .map((o) => o.trim()); +app.use(cors({ + origin: allowedOrigins, + credentials: true +})); + +// 请求体大小限制(防止大body攻击) +app.use(express.json({ limit: '10kb' })); +app.use(express.urlencoded({ extended: true, limit: '10kb' })); + +// 全局限流(所有 API) +const globalLimiter = rateLimit({ + windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, + max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100, + standardHeaders: true, + legacyHeaders: false, + message: { success: false, message: '请求过于频繁,请稍后再试' } +}); +app.use('/api', globalLimiter); + +// 登录接口更严格的限流(单独限流器) +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 20, + standardHeaders: true, + legacyHeaders: false, + message: { success: false, message: '登录尝试过于频繁,请15分钟后再试' } +}); + +// ─── 连接 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')); @@ -30,12 +65,12 @@ 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 处理 +// ─── 404 处理 ─────────────────────────────────────────────── app.use((req, res) => { res.status(404).json({ success: false, @@ -44,11 +79,11 @@ app.use((req, res) => { }); }); -// 错误处理中间件 +// ─── 错误处理中间件 ───────────────────────────────────────── const errorHandler = require('./middleware/errorHandler'); app.use(errorHandler); -// 启动服务器 +// ─── 启动 ─────────────────────────────────────────────────── app.listen(PORT, () => { console.log(`🚀 服务器运行在 http://localhost:${PORT}`); }); diff --git a/server/middleware/auth.js b/server/middleware/auth.js new file mode 100644 index 0000000..5da432f --- /dev/null +++ b/server/middleware/auth.js @@ -0,0 +1,34 @@ +const jwt = require('jsonwebtoken'); + +/** + * JWT 鉴权中间件 + * 验证请求头中的 Bearer token,写入 req.user + */ +const authMiddleware = (req, res, next) => { + const token = req.headers.authorization?.replace('Bearer ', ''); + if (!token) { + return res.status(401).json({ success: false, message: '未登录' }); + } + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; + next(); + } catch (err) { + return res.status(401).json({ success: false, message: 'token无效或已过期' }); + } +}; + +/** + * 角色鉴权中间件工厂 + * 用法: requireRole('admin', 'store') + */ +const requireRole = (...roles) => { + return (req, res, next) => { + if (!roles.includes(req.user?.role)) { + return res.status(403).json({ success: false, message: '权限不足' }); + } + next(); + }; +}; + +module.exports = { authMiddleware, requireRole }; diff --git a/server/middleware/validate.js b/server/middleware/validate.js new file mode 100644 index 0000000..8880d07 --- /dev/null +++ b/server/middleware/validate.js @@ -0,0 +1,75 @@ +const Joi = require('joi'); + +/** + * Joi 输入校验中间件 + */ +const validate = (schema) => { + return (req, res, next) => { + const { error } = schema.validate(req.body, { abortEarly: true }); + if (error) { + return res.status(400).json({ + success: false, + message: '输入校验失败: ' + error.details[0].message + }); + } + next(); + }; +}; + +module.exports = { validate }; + +// ─── 常用校验 Schema ─────────────────────────────────────────── + +const schemas = { + // 骑手登录 + login: Joi.object({ + phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required(), + password: Joi.string().min(6).max(20).required() + }), + + // 车辆创建/更新 + vehicle: Joi.object({ + storeId: Joi.string().allow(''), + frameNumber: Joi.string().allow(''), + plateNumber: Joi.string().allow(''), + brand: Joi.string().allow(''), + vehicleType: Joi.string().allow(''), + color: Joi.string().allow(''), + batteryType: Joi.string().allow(''), + batteryCapacity: Joi.number().min(0).max(200), + batteryStatus: Joi.string().valid('正常', '老化', '待更换'), + status: Joi.string().valid('空闲', '在租', '维修中', '已报废', '待回收') + }), + + // 订单 + order: Joi.object({ + customer: Joi.string().allow(''), + vehicle: Joi.string().allow(''), + contractMonths: Joi.number().min(0).allow(''), + startDate: Joi.date().allow(''), + endDate: Joi.date().allow(''), + rentalFee: Joi.number().min(0).allow(''), + deposit: Joi.number().min(0).allow(''), + orderType: Joi.string().allow('') + }), + + // 客户 + customer: Joi.object({ + name: Joi.string().min(1).max(50).required(), + phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required(), + idCard: Joi.string().allow(''), + address: Joi.string().allow(''), + email: Joi.string().email().allow('') + }), + + // 门店 + store: Joi.object({ + name: Joi.string().min(1).max(100).required(), + address: Joi.string().allow(''), + phone: Joi.string().allow(''), + manager: Joi.string().allow(''), + status: Joi.string().valid('营业中', '休息中', '已关闭') + }) +}; + +module.exports.schemas = schemas; diff --git a/server/models/Rider.js b/server/models/Rider.js index 9fb67b7..7af95ac 100644 --- a/server/models/Rider.js +++ b/server/models/Rider.js @@ -5,7 +5,12 @@ 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 }, // 密码(简单位符串存储) + + // 密码(bcrypt 哈希存储,查询时默认不返回) + password: { type: String, required: true, select: false }, + + // 角色(customer=普通用户, admin=管理员, store=门店管理员) + role: { type: String, enum: ['customer', 'admin', 'store'], default: 'customer' }, // 接单状态 status: { @@ -24,7 +29,7 @@ const riderSchema = new mongoose.Schema({ }); // 生成骑手编号 -riderSchema.pre('save', function(next) { +riderSchema.pre('save', function (next) { if (!this.riderId) { const date = new Date(); const year = date.getFullYear(); diff --git a/server/routes/customers.js b/server/routes/customers.js index ff5fac2..abf2792 100644 --- a/server/routes/customers.js +++ b/server/routes/customers.js @@ -1,9 +1,12 @@ const express = require('express'); const router = express.Router(); const Customer = require('../models/Customer'); +const { authMiddleware, requireRole } = require('../middleware/auth'); +const { validate } = require('../middleware/validate'); +const { schemas } = require('../middleware/validate'); -// 获取所有客户 -router.get('/', async (req, res) => { +// 获取所有客户(admin 或 store 可查) +router.get('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const customers = await Customer.find(); res.json({ success: true, data: customers }); @@ -13,20 +16,18 @@ router.get('/', async (req, res) => { }); // 获取单个客户 -router.get('/:id', async (req, res) => { +router.get('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const customer = await Customer.findById(req.params.id); - if (!customer) { - return res.status(404).json({ success: false, message: '客户不存在' }); - } + 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) => { +// 创建客户(admin 或 store) +router.post('/', authMiddleware, requireRole('admin', 'store'), validate(schemas.customer), async (req, res) => { try { const customer = new Customer(req.body); await customer.save(); @@ -37,29 +38,21 @@ router.post('/', async (req, res) => { }); // 更新客户 -router.put('/:id', async (req, res) => { +router.put('/:id', authMiddleware, requireRole('admin', 'store'), 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: '客户不存在' }); - } + 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) => { +// 删除客户(仅 admin) +router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => { try { const customer = await Customer.findByIdAndDelete(req.params.id); - if (!customer) { - return res.status(404).json({ success: false, message: '客户不存在' }); - } + 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 }); @@ -67,7 +60,7 @@ router.delete('/:id', async (req, res) => { }); // 搜索客户 -router.get('/search/:keyword', async (req, res) => { +router.get('/search/:keyword', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const keyword = req.params.keyword; const customers = await Customer.find({ @@ -83,8 +76,8 @@ router.get('/search/:keyword', async (req, res) => { } }); -// 更新客户信用评分 -router.patch('/:id/credit', async (req, res) => { +// 更新客户信用评分(admin 或 store) +router.patch('/:id/credit', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const { creditScore } = req.body; let creditLevel = '优秀'; @@ -97,9 +90,7 @@ router.patch('/:id/credit', async (req, res) => { { creditScore, creditLevel }, { new: true } ); - if (!customer) { - return res.status(404).json({ success: false, message: '客户不存在' }); - } + 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 }); diff --git a/server/routes/finance.js b/server/routes/finance.js index 0a07cd4..e95d9f2 100644 --- a/server/routes/finance.js +++ b/server/routes/finance.js @@ -1,12 +1,12 @@ const express = require('express'); const router = express.Router(); const Payment = require('../models/Payment'); +const { authMiddleware, requireRole } = require('../middleware/auth'); -// 获取收支明细列表 -router.get('/', async (req, res) => { +// 获取收支明细(仅 admin) +router.get('/', authMiddleware, requireRole('admin'), 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'); @@ -16,25 +16,21 @@ router.get('/', async (req, res) => { if (endDate) filter.createdAt.$lte = new Date(endDate); } - const payments = await Payment.find(filter) - .sort('-createdAt') - .limit(100); + 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; + const incomeMap = {}; + const expenseMap = {}; + + const allPayments = await Payment.find(filter); + for (const p of allPayments) { + if (['income', '收入', '租金收入', '押金退还', '租金'].includes(p.type)) { + totalIncome += p.amount; + incomeMap[p.category || '其他'] = (incomeMap[p.category || '其他'] || 0) + p.amount; } else { - totalExpense += item.total; + totalExpense += p.amount; + expenseMap[p.category || '其他'] = (expenseMap[p.category || '其他'] || 0) + p.amount; } } @@ -43,10 +39,12 @@ router.get('/', async (req, res) => { data: { list: payments, summary: { - totalIncome: totalIncome, - totalExpense: totalExpense, + totalIncome, + totalExpense, balance: totalIncome - totalExpense - } + }, + incomeByCategory: incomeMap, + expenseByCategory: expenseMap } }); } catch (error) { @@ -54,8 +52,8 @@ router.get('/', async (req, res) => { } }); -// 创建收支记录 -router.post('/', async (req, res) => { +// 创建收支记录(仅 admin) +router.post('/', authMiddleware, requireRole('admin'), async (req, res) => { try { const payment = new Payment(req.body); await payment.save(); @@ -65,8 +63,8 @@ router.post('/', async (req, res) => { } }); -// 更新收支记录 -router.put('/:id', async (req, res) => { +// 更新收支记录(仅 admin) +router.put('/:id', authMiddleware, requireRole('admin'), async (req, res) => { try { const payment = await Payment.findByIdAndUpdate(req.params.id, req.body, { new: true }); res.json({ success: true, data: payment }); @@ -75,8 +73,8 @@ router.put('/:id', async (req, res) => { } }); -// 删除收支记录 -router.delete('/:id', async (req, res) => { +// 删除收支记录(仅 admin) +router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => { try { await Payment.findByIdAndDelete(req.params.id); res.json({ success: true }); @@ -85,8 +83,8 @@ router.delete('/:id', async (req, res) => { } }); -// 清空所有收支记录 -router.post('/delete-all', async (req, res) => { +// 清空所有收支记录(仅 admin) +router.post('/delete-all', authMiddleware, requireRole('admin'), async (req, res) => { try { await Payment.deleteMany({}); res.json({ success: true, message: 'All payments deleted' }); @@ -95,64 +93,51 @@ router.post('/delete-all', async (req, res) => { } }); -// 获取收支统计 -router.get('/stats', async (req, res) => { +// 收支统计(仅 admin) +router.get('/stats', authMiddleware, requireRole('admin'), 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); + 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([ + const incomeAgg = await Payment.aggregate([ { $match: { type: '收入', createdAt: { $gte: startDate } } }, { $group: { _id: null, total: { $sum: '$amount' } } } ]); - - const expense = await Payment.aggregate([ + const expenseAgg = 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' } } } ]); + const totalIncome = incomeAgg[0]?.total || 0; + const totalExpense = expenseAgg[0]?.total || 0; + res.json({ success: true, data: { - income: income[0]?.total || 0, - expense: expense[0]?.total || 0, - balance: (income[0]?.total || 0) - (expense[0]?.total || 0), + income: totalIncome, + expense: totalExpense, + balance: totalIncome - totalExpense, incomeByCategory: incomeByCategory.reduce((acc, item) => { - acc[item._id || '其他'] = item.total; - return acc; + acc[item._id || '其他'] = item.total; return acc; }, {}), expenseByCategory: expenseByCategory.reduce((acc, item) => { - acc[item._id || '其他'] = item.total; - return acc; + acc[item._id || '其他'] = item.total; return acc; }, {}) } }); diff --git a/server/routes/orders.js b/server/routes/orders.js index 0e87b0d..05fa73d 100644 --- a/server/routes/orders.js +++ b/server/routes/orders.js @@ -3,11 +3,25 @@ const router = express.Router(); const Order = require('../models/Order'); const Vehicle = require('../models/Vehicle'); const Customer = require('../models/Customer'); +const { authMiddleware, requireRole } = require('../middleware/auth'); +const { validate } = require('../middleware/validate'); +const { schemas } = require('../middleware/validate'); -// 获取所有订单 -router.get('/', async (req, res) => { +// ─── 公开列表查询(登录即可,admin 可看全部,customer 只能看自己的) ─── +router.get('/', authMiddleware, async (req, res) => { try { - const orders = await Order.find() + const filter = {}; + if (req.query.status) filter.status = req.query.status; + // 非 admin 角色只能看自己的订单 + if (req.user.role === 'customer') { + // 查找当前 rider 对应的客户手机号(需关联 Customer 表) + // 此处通过 rider id 查找(假设订单中 rider 字段关联骑手) + filter.rider = req.user.id; + } else if (req.user.role === 'store') { + // store 角色可按门店过滤(前端传 storeId) + if (req.query.storeId) filter.storeId = req.query.storeId; + } + const orders = await Order.find(filter) .populate('customer', 'name phone') .populate('vehicle', 'model vehicleId'); res.json({ success: true, data: orders }); @@ -16,14 +30,18 @@ router.get('/', async (req, res) => { } }); -// 获取单个订单 -router.get('/:id', async (req, res) => { +// 获取单个订单(登录即可,检查是否本人或 admin/store) +router.get('/:id', authMiddleware, 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: '订单不存在' }); + if (!order) return res.status(404).json({ success: false, message: '订单不存在' }); + + // 权限检查:本人骑手 或 admin/store + const isOwner = order.rider?.toString() === req.user.id; + if (req.user.role !== 'admin' && req.user.role !== 'store' && !isOwner) { + return res.status(403).json({ success: false, message: '权限不足' }); } res.json({ success: true, data: order }); } catch (error) { @@ -31,25 +49,22 @@ router.get('/:id', async (req, res) => { } }); -// 创建订单(支持门店端新客创建订单) -router.post('/', async (req, res) => { +// 创建订单(store 或 admin) +router.post('/', authMiddleware, requireRole('admin', 'store'), validate(schemas.order), async (req, res) => { try { const { - customerName, customer, // customerName=新客姓名(字符串),customer=已有客户ID - vehicleId, vehicle, // vehicleId=前端字段名,vehicle=后端字段名 + customerName, customer, + 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', @@ -59,32 +74,24 @@ router.post('/', async (req, res) => { customerObjectId = customerDoc._id; } - // 检查车辆是否存在 const vehicleDoc = await Vehicle.findById(vehicleObjectId); - if (!vehicleDoc) { - return res.status(404).json({ success: false, message: '车辆不存在' }); - } + 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; - + const 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 fee = Number(rentalFee) || 0; const depositAmt = Number(deposit) || 0; const totalAmount = fee + depositAmt; - // 创建订单 const order = new Order({ customer: customerObjectId, vehicle: vehicleObjectId, @@ -98,13 +105,11 @@ router.post('/', async (req, res) => { 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) { @@ -121,36 +126,27 @@ router.post('/', async (req, res) => { } }); -// 更新订单 -router.put('/:id', async (req, res) => { +// 更新订单(admin 或 store) +router.put('/:id', authMiddleware, requireRole('admin', 'store'), 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: '订单不存在' }); - } + 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) => { +// 结束订单(admin 或 store) +router.patch('/:id/complete', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const order = await Order.findById(req.params.id); - if (!order) { - return res.status(404).json({ success: false, message: '订单不存在' }); - } + 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 = '空闲'; @@ -160,7 +156,6 @@ router.patch('/:id/complete', async (req, res) => { await vehicle.save(); } - // 更新客户信息 const customer = await Customer.findById(order.customer); if (customer) { customer.currentRentals -= 1; @@ -173,13 +168,11 @@ router.patch('/:id/complete', async (req, res) => { } }); -// 取消订单 -router.patch('/:id/cancel', async (req, res) => { +// 取消订单(admin 或 store) +router.patch('/:id/cancel', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const order = await Order.findById(req.params.id); - if (!order) { - return res.status(404).json({ success: false, message: '订单不存在' }); - } + if (!order) return res.status(404).json({ success: false, message: '订单不存在' }); if (order.status === '已完成' || order.status === '已取消') { return res.status(400).json({ success: false, message: '当前状态无法取消' }); } @@ -188,7 +181,6 @@ router.patch('/:id/cancel', async (req, res) => { order.actualEndDate = new Date(); await order.save(); - // 更新车辆状态 const vehicle = await Vehicle.findById(order.vehicle); if (vehicle) { vehicle.status = '空闲'; @@ -197,7 +189,6 @@ router.patch('/:id/cancel', async (req, res) => { await vehicle.save(); } - // 更新客户信息 const customer = await Customer.findById(order.customer); if (customer) { customer.currentRentals = Math.max((customer.currentRentals || 0) - 1, 0); @@ -210,8 +201,8 @@ router.patch('/:id/cancel', async (req, res) => { } }); -// 获取逾期订单 -router.get('/status/overdue', async (req, res) => { +// 逾期订单(admin 或 store) +router.get('/status/overdue', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const now = new Date(); const orders = await Order.find({ @@ -221,23 +212,21 @@ router.get('/status/overdue', async (req, res) => { .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.overdueFee = overdueDays * order.rentalFee * 0.1; 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) => { +// 按状态筛选(admin 或 store) +router.get('/status/:status', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const orders = await Order.find({ status: req.params.status }) .populate('customer', 'name phone') diff --git a/server/routes/riders.js b/server/routes/riders.js index 5b6df20..e68b237 100644 --- a/server/routes/riders.js +++ b/server/routes/riders.js @@ -1,23 +1,45 @@ const express = require('express'); const router = express.Router(); +const jwt = require('jsonwebtoken'); +const rateLimit = require('express-rate-limit'); const Rider = require('../models/Rider'); const Order = require('../models/Order'); +const { comparePassword } = require('../utils/password'); +const { validate } = require('../middleware/validate'); +const { schemas } = require('../middleware/validate'); + +// 登录限流(单独,更严格) +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 20, + standardHeaders: true, + legacyHeaders: false, + message: { success: false, message: '登录尝试过于频繁,请15分钟后再试' } +}); // 骑手登录 -router.post('/login', async (req, res) => { +router.post('/login', loginLimiter, validate(schemas.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 }); + // 用 select(false) 主动拉取 password 字段 + const rider = await Rider.findOne({ phone }).select('+password'); if (!rider) { return res.status(401).json({ success: false, message: '手机号或密码错误' }); } - // 简单生成一个token(实际生产应使用 JWT) - const token = Buffer.from(`${rider._id}:${Date.now()}`).toString('base64'); + // bcrypt 比对密码 + const isMatch = await comparePassword(password, rider.password); + if (!isMatch) { + return res.status(401).json({ success: false, message: '手机号或密码错误' }); + } + + // 签发 JWT(包含 id、role) + const token = jwt.sign( + { id: rider._id, role: rider.role, phone: rider.phone }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN || '7d' } + ); res.json({ success: true, @@ -27,6 +49,7 @@ router.post('/login', async (req, res) => { name: rider.name, phone: rider.phone, status: rider.status, + role: rider.role, rating: rider.rating, totalOrders: rider.totalOrders, totalIncome: rider.totalIncome, @@ -38,7 +61,7 @@ router.post('/login', async (req, res) => { } }); -// 获取骑手信息 +// 获取骑手信息(需登录) router.get('/:id', async (req, res) => { try { const rider = await Rider.findById(req.params.id); @@ -51,12 +74,14 @@ router.get('/:id', async (req, res) => { } }); -// 更新骑手信息(如状态切换) +// 更新骑手信息(如状态切换,需登录本人或管理员) router.put('/:id', async (req, res) => { try { + // 不允许通过 PUT 修改密码和 role + const { password, role, ...safeBody } = req.body; const rider = await Rider.findByIdAndUpdate( req.params.id, - req.body, + safeBody, { new: true, runValidators: true } ); if (!rider) { diff --git a/server/routes/stores.js b/server/routes/stores.js index dc1c3e9..4cf6c6b 100644 --- a/server/routes/stores.js +++ b/server/routes/stores.js @@ -1,9 +1,12 @@ const express = require('express'); const router = express.Router(); const Store = require('../models/Store'); +const { authMiddleware, requireRole } = require('../middleware/auth'); +const { validate } = require('../middleware/validate'); +const { schemas } = require('../middleware/validate'); -// 获取所有门店 -router.get('/', async (req, res) => { +// 获取所有门店(登录即可) +router.get('/', authMiddleware, async (req, res) => { try { const stores = await Store.find(); res.json({ success: true, data: stores }); @@ -12,8 +15,8 @@ router.get('/', async (req, res) => { } }); -// 创建门店 -router.post('/', async (req, res) => { +// 创建门店(仅 admin) +router.post('/', authMiddleware, requireRole('admin'), validate(schemas.store), async (req, res) => { try { const store = new Store(req.body); await store.save(); @@ -23,18 +26,19 @@ router.post('/', async (req, res) => { } }); -// 更新门店 -router.put('/:id', async (req, res) => { +// 更新门店(仅 admin) +router.put('/:id', authMiddleware, requireRole('admin'), async (req, res) => { try { const store = await Store.findByIdAndUpdate(req.params.id, req.body, { new: true }); + if (!store) return res.status(404).json({ success: false, message: '门店不存在' }); res.json({ success: true, data: store }); } catch (error) { res.status(400).json({ success: false, message: error.message }); } }); -// 删除门店 -router.delete('/:id', async (req, res) => { +// 删除门店(仅 admin) +router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => { try { await Store.findByIdAndDelete(req.params.id); res.json({ success: true }); diff --git a/server/routes/vehicles.js b/server/routes/vehicles.js index 0d51aea..21f9e66 100644 --- a/server/routes/vehicles.js +++ b/server/routes/vehicles.js @@ -1,14 +1,16 @@ const express = require('express'); const router = express.Router(); const Vehicle = require('../models/Vehicle'); +const { authMiddleware, requireRole } = require('../middleware/auth'); +const { validate } = require('../middleware/validate'); +const { schemas } = require('../middleware/validate'); -// 获取所有车辆(支持按 storeId 过滤) -router.get('/', async (req, res) => { +// 获取所有车辆(公开,仅需登录) +router.get('/', authMiddleware, async (req, res) => { try { const filter = {}; - if (req.query.storeId) { - filter.storeId = req.query.storeId; - } + if (req.query.storeId) filter.storeId = req.query.storeId; + if (req.query.status) filter.status = req.query.status; const vehicles = await Vehicle.find(filter).populate('currentOrderId'); res.json({ success: true, data: vehicles }); } catch (error) { @@ -16,27 +18,22 @@ router.get('/', async (req, res) => { } }); -// 获取单个车辆 -router.get('/:id', async (req, res) => { +// 获取单个车辆(公开) +router.get('/:id', authMiddleware, async (req, res) => { try { const vehicle = await Vehicle.findById(req.params.id).populate('currentOrderId'); - if (!vehicle) { - return res.status(404).json({ success: false, message: '车辆不存在' }); - } + 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) => { +// 创建车辆(需 admin 或 store) +router.post('/', authMiddleware, requireRole('admin', 'store'), validate(schemas.vehicle), async (req, res) => { try { const data = { ...req.body }; - // status 为"在租"时自动设置 isRented - if (data.status === '在租') { - data.isRented = true; - } + if (data.status === '在租') data.isRented = true; const vehicle = new Vehicle(data); await vehicle.save(); res.status(201).json({ success: true, data: vehicle }); @@ -45,43 +42,32 @@ router.post('/', async (req, res) => { } }); -// 更新车辆 -router.put('/:id', async (req, res) => { +// 更新车辆(需 admin 或 store) +router.put('/:id', authMiddleware, requireRole('admin', 'store'), validate(schemas.vehicle), 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: '车辆不存在' }); - } + 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) => { +// 删除车辆(需 admin) +router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => { try { const vehicle = await Vehicle.findByIdAndDelete(req.params.id); - if (!vehicle) { - return res.status(404).json({ success: false, message: '车辆不存在' }); - } + 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) => { +// 按状态筛选车辆(公开) +router.get('/status/:status', authMiddleware, async (req, res) => { try { const vehicles = await Vehicle.find({ status: req.params.status }); res.json({ success: true, data: vehicles }); @@ -90,24 +76,19 @@ router.get('/status/:status', async (req, res) => { } }); -// 更新车辆位置 -router.patch('/:id/location', async (req, res) => { +// 更新车辆位置(需 admin 或 store) +router.patch('/:id/location', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const { longitude, latitude } = req.body; const vehicle = await Vehicle.findByIdAndUpdate( req.params.id, { - location: { - type: 'Point', - coordinates: [longitude, latitude] - }, + location: { type: 'Point', coordinates: [longitude, latitude] }, lastLocationUpdate: new Date() }, { new: true } ); - if (!vehicle) { - return res.status(404).json({ success: false, message: '车辆不存在' }); - } + 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 }); diff --git a/server/seed.js b/server/seed.js index 47f41e3..fdc751b 100644 --- a/server/seed.js +++ b/server/seed.js @@ -4,13 +4,12 @@ const Customer = require('./models/Customer'); const Order = require('./models/Order'); const Store = require('./models/Store'); const Rider = require('./models/Rider'); +const { hashPassword } = require('./utils/password'); -// 连接 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({}); @@ -21,222 +20,66 @@ async function clearData() { 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: '爱玛电动车' - } + { 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' - } + { 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: '已完成' - } + { 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: [] - } + { 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() { + // 用 bcrypt 哈希密码 + const hashedPwd = await hashPassword('123456'); const riders = [ { riderId: 'RIDER001', name: '张骑手', phone: '13900139000', - password: '123456', + password: hashedPwd, + role: 'admin', status: 'active', rating: 4.8, totalOrders: 120, @@ -246,7 +89,8 @@ async function createRiders() { riderId: 'RIDER002', name: '李骑手', phone: '13900139001', - password: '123456', + password: hashedPwd, + role: 'store', status: 'active', rating: 4.5, totalOrders: 80, @@ -256,20 +100,19 @@ async function createRiders() { riderId: 'RIDER003', name: '王骑手', phone: '13900139002', - password: '123456', + password: hashedPwd, + role: 'customer', 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(); @@ -279,11 +122,9 @@ async function main() { await createStores(); await createRiders(); console.log('\n🎉 示例数据创建完成!'); - console.log('车辆: 5 辆'); - console.log('客户: 5 个'); - console.log('订单: 3 个'); - console.log('门店: 3 个'); - console.log('骑手: 3 个'); + console.log('车辆: 5 辆 | 客户: 5 个 | 订单: 3 个 | 门店: 3 个 | 骑手: 3 个'); + console.log('骑手密码统一: 123456'); + console.log('RIDER001=admin | RIDER002=store | RIDER003=customer'); } catch (error) { console.error('❌ 创建示例数据失败:', error); } finally { diff --git a/server/utils/password.js b/server/utils/password.js new file mode 100644 index 0000000..8207e7b --- /dev/null +++ b/server/utils/password.js @@ -0,0 +1,17 @@ +const bcrypt = require('bcryptjs'); + +/** + * 密码哈希 + */ +const hashPassword = async (password) => { + return await bcrypt.hash(password, 10); +}; + +/** + * 密码比对 + */ +const comparePassword = async (password, hash) => { + return await bcrypt.compare(password, hash); +}; + +module.exports = { hashPassword, comparePassword };