1. 项目概述
这个基于Spring Boot的ERP进销存管理系统是我和团队花了半年时间打磨出来的实战项目,核心解决了中小企业在商品流转过程中的单据管理难题。系统最大的特色是通过六大核心单据(销售单、入库单、出库单、采购申请单、采购单、退货单)的智能关联,实现了业务流的自动化衔接。
特别提醒:单据流转功能如果设计不当,极易导致库存数据混乱。我们曾在一个客户现场看到,由于采购单转入库单时没有做好事务控制,结果同一批货被重复入库两次,库存数据完全对不上。
系统采用前后端分离架构,后端基于Spring Boot+MyBatis,前端使用Vue+Element UI,数据库选用MySQL配合Redis做缓存和分布式锁。这种技术栈组合既保证了系统性能,又便于团队协作开发。
2. 核心功能设计
2.1 单据流转机制
单据流转是本系统的核心创新点,我们设计了三级关联体系:
- 基础关联:通过JPA的@OneToOne注解建立单据间关系
java复制@Entity
@Table(name = "stock_in")
public class StockIn {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "source_order_id")
private Long sourceOrderId; // 来源单据ID
@Enumerated(EnumType.STRING)
@Column(name = "source_order_type")
private OrderType sourceOrderType; // 来源单据类型
@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "procurement_id")
private ProcurementOrder procurementOrder; // 关联的采购单实体
}
- 状态机控制:每个单据都有明确的状态流转规则
mermaid复制stateDiagram-v2
[*] --> DRAFT
DRAFT --> APPROVED: 提交审核
APPROVED --> PROCESSING: 审核通过
PROCESSING --> COMPLETED: 执行完成
COMPLETED --> CONVERTED: 生成下游单据
APPROVED --> REJECTED: 审核驳回
- 业务规则引擎:通过Spring的EventListener实现业务钩子
java复制@EventListener
public void handleProcurementComplete(ProcurementCompleteEvent event) {
// 自动生成入库单
StockIn stockIn = new StockIn();
stockIn.setSourceOrderId(event.getOrderId());
stockIn.setSourceOrderType(OrderType.PROCUREMENT);
// 复制商品明细
event.getItems().forEach(item -> {
stockIn.addItem(new StockInItem(item.getProductId(), item.getQuantity()));
});
stockInRepository.save(stockIn);
// 更新原采购单状态
ProcurementOrder order = procurementOrderRepository.findById(event.getOrderId())
.orElseThrow(() -> new BusinessException("采购单不存在"));
order.setStatus(OrderStatus.CONVERTED);
procurementOrderRepository.save(order);
}
2.2 库存管理设计
库存管理采用"预占+实际"的双层机制:
- 库存预占:销售单生成时预占库存
java复制@Transactional
public void reserveStock(SalesOrder order) {
order.getItems().forEach(item -> {
ProductStock stock = stockRepository.findByProductIdForUpdate(item.getProductId());
if (stock.getAvailableQuantity() < item.getQuantity()) {
throw new BusinessException("商品库存不足");
}
stock.setReservedQuantity(stock.getReservedQuantity() + item.getQuantity());
stockRepository.save(stock);
});
}
- 分布式锁控制:使用Redisson实现
java复制public void updateActualStock(Long productId, int delta) {
String lockKey = "stock_update:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
ProductStock stock = stockRepository.findByProductIdForUpdate(productId);
stock.setActualQuantity(stock.getActualQuantity() + delta);
stockRepository.save(stock);
}
} finally {
lock.unlock();
}
}
- 库存预警:定时任务检查库存水位
java复制@Scheduled(cron = "0 0 9 * * ?") // 每天上午9点执行
public void checkStockWarning() {
List<ProductStock> lowStocks = stockRepository.findByQuantityLessThan(warningThreshold);
lowStocks.forEach(stock -> {
String message = String.format("商品%s库存不足,当前库存%d",
stock.getProduct().getName(), stock.getActualQuantity());
warningService.sendAlert(message);
});
}
3. 权限系统实现
3.1 基于RBAC的权限模型
我们扩展了标准RBAC模型,增加了数据权限控制:
java复制@Entity
@Table(name = "sys_role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "sys_role_menu",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "menu_id"))
private Set<Menu> menus = new HashSet<>();
@ElementCollection
@CollectionTable(name = "sys_role_data_scope",
joinColumns = @JoinColumn(name = "role_id"))
@Column(name = "dept_id")
private Set<Long> dataScopeDepts = new HashSet<>();
}
3.2 注解式权限控制
通过自定义注解实现方法级权限校验:
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions {
String[] value();
Logical logical() default Logical.AND;
String dataAuth() default "";
}
// 使用示例
@RequiresPermissions(value = {"order:create", "order:edit"},
logical = Logical.OR, dataAuth = "dept")
@PostMapping("/orders")
public Result createOrder(@RequestBody Order order) {
// 业务逻辑
}
3.3 数据权限拦截器
java复制@Component
public class DataAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequiresPermissions annotation = handlerMethod.getMethodAnnotation(RequiresPermissions.class);
if (annotation != null && StringUtils.isNotBlank(annotation.dataAuth())) {
User currentUser = SecurityUtils.getCurrentUser();
String dataAuthType = annotation.dataAuth();
switch (dataAuthType) {
case "dept":
checkDeptDataAuth(request, currentUser);
break;
case "custom":
checkCustomDataAuth(request, currentUser);
break;
default:
throw new BusinessException("不支持的权限类型");
}
}
return true;
}
}
4. 前端设计要点
4.1 单据关联可视化
使用Element UI实现关联单据的可视化选择:
vue复制<template>
<el-dialog title="选择关联单据" :visible.sync="dialogVisible">
<el-tabs v-model="activeTab">
<el-tab-pane v-for="type in orderTypes"
:key="type.value"
:label="type.label"
:name="type.value">
<el-table :data="filteredOrders(type.value)"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"
:selectable="checkSelectable"></el-table-column>
<el-table-column prop="code" label="单据编号"></el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column prop="status" label="状态">
<template slot-scope="scope">
<el-tag :type="statusTagType(scope.row.status)">
{{ formatStatus(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</el-dialog>
</template>
<script>
export default {
methods: {
checkSelectable(row) {
return row.status === 'APPROVED' &&
!this.selectedOrderIds.includes(row.id);
},
filteredOrders(type) {
return this.orders.filter(
order => order.type === type &&
order.status === 'APPROVED'
);
}
}
}
</script>
4.2 实时库存提示
在商品选择时实时显示库存情况:
vue复制<template>
<el-select v-model="selectedProduct"
filterable
@change="handleProductChange">
<el-option
v-for="product in products"
:key="product.id"
:label="product.name"
:value="product.id">
<span style="float: left">{{ product.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">
库存: {{ product.stock }}
<el-tag v-if="product.stock < product.warningStock"
size="mini"
type="danger">低库存</el-tag>
</span>
</el-option>
</el-select>
</template>
5. 性能优化实践
5.1 单据编号生成优化
采用Redis原子操作生成唯一编号:
java复制public String generateOrderCode(String prefix) {
String dateStr = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String key = "order_code:" + prefix + ":" + dateStr;
Long sequence = redisTemplate.opsForValue().increment(key, 1);
return prefix + dateStr + String.format("%04d", sequence);
}
5.2 大数据量查询优化
- 分页查询优化:
java复制public PageResult<Order> queryOrders(OrderQuery query) {
PageHelper.startPage(query.getPageNum(), query.getPageSize());
List<Order> orders = orderMapper.selectByQuery(query);
PageInfo<Order> pageInfo = new PageInfo<>(orders);
return new PageResult<>(
pageInfo.getTotal(),
pageInfo.getList()
);
}
- 关联查询优化:
xml复制<select id="selectWithItems" resultMap="OrderWithItemsResultMap">
SELECT o.*,
i.id as item_id, i.product_id, i.quantity, i.price,
p.name as product_name, p.spec as product_spec
FROM orders o
LEFT JOIN order_items i ON o.id = i.order_id
LEFT JOIN products p ON i.product_id = p.id
WHERE o.id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
5.3 缓存策略设计
采用多级缓存策略:
- 本地缓存:使用Caffeine缓存基础数据
java复制@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000));
return cacheManager;
}
}
- 分布式缓存:Redis缓存热点数据
java复制public Product getProductWithCache(Long id) {
String cacheKey = "product:" + id;
Product product = redisTemplate.opsForValue().get(cacheKey);
if (product == null) {
product = productRepository.findById(id).orElse(null);
if (product != null) {
redisTemplate.opsForValue().set(
cacheKey,
product,
30, TimeUnit.MINUTES);
}
}
return product;
}
6. 部署与监控
6.1 Docker化部署
dockerfile复制# 后端Dockerfile
FROM openjdk:11-jdk
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
# 前端Dockerfile
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
6.2 Prometheus监控配置
yaml复制# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
6.3 日志收集方案
采用ELK栈收集日志:
java复制@Configuration
public class LogbackConfig {
@Bean
public LoggerContext loggerContext() {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
context.reset();
try {
configurator.doConfigure(getClass().getResourceAsStream("/logback-spring.xml"));
} catch (Exception e) {
e.printStackTrace();
}
return context;
}
}
xml复制<!-- logback-spring.xml -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>logstash:5044</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"${spring.application.name}"}</customFields>
</encoder>
</appender>
7. 项目总结与展望
经过半年多的开发和三个月的生产环境验证,这套ERP系统已经稳定支撑了5家中小企业的日常进销存管理。最大的收获是深刻理解了业务闭环的重要性 - 单据流转不是简单的数据传递,而是业务规则的具象化。
未来计划在以下方面继续优化:
- 引入工作流引擎实现更灵活的单据审批流
- 增加移动端支持,实现扫码入库/出库
- 整合财务模块,实现业财一体化
- 尝试使用Flink实现实时数据分析
这个项目的全部源码和2.3万字的设计文档已经整理完毕,包含完整的部署手册和API文档。对于想要学习Spring Boot实战开发的同学,这个项目涵盖了企业级应用的典型场景,值得深入研究。