从零构建一个企业级 ERP 系统:.NET 8 + Vue 全栈实战指南
这不是一篇普通的 CRUD 教程而是一个完整的技术成长实录——从建表到分布式锁从三层架构到接口解耦从审计日志到熔断降级每一步都是真实踩坑后的沉淀。一、写在前面为什么要造这个轮子很多 .NET 开发者工作多年依然停留在“复制粘贴 Controller-Service-Repository”的层面对事务、并发、业务闭环、架构演进缺乏系统性认知。我决定从零写一个 Mini ERP 系统目标不是商业落地而是用代码打通整个后端知识体系。技术栈选型如下后端ASP.NET Core 8 EF Core SQL Server前端Vue 3 Element Plus辅助展示架构三层架构Domain / Application / Infrastructure / API 依赖注入解耦高级特性分布式锁Redis、审计日志Interceptor、发件箱模式Outbox、熔断重试Polly、CQRS 读写分离、容器化部署二、第一阶段打好地基三层架构 DI 解耦2.1 项目分层不是文件夹是类库很多初学者把三层架构做成“文件夹”那是伪分层。真正的分层必须通过项目引用强制隔离textMyERP.sln ├── MyERP.Domain # 实体、枚举纯 C#无任何外部依赖 ├── MyERP.Infrastructure # DbContext、Repository、EF Core 迁移 ├── MyERP.Application # DTO、Service 业务逻辑、接口定义 └── MyERP.API # Controller、JWT 鉴权、Program.cs 启动项依赖方向API → Application → Infrastructure → Domain依赖倒置2.2 最重要的一步面向接口编程DI 解耦绝大多数初学者写的 Service 是“裸类”csharp// ❌ 错误写法没有接口 public class InventoryService { ... } public class InventoryController(InventoryService svc) { ... }一旦需要换成另一个实现比如从 SQL Server 换成 Redis 缓存所有 Controller 都要改。正确写法提取接口csharp// ✅ 接口定义 public interface IInventoryService { Taskstring StockInAsync(StockInRequest request); Taskstring StockOutAsync(StockOutRequest request); } // ✅ 实现类 public class InventoryService : IInventoryService { ... } // ✅ Controller 依赖接口 public class InventoryController(IInventoryService inventoryService) { ... } // ✅ DI 注册 builder.Services.AddScopedIInventoryService, InventoryService();价值换实现只需改一行注册代码符合依赖倒置原则DIP同时也是单元测试 Mock 的基础。三、第二阶段核心业务闭环ERP 的灵魂3.1 数据库设计核心表表名作用关键字段Base_Item物料成品/原材料ItemCode,ItemTypeBase_Warehouse仓库WarehouseCodeBase_Partner客户/供应商PartnerCode,TypeInv_Stock实时库存快照AvailableQty,LockedQty,Version乐观锁Inv_Transaction库存流水只增不改Direction,BeforeQty,AfterQtySales_Order销售订单Status待审核/已审核/生产中/已发货/已完成Production_Order生产工单Status待发料/进行中/已完工/已入库Production_OrderDetail工单 BOM 明细RequiredQty,IssuedQty3.2 业务闭环流程核心中的核心这是整个 ERP 最有价值的部分——需求驱动生产text销售下单待审核 ↓ 审核通过 → 自动生成生产工单待发料 ↓ 生产领料 → 扣减原材料库存工单状态变为“进行中” ↓ 生产完工 → 成品入库工单状态变为“已完工”关键代码片段销售审核自动生成工单csharppublic async Taskbool ApproveAsync(long salesOrderId) { using var transaction await _context.Database.BeginTransactionAsync(); try { // 1. 更新销售订单状态 order.Status SalesOrderStatus.Approved; // 2. 自动生成生产工单 var prodOrder new ProductionOrder { OrderNo GenerateOrderNo(), ItemId order.ItemId, Quantity order.TotalQty, Status ProductionOrderStatus.PendingMaterial }; _context.ProductionOrders.Add(prodOrder); // 3. 展开 BOM 生成工单明细 foreach (var bom in GetBOM(order.ItemId)) { _context.ProductionOrderDetails.Add(new ProductionOrderDetail { ProductionOrderId prodOrder.Id, ItemId bom.MaterialItemId, RequiredQty bom.RequiredQty * order.TotalQty }); } await _context.SaveChangesAsync(); await transaction.CommitAsync(); return true; } catch { await transaction.RollbackAsync(); throw; } }3.3 库存扣减乐观锁防并发超卖这是 ERP 最核心的技术难点——多个请求同时扣同一个物料的库存。csharppublic async Taskstring StockOutAsync(StockOutRequest request) { // 1. 查询库存并记录旧版本号 var stock await _context.InvStocks.FindAsync(request.ItemId, request.WarehouseId); var oldVersion stock.Version; var beforeQty stock.AvailableQty; var afterQty stock.AvailableQty - request.Quantity; // 2. 乐观锁更新WHERE 条件带上 Version var rows await _context.InvStocks .Where(s s.Id stock.Id s.Version oldVersion) .ExecuteUpdateAsync(setters setters .SetProperty(s s.AvailableQty, afterQty) .SetProperty(s s.Version, oldVersion 1) ); // 3. 如果影响行数为 0说明被其他请求修改了 if (rows 0) throw new Exception(库存已被修改请重试); // 4. 写入流水审计 _context.InvTransactions.Add(new InvTransaction { ItemId request.ItemId, Direction Direction.Out, BeforeQty beforeQty, AfterQty afterQty }); await _context.SaveChangesAsync(); }面试常问为什么不用悲观锁SELECT ... FOR UPDATE因为乐观锁在冲突率低的场景下性能更好且不会产生死锁。四、第三阶段架构进阶从“能用”到“好用”4.1 审计日志利用 EF Core Interceptor 自动记录不写任何业务代码自动捕获所有实体变更csharppublic class AuditInterceptor : SaveChangesInterceptor { public override async ValueTaskInterceptionResultint SavingChangesAsync( DbContextEventData eventData, InterceptionResultint result, CancellationToken cancellationToken default) { var entries eventData.Context.ChangeTracker.Entries() .Where(e e.State EntityState.Modified || e.State EntityState.Added); foreach (var entry in entries) { // 记录旧值、新值、操作人、时间戳 var audit new AuditLog { TableName entry.Entity.GetType().Name, Action entry.State.ToString(), OldValues JsonSerializer.Serialize(entry.OriginalValues.Properties.ToDictionary(...)), NewValues JsonSerializer.Serialize(entry.CurrentValues.Properties.ToDictionary(...)), ChangedBy GetCurrentUserId(), ChangedAt DateTime.UtcNow }; eventData.Context.Add(audit); } return await base.SavingChangesAsync(eventData, result, cancellationToken); } }价值财务/老板最看重的功能——数据追溯。谁在什么时间改了哪个字段一目了然。4.2 分布式锁 幂等性Redis乐观锁解决了并发冲突但如果请求重试会重复扣库存。需要幂等令牌csharppublic async Taskstring StockOutAsync(StockOutRequest request) { // 1. 幂等性检查 var idempotentKey $Idempotent:{request.RequestId}; if (await _redis.StringGetAsync(idempotentKey) 1) return await GetCachedResult(request.RequestId); // 2. 分布式锁Key 物料仓库 var lockKey $Lock:Inventory:{request.ItemId}:{request.WarehouseId}; using var redisLock await _redis.LockTakeAsync(lockKey, ...); if (!redisLock.Acquired) throw new Exception(系统繁忙请稍后重试); // 3. 执行扣库存... await ExecuteStockOutAsync(request); // 4. 标记幂等 await _redis.StringSetAsync(idempotentKey, 1, TimeSpan.FromHours(24)); }价值工业级扣库存方案 幂等性 分布式锁 乐观锁三重保障。4.3 熔断与重试Polly当系统调用外部 API第三方物流、短信网关时必须加保护csharpbuilder.Services.AddHttpClient(ExternalApi, client ...) .AddPolicyHandler(HttpPolicyExtensions .HandleTransientHttpError() .WaitAndRetryAsync(3, retry TimeSpan.FromSeconds(Math.Pow(2, retry)))) // 指数退避 .AddPolicyHandler(PolicyHttpResponseMessage .HandleException() .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30))); // 连续失败5次熔断30秒面试高频题熔断和重试的区别重试是“再试一次”熔断是“别试了赶紧失败”防止级联故障。4.4 发件箱模式Outbox Pattern—— 最终一致性如果用 MediatR 解耦业务可能出现“订单状态改了但事件没发出去”的问题。解决方案把“发事件”变成数据库事务的一部分sql-- 在同一事务中 UPDATE Sales_Order SET Status Approved WHERE Id 1; INSERT INTO Outbox_Messages (EventType, Payload, CreatedAt) VALUES (OrderApproved, {OrderId:1}, GETDATE());后台轮询扫描Outbox_Messages表发布成功后标记ProcessedAt。价值微服务最终一致性的黄金标准保证 100% 数据不丢。五、第四阶段数据库与性能优化5.1 CQRS 读写分离报表查询Group By几百万条流水会阻塞主库事务。引入 CQRS操作数据库ORM写入Command主库MasterEF Core读取Query只读副本SlaveDapper 原生 SQLcsharp// 写入走 EF Core public class InventoryService(AppDbContext context) : IInventoryService // 查询走 Dapper public class ReportService(IDbConnection slaveConnection) : IReportService { public async TaskListStockLedger GetLedgerAsync(...) { return await slaveConnection.QueryAsyncStockLedger(sql, param); } }5.2 冷热数据分离Inv_Transaction会无限增长超过千万级后报表超时。自动归档策略保留 2 年在线数据每月凌晨将过期数据迁移到Inv_Transaction_Archive分区表报表查询时根据日期范围自动路由六、第五阶段容器化与可观测性6.1 Docker Compose 一键启动yamlversion: 3.8 services: sqlserver: image: mcr.microsoft.com/mssql/server:2022-latest environment: SA_PASSWORD: Your_password123 ACCEPT_EULA: Y ports: - 1433:1433 redis: image: redis:alpine ports: - 6379:6379 myerp-api: build: . ports: - 8080:8080 depends_on: - sqlserver - redis6.2 可观测性三件套Metrics使用 Prometheus OpenTelemetry 收集接口耗时、错误率Tracing给每个请求生成 TraceId跨服务串联日志HealthCheck暴露/health接口配合云服务商实现自动告警七、写在最后我的技术栈清单CSDN 读者可自检分类技能点状态基础架构三层架构 DI 接口解耦✅数据库EF Core SQL Server 迁移管理✅认证授权JWT 角色权限 动态用户上下文✅并发控制乐观锁 分布式锁(Redis) 幂等性✅业务闭环销售→工单→领料→完工入库✅审计追溯EF Core Interceptor 自动审计日志✅系统弹性Polly 熔断/重试/超时✅最终一致性发件箱模式Outbox✅性能优化CQRS 读写分离 冷热数据归档✅测试Testcontainers 集成测试✅部署Docker K8s 编排✅可观测性OpenTelemetry Prometheus HealthCheck✅前端Vue 3 Element Plus✅八、给读者的建议先跑通业务闭环不要上来就搞分布式锁先把销售→生产→入库跑通。接口先行先写接口定义IxxxService再写实现养成面向接口编程的习惯。事务是底线涉及多表操作永远用BeginTransaction包裹。日志是命根子审计日志 操作日志 异常日志一个都不能少。不要过度设计小型项目不需要 CQRS 和发件箱但在架构层面知道它们的存在是进阶的起点。

相关新闻

Linux Ubuntu与安卓设备文件传输全方案:从USB到无线实战指南

Linux Ubuntu与安卓设备文件传输全方案:从USB到无线实战指南

1. 跨系统文件交换:一个高频且真实的痛点如果你同时使用Linux Ubuntu桌面系统和安卓手机,那么在两台设备之间传文件这件事,大概率是你日常工作中的高频操作。可能是把手机拍的照片、录的视频传到电脑上剪辑处理,也可能是把电脑上写…

2026/10/18 9:59:42 阅读更多 →
华为TCX转换器:打破运动数据孤岛的高效实用工具

华为TCX转换器:打破运动数据孤岛的高效实用工具

华为TCX转换器:打破运动数据孤岛的高效实用工具 【免费下载链接】Huawei-TCX-Converter A makeshift python tool that generates TCX files from Huawei HiTrack files 项目地址: https://gitcode.com/gh_mirrors/hu/Huawei-TCX-Converter 华为TCX转换器是一…

2026/10/19 13:27:53 阅读更多 →
解码生命暗物质:从无序蛋白到人类IDRome的分子语法与实战分析

解码生命暗物质:从无序蛋白到人类IDRome的分子语法与实战分析

1. 项目概述:从“垃圾”到“宝藏”的认知革命如果你在几年前问我,细胞内那些结构松散、像一团乱麻的蛋白质片段有什么用,我大概率会耸耸肩,把它们归为“分子垃圾”或“进化残留物”。这几乎是当时整个领域的共识。然而&#xff0c…

2026/10/17 10:57:33 阅读更多 →

最新新闻

基于时空图神经网络与条件正规化流的AUV自适应路径规划

基于时空图神经网络与条件正规化流的AUV自适应路径规划

在无人潜航器这个圈子里摸爬滚打这些年,我越来越觉得一个道理是绕不过去的:水下机器人真正难的,往往不是机械和通讯,而是“它在水底下到底知不知道自己在哪儿、前面是什么”。光靠惯导和 DVL(多普勒计程仪)…

2026/10/19 17:29:13 阅读更多 →
开源代码审查框架 open-code-review:基于 diff 的确定性规则引擎

开源代码审查框架 open-code-review:基于 diff 的确定性规则引擎

先说结论:做 open-code-review 的初衷,不是想替代 CodeRabbit、Sweep 或者市面上一堆 AI Code Review 服务,而是受够了每次提 PR 都要等外部服务回调、受够了规则模板无法和团队内部规范打通。这个项目定位是"可以长期跑在自己 CI 里的开…

2026/10/19 17:29:13 阅读更多 →
open-code-review:基于CLI+Git Hook的轻量级AI代码审查工具

open-code-review:基于CLI+Git Hook的轻量级AI代码审查工具

1. 项目概述:这不是又一个“AI写代码”玩具,而是一套嵌入开发流程的轻量级代码审查协作者“open-code-review”这个名字乍一听像某个开源库的GitHub仓库名,但拆开来看——open(开放)、code(代码&#xff09…

2026/10/19 17:29:13 阅读更多 →
BP神经网络多输入单输出回归预测与K折交叉验证Matlab实现

BP神经网络多输入单输出回归预测与K折交叉验证Matlab实现

搞回归预测的伙伴应该都被问过这么一个问题:你的模型到底是靠什么给出的预测结果?尤其是那种输入七八个变量、输出一个值的问题,线性回归解释不了,决策树又怕过拟合,这时候很多人第一反应就是BP神经网络。多输入单输出…

2026/10/19 17:29:13 阅读更多 →
RGB-D点云融合实战:三帧图像拼接完整点云的原理与实现

RGB-D点云融合实战:三帧图像拼接完整点云的原理与实现

简介:一份面向三维重建与计算机视觉学习者的点云融合实践项目,对应作业13「多帧点云融合」实验。项目基于RGB-D相机拍摄的不连续三帧图像,借助44齐次变换矩阵,逐步完成点云生成、坐标变换、ICP配准与融合,直观展示多帧…

2026/10/19 17:29:13 阅读更多 →
open-code-review:CLI驱动的开放代码审查范式

open-code-review:CLI驱动的开放代码审查范式

1. “open-code-review”不是工具名,而是正在发生的协作范式迁移 你最近在 GitHub 提交 PR 后,是不是发现评论区里多了一条带 🤖 图标的自动反馈?或者某天早上打开终端, git diff --cached | open-code-review 这行命…

2026/10/19 17:28:05 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →