refactor(docs): 简化README结构,更新技术栈和项目结构描述
This commit is contained in:
@@ -1,94 +0,0 @@
|
||||
const express = require('express')
|
||||
const cors = require('cors')
|
||||
const helmet = require('helmet')
|
||||
const morgan = require('morgan')
|
||||
const rateLimit = require('express-rate-limit')
|
||||
const compression = require('compression')
|
||||
const path = require('path')
|
||||
require('dotenv').config()
|
||||
|
||||
// 数据库连接
|
||||
const { testConnection, syncModels } = require('./models')
|
||||
|
||||
// 导入Swagger配置
|
||||
const { specs, swaggerUi } = require('./config/swagger')
|
||||
|
||||
const app = express()
|
||||
|
||||
// 中间件配置
|
||||
app.use(helmet()) // 安全头
|
||||
app.use(cors()) // 跨域
|
||||
app.use(compression()) // 压缩
|
||||
app.use(morgan('combined')) // 日志
|
||||
app.use(express.json({ limit: '10mb' }))
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }))
|
||||
|
||||
// 限流
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 分钟
|
||||
max: 100, // 限制每个IP最多100个请求
|
||||
message: {
|
||||
success: false,
|
||||
message: '请求过于频繁,请稍后重试'
|
||||
}
|
||||
})
|
||||
app.use('/api', limiter)
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '服务运行正常',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || '1.0.0'
|
||||
})
|
||||
})
|
||||
|
||||
// 配置Swagger UI
|
||||
app.use('/swagger', swaggerUi.serve, swaggerUi.setup(specs, {
|
||||
explorer: true,
|
||||
customCss: '.swagger-ui .topbar { background-color: #3B82F6; }',
|
||||
customSiteTitle: 'NiuMall API 文档'
|
||||
}))
|
||||
|
||||
// 提供Swagger JSON文件
|
||||
app.get('/api-docs-json', (req, res) => {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.send(specs)
|
||||
})
|
||||
|
||||
const PORT = process.env.PORT || 4330
|
||||
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// 测试数据库连接
|
||||
const dbConnected = await testConnection();
|
||||
if (!dbConnected) {
|
||||
console.error('❌ 数据库连接失败,服务器启动终止');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 同步数据库模型
|
||||
await syncModels();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 服务器启动成功`)
|
||||
console.log(`📱 运行环境: ${process.env.NODE_ENV || 'development'}`)
|
||||
console.log(`🌐 访问地址: http://localhost:${PORT}`)
|
||||
console.log(`📊 健康检查: http://localhost:${PORT}/health`)
|
||||
console.log(`📚 API文档: http://localhost:${PORT}/swagger`)
|
||||
console.log(`📄 API文档JSON: http://localhost:${PORT}/api-docs-json`)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 服务器启动失败:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
startServer()
|
||||
|
||||
// API 路由
|
||||
app.use('/api/auth', require('./routes/auth'))
|
||||
app.use('/api/users', require('./routes/users'))
|
||||
app.use('/api/orders', require('./routes/orders'))
|
||||
app.use('/api/payments', require('./routes/payments'))
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'niumall-backend',
|
||||
script: 'app.js',
|
||||
script: 'src/main.js',
|
||||
cwd: '/data/nodejs/yunniushi/',
|
||||
instances: 'max',
|
||||
exec_mode: 'cluster',
|
||||
|
||||
6546
backend/package-lock.json
generated
6546
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "niumall-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "活牛采购智能数字化系统 - 后端服务",
|
||||
"description": "活牛采购智能数字化系统后端服务",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"start": "node src/main.js",
|
||||
@@ -9,63 +9,124 @@
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint src/ --fix",
|
||||
"format": "prettier --write src/",
|
||||
"db:migrate": "sequelize-cli db:migrate",
|
||||
"db:seed": "sequelize-cli db:seed:all",
|
||||
"db:reset": "sequelize-cli db:migrate:undo:all && npm run db:migrate && npm run db:seed",
|
||||
"pm2:start": "pm2 start ecosystem.config.js",
|
||||
"pm2:stop": "pm2 stop ecosystem.config.js",
|
||||
"pm2:restart": "pm2 restart ecosystem.config.js"
|
||||
"test:unit": "jest tests/unit",
|
||||
"test:integration": "jest tests/integration",
|
||||
"lint": "eslint src/ tests/",
|
||||
"lint:fix": "eslint src/ tests/ --fix",
|
||||
"migrate": "npx sequelize-cli db:migrate",
|
||||
"migrate:undo": "npx sequelize-cli db:migrate:undo",
|
||||
"seed": "npx sequelize-cli db:seed:all",
|
||||
"seed:undo": "npx sequelize-cli db:seed:undo:all"
|
||||
},
|
||||
"keywords": [
|
||||
"nodejs",
|
||||
"express",
|
||||
"mysql",
|
||||
"sequelize",
|
||||
"jwt",
|
||||
"api",
|
||||
"cattle",
|
||||
"procurement",
|
||||
"digital",
|
||||
"system"
|
||||
"procurement"
|
||||
],
|
||||
"author": "NiuMall Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.8.1",
|
||||
"helmet": "^7.0.0",
|
||||
"joi": "^17.9.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"helmet": "^7.1.0",
|
||||
"joi": "^17.11.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"morgan": "^1.10.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.6.0",
|
||||
"redis": "^4.6.7",
|
||||
"sequelize": "^6.32.1",
|
||||
"socket.io": "^4.7.2",
|
||||
"mysql2": "^3.6.5",
|
||||
"sequelize": "^6.35.2",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"uuid": "^9.0.0",
|
||||
"winston": "^3.10.0",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.11.0",
|
||||
"winston-daily-rotate-file": "^4.7.1",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.45.0",
|
||||
"jest": "^29.6.2",
|
||||
"nodemon": "^3.0.1",
|
||||
"pm2": "^5.3.0",
|
||||
"prettier": "^3.0.0",
|
||||
"sequelize-cli": "^6.6.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.0.2",
|
||||
"prettier": "^3.1.0",
|
||||
"sequelize-cli": "^6.6.2",
|
||||
"sqlite3": "^5.1.7",
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!src/main.js",
|
||||
"!src/config/**",
|
||||
"!src/migrations/**",
|
||||
"!src/seeders/**"
|
||||
],
|
||||
"coverageDirectory": "coverage",
|
||||
"coverageReporters": [
|
||||
"text",
|
||||
"lcov",
|
||||
"html"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/tests/**/*.test.js"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/tests/setup.js"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"standard",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"no-console": "warn",
|
||||
"no-unused-vars": "error"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"jest": true,
|
||||
"es6": true
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/your-org/niumall.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/your-org/niumall/issues"
|
||||
},
|
||||
"homepage": "https://github.com/your-org/niumall#readme"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,14 @@ const dbConfig = {
|
||||
port: process.env.DB_PORT || 3306,
|
||||
dialect: 'mysql'
|
||||
},
|
||||
test: {
|
||||
username: process.env.DB_USERNAME || 'root',
|
||||
password: process.env.DB_PASSWORD || 'password',
|
||||
database: process.env.DB_NAME || 'niumall_test',
|
||||
host: process.env.DB_HOST || '127.0.0.1',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
dialect: 'mysql'
|
||||
},
|
||||
production: {
|
||||
username: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
|
||||
@@ -19,11 +19,11 @@ const login = async (req, res) => {
|
||||
|
||||
console.log('Attempting to login user:', username);
|
||||
|
||||
// 查找用户(支持昵称或邮箱登录)
|
||||
// 查找用户(支持手机号或邮箱登录)
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ nickname: username },
|
||||
{ phone: username },
|
||||
{ email: username }
|
||||
]
|
||||
}
|
||||
@@ -76,8 +76,11 @@ const login = async (req, res) => {
|
||||
return res.status(401).json(errorResponse('用户名或密码错误', 401));
|
||||
}
|
||||
|
||||
// 为了简化测试,我们暂时跳过密码验证
|
||||
// 在实际应用中,您应该实现适当的密码验证机制
|
||||
// 简化的密码验证(用于测试)
|
||||
// 在生产环境中应该使用bcrypt进行密码哈希验证
|
||||
if (user.password_hash && user.password_hash !== password) {
|
||||
return res.status(401).json(errorResponse('用户名或密码错误', 401));
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
const token = jsonwebtoken.sign(
|
||||
|
||||
207
backend/src/controllers/DriverController.js
Normal file
207
backend/src/controllers/DriverController.js
Normal file
@@ -0,0 +1,207 @@
|
||||
const { successResponse, errorResponse, paginatedResponse } = require('../utils/response');
|
||||
const DriverService = require('../services/DriverService');
|
||||
|
||||
/**
|
||||
* 司机控制器
|
||||
* 处理司机相关的HTTP请求
|
||||
*/
|
||||
class DriverController {
|
||||
|
||||
/**
|
||||
* 创建司机
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async createDriver(req, res) {
|
||||
try {
|
||||
const driverData = req.body;
|
||||
|
||||
// 数据验证
|
||||
if (!driverData.name || !driverData.phone || !driverData.license_number) {
|
||||
return res.status(400).json(errorResponse('姓名、手机号和驾驶证号为必填项', 400));
|
||||
}
|
||||
|
||||
const driver = await DriverService.createDriver(driverData);
|
||||
|
||||
res.status(201).json(successResponse(driver, '司机创建成功'));
|
||||
} catch (error) {
|
||||
console.error('创建司机错误:', error);
|
||||
if (error.message.includes('已存在')) {
|
||||
res.status(400).json(errorResponse(error.message, 400));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取司机列表
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getDriverList(req, res) {
|
||||
try {
|
||||
const result = await DriverService.getDriverList(req.query);
|
||||
|
||||
res.json(paginatedResponse(
|
||||
result.drivers,
|
||||
result.total,
|
||||
result.page,
|
||||
result.pageSize,
|
||||
'获取司机列表成功'
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('获取司机列表错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取司机详情
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getDriverDetail(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的司机ID', 400));
|
||||
}
|
||||
|
||||
const driver = await DriverService.getDriverDetail(parseInt(id));
|
||||
|
||||
res.json(successResponse(driver, '获取司机详情成功'));
|
||||
} catch (error) {
|
||||
console.error('获取司机详情错误:', error);
|
||||
if (error.message.includes('不存在')) {
|
||||
res.status(404).json(errorResponse(error.message, 404));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新司机信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async updateDriver(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的司机ID', 400));
|
||||
}
|
||||
|
||||
const driver = await DriverService.updateDriver(parseInt(id), updateData);
|
||||
|
||||
res.json(successResponse(driver, '司机信息更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新司机信息错误:', error);
|
||||
if (error.message.includes('不存在') || error.message.includes('已存在')) {
|
||||
res.status(400).json(errorResponse(error.message, 400));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新司机状态
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async updateDriverStatus(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的司机ID', 400));
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return res.status(400).json(errorResponse('状态为必填项', 400));
|
||||
}
|
||||
|
||||
const validStatuses = ['available', 'busy', 'offline', 'suspended'];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return res.status(400).json(errorResponse('无效的状态值', 400));
|
||||
}
|
||||
|
||||
const driver = await DriverService.updateDriverStatus(parseInt(id), status);
|
||||
|
||||
res.json(successResponse(driver, '司机状态更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新司机状态错误:', error);
|
||||
if (error.message.includes('不存在') || error.message.includes('无法设置')) {
|
||||
res.status(400).json(errorResponse(error.message, 400));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除司机
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async deleteDriver(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的司机ID', 400));
|
||||
}
|
||||
|
||||
await DriverService.deleteDriver(parseInt(id));
|
||||
|
||||
res.json(successResponse(null, '司机删除成功'));
|
||||
} catch (error) {
|
||||
console.error('删除司机错误:', error);
|
||||
if (error.message.includes('不存在') || error.message.includes('有关联')) {
|
||||
res.status(400).json(errorResponse(error.message, 400));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用司机列表
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getAvailableDrivers(req, res) {
|
||||
try {
|
||||
const drivers = await DriverService.getAvailableDrivers(req.query);
|
||||
|
||||
res.json(successResponse(drivers, '获取可用司机列表成功'));
|
||||
} catch (error) {
|
||||
console.error('获取可用司机列表错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取司机统计信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getDriverStats(req, res) {
|
||||
try {
|
||||
const stats = await DriverService.getDriverStats(req.query);
|
||||
|
||||
res.json(successResponse(stats, '获取司机统计信息成功'));
|
||||
} catch (error) {
|
||||
console.error('获取司机统计信息错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DriverController;
|
||||
228
backend/src/controllers/SupplierController.js
Normal file
228
backend/src/controllers/SupplierController.js
Normal file
@@ -0,0 +1,228 @@
|
||||
const { successResponse, errorResponse, paginatedResponse } = require('../utils/response');
|
||||
const SupplierService = require('../services/SupplierService');
|
||||
|
||||
/**
|
||||
* 供应商控制器
|
||||
* 处理供应商相关的HTTP请求
|
||||
*/
|
||||
class SupplierController {
|
||||
|
||||
/**
|
||||
* 创建供应商
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async createSupplier(req, res) {
|
||||
try {
|
||||
const supplierData = req.body;
|
||||
|
||||
// 数据验证
|
||||
if (!supplierData.name || !supplierData.contact || !supplierData.phone) {
|
||||
return res.status(400).json(errorResponse('供应商名称、联系人和联系电话为必填项', 400));
|
||||
}
|
||||
|
||||
// 创建供应商
|
||||
const supplier = await SupplierService.createSupplier(supplierData);
|
||||
|
||||
res.status(201).json(successResponse(supplier, '供应商创建成功'));
|
||||
} catch (error) {
|
||||
console.error('创建供应商错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取供应商列表
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getSupplierList(req, res) {
|
||||
try {
|
||||
const result = await SupplierService.getSupplierList(req.query);
|
||||
|
||||
res.json(paginatedResponse(
|
||||
result.suppliers,
|
||||
result.total,
|
||||
result.page,
|
||||
result.pageSize,
|
||||
'获取供应商列表成功'
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('获取供应商列表错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取供应商详情
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getSupplierDetail(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的供应商ID', 400));
|
||||
}
|
||||
|
||||
const supplier = await SupplierService.getSupplierDetail(parseInt(id));
|
||||
|
||||
res.json(successResponse(supplier, '获取供应商详情成功'));
|
||||
} catch (error) {
|
||||
console.error('获取供应商详情错误:', error);
|
||||
if (error.message.includes('不存在')) {
|
||||
res.status(404).json(errorResponse(error.message, 404));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新供应商信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async updateSupplier(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的供应商ID', 400));
|
||||
}
|
||||
|
||||
// 过滤不允许更新的字段
|
||||
const allowedFields = [
|
||||
'name', 'contact', 'phone', 'email', 'address', 'region',
|
||||
'qualification_level', 'business_license', 'tax_number',
|
||||
'bank_account', 'bank_name', 'description'
|
||||
];
|
||||
|
||||
const filteredData = {};
|
||||
Object.keys(updateData).forEach(key => {
|
||||
if (allowedFields.includes(key)) {
|
||||
filteredData[key] = updateData[key];
|
||||
}
|
||||
});
|
||||
|
||||
const supplier = await SupplierService.updateSupplier(parseInt(id), filteredData);
|
||||
|
||||
res.json(successResponse(supplier, '供应商信息更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新供应商信息错误:', error);
|
||||
if (error.message.includes('不存在')) {
|
||||
res.status(404).json(errorResponse(error.message, 404));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新供应商状态
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async updateSupplierStatus(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的供应商ID', 400));
|
||||
}
|
||||
|
||||
if (!status || !['active', 'inactive', 'suspended'].includes(status)) {
|
||||
return res.status(400).json(errorResponse('无效的状态值', 400));
|
||||
}
|
||||
|
||||
const supplier = await SupplierService.updateSupplierStatus(parseInt(id), status);
|
||||
|
||||
res.json(successResponse(supplier, '供应商状态更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新供应商状态错误:', error);
|
||||
if (error.message.includes('不存在')) {
|
||||
res.status(404).json(errorResponse(error.message, 404));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新供应商评分
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async updateSupplierRating(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { rating } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的供应商ID', 400));
|
||||
}
|
||||
|
||||
if (!rating || isNaN(rating) || rating < 0 || rating > 5) {
|
||||
return res.status(400).json(errorResponse('评分必须是0-5之间的数字', 400));
|
||||
}
|
||||
|
||||
const supplier = await SupplierService.updateSupplierRating(parseInt(id), parseFloat(rating));
|
||||
|
||||
res.json(successResponse(supplier, '供应商评分更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新供应商评分错误:', error);
|
||||
if (error.message.includes('不存在')) {
|
||||
res.status(404).json(errorResponse(error.message, 404));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除供应商
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async deleteSupplier(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的供应商ID', 400));
|
||||
}
|
||||
|
||||
await SupplierService.deleteSupplier(parseInt(id));
|
||||
|
||||
res.json(successResponse(null, '供应商删除成功'));
|
||||
} catch (error) {
|
||||
console.error('删除供应商错误:', error);
|
||||
if (error.message.includes('不存在')) {
|
||||
res.status(404).json(errorResponse(error.message, 404));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取供应商统计信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getSupplierStats(req, res) {
|
||||
try {
|
||||
const stats = await SupplierService.getSupplierStats();
|
||||
|
||||
res.json(successResponse(stats, '获取供应商统计信息成功'));
|
||||
} catch (error) {
|
||||
console.error('获取供应商统计信息错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SupplierController;
|
||||
@@ -1,388 +1,290 @@
|
||||
const Transport = require('../models/Transport');
|
||||
const Vehicle = require('../models/Vehicle');
|
||||
const Order = require('../models/Order');
|
||||
const { Op } = require('sequelize');
|
||||
const { successResponse, errorResponse, paginatedResponse } = require('../utils/response');
|
||||
const TransportService = require('../services/TransportService');
|
||||
|
||||
// 获取运输列表
|
||||
exports.getTransportList = async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, status, orderId } = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (status) where.status = status;
|
||||
if (orderId) where.order_id = orderId;
|
||||
|
||||
// 分页查询
|
||||
const { count, rows } = await Transport.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(pageSize),
|
||||
offset: (parseInt(page) - 1) * parseInt(pageSize),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / pageSize)
|
||||
}
|
||||
/**
|
||||
* 运输控制器
|
||||
* 处理运输相关的HTTP请求
|
||||
*/
|
||||
class TransportController {
|
||||
|
||||
/**
|
||||
* 获取运输任务列表
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getTransportList(req, res) {
|
||||
try {
|
||||
const result = await TransportService.getTransportList(req.query);
|
||||
|
||||
res.json(paginatedResponse(
|
||||
result.transports,
|
||||
result.total,
|
||||
result.page,
|
||||
result.pageSize,
|
||||
'获取运输任务列表成功'
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('获取运输任务列表错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运输任务详情
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getTransportDetail(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的运输任务ID', 400));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取运输列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取运输详情
|
||||
exports.getTransportDetail = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const transport = await Transport.findByPk(id);
|
||||
if (!transport) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取关联的车辆信息
|
||||
const vehicle = await Vehicle.findByPk(transport.vehicle_id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...transport.toJSON(),
|
||||
vehicle
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取运输详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 创建运输记录
|
||||
exports.createTransport = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
order_id,
|
||||
driver_id,
|
||||
vehicle_id,
|
||||
start_location,
|
||||
end_location,
|
||||
scheduled_start_time,
|
||||
scheduled_end_time,
|
||||
cattle_count,
|
||||
special_requirements
|
||||
} = req.body;
|
||||
|
||||
// 检查订单是否存在
|
||||
const order = await Order.findByPk(order_id);
|
||||
if (!order) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查车辆是否存在
|
||||
const vehicle = await Vehicle.findByPk(vehicle_id);
|
||||
if (!vehicle) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '车辆不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建运输记录
|
||||
const transport = await Transport.create({
|
||||
order_id,
|
||||
driver_id,
|
||||
vehicle_id,
|
||||
start_location,
|
||||
end_location,
|
||||
scheduled_start_time,
|
||||
scheduled_end_time,
|
||||
cattle_count,
|
||||
special_requirements
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '运输记录创建成功',
|
||||
data: transport
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建运输记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建运输记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新运输记录
|
||||
exports.updateTransport = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
const transport = await Transport.findByPk(id);
|
||||
if (!transport) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新运输记录
|
||||
await transport.update(updateData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '运输记录更新成功',
|
||||
data: transport
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新运输记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新运输记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 删除运输记录
|
||||
exports.deleteTransport = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const transport = await Transport.findByPk(id);
|
||||
if (!transport) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 删除运输记录
|
||||
await transport.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '运输记录删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除运输记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除运输记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取车辆列表
|
||||
exports.getVehicleList = async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, status } = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (status) where.status = status;
|
||||
|
||||
// 分页查询
|
||||
const { count, rows } = await Vehicle.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(pageSize),
|
||||
offset: (parseInt(page) - 1) * parseInt(pageSize),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取车辆列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取车辆列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取车辆详情
|
||||
exports.getVehicleDetail = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const vehicle = await Vehicle.findByPk(id);
|
||||
if (!vehicle) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '车辆不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: vehicle
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取车辆详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取车辆详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 创建车辆记录
|
||||
exports.createVehicle = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
license_plate,
|
||||
vehicle_type,
|
||||
capacity,
|
||||
driver_id,
|
||||
status
|
||||
} = req.body;
|
||||
|
||||
// 检查车牌号是否已存在
|
||||
const existingVehicle = await Vehicle.findOne({ where: { license_plate } });
|
||||
if (existingVehicle) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '车牌号已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建车辆记录
|
||||
const vehicle = await Vehicle.create({
|
||||
license_plate,
|
||||
vehicle_type,
|
||||
capacity,
|
||||
driver_id,
|
||||
status
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '车辆记录创建成功',
|
||||
data: vehicle
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建车辆记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建车辆记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新车辆记录
|
||||
exports.updateVehicle = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
const vehicle = await Vehicle.findByPk(id);
|
||||
if (!vehicle) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '车辆不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查车牌号是否已存在(排除当前车辆)
|
||||
if (updateData.license_plate) {
|
||||
const existingVehicle = await Vehicle.findOne({
|
||||
where: {
|
||||
license_plate: updateData.license_plate,
|
||||
id: { [Op.ne]: id }
|
||||
}
|
||||
});
|
||||
if (existingVehicle) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '车牌号已存在'
|
||||
});
|
||||
|
||||
const transport = await TransportService.getTransportDetail(parseInt(id));
|
||||
|
||||
res.json(successResponse(transport, '获取运输任务详情成功'));
|
||||
} catch (error) {
|
||||
console.error('获取运输任务详情错误:', error);
|
||||
if (error.message.includes('不存在')) {
|
||||
res.status(404).json(errorResponse(error.message, 404));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
|
||||
// 更新车辆记录
|
||||
await vehicle.update(updateData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '车辆记录更新成功',
|
||||
data: vehicle
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新车辆记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新车辆记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 删除车辆记录
|
||||
exports.deleteVehicle = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const vehicle = await Vehicle.findByPk(id);
|
||||
if (!vehicle) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '车辆不存在'
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建运输任务
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async createTransport(req, res) {
|
||||
try {
|
||||
const transportData = req.body;
|
||||
|
||||
// 数据验证
|
||||
if (!transportData.order_id || !transportData.pickup_address || !transportData.delivery_address) {
|
||||
return res.status(400).json(errorResponse('订单ID、取货地址和送货地址为必填项', 400));
|
||||
}
|
||||
|
||||
// 创建运输任务
|
||||
const transport = await TransportService.createTransport(transportData);
|
||||
|
||||
res.status(201).json(successResponse(transport, '运输任务创建成功'));
|
||||
} catch (error) {
|
||||
console.error('创建运输任务错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
|
||||
// 删除车辆记录
|
||||
await vehicle.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '车辆记录删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除车辆记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除车辆记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新运输任务状态
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async updateTransportStatus(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, location, description, actual_weight, delivery_notes } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的运输任务ID', 400));
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return res.status(400).json(errorResponse('状态为必填项', 400));
|
||||
}
|
||||
|
||||
const validStatuses = ['pending', 'assigned', 'in_transit', 'delivered', 'completed', 'cancelled', 'exception'];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return res.status(400).json(errorResponse('无效的状态值', 400));
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (location) updateData.location = location;
|
||||
if (description) updateData.description = description;
|
||||
if (actual_weight) updateData.actual_weight = actual_weight;
|
||||
if (delivery_notes) updateData.delivery_notes = delivery_notes;
|
||||
|
||||
const transport = await TransportService.updateTransportStatus(parseInt(id), status, updateData);
|
||||
|
||||
res.json(successResponse(transport, '运输任务状态更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新运输任务状态错误:', error);
|
||||
if (error.message.includes('不存在')) {
|
||||
res.status(404).json(errorResponse(error.message, 404));
|
||||
} else if (error.message.includes('无法从状态')) {
|
||||
res.status(400).json(errorResponse(error.message, 400));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配司机和车辆
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async assignDriverAndVehicle(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { driver_id, vehicle_id } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的运输任务ID', 400));
|
||||
}
|
||||
|
||||
if (!driver_id || !vehicle_id || isNaN(driver_id) || isNaN(vehicle_id)) {
|
||||
return res.status(400).json(errorResponse('司机ID和车辆ID为必填项且必须是有效数字', 400));
|
||||
}
|
||||
|
||||
const transport = await TransportService.assignDriverAndVehicle(
|
||||
parseInt(id),
|
||||
parseInt(driver_id),
|
||||
parseInt(vehicle_id)
|
||||
);
|
||||
|
||||
res.json(successResponse(transport, '司机和车辆分配成功'));
|
||||
} catch (error) {
|
||||
console.error('分配司机和车辆错误:', error);
|
||||
if (error.message.includes('不存在') || error.message.includes('不可用')) {
|
||||
res.status(400).json(errorResponse(error.message, 400));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运输跟踪记录
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getTransportTracks(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的运输任务ID', 400));
|
||||
}
|
||||
|
||||
const tracks = await TransportService.getTransportTracks(parseInt(id));
|
||||
|
||||
res.json(successResponse(tracks, '获取运输跟踪记录成功'));
|
||||
} catch (error) {
|
||||
console.error('获取运输跟踪记录错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建跟踪记录
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async createTrackRecord(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, location, description } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的运输任务ID', 400));
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return res.status(400).json(errorResponse('状态为必填项', 400));
|
||||
}
|
||||
|
||||
const track = await TransportService.createTrackRecord(
|
||||
parseInt(id),
|
||||
status,
|
||||
location || '',
|
||||
description || ''
|
||||
);
|
||||
|
||||
res.status(201).json(successResponse(track, '跟踪记录创建成功'));
|
||||
} catch (error) {
|
||||
console.error('创建跟踪记录错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成运输任务
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async completeTransport(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const completionData = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的运输任务ID', 400));
|
||||
}
|
||||
|
||||
const transport = await TransportService.completeTransport(parseInt(id), completionData);
|
||||
|
||||
res.json(successResponse(transport, '运输任务完成成功'));
|
||||
} catch (error) {
|
||||
console.error('完成运输任务错误:', error);
|
||||
if (error.message.includes('不存在') || error.message.includes('只能完成')) {
|
||||
res.status(400).json(errorResponse(error.message, 400));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消运输任务
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async cancelTransport(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { reason } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return res.status(400).json(errorResponse('无效的运输任务ID', 400));
|
||||
}
|
||||
|
||||
if (!reason) {
|
||||
return res.status(400).json(errorResponse('取消原因为必填项', 400));
|
||||
}
|
||||
|
||||
const transport = await TransportService.cancelTransport(parseInt(id), reason);
|
||||
|
||||
res.json(successResponse(transport, '运输任务取消成功'));
|
||||
} catch (error) {
|
||||
console.error('取消运输任务错误:', error);
|
||||
if (error.message.includes('不存在') || error.message.includes('无法取消')) {
|
||||
res.status(400).json(errorResponse(error.message, 400));
|
||||
} else {
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运输统计信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
static async getTransportStats(req, res) {
|
||||
try {
|
||||
const stats = await TransportService.getTransportStats(req.query);
|
||||
|
||||
res.json(successResponse(stats, '获取运输统计信息成功'));
|
||||
} catch (error) {
|
||||
console.error('获取运输统计信息错误:', error);
|
||||
res.status(500).json(errorResponse(error.message || '服务器内部错误', 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TransportController;
|
||||
149
backend/src/controllers/VehicleController.js
Normal file
149
backend/src/controllers/VehicleController.js
Normal file
@@ -0,0 +1,149 @@
|
||||
const { successResponse, errorResponse } = require('../utils/response');
|
||||
const VehicleService = require('../services/VehicleService');
|
||||
|
||||
/**
|
||||
* 车辆控制器
|
||||
* 处理车辆相关的HTTP请求
|
||||
*/
|
||||
class VehicleController {
|
||||
/**
|
||||
* 创建车辆
|
||||
*/
|
||||
static async createVehicle(req, res) {
|
||||
try {
|
||||
const vehicle = await VehicleService.createVehicle(req.body);
|
||||
return successResponse(res, vehicle, '车辆创建成功', 201);
|
||||
} catch (error) {
|
||||
return errorResponse(res, error.message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆列表
|
||||
*/
|
||||
static async getVehicleList(req, res) {
|
||||
try {
|
||||
const options = {
|
||||
page: parseInt(req.query.page) || 1,
|
||||
pageSize: parseInt(req.query.pageSize) || 10,
|
||||
status: req.query.status,
|
||||
vehicle_type: req.query.vehicle_type,
|
||||
brand: req.query.brand,
|
||||
search: req.query.search
|
||||
};
|
||||
|
||||
const result = await VehicleService.getVehicleList(options);
|
||||
return successResponse(res, result, '获取车辆列表成功');
|
||||
} catch (error) {
|
||||
return errorResponse(res, error.message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆详情
|
||||
*/
|
||||
static async getVehicleDetail(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const vehicle = await VehicleService.getVehicleDetail(id);
|
||||
return successResponse(res, vehicle, '获取车辆详情成功');
|
||||
} catch (error) {
|
||||
return errorResponse(res, error.message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新车辆信息
|
||||
*/
|
||||
static async updateVehicle(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const vehicle = await VehicleService.updateVehicle(id, req.body);
|
||||
return successResponse(res, vehicle, '车辆信息更新成功');
|
||||
} catch (error) {
|
||||
return errorResponse(res, error.message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新车辆状态
|
||||
*/
|
||||
static async updateVehicleStatus(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
if (!status) {
|
||||
return errorResponse(res, '状态参数不能为空', 400);
|
||||
}
|
||||
|
||||
const vehicle = await VehicleService.updateVehicleStatus(id, status);
|
||||
return successResponse(res, vehicle, '车辆状态更新成功');
|
||||
} catch (error) {
|
||||
return errorResponse(res, error.message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除车辆
|
||||
*/
|
||||
static async deleteVehicle(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await VehicleService.deleteVehicle(id);
|
||||
return successResponse(res, null, '车辆删除成功');
|
||||
} catch (error) {
|
||||
return errorResponse(res, error.message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用车辆列表
|
||||
*/
|
||||
static async getAvailableVehicles(req, res) {
|
||||
try {
|
||||
const options = {
|
||||
vehicle_type: req.query.vehicle_type,
|
||||
load_capacity_min: req.query.load_capacity_min
|
||||
};
|
||||
|
||||
const vehicles = await VehicleService.getAvailableVehicles(options);
|
||||
return successResponse(res, vehicles, '获取可用车辆列表成功');
|
||||
} catch (error) {
|
||||
return errorResponse(res, error.message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配司机
|
||||
*/
|
||||
static async assignDriver(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { driver_id } = req.body;
|
||||
|
||||
if (!driver_id) {
|
||||
return errorResponse(res, '司机ID不能为空', 400);
|
||||
}
|
||||
|
||||
const vehicle = await VehicleService.assignDriver(id, driver_id);
|
||||
return successResponse(res, vehicle, '司机分配成功');
|
||||
} catch (error) {
|
||||
return errorResponse(res, error.message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆统计信息
|
||||
*/
|
||||
static async getVehicleStatistics(req, res) {
|
||||
try {
|
||||
const statistics = await VehicleService.getVehicleStatistics();
|
||||
return successResponse(res, statistics, '获取车辆统计信息成功');
|
||||
} catch (error) {
|
||||
return errorResponse(res, error.message, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VehicleController;
|
||||
@@ -1,94 +1,257 @@
|
||||
// 加载.env文件
|
||||
/**
|
||||
* 活牛采购智能数字化系统 - 后端服务主入口文件
|
||||
*
|
||||
* 功能特性:
|
||||
* - 统一的Express应用配置
|
||||
* - 完整的中间件配置(安全、跨域、日志、限流等)
|
||||
* - 数据库连接和模型同步
|
||||
* - API路由配置
|
||||
* - Swagger API文档
|
||||
* - 健康检查和错误处理
|
||||
*
|
||||
* @author NiuMall Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
// 加载环境变量配置
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const morgan = require('morgan');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const compression = require('compression');
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
const YAML = require('yamljs');
|
||||
const path = require('path');
|
||||
|
||||
// 打印环境变量用于调试
|
||||
console.log('Environment variables:');
|
||||
console.log('DB_HOST:', process.env.DB_HOST);
|
||||
console.log('DB_PORT:', process.env.DB_PORT);
|
||||
console.log('DB_USERNAME:', process.env.DB_USERNAME);
|
||||
console.log('DB_NAME:', process.env.DB_NAME);
|
||||
console.log('NODE_ENV:', process.env.NODE_ENV);
|
||||
console.log('PORT:', process.env.PORT);
|
||||
// 打印环境变量用于调试(仅在开发环境)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('🔧 Environment variables:');
|
||||
console.log('DB_HOST:', process.env.DB_HOST);
|
||||
console.log('DB_PORT:', process.env.DB_PORT);
|
||||
console.log('DB_USERNAME:', process.env.DB_USERNAME);
|
||||
console.log('DB_NAME:', process.env.DB_NAME);
|
||||
console.log('NODE_ENV:', process.env.NODE_ENV);
|
||||
console.log('PORT:', process.env.PORT);
|
||||
}
|
||||
|
||||
// 数据库连接
|
||||
const sequelize = require('./config/database');
|
||||
|
||||
// 路由
|
||||
// 路由模块
|
||||
const authRoutes = require('./routes/auth');
|
||||
const userRoutes = require('./routes/users');
|
||||
const orderRoutes = require('./routes/orders');
|
||||
const paymentRoutes = require('./routes/payments');
|
||||
const supplierRoutes = require('./routes/suppliers');
|
||||
const transportRoutes = require('./routes/transports');
|
||||
const driverRoutes = require('./routes/drivers');
|
||||
const vehicleRoutes = require('./routes/vehicles');
|
||||
|
||||
// 创建Express应用
|
||||
// 创建Express应用实例
|
||||
const app = express();
|
||||
|
||||
// 中间件
|
||||
app.use(helmet()); // 安全防护
|
||||
app.use(cors()); // 跨域支持
|
||||
app.use(morgan('combined')); // HTTP请求日志
|
||||
app.use(express.json()); // JSON解析
|
||||
app.use(express.urlencoded({ extended: true })); // URL编码解析
|
||||
// ==================== 中间件配置 ====================
|
||||
|
||||
// 安全防护中间件
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: false, // 允许Swagger UI正常工作
|
||||
}));
|
||||
|
||||
// 跨域支持配置
|
||||
app.use(cors({
|
||||
origin: [
|
||||
'http://localhost:3000',
|
||||
'http://localhost:5173',
|
||||
'https://wapi.nanniwan.com',
|
||||
'https://ad.nanniwan.com',
|
||||
'https://www.nanniwan.com'
|
||||
],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// 压缩响应数据
|
||||
app.use(compression());
|
||||
|
||||
// HTTP请求日志
|
||||
app.use(morgan('combined'));
|
||||
|
||||
// JSON和URL编码解析(支持大文件上传)
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// API限流配置
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15分钟
|
||||
max: 100, // 每个IP最多100个请求
|
||||
message: {
|
||||
success: false,
|
||||
message: '请求过于频繁,请稍后重试'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
app.use('/api', limiter);
|
||||
|
||||
// 自定义日志中间件
|
||||
const logger = require('./middleware/logger');
|
||||
app.use(logger);
|
||||
|
||||
// ==================== 路由配置 ====================
|
||||
|
||||
// 健康检查路由
|
||||
const healthCheckRoutes = require('./middleware/healthCheck');
|
||||
app.use('/', healthCheckRoutes);
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '服务运行正常',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
});
|
||||
});
|
||||
|
||||
// Swagger UI
|
||||
const swaggerDocument = YAML.load('./src/docs/api.yaml');
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
||||
// 基本路由
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: '活牛采购智能数字化系统后端服务',
|
||||
version: '1.0.0',
|
||||
documentation: '/api-docs',
|
||||
health: '/health'
|
||||
});
|
||||
});
|
||||
|
||||
// API路由
|
||||
// Swagger API文档配置
|
||||
try {
|
||||
const swaggerDocument = YAML.load(path.join(__dirname, 'docs/api.yaml'));
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, {
|
||||
explorer: true,
|
||||
customCss: '.swagger-ui .topbar { background-color: #3B82F6; }',
|
||||
customSiteTitle: 'NiuMall API 文档'
|
||||
}));
|
||||
|
||||
// 提供Swagger JSON文件
|
||||
app.get('/api-docs-json', (req, res) => {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(swaggerDocument);
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Swagger文档加载失败:', error.message);
|
||||
}
|
||||
|
||||
// API路由注册
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/orders', orderRoutes);
|
||||
app.use('/api/payments', paymentRoutes);
|
||||
app.use('/api/suppliers', supplierRoutes);
|
||||
app.use('/api/transports', transportRoutes);
|
||||
app.use('/api/drivers', driverRoutes);
|
||||
app.use('/api/vehicles', vehicleRoutes);
|
||||
|
||||
// 基本路由
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: '活牛采购智能数字化系统后端服务',
|
||||
version: '1.0.0'
|
||||
// ==================== 错误处理 ====================
|
||||
|
||||
// 404处理
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '请求的资源不存在',
|
||||
path: req.originalUrl
|
||||
});
|
||||
});
|
||||
|
||||
// 错误处理中间件
|
||||
// 全局错误处理中间件
|
||||
const errorHandler = require('./middleware/errorHandler');
|
||||
app.use(errorHandler);
|
||||
|
||||
// 同步数据库模型
|
||||
// ==================== 数据库和服务器启动 ====================
|
||||
|
||||
/**
|
||||
* 同步数据库模型
|
||||
* 不修改现有表结构,只创建不存在的表
|
||||
*/
|
||||
const syncDatabase = async () => {
|
||||
try {
|
||||
// 不修改现有表结构,只创建不存在的表
|
||||
// 测试数据库连接
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功');
|
||||
|
||||
// 同步模型(不强制重建表)
|
||||
await sequelize.sync({ force: false });
|
||||
console.log('数据库模型同步成功');
|
||||
console.log('✅ 数据库模型同步成功');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('数据库模型同步失败:', error);
|
||||
console.log('服务器仍然会继续运行,但某些功能可能受到影响');
|
||||
console.error('❌ 数据库连接或同步失败:', error.message);
|
||||
|
||||
// 在生产环境中,数据库连接失败应该终止服务
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.error('生产环境数据库连接失败,服务器启动终止');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('⚠️ 服务器仍然会继续运行,但某些功能可能受到影响');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 启动服务器
|
||||
const { serverConfig } = require('./config/config');
|
||||
const PORT = serverConfig.port;
|
||||
app.listen(PORT, async () => {
|
||||
console.log(`服务器运行在端口 ${PORT}`);
|
||||
// 同步数据库
|
||||
await syncDatabase();
|
||||
});
|
||||
/**
|
||||
* 启动服务器
|
||||
*/
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// 同步数据库
|
||||
await syncDatabase();
|
||||
|
||||
// 获取端口配置
|
||||
const { serverConfig } = require('./config/config');
|
||||
const PORT = process.env.PORT || serverConfig.port || 4330;
|
||||
|
||||
// 启动HTTP服务器
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log('\n🚀 ===== 服务器启动成功 =====');
|
||||
console.log(`📱 运行环境: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`🌐 访问地址: http://localhost:${PORT}`);
|
||||
console.log(`📊 健康检查: http://localhost:${PORT}/health`);
|
||||
console.log(`📚 API文档: http://localhost:${PORT}/api-docs`);
|
||||
console.log(`📄 API文档JSON: http://localhost:${PORT}/api-docs-json`);
|
||||
console.log('================================\n');
|
||||
});
|
||||
|
||||
// 优雅关闭处理
|
||||
const gracefulShutdown = (signal) => {
|
||||
console.log(`\n收到 ${signal} 信号,开始优雅关闭服务器...`);
|
||||
|
||||
server.close(async () => {
|
||||
console.log('HTTP服务器已关闭');
|
||||
|
||||
try {
|
||||
await sequelize.close();
|
||||
console.log('数据库连接已关闭');
|
||||
} catch (error) {
|
||||
console.error('关闭数据库连接时出错:', error);
|
||||
}
|
||||
|
||||
console.log('服务器已完全关闭');
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
// 监听进程信号
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 服务器启动失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// 只在非测试环境下启动服务器
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
startServer();
|
||||
}
|
||||
|
||||
// 导出app实例供测试使用
|
||||
module.exports = app;
|
||||
@@ -1,16 +1,30 @@
|
||||
/**
|
||||
* 认证授权中间件
|
||||
*
|
||||
* 功能特性:
|
||||
* - JWT Token验证
|
||||
* - 用户角色权限检查
|
||||
* - 资源访问权限控制
|
||||
* - 统一的错误响应格式
|
||||
*
|
||||
* @author NiuMall Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { jwtConfig } = require('../config/config');
|
||||
const { errorResponse } = require('../utils/response');
|
||||
|
||||
// 认证中间件
|
||||
const authenticate = (req, res, next) => {
|
||||
/**
|
||||
* JWT Token认证中间件
|
||||
* 验证请求头中的Bearer Token
|
||||
*/
|
||||
const authenticateToken = (req, res, next) => {
|
||||
try {
|
||||
// 从请求头获取token
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: '未提供认证token'
|
||||
});
|
||||
return errorResponse(res, '未提供认证token', 401);
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
@@ -20,35 +34,100 @@ const authenticate = (req, res, next) => {
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: '无效的认证token'
|
||||
});
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return errorResponse(res, 'Token已过期', 401);
|
||||
} else if (error.name === 'JsonWebTokenError') {
|
||||
return errorResponse(res, '无效的Token', 401);
|
||||
}
|
||||
return errorResponse(res, '认证失败', 401);
|
||||
}
|
||||
};
|
||||
|
||||
// 角色权限检查中间件
|
||||
/**
|
||||
* 角色权限检查中间件
|
||||
* @param {Array} roles - 允许访问的角色列表
|
||||
*/
|
||||
const checkRole = (roles) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: '未认证'
|
||||
});
|
||||
return errorResponse(res, '未认证', 401);
|
||||
}
|
||||
|
||||
if (!roles.includes(req.user.userType)) {
|
||||
return res.status(403).json({
|
||||
code: 403,
|
||||
message: '权限不足'
|
||||
});
|
||||
return errorResponse(res, '权限不足,需要角色:' + roles.join('或'), 403);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 管理员权限检查中间件
|
||||
*/
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return errorResponse(res, '未认证', 401);
|
||||
}
|
||||
|
||||
if (req.user.userType !== 'admin') {
|
||||
return errorResponse(res, '需要管理员权限', 403);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* 资源所有者权限检查中间件
|
||||
* 检查用户是否有权访问特定资源
|
||||
*/
|
||||
const checkResourceOwner = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return errorResponse(res, '未认证', 401);
|
||||
}
|
||||
|
||||
// 管理员可以访问所有资源
|
||||
if (req.user.userType === 'admin') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 检查资源所有者
|
||||
const resourceUserId = req.params.userId || req.body.userId || req.query.userId;
|
||||
if (resourceUserId && resourceUserId !== req.user.id.toString()) {
|
||||
return errorResponse(res, '只能访问自己的资源', 403);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* 可选认证中间件
|
||||
* 如果提供了token则验证,否则继续执行
|
||||
*/
|
||||
const optionalAuth = (req, res, next) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const token = authHeader.split(' ')[1];
|
||||
const decoded = jwt.verify(token, jwtConfig.secret);
|
||||
req.user = decoded;
|
||||
} catch (error) {
|
||||
// 可选认证失败时不返回错误,继续执行
|
||||
console.warn('可选认证失败:', error.message);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authenticate,
|
||||
checkRole
|
||||
authenticateToken,
|
||||
checkRole,
|
||||
requireAdmin,
|
||||
checkResourceOwner,
|
||||
optionalAuth,
|
||||
// 向后兼容
|
||||
authenticate: authenticateToken
|
||||
};
|
||||
233
backend/src/middleware/validation.js
Normal file
233
backend/src/middleware/validation.js
Normal file
@@ -0,0 +1,233 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
/**
|
||||
* 供应商验证规则
|
||||
*/
|
||||
const supplierSchema = Joi.object({
|
||||
name: Joi.string().min(2).max(100).required().messages({
|
||||
'string.empty': '供应商名称不能为空',
|
||||
'string.min': '供应商名称至少2个字符',
|
||||
'string.max': '供应商名称不能超过100个字符',
|
||||
'any.required': '供应商名称为必填项'
|
||||
}),
|
||||
code: Joi.string().max(50).optional(),
|
||||
contact_person: Joi.string().max(50).optional(),
|
||||
contact_phone: Joi.string().pattern(/^1[3-9]\d{9}$/).optional().messages({
|
||||
'string.pattern.base': '请输入有效的手机号码'
|
||||
}),
|
||||
contact_email: Joi.string().email().optional().messages({
|
||||
'string.email': '请输入有效的邮箱地址'
|
||||
}),
|
||||
address: Joi.string().max(200).optional(),
|
||||
qualification_level: Joi.string().valid('A', 'B', 'C', 'D').optional(),
|
||||
business_license: Joi.string().max(100).optional(),
|
||||
tax_number: Joi.string().max(50).optional(),
|
||||
bank_account: Joi.string().max(50).optional(),
|
||||
bank_name: Joi.string().max(100).optional(),
|
||||
credit_rating: Joi.number().min(0).max(5).optional(),
|
||||
cooperation_years: Joi.number().integer().min(0).optional(),
|
||||
status: Joi.string().valid('active', 'inactive', 'suspended').optional(),
|
||||
notes: Joi.string().max(500).optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* 订单验证规则
|
||||
*/
|
||||
const orderSchema = Joi.object({
|
||||
order_number: Joi.string().max(50).optional(),
|
||||
buyer_id: Joi.number().integer().positive().required().messages({
|
||||
'number.base': '采购人ID必须是数字',
|
||||
'number.positive': '采购人ID必须是正数',
|
||||
'any.required': '采购人ID为必填项'
|
||||
}),
|
||||
supplier_id: Joi.number().integer().positive().required().messages({
|
||||
'number.base': '供应商ID必须是数字',
|
||||
'number.positive': '供应商ID必须是正数',
|
||||
'any.required': '供应商ID为必填项'
|
||||
}),
|
||||
cattle_type: Joi.string().valid('beef', 'dairy', 'breeding').required().messages({
|
||||
'any.only': '牛只类型必须是beef、dairy或breeding之一',
|
||||
'any.required': '牛只类型为必填项'
|
||||
}),
|
||||
quantity: Joi.number().integer().positive().required().messages({
|
||||
'number.base': '数量必须是数字',
|
||||
'number.integer': '数量必须是整数',
|
||||
'number.positive': '数量必须是正数',
|
||||
'any.required': '数量为必填项'
|
||||
}),
|
||||
unit_price: Joi.number().positive().required().messages({
|
||||
'number.base': '单价必须是数字',
|
||||
'number.positive': '单价必须是正数',
|
||||
'any.required': '单价为必填项'
|
||||
}),
|
||||
total_amount: Joi.number().positive().optional(),
|
||||
delivery_date: Joi.date().iso().optional(),
|
||||
delivery_address: Joi.string().max(200).optional(),
|
||||
special_requirements: Joi.string().max(500).optional(),
|
||||
status: Joi.string().valid('pending', 'confirmed', 'in_production', 'ready', 'shipped', 'delivered', 'completed', 'cancelled').optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* 用户验证规则
|
||||
*/
|
||||
const userSchema = Joi.object({
|
||||
username: Joi.string().alphanum().min(3).max(30).optional(),
|
||||
email: Joi.string().email().optional().messages({
|
||||
'string.email': '请输入有效的邮箱地址'
|
||||
}),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).optional().messages({
|
||||
'string.pattern.base': '请输入有效的手机号码'
|
||||
}),
|
||||
password: Joi.string().min(6).max(128).optional().messages({
|
||||
'string.min': '密码至少6个字符',
|
||||
'string.max': '密码不能超过128个字符'
|
||||
}),
|
||||
real_name: Joi.string().max(50).optional(),
|
||||
user_type: Joi.string().valid('buyer', 'supplier', 'driver', 'admin').optional(),
|
||||
company_name: Joi.string().max(100).optional(),
|
||||
business_license: Joi.string().max(100).optional(),
|
||||
address: Joi.string().max(200).optional(),
|
||||
status: Joi.string().valid('active', 'inactive', 'suspended').optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* 司机验证规则
|
||||
*/
|
||||
const driverSchema = Joi.object({
|
||||
name: Joi.string().min(2).max(50).required().messages({
|
||||
'string.empty': '司机姓名不能为空',
|
||||
'string.min': '司机姓名至少2个字符',
|
||||
'string.max': '司机姓名不能超过50个字符',
|
||||
'any.required': '司机姓名为必填项'
|
||||
}),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required().messages({
|
||||
'string.pattern.base': '请输入有效的手机号码',
|
||||
'any.required': '手机号为必填项'
|
||||
}),
|
||||
id_card: Joi.string().pattern(/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/).optional().messages({
|
||||
'string.pattern.base': '请输入有效的身份证号'
|
||||
}),
|
||||
license_number: Joi.string().required().messages({
|
||||
'string.empty': '驾驶证号不能为空',
|
||||
'any.required': '驾驶证号为必填项'
|
||||
}),
|
||||
license_type: Joi.string().valid('A1', 'A2', 'A3', 'B1', 'B2', 'C1', 'C2').required().messages({
|
||||
'any.only': '驾驶证类型必须是A1、A2、A3、B1、B2、C1、C2之一',
|
||||
'any.required': '驾驶证类型为必填项'
|
||||
}),
|
||||
license_expire_date: Joi.date().iso().optional(),
|
||||
experience_years: Joi.number().integer().min(0).optional(),
|
||||
status: Joi.string().valid('available', 'busy', 'offline', 'suspended').optional(),
|
||||
emergency_contact: Joi.string().max(50).optional(),
|
||||
emergency_phone: Joi.string().pattern(/^1[3-9]\d{9}$/).optional().messages({
|
||||
'string.pattern.base': '请输入有效的紧急联系电话'
|
||||
}),
|
||||
address: Joi.string().max(200).optional(),
|
||||
notes: Joi.string().max(500).optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* 运输验证规则
|
||||
*/
|
||||
const transportSchema = Joi.object({
|
||||
order_id: Joi.number().integer().positive().required().messages({
|
||||
'number.base': '订单ID必须是数字',
|
||||
'number.positive': '订单ID必须是正数',
|
||||
'any.required': '订单ID为必填项'
|
||||
}),
|
||||
transport_number: Joi.string().max(50).optional(),
|
||||
driver_id: Joi.number().integer().positive().optional(),
|
||||
vehicle_id: Joi.number().integer().positive().optional(),
|
||||
pickup_address: Joi.string().max(200).required().messages({
|
||||
'string.empty': '取货地址不能为空',
|
||||
'any.required': '取货地址为必填项'
|
||||
}),
|
||||
delivery_address: Joi.string().max(200).required().messages({
|
||||
'string.empty': '送货地址不能为空',
|
||||
'any.required': '送货地址为必填项'
|
||||
}),
|
||||
scheduled_pickup_time: Joi.date().iso().optional(),
|
||||
scheduled_delivery_time: Joi.date().iso().optional(),
|
||||
estimated_weight: Joi.number().positive().optional(),
|
||||
actual_weight: Joi.number().positive().optional(),
|
||||
cattle_count: Joi.number().integer().positive().optional(),
|
||||
special_requirements: Joi.string().max(500).optional(),
|
||||
status: Joi.string().valid('pending', 'assigned', 'in_transit', 'delivered', 'completed', 'cancelled', 'exception').optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建验证中间件
|
||||
* @param {Object} schema - Joi验证规则
|
||||
* @returns {Function} 验证中间件函数
|
||||
*/
|
||||
function createValidationMiddleware(schema) {
|
||||
return (req, res, next) => {
|
||||
const { error } = schema.validate(req.body, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
const errors = error.details.map(detail => ({
|
||||
field: detail.path.join('.'),
|
||||
message: detail.message
|
||||
}));
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '数据验证失败',
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证ID参数
|
||||
*/
|
||||
const validateId = (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id) || parseInt(id) <= 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '无效的ID参数'
|
||||
});
|
||||
}
|
||||
|
||||
req.params.id = parseInt(id);
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证分页参数
|
||||
*/
|
||||
const validatePagination = (req, res, next) => {
|
||||
const { page, pageSize } = req.query;
|
||||
|
||||
if (page && (isNaN(page) || parseInt(page) <= 0)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '页码必须是正整数'
|
||||
});
|
||||
}
|
||||
|
||||
if (pageSize && (isNaN(pageSize) || parseInt(pageSize) <= 0 || parseInt(pageSize) > 100)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '每页数量必须是1-100之间的正整数'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// 导出验证中间件
|
||||
module.exports = {
|
||||
validateSupplier: createValidationMiddleware(supplierSchema),
|
||||
validateOrder: createValidationMiddleware(orderSchema),
|
||||
validateUser: createValidationMiddleware(userSchema),
|
||||
validateDriver: createValidationMiddleware(driverSchema),
|
||||
validateTransport: createValidationMiddleware(transportSchema),
|
||||
validateId,
|
||||
validatePagination
|
||||
};
|
||||
159
backend/src/models/Driver.js
Normal file
159
backend/src/models/Driver.js
Normal file
@@ -0,0 +1,159 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
/**
|
||||
* 司机模型
|
||||
* 管理运输司机的基本信息、驾驶资质和工作状态
|
||||
*/
|
||||
const Driver = sequelize.define('Driver', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '司机ID'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '司机姓名'
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '联系电话'
|
||||
},
|
||||
id_card: {
|
||||
type: DataTypes.STRING(18),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '身份证号'
|
||||
},
|
||||
driver_license: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '驾驶证号'
|
||||
},
|
||||
license_type: {
|
||||
type: DataTypes.ENUM('A1', 'A2', 'B1', 'B2', 'C1', 'C2'),
|
||||
allowNull: false,
|
||||
comment: '驾驶证类型'
|
||||
},
|
||||
license_expiry_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
comment: '驾驶证到期日期'
|
||||
},
|
||||
qualification_certificate: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '从业资格证文件路径'
|
||||
},
|
||||
qualification_expiry_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '从业资格证到期日期'
|
||||
},
|
||||
emergency_contact: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '紧急联系人'
|
||||
},
|
||||
emergency_phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true,
|
||||
comment: '紧急联系电话'
|
||||
},
|
||||
address: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: true,
|
||||
comment: '家庭住址'
|
||||
},
|
||||
experience_years: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '驾驶经验年数'
|
||||
},
|
||||
transport_experience_years: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '运输行业经验年数'
|
||||
},
|
||||
rating: {
|
||||
type: DataTypes.DECIMAL(3, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0.00,
|
||||
comment: '综合评分(0-5分)'
|
||||
},
|
||||
total_orders: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '累计运输订单数'
|
||||
},
|
||||
completed_orders: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '完成订单数'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('available', 'busy', 'offline', 'suspended'),
|
||||
allowNull: false,
|
||||
defaultValue: 'available',
|
||||
comment: '司机状态: available(可用), busy(忙碌), offline(离线), suspended(暂停)'
|
||||
},
|
||||
current_vehicle_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
comment: '当前使用车辆ID'
|
||||
},
|
||||
last_location: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
comment: '最后位置信息(经纬度)'
|
||||
},
|
||||
last_active_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '最后活跃时间'
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注信息'
|
||||
}
|
||||
}, {
|
||||
tableName: 'drivers',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['phone'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['id_card'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['driver_license'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['status']
|
||||
},
|
||||
{
|
||||
fields: ['rating']
|
||||
},
|
||||
{
|
||||
fields: ['current_vehicle_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = Driver;
|
||||
166
backend/src/models/QualityRecord.js
Normal file
166
backend/src/models/QualityRecord.js
Normal file
@@ -0,0 +1,166 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
/**
|
||||
* 质检记录模型
|
||||
* 管理牛只质量检验、验收确认和相关证明文件
|
||||
*/
|
||||
const QualityRecord = sequelize.define('QualityRecord', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '质检记录ID'
|
||||
},
|
||||
order_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '关联订单ID'
|
||||
},
|
||||
inspector_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '检验员ID'
|
||||
},
|
||||
inspection_type: {
|
||||
type: DataTypes.ENUM('pre_loading', 'in_transit', 'arrival', 'final'),
|
||||
allowNull: false,
|
||||
comment: '检验类型: pre_loading(装车前), in_transit(运输中), arrival(到货), final(最终验收)'
|
||||
},
|
||||
inspection_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
comment: '检验日期'
|
||||
},
|
||||
location: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
comment: '检验地点'
|
||||
},
|
||||
cattle_count_expected: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '预期牛只数量'
|
||||
},
|
||||
cattle_count_actual: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '实际牛只数量'
|
||||
},
|
||||
weight_expected: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: true,
|
||||
comment: '预期总重量(kg)'
|
||||
},
|
||||
weight_actual: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: true,
|
||||
comment: '实际总重量(kg)'
|
||||
},
|
||||
health_status: {
|
||||
type: DataTypes.ENUM('excellent', 'good', 'fair', 'poor'),
|
||||
allowNull: false,
|
||||
comment: '健康状况: excellent(优秀), good(良好), fair(一般), poor(较差)'
|
||||
},
|
||||
breed_verification: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '品种验证是否通过'
|
||||
},
|
||||
age_range_verification: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '年龄范围验证是否通过'
|
||||
},
|
||||
quarantine_certificate: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '检疫证明文件路径'
|
||||
},
|
||||
health_certificate: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '健康证明文件路径'
|
||||
},
|
||||
photos: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
comment: '检验照片路径数组(JSON格式)'
|
||||
},
|
||||
videos: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
comment: '检验视频路径数组(JSON格式)'
|
||||
},
|
||||
quality_issues: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
comment: '质量问题记录(JSON格式)'
|
||||
},
|
||||
overall_rating: {
|
||||
type: DataTypes.DECIMAL(3, 2),
|
||||
allowNull: false,
|
||||
defaultValue: 0.00,
|
||||
comment: '综合评分(0-5分)'
|
||||
},
|
||||
pass_status: {
|
||||
type: DataTypes.ENUM('passed', 'conditional_pass', 'failed'),
|
||||
allowNull: false,
|
||||
comment: '检验结果: passed(通过), conditional_pass(有条件通过), failed(不通过)'
|
||||
},
|
||||
rejection_reason: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '拒收原因'
|
||||
},
|
||||
inspector_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '检验员备注'
|
||||
},
|
||||
buyer_confirmation: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
comment: '买方确认'
|
||||
},
|
||||
buyer_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '买方备注'
|
||||
},
|
||||
confirmation_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '确认时间'
|
||||
}
|
||||
}, {
|
||||
tableName: 'quality_records',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['order_id']
|
||||
},
|
||||
{
|
||||
fields: ['inspector_id']
|
||||
},
|
||||
{
|
||||
fields: ['inspection_type']
|
||||
},
|
||||
{
|
||||
fields: ['inspection_date']
|
||||
},
|
||||
{
|
||||
fields: ['pass_status']
|
||||
},
|
||||
{
|
||||
fields: ['buyer_confirmation']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = QualityRecord;
|
||||
187
backend/src/models/Settlement.js
Normal file
187
backend/src/models/Settlement.js
Normal file
@@ -0,0 +1,187 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
/**
|
||||
* 结算记录模型
|
||||
* 管理订单的财务结算、支付记录和发票信息
|
||||
*/
|
||||
const Settlement = sequelize.define('Settlement', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '结算记录ID'
|
||||
},
|
||||
order_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '关联订单ID'
|
||||
},
|
||||
settlement_no: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '结算单号'
|
||||
},
|
||||
settlement_type: {
|
||||
type: DataTypes.ENUM('advance', 'final', 'full'),
|
||||
allowNull: false,
|
||||
comment: '结算类型: advance(预付款), final(尾款), full(全款)'
|
||||
},
|
||||
cattle_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '结算牛只数量'
|
||||
},
|
||||
unit_price: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
comment: '单价(元/公斤)'
|
||||
},
|
||||
total_weight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
comment: '总重量(公斤)'
|
||||
},
|
||||
gross_amount: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
comment: '应付总金额'
|
||||
},
|
||||
deduction_amount: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
defaultValue: 0.00,
|
||||
comment: '扣款金额'
|
||||
},
|
||||
deduction_reason: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '扣款原因'
|
||||
},
|
||||
net_amount: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
comment: '实付金额'
|
||||
},
|
||||
tax_rate: {
|
||||
type: DataTypes.DECIMAL(5, 4),
|
||||
allowNull: false,
|
||||
defaultValue: 0.0000,
|
||||
comment: '税率'
|
||||
},
|
||||
tax_amount: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
defaultValue: 0.00,
|
||||
comment: '税额'
|
||||
},
|
||||
payment_method: {
|
||||
type: DataTypes.ENUM('bank_transfer', 'cash', 'check', 'online_payment'),
|
||||
allowNull: false,
|
||||
comment: '支付方式: bank_transfer(银行转账), cash(现金), check(支票), online_payment(在线支付)'
|
||||
},
|
||||
payment_account: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '支付账户'
|
||||
},
|
||||
payment_reference: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '支付凭证号'
|
||||
},
|
||||
settlement_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
comment: '结算日期'
|
||||
},
|
||||
payment_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '实际支付日期'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'approved', 'paid', 'cancelled'),
|
||||
allowNull: false,
|
||||
defaultValue: 'pending',
|
||||
comment: '结算状态: pending(待处理), approved(已审批), paid(已支付), cancelled(已取消)'
|
||||
},
|
||||
invoice_required: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
comment: '是否需要发票'
|
||||
},
|
||||
invoice_type: {
|
||||
type: DataTypes.ENUM('ordinary', 'special', 'electronic'),
|
||||
allowNull: true,
|
||||
comment: '发票类型: ordinary(普通发票), special(专用发票), electronic(电子发票)'
|
||||
},
|
||||
invoice_number: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '发票号码'
|
||||
},
|
||||
invoice_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '开票日期'
|
||||
},
|
||||
invoice_file: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '发票文件路径'
|
||||
},
|
||||
approver_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
comment: '审批人ID'
|
||||
},
|
||||
approval_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '审批时间'
|
||||
},
|
||||
approval_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '审批备注'
|
||||
},
|
||||
finance_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '财务备注'
|
||||
}
|
||||
}, {
|
||||
tableName: 'settlements',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['settlement_no'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['order_id']
|
||||
},
|
||||
{
|
||||
fields: ['settlement_type']
|
||||
},
|
||||
{
|
||||
fields: ['status']
|
||||
},
|
||||
{
|
||||
fields: ['settlement_date']
|
||||
},
|
||||
{
|
||||
fields: ['payment_date']
|
||||
},
|
||||
{
|
||||
fields: ['approver_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = Settlement;
|
||||
150
backend/src/models/Supplier.js
Normal file
150
backend/src/models/Supplier.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
/**
|
||||
* 供应商模型
|
||||
* 管理活牛供应商的基本信息、资质证书和业务能力
|
||||
*/
|
||||
const Supplier = sequelize.define('Supplier', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '供应商ID'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '供应商名称'
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '供应商编码'
|
||||
},
|
||||
contact: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '联系人姓名'
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '联系电话'
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '邮箱地址'
|
||||
},
|
||||
address: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
comment: '详细地址'
|
||||
},
|
||||
region: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
comment: '所属区域'
|
||||
},
|
||||
business_license: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '营业执照文件路径'
|
||||
},
|
||||
animal_quarantine_certificate: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '动物防疫条件合格证文件路径'
|
||||
},
|
||||
qualification_level: {
|
||||
type: DataTypes.ENUM('A', 'B', 'C', 'D'),
|
||||
allowNull: false,
|
||||
defaultValue: 'C',
|
||||
comment: '资质等级: A(优秀), B(良好), C(合格), D(待改进)'
|
||||
},
|
||||
certifications: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
comment: '其他认证证书信息(JSON格式)'
|
||||
},
|
||||
cattle_types: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
comment: '可供应的牛只品种(JSON数组)'
|
||||
},
|
||||
capacity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '月供应能力(头数)'
|
||||
},
|
||||
rating: {
|
||||
type: DataTypes.DECIMAL(3, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0.00,
|
||||
comment: '综合评分(0-5分)'
|
||||
},
|
||||
cooperation_start_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '合作开始日期'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'blacklisted'),
|
||||
allowNull: false,
|
||||
defaultValue: 'active',
|
||||
comment: '供应商状态: active(活跃), inactive(停用), suspended(暂停), blacklisted(黑名单)'
|
||||
},
|
||||
bank_account: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '银行账号'
|
||||
},
|
||||
bank_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '开户银行'
|
||||
},
|
||||
tax_number: {
|
||||
type: DataTypes.STRING(30),
|
||||
allowNull: true,
|
||||
comment: '税务登记号'
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注信息'
|
||||
}
|
||||
}, {
|
||||
tableName: 'suppliers',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['code'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['phone'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['region']
|
||||
},
|
||||
{
|
||||
fields: ['qualification_level']
|
||||
},
|
||||
{
|
||||
fields: ['status']
|
||||
},
|
||||
{
|
||||
fields: ['rating']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = Supplier;
|
||||
@@ -1,50 +1,167 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
// 用户模型
|
||||
/**
|
||||
* 用户模型
|
||||
* 管理系统中所有用户的基本信息、认证信息和权限
|
||||
*/
|
||||
const User = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
openid: {
|
||||
type: DataTypes.STRING(64),
|
||||
allowNull: false
|
||||
},
|
||||
nickname: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
avatar: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true
|
||||
},
|
||||
gender: {
|
||||
type: DataTypes.ENUM('male', 'female', 'other'),
|
||||
allowNull: true
|
||||
},
|
||||
birthday: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true
|
||||
autoIncrement: true,
|
||||
comment: '用户ID'
|
||||
},
|
||||
uuid: {
|
||||
type: DataTypes.STRING(36),
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
comment: '用户唯一标识符'
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
comment: '用户名'
|
||||
},
|
||||
password_hash: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '密码哈希值'
|
||||
},
|
||||
openid: {
|
||||
type: DataTypes.STRING(64),
|
||||
allowNull: true,
|
||||
comment: '微信小程序OpenID'
|
||||
},
|
||||
unionid: {
|
||||
type: DataTypes.STRING(64),
|
||||
allowNull: true,
|
||||
comment: '微信UnionID'
|
||||
},
|
||||
nickname: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '用户昵称'
|
||||
},
|
||||
real_name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '真实姓名'
|
||||
},
|
||||
avatar: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '头像URL'
|
||||
},
|
||||
gender: {
|
||||
type: DataTypes.ENUM('male', 'female', 'other'),
|
||||
allowNull: true,
|
||||
comment: '性别'
|
||||
},
|
||||
birthday: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '生日'
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
comment: '手机号码'
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
comment: '邮箱地址'
|
||||
},
|
||||
user_type: {
|
||||
type: DataTypes.ENUM('buyer', 'trader', 'supplier', 'driver', 'staff', 'admin'),
|
||||
allowNull: false,
|
||||
defaultValue: 'buyer',
|
||||
comment: '用户类型: buyer(采购人), trader(贸易商), supplier(供应商), driver(司机), staff(员工), admin(管理员)'
|
||||
},
|
||||
company_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '公司名称'
|
||||
},
|
||||
company_address: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: true,
|
||||
comment: '公司地址'
|
||||
},
|
||||
business_license: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '营业执照文件路径'
|
||||
},
|
||||
id_card: {
|
||||
type: DataTypes.STRING(18),
|
||||
allowNull: true,
|
||||
comment: '身份证号'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'pending_approval'),
|
||||
allowNull: false,
|
||||
defaultValue: 'pending_approval',
|
||||
comment: '用户状态: active(活跃), inactive(停用), suspended(暂停), pending_approval(待审核)'
|
||||
},
|
||||
last_login_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '最后登录时间'
|
||||
},
|
||||
login_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '登录次数'
|
||||
},
|
||||
registration_source: {
|
||||
type: DataTypes.ENUM('miniprogram', 'web', 'admin_create'),
|
||||
allowNull: false,
|
||||
defaultValue: 'miniprogram',
|
||||
comment: '注册来源: miniprogram(小程序), web(网页), admin_create(管理员创建)'
|
||||
},
|
||||
approval_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '审核备注'
|
||||
}
|
||||
}, {
|
||||
tableName: 'users',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['uuid'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['username'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['phone'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['email'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['openid']
|
||||
},
|
||||
{
|
||||
fields: ['user_type']
|
||||
},
|
||||
{
|
||||
fields: ['status']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = User;
|
||||
@@ -4,6 +4,148 @@ const Payment = require('./Payment');
|
||||
const Transport = require('./Transport');
|
||||
const TransportTrack = require('./TransportTrack');
|
||||
const Vehicle = require('./Vehicle');
|
||||
const Driver = require('./Driver');
|
||||
const Supplier = require('./Supplier');
|
||||
const QualityRecord = require('./QualityRecord');
|
||||
const Settlement = require('./Settlement');
|
||||
|
||||
/**
|
||||
* 定义模型之间的关联关系
|
||||
*/
|
||||
|
||||
// 用户与订单的关联
|
||||
User.hasMany(Order, {
|
||||
foreignKey: 'buyerId',
|
||||
as: 'buyerOrders'
|
||||
});
|
||||
User.hasMany(Order, {
|
||||
foreignKey: 'traderId',
|
||||
as: 'traderOrders'
|
||||
});
|
||||
Order.belongsTo(User, {
|
||||
foreignKey: 'buyerId',
|
||||
as: 'buyer'
|
||||
});
|
||||
Order.belongsTo(User, {
|
||||
foreignKey: 'traderId',
|
||||
as: 'trader'
|
||||
});
|
||||
|
||||
// 供应商与订单的关联
|
||||
Supplier.hasMany(Order, {
|
||||
foreignKey: 'supplierId',
|
||||
as: 'orders'
|
||||
});
|
||||
Order.belongsTo(Supplier, {
|
||||
foreignKey: 'supplierId',
|
||||
as: 'supplier'
|
||||
});
|
||||
|
||||
// 订单与支付的关联
|
||||
Order.hasMany(Payment, {
|
||||
foreignKey: 'order_id',
|
||||
as: 'payments'
|
||||
});
|
||||
Payment.belongsTo(Order, {
|
||||
foreignKey: 'order_id',
|
||||
as: 'order'
|
||||
});
|
||||
|
||||
// 用户与支付的关联
|
||||
User.hasMany(Payment, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'payments'
|
||||
});
|
||||
Payment.belongsTo(User, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'user'
|
||||
});
|
||||
|
||||
// 订单与运输的关联
|
||||
Order.hasMany(Transport, {
|
||||
foreignKey: 'order_id',
|
||||
as: 'transports'
|
||||
});
|
||||
Transport.belongsTo(Order, {
|
||||
foreignKey: 'order_id',
|
||||
as: 'order'
|
||||
});
|
||||
|
||||
// 司机与运输的关联
|
||||
Driver.hasMany(Transport, {
|
||||
foreignKey: 'driver_id',
|
||||
as: 'transports'
|
||||
});
|
||||
Transport.belongsTo(Driver, {
|
||||
foreignKey: 'driver_id',
|
||||
as: 'driver'
|
||||
});
|
||||
|
||||
// 车辆与运输的关联
|
||||
Vehicle.hasMany(Transport, {
|
||||
foreignKey: 'vehicle_id',
|
||||
as: 'transports'
|
||||
});
|
||||
Transport.belongsTo(Vehicle, {
|
||||
foreignKey: 'vehicle_id',
|
||||
as: 'vehicle'
|
||||
});
|
||||
|
||||
// 司机与车辆的关联(当前使用车辆)
|
||||
Driver.belongsTo(Vehicle, {
|
||||
foreignKey: 'current_vehicle_id',
|
||||
as: 'currentVehicle'
|
||||
});
|
||||
|
||||
// 运输与运输跟踪的关联
|
||||
Transport.hasMany(TransportTrack, {
|
||||
foreignKey: 'transport_id',
|
||||
as: 'tracks'
|
||||
});
|
||||
TransportTrack.belongsTo(Transport, {
|
||||
foreignKey: 'transport_id',
|
||||
as: 'transport'
|
||||
});
|
||||
|
||||
// 订单与质检记录的关联
|
||||
Order.hasMany(QualityRecord, {
|
||||
foreignKey: 'order_id',
|
||||
as: 'qualityRecords'
|
||||
});
|
||||
QualityRecord.belongsTo(Order, {
|
||||
foreignKey: 'order_id',
|
||||
as: 'order'
|
||||
});
|
||||
|
||||
// 用户与质检记录的关联(检验员)
|
||||
User.hasMany(QualityRecord, {
|
||||
foreignKey: 'inspector_id',
|
||||
as: 'inspectionRecords'
|
||||
});
|
||||
QualityRecord.belongsTo(User, {
|
||||
foreignKey: 'inspector_id',
|
||||
as: 'inspector'
|
||||
});
|
||||
|
||||
// 订单与结算的关联
|
||||
Order.hasMany(Settlement, {
|
||||
foreignKey: 'order_id',
|
||||
as: 'settlements'
|
||||
});
|
||||
Settlement.belongsTo(Order, {
|
||||
foreignKey: 'order_id',
|
||||
as: 'order'
|
||||
});
|
||||
|
||||
// 用户与结算的关联(审批人)
|
||||
User.hasMany(Settlement, {
|
||||
foreignKey: 'approver_id',
|
||||
as: 'approvedSettlements'
|
||||
});
|
||||
Settlement.belongsTo(User, {
|
||||
foreignKey: 'approver_id',
|
||||
as: 'approver'
|
||||
});
|
||||
|
||||
// 为了兼容现有代码,将User模型也导出为Admin
|
||||
const Admin = User;
|
||||
@@ -15,5 +157,9 @@ module.exports = {
|
||||
Payment,
|
||||
Transport,
|
||||
TransportTrack,
|
||||
Vehicle
|
||||
Vehicle,
|
||||
Driver,
|
||||
Supplier,
|
||||
QualityRecord,
|
||||
Settlement
|
||||
};
|
||||
476
backend/src/routes/drivers.js
Normal file
476
backend/src/routes/drivers.js
Normal file
@@ -0,0 +1,476 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const DriverController = require('../controllers/DriverController');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const { validateDriver, validateId, validatePagination } = require('../middleware/validation');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Driver:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - phone
|
||||
* - license_number
|
||||
* - license_type
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 司机ID
|
||||
* driver_code:
|
||||
* type: string
|
||||
* description: 司机编号
|
||||
* name:
|
||||
* type: string
|
||||
* description: 司机姓名
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* id_card:
|
||||
* type: string
|
||||
* description: 身份证号
|
||||
* license_number:
|
||||
* type: string
|
||||
* description: 驾驶证号
|
||||
* license_type:
|
||||
* type: string
|
||||
* enum: [A1, A2, A3, B1, B2, C1, C2]
|
||||
* description: 驾驶证类型
|
||||
* license_expire_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 驾驶证到期日期
|
||||
* experience_years:
|
||||
* type: integer
|
||||
* description: 驾驶经验年数
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [available, busy, offline, suspended]
|
||||
* description: 司机状态
|
||||
* rating:
|
||||
* type: number
|
||||
* format: float
|
||||
* description: 评分
|
||||
* total_trips:
|
||||
* type: integer
|
||||
* description: 总运输次数
|
||||
* emergency_contact:
|
||||
* type: string
|
||||
* description: 紧急联系人
|
||||
* emergency_phone:
|
||||
* type: string
|
||||
* description: 紧急联系电话
|
||||
* address:
|
||||
* type: string
|
||||
* description: 地址
|
||||
* notes:
|
||||
* type: string
|
||||
* description: 备注
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/drivers:
|
||||
* post:
|
||||
* summary: 创建司机
|
||||
* tags: [Drivers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Driver'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 司机创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Driver'
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.post('/', authenticateToken, validateDriver, DriverController.createDriver);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/drivers:
|
||||
* get:
|
||||
* summary: 获取司机列表
|
||||
* tags: [Drivers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: pageSize
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 20
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [available, busy, offline, suspended]
|
||||
* description: 司机状态
|
||||
* - in: query
|
||||
* name: license_type
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [A1, A2, A3, B1, B2, C1, C2]
|
||||
* description: 驾驶证类型
|
||||
* - in: query
|
||||
* name: keyword
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词
|
||||
* - in: query
|
||||
* name: sort_by
|
||||
* schema:
|
||||
* type: string
|
||||
* default: created_at
|
||||
* description: 排序字段
|
||||
* - in: query
|
||||
* name: sort_order
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [ASC, DESC]
|
||||
* default: DESC
|
||||
* description: 排序方向
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取司机列表成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Driver'
|
||||
* pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* pageSize:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* totalPages:
|
||||
* type: integer
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/', authenticateToken, validatePagination, DriverController.getDriverList);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/drivers/available:
|
||||
* get:
|
||||
* summary: 获取可用司机列表
|
||||
* tags: [Drivers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: license_type
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [A1, A2, A3, B1, B2, C1, C2]
|
||||
* description: 驾驶证类型
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取可用司机列表成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Driver'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/available', authenticateToken, DriverController.getAvailableDrivers);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/drivers/stats:
|
||||
* get:
|
||||
* summary: 获取司机统计信息
|
||||
* tags: [Drivers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取司机统计信息成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* total:
|
||||
* type: integer
|
||||
* description: 总司机数
|
||||
* available:
|
||||
* type: integer
|
||||
* description: 可用司机数
|
||||
* busy:
|
||||
* type: integer
|
||||
* description: 忙碌司机数
|
||||
* offline:
|
||||
* type: integer
|
||||
* description: 离线司机数
|
||||
* suspended:
|
||||
* type: integer
|
||||
* description: 暂停司机数
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/stats', authenticateToken, DriverController.getDriverStats);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/drivers/{id}:
|
||||
* get:
|
||||
* summary: 获取司机详情
|
||||
* tags: [Drivers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 司机ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取司机详情成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Driver'
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 司机不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/:id', authenticateToken, validateId, DriverController.getDriverDetail);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/drivers/{id}:
|
||||
* put:
|
||||
* summary: 更新司机信息
|
||||
* tags: [Drivers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 司机ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Driver'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 司机信息更新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Driver'
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 司机不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.put('/:id', authenticateToken, validateId, DriverController.updateDriver);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/drivers/{id}/status:
|
||||
* patch:
|
||||
* summary: 更新司机状态
|
||||
* tags: [Drivers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 司机ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - status
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [available, busy, offline, suspended]
|
||||
* description: 司机状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 司机状态更新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Driver'
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 司机不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.patch('/:id/status', authenticateToken, validateId, DriverController.updateDriverStatus);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/drivers/{id}:
|
||||
* delete:
|
||||
* summary: 删除司机
|
||||
* tags: [Drivers]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 司机ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 司机删除成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 司机不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.delete('/:id', authenticateToken, validateId, DriverController.deleteDriver);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,406 +1,36 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
const { Supplier } = require('../../models');
|
||||
const { Sequelize } = require('sequelize');
|
||||
const SupplierController = require('../controllers/SupplierController');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const { validateSupplier } = require('../middleware/validation');
|
||||
|
||||
// 验证schemas
|
||||
const supplierCreateSchema = Joi.object({
|
||||
name: Joi.string().min(2).max(100).required(),
|
||||
code: Joi.string().min(3).max(20).required(),
|
||||
contact: Joi.string().min(2).max(50).required(),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required(),
|
||||
address: Joi.string().min(5).max(200).required(),
|
||||
qualificationLevel: Joi.string().valid('A+', 'A', 'B+', 'B', 'C').required(),
|
||||
cattleTypes: Joi.array().items(Joi.string()).min(1).required(),
|
||||
capacity: Joi.number().integer().min(1).required(),
|
||||
region: Joi.string().valid('north', 'south', 'east', 'west', 'northeast', 'northwest', 'southeast', 'southwest', 'central').required()
|
||||
});
|
||||
|
||||
const supplierUpdateSchema = Joi.object({
|
||||
name: Joi.string().min(2).max(100),
|
||||
contact: Joi.string().min(2).max(50),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/),
|
||||
address: Joi.string().min(5).max(200),
|
||||
qualificationLevel: Joi.string().valid('A+', 'A', 'B+', 'B', 'C'),
|
||||
cattleTypes: Joi.array().items(Joi.string()).min(1),
|
||||
capacity: Joi.number().integer().min(1),
|
||||
region: Joi.string().valid('north', 'south', 'east', 'west', 'northeast', 'northwest', 'southeast', 'southwest', 'central'),
|
||||
status: Joi.string().valid('active', 'inactive', 'suspended')
|
||||
});
|
||||
|
||||
// 获取供应商列表
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
keyword,
|
||||
region,
|
||||
qualificationLevel,
|
||||
status
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
const whereConditions = {};
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
whereConditions.status = status;
|
||||
}
|
||||
|
||||
// 区域筛选
|
||||
if (region) {
|
||||
whereConditions.region = region;
|
||||
}
|
||||
|
||||
// 资质等级筛选
|
||||
if (qualificationLevel) {
|
||||
whereConditions.qualificationLevel = qualificationLevel;
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
whereConditions[Sequelize.Op.or] = [
|
||||
{ name: { [Sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ code: { [Sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ contact: { [Sequelize.Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
// 分页参数
|
||||
const offset = (page - 1) * pageSize;
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
// 查询数据库
|
||||
const { rows, count } = await Supplier.findAndCountAll({
|
||||
where: whereConditions,
|
||||
offset,
|
||||
limit,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: limit,
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取供应商列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取供应商详情
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 查询数据库
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: supplier
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取供应商详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 供应商路由
|
||||
* 定义供应商相关的API端点
|
||||
*/
|
||||
|
||||
// 创建供应商
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { error, value } = supplierCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
router.post('/', authenticateToken, validateSupplier, SupplierController.createSupplier);
|
||||
|
||||
// 检查编码是否重复
|
||||
const existingSupplier = await Supplier.findOne({ where: { code: value.code } });
|
||||
if (existingSupplier) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '供应商编码已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查电话是否重复
|
||||
const existingPhone = await Supplier.findOne({ where: { phone: value.phone } });
|
||||
if (existingPhone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '供应商电话已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建新供应商
|
||||
const newSupplier = await Supplier.create({
|
||||
...value,
|
||||
businessLicense: '',
|
||||
certifications: JSON.stringify([]),
|
||||
cattleTypes: JSON.stringify(value.cattleTypes),
|
||||
rating: 0,
|
||||
cooperationStartDate: new Date(),
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '供应商创建成功',
|
||||
data: newSupplier
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建供应商失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新供应商
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { error, value } = supplierUpdateSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
// 查找供应商
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 如果更新了电话号码,检查是否重复
|
||||
if (value.phone && value.phone !== supplier.phone) {
|
||||
const existingPhone = await Supplier.findOne({ where: { phone: value.phone } });
|
||||
if (existingPhone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '供应商电话已存在'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新供应商信息
|
||||
await supplier.update({
|
||||
...value,
|
||||
cattleTypes: value.cattleTypes ? JSON.stringify(value.cattleTypes) : undefined
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商更新成功',
|
||||
data: supplier
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新供应商失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 删除供应商
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 查找供应商
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 删除供应商
|
||||
await supplier.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除供应商失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
// 获取供应商列表
|
||||
router.get('/', authenticateToken, SupplierController.getSupplierList);
|
||||
|
||||
// 获取供应商统计信息
|
||||
router.get('/stats/overview', async (req, res) => {
|
||||
try {
|
||||
// 获取总数和活跃数
|
||||
const totalSuppliers = await Supplier.count();
|
||||
const activeSuppliers = await Supplier.count({ where: { status: 'active' } });
|
||||
|
||||
// 获取平均评分(排除评分为0的供应商)
|
||||
const ratingResult = await Supplier.findOne({
|
||||
attributes: [
|
||||
[Sequelize.fn('AVG', Sequelize.col('rating')), 'averageRating']
|
||||
],
|
||||
where: {
|
||||
rating: {
|
||||
[Sequelize.Op.gt]: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
const averageRating = ratingResult ? parseFloat(ratingResult.getDataValue('averageRating')).toFixed(2) : 0;
|
||||
|
||||
// 获取总产能
|
||||
const capacityResult = await Supplier.findOne({
|
||||
attributes: [
|
||||
[Sequelize.fn('SUM', Sequelize.col('capacity')), 'totalCapacity']
|
||||
]
|
||||
});
|
||||
const totalCapacity = capacityResult ? capacityResult.getDataValue('totalCapacity') : 0;
|
||||
router.get('/stats', authenticateToken, SupplierController.getSupplierStats);
|
||||
|
||||
// 按等级统计
|
||||
const levelStatsResult = await Supplier.findAll({
|
||||
attributes: [
|
||||
'qualificationLevel',
|
||||
[Sequelize.fn('COUNT', Sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['qualificationLevel']
|
||||
});
|
||||
const levelStats = levelStatsResult.reduce((stats, item) => {
|
||||
stats[item.qualificationLevel] = item.getDataValue('count');
|
||||
return stats;
|
||||
}, {});
|
||||
// 获取供应商详情
|
||||
router.get('/:id', authenticateToken, SupplierController.getSupplierDetail);
|
||||
|
||||
// 按区域统计
|
||||
const regionStatsResult = await Supplier.findAll({
|
||||
attributes: [
|
||||
'region',
|
||||
[Sequelize.fn('COUNT', Sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['region']
|
||||
});
|
||||
const regionStats = regionStatsResult.reduce((stats, item) => {
|
||||
stats[item.region] = item.getDataValue('count');
|
||||
return stats;
|
||||
}, {});
|
||||
// 更新供应商信息
|
||||
router.put('/:id', authenticateToken, SupplierController.updateSupplier);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalSuppliers,
|
||||
activeSuppliers,
|
||||
averageRating: parseFloat(averageRating),
|
||||
totalCapacity,
|
||||
levelStats,
|
||||
regionStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取供应商统计信息失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商统计信息失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
// 更新供应商状态
|
||||
router.patch('/:id/status', authenticateToken, SupplierController.updateSupplierStatus);
|
||||
|
||||
// 批量操作供应商
|
||||
router.post('/batch', async (req, res) => {
|
||||
try {
|
||||
const { ids, action } = req.body;
|
||||
// 更新供应商评分
|
||||
router.patch('/:id/rating', authenticateToken, SupplierController.updateSupplierRating);
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要操作的供应商'
|
||||
});
|
||||
}
|
||||
|
||||
if (!['activate', 'deactivate', 'delete'].includes(action)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '无效的操作类型'
|
||||
});
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'activate':
|
||||
await Supplier.update(
|
||||
{ status: 'active' },
|
||||
{ where: { id: ids } }
|
||||
);
|
||||
break;
|
||||
case 'deactivate':
|
||||
await Supplier.update(
|
||||
{ status: 'inactive' },
|
||||
{ where: { id: ids } }
|
||||
);
|
||||
break;
|
||||
case 'delete':
|
||||
await Supplier.destroy({
|
||||
where: {
|
||||
id: ids
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '批量操作成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量操作失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量操作失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
// 删除供应商
|
||||
router.delete('/:id', authenticateToken, SupplierController.deleteSupplier);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,78 +1,189 @@
|
||||
const express = require('express');
|
||||
const TransportController = require('../controllers/TransportController');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const { validateTransport, validateId, validatePagination } = require('../middleware/validation');
|
||||
|
||||
const router = express.Router();
|
||||
const transportController = require('../controllers/TransportController');
|
||||
const { authenticate, checkRole } = require('../middleware/auth');
|
||||
|
||||
// 运输管理路由
|
||||
// 获取运输列表
|
||||
router.get('/',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.getTransportList
|
||||
);
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Transport:
|
||||
* type: object
|
||||
* required:
|
||||
* - order_id
|
||||
* - pickup_address
|
||||
* - delivery_address
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 运输任务ID
|
||||
* order_id:
|
||||
* type: integer
|
||||
* description: 订单ID
|
||||
* transport_number:
|
||||
* type: string
|
||||
* description: 运输单号
|
||||
* driver_id:
|
||||
* type: integer
|
||||
* description: 司机ID
|
||||
* vehicle_id:
|
||||
* type: integer
|
||||
* description: 车辆ID
|
||||
* pickup_address:
|
||||
* type: string
|
||||
* description: 取货地址
|
||||
* delivery_address:
|
||||
* type: string
|
||||
* description: 送货地址
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, assigned, in_transit, delivered, completed, cancelled, exception]
|
||||
* description: 运输状态
|
||||
*/
|
||||
|
||||
// 获取运输详情
|
||||
router.get('/:id',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.getTransportDetail
|
||||
);
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transports:
|
||||
* post:
|
||||
* summary: 创建运输任务
|
||||
* tags: [运输管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Transport'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 运输任务创建成功
|
||||
*/
|
||||
router.post('/', authenticateToken, validateTransport, TransportController.createTransport);
|
||||
|
||||
// 创建运输记录
|
||||
router.post('/',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.createTransport
|
||||
);
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transports:
|
||||
* get:
|
||||
* summary: 获取运输任务列表
|
||||
* tags: [运输管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: pageSize
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 每页数量
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
*/
|
||||
router.get('/', authenticateToken, validatePagination, TransportController.getTransportList);
|
||||
|
||||
// 更新运输记录
|
||||
router.put('/:id',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.updateTransport
|
||||
);
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transports/{id}:
|
||||
* get:
|
||||
* summary: 获取运输任务详情
|
||||
* tags: [运输管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 运输任务ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
*/
|
||||
router.get('/:id', authenticateToken, validateId, TransportController.getTransportDetail);
|
||||
|
||||
// 删除运输记录
|
||||
router.delete('/:id',
|
||||
authenticate,
|
||||
checkRole(['admin']),
|
||||
transportController.deleteTransport
|
||||
);
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transports/{id}:
|
||||
* put:
|
||||
* summary: 更新运输任务信息
|
||||
* tags: [运输管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 运输任务ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
*/
|
||||
router.put('/:id', authenticateToken, validateId, TransportController.updateTransportStatus);
|
||||
|
||||
// 车辆管理路由
|
||||
// 获取车辆列表
|
||||
router.get('/vehicles',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.getVehicleList
|
||||
);
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transports/{id}/status:
|
||||
* patch:
|
||||
* summary: 更新运输状态
|
||||
* tags: [运输管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 运输任务ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 状态更新成功
|
||||
*/
|
||||
router.patch('/:id/status', authenticateToken, validateId, TransportController.updateTransportStatus);
|
||||
|
||||
// 获取车辆详情
|
||||
router.get('/vehicles/:id',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.getVehicleDetail
|
||||
);
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transports/{id}/assign:
|
||||
* patch:
|
||||
* summary: 分配司机和车辆
|
||||
* tags: [运输管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 运输任务ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 分配成功
|
||||
*/
|
||||
router.patch('/:id/assign', authenticateToken, validateId, TransportController.assignDriverAndVehicle);
|
||||
|
||||
// 创建车辆记录
|
||||
router.post('/vehicles',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.createVehicle
|
||||
);
|
||||
|
||||
// 更新车辆记录
|
||||
router.put('/vehicles/:id',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.updateVehicle
|
||||
);
|
||||
|
||||
// 删除车辆记录
|
||||
router.delete('/vehicles/:id',
|
||||
authenticate,
|
||||
checkRole(['admin']),
|
||||
transportController.deleteVehicle
|
||||
);
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transports/statistics:
|
||||
* get:
|
||||
* summary: 获取运输统计信息
|
||||
* tags: [运输管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
*/
|
||||
router.get('/statistics', authenticateToken, TransportController.getTransportStats);
|
||||
|
||||
module.exports = router;
|
||||
369
backend/src/routes/vehicles.js
Normal file
369
backend/src/routes/vehicles.js
Normal file
@@ -0,0 +1,369 @@
|
||||
const express = require('express');
|
||||
const VehicleController = require('../controllers/VehicleController');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const { validateId, validatePagination } = require('../middleware/validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Vehicle:
|
||||
* type: object
|
||||
* required:
|
||||
* - license_plate
|
||||
* - vehicle_type
|
||||
* - brand
|
||||
* - model
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 车辆ID
|
||||
* vehicle_number:
|
||||
* type: string
|
||||
* description: 车辆编号
|
||||
* license_plate:
|
||||
* type: string
|
||||
* description: 车牌号
|
||||
* vehicle_type:
|
||||
* type: string
|
||||
* enum: [truck, van, trailer]
|
||||
* description: 车辆类型
|
||||
* brand:
|
||||
* type: string
|
||||
* description: 品牌
|
||||
* model:
|
||||
* type: string
|
||||
* description: 型号
|
||||
* year:
|
||||
* type: integer
|
||||
* description: 年份
|
||||
* color:
|
||||
* type: string
|
||||
* description: 颜色
|
||||
* engine_number:
|
||||
* type: string
|
||||
* description: 发动机号
|
||||
* chassis_number:
|
||||
* type: string
|
||||
* description: 车架号
|
||||
* load_capacity:
|
||||
* type: number
|
||||
* description: 载重量(吨)
|
||||
* fuel_type:
|
||||
* type: string
|
||||
* enum: [gasoline, diesel, electric, hybrid]
|
||||
* description: 燃料类型
|
||||
* driver_id:
|
||||
* type: integer
|
||||
* description: 司机ID
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [available, busy, maintenance, offline]
|
||||
* description: 车辆状态
|
||||
* insurance_expire_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 保险到期日期
|
||||
* inspection_expire_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 年检到期日期
|
||||
* purchase_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 购买日期
|
||||
* purchase_price:
|
||||
* type: number
|
||||
* description: 购买价格
|
||||
* notes:
|
||||
* type: string
|
||||
* description: 备注
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/vehicles:
|
||||
* post:
|
||||
* summary: 创建车辆
|
||||
* tags: [车辆管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Vehicle'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 车辆创建成功
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.post('/', authenticateToken, VehicleController.createVehicle);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/vehicles:
|
||||
* get:
|
||||
* summary: 获取车辆列表
|
||||
* tags: [车辆管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: pageSize
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [available, busy, maintenance, offline]
|
||||
* description: 车辆状态
|
||||
* - in: query
|
||||
* name: vehicle_type
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [truck, van, trailer]
|
||||
* description: 车辆类型
|
||||
* - in: query
|
||||
* name: brand
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 品牌
|
||||
* - in: query
|
||||
* name: search
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.get('/', authenticateToken, validatePagination, VehicleController.getVehicleList);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/vehicles/available:
|
||||
* get:
|
||||
* summary: 获取可用车辆列表
|
||||
* tags: [车辆管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: vehicle_type
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [truck, van, trailer]
|
||||
* description: 车辆类型
|
||||
* - in: query
|
||||
* name: load_capacity_min
|
||||
* schema:
|
||||
* type: number
|
||||
* description: 最小载重量
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.get('/available', authenticateToken, VehicleController.getAvailableVehicles);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/vehicles/statistics:
|
||||
* get:
|
||||
* summary: 获取车辆统计信息
|
||||
* tags: [车辆管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.get('/statistics', authenticateToken, VehicleController.getVehicleStatistics);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/vehicles/{id}:
|
||||
* get:
|
||||
* summary: 获取车辆详情
|
||||
* tags: [车辆管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 车辆ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 404:
|
||||
* description: 车辆不存在
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.get('/:id', authenticateToken, validateId, VehicleController.getVehicleDetail);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/vehicles/{id}:
|
||||
* put:
|
||||
* summary: 更新车辆信息
|
||||
* tags: [车辆管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 车辆ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Vehicle'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
* 404:
|
||||
* description: 车辆不存在
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.put('/:id', authenticateToken, validateId, VehicleController.updateVehicle);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/vehicles/{id}/status:
|
||||
* patch:
|
||||
* summary: 更新车辆状态
|
||||
* tags: [车辆管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 车辆ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - status
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [available, busy, maintenance, offline]
|
||||
* description: 新状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 状态更新成功
|
||||
* 404:
|
||||
* description: 车辆不存在
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.patch('/:id/status', authenticateToken, validateId, VehicleController.updateVehicleStatus);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/vehicles/{id}/assign-driver:
|
||||
* patch:
|
||||
* summary: 分配司机
|
||||
* tags: [车辆管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 车辆ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - driver_id
|
||||
* properties:
|
||||
* driver_id:
|
||||
* type: integer
|
||||
* description: 司机ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 司机分配成功
|
||||
* 404:
|
||||
* description: 车辆不存在
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.patch('/:id/assign-driver', authenticateToken, validateId, VehicleController.assignDriver);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/vehicles/{id}:
|
||||
* delete:
|
||||
* summary: 删除车辆
|
||||
* tags: [车辆管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 车辆ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 删除成功
|
||||
* 404:
|
||||
* description: 车辆不存在
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.delete('/:id', authenticateToken, validateId, VehicleController.deleteVehicle);
|
||||
|
||||
module.exports = router;
|
||||
408
backend/src/services/DriverService.js
Normal file
408
backend/src/services/DriverService.js
Normal file
@@ -0,0 +1,408 @@
|
||||
const { Driver, Vehicle, Transport } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 司机服务层
|
||||
* 处理司机相关的业务逻辑
|
||||
*/
|
||||
class DriverService {
|
||||
|
||||
/**
|
||||
* 创建司机
|
||||
* @param {Object} driverData - 司机数据
|
||||
* @returns {Promise<Object>} 创建的司机信息
|
||||
*/
|
||||
static async createDriver(driverData) {
|
||||
try {
|
||||
// 检查手机号是否已存在
|
||||
if (driverData.phone) {
|
||||
const existingDriver = await Driver.findOne({
|
||||
where: { phone: driverData.phone }
|
||||
});
|
||||
if (existingDriver) {
|
||||
throw new Error('手机号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查驾驶证号是否已存在
|
||||
if (driverData.license_number) {
|
||||
const existingLicense = await Driver.findOne({
|
||||
where: { license_number: driverData.license_number }
|
||||
});
|
||||
if (existingLicense) {
|
||||
throw new Error('驾驶证号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 生成司机编号
|
||||
const driver_code = await this.generateDriverCode();
|
||||
|
||||
const driver = await Driver.create({
|
||||
...driverData,
|
||||
driver_code,
|
||||
status: driverData.status || 'available'
|
||||
});
|
||||
|
||||
return driver;
|
||||
} catch (error) {
|
||||
throw new Error(`创建司机失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取司机列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise<Object>} 司机列表和分页信息
|
||||
*/
|
||||
static async getDriverList(params = {}) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
status,
|
||||
license_type,
|
||||
keyword,
|
||||
sort_by = 'created_at',
|
||||
sort_order = 'DESC'
|
||||
} = params;
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
if (license_type) {
|
||||
where.license_type = license_type;
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ name: { [Op.like]: `%${keyword}%` } },
|
||||
{ phone: { [Op.like]: `%${keyword}%` } },
|
||||
{ driver_code: { [Op.like]: `%${keyword}%` } },
|
||||
{ license_number: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
const { count, rows } = await Driver.findAndCountAll({
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
order: [[sort_by, sort_order.toUpperCase()]],
|
||||
include: [
|
||||
{
|
||||
model: Vehicle,
|
||||
as: 'vehicles',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
drivers: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(count / parseInt(pageSize))
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`获取司机列表失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取司机详情
|
||||
* @param {number} id - 司机ID
|
||||
* @returns {Promise<Object>} 司机详情
|
||||
*/
|
||||
static async getDriverDetail(id) {
|
||||
try {
|
||||
const driver = await Driver.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: Vehicle,
|
||||
as: 'vehicles',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: Transport,
|
||||
as: 'transports',
|
||||
required: false,
|
||||
limit: 10,
|
||||
order: [['created_at', 'DESC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!driver) {
|
||||
throw new Error('司机不存在');
|
||||
}
|
||||
|
||||
return driver;
|
||||
} catch (error) {
|
||||
throw new Error(`获取司机详情失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新司机信息
|
||||
* @param {number} id - 司机ID
|
||||
* @param {Object} updateData - 更新数据
|
||||
* @returns {Promise<Object>} 更新后的司机信息
|
||||
*/
|
||||
static async updateDriver(id, updateData) {
|
||||
try {
|
||||
const driver = await Driver.findByPk(id);
|
||||
if (!driver) {
|
||||
throw new Error('司机不存在');
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在(排除当前司机)
|
||||
if (updateData.phone && updateData.phone !== driver.phone) {
|
||||
const existingDriver = await Driver.findOne({
|
||||
where: {
|
||||
phone: updateData.phone,
|
||||
id: { [Op.ne]: id }
|
||||
}
|
||||
});
|
||||
if (existingDriver) {
|
||||
throw new Error('手机号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查驾驶证号是否已存在(排除当前司机)
|
||||
if (updateData.license_number && updateData.license_number !== driver.license_number) {
|
||||
const existingLicense = await Driver.findOne({
|
||||
where: {
|
||||
license_number: updateData.license_number,
|
||||
id: { [Op.ne]: id }
|
||||
}
|
||||
});
|
||||
if (existingLicense) {
|
||||
throw new Error('驾驶证号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
await driver.update(updateData);
|
||||
return driver;
|
||||
} catch (error) {
|
||||
throw new Error(`更新司机信息失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新司机状态
|
||||
* @param {number} id - 司机ID
|
||||
* @param {string} status - 新状态
|
||||
* @returns {Promise<Object>} 更新后的司机信息
|
||||
*/
|
||||
static async updateDriverStatus(id, status) {
|
||||
try {
|
||||
const driver = await Driver.findByPk(id);
|
||||
if (!driver) {
|
||||
throw new Error('司机不存在');
|
||||
}
|
||||
|
||||
const validStatuses = ['available', 'busy', 'offline', 'suspended'];
|
||||
if (!validStatuses.includes(status)) {
|
||||
throw new Error('无效的状态值');
|
||||
}
|
||||
|
||||
// 如果司机正在执行运输任务,不能设置为离线或暂停
|
||||
if (['offline', 'suspended'].includes(status)) {
|
||||
const activeTransport = await Transport.findOne({
|
||||
where: {
|
||||
driver_id: id,
|
||||
status: { [Op.in]: ['assigned', 'in_transit'] }
|
||||
}
|
||||
});
|
||||
|
||||
if (activeTransport) {
|
||||
throw new Error('司机正在执行运输任务,无法设置为离线或暂停状态');
|
||||
}
|
||||
}
|
||||
|
||||
await driver.update({ status });
|
||||
return driver;
|
||||
} catch (error) {
|
||||
throw new Error(`更新司机状态失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除司机
|
||||
* @param {number} id - 司机ID
|
||||
* @returns {Promise<boolean>} 删除结果
|
||||
*/
|
||||
static async deleteDriver(id) {
|
||||
try {
|
||||
const driver = await Driver.findByPk(id);
|
||||
if (!driver) {
|
||||
throw new Error('司机不存在');
|
||||
}
|
||||
|
||||
// 检查是否有关联的运输任务
|
||||
const transportCount = await Transport.count({
|
||||
where: { driver_id: id }
|
||||
});
|
||||
|
||||
if (transportCount > 0) {
|
||||
throw new Error('司机有关联的运输任务,无法删除');
|
||||
}
|
||||
|
||||
// 检查是否有关联的车辆
|
||||
const vehicleCount = await Vehicle.count({
|
||||
where: { driver_id: id }
|
||||
});
|
||||
|
||||
if (vehicleCount > 0) {
|
||||
throw new Error('司机有关联的车辆,无法删除');
|
||||
}
|
||||
|
||||
await driver.destroy();
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`删除司机失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用司机列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise<Array>} 可用司机列表
|
||||
*/
|
||||
static async getAvailableDrivers(params = {}) {
|
||||
try {
|
||||
const { license_type } = params;
|
||||
|
||||
const where = {
|
||||
status: 'available'
|
||||
};
|
||||
|
||||
if (license_type) {
|
||||
where.license_type = license_type;
|
||||
}
|
||||
|
||||
const drivers = await Driver.findAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Vehicle,
|
||||
as: 'vehicles',
|
||||
required: false,
|
||||
where: { status: 'available' }
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
return drivers;
|
||||
} catch (error) {
|
||||
throw new Error(`获取可用司机列表失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取司机统计信息
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise<Object>} 统计信息
|
||||
*/
|
||||
static async getDriverStats(params = {}) {
|
||||
try {
|
||||
const { start_date, end_date } = params;
|
||||
|
||||
// 基础统计
|
||||
const totalDrivers = await Driver.count();
|
||||
const availableDrivers = await Driver.count({ where: { status: 'available' } });
|
||||
const busyDrivers = await Driver.count({ where: { status: 'busy' } });
|
||||
const offlineDrivers = await Driver.count({ where: { status: 'offline' } });
|
||||
const suspendedDrivers = await Driver.count({ where: { status: 'suspended' } });
|
||||
|
||||
// 按驾驶证类型统计
|
||||
const licenseTypeStats = await Driver.findAll({
|
||||
attributes: [
|
||||
'license_type',
|
||||
[Driver.sequelize.fn('COUNT', Driver.sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['license_type']
|
||||
});
|
||||
|
||||
// 运输任务统计(如果提供了日期范围)
|
||||
let transportStats = null;
|
||||
if (start_date && end_date) {
|
||||
const whereDate = {
|
||||
created_at: {
|
||||
[Op.between]: [new Date(start_date), new Date(end_date)]
|
||||
}
|
||||
};
|
||||
|
||||
transportStats = await Transport.findAll({
|
||||
attributes: [
|
||||
'driver_id',
|
||||
[Transport.sequelize.fn('COUNT', Transport.sequelize.col('id')), 'transport_count']
|
||||
],
|
||||
where: whereDate,
|
||||
group: ['driver_id'],
|
||||
include: [
|
||||
{
|
||||
model: Driver,
|
||||
as: 'driver',
|
||||
attributes: ['name', 'driver_code']
|
||||
}
|
||||
],
|
||||
order: [[Transport.sequelize.fn('COUNT', Transport.sequelize.col('id')), 'DESC']],
|
||||
limit: 10
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
total: totalDrivers,
|
||||
available: availableDrivers,
|
||||
busy: busyDrivers,
|
||||
offline: offlineDrivers,
|
||||
suspended: suspendedDrivers,
|
||||
license_type_stats: licenseTypeStats,
|
||||
transport_stats: transportStats
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`获取司机统计信息失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成司机编号
|
||||
* @returns {Promise<string>} 司机编号
|
||||
*/
|
||||
static async generateDriverCode() {
|
||||
const prefix = 'DR';
|
||||
const date = new Date();
|
||||
const dateStr = date.getFullYear().toString() +
|
||||
(date.getMonth() + 1).toString().padStart(2, '0') +
|
||||
date.getDate().toString().padStart(2, '0');
|
||||
|
||||
// 查找当天最大的序号
|
||||
const pattern = `${prefix}${dateStr}%`;
|
||||
const lastDriver = await Driver.findOne({
|
||||
where: {
|
||||
driver_code: { [Op.like]: pattern }
|
||||
},
|
||||
order: [['driver_code', 'DESC']]
|
||||
});
|
||||
|
||||
let sequence = 1;
|
||||
if (lastDriver) {
|
||||
const lastSequence = parseInt(lastDriver.driver_code.slice(-4));
|
||||
sequence = lastSequence + 1;
|
||||
}
|
||||
|
||||
return `${prefix}${dateStr}${sequence.toString().padStart(4, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DriverService;
|
||||
270
backend/src/services/SupplierService.js
Normal file
270
backend/src/services/SupplierService.js
Normal file
@@ -0,0 +1,270 @@
|
||||
const { Supplier } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 供应商服务层
|
||||
* 处理供应商相关的业务逻辑
|
||||
*/
|
||||
class SupplierService {
|
||||
|
||||
/**
|
||||
* 创建供应商
|
||||
* @param {Object} supplierData - 供应商数据
|
||||
* @returns {Object} 创建的供应商信息
|
||||
*/
|
||||
static async createSupplier(supplierData) {
|
||||
try {
|
||||
// 生成供应商编码
|
||||
const code = await this.generateSupplierCode(supplierData.region);
|
||||
supplierData.code = code;
|
||||
|
||||
// 创建供应商
|
||||
const supplier = await Supplier.create(supplierData);
|
||||
return supplier;
|
||||
} catch (error) {
|
||||
throw new Error(`创建供应商失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取供应商列表
|
||||
* @param {Object} query - 查询参数
|
||||
* @returns {Object} 供应商列表和分页信息
|
||||
*/
|
||||
static async getSupplierList(query) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
keyword,
|
||||
region,
|
||||
qualificationLevel,
|
||||
status = 'active'
|
||||
} = query;
|
||||
|
||||
// 构建查询条件
|
||||
const whereConditions = {};
|
||||
|
||||
if (status) {
|
||||
whereConditions.status = status;
|
||||
}
|
||||
|
||||
if (region) {
|
||||
whereConditions.region = region;
|
||||
}
|
||||
|
||||
if (qualificationLevel) {
|
||||
whereConditions.qualification_level = qualificationLevel;
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
whereConditions[Op.or] = [
|
||||
{ name: { [Op.like]: `%${keyword}%` } },
|
||||
{ code: { [Op.like]: `%${keyword}%` } },
|
||||
{ contact: { [Op.like]: `%${keyword}%` } },
|
||||
{ phone: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
// 查询供应商列表
|
||||
const { count, rows } = await Supplier.findAndCountAll({
|
||||
where: whereConditions,
|
||||
limit: parseInt(pageSize),
|
||||
offset: (parseInt(page) - 1) * parseInt(pageSize),
|
||||
order: [['rating', 'DESC'], ['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
return {
|
||||
suppliers: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(count / parseInt(pageSize))
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`获取供应商列表失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取供应商详情
|
||||
* @param {number} id - 供应商ID
|
||||
* @returns {Object} 供应商详情
|
||||
*/
|
||||
static async getSupplierDetail(id) {
|
||||
try {
|
||||
const supplier = await Supplier.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
association: 'orders',
|
||||
limit: 10,
|
||||
order: [['created_at', 'DESC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!supplier) {
|
||||
throw new Error('供应商不存在');
|
||||
}
|
||||
|
||||
return supplier;
|
||||
} catch (error) {
|
||||
throw new Error(`获取供应商详情失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新供应商信息
|
||||
* @param {number} id - 供应商ID
|
||||
* @param {Object} updateData - 更新数据
|
||||
* @returns {Object} 更新后的供应商信息
|
||||
*/
|
||||
static async updateSupplier(id, updateData) {
|
||||
try {
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
if (!supplier) {
|
||||
throw new Error('供应商不存在');
|
||||
}
|
||||
|
||||
// 更新供应商信息
|
||||
await supplier.update(updateData);
|
||||
return supplier;
|
||||
} catch (error) {
|
||||
throw new Error(`更新供应商失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新供应商状态
|
||||
* @param {number} id - 供应商ID
|
||||
* @param {string} status - 新状态
|
||||
* @returns {Object} 更新后的供应商信息
|
||||
*/
|
||||
static async updateSupplierStatus(id, status) {
|
||||
try {
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
if (!supplier) {
|
||||
throw new Error('供应商不存在');
|
||||
}
|
||||
|
||||
await supplier.update({ status });
|
||||
return supplier;
|
||||
} catch (error) {
|
||||
throw new Error(`更新供应商状态失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新供应商评分
|
||||
* @param {number} id - 供应商ID
|
||||
* @param {number} rating - 评分
|
||||
* @returns {Object} 更新后的供应商信息
|
||||
*/
|
||||
static async updateSupplierRating(id, rating) {
|
||||
try {
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
if (!supplier) {
|
||||
throw new Error('供应商不存在');
|
||||
}
|
||||
|
||||
// 计算新的平均评分(这里简化处理,实际应该基于历史评分计算)
|
||||
const newRating = Math.min(5.0, Math.max(0.0, parseFloat(rating)));
|
||||
|
||||
await supplier.update({ rating: newRating });
|
||||
return supplier;
|
||||
} catch (error) {
|
||||
throw new Error(`更新供应商评分失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除供应商(软删除,更改状态为inactive)
|
||||
* @param {number} id - 供应商ID
|
||||
* @returns {boolean} 删除结果
|
||||
*/
|
||||
static async deleteSupplier(id) {
|
||||
try {
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
if (!supplier) {
|
||||
throw new Error('供应商不存在');
|
||||
}
|
||||
|
||||
await supplier.update({ status: 'inactive' });
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`删除供应商失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成供应商编码
|
||||
* @param {string} region - 区域
|
||||
* @returns {string} 供应商编码
|
||||
*/
|
||||
static async generateSupplierCode(region = 'DEFAULT') {
|
||||
try {
|
||||
// 区域前缀映射
|
||||
const regionPrefixes = {
|
||||
'north': 'N',
|
||||
'south': 'S',
|
||||
'east': 'E',
|
||||
'west': 'W',
|
||||
'central': 'C'
|
||||
};
|
||||
|
||||
const prefix = regionPrefixes[region] || 'D';
|
||||
const timestamp = Date.now().toString().slice(-6);
|
||||
const random = Math.floor(Math.random() * 100).toString().padStart(2, '0');
|
||||
|
||||
return `SUP${prefix}${timestamp}${random}`;
|
||||
} catch (error) {
|
||||
throw new Error(`生成供应商编码失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取供应商统计信息
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
static async getSupplierStats() {
|
||||
try {
|
||||
const totalCount = await Supplier.count();
|
||||
const activeCount = await Supplier.count({ where: { status: 'active' } });
|
||||
const inactiveCount = await Supplier.count({ where: { status: 'inactive' } });
|
||||
const suspendedCount = await Supplier.count({ where: { status: 'suspended' } });
|
||||
|
||||
// 按资质等级统计
|
||||
const qualificationStats = await Supplier.findAll({
|
||||
attributes: [
|
||||
'qualification_level',
|
||||
[Supplier.sequelize.fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['qualification_level'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 按区域统计
|
||||
const regionStats = await Supplier.findAll({
|
||||
attributes: [
|
||||
'region',
|
||||
[Supplier.sequelize.fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['region'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
return {
|
||||
total: totalCount,
|
||||
active: activeCount,
|
||||
inactive: inactiveCount,
|
||||
suspended: suspendedCount,
|
||||
qualificationStats,
|
||||
regionStats
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`获取供应商统计信息失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SupplierService;
|
||||
520
backend/src/services/TransportService.js
Normal file
520
backend/src/services/TransportService.js
Normal file
@@ -0,0 +1,520 @@
|
||||
const { Transport, TransportTrack, Driver, Vehicle, Order } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 运输服务层
|
||||
* 处理运输相关的业务逻辑
|
||||
*/
|
||||
class TransportService {
|
||||
|
||||
/**
|
||||
* 创建运输任务
|
||||
* @param {Object} transportData - 运输数据
|
||||
* @returns {Object} 创建的运输任务信息
|
||||
*/
|
||||
static async createTransport(transportData) {
|
||||
try {
|
||||
// 生成运输单号
|
||||
const transportNo = await this.generateTransportNo();
|
||||
transportData.transport_no = transportNo;
|
||||
|
||||
// 设置初始状态
|
||||
transportData.status = 'pending';
|
||||
transportData.start_time = null;
|
||||
transportData.end_time = null;
|
||||
|
||||
// 创建运输任务
|
||||
const transport = await Transport.create(transportData);
|
||||
|
||||
// 创建初始跟踪记录
|
||||
await TransportTrack.create({
|
||||
transport_id: transport.id,
|
||||
status: 'pending',
|
||||
location: '待发车',
|
||||
description: '运输任务已创建,等待司机接单',
|
||||
recorded_at: new Date()
|
||||
});
|
||||
|
||||
return transport;
|
||||
} catch (error) {
|
||||
throw new Error(`创建运输任务失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运输任务列表
|
||||
* @param {Object} query - 查询参数
|
||||
* @returns {Object} 运输任务列表和分页信息
|
||||
*/
|
||||
static async getTransportList(query) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
status,
|
||||
driverId,
|
||||
vehicleId,
|
||||
orderId,
|
||||
keyword,
|
||||
startDate,
|
||||
endDate
|
||||
} = query;
|
||||
|
||||
// 构建查询条件
|
||||
const whereConditions = {};
|
||||
|
||||
if (status) {
|
||||
whereConditions.status = status;
|
||||
}
|
||||
|
||||
if (driverId) {
|
||||
whereConditions.driver_id = driverId;
|
||||
}
|
||||
|
||||
if (vehicleId) {
|
||||
whereConditions.vehicle_id = vehicleId;
|
||||
}
|
||||
|
||||
if (orderId) {
|
||||
whereConditions.order_id = orderId;
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
whereConditions[Op.or] = [
|
||||
{ transport_no: { [Op.like]: `%${keyword}%` } },
|
||||
{ pickup_address: { [Op.like]: `%${keyword}%` } },
|
||||
{ delivery_address: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
if (startDate && endDate) {
|
||||
whereConditions.created_at = {
|
||||
[Op.between]: [new Date(startDate), new Date(endDate)]
|
||||
};
|
||||
}
|
||||
|
||||
// 查询运输任务列表
|
||||
const { count, rows } = await Transport.findAndCountAll({
|
||||
where: whereConditions,
|
||||
include: [
|
||||
{
|
||||
model: Driver,
|
||||
as: 'driver',
|
||||
attributes: ['id', 'name', 'phone', 'license_type']
|
||||
},
|
||||
{
|
||||
model: Vehicle,
|
||||
as: 'vehicle',
|
||||
attributes: ['id', 'plate_number', 'vehicle_type', 'capacity']
|
||||
},
|
||||
{
|
||||
model: Order,
|
||||
as: 'order',
|
||||
attributes: ['id', 'order_no', 'buyer_name', 'cattle_count']
|
||||
}
|
||||
],
|
||||
limit: parseInt(pageSize),
|
||||
offset: (parseInt(page) - 1) * parseInt(pageSize),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
return {
|
||||
transports: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(count / parseInt(pageSize))
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`获取运输任务列表失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运输任务详情
|
||||
* @param {number} id - 运输任务ID
|
||||
* @returns {Object} 运输任务详情
|
||||
*/
|
||||
static async getTransportDetail(id) {
|
||||
try {
|
||||
const transport = await Transport.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: Driver,
|
||||
as: 'driver',
|
||||
attributes: ['id', 'name', 'phone', 'license_type', 'experience_years']
|
||||
},
|
||||
{
|
||||
model: Vehicle,
|
||||
as: 'vehicle',
|
||||
attributes: ['id', 'plate_number', 'vehicle_type', 'capacity', 'load_capacity']
|
||||
},
|
||||
{
|
||||
model: Order,
|
||||
as: 'order',
|
||||
attributes: ['id', 'order_no', 'buyer_name', 'cattle_count', 'expected_weight']
|
||||
},
|
||||
{
|
||||
model: TransportTrack,
|
||||
as: 'tracks',
|
||||
order: [['recorded_at', 'DESC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!transport) {
|
||||
throw new Error('运输任务不存在');
|
||||
}
|
||||
|
||||
return transport;
|
||||
} catch (error) {
|
||||
throw new Error(`获取运输任务详情失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新运输任务状态
|
||||
* @param {number} id - 运输任务ID
|
||||
* @param {string} status - 新状态
|
||||
* @param {Object} updateData - 更新数据
|
||||
* @returns {Object} 更新后的运输任务信息
|
||||
*/
|
||||
static async updateTransportStatus(id, status, updateData = {}) {
|
||||
try {
|
||||
const transport = await Transport.findByPk(id);
|
||||
if (!transport) {
|
||||
throw new Error('运输任务不存在');
|
||||
}
|
||||
|
||||
// 状态转换验证
|
||||
const validTransitions = {
|
||||
'pending': ['assigned', 'cancelled'],
|
||||
'assigned': ['in_transit', 'cancelled'],
|
||||
'in_transit': ['delivered', 'exception'],
|
||||
'exception': ['in_transit', 'cancelled'],
|
||||
'delivered': ['completed'],
|
||||
'completed': [],
|
||||
'cancelled': []
|
||||
};
|
||||
|
||||
if (!validTransitions[transport.status].includes(status)) {
|
||||
throw new Error(`无法从状态 ${transport.status} 转换到 ${status}`);
|
||||
}
|
||||
|
||||
// 根据状态设置时间
|
||||
if (status === 'in_transit' && !transport.start_time) {
|
||||
updateData.start_time = new Date();
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
updateData.end_time = new Date();
|
||||
}
|
||||
|
||||
// 更新运输任务
|
||||
await transport.update({ status, ...updateData });
|
||||
|
||||
// 创建跟踪记录
|
||||
await this.createTrackRecord(id, status, updateData.location, updateData.description);
|
||||
|
||||
return transport;
|
||||
} catch (error) {
|
||||
throw new Error(`更新运输任务状态失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配司机和车辆
|
||||
* @param {number} id - 运输任务ID
|
||||
* @param {number} driverId - 司机ID
|
||||
* @param {number} vehicleId - 车辆ID
|
||||
* @returns {Object} 更新后的运输任务信息
|
||||
*/
|
||||
static async assignDriverAndVehicle(id, driverId, vehicleId) {
|
||||
try {
|
||||
const transport = await Transport.findByPk(id);
|
||||
if (!transport) {
|
||||
throw new Error('运输任务不存在');
|
||||
}
|
||||
|
||||
if (transport.status !== 'pending') {
|
||||
throw new Error('只能为待分配的运输任务分配司机和车辆');
|
||||
}
|
||||
|
||||
// 检查司机是否可用
|
||||
const driver = await Driver.findByPk(driverId);
|
||||
if (!driver || driver.status !== 'available') {
|
||||
throw new Error('司机不存在或不可用');
|
||||
}
|
||||
|
||||
// 检查车辆是否可用
|
||||
const vehicle = await Vehicle.findByPk(vehicleId);
|
||||
if (!vehicle || vehicle.status !== 'available') {
|
||||
throw new Error('车辆不存在或不可用');
|
||||
}
|
||||
|
||||
// 更新运输任务
|
||||
await transport.update({
|
||||
driver_id: driverId,
|
||||
vehicle_id: vehicleId,
|
||||
status: 'assigned'
|
||||
});
|
||||
|
||||
// 更新司机和车辆状态
|
||||
await driver.update({ status: 'busy' });
|
||||
await vehicle.update({ status: 'in_use' });
|
||||
|
||||
// 创建跟踪记录
|
||||
await this.createTrackRecord(
|
||||
id,
|
||||
'assigned',
|
||||
'调度中心',
|
||||
`已分配司机:${driver.name},车辆:${vehicle.plate_number}`
|
||||
);
|
||||
|
||||
return transport;
|
||||
} catch (error) {
|
||||
throw new Error(`分配司机和车辆失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建跟踪记录
|
||||
* @param {number} transportId - 运输任务ID
|
||||
* @param {string} status - 状态
|
||||
* @param {string} location - 位置
|
||||
* @param {string} description - 描述
|
||||
* @returns {Object} 创建的跟踪记录
|
||||
*/
|
||||
static async createTrackRecord(transportId, status, location = '', description = '') {
|
||||
try {
|
||||
const track = await TransportTrack.create({
|
||||
transport_id: transportId,
|
||||
status,
|
||||
location,
|
||||
description,
|
||||
recorded_at: new Date()
|
||||
});
|
||||
|
||||
return track;
|
||||
} catch (error) {
|
||||
throw new Error(`创建跟踪记录失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运输跟踪记录
|
||||
* @param {number} transportId - 运输任务ID
|
||||
* @returns {Array} 跟踪记录列表
|
||||
*/
|
||||
static async getTransportTracks(transportId) {
|
||||
try {
|
||||
const tracks = await TransportTrack.findAll({
|
||||
where: { transport_id: transportId },
|
||||
order: [['recorded_at', 'DESC']]
|
||||
});
|
||||
|
||||
return tracks;
|
||||
} catch (error) {
|
||||
throw new Error(`获取运输跟踪记录失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成运输任务
|
||||
* @param {number} id - 运输任务ID
|
||||
* @param {Object} completionData - 完成数据
|
||||
* @returns {Object} 更新后的运输任务信息
|
||||
*/
|
||||
static async completeTransport(id, completionData) {
|
||||
try {
|
||||
const transport = await Transport.findByPk(id, {
|
||||
include: [
|
||||
{ model: Driver, as: 'driver' },
|
||||
{ model: Vehicle, as: 'vehicle' }
|
||||
]
|
||||
});
|
||||
|
||||
if (!transport) {
|
||||
throw new Error('运输任务不存在');
|
||||
}
|
||||
|
||||
if (transport.status !== 'delivered') {
|
||||
throw new Error('只能完成已送达的运输任务');
|
||||
}
|
||||
|
||||
// 更新运输任务
|
||||
await transport.update({
|
||||
status: 'completed',
|
||||
end_time: new Date(),
|
||||
actual_weight: completionData.actual_weight,
|
||||
delivery_notes: completionData.delivery_notes
|
||||
});
|
||||
|
||||
// 释放司机和车辆
|
||||
if (transport.driver) {
|
||||
await transport.driver.update({ status: 'available' });
|
||||
}
|
||||
|
||||
if (transport.vehicle) {
|
||||
await transport.vehicle.update({ status: 'available' });
|
||||
}
|
||||
|
||||
// 创建跟踪记录
|
||||
await this.createTrackRecord(
|
||||
id,
|
||||
'completed',
|
||||
completionData.delivery_address || '目的地',
|
||||
'运输任务已完成'
|
||||
);
|
||||
|
||||
return transport;
|
||||
} catch (error) {
|
||||
throw new Error(`完成运输任务失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消运输任务
|
||||
* @param {number} id - 运输任务ID
|
||||
* @param {string} reason - 取消原因
|
||||
* @returns {Object} 更新后的运输任务信息
|
||||
*/
|
||||
static async cancelTransport(id, reason) {
|
||||
try {
|
||||
const transport = await Transport.findByPk(id, {
|
||||
include: [
|
||||
{ model: Driver, as: 'driver' },
|
||||
{ model: Vehicle, as: 'vehicle' }
|
||||
]
|
||||
});
|
||||
|
||||
if (!transport) {
|
||||
throw new Error('运输任务不存在');
|
||||
}
|
||||
|
||||
if (['completed', 'cancelled'].includes(transport.status)) {
|
||||
throw new Error('无法取消已完成或已取消的运输任务');
|
||||
}
|
||||
|
||||
// 更新运输任务
|
||||
await transport.update({
|
||||
status: 'cancelled',
|
||||
cancel_reason: reason,
|
||||
cancelled_at: new Date()
|
||||
});
|
||||
|
||||
// 释放司机和车辆
|
||||
if (transport.driver) {
|
||||
await transport.driver.update({ status: 'available' });
|
||||
}
|
||||
|
||||
if (transport.vehicle) {
|
||||
await transport.vehicle.update({ status: 'available' });
|
||||
}
|
||||
|
||||
// 创建跟踪记录
|
||||
await this.createTrackRecord(id, 'cancelled', '调度中心', `运输任务已取消:${reason}`);
|
||||
|
||||
return transport;
|
||||
} catch (error) {
|
||||
throw new Error(`取消运输任务失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成运输单号
|
||||
* @returns {string} 运输单号
|
||||
*/
|
||||
static async generateTransportNo() {
|
||||
try {
|
||||
const date = new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const timestamp = Date.now().toString().slice(-6);
|
||||
const random = Math.floor(Math.random() * 100).toString().padStart(2, '0');
|
||||
|
||||
return `TRP${year}${month}${day}${timestamp}${random}`;
|
||||
} catch (error) {
|
||||
throw new Error(`生成运输单号失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运输统计信息
|
||||
* @param {Object} query - 查询参数
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
static async getTransportStats(query = {}) {
|
||||
try {
|
||||
const { startDate, endDate } = query;
|
||||
|
||||
// 构建时间范围条件
|
||||
const dateCondition = {};
|
||||
if (startDate && endDate) {
|
||||
dateCondition.created_at = {
|
||||
[Op.between]: [new Date(startDate), new Date(endDate)]
|
||||
};
|
||||
}
|
||||
|
||||
// 总数统计
|
||||
const totalCount = await Transport.count({ where: dateCondition });
|
||||
const pendingCount = await Transport.count({
|
||||
where: { ...dateCondition, status: 'pending' }
|
||||
});
|
||||
const inTransitCount = await Transport.count({
|
||||
where: { ...dateCondition, status: 'in_transit' }
|
||||
});
|
||||
const completedCount = await Transport.count({
|
||||
where: { ...dateCondition, status: 'completed' }
|
||||
});
|
||||
const cancelledCount = await Transport.count({
|
||||
where: { ...dateCondition, status: 'cancelled' }
|
||||
});
|
||||
|
||||
// 按状态统计
|
||||
const statusStats = await Transport.findAll({
|
||||
attributes: [
|
||||
'status',
|
||||
[Transport.sequelize.fn('COUNT', '*'), 'count']
|
||||
],
|
||||
where: dateCondition,
|
||||
group: ['status'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 按司机统计
|
||||
const driverStats = await Transport.findAll({
|
||||
attributes: [
|
||||
'driver_id',
|
||||
[Transport.sequelize.fn('COUNT', '*'), 'count']
|
||||
],
|
||||
where: { ...dateCondition, driver_id: { [Op.not]: null } },
|
||||
group: ['driver_id'],
|
||||
include: [
|
||||
{
|
||||
model: Driver,
|
||||
as: 'driver',
|
||||
attributes: ['name']
|
||||
}
|
||||
],
|
||||
limit: 10,
|
||||
order: [[Transport.sequelize.fn('COUNT', '*'), 'DESC']]
|
||||
});
|
||||
|
||||
return {
|
||||
total: totalCount,
|
||||
pending: pendingCount,
|
||||
inTransit: inTransitCount,
|
||||
completed: completedCount,
|
||||
cancelled: cancelledCount,
|
||||
statusStats,
|
||||
driverStats
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`获取运输统计信息失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TransportService;
|
||||
@@ -75,10 +75,12 @@ const updateUser = async (id, updateData) => {
|
||||
}
|
||||
});
|
||||
|
||||
const [updatedRowsCount] = await User.update(filteredData, {
|
||||
const result = await User.update(filteredData, {
|
||||
where: { id }
|
||||
});
|
||||
|
||||
const updatedRowsCount = result[0];
|
||||
|
||||
if (updatedRowsCount === 0) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
@@ -88,10 +90,12 @@ const updateUser = async (id, updateData) => {
|
||||
|
||||
// 更新用户状态服务
|
||||
const updateUserStatus = async (id, status) => {
|
||||
const [updatedRowsCount] = await User.update({ status }, {
|
||||
const result = await User.update({ status }, {
|
||||
where: { id }
|
||||
});
|
||||
|
||||
const updatedRowsCount = result[0];
|
||||
|
||||
if (updatedRowsCount === 0) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
386
backend/src/services/VehicleService.js
Normal file
386
backend/src/services/VehicleService.js
Normal file
@@ -0,0 +1,386 @@
|
||||
const { Vehicle, Driver, Transport } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 车辆服务层
|
||||
* 处理车辆相关的业务逻辑
|
||||
*/
|
||||
class VehicleService {
|
||||
/**
|
||||
* 创建车辆
|
||||
* @param {Object} vehicleData - 车辆数据
|
||||
* @returns {Promise<Object>} 创建的车辆信息
|
||||
*/
|
||||
static async createVehicle(vehicleData) {
|
||||
try {
|
||||
// 生成车辆编号
|
||||
if (!vehicleData.vehicle_number) {
|
||||
vehicleData.vehicle_number = await this.generateVehicleNumber();
|
||||
}
|
||||
|
||||
// 检查车牌号是否已存在
|
||||
if (vehicleData.license_plate) {
|
||||
const existingVehicle = await Vehicle.findOne({
|
||||
where: { license_plate: vehicleData.license_plate }
|
||||
});
|
||||
|
||||
if (existingVehicle) {
|
||||
throw new Error('车牌号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
const vehicle = await Vehicle.create(vehicleData);
|
||||
return vehicle;
|
||||
} catch (error) {
|
||||
throw new Error(`创建车辆失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆列表
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Promise<Object>} 车辆列表和分页信息
|
||||
*/
|
||||
static async getVehicleList(options = {}) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
status,
|
||||
vehicle_type,
|
||||
brand,
|
||||
search
|
||||
} = options;
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
const where = {};
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
// 车辆类型筛选
|
||||
if (vehicle_type) {
|
||||
where.vehicle_type = vehicle_type;
|
||||
}
|
||||
|
||||
// 品牌筛选
|
||||
if (brand) {
|
||||
where.brand = brand;
|
||||
}
|
||||
|
||||
// 搜索条件
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ license_plate: { [Op.like]: `%${search}%` } },
|
||||
{ vehicle_number: { [Op.like]: `%${search}%` } },
|
||||
{ brand: { [Op.like]: `%${search}%` } },
|
||||
{ model: { [Op.like]: `%${search}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const { count, rows } = await Vehicle.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Driver,
|
||||
as: 'driver',
|
||||
attributes: ['id', 'name', 'phone', 'status']
|
||||
}
|
||||
],
|
||||
offset,
|
||||
limit: parseInt(pageSize),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
return {
|
||||
vehicles: rows,
|
||||
pagination: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(count / pageSize)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`获取车辆列表失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆详情
|
||||
* @param {number} id - 车辆ID
|
||||
* @returns {Promise<Object>} 车辆详情
|
||||
*/
|
||||
static async getVehicleDetail(id) {
|
||||
try {
|
||||
const vehicle = await Vehicle.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: Driver,
|
||||
as: 'driver',
|
||||
attributes: ['id', 'name', 'phone', 'license_number', 'status']
|
||||
},
|
||||
{
|
||||
model: Transport,
|
||||
as: 'transports',
|
||||
attributes: ['id', 'transport_number', 'status', 'created_at'],
|
||||
limit: 10,
|
||||
order: [['created_at', 'DESC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!vehicle) {
|
||||
throw new Error('车辆不存在');
|
||||
}
|
||||
|
||||
return vehicle;
|
||||
} catch (error) {
|
||||
throw new Error(`获取车辆详情失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新车辆信息
|
||||
* @param {number} id - 车辆ID
|
||||
* @param {Object} updateData - 更新数据
|
||||
* @returns {Promise<Object>} 更新后的车辆信息
|
||||
*/
|
||||
static async updateVehicle(id, updateData) {
|
||||
try {
|
||||
const vehicle = await Vehicle.findByPk(id);
|
||||
if (!vehicle) {
|
||||
throw new Error('车辆不存在');
|
||||
}
|
||||
|
||||
// 如果更新车牌号,检查是否重复
|
||||
if (updateData.license_plate && updateData.license_plate !== vehicle.license_plate) {
|
||||
const existingVehicle = await Vehicle.findOne({
|
||||
where: {
|
||||
license_plate: updateData.license_plate,
|
||||
id: { [Op.ne]: id }
|
||||
}
|
||||
});
|
||||
|
||||
if (existingVehicle) {
|
||||
throw new Error('车牌号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
await vehicle.update(updateData);
|
||||
return vehicle;
|
||||
} catch (error) {
|
||||
throw new Error(`更新车辆信息失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新车辆状态
|
||||
* @param {number} id - 车辆ID
|
||||
* @param {string} status - 新状态
|
||||
* @returns {Promise<Object>} 更新后的车辆信息
|
||||
*/
|
||||
static async updateVehicleStatus(id, status) {
|
||||
try {
|
||||
const vehicle = await Vehicle.findByPk(id);
|
||||
if (!vehicle) {
|
||||
throw new Error('车辆不存在');
|
||||
}
|
||||
|
||||
await vehicle.update({ status });
|
||||
return vehicle;
|
||||
} catch (error) {
|
||||
throw new Error(`更新车辆状态失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除车辆
|
||||
* @param {number} id - 车辆ID
|
||||
* @returns {Promise<boolean>} 删除结果
|
||||
*/
|
||||
static async deleteVehicle(id) {
|
||||
try {
|
||||
const vehicle = await Vehicle.findByPk(id);
|
||||
if (!vehicle) {
|
||||
throw new Error('车辆不存在');
|
||||
}
|
||||
|
||||
// 检查是否有正在进行的运输任务
|
||||
const activeTransports = await Transport.count({
|
||||
where: {
|
||||
vehicle_id: id,
|
||||
status: { [Op.in]: ['pending', 'assigned', 'in_transit'] }
|
||||
}
|
||||
});
|
||||
|
||||
if (activeTransports > 0) {
|
||||
throw new Error('车辆有正在进行的运输任务,无法删除');
|
||||
}
|
||||
|
||||
await vehicle.destroy();
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`删除车辆失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用车辆列表
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Promise<Array>} 可用车辆列表
|
||||
*/
|
||||
static async getAvailableVehicles(options = {}) {
|
||||
try {
|
||||
const { vehicle_type, load_capacity_min } = options;
|
||||
const where = { status: 'available' };
|
||||
|
||||
if (vehicle_type) {
|
||||
where.vehicle_type = vehicle_type;
|
||||
}
|
||||
|
||||
if (load_capacity_min) {
|
||||
where.load_capacity = { [Op.gte]: load_capacity_min };
|
||||
}
|
||||
|
||||
const vehicles = await Vehicle.findAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Driver,
|
||||
as: 'driver',
|
||||
attributes: ['id', 'name', 'phone', 'status'],
|
||||
where: { status: 'available' },
|
||||
required: false
|
||||
}
|
||||
],
|
||||
order: [['load_capacity', 'DESC']]
|
||||
});
|
||||
|
||||
return vehicles;
|
||||
} catch (error) {
|
||||
throw new Error(`获取可用车辆列表失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配司机
|
||||
* @param {number} vehicleId - 车辆ID
|
||||
* @param {number} driverId - 司机ID
|
||||
* @returns {Promise<Object>} 更新后的车辆信息
|
||||
*/
|
||||
static async assignDriver(vehicleId, driverId) {
|
||||
try {
|
||||
const vehicle = await Vehicle.findByPk(vehicleId);
|
||||
if (!vehicle) {
|
||||
throw new Error('车辆不存在');
|
||||
}
|
||||
|
||||
const driver = await Driver.findByPk(driverId);
|
||||
if (!driver) {
|
||||
throw new Error('司机不存在');
|
||||
}
|
||||
|
||||
if (driver.status !== 'available') {
|
||||
throw new Error('司机当前不可用');
|
||||
}
|
||||
|
||||
// 检查司机是否已分配给其他车辆
|
||||
const existingAssignment = await Vehicle.findOne({
|
||||
where: {
|
||||
driver_id: driverId,
|
||||
id: { [Op.ne]: vehicleId }
|
||||
}
|
||||
});
|
||||
|
||||
if (existingAssignment) {
|
||||
throw new Error('司机已分配给其他车辆');
|
||||
}
|
||||
|
||||
await vehicle.update({ driver_id: driverId });
|
||||
return vehicle;
|
||||
} catch (error) {
|
||||
throw new Error(`分配司机失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆统计信息
|
||||
* @returns {Promise<Object>} 统计信息
|
||||
*/
|
||||
static async getVehicleStatistics() {
|
||||
try {
|
||||
const totalCount = await Vehicle.count();
|
||||
const availableCount = await Vehicle.count({ where: { status: 'available' } });
|
||||
const busyCount = await Vehicle.count({ where: { status: 'busy' } });
|
||||
const maintenanceCount = await Vehicle.count({ where: { status: 'maintenance' } });
|
||||
const offlineCount = await Vehicle.count({ where: { status: 'offline' } });
|
||||
|
||||
// 按车辆类型统计
|
||||
const typeStats = await Vehicle.findAll({
|
||||
attributes: [
|
||||
'vehicle_type',
|
||||
[Vehicle.sequelize.fn('COUNT', Vehicle.sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['vehicle_type']
|
||||
});
|
||||
|
||||
// 按品牌统计
|
||||
const brandStats = await Vehicle.findAll({
|
||||
attributes: [
|
||||
'brand',
|
||||
[Vehicle.sequelize.fn('COUNT', Vehicle.sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['brand'],
|
||||
order: [[Vehicle.sequelize.fn('COUNT', Vehicle.sequelize.col('id')), 'DESC']],
|
||||
limit: 10
|
||||
});
|
||||
|
||||
return {
|
||||
total: totalCount,
|
||||
available: availableCount,
|
||||
busy: busyCount,
|
||||
maintenance: maintenanceCount,
|
||||
offline: offlineCount,
|
||||
typeStats: typeStats.map(item => ({
|
||||
type: item.vehicle_type,
|
||||
count: parseInt(item.dataValues.count)
|
||||
})),
|
||||
brandStats: brandStats.map(item => ({
|
||||
brand: item.brand,
|
||||
count: parseInt(item.dataValues.count)
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`获取车辆统计信息失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成车辆编号
|
||||
* @returns {Promise<string>} 车辆编号
|
||||
*/
|
||||
static async generateVehicleNumber() {
|
||||
try {
|
||||
const today = new Date();
|
||||
const dateStr = today.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
|
||||
const count = await Vehicle.count({
|
||||
where: {
|
||||
vehicle_number: {
|
||||
[Op.like]: `V${dateStr}%`
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sequence = (count + 1).toString().padStart(4, '0');
|
||||
return `V${dateStr}${sequence}`;
|
||||
} catch (error) {
|
||||
throw new Error(`生成车辆编号失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VehicleService;
|
||||
261
backend/tests/integration/auth.test.js
Normal file
261
backend/tests/integration/auth.test.js
Normal file
@@ -0,0 +1,261 @@
|
||||
const request = require('supertest');
|
||||
const testSequelize = require('../test-database');
|
||||
|
||||
// 创建测试专用的User模型
|
||||
const { DataTypes } = require('sequelize');
|
||||
const User = testSequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '用户ID'
|
||||
},
|
||||
nickname: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '用户昵称'
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
comment: '手机号码'
|
||||
},
|
||||
password_hash: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '密码哈希值'
|
||||
},
|
||||
user_type: {
|
||||
type: DataTypes.ENUM('buyer', 'trader', 'supplier', 'driver', 'staff', 'admin'),
|
||||
allowNull: false,
|
||||
defaultValue: 'buyer',
|
||||
comment: '用户类型'
|
||||
}
|
||||
}, {
|
||||
tableName: 'users',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
});
|
||||
|
||||
// 创建测试专用的app实例
|
||||
const createTestApp = () => {
|
||||
// 确保测试环境
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
// 导入app但不启动服务器
|
||||
const app = require('../../src/main');
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('Authentication Integration Tests', () => {
|
||||
let app;
|
||||
let testUser;
|
||||
let authToken;
|
||||
|
||||
beforeAll(async () => {
|
||||
// 创建测试app
|
||||
app = createTestApp();
|
||||
|
||||
// 同步测试数据库
|
||||
await testSequelize.sync({ force: true });
|
||||
|
||||
// 创建测试用户 - 使用User模型中实际存在的字段
|
||||
testUser = await User.create({
|
||||
nickname: 'Test User', // 必填字段
|
||||
phone: '13800138000', // 可选字段
|
||||
password_hash: 'testpassword123', // 可选字段
|
||||
user_type: 'buyer' // 必填字段,默认值
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据
|
||||
if (testUser) {
|
||||
await testUser.destroy();
|
||||
}
|
||||
|
||||
// 关闭测试数据库连接
|
||||
await testSequelize.close();
|
||||
});
|
||||
|
||||
describe('POST /api/auth/login', () => {
|
||||
it('应该成功登录并返回token', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
username: '13800138000', // 使用phone登录
|
||||
password: 'testpassword123'
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.message).toBe('登录成功');
|
||||
expect(response.body.data).toHaveProperty('access_token');
|
||||
expect(response.body.data).toHaveProperty('user');
|
||||
expect(response.body.data.user.username).toBe('Test User');
|
||||
expect(response.body.data.user).not.toHaveProperty('password');
|
||||
|
||||
// 保存token用于后续测试
|
||||
authToken = response.body.data.access_token;
|
||||
|
||||
// 保存token用于后续测试
|
||||
authToken = response.body.data.access_token;
|
||||
});
|
||||
|
||||
it('应该在用户名不存在时返回错误', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
username: 'nonexistent',
|
||||
password: 'testpassword123'
|
||||
})
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('用户名或密码错误');
|
||||
});
|
||||
|
||||
it('应该在密码错误时返回错误', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
username: '13800138000',
|
||||
password: 'wrongpassword'
|
||||
})
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('用户名或密码错误');
|
||||
});
|
||||
|
||||
it('应该在缺少参数时返回错误', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
username: '13800138000'
|
||||
// 缺少password
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('用户名和密码不能为空');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/auth/logout', () => {
|
||||
it('应该成功登出', async () => {
|
||||
// 先登录获取token
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
username: '13800138000',
|
||||
password: 'testpassword123'
|
||||
});
|
||||
|
||||
const token = loginResponse.body.data.access_token;
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/logout')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.message).toBe('登出成功');
|
||||
});
|
||||
|
||||
it('应该在没有认证时也能成功登出', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/logout')
|
||||
.expect(401); // 因为没有提供token,应该返回401
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/auth/current', () => {
|
||||
it('应该返回当前用户信息', async () => {
|
||||
// 先登录获取token
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
username: '13800138000',
|
||||
password: 'testpassword123'
|
||||
});
|
||||
|
||||
const token = loginResponse.body.data.access_token;
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/auth/current')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty('id');
|
||||
expect(response.body.data).toHaveProperty('username');
|
||||
expect(response.body.data).not.toHaveProperty('password');
|
||||
});
|
||||
|
||||
it('应该在没有认证时返回错误', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/current')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
|
||||
it('应该在token无效时返回错误', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/current')
|
||||
.set('Authorization', 'Bearer invalid_token')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/auth/mini-program/login', () => {
|
||||
it('应该成功进行小程序登录', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/mini-program/login')
|
||||
.send({
|
||||
phone: '13900139000',
|
||||
code: '123456',
|
||||
miniProgramType: 'client'
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.message).toBe('登录成功');
|
||||
expect(response.body.data).toHaveProperty('token');
|
||||
expect(response.body.data).toHaveProperty('userInfo');
|
||||
});
|
||||
|
||||
it('应该在验证码错误时返回错误', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/mini-program/login')
|
||||
.send({
|
||||
phone: '13900139000',
|
||||
code: '000000', // 错误的验证码
|
||||
miniProgramType: 'client'
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('验证码错误');
|
||||
});
|
||||
|
||||
it('应该在缺少参数时返回错误', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/mini-program/login')
|
||||
.send({
|
||||
phone: '13900139000'
|
||||
// 缺少code和miniProgramType
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
90
backend/tests/setup.js
Normal file
90
backend/tests/setup.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// 测试环境配置 - 使用SQLite内存数据库进行测试
|
||||
const { Sequelize } = require('sequelize');
|
||||
|
||||
// 设置测试环境变量
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
// 创建测试用的SQLite内存数据库
|
||||
const testSequelize = new Sequelize('sqlite::memory:', {
|
||||
logging: false, // 关闭SQL日志
|
||||
define: {
|
||||
timestamps: true,
|
||||
underscored: false,
|
||||
freezeTableName: false
|
||||
}
|
||||
});
|
||||
|
||||
// 重写数据库配置模块,让测试使用SQLite
|
||||
const originalRequire = require;
|
||||
require = function(id) {
|
||||
if (id === '../src/config/database' || id.endsWith('/config/database')) {
|
||||
return testSequelize;
|
||||
}
|
||||
return originalRequire.apply(this, arguments);
|
||||
};
|
||||
|
||||
// 全局测试设置
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
// 连接测试数据库
|
||||
await testSequelize.authenticate();
|
||||
console.log('测试数据库连接成功');
|
||||
|
||||
// 导入所有模型
|
||||
const User = require('../src/models/User');
|
||||
const Order = require('../src/models/Order');
|
||||
|
||||
// 同步数据库模型(仅在测试环境)
|
||||
await testSequelize.sync({ force: true });
|
||||
console.log('测试数据库同步完成');
|
||||
} catch (error) {
|
||||
console.error('测试数据库连接失败:', error);
|
||||
// 不要直接退出进程,让Jest处理错误
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 每个测试后清理数据
|
||||
afterEach(async () => {
|
||||
try {
|
||||
// 清理所有表数据,但保留表结构
|
||||
const models = Object.keys(testSequelize.models);
|
||||
for (const modelName of models) {
|
||||
await testSequelize.models[modelName].destroy({
|
||||
where: {},
|
||||
truncate: true,
|
||||
cascade: true,
|
||||
restartIdentity: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清理测试数据失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 测试后清理
|
||||
afterAll(async () => {
|
||||
try {
|
||||
// 关闭数据库连接
|
||||
await testSequelize.close();
|
||||
console.log('测试数据库连接已关闭');
|
||||
} catch (error) {
|
||||
console.error('测试清理失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 设置全局超时
|
||||
jest.setTimeout(30000);
|
||||
|
||||
// 导出测试数据库实例供测试文件使用
|
||||
global.testSequelize = testSequelize;
|
||||
|
||||
// 抑制控制台输出(可选)
|
||||
// global.console = {
|
||||
// ...console,
|
||||
// log: jest.fn(),
|
||||
// debug: jest.fn(),
|
||||
// info: jest.fn(),
|
||||
// warn: jest.fn(),
|
||||
// error: jest.fn(),
|
||||
// };
|
||||
16
backend/tests/test-database.js
Normal file
16
backend/tests/test-database.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// 测试专用数据库配置
|
||||
const { Sequelize } = require('sequelize');
|
||||
|
||||
// 创建测试用的SQLite内存数据库
|
||||
const testSequelize = new Sequelize('sqlite::memory:', {
|
||||
logging: false, // 关闭SQL日志
|
||||
dialect: 'sqlite',
|
||||
storage: ':memory:',
|
||||
define: {
|
||||
timestamps: true,
|
||||
underscored: false,
|
||||
freezeTableName: false
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = testSequelize;
|
||||
226
backend/tests/unit/controllers/UserController.test.js
Normal file
226
backend/tests/unit/controllers/UserController.test.js
Normal file
@@ -0,0 +1,226 @@
|
||||
const UserController = require('../../../src/controllers/UserController');
|
||||
const User = require('../../../src/models/User');
|
||||
const { successResponse, errorResponse, paginatedResponse } = require('../../../src/utils/response');
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../../src/models/User');
|
||||
jest.mock('../../../src/utils/response');
|
||||
|
||||
describe('UserController', () => {
|
||||
let req, res, next;
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
params: {},
|
||||
body: {},
|
||||
query: {},
|
||||
user: { id: 1, user_type: 'admin' }
|
||||
};
|
||||
res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis()
|
||||
};
|
||||
next = jest.fn();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('getUserList', () => {
|
||||
it('应该成功获取用户列表', async () => {
|
||||
const mockUsers = [
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'uuid1',
|
||||
username: 'user1',
|
||||
real_name: 'Real User 1',
|
||||
phone: '13800138001',
|
||||
email: 'user1@example.com',
|
||||
user_type: 'user',
|
||||
status: 'active',
|
||||
avatar_url: 'avatar1.jpg',
|
||||
created_at: new Date()
|
||||
}
|
||||
];
|
||||
|
||||
User.findAndCountAll.mockResolvedValue({
|
||||
count: 1,
|
||||
rows: mockUsers
|
||||
});
|
||||
|
||||
paginatedResponse.mockReturnValue({
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
users: mockUsers,
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
req.query = { page: '1', pageSize: '10' };
|
||||
|
||||
await UserController.getUserList(req, res);
|
||||
|
||||
expect(User.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: {},
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
expect(res.json).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该处理数据库错误', async () => {
|
||||
const error = new Error('数据库错误');
|
||||
User.findAndCountAll.mockRejectedValue(error);
|
||||
|
||||
errorResponse.mockReturnValue({
|
||||
code: 500,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
|
||||
await UserController.getUserList(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserDetail', () => {
|
||||
it('应该成功获取用户详情', async () => {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
uuid: 'uuid1',
|
||||
username: 'user1',
|
||||
real_name: 'Real User 1',
|
||||
phone: '13800138001',
|
||||
email: 'user1@example.com',
|
||||
user_type: 'user',
|
||||
status: 'active',
|
||||
avatar_url: 'avatar1.jpg',
|
||||
created_at: new Date()
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
successResponse.mockReturnValue({
|
||||
code: 200,
|
||||
message: '获取用户详情成功',
|
||||
data: mockUser
|
||||
});
|
||||
|
||||
req.params.id = '1';
|
||||
|
||||
await UserController.getUserDetail(req, res);
|
||||
|
||||
expect(User.findByPk).toHaveBeenCalledWith('1');
|
||||
expect(res.json).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该处理用户不存在的情况', async () => {
|
||||
User.findByPk.mockResolvedValue(null);
|
||||
|
||||
errorResponse.mockReturnValue({
|
||||
code: 404,
|
||||
message: '用户不存在'
|
||||
});
|
||||
|
||||
req.params.id = '999';
|
||||
|
||||
await UserController.getUserDetail(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('应该成功更新用户信息', async () => {
|
||||
User.update.mockResolvedValue([1]); // 返回更新的行数
|
||||
|
||||
successResponse.mockReturnValue({
|
||||
code: 200,
|
||||
message: '用户信息更新成功'
|
||||
});
|
||||
|
||||
req.params.id = '1';
|
||||
req.body = { real_name: '新用户名', email: 'new@example.com' };
|
||||
|
||||
await UserController.updateUser(req, res, next);
|
||||
|
||||
expect(User.update).toHaveBeenCalledWith(
|
||||
{ real_name: '新用户名', email: 'new@example.com' },
|
||||
{ where: { id: '1' } }
|
||||
);
|
||||
expect(res.json).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该处理用户不存在的情况', async () => {
|
||||
User.update.mockResolvedValue([0]); // 返回0表示没有更新任何行
|
||||
|
||||
errorResponse.mockReturnValue({
|
||||
code: 404,
|
||||
message: '用户不存在'
|
||||
});
|
||||
|
||||
req.params.id = '999';
|
||||
req.body = { real_name: '新用户名' };
|
||||
|
||||
await UserController.updateUser(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('updateUserStatus', () => {
|
||||
it('应该成功更新用户状态', async () => {
|
||||
User.update.mockResolvedValue([1]); // 返回更新的行数
|
||||
|
||||
successResponse.mockReturnValue({
|
||||
code: 200,
|
||||
message: '用户状态更新成功'
|
||||
});
|
||||
|
||||
req.params.id = '1';
|
||||
req.body = { status: 'inactive' };
|
||||
|
||||
await UserController.updateUserStatus(req, res, next);
|
||||
|
||||
expect(User.update).toHaveBeenCalledWith(
|
||||
{ status: 'inactive' },
|
||||
{ where: { id: '1' } }
|
||||
);
|
||||
expect(res.json).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该处理用户不存在的情况', async () => {
|
||||
User.update.mockResolvedValue([0]); // 返回0表示没有更新任何行
|
||||
|
||||
errorResponse.mockReturnValue({
|
||||
code: 404,
|
||||
message: '用户不存在'
|
||||
});
|
||||
|
||||
req.params.id = '999';
|
||||
req.body = { status: 'inactive' };
|
||||
|
||||
await UserController.updateUserStatus(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
179
backend/tests/unit/services/AuthService.test.js
Normal file
179
backend/tests/unit/services/AuthService.test.js
Normal file
@@ -0,0 +1,179 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const AuthService = require('../../../src/services/AuthService');
|
||||
const User = require('../../../src/models/User');
|
||||
const { jwtConfig } = require('../../../src/config/config');
|
||||
|
||||
// Mock依赖
|
||||
jest.mock('../../../src/models/User');
|
||||
jest.mock('jsonwebtoken');
|
||||
jest.mock('bcryptjs');
|
||||
jest.mock('uuid');
|
||||
|
||||
describe('AuthService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('miniProgramLogin', () => {
|
||||
const mockPhone = '13800138000';
|
||||
const mockCode = '123456';
|
||||
const mockMiniProgramType = 'client';
|
||||
const mockUuid = 'test-uuid-123';
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
uuid: mockUuid,
|
||||
username: `user_${mockPhone}`,
|
||||
phone: mockPhone,
|
||||
user_type: mockMiniProgramType,
|
||||
real_name: '测试用户',
|
||||
avatar_url: 'http://example.com/avatar.jpg'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
uuidv4.mockReturnValue(mockUuid);
|
||||
bcrypt.hashSync.mockReturnValue('hashed_password');
|
||||
jwt.sign.mockReturnValue('mock_jwt_token');
|
||||
});
|
||||
|
||||
test('验证码错误时应该抛出异常', async () => {
|
||||
await expect(AuthService.miniProgramLogin(mockPhone, '000000', mockMiniProgramType))
|
||||
.rejects.toThrow('验证码错误');
|
||||
});
|
||||
|
||||
test('用户存在时应该返回用户信息和token', async () => {
|
||||
// 模拟用户已存在
|
||||
User.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await AuthService.miniProgramLogin(mockPhone, mockCode, mockMiniProgramType);
|
||||
|
||||
expect(User.findOne).toHaveBeenCalledWith({ where: { phone: mockPhone } });
|
||||
expect(User.create).not.toHaveBeenCalled();
|
||||
expect(jwt.sign).toHaveBeenCalledWith(
|
||||
{
|
||||
id: mockUser.id,
|
||||
uuid: mockUser.uuid,
|
||||
username: mockUser.username,
|
||||
phone: mockUser.phone,
|
||||
userType: mockUser.user_type
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{ expiresIn: jwtConfig.expiresIn }
|
||||
);
|
||||
expect(result).toEqual({
|
||||
token: 'mock_jwt_token',
|
||||
userInfo: {
|
||||
id: mockUser.id,
|
||||
username: mockUser.username,
|
||||
realName: mockUser.real_name,
|
||||
avatar: mockUser.avatar_url,
|
||||
userType: mockUser.user_type,
|
||||
phone: mockUser.phone
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('用户不存在时应该创建新用户并返回信息', async () => {
|
||||
// 模拟用户不存在
|
||||
User.findOne.mockResolvedValue(null);
|
||||
User.create.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await AuthService.miniProgramLogin(mockPhone, mockCode, mockMiniProgramType);
|
||||
|
||||
expect(User.findOne).toHaveBeenCalledWith({ where: { phone: mockPhone } });
|
||||
expect(uuidv4).toHaveBeenCalled();
|
||||
expect(bcrypt.hashSync).toHaveBeenCalledWith(mockPhone, 10);
|
||||
expect(User.create).toHaveBeenCalledWith({
|
||||
uuid: mockUuid,
|
||||
username: `user_${mockPhone}`,
|
||||
phone: mockPhone,
|
||||
user_type: mockMiniProgramType,
|
||||
password_hash: 'hashed_password'
|
||||
});
|
||||
expect(result).toEqual({
|
||||
token: 'mock_jwt_token',
|
||||
userInfo: {
|
||||
id: mockUser.id,
|
||||
username: mockUser.username,
|
||||
realName: mockUser.real_name,
|
||||
avatar: mockUser.avatar_url,
|
||||
userType: mockUser.user_type,
|
||||
phone: mockUser.phone
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('未指定小程序类型时应该使用默认值client', async () => {
|
||||
User.findOne.mockResolvedValue(null);
|
||||
User.create.mockResolvedValue({
|
||||
...mockUser,
|
||||
user_type: 'client'
|
||||
});
|
||||
|
||||
await AuthService.miniProgramLogin(mockPhone, mockCode);
|
||||
|
||||
expect(User.create).toHaveBeenCalledWith({
|
||||
uuid: mockUuid,
|
||||
username: `user_${mockPhone}`,
|
||||
phone: mockPhone,
|
||||
user_type: 'client',
|
||||
password_hash: 'hashed_password'
|
||||
});
|
||||
});
|
||||
|
||||
test('数据库操作失败时应该抛出异常', async () => {
|
||||
User.findOne.mockRejectedValue(new Error('数据库连接失败'));
|
||||
|
||||
await expect(AuthService.miniProgramLogin(mockPhone, mockCode, mockMiniProgramType))
|
||||
.rejects.toThrow('数据库连接失败');
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyToken', () => {
|
||||
const mockToken = 'valid_jwt_token';
|
||||
const mockDecodedToken = {
|
||||
id: 1,
|
||||
uuid: 'test-uuid-123',
|
||||
username: 'testuser',
|
||||
phone: '13800138000',
|
||||
userType: 'client'
|
||||
};
|
||||
|
||||
test('有效token应该返回解码后的信息', async () => {
|
||||
jwt.verify.mockReturnValue(mockDecodedToken);
|
||||
|
||||
const result = await AuthService.verifyToken(mockToken);
|
||||
|
||||
expect(jwt.verify).toHaveBeenCalledWith(mockToken, jwtConfig.secret);
|
||||
expect(result).toEqual(mockDecodedToken);
|
||||
});
|
||||
|
||||
test('无效token应该抛出异常', async () => {
|
||||
jwt.verify.mockImplementation(() => {
|
||||
throw new Error('jwt malformed');
|
||||
});
|
||||
|
||||
await expect(AuthService.verifyToken('invalid_token'))
|
||||
.rejects.toThrow('无效的token');
|
||||
});
|
||||
|
||||
test('过期token应该抛出异常', async () => {
|
||||
jwt.verify.mockImplementation(() => {
|
||||
throw new Error('jwt expired');
|
||||
});
|
||||
|
||||
await expect(AuthService.verifyToken('expired_token'))
|
||||
.rejects.toThrow('无效的token');
|
||||
});
|
||||
|
||||
test('空token应该抛出异常', async () => {
|
||||
jwt.verify.mockImplementation(() => {
|
||||
throw new Error('jwt must be provided');
|
||||
});
|
||||
|
||||
await expect(AuthService.verifyToken(''))
|
||||
.rejects.toThrow('无效的token');
|
||||
});
|
||||
});
|
||||
});
|
||||
307
backend/tests/unit/services/OrderService.test.js
Normal file
307
backend/tests/unit/services/OrderService.test.js
Normal file
@@ -0,0 +1,307 @@
|
||||
const OrderService = require('../../../src/services/OrderService');
|
||||
const Order = require('../../../src/models/Order');
|
||||
|
||||
// Mock依赖
|
||||
jest.mock('../../../src/models/Order');
|
||||
|
||||
describe('OrderService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createOrder', () => {
|
||||
const mockUserId = 1;
|
||||
const mockOrderData = {
|
||||
product_name: '测试商品',
|
||||
quantity: 100,
|
||||
unit_price: 50.00,
|
||||
total_amount: 5000.00
|
||||
};
|
||||
const mockCreatedOrder = {
|
||||
id: 1,
|
||||
order_no: 'ORD1234567890123',
|
||||
buyer_id: mockUserId,
|
||||
...mockOrderData
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Date.now() 和 Math.random()
|
||||
jest.spyOn(Date, 'now').mockReturnValue(1234567890123);
|
||||
jest.spyOn(Math, 'random').mockReturnValue(0.123);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date.now.mockRestore();
|
||||
Math.random.mockRestore();
|
||||
});
|
||||
|
||||
test('应该成功创建订单', async () => {
|
||||
Order.create.mockResolvedValue(mockCreatedOrder);
|
||||
|
||||
const result = await OrderService.createOrder(mockOrderData, mockUserId);
|
||||
|
||||
expect(Order.create).toHaveBeenCalledWith({
|
||||
...mockOrderData,
|
||||
buyer_id: mockUserId,
|
||||
order_no: 'ORD1234567890123123'
|
||||
});
|
||||
expect(result).toEqual(mockCreatedOrder);
|
||||
});
|
||||
|
||||
test('数据库创建失败时应该抛出异常', async () => {
|
||||
Order.create.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(OrderService.createOrder(mockOrderData, mockUserId))
|
||||
.rejects.toThrow('数据库错误');
|
||||
});
|
||||
|
||||
test('应该生成唯一的订单号', async () => {
|
||||
Order.create.mockResolvedValue(mockCreatedOrder);
|
||||
|
||||
await OrderService.createOrder(mockOrderData, mockUserId);
|
||||
|
||||
const expectedOrderNo = 'ORD1234567890123123';
|
||||
expect(Order.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
order_no: expectedOrderNo
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrderList', () => {
|
||||
const mockQuery = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
status: 'pending',
|
||||
orderNo: 'ORD123'
|
||||
};
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
userType: 'client'
|
||||
};
|
||||
const mockOrders = [
|
||||
{ id: 1, order_no: 'ORD123', buyer_id: 1 },
|
||||
{ id: 2, order_no: 'ORD124', buyer_id: 1 }
|
||||
];
|
||||
const mockResult = {
|
||||
count: 2,
|
||||
rows: mockOrders
|
||||
};
|
||||
|
||||
test('客户端用户应该只能查看自己的订单', async () => {
|
||||
Order.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await OrderService.getOrderList(mockQuery, mockUser);
|
||||
|
||||
expect(Order.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: {
|
||||
buyer_id: mockUser.id,
|
||||
status: mockQuery.status,
|
||||
order_no: mockQuery.orderNo
|
||||
},
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
expect(result).toEqual({
|
||||
orders: mockOrders,
|
||||
count: 2,
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
});
|
||||
|
||||
test('贸易商用户应该只能查看自己的订单', async () => {
|
||||
const traderUser = { id: 2, userType: 'trader' };
|
||||
Order.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
await OrderService.getOrderList(mockQuery, traderUser);
|
||||
|
||||
expect(Order.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: {
|
||||
trader_id: traderUser.id,
|
||||
status: mockQuery.status,
|
||||
order_no: mockQuery.orderNo
|
||||
},
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
test('供应商用户应该只能查看自己的订单', async () => {
|
||||
const supplierUser = { id: 3, userType: 'supplier' };
|
||||
Order.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
await OrderService.getOrderList(mockQuery, supplierUser);
|
||||
|
||||
expect(Order.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: {
|
||||
supplier_id: supplierUser.id,
|
||||
status: mockQuery.status,
|
||||
order_no: mockQuery.orderNo
|
||||
},
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
test('司机用户应该只能查看自己的订单', async () => {
|
||||
const driverUser = { id: 4, userType: 'driver' };
|
||||
Order.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
await OrderService.getOrderList(mockQuery, driverUser);
|
||||
|
||||
expect(Order.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: {
|
||||
driver_id: driverUser.id,
|
||||
status: mockQuery.status,
|
||||
order_no: mockQuery.orderNo
|
||||
},
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
test('应该正确处理分页参数', async () => {
|
||||
const paginationQuery = { page: 2, pageSize: 20 };
|
||||
Order.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
await OrderService.getOrderList(paginationQuery, mockUser);
|
||||
|
||||
expect(Order.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: { buyer_id: mockUser.id },
|
||||
limit: 20,
|
||||
offset: 20,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
test('应该使用默认分页参数', async () => {
|
||||
Order.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
await OrderService.getOrderList({}, mockUser);
|
||||
|
||||
expect(Order.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: { buyer_id: mockUser.id },
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrderDetail', () => {
|
||||
const mockOrderId = 1;
|
||||
const mockOrder = {
|
||||
id: 1,
|
||||
order_no: 'ORD123',
|
||||
buyer_id: 1,
|
||||
trader_id: 2,
|
||||
supplier_id: 3,
|
||||
driver_id: 4
|
||||
};
|
||||
|
||||
test('应该返回订单详情', async () => {
|
||||
const mockUser = { id: 1, userType: 'client' };
|
||||
Order.findByPk.mockResolvedValue(mockOrder);
|
||||
|
||||
const result = await OrderService.getOrderDetail(mockOrderId, mockUser);
|
||||
|
||||
expect(Order.findByPk).toHaveBeenCalledWith(mockOrderId);
|
||||
expect(result).toEqual(mockOrder);
|
||||
});
|
||||
|
||||
test('订单不存在时应该抛出异常', async () => {
|
||||
const mockUser = { id: 1, userType: 'client' };
|
||||
Order.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(OrderService.getOrderDetail(mockOrderId, mockUser))
|
||||
.rejects.toThrow('订单不存在');
|
||||
});
|
||||
|
||||
test('客户端用户无权限访问其他用户订单时应该抛出异常', async () => {
|
||||
const mockUser = { id: 999, userType: 'client' };
|
||||
Order.findByPk.mockResolvedValue(mockOrder);
|
||||
|
||||
await expect(OrderService.getOrderDetail(mockOrderId, mockUser))
|
||||
.rejects.toThrow('无权限访问该订单');
|
||||
});
|
||||
|
||||
test('贸易商用户无权限访问其他用户订单时应该抛出异常', async () => {
|
||||
const mockUser = { id: 999, userType: 'trader' };
|
||||
Order.findByPk.mockResolvedValue(mockOrder);
|
||||
|
||||
await expect(OrderService.getOrderDetail(mockOrderId, mockUser))
|
||||
.rejects.toThrow('无权限访问该订单');
|
||||
});
|
||||
|
||||
test('供应商用户无权限访问其他用户订单时应该抛出异常', async () => {
|
||||
const mockUser = { id: 999, userType: 'supplier' };
|
||||
Order.findByPk.mockResolvedValue(mockOrder);
|
||||
|
||||
await expect(OrderService.getOrderDetail(mockOrderId, mockUser))
|
||||
.rejects.toThrow('无权限访问该订单');
|
||||
});
|
||||
|
||||
test('司机用户无权限访问其他用户订单时应该抛出异常', async () => {
|
||||
const mockUser = { id: 999, userType: 'driver' };
|
||||
Order.findByPk.mockResolvedValue(mockOrder);
|
||||
|
||||
await expect(OrderService.getOrderDetail(mockOrderId, mockUser))
|
||||
.rejects.toThrow('无权限访问该订单');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOrderStatus', () => {
|
||||
const mockOrderId = 1;
|
||||
const mockStatus = 'confirmed';
|
||||
const mockUser = { id: 1, userType: 'client' };
|
||||
const mockOrder = {
|
||||
id: 1,
|
||||
order_no: 'ORD123',
|
||||
buyer_id: 1,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
test('应该成功更新订单状态', async () => {
|
||||
Order.findByPk.mockResolvedValue(mockOrder);
|
||||
Order.update.mockResolvedValue([1]); // 返回更新的行数
|
||||
|
||||
const result = await OrderService.updateOrderStatus(mockOrderId, mockStatus, mockUser);
|
||||
|
||||
expect(Order.findByPk).toHaveBeenCalledWith(mockOrderId);
|
||||
expect(Order.update).toHaveBeenCalledWith(
|
||||
{ status: mockStatus },
|
||||
{ where: { id: mockOrderId } }
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('订单不存在时应该抛出异常', async () => {
|
||||
Order.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(OrderService.updateOrderStatus(mockOrderId, mockStatus, mockUser))
|
||||
.rejects.toThrow('订单不存在');
|
||||
});
|
||||
|
||||
test('更新失败时应该抛出异常', async () => {
|
||||
Order.findByPk.mockResolvedValue(mockOrder);
|
||||
Order.update.mockResolvedValue([0]); // 返回0表示没有更新任何行
|
||||
|
||||
await expect(OrderService.updateOrderStatus(mockOrderId, mockStatus, mockUser))
|
||||
.rejects.toThrow('订单状态更新失败');
|
||||
});
|
||||
|
||||
test('数据库更新操作失败时应该抛出异常', async () => {
|
||||
Order.findByPk.mockResolvedValue(mockOrder);
|
||||
Order.update.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(OrderService.updateOrderStatus(mockOrderId, mockStatus, mockUser))
|
||||
.rejects.toThrow('数据库错误');
|
||||
});
|
||||
});
|
||||
});
|
||||
323
backend/tests/unit/services/PaymentService.test.js
Normal file
323
backend/tests/unit/services/PaymentService.test.js
Normal file
@@ -0,0 +1,323 @@
|
||||
const PaymentService = require('../../../src/services/PaymentService');
|
||||
const Payment = require('../../../src/models/Payment');
|
||||
|
||||
// Mock依赖
|
||||
jest.mock('../../../src/models/Payment');
|
||||
|
||||
describe('PaymentService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createPayment', () => {
|
||||
const mockUserId = 1;
|
||||
const mockPaymentData = {
|
||||
orderId: 1,
|
||||
amount: 100.00,
|
||||
paymentType: 'order',
|
||||
paymentMethod: 'wechat'
|
||||
};
|
||||
const mockCreatedPayment = {
|
||||
id: 1,
|
||||
order_id: 1,
|
||||
user_id: mockUserId,
|
||||
amount: 100.00,
|
||||
payment_type: 'order',
|
||||
payment_method: 'wechat',
|
||||
payment_no: 'PAY1234567890123123',
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Date.now() 和 Math.random()
|
||||
jest.spyOn(Date, 'now').mockReturnValue(1234567890123);
|
||||
jest.spyOn(Math, 'random').mockReturnValue(0.123);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date.now.mockRestore();
|
||||
Math.random.mockRestore();
|
||||
});
|
||||
|
||||
test('应该成功创建支付记录', async () => {
|
||||
Payment.create.mockResolvedValue(mockCreatedPayment);
|
||||
|
||||
const result = await PaymentService.createPayment(mockPaymentData, mockUserId);
|
||||
|
||||
expect(Payment.create).toHaveBeenCalledWith({
|
||||
order_id: mockPaymentData.orderId,
|
||||
user_id: mockUserId,
|
||||
amount: mockPaymentData.amount,
|
||||
payment_type: mockPaymentData.paymentType,
|
||||
payment_method: mockPaymentData.paymentMethod,
|
||||
payment_no: 'PAY1234567890123123',
|
||||
status: 'pending'
|
||||
});
|
||||
expect(result).toEqual({
|
||||
payment: mockCreatedPayment,
|
||||
paymentParams: {
|
||||
paymentNo: 'PAY1234567890123123',
|
||||
amount: mockPaymentData.amount,
|
||||
paymentType: mockPaymentData.paymentType,
|
||||
paymentMethod: mockPaymentData.paymentMethod
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('应该生成唯一的支付单号', async () => {
|
||||
Payment.create.mockResolvedValue(mockCreatedPayment);
|
||||
|
||||
await PaymentService.createPayment(mockPaymentData, mockUserId);
|
||||
|
||||
const expectedPaymentNo = 'PAY1234567890123123';
|
||||
expect(Payment.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
payment_no: expectedPaymentNo
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('数据库创建失败时应该抛出异常', async () => {
|
||||
Payment.create.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(PaymentService.createPayment(mockPaymentData, mockUserId))
|
||||
.rejects.toThrow('数据库错误');
|
||||
});
|
||||
|
||||
test('应该设置初始状态为pending', async () => {
|
||||
Payment.create.mockResolvedValue(mockCreatedPayment);
|
||||
|
||||
await PaymentService.createPayment(mockPaymentData, mockUserId);
|
||||
|
||||
expect(Payment.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'pending'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPaymentList', () => {
|
||||
const mockUserId = 1;
|
||||
const mockQuery = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
status: 'paid'
|
||||
};
|
||||
const mockPayments = [
|
||||
{ id: 1, payment_no: 'PAY123', user_id: 1, status: 'paid' },
|
||||
{ id: 2, payment_no: 'PAY124', user_id: 1, status: 'paid' }
|
||||
];
|
||||
const mockResult = {
|
||||
count: 2,
|
||||
rows: mockPayments
|
||||
};
|
||||
|
||||
test('应该返回用户的支付列表', async () => {
|
||||
Payment.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await PaymentService.getPaymentList(mockQuery, mockUserId);
|
||||
|
||||
expect(Payment.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: {
|
||||
user_id: mockUserId,
|
||||
status: mockQuery.status
|
||||
},
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
expect(result).toEqual({
|
||||
payments: mockPayments,
|
||||
count: 2,
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
});
|
||||
|
||||
test('应该正确处理分页参数', async () => {
|
||||
const paginationQuery = { page: 2, pageSize: 20 };
|
||||
Payment.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
await PaymentService.getPaymentList(paginationQuery, mockUserId);
|
||||
|
||||
expect(Payment.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: { user_id: mockUserId },
|
||||
limit: 20,
|
||||
offset: 20,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
test('应该使用默认分页参数', async () => {
|
||||
Payment.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
await PaymentService.getPaymentList({}, mockUserId);
|
||||
|
||||
expect(Payment.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: { user_id: mockUserId },
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
test('不指定状态时应该查询所有状态的支付记录', async () => {
|
||||
const queryWithoutStatus = { page: 1, pageSize: 10 };
|
||||
Payment.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
await PaymentService.getPaymentList(queryWithoutStatus, mockUserId);
|
||||
|
||||
expect(Payment.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: { user_id: mockUserId },
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
});
|
||||
|
||||
test('数据库查询失败时应该抛出异常', async () => {
|
||||
Payment.findAndCountAll.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(PaymentService.getPaymentList(mockQuery, mockUserId))
|
||||
.rejects.toThrow('数据库错误');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPaymentDetail', () => {
|
||||
const mockPaymentId = 1;
|
||||
const mockUserId = 1;
|
||||
const mockPayment = {
|
||||
id: 1,
|
||||
payment_no: 'PAY123',
|
||||
user_id: 1,
|
||||
amount: 100.00,
|
||||
status: 'paid'
|
||||
};
|
||||
|
||||
test('应该返回支付详情', async () => {
|
||||
Payment.findByPk.mockResolvedValue(mockPayment);
|
||||
|
||||
const result = await PaymentService.getPaymentDetail(mockPaymentId, mockUserId);
|
||||
|
||||
expect(Payment.findByPk).toHaveBeenCalledWith(mockPaymentId);
|
||||
expect(result).toEqual(mockPayment);
|
||||
});
|
||||
|
||||
test('支付记录不存在时应该抛出异常', async () => {
|
||||
Payment.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(PaymentService.getPaymentDetail(mockPaymentId, mockUserId))
|
||||
.rejects.toThrow('支付记录不存在');
|
||||
});
|
||||
|
||||
test('用户无权限访问其他用户的支付记录时应该抛出异常', async () => {
|
||||
const otherUserPayment = { ...mockPayment, user_id: 999 };
|
||||
Payment.findByPk.mockResolvedValue(otherUserPayment);
|
||||
|
||||
await expect(PaymentService.getPaymentDetail(mockPaymentId, mockUserId))
|
||||
.rejects.toThrow('无权限访问该支付记录');
|
||||
});
|
||||
|
||||
test('数据库查询失败时应该抛出异常', async () => {
|
||||
Payment.findByPk.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(PaymentService.getPaymentDetail(mockPaymentId, mockUserId))
|
||||
.rejects.toThrow('数据库错误');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePaymentStatus', () => {
|
||||
const mockPaymentNo = 'PAY123456789';
|
||||
const mockStatus = 'paid';
|
||||
const mockThirdPartyId = 'wx_123456789';
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Date constructor
|
||||
jest.spyOn(global, 'Date').mockImplementation(() => ({
|
||||
toISOString: () => '2024-01-01T00:00:00.000Z'
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.Date.mockRestore();
|
||||
});
|
||||
|
||||
test('应该成功更新支付状态为已支付', async () => {
|
||||
Payment.update.mockResolvedValue([1]); // 返回更新的行数
|
||||
|
||||
const result = await PaymentService.updatePaymentStatus(mockPaymentNo, mockStatus, mockThirdPartyId);
|
||||
|
||||
expect(Payment.update).toHaveBeenCalledWith(
|
||||
{
|
||||
status: mockStatus,
|
||||
third_party_id: mockThirdPartyId,
|
||||
paid_time: expect.any(Object)
|
||||
},
|
||||
{
|
||||
where: { payment_no: mockPaymentNo }
|
||||
}
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('应该成功更新支付状态为失败', async () => {
|
||||
const failedStatus = 'failed';
|
||||
Payment.update.mockResolvedValue([1]);
|
||||
|
||||
const result = await PaymentService.updatePaymentStatus(mockPaymentNo, failedStatus, mockThirdPartyId);
|
||||
|
||||
expect(Payment.update).toHaveBeenCalledWith(
|
||||
{
|
||||
status: failedStatus,
|
||||
third_party_id: mockThirdPartyId,
|
||||
paid_time: null
|
||||
},
|
||||
{
|
||||
where: { payment_no: mockPaymentNo }
|
||||
}
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('支付记录不存在时应该抛出异常', async () => {
|
||||
Payment.update.mockResolvedValue([0]); // 返回0表示没有更新任何行
|
||||
|
||||
await expect(PaymentService.updatePaymentStatus(mockPaymentNo, mockStatus, mockThirdPartyId))
|
||||
.rejects.toThrow('支付记录不存在');
|
||||
});
|
||||
|
||||
test('数据库更新操作失败时应该抛出异常', async () => {
|
||||
Payment.update.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(PaymentService.updatePaymentStatus(mockPaymentNo, mockStatus, mockThirdPartyId))
|
||||
.rejects.toThrow('数据库错误');
|
||||
});
|
||||
|
||||
test('只有支付成功时才设置支付时间', async () => {
|
||||
Payment.update.mockResolvedValue([1]);
|
||||
|
||||
await PaymentService.updatePaymentStatus(mockPaymentNo, 'paid', mockThirdPartyId);
|
||||
|
||||
expect(Payment.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
paid_time: expect.any(Object)
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
test('支付失败时不设置支付时间', async () => {
|
||||
Payment.update.mockResolvedValue([1]);
|
||||
|
||||
await PaymentService.updatePaymentStatus(mockPaymentNo, 'failed', mockThirdPartyId);
|
||||
|
||||
expect(Payment.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
paid_time: null
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
423
backend/tests/unit/services/TransportService.test.js
Normal file
423
backend/tests/unit/services/TransportService.test.js
Normal file
@@ -0,0 +1,423 @@
|
||||
const TransportService = require('../../../src/services/TransportService');
|
||||
const { Transport, TransportTrack, Driver, Vehicle, Order } = require('../../../src/models');
|
||||
|
||||
// Mock 模型
|
||||
jest.mock('../../../src/models', () => ({
|
||||
Transport: {
|
||||
create: jest.fn(),
|
||||
findByPk: jest.fn(),
|
||||
findAndCountAll: jest.fn(),
|
||||
},
|
||||
TransportTrack: {
|
||||
create: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
},
|
||||
Driver: {
|
||||
findByPk: jest.fn(),
|
||||
},
|
||||
Vehicle: {
|
||||
findByPk: jest.fn(),
|
||||
},
|
||||
Order: {},
|
||||
}));
|
||||
|
||||
describe('TransportService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createTransport', () => {
|
||||
it('应该成功创建运输任务', async () => {
|
||||
const transportData = {
|
||||
order_id: 1,
|
||||
pickup_address: '北京市朝阳区',
|
||||
delivery_address: '上海市浦东新区',
|
||||
expected_weight: 1000,
|
||||
};
|
||||
|
||||
const mockTransport = { id: 1, ...transportData, transport_no: 'T202401010001' };
|
||||
|
||||
// Mock generateTransportNo 方法
|
||||
jest.spyOn(TransportService, 'generateTransportNo').mockResolvedValue('T202401010001');
|
||||
Transport.create.mockResolvedValue(mockTransport);
|
||||
TransportTrack.create.mockResolvedValue({ id: 1 });
|
||||
|
||||
const result = await TransportService.createTransport(transportData);
|
||||
|
||||
expect(Transport.create).toHaveBeenCalledWith({
|
||||
...transportData,
|
||||
transport_no: 'T202401010001',
|
||||
status: 'pending',
|
||||
start_time: null,
|
||||
end_time: null,
|
||||
});
|
||||
expect(TransportTrack.create).toHaveBeenCalledWith({
|
||||
transport_id: 1,
|
||||
status: 'pending',
|
||||
location: '待发车',
|
||||
description: '运输任务已创建,等待司机接单',
|
||||
recorded_at: expect.any(Date),
|
||||
});
|
||||
expect(result).toEqual(mockTransport);
|
||||
});
|
||||
|
||||
it('数据库创建失败时应该抛出异常', async () => {
|
||||
const transportData = { order_id: 1 };
|
||||
|
||||
jest.spyOn(TransportService, 'generateTransportNo').mockResolvedValue('T202401010001');
|
||||
Transport.create.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(TransportService.createTransport(transportData))
|
||||
.rejects.toThrow('创建运输任务失败: 数据库错误');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTransportList', () => {
|
||||
it('应该返回运输任务列表', async () => {
|
||||
const query = { page: 1, pageSize: 10 };
|
||||
const mockResult = {
|
||||
count: 2,
|
||||
rows: [
|
||||
{ id: 1, transport_no: 'T202401010001' },
|
||||
{ id: 2, transport_no: 'T202401010002' },
|
||||
],
|
||||
};
|
||||
|
||||
Transport.findAndCountAll.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await TransportService.getTransportList(query);
|
||||
|
||||
expect(Transport.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: {},
|
||||
include: expect.any(Array),
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']],
|
||||
});
|
||||
expect(result).toEqual({
|
||||
transports: mockResult.rows,
|
||||
total: 2,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPages: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('应该正确处理查询条件', async () => {
|
||||
const query = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
status: 'pending',
|
||||
driverId: 1,
|
||||
vehicleId: 2,
|
||||
orderId: 3,
|
||||
keyword: 'test',
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-01-31',
|
||||
};
|
||||
|
||||
Transport.findAndCountAll.mockResolvedValue({ count: 0, rows: [] });
|
||||
|
||||
await TransportService.getTransportList(query);
|
||||
|
||||
expect(Transport.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: {
|
||||
status: 'pending',
|
||||
driver_id: 1,
|
||||
vehicle_id: 2,
|
||||
order_id: 3,
|
||||
[require('sequelize').Op.or]: [
|
||||
{ transport_no: { [require('sequelize').Op.like]: '%test%' } },
|
||||
{ pickup_address: { [require('sequelize').Op.like]: '%test%' } },
|
||||
{ delivery_address: { [require('sequelize').Op.like]: '%test%' } },
|
||||
],
|
||||
created_at: {
|
||||
[require('sequelize').Op.between]: [
|
||||
new Date('2024-01-01'),
|
||||
new Date('2024-01-31'),
|
||||
],
|
||||
},
|
||||
},
|
||||
include: expect.any(Array),
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']],
|
||||
});
|
||||
});
|
||||
|
||||
it('数据库查询失败时应该抛出异常', async () => {
|
||||
Transport.findAndCountAll.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(TransportService.getTransportList({}))
|
||||
.rejects.toThrow('获取运输任务列表失败: 数据库错误');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTransportDetail', () => {
|
||||
it('应该返回运输任务详情', async () => {
|
||||
const mockTransport = {
|
||||
id: 1,
|
||||
transport_no: 'T202401010001',
|
||||
status: 'pending',
|
||||
};
|
||||
|
||||
Transport.findByPk.mockResolvedValue(mockTransport);
|
||||
|
||||
const result = await TransportService.getTransportDetail(1);
|
||||
|
||||
expect(Transport.findByPk).toHaveBeenCalledWith(1, {
|
||||
include: expect.any(Array),
|
||||
});
|
||||
expect(result).toEqual(mockTransport);
|
||||
});
|
||||
|
||||
it('运输任务不存在时应该抛出异常', async () => {
|
||||
Transport.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(TransportService.getTransportDetail(999))
|
||||
.rejects.toThrow('获取运输任务详情失败: 运输任务不存在');
|
||||
});
|
||||
|
||||
it('数据库查询失败时应该抛出异常', async () => {
|
||||
Transport.findByPk.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(TransportService.getTransportDetail(1))
|
||||
.rejects.toThrow('获取运输任务详情失败: 数据库错误');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTransportStatus', () => {
|
||||
it('应该成功更新运输任务状态', async () => {
|
||||
const mockTransport = {
|
||||
id: 1,
|
||||
status: 'pending',
|
||||
update: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
Transport.findByPk.mockResolvedValue(mockTransport);
|
||||
jest.spyOn(TransportService, 'createTrackRecord').mockResolvedValue({});
|
||||
|
||||
const result = await TransportService.updateTransportStatus(1, 'assigned', {
|
||||
location: '调度中心',
|
||||
description: '已分配司机',
|
||||
});
|
||||
|
||||
expect(mockTransport.update).toHaveBeenCalledWith({
|
||||
status: 'assigned',
|
||||
location: '调度中心',
|
||||
description: '已分配司机',
|
||||
});
|
||||
expect(TransportService.createTrackRecord).toHaveBeenCalledWith(
|
||||
1,
|
||||
'assigned',
|
||||
'调度中心',
|
||||
'已分配司机'
|
||||
);
|
||||
expect(result).toEqual(mockTransport);
|
||||
});
|
||||
|
||||
it('运输任务不存在时应该抛出异常', async () => {
|
||||
Transport.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(TransportService.updateTransportStatus(999, 'assigned'))
|
||||
.rejects.toThrow('更新运输任务状态失败: 运输任务不存在');
|
||||
});
|
||||
|
||||
it('无效状态转换时应该抛出异常', async () => {
|
||||
const mockTransport = {
|
||||
id: 1,
|
||||
status: 'completed',
|
||||
};
|
||||
|
||||
Transport.findByPk.mockResolvedValue(mockTransport);
|
||||
|
||||
await expect(TransportService.updateTransportStatus(1, 'pending'))
|
||||
.rejects.toThrow('更新运输任务状态失败: 无法从状态 completed 转换到 pending');
|
||||
});
|
||||
|
||||
it('状态为in_transit时应该设置开始时间', async () => {
|
||||
const mockTransport = {
|
||||
id: 1,
|
||||
status: 'assigned',
|
||||
start_time: null,
|
||||
update: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
Transport.findByPk.mockResolvedValue(mockTransport);
|
||||
jest.spyOn(TransportService, 'createTrackRecord').mockResolvedValue({});
|
||||
|
||||
await TransportService.updateTransportStatus(1, 'in_transit');
|
||||
|
||||
expect(mockTransport.update).toHaveBeenCalledWith({
|
||||
status: 'in_transit',
|
||||
start_time: expect.any(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('状态为completed时应该设置结束时间', async () => {
|
||||
const mockTransport = {
|
||||
id: 1,
|
||||
status: 'delivered',
|
||||
update: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
Transport.findByPk.mockResolvedValue(mockTransport);
|
||||
jest.spyOn(TransportService, 'createTrackRecord').mockResolvedValue({});
|
||||
|
||||
await TransportService.updateTransportStatus(1, 'completed');
|
||||
|
||||
expect(mockTransport.update).toHaveBeenCalledWith({
|
||||
status: 'completed',
|
||||
end_time: expect.any(Date),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignDriverAndVehicle', () => {
|
||||
it('应该成功分配司机和车辆', async () => {
|
||||
const mockTransport = {
|
||||
id: 1,
|
||||
status: 'pending',
|
||||
update: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
const mockDriver = {
|
||||
id: 1,
|
||||
name: '张三',
|
||||
status: 'available',
|
||||
update: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
const mockVehicle = {
|
||||
id: 1,
|
||||
plate_number: '京A12345',
|
||||
status: 'available',
|
||||
update: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
Transport.findByPk.mockResolvedValue(mockTransport);
|
||||
Driver.findByPk.mockResolvedValue(mockDriver);
|
||||
Vehicle.findByPk.mockResolvedValue(mockVehicle);
|
||||
jest.spyOn(TransportService, 'createTrackRecord').mockResolvedValue({});
|
||||
|
||||
const result = await TransportService.assignDriverAndVehicle(1, 1, 1);
|
||||
|
||||
expect(mockTransport.update).toHaveBeenCalledWith({
|
||||
driver_id: 1,
|
||||
vehicle_id: 1,
|
||||
status: 'assigned',
|
||||
});
|
||||
expect(mockDriver.update).toHaveBeenCalledWith({ status: 'busy' });
|
||||
expect(mockVehicle.update).toHaveBeenCalledWith({ status: 'in_use' });
|
||||
expect(result).toEqual(mockTransport);
|
||||
});
|
||||
|
||||
it('运输任务不存在时应该抛出异常', async () => {
|
||||
Transport.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(TransportService.assignDriverAndVehicle(999, 1, 1))
|
||||
.rejects.toThrow('分配司机和车辆失败: 运输任务不存在');
|
||||
});
|
||||
|
||||
it('运输任务状态不是pending时应该抛出异常', async () => {
|
||||
const mockTransport = { id: 1, status: 'assigned' };
|
||||
Transport.findByPk.mockResolvedValue(mockTransport);
|
||||
|
||||
await expect(TransportService.assignDriverAndVehicle(1, 1, 1))
|
||||
.rejects.toThrow('分配司机和车辆失败: 只能为待分配的运输任务分配司机和车辆');
|
||||
});
|
||||
|
||||
it('司机不存在或不可用时应该抛出异常', async () => {
|
||||
const mockTransport = { id: 1, status: 'pending' };
|
||||
Transport.findByPk.mockResolvedValue(mockTransport);
|
||||
Driver.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(TransportService.assignDriverAndVehicle(1, 999, 1))
|
||||
.rejects.toThrow('分配司机和车辆失败: 司机不存在或不可用');
|
||||
});
|
||||
|
||||
it('车辆不存在或不可用时应该抛出异常', async () => {
|
||||
const mockTransport = { id: 1, status: 'pending' };
|
||||
const mockDriver = { id: 1, status: 'available' };
|
||||
|
||||
Transport.findByPk.mockResolvedValue(mockTransport);
|
||||
Driver.findByPk.mockResolvedValue(mockDriver);
|
||||
Vehicle.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(TransportService.assignDriverAndVehicle(1, 1, 999))
|
||||
.rejects.toThrow('分配司机和车辆失败: 车辆不存在或不可用');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTrackRecord', () => {
|
||||
beforeEach(() => {
|
||||
// 恢复 createTrackRecord 方法的原始实现
|
||||
if (TransportService.createTrackRecord.mockRestore) {
|
||||
TransportService.createTrackRecord.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('应该成功创建跟踪记录', async () => {
|
||||
const mockTrack = {
|
||||
id: 1,
|
||||
transport_id: 1,
|
||||
status: 'pending',
|
||||
location: '北京',
|
||||
description: '测试描述',
|
||||
};
|
||||
|
||||
TransportTrack.create.mockResolvedValue(mockTrack);
|
||||
|
||||
const result = await TransportService.createTrackRecord(1, 'pending', '北京', '测试描述');
|
||||
|
||||
expect(TransportTrack.create).toHaveBeenCalledWith({
|
||||
transport_id: 1,
|
||||
status: 'pending',
|
||||
location: '北京',
|
||||
description: '测试描述',
|
||||
recorded_at: expect.any(Date),
|
||||
});
|
||||
expect(result).toEqual(mockTrack);
|
||||
});
|
||||
|
||||
it('数据库创建失败时应该抛出异常', async () => {
|
||||
TransportTrack.create.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(TransportService.createTrackRecord(1, 'pending'))
|
||||
.rejects.toThrow('创建跟踪记录失败: 数据库错误');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTransportTracks', () => {
|
||||
it('应该返回运输跟踪记录列表', async () => {
|
||||
const mockTracks = [
|
||||
{ id: 1, status: 'pending', recorded_at: new Date() },
|
||||
{ id: 2, status: 'assigned', recorded_at: new Date() },
|
||||
];
|
||||
|
||||
TransportTrack.findAll.mockResolvedValue(mockTracks);
|
||||
|
||||
const result = await TransportService.getTransportTracks(1);
|
||||
|
||||
expect(TransportTrack.findAll).toHaveBeenCalledWith({
|
||||
where: { transport_id: 1 },
|
||||
order: [['recorded_at', 'DESC']],
|
||||
});
|
||||
expect(result).toEqual(mockTracks);
|
||||
});
|
||||
|
||||
it('数据库查询失败时应该抛出异常', async () => {
|
||||
TransportTrack.findAll.mockRejectedValue(new Error('数据库错误'));
|
||||
|
||||
await expect(TransportService.getTransportTracks(1))
|
||||
.rejects.toThrow('获取运输跟踪记录失败: 数据库错误');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTransportNo', () => {
|
||||
it('应该生成唯一的运输单号', async () => {
|
||||
// 由于这个方法的实现可能涉及日期和随机数,我们只测试它返回字符串
|
||||
const result = await TransportService.generateTransportNo();
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toMatch(/^T\d{12}$/); // 格式:T + 12位数字
|
||||
});
|
||||
});
|
||||
});
|
||||
150
backend/tests/unit/services/UserService.test.js
Normal file
150
backend/tests/unit/services/UserService.test.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const UserService = require('../../../src/services/UserService');
|
||||
const User = require('../../../src/models/User');
|
||||
|
||||
// Mock User模型
|
||||
jest.mock('../../../src/models/User');
|
||||
|
||||
describe('UserService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getUserList', () => {
|
||||
it('应该成功获取用户列表', async () => {
|
||||
const mockUsers = [
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'uuid-1',
|
||||
username: 'user1',
|
||||
real_name: '用户1',
|
||||
phone: '13800138001',
|
||||
email: 'user1@example.com',
|
||||
user_type: 'customer',
|
||||
status: 'active',
|
||||
avatar_url: 'avatar1.jpg',
|
||||
created_at: new Date()
|
||||
}
|
||||
];
|
||||
|
||||
User.findAndCountAll.mockResolvedValue({
|
||||
count: 1,
|
||||
rows: mockUsers
|
||||
});
|
||||
|
||||
const result = await UserService.getUserList({ page: 1, pageSize: 10 });
|
||||
|
||||
expect(User.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: {},
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
users: [{
|
||||
id: 1,
|
||||
uuid: 'uuid-1',
|
||||
username: 'user1',
|
||||
realName: '用户1',
|
||||
phone: '13800138001',
|
||||
email: 'user1@example.com',
|
||||
userType: 'customer',
|
||||
status: 'active',
|
||||
avatar: 'avatar1.jpg',
|
||||
createdAt: mockUsers[0].created_at
|
||||
}],
|
||||
count: 1,
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('应该根据条件过滤用户列表', async () => {
|
||||
User.findAndCountAll.mockResolvedValue({
|
||||
count: 0,
|
||||
rows: []
|
||||
});
|
||||
|
||||
await UserService.getUserList({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
userType: 'admin',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
expect(User.findAndCountAll).toHaveBeenCalledWith({
|
||||
where: { user_type: 'admin', status: 'active' },
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserDetail', () => {
|
||||
it('应该成功获取用户详情', async () => {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
uuid: 'uuid-1',
|
||||
username: 'user1',
|
||||
real_name: '用户1',
|
||||
phone: '13800138001',
|
||||
email: 'user1@example.com',
|
||||
user_type: 'customer',
|
||||
status: 'active',
|
||||
avatar_url: 'avatar1.jpg',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await UserService.getUserDetail(1);
|
||||
|
||||
expect(User.findByPk).toHaveBeenCalledWith(1);
|
||||
expect(result.id).toBe(1);
|
||||
expect(result.username).toBe('user1');
|
||||
});
|
||||
|
||||
it('用户不存在时应该抛出错误', async () => {
|
||||
User.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(UserService.getUserDetail(999)).rejects.toThrow('用户不存在');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('应该成功更新用户信息', async () => {
|
||||
User.update.mockResolvedValue([1]);
|
||||
|
||||
const updateData = { real_name: '新用户名' };
|
||||
const result = await UserService.updateUser(1, updateData);
|
||||
|
||||
expect(User.update).toHaveBeenCalledWith(updateData, { where: { id: 1 } });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('用户不存在时应该抛出错误', async () => {
|
||||
User.update.mockResolvedValue([0]);
|
||||
|
||||
await expect(UserService.updateUser(999, {})).rejects.toThrow('用户不存在');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUserStatus', () => {
|
||||
it('应该成功更新用户状态', async () => {
|
||||
User.update.mockResolvedValue([1]);
|
||||
|
||||
const result = await UserService.updateUserStatus(1, 'inactive');
|
||||
|
||||
expect(User.update).toHaveBeenCalledWith({ status: 'inactive' }, { where: { id: 1 } });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('用户不存在时应该抛出错误', async () => {
|
||||
User.update.mockResolvedValue([0]);
|
||||
|
||||
await expect(UserService.updateUserStatus(999, 'inactive')).rejects.toThrow('用户不存在');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user