feat: 补全安全体系 - JWT鉴权、bcrypt密码加密、RBAC权限控制、速率限制、CORS白名单、输入校验
This commit is contained in:
parent
6e1378c812
commit
c48e2c2961
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}, {})
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
215
server/seed.js
215
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 {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
Loading…
Reference in New Issue