添加银行端后端接口

This commit is contained in:
2025-09-24 17:49:32 +08:00
parent b58ed724b0
commit 111ebaec84
95 changed files with 22115 additions and 4246 deletions

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

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

View File

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

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