feat: 补全安全体系 - JWT鉴权、bcrypt密码加密、RBAC权限控制、速率限制、CORS白名单、输入校验

This commit is contained in:
notyclaw 2026-03-27 23:43:42 +08:00
parent 6e1378c812
commit c48e2c2961
16 changed files with 897 additions and 413 deletions

8
.env.example Normal file
View File

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

226
package-lock.json generated
View File

@ -10,15 +10,68 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.13.6", "axios": "^1.13.6",
"cors": "^2.8.5", "bcryptjs": "^3.0.3",
"cors": "^2.8.6",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "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" "mongoose": "^8.10.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.14" "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": { "node_modules/@mongodb-js/saslprep": {
"version": "1.4.6", "version": "1.4.6",
"resolved": "https://registry.npmmirror.com/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", "resolved": "https://registry.npmmirror.com/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
@ -28,6 +81,12 @@
"sparse-bitfield": "^3.0.3" "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": { "node_modules/@types/webidl-conversions": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "resolved": "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
@ -103,6 +162,15 @@
"node": "18 || 20 || >=22" "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": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -175,6 +243,12 @@
"node": ">=16.20.1" "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": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
@ -288,7 +362,7 @@
}, },
"node_modules/cors": { "node_modules/cors": {
"version": "2.8.6", "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==", "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -372,6 +446,15 @@
"node": ">= 0.4" "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": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
@ -493,6 +576,24 @@
"url": "https://opencollective.com/express" "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": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
@ -713,6 +814,15 @@
"node": ">= 0.4" "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": { "node_modules/http-errors": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
@ -758,6 +868,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "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": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -813,6 +932,66 @@
"node": ">=0.12.0" "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": { "node_modules/kareem": {
"version": "2.6.3", "version": "2.6.3",
"resolved": "https://registry.npmmirror.com/kareem/-/kareem-2.6.3.tgz", "resolved": "https://registry.npmmirror.com/kareem/-/kareem-2.6.3.tgz",
@ -822,6 +1001,48 @@
"node": ">=12.0.0" "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": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -1279,7 +1500,6 @@
"version": "7.7.4", "version": "7.7.4",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"

View File

@ -19,9 +19,14 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.13.6", "axios": "^1.13.6",
"cors": "^2.8.5", "bcryptjs": "^3.0.3",
"cors": "^2.8.6",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "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" "mongoose": "^8.10.0"
}, },
"devDependencies": { "devDependencies": {

269
seed-full-data.js Normal file
View File

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

View File

@ -1,21 +1,56 @@
require('dotenv').config();
const express = require('express'); const express = require('express');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const cors = require('cors'); const cors = require('cors');
require('dotenv').config(); const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; 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') mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/e-scooter-rental')
.then(() => console.log('✅ MongoDB 连接成功')) .then(() => console.log('✅ MongoDB 连接成功'))
.catch(err => console.error('❌ MongoDB 连接失败:', err.message)); .catch(err => console.error('❌ MongoDB 连接失败:', err.message));
// 路由 // ─── 路由 ───────────────────────────────────────────────────
app.use('/api/vehicles', require('./routes/vehicles')); app.use('/api/vehicles', require('./routes/vehicles'));
app.use('/api/orders', require('./routes/orders')); app.use('/api/orders', require('./routes/orders'));
app.use('/api/customers', require('./routes/customers')); 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/riders', require('./routes/riders'));
app.use('/api/vehicle-types', require('./routes/vehicleTypes')); app.use('/api/vehicle-types', require('./routes/vehicleTypes'));
// 健康检查 // ─── 健康检查(不限流) ─────────────────────────────────────
app.get('/health', (req, res) => { app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() }); res.json({ status: 'ok', timestamp: new Date().toISOString() });
}); });
// 404 处理 // ─── 404 处理 ───────────────────────────────────────────────
app.use((req, res) => { app.use((req, res) => {
res.status(404).json({ res.status(404).json({
success: false, success: false,
@ -44,11 +79,11 @@ app.use((req, res) => {
}); });
}); });
// 错误处理中间件 // ─── 错误处理中间件 ─────────────────────────────────────────
const errorHandler = require('./middleware/errorHandler'); const errorHandler = require('./middleware/errorHandler');
app.use(errorHandler); app.use(errorHandler);
// 启动服务器 // ─── 启动 ───────────────────────────────────────────────────
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`🚀 服务器运行在 http://localhost:${PORT}`); console.log(`🚀 服务器运行在 http://localhost:${PORT}`);
}); });

34
server/middleware/auth.js Normal file
View File

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

View File

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

View File

@ -5,7 +5,12 @@ const riderSchema = new mongoose.Schema({
riderId: { type: String, required: true, unique: true }, // 骑手编号 riderId: { type: String, required: true, unique: true }, // 骑手编号
name: { type: String, required: true }, // 姓名 name: { type: String, required: true }, // 姓名
phone: { 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: { status: {
@ -24,7 +29,7 @@ const riderSchema = new mongoose.Schema({
}); });
// 生成骑手编号 // 生成骑手编号
riderSchema.pre('save', function(next) { riderSchema.pre('save', function (next) {
if (!this.riderId) { if (!this.riderId) {
const date = new Date(); const date = new Date();
const year = date.getFullYear(); const year = date.getFullYear();

View File

@ -1,9 +1,12 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const Customer = require('../models/Customer'); const Customer = require('../models/Customer');
const { authMiddleware, requireRole } = require('../middleware/auth');
const { validate } = require('../middleware/validate');
const { schemas } = require('../middleware/validate');
// 获取所有客户 // 获取所有客户admin 或 store 可查)
router.get('/', async (req, res) => { router.get('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => {
try { try {
const customers = await Customer.find(); const customers = await Customer.find();
res.json({ success: true, data: customers }); 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 { try {
const customer = await Customer.findById(req.params.id); const customer = await Customer.findById(req.params.id);
if (!customer) { if (!customer) return res.status(404).json({ success: false, message: '客户不存在' });
return res.status(404).json({ success: false, message: '客户不存在' });
}
res.json({ success: true, data: customer }); res.json({ success: true, data: customer });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, message: error.message }); res.status(500).json({ success: false, message: error.message });
} }
}); });
// 创建客户 // 创建客户admin 或 store
router.post('/', async (req, res) => { router.post('/', authMiddleware, requireRole('admin', 'store'), validate(schemas.customer), async (req, res) => {
try { try {
const customer = new Customer(req.body); const customer = new Customer(req.body);
await customer.save(); 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 { try {
const customer = await Customer.findByIdAndUpdate( const customer = await Customer.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
req.params.id, if (!customer) return res.status(404).json({ success: false, message: '客户不存在' });
req.body,
{ new: true, runValidators: true }
);
if (!customer) {
return res.status(404).json({ success: false, message: '客户不存在' });
}
res.json({ success: true, data: customer }); res.json({ success: true, data: customer });
} catch (error) { } catch (error) {
res.status(400).json({ success: false, message: error.message }); res.status(400).json({ success: false, message: error.message });
} }
}); });
// 删除客户 // 删除客户(仅 admin
router.delete('/:id', async (req, res) => { router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => {
try { try {
const customer = await Customer.findByIdAndDelete(req.params.id); const customer = await Customer.findByIdAndDelete(req.params.id);
if (!customer) { if (!customer) return res.status(404).json({ success: false, message: '客户不存在' });
return res.status(404).json({ success: false, message: '客户不存在' });
}
res.json({ success: true, message: '客户已删除' }); res.json({ success: true, message: '客户已删除' });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, message: error.message }); 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 { try {
const keyword = req.params.keyword; const keyword = req.params.keyword;
const customers = await Customer.find({ const customers = await Customer.find({
@ -83,8 +76,8 @@ router.get('/search/:keyword', async (req, res) => {
} }
}); });
// 更新客户信用评分 // 更新客户信用评分admin 或 store
router.patch('/:id/credit', async (req, res) => { router.patch('/:id/credit', authMiddleware, requireRole('admin', 'store'), async (req, res) => {
try { try {
const { creditScore } = req.body; const { creditScore } = req.body;
let creditLevel = '优秀'; let creditLevel = '优秀';
@ -97,9 +90,7 @@ router.patch('/:id/credit', async (req, res) => {
{ creditScore, creditLevel }, { creditScore, creditLevel },
{ new: true } { new: true }
); );
if (!customer) { if (!customer) return res.status(404).json({ success: false, message: '客户不存在' });
return res.status(404).json({ success: false, message: '客户不存在' });
}
res.json({ success: true, data: customer }); res.json({ success: true, data: customer });
} catch (error) { } catch (error) {
res.status(400).json({ success: false, message: error.message }); res.status(400).json({ success: false, message: error.message });

View File

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

View File

@ -3,11 +3,25 @@ const router = express.Router();
const Order = require('../models/Order'); const Order = require('../models/Order');
const Vehicle = require('../models/Vehicle'); const Vehicle = require('../models/Vehicle');
const Customer = require('../models/Customer'); const Customer = require('../models/Customer');
const { authMiddleware, requireRole } = require('../middleware/auth');
const { validate } = require('../middleware/validate');
const { schemas } = require('../middleware/validate');
// 获取所有订单 // ─── 公开列表查询登录即可admin 可看全部customer 只能看自己的) ───
router.get('/', async (req, res) => { router.get('/', authMiddleware, async (req, res) => {
try { 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('customer', 'name phone')
.populate('vehicle', 'model vehicleId'); .populate('vehicle', 'model vehicleId');
res.json({ success: true, data: orders }); res.json({ success: true, data: orders });
@ -16,14 +30,18 @@ router.get('/', async (req, res) => {
} }
}); });
// 获取单个订单 // 获取单个订单(登录即可,检查是否本人或 admin/store
router.get('/:id', async (req, res) => { router.get('/:id', authMiddleware, async (req, res) => {
try { try {
const order = await Order.findById(req.params.id) const order = await Order.findById(req.params.id)
.populate('customer') .populate('customer')
.populate('vehicle'); .populate('vehicle');
if (!order) { if (!order) return res.status(404).json({ success: false, message: '订单不存在' });
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 }); res.json({ success: true, data: order });
} catch (error) { } catch (error) {
@ -31,25 +49,22 @@ router.get('/:id', async (req, res) => {
} }
}); });
// 创建订单(支持门店端新客创建订单 // 创建订单(store 或 admin
router.post('/', async (req, res) => { router.post('/', authMiddleware, requireRole('admin', 'store'), validate(schemas.order), async (req, res) => {
try { try {
const { const {
customerName, customer, // customerName=新客姓名字符串customer=已有客户ID customerName, customer,
vehicleId, vehicle, // vehicleId=前端字段名vehicle=后端字段名 vehicleId, vehicle,
contractMonths, startDate, endDate, rentalFee, deposit, orderType contractMonths, startDate, endDate, rentalFee, deposit, orderType
} = req.body; } = req.body;
// 解析车辆 ID兼容前端 vehicleId 字段名)
const vehicleObjectId = vehicle || vehicleId; const vehicleObjectId = vehicle || vehicleId;
// 解析客户优先用已有客户ID否则用姓名查找或创建新客户 // 解析客户
let customerObjectId = customer; let customerObjectId = customer;
if (!customerObjectId && customerName) { if (!customerObjectId && customerName) {
// 查找同名客户
let customerDoc = await Customer.findOne({ name: customerName }); let customerDoc = await Customer.findOne({ name: customerName });
if (!customerDoc) { if (!customerDoc) {
// 创建新客户phone 必填,设为默认值)
customerDoc = await Customer.create({ customerDoc = await Customer.create({
name: customerName, name: customerName,
phone: '00000000000', phone: '00000000000',
@ -59,32 +74,24 @@ router.post('/', async (req, res) => {
customerObjectId = customerDoc._id; customerObjectId = customerDoc._id;
} }
// 检查车辆是否存在
const vehicleDoc = await Vehicle.findById(vehicleObjectId); const vehicleDoc = await Vehicle.findById(vehicleObjectId);
if (!vehicleDoc) { if (!vehicleDoc) return res.status(404).json({ success: false, message: '车辆不存在' });
return res.status(404).json({ success: false, message: '车辆不存在' });
}
if (vehicleDoc.status !== '空闲') { if (vehicleDoc.status !== '空闲') {
return res.status(400).json({ success: false, message: '车辆不可用,当前状态:' + vehicleDoc.status }); return res.status(400).json({ success: false, message: '车辆不可用,当前状态:' + vehicleDoc.status });
} }
// 日期处理startDate/endDate 或根据 contractMonths 计算
const start = startDate ? new Date(startDate) : new Date(); const start = startDate ? new Date(startDate) : new Date();
let end = endDate ? new Date(endDate) : null; let end = endDate ? new Date(endDate) : null;
let months = Number(contractMonths) || 0; const months = Number(contractMonths) || 0;
if (!end && months > 0) { if (!end && months > 0) {
end = new Date(start); end = new Date(start);
end.setMonth(end.getMonth() + months); end.setMonth(end.getMonth() + months);
} }
// 租金计算:使用传入的 rentalFee若未传则为 0 const fee = Number(rentalFee) || 0;
let fee = Number(rentalFee) || 0;
const depositAmt = Number(deposit) || 0; const depositAmt = Number(deposit) || 0;
const totalAmount = fee + depositAmt; const totalAmount = fee + depositAmt;
// 创建订单
const order = new Order({ const order = new Order({
customer: customerObjectId, customer: customerObjectId,
vehicle: vehicleObjectId, vehicle: vehicleObjectId,
@ -98,13 +105,11 @@ router.post('/', async (req, res) => {
await order.save(); await order.save();
// 更新车辆状态
vehicleDoc.status = '在租'; vehicleDoc.status = '在租';
vehicleDoc.isRented = true; vehicleDoc.isRented = true;
vehicleDoc.currentOrderId = order._id; vehicleDoc.currentOrderId = order._id;
await vehicleDoc.save(); await vehicleDoc.save();
// 更新客户统计(如客户已存在)
if (customerObjectId) { if (customerObjectId) {
const cust = await Customer.findById(customerObjectId); const cust = await Customer.findById(customerObjectId);
if (cust) { if (cust) {
@ -121,36 +126,27 @@ router.post('/', async (req, res) => {
} }
}); });
// 更新订单 // 更新订单admin 或 store
router.put('/:id', async (req, res) => { router.put('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => {
try { try {
const order = await Order.findByIdAndUpdate( const order = await Order.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
req.params.id, if (!order) return res.status(404).json({ success: false, message: '订单不存在' });
req.body,
{ new: true, runValidators: true }
);
if (!order) {
return res.status(404).json({ success: false, message: '订单不存在' });
}
res.json({ success: true, data: order }); res.json({ success: true, data: order });
} catch (error) { } catch (error) {
res.status(400).json({ success: false, message: error.message }); res.status(400).json({ success: false, message: error.message });
} }
}); });
// 结束订单 // 结束订单admin 或 store
router.patch('/:id/complete', async (req, res) => { router.patch('/:id/complete', authMiddleware, requireRole('admin', 'store'), async (req, res) => {
try { try {
const order = await Order.findById(req.params.id); const order = await Order.findById(req.params.id);
if (!order) { if (!order) return res.status(404).json({ success: false, message: '订单不存在' });
return res.status(404).json({ success: false, message: '订单不存在' });
}
order.status = '已完成'; order.status = '已完成';
order.actualEndDate = new Date(); order.actualEndDate = new Date();
await order.save(); await order.save();
// 更新车辆状态
const vehicle = await Vehicle.findById(order.vehicle); const vehicle = await Vehicle.findById(order.vehicle);
if (vehicle) { if (vehicle) {
vehicle.status = '空闲'; vehicle.status = '空闲';
@ -160,7 +156,6 @@ router.patch('/:id/complete', async (req, res) => {
await vehicle.save(); await vehicle.save();
} }
// 更新客户信息
const customer = await Customer.findById(order.customer); const customer = await Customer.findById(order.customer);
if (customer) { if (customer) {
customer.currentRentals -= 1; customer.currentRentals -= 1;
@ -173,13 +168,11 @@ router.patch('/:id/complete', async (req, res) => {
} }
}); });
// 取消订单 // 取消订单admin 或 store
router.patch('/:id/cancel', async (req, res) => { router.patch('/:id/cancel', authMiddleware, requireRole('admin', 'store'), async (req, res) => {
try { try {
const order = await Order.findById(req.params.id); const order = await Order.findById(req.params.id);
if (!order) { if (!order) return res.status(404).json({ success: false, message: '订单不存在' });
return res.status(404).json({ success: false, message: '订单不存在' });
}
if (order.status === '已完成' || order.status === '已取消') { if (order.status === '已完成' || order.status === '已取消') {
return res.status(400).json({ success: false, message: '当前状态无法取消' }); return res.status(400).json({ success: false, message: '当前状态无法取消' });
} }
@ -188,7 +181,6 @@ router.patch('/:id/cancel', async (req, res) => {
order.actualEndDate = new Date(); order.actualEndDate = new Date();
await order.save(); await order.save();
// 更新车辆状态
const vehicle = await Vehicle.findById(order.vehicle); const vehicle = await Vehicle.findById(order.vehicle);
if (vehicle) { if (vehicle) {
vehicle.status = '空闲'; vehicle.status = '空闲';
@ -197,7 +189,6 @@ router.patch('/:id/cancel', async (req, res) => {
await vehicle.save(); await vehicle.save();
} }
// 更新客户信息
const customer = await Customer.findById(order.customer); const customer = await Customer.findById(order.customer);
if (customer) { if (customer) {
customer.currentRentals = Math.max((customer.currentRentals || 0) - 1, 0); customer.currentRentals = Math.max((customer.currentRentals || 0) - 1, 0);
@ -210,8 +201,8 @@ router.patch('/:id/cancel', async (req, res) => {
} }
}); });
// 获取逾期订单 // 逾期订单admin 或 store
router.get('/status/overdue', async (req, res) => { router.get('/status/overdue', authMiddleware, requireRole('admin', 'store'), async (req, res) => {
try { try {
const now = new Date(); const now = new Date();
const orders = await Order.find({ const orders = await Order.find({
@ -221,23 +212,21 @@ router.get('/status/overdue', async (req, res) => {
.populate('customer', 'name phone') .populate('customer', 'name phone')
.populate('vehicle', 'model vehicleId'); .populate('vehicle', 'model vehicleId');
// 更新逾期天数和费用
for (const order of orders) { for (const order of orders) {
const overdueDays = Math.ceil((now - order.endDate) / (1000 * 60 * 60 * 24)); const overdueDays = Math.ceil((now - order.endDate) / (1000 * 60 * 60 * 24));
order.overdueDays = overdueDays; order.overdueDays = overdueDays;
order.overdueFee = overdueDays * order.rentalFee * 0.1; // 10% 滞纳金 order.overdueFee = overdueDays * order.rentalFee * 0.1;
order.status = '逾期'; order.status = '逾期';
await order.save(); await order.save();
} }
res.json({ success: true, data: orders }); res.json({ success: true, data: orders });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, message: error.message }); res.status(500).json({ success: false, message: error.message });
} }
}); });
// 按状态筛选订单 // 按状态筛选admin 或 store
router.get('/status/:status', async (req, res) => { router.get('/status/:status', authMiddleware, requireRole('admin', 'store'), async (req, res) => {
try { try {
const orders = await Order.find({ status: req.params.status }) const orders = await Order.find({ status: req.params.status })
.populate('customer', 'name phone') .populate('customer', 'name phone')

View File

@ -1,23 +1,45 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');
const Rider = require('../models/Rider'); const Rider = require('../models/Rider');
const Order = require('../models/Order'); 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 { try {
const { phone, password } = req.body; 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) { if (!rider) {
return res.status(401).json({ success: false, message: '手机号或密码错误' }); return res.status(401).json({ success: false, message: '手机号或密码错误' });
} }
// 简单生成一个token实际生产应使用 JWT // bcrypt 比对密码
const token = Buffer.from(`${rider._id}:${Date.now()}`).toString('base64'); 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({ res.json({
success: true, success: true,
@ -27,6 +49,7 @@ router.post('/login', async (req, res) => {
name: rider.name, name: rider.name,
phone: rider.phone, phone: rider.phone,
status: rider.status, status: rider.status,
role: rider.role,
rating: rider.rating, rating: rider.rating,
totalOrders: rider.totalOrders, totalOrders: rider.totalOrders,
totalIncome: rider.totalIncome, totalIncome: rider.totalIncome,
@ -38,7 +61,7 @@ router.post('/login', async (req, res) => {
} }
}); });
// 获取骑手信息 // 获取骑手信息(需登录)
router.get('/:id', async (req, res) => { router.get('/:id', async (req, res) => {
try { try {
const rider = await Rider.findById(req.params.id); const rider = await Rider.findById(req.params.id);
@ -51,12 +74,14 @@ router.get('/:id', async (req, res) => {
} }
}); });
// 更新骑手信息(如状态切换 // 更新骑手信息(如状态切换,需登录本人或管理员
router.put('/:id', async (req, res) => { router.put('/:id', async (req, res) => {
try { try {
// 不允许通过 PUT 修改密码和 role
const { password, role, ...safeBody } = req.body;
const rider = await Rider.findByIdAndUpdate( const rider = await Rider.findByIdAndUpdate(
req.params.id, req.params.id,
req.body, safeBody,
{ new: true, runValidators: true } { new: true, runValidators: true }
); );
if (!rider) { if (!rider) {

View File

@ -1,9 +1,12 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const Store = require('../models/Store'); 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 { try {
const stores = await Store.find(); const stores = await Store.find();
res.json({ success: true, data: stores }); res.json({ success: true, data: stores });
@ -12,8 +15,8 @@ router.get('/', async (req, res) => {
} }
}); });
// 创建门店 // 创建门店(仅 admin
router.post('/', async (req, res) => { router.post('/', authMiddleware, requireRole('admin'), validate(schemas.store), async (req, res) => {
try { try {
const store = new Store(req.body); const store = new Store(req.body);
await store.save(); await store.save();
@ -23,18 +26,19 @@ router.post('/', async (req, res) => {
} }
}); });
// 更新门店 // 更新门店(仅 admin
router.put('/:id', async (req, res) => { router.put('/:id', authMiddleware, requireRole('admin'), async (req, res) => {
try { try {
const store = await Store.findByIdAndUpdate(req.params.id, req.body, { new: true }); 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 }); res.json({ success: true, data: store });
} catch (error) { } catch (error) {
res.status(400).json({ success: false, message: error.message }); res.status(400).json({ success: false, message: error.message });
} }
}); });
// 删除门店 // 删除门店(仅 admin
router.delete('/:id', async (req, res) => { router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => {
try { try {
await Store.findByIdAndDelete(req.params.id); await Store.findByIdAndDelete(req.params.id);
res.json({ success: true }); res.json({ success: true });

View File

@ -1,14 +1,16 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const Vehicle = require('../models/Vehicle'); 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 { try {
const filter = {}; const filter = {};
if (req.query.storeId) { if (req.query.storeId) filter.storeId = 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'); const vehicles = await Vehicle.find(filter).populate('currentOrderId');
res.json({ success: true, data: vehicles }); res.json({ success: true, data: vehicles });
} catch (error) { } 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 { try {
const vehicle = await Vehicle.findById(req.params.id).populate('currentOrderId'); const vehicle = await Vehicle.findById(req.params.id).populate('currentOrderId');
if (!vehicle) { if (!vehicle) return res.status(404).json({ success: false, message: '车辆不存在' });
return res.status(404).json({ success: false, message: '车辆不存在' });
}
res.json({ success: true, data: vehicle }); res.json({ success: true, data: vehicle });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, message: error.message }); res.status(500).json({ success: false, message: error.message });
} }
}); });
// 创建车辆 // 创建车辆(需 admin 或 store
router.post('/', async (req, res) => { router.post('/', authMiddleware, requireRole('admin', 'store'), validate(schemas.vehicle), async (req, res) => {
try { try {
const data = { ...req.body }; const data = { ...req.body };
// status 为"在租"时自动设置 isRented if (data.status === '在租') data.isRented = true;
if (data.status === '在租') {
data.isRented = true;
}
const vehicle = new Vehicle(data); const vehicle = new Vehicle(data);
await vehicle.save(); await vehicle.save();
res.status(201).json({ success: true, data: vehicle }); res.status(201).json({ success: true, data: vehicle });
@ -45,43 +42,32 @@ router.post('/', async (req, res) => {
} }
}); });
// 更新车辆 // 更新车辆(需 admin 或 store
router.put('/:id', async (req, res) => { router.put('/:id', authMiddleware, requireRole('admin', 'store'), validate(schemas.vehicle), async (req, res) => {
try { try {
const data = { ...req.body }; const data = { ...req.body };
// status 变化时同步 isRented if (data.status !== undefined) data.isRented = data.status === '在租';
if (data.status !== undefined) { const vehicle = await Vehicle.findByIdAndUpdate(req.params.id, data, { new: true, runValidators: true });
data.isRented = data.status === '在租'; if (!vehicle) return res.status(404).json({ success: false, message: '车辆不存在' });
}
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 }); res.json({ success: true, data: vehicle });
} catch (error) { } catch (error) {
res.status(400).json({ success: false, message: error.message }); res.status(400).json({ success: false, message: error.message });
} }
}); });
// 删除车辆 // 删除车辆(需 admin
router.delete('/:id', async (req, res) => { router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => {
try { try {
const vehicle = await Vehicle.findByIdAndDelete(req.params.id); const vehicle = await Vehicle.findByIdAndDelete(req.params.id);
if (!vehicle) { if (!vehicle) return res.status(404).json({ success: false, message: '车辆不存在' });
return res.status(404).json({ success: false, message: '车辆不存在' });
}
res.json({ success: true, message: '车辆已删除' }); res.json({ success: true, message: '车辆已删除' });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, message: error.message }); 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 { try {
const vehicles = await Vehicle.find({ status: req.params.status }); const vehicles = await Vehicle.find({ status: req.params.status });
res.json({ success: true, data: vehicles }); res.json({ success: true, data: vehicles });
@ -90,24 +76,19 @@ router.get('/status/:status', async (req, res) => {
} }
}); });
// 更新车辆位置 // 更新车辆位置(需 admin 或 store
router.patch('/:id/location', async (req, res) => { router.patch('/:id/location', authMiddleware, requireRole('admin', 'store'), async (req, res) => {
try { try {
const { longitude, latitude } = req.body; const { longitude, latitude } = req.body;
const vehicle = await Vehicle.findByIdAndUpdate( const vehicle = await Vehicle.findByIdAndUpdate(
req.params.id, req.params.id,
{ {
location: { location: { type: 'Point', coordinates: [longitude, latitude] },
type: 'Point',
coordinates: [longitude, latitude]
},
lastLocationUpdate: new Date() lastLocationUpdate: new Date()
}, },
{ new: true } { new: true }
); );
if (!vehicle) { if (!vehicle) return res.status(404).json({ success: false, message: '车辆不存在' });
return res.status(404).json({ success: false, message: '车辆不存在' });
}
res.json({ success: true, data: vehicle }); res.json({ success: true, data: vehicle });
} catch (error) { } catch (error) {
res.status(400).json({ success: false, message: error.message }); res.status(400).json({ success: false, message: error.message });

View File

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

17
server/utils/password.js Normal file
View File

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