项目实战:出入库管理系统

发布时间:2026/11/4 2:25:13
项目实战:出入库管理系统

前言

这一篇进入项目实战:一个完整的出入库管理系统。

这个项目把前面学的 Spring Boot、MyBatis、Vue 3、Element Plus、Axios 全部串起来

“请做好准备,全军出击”

一、出入库业务理解(完整业务流程)

1.1 什么是出入库管理?

出入库管理就是记录货物的进(入库)和出(出库),并跟踪库存变化。

1.2 核心业务实体

实体说明
商品(Goods)仓库里的货物
仓库(Warehouse)存放商品的地方
出入库记录(StockRecord)一次入库或出库的操作记录
审核状态(Status)记录是否已审核

1.3 完整业务流程

text

1. 新增出入库记录(草稿/待审核) ↓ 2. 编辑记录(未审核前可修改) ↓ 3. 批量审核(审核通过/驳回) ↓ 4. 审核通过后,更新库存 ↓ 5. 查询出入库记录(多条件 + 分页)

1.4 业务规则

规则说明
未审核可编辑已审核的记录不能改
审核后改库存入库加库存,出库减库存
批量审核可以一次审核多条
出库不能超库存出库数量不能大于当前库存

1.5 出入库记录字段设计

字段类型说明
idInteger主键
recordNoString记录编号
goodsIdInteger商品 ID
goodsNameString商品名称
warehouseIdInteger仓库 ID
typeInteger类型:1 入库,2 出库
quantityInteger数量
statusInteger状态:0 待审核,1 已通过,2 已驳回
remarkString备注
createTimeLocalDateTime创建时间
auditTimeLocalDateTime审核时间

二、后端接口分析(业务需求对应接口)

2.1 接口清单

业务需求请求方法接口路径说明
分页查询记录GET/api/records多条件 + 分页
查询单条记录GET/api/records/{id}编辑前回显
新增记录POST/api/records新增出入库记录
编辑记录PUT/api/records/{id}修改未审核记录
删除记录DELETE/api/records/{id}删除未审核记录
批量审核PUT/api/records/audit批量通过/驳回

2.2 接口详细设计

2.2.1 分页查询

text

GET /api/records?recordNo=&goodsName=&type=&status=&page=1&size=10

响应:

json

{ "code": 200, "message": "成功", "data": { "list": [ ... ], "total": 100 } }
2.2.2 新增记录

text

POST /api/records Body: { "goodsId": 1, "warehouseId": 1, "type": 1, "quantity": 100, "remark": "首次入库" }
2.2.3 批量审核

text

PUT /api/records/audit Body: { "ids": [1, 2, 3], "status": 1 }

2.3 对应关系

业务Controller 方法Service 方法Mapper 方法
分页查询page()page()selectByCondition()
查询单条getById()getById()selectById()
新增add()add()insert()
编辑update()update()update()
删除delete()delete()deleteById()
批量审核audit()audit()updateStatusBatch()

三、后端接口开发

3.1 Entity

java

@Data public class StockRecord { private Integer id; private String recordNo; private Integer goodsId; private String goodsName; private Integer warehouseId; private Integer type; // 1 入库,2 出库 private Integer quantity; private Integer status; // 0 待审核,1 通过,2 驳回 private String remark; private LocalDateTime createTime; private LocalDateTime auditTime; }

3.2 Query 对象

java

@Data public class StockRecordQuery { private String recordNo; private String goodsName; private Integer type; private Integer status; private Integer page = 1; private Integer size = 10; }

3.3 Mapper

java

@Mapper public interface StockRecordMapper { List<StockRecord> selectByCondition(StockRecordQuery query); StockRecord selectById(Integer id); int insert(StockRecord record); int update(StockRecord record); int deleteById(Integer id); int updateStatusBatch(@Param("ids") List<Integer> ids, @Param("status") Integer status); }

3.4 Mapper XML

xml

<select id="selectByCondition" resultType="StockRecord"> SELECT * FROM stock_record <where> <if test="recordNo != null and recordNo != ''"> AND record_no LIKE CONCAT('%', #{recordNo}, '%') </if> <if test="goodsName != null and goodsName != ''"> AND goods_name LIKE CONCAT('%', #{goodsName}, '%') </if> <if test="type != null"> AND type = #{type} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select> <select id="selectById" resultType="StockRecord"> SELECT * FROM stock_record WHERE id = #{id} </select> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO stock_record (record_no, goods_id, goods_name, warehouse_id, type, quantity, status, remark, create_time) VALUES (#{recordNo}, #{goodsId}, #{goodsName}, #{warehouseId}, #{type}, #{quantity}, 0, #{remark}, NOW()) </insert> <update id="update"> UPDATE stock_record <set> <if test="goodsId != null">goods_id = #{goodsId},</if> <if test="warehouseId != null">warehouse_id = #{warehouseId},</if> <if test="type != null">type = #{type},</if> <if test="quantity != null">quantity = #{quantity},</if> <if test="remark != null">remark = #{remark},</if> </set> WHERE id = #{id} AND status = 0 </update> <delete id="deleteById"> DELETE FROM stock_record WHERE id = #{id} AND status = 0 </delete> <update id="updateStatusBatch"> UPDATE stock_record SET status = #{status}, audit_time = NOW() WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> AND status = 0 </update>

3.5 Service

java

public interface StockRecordService { PageResult<StockRecord> page(StockRecordQuery query); StockRecord getById(Integer id); void add(StockRecord record); void update(StockRecord record); void delete(Integer id); void audit(List<Integer> ids, Integer status); }

java

@Service public class StockRecordServiceImpl implements StockRecordService { @Autowired private StockRecordMapper recordMapper; @Autowired private GoodsMapper goodsMapper; @Override public PageResult<StockRecord> page(StockRecordQuery query) { PageHelper.startPage(query.getPage(), query.getSize()); List<StockRecord> list = recordMapper.selectByCondition(query); PageInfo<StockRecord> pageInfo = new PageInfo<>(list); PageResult<StockRecord> result = new PageResult<>(); result.setList(list); result.setTotal(pageInfo.getTotal()); return result; } @Override public StockRecord getById(Integer id) { StockRecord record = recordMapper.selectById(id); if (record == null) { throw new BusinessException(404, "记录不存在"); } return record; } @Override public void add(StockRecord record) { // 生成记录编号 record.setRecordNo("RK" + System.currentTimeMillis()); recordMapper.insert(record); } @Override public void update(StockRecord record) { StockRecord old = recordMapper.selectById(record.getId()); if (old == null) { throw new BusinessException(404, "记录不存在"); } if (old.getStatus() != 0) { throw new BusinessException("已审核的记录不能修改"); } recordMapper.update(record); } @Override public void delete(Integer id) { StockRecord old = recordMapper.selectById(id); if (old == null) { throw new BusinessException(404, "记录不存在"); } if (old.getStatus() != 0) { throw new BusinessException("已审核的记录不能删除"); } recordMapper.deleteById(id); } @Transactional @Override public void audit(List<Integer> ids, Integer status) { // 1. 批量更新状态 recordMapper.updateStatusBatch(ids, status); // 2. 审核通过时更新库存 if (status == 1) { for (Integer id : ids) { StockRecord record = recordMapper.selectById(id); if (record.getType() == 1) { goodsMapper.increaseStock(record.getGoodsId(), record.getQuantity()); } else { goodsMapper.decreaseStock(record.getGoodsId(), record.getQuantity()); } } } } }

3.6 Controller

java

@RestController @RequestMapping("/api/records") public class StockRecordController { @Autowired private StockRecordService recordService; @GetMapping public Result<PageResult<StockRecord>> page(StockRecordQuery query) { return Result.success(recordService.page(query)); } @GetMapping("/{id}") public Result<StockRecord> getById(@PathVariable Integer id) { return Result.success(recordService.getById(id)); } @PostMapping public Result<Void> add(@RequestBody StockRecord record) { recordService.add(record); return Result.success(); } @PutMapping("/{id}") public Result<Void> update(@PathVariable Integer id, @RequestBody StockRecord record) { record.setId(id); recordService.update(record); return Result.success(); } @DeleteMapping("/{id}") public Result<Void> delete(@PathVariable Integer id) { recordService.delete(id); return Result.success(); } @PutMapping("/audit") public Result<Void> audit(@RequestBody AuditRequest request) { recordService.audit(request.getIds(), request.getStatus()); return Result.success(); } }

3.7 批量审核请求对象

java

@Data public class AuditRequest { private List<Integer> ids; private Integer status; }

四、前端界面搭建

4.1 页面结构

text

+------------------------------------------+ | 搜索栏:记录编号 | 商品名称 | 类型 | 状态 | 查询 | 重置 | +------------------------------------------+ | 操作栏:新增 | 批量审核 | +------------------------------------------+ | 表格:编号 | 商品 | 类型 | 数量 | 状态 | 操作 | +------------------------------------------+ | 分页 | +------------------------------------------+

4.2 页面组件

vue

<template> <div class="record-page"> <!-- 搜索栏 --> <el-form :inline="true" :model="query" class="search-form"> <el-form-item label="记录编号"> <el-input v-model="query.recordNo" placeholder="请输入" clearable /> </el-form-item> <el-form-item label="商品名称"> <el-input v-model="query.goodsName" placeholder="请输入" clearable /> </el-form-item> <el-form-item label="类型"> <el-select v-model="query.type" placeholder="全部" clearable> <el-option label="入库" :value="1" /> <el-option label="出库" :value="2" /> </el-select> </el-form-item> <el-form-item label="状态"> <el-select v-model="query.status" placeholder="全部" clearable> <el-option label="待审核" :value="0" /> <el-option label="已通过" :value="1" /> <el-option label="已驳回" :value="2" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> <el-button @click="handleReset">重置</el-button> </el-form-item> </el-form> <!-- 操作栏 --> <div class="toolbar"> <el-button type="primary" @click="handleAdd">新增</el-button> <el-button type="success" :disabled="selectedIds.length === 0" @click="handleAudit"> 批量审核 </el-button> </div> <!-- 表格 --> <el-table :data="tableData" border stripe @selection-change="handleSelectionChange" > <el-table-column type="selection" width="50" /> <el-table-column prop="recordNo" label="记录编号" width="160" /> <el-table-column prop="goodsName" label="商品名称" /> <el-table-column label="类型" width="100"> <template #default="{ row }"> <el-tag :type="row.type === 1 ? 'success' : 'warning'"> {{ row.type === 1 ? '入库' : '出库' }} </el-tag> </template> </el-table-column> <el-table-column prop="quantity" label="数量" width="100" /> <el-table-column label="状态" width="120"> <template #default="{ row }"> <el-tag :type="statusType(row.status)"> {{ statusText(row.status) }} </el-tag> </template> </el-table-column> <el-table-column prop="createTime" label="创建时间" width="180" /> <el-table-column label="操作" width="200"> <template #default="{ row }"> <el-button size="small" :disabled="row.status !== 0" @click="handleEdit(row)"> 编辑 </el-button> <el-button size="small" type="danger" :disabled="row.status !== 0" @click="handleDelete(row)"> 删除 </el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper" @size-change="fetchData" @current-change="fetchData" /> <!-- 新增/编辑弹窗 --> <RecordForm v-model="formVisible" :edit-data="editData" @success="fetchData" /> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { getRecordList, deleteRecord, auditRecords } from '@/api/record' import RecordForm from './RecordForm.vue' const query = reactive({ recordNo: '', goodsName: '', type: null, status: null, page: 1, size: 10 }) const tableData = ref([]) const total = ref(0) const selectedIds = ref([]) const formVisible = ref(false) const editData = ref(null) async function fetchData() { const res = await getRecordList(query) tableData.value = res.list total.value = res.total } function handleSearch() { query.page = 1 fetchData() } function handleReset() { query.recordNo = '' query.goodsName = '' query.type = null query.status = null query.page = 1 fetchData() } function handleSelectionChange(rows) { selectedIds.value = rows.filter(r => r.status === 0).map(r => r.id) } function handleAdd() { editData.value = null formVisible.value = true } function handleEdit(row) { editData.value = row formVisible.value = true } async function handleDelete(row) { try { await ElMessageBox.confirm('确定要删除吗?', '提示', { type: 'warning' }) await deleteRecord(row.id) ElMessage.success('删除成功') fetchData() } catch {} } async function handleAudit() { if (selectedIds.value.length === 0) { ElMessage.warning('请选择待审核的记录') return } try { await ElMessageBox.confirm(`确定审核通过选中的 ${selectedIds.value.length} 条记录吗?`, '提示', { type: 'warning' }) await auditRecords({ ids: selectedIds.value, status: 1 }) ElMessage.success('审核成功') fetchData() } catch {} } function statusText(status) { return { 0: '待审核', 1: '已通过', 2: '已驳回' }[status] } function statusType(status) { return { 0: 'warning', 1: 'success', 2: 'danger' }[status] } onMounted(fetchData) </script>

五、多条件分页查询出入库记录的前端开发

5.1 API 封装

javascript

// api/record.js import request from '@/utils/request' export function getRecordList(params) { return request.get('/records', { params }) } export function getRecordById(id) { return request.get(`/records/${id}`) } export function addRecord(data) { return request.post('/records', data) } export function updateRecord(id, data) { return request.put(`/records/${id}`, data) } export function deleteRecord(id) { return request.delete(`/records/${id}`) } export function auditRecords(data) { return request.put('/records/audit', data) }

5.2 查询参数处理

javascript

const query = reactive({ recordNo: '', goodsName: '', type: null, status: null, page: 1, size: 10 })

5.3 查询与重置

javascript

async function fetchData() { const res = await getRecordList(query) tableData.value = res.list total.value = res.total } function handleSearch() { query.page = 1 // 查询时重置到第一页 fetchData() } function handleReset() { Object.assign(query, { recordNo: '', goodsName: '', type: null, status: null, page: 1, size: 10 }) fetchData() }

5.4 分页切换

vue

<el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" @size-change="fetchData" @current-change="fetchData" />

六、新增出入库记录后端接口开发流程

6.1 流程

text

1. Controller 接收 @RequestBody 2. Service 生成记录编号 3. Service 设置默认状态为待审核 4. Mapper 插入数据库 5. 返回成功

6.2 Controller

java

@PostMapping public Result<Void> add(@RequestBody StockRecord record) { recordService.add(record); return Result.success(); }

6.3 Service

java

@Override public void add(StockRecord record) { // 校验 if (record.getGoodsId() == null || record.getQuantity() == null) { throw new BusinessException("商品和数量不能为空"); } if (record.getQuantity() <= 0) { throw new BusinessException("数量必须大于 0"); } // 生成记录编号 record.setRecordNo("RK" + System.currentTimeMillis()); // 默认待审核 record.setStatus(0); record.setCreateTime(LocalDateTime.now()); recordMapper.insert(record); }

6.4 Mapper

xml

<insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO stock_record (record_no, goods_id, goods_name, warehouse_id, type, quantity, status, remark, create_time) VALUES (#{recordNo}, #{goodsId}, #{goodsName}, #{warehouseId}, #{type}, #{quantity}, #{status}, #{remark}, #{createTime}) </insert>

七、新增出入库记录前端开发流程

7.1 表单组件

vue

<template> <el-dialog v-model="visible" :title="isEdit ? '编辑记录' : '新增记录'" width="600px" @close="handleClose" > <el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> <el-form-item label="商品" prop="goodsId"> <el-select v-model="form.goodsId" placeholder="请选择商品" filterable> <el-option v-for="g in goodsList" :key="g.id" :label="g.name" :value="g.id" /> </el-select> </el-form-item> <el-form-item label="仓库" prop="warehouseId"> <el-select v-model="form.warehouseId" placeholder="请选择仓库"> <el-option v-for="w in warehouseList" :key="w.id" :label="w.name" :value="w.id" /> </el-select> </el-form-item> <el-form-item label="类型" prop="type"> <el-radio-group v-model="form.type"> <el-radio :value="1">入库</el-radio> <el-radio :value="2">出库</el-radio> </el-radio-group> </el-form-item> <el-form-item label="数量" prop="quantity"> <el-input-number v-model="form.quantity" :min="1" /> </el-form-item> <el-form-item label="备注"> <el-input v-model="form.remark" type="textarea" :rows="3" /> </el-form-item> </el-form> <template #footer> <el-button @click="handleClose">取消</el-button> <el-button type="primary" @click="handleSubmit">确定</el-button> </template> </el-dialog> </template> <script setup> import { ref, reactive, computed, watch } from 'vue' import { ElMessage } from 'element-plus' import { addRecord, updateRecord } from '@/api/record' import { getGoodsList } from '@/api/goods' import { getWarehouseList } from '@/api/warehouse' const props = defineProps({ modelValue: Boolean, editData: Object }) const emit = defineEmits(['update:modelValue', 'success']) const visible = computed({ get: () => props.modelValue, set: (val) => emit('update:modelValue', val) }) const isEdit = computed(() => !!props.editData) const formRef = ref(null) const goodsList = ref([]) const warehouseList = ref([]) const form = reactive({ goodsId: null, warehouseId: null, type: 1, quantity: 1, remark: '' }) const rules = { goodsId: [{ required: true, message: '请选择商品', trigger: 'change' }], warehouseId: [{ required: true, message: '请选择仓库', trigger: 'change' }], type: [{ required: true, message: '请选择类型', trigger: 'change' }], quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }] } watch(() => props.modelValue, async (val) => { if (val) { // 加载下拉数据 goodsList.value = await getGoodsList() warehouseList.value = await getWarehouseList() // 编辑时回显 if (props.editData) { Object.assign(form, props.editData) } else { Object.assign(form, { goodsId: null, warehouseId: null, type: 1, quantity: 1, remark: '' }) } } }) async function handleSubmit() { await formRef.value.validate() if (isEdit.value) { await updateRecord(props.editData.id, form) ElMessage.success('修改成功') } else { await addRecord(form) ElMessage.success('新增成功') } emit('success') handleClose() } function handleClose() { visible.value = false formRef.value?.resetFields() } </script>

7.2 提交逻辑

场景调用接口
新增POST /records
编辑PUT /records/{id}

八、编辑出入库记录功能开发【数据回显和编辑】

8.1 数据回显流程

text

1. 点击编辑按钮 → 拿到 row 数据 2. 打开弹窗,把 row 数据传给子组件 3. 子组件 watch 监听到 editData,回显到表单 4. 用户修改后提交

8.2 回显实现

javascript

watch(() => props.modelValue, async (val) => { if (val) { goodsList.value = await getGoodsList() warehouseList.value = await getWarehouseList() if (props.editData) { // 回显 Object.assign(form, { goodsId: props.editData.goodsId, warehouseId: props.editData.warehouseId, type: props.editData.type, quantity: props.editData.quantity, remark: props.editData.remark }) } } })

8.3 编辑提交

java

@PutMapping("/{id}") public Result<Void> update(@PathVariable Integer id, @RequestBody StockRecord record) { record.setId(id); recordService.update(record); return Result.success(); }

java

@Override public void update(StockRecord record) { StockRecord old = recordMapper.selectById(record.getId()); if (old == null) { throw new BusinessException(404, "记录不存在"); } if (old.getStatus() != 0) { throw new BusinessException("已审核的记录不能修改"); } recordMapper.update(record); }

8.4 编辑的注意事项

注意点说明
只有待审核可编辑已审核记录禁用编辑按钮
回显要完整商品、仓库、类型、数量、备注都要回显
提交带上 id路径参数传 id
后端二次校验防止前端绕过,后端再查状态

九、批量审核出入库记录功能开发流程

9.1 流程

text

1. 前端勾选多条待审核记录 2. 点击批量审核,选择通过/驳回 3. 后端批量更新状态 4. 审核通过时,更新库存 5. 返回成功,刷新列表

9.2 前端实现

javascript

const selectedIds = ref([]) function handleSelectionChange(rows) { // 只保留待审核的记录 selectedIds.value = rows.filter(r => r.status === 0).map(r => r.id) } async function handleAudit() { if (selectedIds.value.length === 0) { ElMessage.warning('请选择待审核的记录') return } try { await ElMessageBox.confirm( `确定审核通过选中的 ${selectedIds.value.length} 条记录吗?`, '提示', { type: 'warning' } ) await auditRecords({ ids: selectedIds.value, status: 1 }) ElMessage.success('审核成功') fetchData() } catch {} }

9.3 后端实现

java

@PutMapping("/audit") public Result<Void> audit(@RequestBody AuditRequest request) { recordService.audit(request.getIds(), request.getStatus()); return Result.success(); }

java

@Transactional @Override public void audit(List<Integer> ids, Integer status) { if (ids == null || ids.isEmpty()) { throw new BusinessException("请选择要审核的记录"); } // 1. 批量更新状态 int rows = recordMapper.updateStatusBatch(ids, status); if (rows == 0) { throw new BusinessException("没有可审核的记录"); } // 2. 审核通过时更新库存 if (status == 1) { for (Integer id : ids) { StockRecord record = recordMapper.selectById(id); if (record.getType() == 1) { goodsMapper.increaseStock(record.getGoodsId(), record.getQuantity()); } else { // 出库前检查库存 int stock = goodsMapper.getStock(record.getGoodsId()); if (stock < record.getQuantity()) { throw new BusinessException("商品 " + record.getGoodsName() + " 库存不足"); } goodsMapper.decreaseStock(record.getGoodsId(), record.getQuantity()); } } } }

9.4 Mapper 批量更新

xml

<update id="updateStatusBatch"> UPDATE stock_record SET status = #{status}, audit_time = NOW() WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> AND status = 0 </update>

9.5 注意事项

注意点说明
只审核待审核记录WHERE status = 0防止重复审核
事务保证状态更新 + 库存更新必须在一个事务里
库存校验出库前检查库存,不足则抛异常回滚
批量操作用<foreach>拼接 IN 语句
审核后不可改已审核记录禁用编辑和删除

十、核心口诀

知识点口诀
业务理解新增 → 编辑 → 审核 → 更新库存 → 查询
接口分析业务需求对应 Controller/Service/Mapper 方法
后端开发Entity → Query → Mapper → Service → Controller
前端搭建搜索栏 + 操作栏 + 表格 + 分页 + 弹窗
多条件分页查询参数 + PageHelper + el-pagination
新增后端生成编号、默认待审核、插入数据库
新增前端表单校验 + 提交 + 刷新列表
编辑回显拿到 row 数据 → 传给子组件 → 回显表单
批量审核勾选待审核 → 批量更新状态 → 事务更新库存

十一、常见易错点

易错点说明
已审核记录还能编辑前端禁用按钮,后端二次校验状态
批量审核重复审核SQL 加WHERE status = 0
审核更新库存没加事务状态和库存必须在一个事务里
出库不检查库存出库前查库存,不足抛异常
编辑回显不完整所有字段都要回显
分页查询后没重置页码搜索时page = 1
批量审核勾选已审核记录前端过滤只保留status === 0
新增编号重复用时间戳或 UUID 生成唯一编号
删除已审核记录后端校验状态,只允许删待审核
库存更新并发问题用UPDATE ... SET stock = stock - ?原子操作

十二、总结

这篇覆盖了出入库业务理解、后端接口分析、后端接口开发、前端界面搭建、多条件分页查询、新增后端/前端流程、编辑数据回显、批量审核