diff --git a/package-lock.json b/package-lock.json index b7a1986..b7aa0e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -209,9 +209,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "http://mirrors.tencentyun.com/npm/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1365,15 +1365,15 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "http://mirrors.tencentyun.com/npm/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "http://mirrors.tencentyun.com/npm/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { diff --git a/server/index.js b/server/index.js index 122caa5..30cdc69 100644 --- a/server/index.js +++ b/server/index.js @@ -8,6 +8,18 @@ const rateLimit = require('express-rate-limit'); const app = express(); const PORT = process.env.PORT || 3000; +// ─── 自定义中间件 ──────────────────────────────────────────── +const { authMiddleware } = require('./middleware/auth'); +const roleAuth = require('./middleware/roleAuth'); +const rbacAuth = require('./middleware/rbacAuth'); + +// ─── 加载模型(注册到 mongoose)─────────────────────────────── +require('./models/User'); +require('./models/Role'); +require('./models/UserRole'); +require('./models/Permission'); +require('./models/RolePerm'); + // ─── 安全中间件 ────────────────────────────────────────────── // helmet: 安全响应头 @@ -50,20 +62,33 @@ mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/e-scooter .then(() => console.log('✅ MongoDB 连接成功')) .catch(err => console.error('❌ MongoDB 连接失败:', err.message)); -// ─── 路由 ─────────────────────────────────────────────────── -app.use('/api/vehicles', require('./routes/vehicles')); -app.use('/api/orders', require('./routes/orders')); -app.use('/api/customers', require('./routes/customers')); -app.use('/api/finance', require('./routes/finance')); -app.use('/api/stores', require('./routes/stores')); -app.use('/api/complaints', require('./routes/complaints')); -app.use('/api/approvals', require('./routes/approvals')); -app.use('/api/payments', require('./routes/payments')); -app.use('/api/conflicts', require('./routes/conflicts')); -app.use('/api/applications', require('./routes/applications')); -app.use('/api/disputes', require('./routes/disputes')); +// ─── 路由(统一加 authMiddleware,按角色分级授权) ──────────── +// +/// 管理后台 API(admin only) +app.use('/api/vehicles', rbacAuth('vehicles:read', 'vehicles:write'), require('./routes/vehicles')); +app.use('/api/customers', rbacAuth('customers:read', 'customers:write'), require('./routes/customers')); +app.use('/api/finance', rbacAuth('finance:read'), require('./routes/finance')); +app.use('/api/orders', rbacAuth('orders:read', 'orders:write'), require('./routes/orders')); +app.use('/api/approvals', rbacAuth('approvals:read', 'approvals:write'), require('./routes/approvals')); +app.use('/api/disputes', rbacAuth('disputes:read', 'disputes:write'), require('./routes/disputes')); +app.use('/api/conflicts', rbacAuth('disputes:read', 'disputes:write'), require('./routes/conflicts')); + +/// 门店端 API(store + admin) +app.use('/api/stores', rbacAuth('store:read', 'store:write'), require('./routes/stores')); +app.use('/api/complaints', rbacAuth('complaints:read', 'complaints:write'), require('./routes/complaints')); +app.use('/api/payments', rbacAuth('payments:read', 'payments:write'), require('./routes/payments')); +app.use('/api/applications', rbacAuth('applications:read', 'applications:write'), require('./routes/applications')); + +// ─── 独立账号体系登录路由(无需鉴权) ───────────────────────── +app.use('/api/admin', require('./routes/adminAuth')); +app.use('/api/store-auth', require('./routes/storeAuth')); +// ─── 新的 RBAC 统一认证登录路由 ───────────────────────────── +app.use('/api/auth', require('./routes/auth')); + +/// 骑手端 API +/// 注:riders.js 内部对非登录路由独立加 authMiddleware,故此处不挂载 roleAuth app.use('/api/riders', require('./routes/riders')); -app.use('/api/vehicle-types', require('./routes/vehicleTypes')); +app.use('/api/vehicle-types', rbacAuth('vehicleTypes:read', 'vehicleTypes:write'), require('./routes/vehicleTypes')); // ─── 健康检查(不限流) ───────────────────────────────────── app.get('/health', (req, res) => { diff --git a/server/initAdmin.js b/server/initAdmin.js new file mode 100644 index 0000000..6cbccb3 --- /dev/null +++ b/server/initAdmin.js @@ -0,0 +1,30 @@ +/** + * 初始化默认 admin 账号 + * 用法: node server/initAdmin.js + */ +require('dotenv').config(); +const mongoose = require('mongoose'); +const Admin = require('./models/Admin'); +const { hashPassword } = require('./utils/password'); + +async function init() { + await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/e-scooter-rental'); + + const existing = await Admin.findOne({ username: 'admin' }); + if (existing) { + console.log('⚠️ Admin already exists:', existing.username); + } else { + const hashed = await hashPassword('admin123'); + await Admin.create({ + username: 'admin', + password: hashed, + name: '系统管理员', + role: 'superadmin' + }); + console.log('✅ Admin created: admin / admin123'); + } + + await mongoose.disconnect(); +} + +init().catch(err => { console.error(err); process.exit(1); }); diff --git a/server/initRBAC.js b/server/initRBAC.js new file mode 100644 index 0000000..4cccadf --- /dev/null +++ b/server/initRBAC.js @@ -0,0 +1,114 @@ +const mongoose = require('mongoose'); +const User = require('./models/User'); +const Role = require('./models/Role'); +const UserRole = require('./models/UserRole'); +const Permission = require('./models/Permission'); +const RolePerm = require('./models/RolePerm'); +const { hashPassword } = require('./utils/password'); + +async function init() { + await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/e-scooter-rental'); + console.log('📦 MongoDB 连接成功,开始初始化 RBAC...'); + + // 清理旧数据(可选,生产环境请注释掉) + await RolePerm.deleteMany({}); + await Permission.deleteMany({}); + await UserRole.deleteMany({}); + await Role.deleteMany({}); + await User.deleteMany({}); + + // 1. 创建角色 + const roles = { + admin: await Role.create({ roleName: 'admin', roleLabel: '管理员', description: '系统管理员' }), + store: await Role.create({ roleName: 'store', roleLabel: '商家', description: '门店管理员' }), + rider: await Role.create({ roleName: 'rider', roleLabel: '骑手', description: '骑手用户' }) + }; + console.log('✅ 角色创建完成'); + + // 2. 创建权限 + const perms = { + vehiclesRead: await Permission.create({ permName: 'vehicles:read', permLabel: '查看车辆', module: 'vehicles', action: 'read' }), + vehiclesWrite: await Permission.create({ permName: 'vehicles:write', permLabel: '管理车辆', module: 'vehicles', action: 'write' }), + ordersRead: await Permission.create({ permName: 'orders:read', permLabel: '查看订单', module: 'orders', action: 'read' }), + ordersWrite: await Permission.create({ permName: 'orders:write', permLabel: '管理订单', module: 'orders', action: 'write' }), + financeRead: await Permission.create({ permName: 'finance:read', permLabel: '查看财务', module: 'finance', action: 'read' }), + usersRead: await Permission.create({ permName: 'users:read', permLabel: '查看用户', module: 'users', action: 'read' }), + usersWrite: await Permission.create({ permName: 'users:write', permLabel: '管理用户', module: 'users', action: 'write' }), + storeRead: await Permission.create({ permName: 'store:read', permLabel: '查看门店', module: 'store', action: 'read' }), + storeWrite: await Permission.create({ permName: 'store:write', permLabel: '管理门店', module: 'store', action: 'write' }), + customersRead: await Permission.create({ permName: 'customers:read', permLabel: '查看客户', module: 'customers', action: 'read' }), + customersWrite: await Permission.create({ permName: 'customers:write', permLabel: '管理客户', module: 'customers', action: 'write' }), + applicationsRead: await Permission.create({ permName: 'applications:read', permLabel: '查看申请', module: 'applications', action: 'read' }), + applicationsWrite: await Permission.create({ permName: 'applications:write', permLabel: '管理申请', module: 'applications', action: 'write' }), + complaintsRead: await Permission.create({ permName: 'complaints:read', permLabel: '查看投诉', module: 'complaints', action: 'read' }), + complaintsWrite: await Permission.create({ permName: 'complaints:write', permLabel: '管理投诉', module: 'complaints', action: 'write' }), + disputesRead: await Permission.create({ permName: 'disputes:read', permLabel: '查看纠纷', module: 'disputes', action: 'read' }), + disputesWrite: await Permission.create({ permName: 'disputes:write', permLabel: '管理纠纷', module: 'disputes', action: 'write' }), + approvalsRead: await Permission.create({ permName: 'approvals:read', permLabel: '查看审批', module: 'approvals', action: 'read' }), + approvalsWrite: await Permission.create({ permName: 'approvals:write', permLabel: '管理审批', module: 'approvals', action: 'write' }), + paymentsRead: await Permission.create({ permName: 'payments:read', permLabel: '查看支付', module: 'payments', action: 'read' }), + paymentsWrite: await Permission.create({ permName: 'payments:write', permLabel: '管理支付', module: 'payments', action: 'write' }), + vehicleTypesRead: await Permission.create({ permName: 'vehicleTypes:read', permLabel: '查看车型', module: 'vehicleTypes', action: 'read' }), + vehicleTypesWrite: await Permission.create({ permName: 'vehicleTypes:write', permLabel: '管理车型', module: 'vehicleTypes', action: 'write' }), + }; + console.log('✅ 权限创建完成'); + + // 3. 角色-权限关联 + + // admin: 所有权限 + for (const key of Object.keys(perms)) { + await RolePerm.create({ role: roles.admin._id, permission: perms[key]._id }); + } + + // store: 门店 + 订单 + 车辆 + 客户 + 投诉 + 申请 + 支付 + 车型(部分写权限) + const storePerms = [ + 'storeRead', 'storeWrite', + 'ordersRead', 'ordersWrite', + 'vehiclesRead', 'vehiclesWrite', + 'customersRead', 'customersWrite', + 'complaintsRead', 'complaintsWrite', + 'applicationsRead', 'applicationsWrite', + 'paymentsRead', 'paymentsWrite', + 'disputesRead', + 'vehicleTypesRead' + ]; + for (const key of storePerms) { + await RolePerm.create({ role: roles.store._id, permission: perms[key]._id }); + } + + // rider: 只读部分 + const riderPerms = [ + 'ordersRead', + 'vehiclesRead', + 'customersRead', + 'vehicleTypesRead' + ]; + for (const key of riderPerms) { + await RolePerm.create({ role: roles.rider._id, permission: perms[key]._id }); + } + console.log('✅ 角色-权限关联完成'); + + // 4. 创建默认 admin 账号 + const hashed = await hashPassword('admin123'); + const adminUser = await User.create({ + username: 'admin', + password: hashed, + name: '系统管理员', + type: 'admin', + status: 'active' + }); + await UserRole.create({ user: adminUser._id, role: roles.admin._id }); + + console.log(''); + console.log('═══════════════════════════════════════'); + console.log('✅ RBAC 初始化完成!'); + console.log('默认账号: admin / admin123'); + console.log('═══════════════════════════════════════'); + + await mongoose.disconnect(); +} + +init().catch(err => { + console.error('❌ RBAC 初始化失败:', err); + process.exit(1); +}); diff --git a/server/initStore.js b/server/initStore.js new file mode 100644 index 0000000..9866fdc --- /dev/null +++ b/server/initStore.js @@ -0,0 +1,42 @@ +/** + * 初始化默认 store(门店)账号 + * 用法: node server/initStore.js + * 注意:需要先有 Store 记录才能创建门店账号 + */ +require('dotenv').config(); +const mongoose = require('mongoose'); +const Store = require('./models/Store'); +const StoreAuth = require('./models/StoreAuth'); +const { hashPassword } = require('./utils/password'); + +async function init() { + await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/e-scooter-rental'); + + // 查找第一个已审批的门店 + const store = await Store.findOne({ approvalStatus: '已通过' }); + if (!store) { + console.log('⚠️ 没有找到已审批的门店,请先创建门店记录'); + await mongoose.disconnect(); + return; + } + + const existing = await StoreAuth.findOne({ storeId: store.storeId }); + if (existing) { + console.log('⚠️ StoreAuth already exists for store:', store.name, '/ storeId:', store.storeId); + } else { + const hashed = await hashPassword('store123'); + await StoreAuth.create({ + storeId: store.storeId, + username: store.storeId, // 默认用户名 = 门店编号 + password: hashed, + name: store.manager || '门店管理员', + status: 'active' + }); + console.log('✅ StoreAuth created for:', store.name); + console.log(' username:', store.storeId, '/ password: store123'); + } + + await mongoose.disconnect(); +} + +init().catch(err => { console.error(err); process.exit(1); }); diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 5da432f..824c678 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -1,5 +1,16 @@ const jwt = require('jsonwebtoken'); +/** + * JWT Token 黑名单(已撤销的 jti 集合) + */ +const revokedTokens = new Set(); + +/** + * 撤销指定 jti 的 token + */ +const revokeToken = (jti) => revokedTokens.add(jti); +module.exports.revokeToken = revokeToken; + /** * JWT 鉴权中间件 * 验证请求头中的 Bearer token,写入 req.user @@ -11,6 +22,10 @@ const authMiddleware = (req, res, next) => { } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); + // 检查 token 是否已撤销 + if (decoded.jti && revokedTokens.has(decoded.jti)) { + return res.status(401).json({ success: false, message: 'token已失效' }); + } req.user = decoded; next(); } catch (err) { diff --git a/server/middleware/errorHandler.js b/server/middleware/errorHandler.js index 6c18bb3..fa20050 100644 --- a/server/middleware/errorHandler.js +++ b/server/middleware/errorHandler.js @@ -6,8 +6,7 @@ const errorHandler = (err, req, res, next) => { if (err.name === 'MongoError' || err.name === 'MongooseError') { return res.status(500).json({ success: false, - message: '数据库错误', - error: err.message + message: '数据库错误' }); } @@ -15,8 +14,7 @@ const errorHandler = (err, req, res, next) => { if (err.name === 'ValidationError') { return res.status(400).json({ success: false, - message: '数据验证失败', - error: err.message + message: '数据验证失败' }); } @@ -24,16 +22,14 @@ const errorHandler = (err, req, res, next) => { if (err.name === 'CastError') { return res.status(404).json({ success: false, - message: '数据不存在', - error: err.message + message: '数据不存在' }); } // 默认错误 res.status(err.status || 500).json({ success: false, - message: err.message || '服务器内部错误', - error: process.env.NODE_ENV === 'development' ? err.stack : undefined + message: '服务器内部错误' }); }; diff --git a/server/middleware/rbacAuth.js b/server/middleware/rbacAuth.js new file mode 100644 index 0000000..c62bec6 --- /dev/null +++ b/server/middleware/rbacAuth.js @@ -0,0 +1,39 @@ +const jwt = require('jsonwebtoken'); + +const revokedTokens = new Set(); + +const revokeToken = (jti) => revokedTokens.add(jti); +module.exports.revokeToken = revokeToken; + +const rbacAuth = (...requiredPerms) => { + return (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); + + if (decoded.jti && revokedTokens.has(decoded.jti)) { + return res.status(401).json({ success: false, message: 'token已失效' }); + } + + req.user = decoded; + + if (requiredPerms.length > 0) { + const userPerms = decoded.permissions || []; + const hasPerm = requiredPerms.some(perm => userPerms.includes(perm)); + if (!hasPerm) { + return res.status(403).json({ success: false, message: '权限不足' }); + } + } + + next(); + } catch (err) { + return res.status(401).json({ success: false, message: 'token无效或已过期' }); + } + }; +}; + +module.exports = rbacAuth; diff --git a/server/middleware/roleAuth.js b/server/middleware/roleAuth.js new file mode 100644 index 0000000..3d93179 --- /dev/null +++ b/server/middleware/roleAuth.js @@ -0,0 +1,23 @@ +/** + * 角色权限分级中间件(RBAC) + * 用法: roleAuth('admin'), roleAuth('admin', 'store', 'rider') + * + * 角色说明: + * admin - 管理后台最高权限,可访问所有 API + * store - 门店管理员,可访问门店相关数据和业务操作 + * rider - 骑手,只能访问自己的数据和公开浏览接口 + */ +const roleAuth = (...allowedRoles) => { + return (req, res, next) => { + if (!req.user) { + return res.status(401).json({ success: false, message: '未登录' }); + } + const { role } = req.user; + if (!allowedRoles.includes(role)) { + return res.status(403).json({ success: false, message: '无权访问' }); + } + next(); + }; +}; + +module.exports = roleAuth; diff --git a/server/models/Admin.js b/server/models/Admin.js new file mode 100644 index 0000000..818f2e3 --- /dev/null +++ b/server/models/Admin.js @@ -0,0 +1,11 @@ +const mongoose = require('mongoose'); + +const AdminSchema = new mongoose.Schema({ + username: { type: String, required: true, unique: true }, // 登录名,如 "admin" + password: { type: String, required: true, select: false }, // bcrypt 加密 + name: { type: String, required: true }, + role: { type: String, enum: ['admin', 'superadmin'], default: 'admin' }, + status: { type: String, enum: ['active', 'disabled'], default: 'active' } +}, { timestamps: true }); + +module.exports = mongoose.model('Admin', AdminSchema); diff --git a/server/models/Order.js b/server/models/Order.js index c023916..665b162 100644 --- a/server/models/Order.js +++ b/server/models/Order.js @@ -32,6 +32,9 @@ const orderSchema = new mongoose.Schema({ paymentMethod: { type: String, enum: ['微信', '支付宝', '现金', '银行卡'] }, paymentDate: { type: Date }, + // 门店关联 + storeId: { type: String, index: true }, // 所属门店 + // 合同信息 contractUrl: { type: String }, // 合同文件路径 contractSigned: { type: Boolean, default: false }, // 合同是否签署 diff --git a/server/models/Permission.js b/server/models/Permission.js new file mode 100644 index 0000000..fa95fb1 --- /dev/null +++ b/server/models/Permission.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); + +const PermissionSchema = new mongoose.Schema({ + permName: { type: String, required: true, unique: true }, + permLabel: { type: String, required: true }, + module: { type: String, required: true }, + action: { type: String, required: true } +}, { timestamps: true }); + +// 索引 +PermissionSchema.index({ permName: 1 }, { unique: true }); +PermissionSchema.index({ module: 1 }); +PermissionSchema.index({ action: 1 }); + +module.exports = mongoose.model('Permission', PermissionSchema); diff --git a/server/models/Rider.js b/server/models/Rider.js index 7af95ac..33e554b 100644 --- a/server/models/Rider.js +++ b/server/models/Rider.js @@ -9,8 +9,8 @@ const riderSchema = new mongoose.Schema({ // 密码(bcrypt 哈希存储,查询时默认不返回) password: { type: String, required: true, select: false }, - // 角色(customer=普通用户, admin=管理员, store=门店管理员) - role: { type: String, enum: ['customer', 'admin', 'store'], default: 'customer' }, + // 角色(customer=普通用户, admin=管理员, store=门店管理员, rider=骑手) + role: { type: String, enum: ['customer', 'admin', 'store', 'rider'], default: 'customer' }, // 接单状态 status: { diff --git a/server/models/Role.js b/server/models/Role.js new file mode 100644 index 0000000..59ce913 --- /dev/null +++ b/server/models/Role.js @@ -0,0 +1,9 @@ +const mongoose = require('mongoose'); + +const RoleSchema = new mongoose.Schema({ + roleName: { type: String, required: true, unique: true }, + roleLabel: { type: String, required: true }, + description: { type: String } +}, { timestamps: true }); + +module.exports = mongoose.model('Role', RoleSchema); diff --git a/server/models/RolePerm.js b/server/models/RolePerm.js new file mode 100644 index 0000000..b70fa10 --- /dev/null +++ b/server/models/RolePerm.js @@ -0,0 +1,13 @@ +const mongoose = require('mongoose'); + +const RolePermSchema = new mongoose.Schema({ + role: { type: mongoose.Schema.Types.ObjectId, ref: 'Role', required: true }, + permission: { type: mongoose.Schema.Types.ObjectId, ref: 'Permission', required: true } +}, { timestamps: true }); + +// 索引 +RolePermSchema.index({ role: 1, permission: 1 }, { unique: true }); +RolePermSchema.index({ role: 1 }); +RolePermSchema.index({ permission: 1 }); + +module.exports = mongoose.model('RolePerm', RolePermSchema); diff --git a/server/models/StoreAuth.js b/server/models/StoreAuth.js new file mode 100644 index 0000000..568bb9c --- /dev/null +++ b/server/models/StoreAuth.js @@ -0,0 +1,11 @@ +const mongoose = require('mongoose'); + +const StoreAuthSchema = new mongoose.Schema({ + storeId: { type: String, required: true, unique: true }, // 关联的门店编号 + username: { type: String, required: true, unique: true }, // 门店登录用户名 + password: { type: String, required: true, select: false }, // bcrypt 加密 + name: { type: String, required: true }, // 门店负责人姓名 + status: { type: String, enum: ['active', 'disabled'], default: 'active' } +}, { timestamps: true }); + +module.exports = mongoose.model('StoreAuth', StoreAuthSchema); diff --git a/server/models/User.js b/server/models/User.js new file mode 100644 index 0000000..4a16a1c --- /dev/null +++ b/server/models/User.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); + +const UserSchema = new mongoose.Schema({ + username: { type: String, required: true, unique: true }, + password: { type: String, required: true, select: false }, + phone: { type: String }, + name: { type: String, required: true }, + status: { type: String, enum: ['active', 'disabled'], default: 'active' }, + type: { type: String, enum: ['admin', 'store', 'rider'], required: true }, + storeId: { type: String } // store 类型账号关联的门店ID +}, { timestamps: true }); + +// 索引 +UserSchema.index({ username: 1 }, { unique: true }); +UserSchema.index({ phone: 1 }); +UserSchema.index({ type: 1 }); +UserSchema.index({ status: 1 }); + +module.exports = mongoose.model('User', UserSchema); diff --git a/server/models/UserRole.js b/server/models/UserRole.js new file mode 100644 index 0000000..8960c0a --- /dev/null +++ b/server/models/UserRole.js @@ -0,0 +1,10 @@ +const mongoose = require('mongoose'); + +const UserRoleSchema = new mongoose.Schema({ + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + role: { type: mongoose.Schema.Types.ObjectId, ref: 'Role', required: true } +}, { timestamps: true }); + +UserRoleSchema.index({ user: 1, role: 1 }, { unique: true }); + +module.exports = mongoose.model('UserRole', UserRoleSchema); diff --git a/server/routes/adminAuth.js b/server/routes/adminAuth.js new file mode 100644 index 0000000..76444dd --- /dev/null +++ b/server/routes/adminAuth.js @@ -0,0 +1,47 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('jsonwebtoken'); +const rateLimit = require('express-rate-limit'); +const Admin = require('../models/Admin'); +const { comparePassword } = require('../utils/password'); + +// 登录限流 +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, + message: { success: false, message: '登录尝试过于频繁' } +}); + +router.post('/login', loginLimiter, async (req, res) => { + try { + const { username, password } = req.body; + if (!username || !password) { + return res.status(400).json({ success: false, message: '用户名和密码不能为空' }); + } + + const admin = await Admin.findOne({ username }).select('+password'); + if (!admin || admin.status !== 'active') { + return res.status(401).json({ success: false, message: '用户名或密码错误' }); + } + + const isMatch = await comparePassword(password, admin.password); + if (!isMatch) { + return res.status(401).json({ success: false, message: '用户名或密码错误' }); + } + + const token = jwt.sign( + { id: admin._id, role: admin.role, type: 'admin', jti: Math.random().toString(36) }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN || '24h' } + ); + + res.json({ + success: true, + data: { id: admin._id, username: admin.username, name: admin.name, role: admin.role, token } + }); + } catch (error) { + res.status(500).json({ success: false, message: '服务器内部错误' }); + } +}); + +module.exports = router; diff --git a/server/routes/applications.js b/server/routes/applications.js index ba08d81..1e3965e 100644 --- a/server/routes/applications.js +++ b/server/routes/applications.js @@ -1,41 +1,46 @@ const express = require('express'); const router = express.Router(); const Application = require('../models/Application'); +const { authMiddleware, requireRole } = require('../middleware/auth'); -router.get('/', async (req, res) => { +// 获取所有申请(admin 或 store) +router.get('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const apps = await Application.find().populate('store'); res.json({ success: true, data: apps }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: '服务器内部错误' }); } }); -router.post('/', async (req, res) => { +// 创建申请(admin 或 store) +router.post('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const app = new Application(req.body); await app.save(); res.json({ success: true, data: app }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -router.put('/:id', async (req, res) => { +// 更新申请(admin 或 store) +router.put('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const app = await Application.findByIdAndUpdate(req.params.id, req.body, { new: true }); res.json({ success: true, data: app }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -router.delete('/:id', async (req, res) => { +// 删除申请(admin 或 store) +router.delete('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { await Application.findByIdAndDelete(req.params.id); res.json({ success: true }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); diff --git a/server/routes/approvals.js b/server/routes/approvals.js index 82de76d..2989314 100644 --- a/server/routes/approvals.js +++ b/server/routes/approvals.js @@ -1,45 +1,46 @@ const express = require('express'); const router = express.Router(); const Approval = require('../models/Approval'); +const { authMiddleware, requireRole } = require('../middleware/auth'); -// 获取所有审批 -router.get('/', async (req, res) => { +// 获取所有审批(仅 admin) +router.get('/', authMiddleware, requireRole('admin'), async (req, res) => { try { const approvals = await Approval.find(); res.json({ success: true, data: approvals }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: '服务器内部错误' }); } }); -// 创建审批 -router.post('/', async (req, res) => { +// 创建审批(仅 admin) +router.post('/', authMiddleware, requireRole('admin'), async (req, res) => { try { const approval = new Approval(req.body); await approval.save(); res.json({ success: true, data: approval }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -// 更新审批 -router.put('/:id', async (req, res) => { +// 更新审批(仅 admin) +router.put('/:id', authMiddleware, requireRole('admin'), async (req, res) => { try { const approval = await Approval.findByIdAndUpdate(req.params.id, req.body, { new: true }); res.json({ success: true, data: approval }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -// 删除审批 -router.delete('/:id', async (req, res) => { +// 删除审批(仅 admin) +router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => { try { await Approval.findByIdAndDelete(req.params.id); res.json({ success: true }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); diff --git a/server/routes/auth.js b/server/routes/auth.js new file mode 100644 index 0000000..8d41aaa --- /dev/null +++ b/server/routes/auth.js @@ -0,0 +1,74 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('jsonwebtoken'); +const rateLimit = require('express-rate-limit'); +const User = require('../models/User'); +const UserRole = require('../models/UserRole'); +const RolePerm = require('../models/RolePerm'); +const { comparePassword } = require('../utils/password'); + +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, + message: { success: false, message: '登录尝试过于频繁' } +}); + +router.post('/login', loginLimiter, async (req, res) => { + try { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ success: false, message: '用户名和密码不能为空' }); + } + + const user = await User.findOne({ username }).select('+password'); + if (!user || user.status !== 'active') { + return res.status(401).json({ success: false, message: '用户名或密码错误' }); + } + + const isMatch = await comparePassword(password, user.password); + if (!isMatch) { + return res.status(401).json({ success: false, message: '用户名或密码错误' }); + } + + const userRole = await UserRole.findOne({ user: user._id }).populate('role'); + if (!userRole) { + return res.status(403).json({ success: false, message: '该用户未分配角色' }); + } + + const rolePerms = await RolePerm.find({ role: userRole.role._id }).populate('permission'); + const permissions = rolePerms.map(rp => rp.permission.permName); + + const token = jwt.sign( + { + id: user._id, + username: user.username, + type: user.type, + role: userRole.role.roleName, + permissions, + jti: Math.random().toString(36) + }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN || '24h' } + ); + + res.json({ + success: true, + data: { + id: user._id, + username: user.username, + name: user.name, + type: user.type, + role: userRole.role.roleName, + roleLabel: userRole.role.roleLabel, + permissions, + token + } + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ success: false, message: '服务器内部错误' }); + } +}); + +module.exports = router; diff --git a/server/routes/complaints.js b/server/routes/complaints.js index a6d617e..f104ac4 100644 --- a/server/routes/complaints.js +++ b/server/routes/complaints.js @@ -1,45 +1,46 @@ const express = require('express'); const router = express.Router(); const Complaint = require('../models/Complaint'); +const { authMiddleware, requireRole } = require('../middleware/auth'); -// 获取所有投诉 -router.get('/', async (req, res) => { +// 获取所有投诉(admin 或 store) +router.get('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const complaints = await Complaint.find().populate('customer order'); res.json({ success: true, data: complaints }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: '服务器内部错误' }); } }); -// 创建投诉 -router.post('/', async (req, res) => { +// 创建投诉(admin 或 store) +router.post('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const complaint = new Complaint(req.body); await complaint.save(); res.json({ success: true, data: complaint }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -// 更新投诉 -router.put('/:id', async (req, res) => { +// 更新投诉(admin 或 store) +router.put('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const complaint = await Complaint.findByIdAndUpdate(req.params.id, req.body, { new: true }); res.json({ success: true, data: complaint }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -// 删除投诉 -router.delete('/:id', async (req, res) => { +// 删除投诉(admin 或 store) +router.delete('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { await Complaint.findByIdAndDelete(req.params.id); res.json({ success: true }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); diff --git a/server/routes/conflicts.js b/server/routes/conflicts.js index d03fdd8..35eec9e 100644 --- a/server/routes/conflicts.js +++ b/server/routes/conflicts.js @@ -1,45 +1,46 @@ const express = require('express'); const router = express.Router(); const Conflict = require('../models/Conflict'); +const { authMiddleware, requireRole } = require('../middleware/auth'); -// 获取所有矛盾记录 -router.get('/', async (req, res) => { +// 获取所有矛盾记录(admin 或 store) +router.get('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const conflicts = await Conflict.find(); res.json({ success: true, data: conflicts }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: '服务器内部错误' }); } }); -// 创建矛盾记录 -router.post('/', async (req, res) => { +// 创建矛盾记录(admin 或 store) +router.post('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const conflict = new Conflict(req.body); await conflict.save(); res.json({ success: true, data: conflict }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -// 更新矛盾记录 -router.put('/:id', async (req, res) => { +// 更新矛盾记录(admin 或 store) +router.put('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const conflict = await Conflict.findByIdAndUpdate(req.params.id, req.body, { new: true }); res.json({ success: true, data: conflict }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -// 删除矛盾记录 -router.delete('/:id', async (req, res) => { +// 删除矛盾记录(admin 或 store) +router.delete('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { await Conflict.findByIdAndDelete(req.params.id); res.json({ success: true }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); diff --git a/server/routes/customers.js b/server/routes/customers.js index abf2792..b1a13d1 100644 --- a/server/routes/customers.js +++ b/server/routes/customers.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); const Customer = require('../models/Customer'); +const Store = require('../models/Store'); const { authMiddleware, requireRole } = require('../middleware/auth'); const { validate } = require('../middleware/validate'); const { schemas } = require('../middleware/validate'); @@ -11,7 +12,7 @@ router.get('/', authMiddleware, requireRole('admin', 'store'), async (req, res) const customers = await Customer.find(); res.json({ success: true, data: customers }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -22,7 +23,7 @@ router.get('/:id', authMiddleware, requireRole('admin', 'store'), async (req, re 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 }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -33,7 +34,7 @@ router.post('/', authMiddleware, requireRole('admin', 'store'), validate(schemas await customer.save(); res.status(201).json({ success: true, data: customer }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -44,7 +45,7 @@ router.put('/:id', authMiddleware, requireRole('admin', 'store'), async (req, re 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 }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -55,7 +56,7 @@ router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => 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 }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -72,7 +73,7 @@ router.get('/search/:keyword', authMiddleware, requireRole('admin', 'store'), as }); res.json({ success: true, data: customers }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -93,7 +94,7 @@ router.patch('/:id/credit', authMiddleware, requireRole('admin', 'store'), async 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 }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); diff --git a/server/routes/disputes.js b/server/routes/disputes.js index 4d97175..4907749 100644 --- a/server/routes/disputes.js +++ b/server/routes/disputes.js @@ -1,41 +1,46 @@ const express = require('express'); const router = express.Router(); const Dispute = require('../models/Dispute'); +const { authMiddleware, requireRole } = require('../middleware/auth'); -router.get('/', async (req, res) => { +// 获取所有纠纷(admin 或 store) +router.get('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const disputes = await Dispute.find(); res.json({ success: true, data: disputes }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: '服务器内部错误' }); } }); -router.post('/', async (req, res) => { +// 创建纠纷(admin 或 store) +router.post('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const dispute = new Dispute(req.body); await dispute.save(); res.json({ success: true, data: dispute }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -router.put('/:id', async (req, res) => { +// 更新纠纷(admin 或 store) +router.put('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const dispute = await Dispute.findByIdAndUpdate(req.params.id, req.body, { new: true }); res.json({ success: true, data: dispute }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -router.delete('/:id', async (req, res) => { +// 删除纠纷(admin 或 store) +router.delete('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { await Dispute.findByIdAndDelete(req.params.id); res.json({ success: true }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); diff --git a/server/routes/finance.js b/server/routes/finance.js index e95d9f2..66d758d 100644 --- a/server/routes/finance.js +++ b/server/routes/finance.js @@ -48,7 +48,7 @@ router.get('/', authMiddleware, requireRole('admin'), async (req, res) => { } }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -59,7 +59,7 @@ router.post('/', authMiddleware, requireRole('admin'), async (req, res) => { await payment.save(); res.json({ success: true, data: payment }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -69,7 +69,7 @@ router.put('/:id', authMiddleware, requireRole('admin'), async (req, res) => { const payment = await Payment.findByIdAndUpdate(req.params.id, req.body, { new: true }); res.json({ success: true, data: payment }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -79,7 +79,7 @@ router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => await Payment.findByIdAndDelete(req.params.id); res.json({ success: true }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -89,7 +89,7 @@ router.post('/delete-all', authMiddleware, requireRole('admin'), async (req, res await Payment.deleteMany({}); res.json({ success: true, message: 'All payments deleted' }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -142,7 +142,7 @@ router.get('/stats', authMiddleware, requireRole('admin'), async (req, res) => { } }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); diff --git a/server/routes/orders.js b/server/routes/orders.js index 05fa73d..7470e07 100644 --- a/server/routes/orders.js +++ b/server/routes/orders.js @@ -3,6 +3,7 @@ const router = express.Router(); const Order = require('../models/Order'); const Vehicle = require('../models/Vehicle'); const Customer = require('../models/Customer'); +const Store = require('../models/Store'); const { authMiddleware, requireRole } = require('../middleware/auth'); const { validate } = require('../middleware/validate'); const { schemas } = require('../middleware/validate'); @@ -18,15 +19,17 @@ router.get('/', authMiddleware, async (req, res) => { // 此处通过 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; + // store 角色永远只能看自己门店的数据,忽略前端传的 storeId 参数 + if (req.user.storeId) { + filter.storeId = req.user.storeId; + } } const orders = await Order.find(filter) .populate('customer', 'name phone') .populate('vehicle', 'model vehicleId'); res.json({ success: true, data: orders }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: '服务器内部错误' }); } }); @@ -45,7 +48,7 @@ router.get('/:id', authMiddleware, async (req, res) => { } res.json({ success: true, data: order }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -122,7 +125,7 @@ router.post('/', authMiddleware, requireRole('admin', 'store'), validate(schemas res.status(201).json({ success: true, data: order }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -133,7 +136,7 @@ router.put('/:id', authMiddleware, requireRole('admin', 'store'), async (req, re 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 }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -164,7 +167,7 @@ router.patch('/:id/complete', authMiddleware, requireRole('admin', 'store'), asy res.json({ success: true, data: order }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -197,7 +200,7 @@ router.patch('/:id/cancel', authMiddleware, requireRole('admin', 'store'), async res.json({ success: true, data: order }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -221,7 +224,7 @@ router.get('/status/overdue', authMiddleware, requireRole('admin', 'store'), asy } res.json({ success: true, data: orders }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -233,7 +236,7 @@ router.get('/status/:status', authMiddleware, requireRole('admin', 'store'), asy .populate('vehicle', 'model vehicleId'); res.json({ success: true, data: orders }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); diff --git a/server/routes/payments.js b/server/routes/payments.js index f8922f6..76b66aa 100644 --- a/server/routes/payments.js +++ b/server/routes/payments.js @@ -1,45 +1,46 @@ 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 或 store) +router.get('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const payments = await Payment.find(); res.json({ success: true, data: payments }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: '服务器内部错误' }); } }); -// 创建打款记录 -router.post('/', async (req, res) => { +// 创建打款记录(需 admin 或 store) +router.post('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const payment = new Payment(req.body); await payment.save(); res.json({ success: true, data: payment }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -// 更新打款记录 -router.put('/:id', async (req, res) => { +// 更新打款记录(需 admin 或 store) +router.put('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const payment = await Payment.findByIdAndUpdate(req.params.id, req.body, { new: true }); res.json({ success: true, data: payment }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -// 删除打款记录 -router.delete('/:id', async (req, res) => { +// 删除打款记录(需 admin 或 store) +router.delete('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { await Payment.findByIdAndDelete(req.params.id); res.json({ success: true }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); diff --git a/server/routes/riders.js b/server/routes/riders.js index e68b237..5638731 100644 --- a/server/routes/riders.js +++ b/server/routes/riders.js @@ -5,6 +5,7 @@ const rateLimit = require('express-rate-limit'); const Rider = require('../models/Rider'); const Order = require('../models/Order'); const { comparePassword } = require('../utils/password'); +const { authMiddleware, requireRole } = require('../middleware/auth'); const { validate } = require('../middleware/validate'); const { schemas } = require('../middleware/validate'); @@ -17,7 +18,7 @@ const loginLimiter = rateLimit({ message: { success: false, message: '登录尝试过于频繁,请15分钟后再试' } }); -// 骑手登录 +// 骑手登录(无需鉴权) router.post('/login', loginLimiter, validate(schemas.login), async (req, res) => { try { const { phone, password } = req.body; @@ -34,9 +35,9 @@ router.post('/login', loginLimiter, validate(schemas.login), async (req, res) => return res.status(401).json({ success: false, message: '手机号或密码错误' }); } - // 签发 JWT(包含 id、role) + // 签发 JWT(包含 id、role、jti) const token = jwt.sign( - { id: rider._id, role: rider.role, phone: rider.phone }, + { id: rider._id, role: rider.role, type: 'rider', phone: rider.phone, jti: Math.random().toString(36) }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '7d' } ); @@ -57,31 +58,45 @@ router.post('/login', loginLimiter, validate(schemas.login), async (req, res) => } }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); -// 获取骑手信息(需登录) -router.get('/:id', async (req, res) => { +// 获取骑手信息(需登录,且只能查看自己;admin/store 可查看任意骑手) +router.get('/:id', authMiddleware, async (req, res) => { try { + const isSelf = req.params.id === req.user.id; + const isPrivileged = ['admin', 'store'].includes(req.user.role); + if (!isSelf && !isPrivileged) { + return res.status(403).json({ success: false, message: '无权查看该骑手信息' }); + } const rider = await Rider.findById(req.params.id); if (!rider) { return res.status(404).json({ success: false, message: '骑手不存在' }); } res.json({ success: true, data: rider }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); -// 更新骑手信息(如状态切换,需登录本人或管理员) -router.put('/:id', async (req, res) => { +// 更新骑手信息(需登录本人,或 admin/store) +router.put('/:id', authMiddleware, async (req, res) => { try { - // 不允许通过 PUT 修改密码和 role - const { password, role, ...safeBody } = req.body; + const isSelf = req.params.id === req.user.id; + const isPrivileged = ['admin', 'store'].includes(req.user.role); + if (!isSelf && !isPrivileged) { + return res.status(403).json({ success: false, message: '无权修改该骑手信息' }); + } + // 不允许通过 PUT 修改密码和 role(仅 admin 可改 role) + const { password, ...safeBody } = req.body; + if (req.body.role && !isPrivileged) { + return res.status(403).json({ success: false, message: '无权修改角色' }); + } + const updateData = isPrivileged ? req.body : safeBody; const rider = await Rider.findByIdAndUpdate( req.params.id, - safeBody, + updateData, { new: true, runValidators: true } ); if (!rider) { @@ -89,20 +104,25 @@ router.put('/:id', async (req, res) => { } res.json({ success: true, data: rider }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); -// 获取骑手的订单 -router.get('/:id/orders', async (req, res) => { +// 获取骑手的订单(需登录本人,或 admin/store) +router.get('/:id/orders', authMiddleware, async (req, res) => { try { + const isSelf = req.params.id === req.user.id; + const isPrivileged = ['admin', 'store'].includes(req.user.role); + if (!isSelf && !isPrivileged) { + return res.status(403).json({ success: false, message: '无权查看该骑手的订单' }); + } const orders = await Order.find({ rider: req.params.id }) .populate('customer', 'name phone') .populate('vehicle', 'vehicleId model color') .sort({ createdAt: -1 }); res.json({ success: true, data: orders }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); diff --git a/server/routes/storeAuth.js b/server/routes/storeAuth.js new file mode 100644 index 0000000..0885939 --- /dev/null +++ b/server/routes/storeAuth.js @@ -0,0 +1,67 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('jsonwebtoken'); +const rateLimit = require('express-rate-limit'); +const User = require('../models/User'); +const UserRole = require('../models/UserRole'); +const Role = require('../models/Role'); +const { comparePassword } = require('../utils/password'); + +// 登录限流 +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, + message: { success: false, message: '登录尝试过于频繁' } +}); + +router.post('/login', loginLimiter, async (req, res) => { + try { + const { username, password } = req.body; + if (!username || !password) { + return res.status(400).json({ success: false, message: '用户名和密码不能为空' }); + } + + // 从 User 表查 store 类型账号 + const user = await User.findOne({ username, type: 'store' }).select('+password'); + // 查关联的门店 + const Store = require('../models/Store'); + const store = await Store.findOne({ storeId: user.storeId }); + if (!user || user.status !== 'active') { + return res.status(401).json({ success: false, message: '用户名或密码错误' }); + } + + const isMatch = await comparePassword(password, user.password); + if (!isMatch) { + return res.status(401).json({ success: false, message: '用户名或密码错误' }); + } + + const token = jwt.sign( + { + id: user._id, + role: 'store', + type: 'store', + storeId: user.storeId || null, + permissions: ['store:read', 'store:write', 'orders:read', 'orders:write', 'vehicles:read', 'vehicles:write', 'vehicleTypes:read'], + jti: Math.random().toString(36) + }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN || '24h' } + ); + + res.json({ + success: true, + data: { + id: store ? store._id : user._id, // 门店的 MongoDB _id + storeId: user.storeId, // 门店编号如 STORE001 + username: user.username, + name: user.name, + role: 'store', + token + } + }); + } catch (error) { + res.status(500).json({ success: false, message: '服务器内部错误' }); + } +}); + +module.exports = router; diff --git a/server/routes/stores.js b/server/routes/stores.js index 4cf6c6b..358f2d8 100644 --- a/server/routes/stores.js +++ b/server/routes/stores.js @@ -8,10 +8,37 @@ const { schemas } = require('../middleware/validate'); // 获取所有门店(登录即可) router.get('/', authMiddleware, async (req, res) => { try { + // store 角色只能看自己关联的门店 + if (req.user.role === 'store' && req.user.storeId) { + const stores = await Store.find({ storeId: req.user.storeId }); + return res.json({ success: true, data: stores }); + } const stores = await Store.find(); res.json({ success: true, data: stores }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); + } +}); + +// 获取单个门店 +router.get('/:id', authMiddleware, async (req, res) => { + try { + let store; + // 如果是 MongoDB ObjectId 格式则用 findById,否则用 storeId 字段查 + if (req.params.id.match(/^[0-9a-fA-F]{24}$/)) { + store = await Store.findById(req.params.id); + } else { + store = await Store.findOne({ storeId: req.params.id }); + } + if (!store) return res.status(404).json({ success: false, message: '门店不存在' }); + + // store 角色只能看自己关联的门店 + if (req.user.role === 'store' && req.user.storeId && store.storeId !== req.user.storeId) { + return res.status(403).json({ success: false, message: '无权操作该门店数据' }); + } + res.json({ success: true, data: store }); + } catch (error) { + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -22,7 +49,7 @@ router.post('/', authMiddleware, requireRole('admin'), validate(schemas.store), await store.save(); res.json({ success: true, data: store }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -33,7 +60,7 @@ router.put('/:id', authMiddleware, requireRole('admin'), async (req, res) => { 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 }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -43,7 +70,7 @@ router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => await Store.findByIdAndDelete(req.params.id); res.json({ success: true }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); diff --git a/server/routes/vehicleTypes.js b/server/routes/vehicleTypes.js index bc5bfe5..ba82dcc 100644 --- a/server/routes/vehicleTypes.js +++ b/server/routes/vehicleTypes.js @@ -1,32 +1,33 @@ const express = require('express'); const router = express.Router(); const VehicleType = require('../models/VehicleType'); +const { authMiddleware, requireRole } = require('../middleware/auth'); -// 获取所有车型(支持按 storeId 筛选) -router.get('/', async (req, res) => { +// 获取所有车型(所有登录用户均可浏览) +router.get('/', authMiddleware, async (req, res) => { try { const filter = {}; if (req.query.storeId) filter.storeId = req.query.storeId; const vehicleTypes = await VehicleType.find(filter).sort({ createdAt: -1 }); res.json({ success: true, data: vehicleTypes }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: '服务器内部错误' }); } }); -// 创建车型 -router.post('/', async (req, res) => { +// 创建车型(需 admin 或 store) +router.post('/', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const vehicleType = new VehicleType(req.body); await vehicleType.save(); res.status(201).json({ success: true, data: vehicleType }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -// 更新车型 -router.put('/:id', async (req, res) => { +// 更新车型(需 admin 或 store) +router.put('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const vehicleType = await VehicleType.findByIdAndUpdate( req.params.id, @@ -38,12 +39,12 @@ router.put('/:id', async (req, res) => { } res.json({ success: true, data: vehicleType }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: '服务器内部错误' }); } }); -// 删除车型 -router.delete('/:id', async (req, res) => { +// 删除车型(需 admin 或 store) +router.delete('/:id', authMiddleware, requireRole('admin', 'store'), async (req, res) => { try { const vehicleType = await VehicleType.findByIdAndDelete(req.params.id); if (!vehicleType) { @@ -51,7 +52,7 @@ router.delete('/:id', async (req, res) => { } res.json({ success: true, message: '车型已删除' }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: '服务器内部错误' }); } }); diff --git a/server/routes/vehicles.js b/server/routes/vehicles.js index 21f9e66..01a6c1e 100644 --- a/server/routes/vehicles.js +++ b/server/routes/vehicles.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); const Vehicle = require('../models/Vehicle'); +const Store = require('../models/Store'); const { authMiddleware, requireRole } = require('../middleware/auth'); const { validate } = require('../middleware/validate'); const { schemas } = require('../middleware/validate'); @@ -9,12 +10,18 @@ const { schemas } = require('../middleware/validate'); router.get('/', authMiddleware, async (req, res) => { try { const filter = {}; - if (req.query.storeId) filter.storeId = req.query.storeId; + // store 角色查询时校验 storeId 归属(只能看自己关联的门店) + if (req.user.role === 'store' && req.user.storeId) { + // store 用户永远只能看自己门店的数据,忽略前端传的 storeId 参数 + filter.storeId = req.user.storeId; + } else 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) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: '服务器内部错误' }); } }); @@ -25,7 +32,7 @@ router.get('/:id', authMiddleware, async (req, res) => { 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 }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -38,7 +45,7 @@ router.post('/', authMiddleware, requireRole('admin', 'store'), validate(schemas await vehicle.save(); res.status(201).json({ success: true, data: vehicle }); } catch (error) { - res.status(400).json({ success: false, message: error.message }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -51,7 +58,7 @@ router.put('/:id', authMiddleware, requireRole('admin', 'store'), validate(schem 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 }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); @@ -62,7 +69,7 @@ router.delete('/:id', authMiddleware, requireRole('admin'), async (req, res) => 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 }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -72,7 +79,7 @@ router.get('/status/:status', authMiddleware, async (req, res) => { const vehicles = await Vehicle.find({ status: req.params.status }); res.json({ success: true, data: vehicles }); } catch (error) { - res.status(500).json({ success: false, message: error.message }); + res.status(500).json({ success: false, message: "服务器内部错误" }); } }); @@ -91,7 +98,7 @@ router.patch('/:id/location', authMiddleware, requireRole('admin', 'store'), asy 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 }); + res.status(400).json({ success: false, message: "服务器内部错误" }); } }); diff --git a/server/seed.js b/server/seed.js index fdc751b..c042594 100644 --- a/server/seed.js +++ b/server/seed.js @@ -22,11 +22,11 @@ async function clearData() { 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', storeId: 'STORE001', frameNumber: 'FN20240001', model: '黑骑士', color: '黑色', batteryType: '锂电池', batteryCapacity: 20, batteryStatus: '正常', status: '空闲', purchaseDate: new Date('2024-01-15'), purchasePrice: 3500, purchaseSupplier: '小牛电动车' }, + { vehicleId: 'SCOOTER002', storeId: 'STORE001', frameNumber: 'FN20240002', model: '黑骑士', color: '白色', batteryType: '锂电池', batteryCapacity: 20, batteryStatus: '正常', status: '空闲', purchaseDate: new Date('2024-02-20'), purchasePrice: 3500, purchaseSupplier: '小牛电动车' }, + { vehicleId: 'SCOOTER003', storeId: 'STORE002', frameNumber: 'FN20240003', model: '电动车', color: '蓝色', batteryType: '铅酸电池', batteryCapacity: 24, batteryStatus: '正常', status: '空闲', purchaseDate: new Date('2024-03-10'), purchasePrice: 2800, purchaseSupplier: '雅迪电动车' }, + { vehicleId: 'SCOOTER004', storeId: 'STORE002', frameNumber: 'FN20240004', model: '高端豪车', color: '红色', batteryType: '锂电池', batteryCapacity: 30, batteryStatus: '正常', status: '空闲', purchaseDate: new Date('2024-04-05'), purchasePrice: 8000, purchaseSupplier: '特斯拉' }, + { vehicleId: 'SCOOTER005', storeId: 'STORE003', frameNumber: 'FN20240005', model: '普通标准套餐', color: '绿色', batteryType: '铅酸电池', batteryCapacity: 20, batteryStatus: '正常', status: '空闲', purchaseDate: new Date('2024-05-01'), purchasePrice: 2500, purchaseSupplier: '爱玛电动车' } ]; console.log('🚗 创建示例车辆...'); await Vehicle.insertMany(vehicles); @@ -116,10 +116,10 @@ async function createRiders() { async function main() { try { await clearData(); + await createStores(); await createVehicles(); await createCustomers(); await createOrders(); - await createStores(); await createRiders(); console.log('\n🎉 示例数据创建完成!'); console.log('车辆: 5 辆 | 客户: 5 个 | 订单: 3 个 | 门店: 3 个 | 骑手: 3 个');