1. 项目概述:企业级进销存ERP系统的技术实现路径
进销存管理一直是企业运营的核心痛点,尤其对于中小型企业而言,一套轻量、灵活且功能完备的ERP系统往往意味着运营效率的质的飞跃。我最近完整开发了一套基于Web的进销存ERP系统,采用前后端分离架构,实现了采购、销售、库存、财务等核心业务模块。这个项目从技术选型到业务逻辑实现,踩过不少坑也积累了些实战经验,今天就把这套系统的开发思路和关键技术点做个完整梳理。
这套系统主要面向中小型商贸企业,解决他们三大核心诉求:一是多终端随时随地访问(PC/手机/平板),二是业务流程的数字化闭环(从采购到销售再到财务对账),三是实时库存预警与数据分析。传统单体架构的ERP系统往往难以同时满足这些需求,而前后端分离的现代Web技术栈恰好能完美应对这些挑战。
技术栈选择上,前端采用Vue3+Element Plus实现响应式界面,后端基于Spring Boot构建微服务架构,数据库使用MySQL配合Redis缓存,权限控制采用RBAC模型。特别值得一提的是,在库存管理模块我们实现了分布式事务处理,确保在多仓库调拨场景下数据的一致性。下面我就从架构设计开始,逐步拆解各环节的实现细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 前后端分离架构设计解析
2.1 技术选型与架构图
前后端分离不是简单地把项目分成两部分开发,而是需要建立完整的协作规范。我们的架构方案是:
- 前端:Vue3 + Vite + Pinia + Element Plus
- 后端:Spring Boot 2.7 + MyBatis-Plus + Spring Security
- 网关:Nginx反向代理 + JWT鉴权
- 部署:Docker容器化
mermaid复制graph TD
A[客户端] -->|HTTPS| B[Nginx]
B -->|API路由| C[前端静态资源]
B -->|/api/*| D[Spring Boot]
D --> E[MySQL]
D --> F[Redis]
D --> G[MinIO]
注意:生产环境务必启用HTTPS,JWT token建议采用HS512算法并设置合理过期时间
2.2 接口规范设计
前后端协作的核心在于明确的接口契约。我们定义的规范包括:
- 响应格式标准化:
json复制{
"code": 200,
"message": "success",
"data": {...},
"timestamp": 1689132000
}
- 错误码体系:
- 4xx 客户端错误(400参数错误,401未授权,403禁止访问)
- 5xx 服务端错误(500系统异常,503服务不可用)
- API版本控制:
通过请求头Accept: application/vnd.erp.v1+json实现
在Spring Boot中通过@RestControllerAdvice统一处理响应格式:
java复制@RestControllerAdvice
public class ResponseWrapper implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return !returnType.getParameterType().isAssignableFrom(Result.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
return Result.success(body);
}
}
3. 核心业务模块实现
3.1 采购管理模块
采购流程包含:供应商管理→采购申请→询价比价→采购订单→入库验收→财务付款。关键实现点:
- 状态机设计:
java复制public enum PurchaseStatus {
DRAFT("草稿"),
APPROVING("审批中"),
APPROVED("已批准"),
ORDERED("已下单"),
PART_RECEIVED("部分到货"),
COMPLETED("已完成"),
CANCELLED("已取消");
// 状态流转校验逻辑
public static boolean canTransfer(PurchaseStatus from, PurchaseStatus to) {
// 具体校验规则...
}
}
- 并发控制方案:
- 使用MySQL乐观锁防止超额采购
sql复制UPDATE purchase_order
SET quantity = quantity - #{applyNum},
version = version + 1
WHERE id = #{orderId} AND version = #{version}
- 审批流集成:
通过Activiti引擎实现多级审批,关键配置:
xml复制<process id="purchaseApproval" name="采购审批流程">
<startEvent id="start"/>
<userTask id="deptLeaderAudit" name="部门领导审批"/>
<userTask id="financeAudit" name="财务审批"
activiti:candidateGroups="finance"/>
<endEvent id="end"/>
<sequenceFlow sourceRef="start" targetRef="deptLeaderAudit"/>
<sequenceFlow sourceRef="deptLeaderAudit" targetRef="financeAudit"/>
<sequenceFlow sourceRef="financeAudit" targetRef="end"/>
</process>
3.2 库存管理模块
库存管理是ERP系统的核心,我们实现了:
- 实时库存计算:
sql复制-- 采用移动加权平均法计算成本
UPDATE inventory
SET stock = stock + #{changeNum},
total_cost = total_cost + #{incomingCost},
avg_cost = (total_cost + #{incomingCost}) / (stock + #{changeNum})
WHERE sku_id = #{skuId} AND warehouse_id = #{warehouseId}
- 分布式事务处理(多仓库调拨场景):
java复制@DSTransactional
public void transferStock(TransferDTO dto) {
// 1. 扣减源仓库库存
inventoryMapper.reduceStock(dto.getFromWarehouse(),
dto.getSkuId(),
dto.getQuantity());
// 2. 增加目标仓库库存
inventoryMapper.addStock(dto.getToWarehouse(),
dto.getSkuId(),
dto.getQuantity());
// 3. 记录调拨单
transferMapper.insert(dto.toTransferOrder());
}
- 库存预警实现方案:
java复制@Scheduled(cron = "0 0 9 * * ?") // 每天9点执行
public void checkInventoryWarning() {
List<InventoryWarning> warnings = inventoryMapper.selectLowStockItems();
warnings.forEach(warning -> {
String message = String.format("商品[%s]库存不足,当前%d,安全库存%d",
warning.getSkuName(),
warning.getCurrentStock(),
warning.getSafetyStock());
alertService.sendTo(warning.getManagerId(), message);
});
}
4. 权限系统与安全控制
4.1 RBAC模型实现
基于角色的访问控制(RBAC)包含五个核心表:
- sys_user(用户表)
- sys_role(角色表)
- sys_menu(菜单权限表)
- sys_user_role(用户角色关联表)
- sys_role_menu(角色权限关联表)
权限校验流程:
java复制@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/login").permitAll()
.antMatchers("/api/**").authenticated()
.anyRequest().access("@rbacService.hasPermission(request,authentication)")
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.csrf().disable();
}
4.2 数据权限设计
除功能权限外,还需控制数据可见范围(如:销售员只能看自己的客户):
sql复制SELECT * FROM sales_order
WHERE 1=1
<if test="!hasRole('admin')">
AND sales_id = #{currentUserId}
</if>
在MyBatis中通过拦截器自动注入数据权限SQL:
java复制@Intercepts({
@Signature(type= Executor.class, method="query",
args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DataPermissionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 解析当前用户权限
// 修改SQL添加数据过滤条件
return invocation.proceed();
}
}
5. 系统部署与性能优化
5.1 容器化部署方案
使用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: erp@1234
volumes:
- ./mysql/data:/var/lib/mysql
redis:
image: redis:alpine
ports:
- "6379:6379"
backend:
build: ./erp-server
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./erp-web
ports:
- "80:80"
5.2 性能优化实践
- 缓存策略:
java复制@Cacheable(value = "product", key = "#skuId",
unless = "#result == null")
public Product getBySku(String skuId) {
return productMapper.selectBySku(skuId);
}
@CacheEvict(value = "product", key = "#product.skuId")
public void updateProduct(Product product) {
productMapper.updateById(product);
}
- 数据库优化:
- 为高频查询字段建立索引
sql复制CREATE INDEX idx_sku_warehouse ON inventory(sku_id, warehouse_id);
- 大表分库分表策略(按仓库ID哈希分片)
- 前端性能优化:
- 路由懒加载
javascript复制const routes = [
{
path: '/purchase',
component: () => import('./views/Purchase.vue')
}
]
- API请求防抖处理
javascript复制import { debounce } from 'lodash';
methods: {
search: debounce(function(keyword) {
this.$api.get('/api/product', { params: { keyword } })
}, 500)
}
6. 开发中的典型问题与解决方案
6.1 前后端数据同步问题
场景:采购单审批通过后,需要实时更新前端状态
解决方案:采用WebSocket推送状态变更
java复制@RestController
@RequestMapping("/ws")
public class WsController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@PostMapping("/approve/{orderId}")
public Result approveOrder(@PathVariable String orderId) {
// 审批逻辑...
messagingTemplate.convertAndSend(
"/topic/order/" + orderId,
new WsMessage("STATUS_UPDATED", newStatus));
return Result.success();
}
}
前端订阅代码:
javascript复制const socket = new SockJS('/ws-endpoint');
const client = Stomp.over(socket);
client.connect({}, () => {
client.subscribe(`/topic/order/${orderId}`, (message) => {
const msg = JSON.parse(message.body);
if(msg.type === 'STATUS_UPDATED') {
this.status = msg.data;
}
});
});
6.2 报表导出性能优化
当导出大量数据时容易导致内存溢出,解决方案:
- 使用POI的SXSSFWorkbook流式导出
java复制try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
Sheet sheet = workbook.createSheet("销售报表");
// 分批查询数据
int page = 1;
while (true) {
List<SalesData> batch = salesMapper.exportBatch(page, 1000);
if (batch.isEmpty()) break;
// 写入行数据
batch.forEach(data -> {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(data.getOrderNo());
// 其他单元格...
});
page++;
}
workbook.write(response.getOutputStream());
}
- 加入导出任务队列,支持异步导出和进度查询
java复制@GetMapping("/export/progress/{taskId}")
public Result getExportProgress(@PathVariable String taskId) {
ExportTask task = taskService.getById(taskId);
return Result.success(task.getProgress());
}
7. 扩展功能与二次开发建议
7.1 移动端适配方案
基于uniapp的跨端解决方案:
- 共用API接口层
- 条件编译处理平台差异
javascript复制// #ifdef H5
import erp from '@/api/web.js'
// #endif
// #ifdef MP-WEIXIN
import erp from '@/api/wechat.js'
// #endif
7.2 第三方系统集成
- 微信支付对接:
java复制public PaymentResult wechatPay(PaymentRequest request) {
WxPayUnifiedOrderRequest req = new WxPayUnifiedOrderRequest();
req.setBody(request.getDescription());
req.setOutTradeNo(request.getOrderNo());
req.setTotalFee(request.getAmount());
req.setSpbillCreateIp(request.getClientIp());
req.setNotifyUrl(config.getNotifyUrl());
try {
WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(req);
return PaymentResult.success(result.getMwebUrl());
} catch (WxPayException e) {
log.error("微信支付失败", e);
return PaymentResult.fail(e.getReturnMsg());
}
}
- 电子发票接口对接:
- 实现金税盘开票接口调用
- 发票数据归档存储
7.3 数据分析扩展
基于Apache ECharts实现销售看板:
javascript复制this.chart = echarts.init(this.$refs.chart);
this.chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['销售额', '利润'] },
xAxis: { type: 'category', data: months },
yAxis: { type: 'value' },
series: [
{ name: '销售额', type: 'line', data: salesData },
{ name: '利润', type: 'bar', data: profitData }
]
});
在开发这套系统的过程中,最深的体会是:业务复杂度远高于技术实现。比如库存周转率的计算逻辑,不同行业有不同算法,需要与客户充分沟通。建议在开发初期就建立完善的领域模型,使用DDD方法划分明确的限界上下文,这对后期维护扩展至关重要。
