添加银行端后端接口
This commit is contained in:
93
government-backend/models/Material.js
Normal file
93
government-backend/models/Material.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const Material = sequelize.define('Material', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '物资编号'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '物资名称'
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '物资类别',
|
||||
validate: {
|
||||
isIn: [['feed', 'medicine', 'equipment', 'other']]
|
||||
}
|
||||
},
|
||||
unit: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '单位'
|
||||
},
|
||||
stockQuantity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '库存数量'
|
||||
},
|
||||
warningQuantity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '预警数量'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'normal',
|
||||
comment: '状态',
|
||||
validate: {
|
||||
isIn: [['normal', 'low', 'out']]
|
||||
}
|
||||
},
|
||||
supplier: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '供应商'
|
||||
},
|
||||
remark: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注'
|
||||
},
|
||||
updateTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '更新时间'
|
||||
}
|
||||
}, {
|
||||
tableName: 'materials',
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
underscored: true,
|
||||
freezeTableName: true
|
||||
});
|
||||
|
||||
// 钩子函数,在保存前更新状态和更新时间
|
||||
Material.beforeSave((material) => {
|
||||
material.updateTime = new Date();
|
||||
|
||||
// 根据库存数量和预警数量更新状态
|
||||
if (material.stockQuantity <= 0) {
|
||||
material.status = 'out';
|
||||
} else if (material.stockQuantity <= material.warningQuantity) {
|
||||
material.status = 'low';
|
||||
} else {
|
||||
material.status = 'normal';
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Material;
|
||||
60
government-backend/models/WarehouseTransaction.js
Normal file
60
government-backend/models/WarehouseTransaction.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
const Material = require('./Material');
|
||||
|
||||
const WarehouseTransaction = sequelize.define('WarehouseTransaction', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
materialId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: Material,
|
||||
key: 'id'
|
||||
},
|
||||
comment: '物资ID'
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
validate: {
|
||||
isIn: [['in', 'out']]
|
||||
},
|
||||
comment: '操作类型:in(入库),out(出库)'
|
||||
},
|
||||
quantity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
validate: {
|
||||
min: 1
|
||||
},
|
||||
comment: '操作数量'
|
||||
},
|
||||
operator: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '操作人'
|
||||
},
|
||||
remark: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注'
|
||||
}
|
||||
}, {
|
||||
tableName: 'warehouse_transactions',
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
underscored: true,
|
||||
freezeTableName: true
|
||||
});
|
||||
|
||||
// 定义关系
|
||||
WarehouseTransaction.belongsTo(Material, {
|
||||
foreignKey: 'materialId',
|
||||
as: 'material'
|
||||
});
|
||||
|
||||
module.exports = WarehouseTransaction;
|
||||
@@ -1,12 +1,390 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Material = require('../models/Material');
|
||||
const WarehouseTransaction = require('../models/WarehouseTransaction');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// 仓库物资列表
|
||||
router.get('/', (req, res) => {
|
||||
res.json({
|
||||
code: 200,
|
||||
data: []
|
||||
});
|
||||
// 仓库物资列表(支持分页、搜索和筛选)
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
keyword = '',
|
||||
category = '',
|
||||
status = '',
|
||||
page = 1,
|
||||
pageSize = 10
|
||||
} = req.query;
|
||||
|
||||
const where = {};
|
||||
|
||||
// 搜索条件
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ code: { [Op.like]: `%${keyword}%` } },
|
||||
{ name: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
// 类别筛选
|
||||
if (category) {
|
||||
where.category = category;
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
const { count, rows } = await Material.findAndCountAll({
|
||||
where,
|
||||
offset,
|
||||
limit,
|
||||
order: [['update_time', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
data: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: limit,
|
||||
totalPages: Math.ceil(count / limit)
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取物资列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取单个物资详情
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const material = await Material.findByPk(id);
|
||||
|
||||
if (!material) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '物资不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
data: material
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取物资详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建新物资
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { code, name, category, unit, stockQuantity, warningQuantity, supplier, remark } = req.body;
|
||||
|
||||
// 检查物资编号是否已存在
|
||||
const existingMaterial = await Material.findOne({ where: { code } });
|
||||
if (existingMaterial) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '物资编号已存在'
|
||||
});
|
||||
}
|
||||
|
||||
const material = await Material.create({
|
||||
code,
|
||||
name,
|
||||
category,
|
||||
unit,
|
||||
stockQuantity,
|
||||
warningQuantity,
|
||||
supplier,
|
||||
remark
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: '创建物资成功',
|
||||
data: material
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '创建物资失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新物资信息
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { code, name, category, unit, stockQuantity, warningQuantity, supplier, remark } = req.body;
|
||||
|
||||
const material = await Material.findByPk(id);
|
||||
if (!material) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '物资不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查物资编号是否已存在(排除当前物资)
|
||||
if (code && code !== material.code) {
|
||||
const existingMaterial = await Material.findOne({ where: { code } });
|
||||
if (existingMaterial) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '物资编号已存在'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await material.update({
|
||||
code,
|
||||
name,
|
||||
category,
|
||||
unit,
|
||||
stockQuantity,
|
||||
warningQuantity,
|
||||
supplier,
|
||||
remark
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: '更新物资成功',
|
||||
data: material
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '更新物资失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 删除物资
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const material = await Material.findByPk(id);
|
||||
|
||||
if (!material) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '物资不存在'
|
||||
});
|
||||
}
|
||||
|
||||
await material.destroy();
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: '删除物资成功'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '删除物资失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 物资入库
|
||||
router.post('/in', async (req, res) => {
|
||||
try {
|
||||
const { materialId, quantity, operator, remark } = req.body;
|
||||
|
||||
// 开始事务
|
||||
const transaction = await Material.sequelize.transaction();
|
||||
|
||||
try {
|
||||
// 查找物资
|
||||
const material = await Material.findByPk(materialId, { transaction });
|
||||
if (!material) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '物资不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新库存
|
||||
material.stockQuantity += parseInt(quantity);
|
||||
await material.save({ transaction });
|
||||
|
||||
// 记录入库记录
|
||||
await WarehouseTransaction.create({
|
||||
materialId,
|
||||
type: 'in',
|
||||
quantity,
|
||||
operator,
|
||||
remark
|
||||
}, { transaction });
|
||||
|
||||
// 提交事务
|
||||
await transaction.commit();
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: '入库成功',
|
||||
data: {
|
||||
materialId,
|
||||
quantity,
|
||||
newStock: material.stockQuantity
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
// 回滚事务
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '入库失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 物资出库
|
||||
router.post('/out', async (req, res) => {
|
||||
try {
|
||||
const { materialId, quantity, operator, remark } = req.body;
|
||||
|
||||
// 开始事务
|
||||
const transaction = await Material.sequelize.transaction();
|
||||
|
||||
try {
|
||||
// 查找物资
|
||||
const material = await Material.findByPk(materialId, { transaction });
|
||||
if (!material) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: '物资不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查库存是否足够
|
||||
if (material.stockQuantity < parseInt(quantity)) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: '库存不足'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新库存
|
||||
material.stockQuantity -= parseInt(quantity);
|
||||
await material.save({ transaction });
|
||||
|
||||
// 记录出库记录
|
||||
await WarehouseTransaction.create({
|
||||
materialId,
|
||||
type: 'out',
|
||||
quantity,
|
||||
operator,
|
||||
remark
|
||||
}, { transaction });
|
||||
|
||||
// 提交事务
|
||||
await transaction.commit();
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: '出库成功',
|
||||
data: {
|
||||
materialId,
|
||||
quantity,
|
||||
newStock: material.stockQuantity
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
// 回滚事务
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '出库失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取库存统计信息
|
||||
router.get('/stats', async (req, res) => {
|
||||
try {
|
||||
// 统计总类别数
|
||||
const totalCategories = await Material.count({
|
||||
distinct: true,
|
||||
col: 'category'
|
||||
});
|
||||
|
||||
// 统计库存总量
|
||||
const totalQuantityResult = await Material.sum('stockQuantity');
|
||||
const totalQuantity = totalQuantityResult || 0;
|
||||
|
||||
// 统计低库存物资数
|
||||
const lowStockCount = await Material.count({
|
||||
where: {
|
||||
status: 'low'
|
||||
}
|
||||
});
|
||||
|
||||
// 统计缺货物资数
|
||||
const outOfStockCount = await Material.count({
|
||||
where: {
|
||||
status: 'out'
|
||||
}
|
||||
});
|
||||
|
||||
// 统计各类别物资数量
|
||||
const categoryStats = await Material.findAll({
|
||||
attributes: [
|
||||
'category',
|
||||
[Material.sequelize.fn('COUNT', Material.sequelize.col('id')), 'count'],
|
||||
[Material.sequelize.fn('SUM', Material.sequelize.col('stockQuantity')), 'totalQuantity']
|
||||
],
|
||||
group: ['category'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
data: {
|
||||
totalCategories,
|
||||
totalQuantity,
|
||||
lowStockCount,
|
||||
outOfStockCount,
|
||||
categoryStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取统计信息失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
58
government-backend/sql/warehouse.sql
Normal file
58
government-backend/sql/warehouse.sql
Normal file
@@ -0,0 +1,58 @@
|
||||
-- 创建物资表
|
||||
CREATE TABLE IF NOT EXISTS materials (
|
||||
id VARCHAR(36) NOT NULL PRIMARY KEY COMMENT '物资ID',
|
||||
code VARCHAR(20) NOT NULL UNIQUE COMMENT '物资编号',
|
||||
name VARCHAR(100) NOT NULL COMMENT '物资名称',
|
||||
category VARCHAR(20) NOT NULL COMMENT '物资类别',
|
||||
unit VARCHAR(20) NOT NULL COMMENT '单位',
|
||||
stock_quantity INT NOT NULL DEFAULT 0 COMMENT '库存数量',
|
||||
warning_quantity INT NOT NULL DEFAULT 0 COMMENT '预警数量',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'normal' COMMENT '状态',
|
||||
supplier VARCHAR(100) NULL COMMENT '供应商',
|
||||
remark TEXT NULL COMMENT '备注',
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间',
|
||||
created_at DATETIME NOT NULL COMMENT '创建时间',
|
||||
updated_at DATETIME NOT NULL COMMENT '更新时间',
|
||||
deleted_at DATETIME NULL COMMENT '删除时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物资表';
|
||||
|
||||
-- 创建仓库交易记录表
|
||||
CREATE TABLE IF NOT EXISTS warehouse_transactions (
|
||||
id VARCHAR(36) NOT NULL PRIMARY KEY COMMENT '交易记录ID',
|
||||
material_id VARCHAR(36) NOT NULL COMMENT '物资ID',
|
||||
type VARCHAR(10) NOT NULL COMMENT '操作类型:in(入库),out(出库)',
|
||||
quantity INT NOT NULL COMMENT '操作数量',
|
||||
operator VARCHAR(50) NOT NULL COMMENT '操作人',
|
||||
remark TEXT NULL COMMENT '备注',
|
||||
created_at DATETIME NOT NULL COMMENT '创建时间',
|
||||
updated_at DATETIME NOT NULL COMMENT '更新时间',
|
||||
deleted_at DATETIME NULL COMMENT '删除时间',
|
||||
INDEX idx_material_id (material_id),
|
||||
FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='仓库交易记录表';
|
||||
|
||||
-- 插入测试数据
|
||||
INSERT INTO materials (id, code, name, category, unit, stock_quantity, warning_quantity, status, supplier, remark, update_time, created_at, updated_at) VALUES
|
||||
('1', 'FEED001', '牛用精饲料', 'feed', '袋', 250, 50, 'normal', '绿源饲料公司', '高蛋白配方', '2024-04-10 09:30:00', '2024-04-01 09:30:00', '2024-04-10 09:30:00'),
|
||||
('2', 'FEED002', '粗饲料', 'feed', '吨', 12, 5, 'low', '草原饲料厂', '优质牧草', '2024-04-09 14:20:00', '2024-04-02 14:20:00', '2024-04-09 14:20:00'),
|
||||
('3', 'MED001', '牛瘟疫苗', 'medicine', '盒', 0, 10, 'out', '动保生物公司', '每盒10支', '2024-04-08 10:15:00', '2024-04-03 10:15:00', '2024-04-08 10:15:00'),
|
||||
('4', 'MED002', '驱虫药', 'medicine', '瓶', 85, 20, 'normal', '兽药批发中心', '广谱驱虫', '2024-04-10 11:45:00', '2024-04-04 11:45:00', '2024-04-10 11:45:00'),
|
||||
('5', 'EQU001', '牛用耳标', 'equipment', '个', 3500, 500, 'normal', '畜牧设备公司', 'RFID电子耳标', '2024-04-07 16:00:00', '2024-04-05 16:00:00', '2024-04-07 16:00:00'),
|
||||
('6', 'EQU002', '体温计', 'equipment', '支', 15, 5, 'normal', '医疗器械公司', '兽用电子体温计', '2024-04-06 13:30:00', '2024-04-06 13:30:00', '2024-04-06 13:30:00'),
|
||||
('7', 'FEED003', '矿物质添加剂', 'feed', 'kg', 35, 10, 'normal', '营养添加剂厂', '补充微量元素', '2024-04-05 10:15:00', '2024-04-07 10:15:00', '2024-04-05 10:15:00'),
|
||||
('8', 'MED003', '抗生素', 'medicine', '盒', 5, 10, 'low', '兽药批发中心', '需处方使用', '2024-04-04 15:45:00', '2024-04-08 15:45:00', '2024-04-04 15:45:00'),
|
||||
('9', 'EQU003', '消毒设备', 'equipment', '台', 3, 1, 'normal', '畜牧设备公司', '自动喷雾消毒机', '2024-04-03 09:30:00', '2024-04-09 09:30:00', '2024-04-03 09:30:00'),
|
||||
('10', 'OTH001', '防护服', 'other', '套', 120, 30, 'normal', '劳保用品公司', '一次性使用', '2024-04-02 14:20:00', '2024-04-10 14:20:00', '2024-04-02 14:20:00');
|
||||
|
||||
-- 插入交易记录测试数据
|
||||
INSERT INTO warehouse_transactions (id, material_id, type, quantity, operator, remark, created_at, updated_at) VALUES
|
||||
('1', '1', 'in', 250, '管理员', '采购入库', '2024-04-10 09:30:00', '2024-04-10 09:30:00'),
|
||||
('2', '2', 'in', 20, '管理员', '采购入库', '2024-04-02 14:20:00', '2024-04-02 14:20:00'),
|
||||
('3', '2', 'out', 8, '操作员A', '领用出库', '2024-04-09 14:20:00', '2024-04-09 14:20:00'),
|
||||
('4', '3', 'in', 50, '管理员', '采购入库', '2024-04-03 10:15:00', '2024-04-03 10:15:00'),
|
||||
('5', '3', 'out', 50, '操作员B', '领用出库', '2024-04-08 10:15:00', '2024-04-08 10:15:00'),
|
||||
('6', '4', 'in', 100, '管理员', '采购入库', '2024-04-04 11:45:00', '2024-04-04 11:45:00'),
|
||||
('7', '4', 'out', 15, '操作员A', '领用出库', '2024-04-10 11:45:00', '2024-04-10 11:45:00'),
|
||||
('8', '5', 'in', 3500, '管理员', '采购入库', '2024-04-05 16:00:00', '2024-04-05 16:00:00'),
|
||||
('9', '6', 'in', 15, '管理员', '采购入库', '2024-04-06 13:30:00', '2024-04-06 13:30:00'),
|
||||
('10', '7', 'in', 35, '管理员', '采购入库', '2024-04-07 10:15:00', '2024-04-07 10:15:00');
|
||||
Reference in New Issue
Block a user