refactor(docs): 简化README结构,更新技术栈和项目结构描述

This commit is contained in:
2025-09-20 15:19:59 +08:00
parent cec08f89e2
commit b8c9e5c959
54 changed files with 14343 additions and 6124 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

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

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

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

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

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

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

View File

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

View File

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

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

View File

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

View File

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

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

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

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

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

View File

@@ -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('用户不存在');
}

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

View 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
View 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(),
// };

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

View 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();
});
});
});

View 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');
});
});
});

View 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('数据库错误');
});
});
});

View 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)
);
});
});
});

View 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位数字
});
});
});

View 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('用户不存在');
});
});
});