1. 项目背景与核心需求
在传统制造业和商贸流通领域,企业资源计划(ERP)系统的进销存管理模块一直是业务运营的核心支柱。我最近完成的一个Spring Boot项目,正是针对中型制造企业的完整物流信息管理解决方案。这个系统最关键的创新点在于实现了采购单、销售单、调拨单等业务单据的数字化全流程追踪,以及与第三方物流系统的深度集成。
从技术架构来看,系统采用Spring Boot 2.7作为基础框架,配合MyBatis-Plus实现数据持久层操作。选择这个技术栈主要基于三个考量:首先,Spring Boot的自动配置特性能够快速搭建符合企业级标准的应用;其次,MyBatis-Plus在复杂SQL操作和动态数据源切换方面具有明显优势;最后,整个技术生态的成熟度能够保障系统在五年内的可维护性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计解析
2.1 分层架构实现
系统采用经典的四层架构设计:
- 表现层:基于Thymeleaf模板引擎的Web界面,配合Vue.js实现前后端分离
- 应用层:Spring MVC控制器处理业务逻辑,自定义注解实现权限控制
- 服务层:领域驱动设计(DDD)划分的各个Service模块
- 基础设施层:集成Redis缓存、RabbitMQ消息队列等组件
特别值得注意的是单据状态机的设计。我们使用枚举类配合策略模式来实现单据状态流转,例如采购单的"草稿→待审核→已审核→部分入库→已完成"等状态变迁。这种设计避免了复杂的if-else嵌套,每个状态变更都是一个独立的策略类。
2.2 数据库模型设计
核心表结构包括:
sql复制CREATE TABLE `erp_bill` (
`id` bigint NOT NULL AUTO_INCREMENT,
`bill_type` varchar(20) COMMENT '单据类型',
`bill_status` varchar(20) COMMENT '单据状态',
`creator_id` bigint COMMENT '创建人',
`create_time` datetime COMMENT '创建时间',
`approver_id` bigint COMMENT '审核人',
`approve_time` datetime COMMENT '审核时间',
`total_amount` decimal(18,2) COMMENT '总金额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `erp_bill_detail` (
`id` bigint NOT NULL AUTO_INCREMENT,
`bill_id` bigint COMMENT '主单ID',
`material_id` bigint COMMENT '物料ID',
`quantity` decimal(18,3) COMMENT '数量',
`unit_price` decimal(18,2) COMMENT '单价',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这种主从表结构设计既保证了单据头信息的完整性,又支持灵活的明细项管理。我们在实际项目中还添加了版本号字段(version)来实现乐观锁,避免并发操作导致的数据不一致。
3. 核心功能实现细节
3.1 单据流转引擎
单据审批流程是系统的核心功能之一。我们采用责任链模式实现多级审批:
java复制public interface BillApprovalHandler {
void handleApproval(ErpBill bill, ApprovalContext context);
}
@Service
@RequiredArgsConstructor
public class DepartmentManagerApproval implements BillApprovalHandler {
private final ErpBillMapper billMapper;
@Override
public void handleApproval(ErpBill bill, ApprovalContext context) {
if (bill.getAmount() > 50000) {
bill.setStatus("DEPARTMENT_APPROVED");
billMapper.updateById(bill);
context.addAuditLog("部门经理审批通过");
}
}
}
每个审批环节都是一个独立的Spring Bean,通过@Order注解确定执行顺序。这种设计使得审批流程的调整变得非常简单,只需修改Bean的装配顺序即可。
3.2 物流信息集成
与物流系统的集成采用了两种方案:
- 对于支持WebService的物流商,使用JAX-WS生成客户端代码
- 对于提供REST API的物流商,使用FeignClient声明式调用
一个典型的物流状态同步代码如下:
java复制@Scheduled(cron = "0 0/30 * * * ?")
public void syncLogisticsStatus() {
List<ErpBill> bills = billMapper.selectUncompletedBills();
bills.forEach(bill -> {
LogisticsDTO logistics = logisticsClient.getLatestStatus(bill.getLogisticsNo());
bill.setLogisticsStatus(logistics.getStatus());
billMapper.updateById(bill);
});
}
这里使用了Spring的@Scheduled实现定时任务,每30分钟同步一次物流状态。为了避免大量查询对物流系统造成压力,我们实现了分页批处理机制。
4. 性能优化实践
4.1 缓存策略设计
系统采用多级缓存架构:
- 本地缓存:使用Caffeine缓存基础数据字典
- 分布式缓存:Redis缓存热点单据数据
- 数据库缓存:MySQL查询缓存
关键配置示例:
yaml复制spring:
cache:
type: redis
redis:
time-to-live: 1800s
cache-null-values: false
特别注意cache-null-values设置为false,避免缓存穿透问题。对于高频访问但可能为空的查询(如不存在的单据号),我们在Service层进行了特殊处理:
java复制@Cacheable(value = "bills", unless = "#result == null")
public ErpBill getBillById(Long id) {
return Optional.ofNullable(billMapper.selectById(id))
.orElseThrow(() -> new BusinessException("单据不存在"));
}
4.2 数据库优化
针对单据查询的典型场景,我们设计了复合索引:
sql复制ALTER TABLE `erp_bill` ADD INDEX `idx_bill_query` (`bill_type`, `bill_status`, `create_time`);
同时使用MyBatis-Plus的分页插件优化大数据量查询:
java复制Page<ErpBill> page = new Page<>(1, 20);
LambdaQueryWrapper<ErpBill> query = Wrappers.lambdaQuery();
query.eq(ErpBill::getBillType, "PURCHASE")
.orderByDesc(ErpBill::getCreateTime);
billMapper.selectPage(page, query);
5. 安全防护措施
5.1 接口安全
所有API接口都经过以下安全处理:
- 使用Spring Security实现基于角色的访问控制
- 敏感接口添加@PreAuthorize注解
- 启用CSRF防护(对于非API调用)
- 关键操作日志记录
示例配置:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/bills/**").hasAnyRole("ACCOUNTING", "MANAGER")
.anyRequest().authenticated()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
}
5.2 数据安全
敏感字段如金额、客户信息等都进行了加密存储。我们使用Jasypt实现字段级加密:
java复制@Encrypted
private String customerBankAccount;
在application.properties中配置加密密钥:
properties复制jasypt.encryptor.password=${JASYPT_ENCRYPTOR_PASSWORD:defaultKey}
6. 部署与监控
6.1 容器化部署
系统采用Docker Compose进行一体化部署:
dockerfile复制version: '3'
services:
erp-app:
image: erp-system:1.0.0
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
redis:
image: redis:6.2-alpine
ports:
- "6379:6379"
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: erp_db
ports:
- "3306:3306"
6.2 监控方案
集成Spring Boot Actuator提供基础监控端点,配合Prometheus和Grafana实现可视化监控。关键指标包括:
- 单据处理吞吐量
- 平均响应时间
- 缓存命中率
- 数据库连接池状态
配置示例:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
7. 典型问题解决方案
7.1 并发修改冲突
在单据审核场景中,我们遇到多个主管同时审批同一单据的问题。最终解决方案是:
- 数据库层面使用乐观锁
- 前端增加操作提示
- 后端实现分布式锁
关键代码:
java复制public boolean approveBill(Long billId, Long userId) {
String lockKey = "bill_approve:" + billId;
try {
// 尝试获取分布式锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("单据正在被其他用户处理");
}
ErpBill bill = billMapper.selectById(billId);
bill.setApproverId(userId);
bill.setApproveTime(new Date());
return billMapper.updateById(bill) > 0;
} finally {
redisTemplate.delete(lockKey);
}
}
7.2 大数据量导出
当用户需要导出大量单据数据时,我们采用以下优化方案:
- 使用MyBatis的流式查询
- 分批次处理数据
- 异步生成下载链接
实现代码:
java复制public void exportBills(Long userId, BillQuery query) {
CompletableFuture.runAsync(() -> {
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100);
SqlSession sqlSession = sqlSessionFactory.openSession();
Cursor<ErpBill> cursor = sqlSession.selectCursor(
"com.erp.mapper.ErpBillMapper.selectByQuery", query)) {
// 创建Excel表格
Sheet sheet = workbook.createSheet("单据列表");
// 流式处理数据
int rowNum = 0;
for (ErpBill bill : cursor) {
Row row = sheet.createRow(rowNum++);
// 填充单元格数据...
}
// 保存到临时文件
Path tempFile = Files.createTempFile("bills_", ".xlsx");
try (OutputStream out = Files.newOutputStream(tempFile)) {
workbook.write(out);
}
// 生成下载链接并通知用户
String downloadUrl = fileService.upload(tempFile);
notificationService.notifyUser(userId, "导出完成", downloadUrl);
}
}, taskExecutor);
}
8. 扩展与集成能力
8.1 第三方系统对接
系统预留了多种集成接口:
- 提供REST API供其他系统调用
- 支持Webhook回调通知
- 实现ERP标准格式的XML/JSON数据交换
一个典型的API接口示例:
java复制@RestController
@RequestMapping("/api/external")
public class ExternalApiController {
@PostMapping("/syncMaterial")
public ResponseEntity<?> syncMaterial(@Valid @RequestBody MaterialSyncDTO dto) {
materialService.syncFromExternal(dto);
return ResponseEntity.ok().build();
}
}
8.2 插件扩展机制
通过Spring的自动装配机制实现插件化扩展。其他模块只需要实现特定接口并添加@ErpPlugin注解即可被主系统发现:
java复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface ErpPlugin {
String value();
}
@ErpPlugin("customReport")
public class CustomReportPlugin implements ReportGenerator {
// 实现自定义报表逻辑
}
主系统通过ApplicationContext获取所有插件实例:
java复制Map<String, ReportGenerator> plugins = applicationContext.getBeansWithAnnotation(ErpPlugin.class)
.values().stream()
.filter(b -> b instanceof ReportGenerator)
.collect(Collectors.toMap(
b -> b.getClass().getAnnotation(ErpPlugin.class).value(),
b -> (ReportGenerator)b
));
在项目实际实施过程中,我们发现单据编号生成策略对系统性能影响很大。最初采用数据库序列方式,在高并发场景下成为瓶颈。后来改为"前缀+日期+Redis原子计数器"的方案,性能提升显著。这个细节告诉我们,在ERP系统设计中,看似简单的功能点也可能成为关键性能瓶颈,需要在架构设计阶段就充分考虑扩展性。
