资讯动态

ASP.NET Core供应商管理系统实战:数据模型、CRUD接口与部署避坑

发布时间:2026/10/14 16:38:10 来源:尧图企业网站定制
简介这是一套面向计算机专业毕业设计场景的ASP.NET供应商管理系统源码基于C#与ASP.NET框架开发适合正在准备毕设或需要学习Web应用完整实现的学生与开发者。系统围绕企业供应链管理展开涵盖供应商分类、信息录入与查询、评价打分、留言沟通以及用户权限分配等核心模块能够帮助读者理解从需求到落地的整体设计思路。压缩包共386个文件以gif界面素材、js脚本、html页面、css样式为主另有12个cs源文件、7个aspx页面及master母版、dll程序集、mdf与ldf数据库文件等完整呈现了前后端与数据层的组织方式包体约1.77MB。目前已有388人学习浏览。借助这套源码读者可参考其MVC分层结构、数据库表设计与权限控制逻辑快速搭建可运行的毕设原型并在此基础上进行功能扩展与代码优化。1. 从一张 Excel 台账说起ASP.NET 供应商管理系统到底在管什么很多中小企业的采购部门供应商信息最初都躺在几张 Excel 里一张记联系方式一张记资质证书到期日一张记历史报价。平时看着还行一旦要查“这家供应商的营业执照下个月是不是到期了”“去年这家供货的合格率是多少”就得三张表来回翻翻完还得靠人脑对齐。ASP.NET 供应商管理系统要解决的就是把这套靠人肉维护的台账变成一个能查、能提醒、能留痕的 Web 应用。它面向的是采购、质检、财务三个角色采购负责录入和比价质检负责打分财务负责对账。技术栈上ASP.NET 这条线目前主流是 ASP.NET Core Entity Framework Core 关系型数据库前端可以是 Razor Pages 也可以是前后端分离。适合谁做适合已经会一点 C#、想拿一个真实业务场景练手全栈的开发者也适合企业内部想自建轻量采购协同工具的团队。这一章先把边界划清楚后面几章再落到表结构、接口和部署。2. 供应商管理系统的数据模型怎么设计才不返工数据模型是这类系统最容易返工的地方。我见过不少项目第一版把供应商、联系人、资质全塞进一张表字段里用逗号分隔多个联系人等到要做“资质到期提醒”时才发现根本没法查。所以这一章先把实体拆清楚再谈代码。2.1 核心实体与关系供应商、联系人、资质、报价、评价一个能撑住业务的模型至少要有五张核心表。供应商主表存企业级信息联系人单独一张表因为一个供应商可能有多个对接人资质证书单独一张表因为要跟踪有效期报价记录单独一张表因为要留历史评价记录单独一张表因为质检要按批次打分。实体关键字段关系SupplierId, Name, TaxNo, Status, CreatedAt一对多联系人、资质、报价、评价ContactId, SupplierId, Name, Phone, Role多对一 SupplierQualificationId, SupplierId, CertType, CertNo, ExpireDate多对一 SupplierQuoteId, SupplierId, MaterialId, Price, QuoteDate多对一 SupplierEvaluationId, SupplierId, BatchNo, Score, Comment多对一 Supplier这里有个选型理由要说清楚为什么资质和报价不合并进主表因为它们的数量是不定的一个供应商可能有 3 张证书、200 条报价塞进主表会导致行宽爆炸查询时拖着一堆用不上的列。拆表之后列表页只查主表详情页再按需加载子表性能差别很明显。状态字段 Status 我一般用枚举而不是布尔值因为供应商不止“启用/禁用”两种状态还有“待审核”“已冻结”“黑名单”。用枚举存整数前端映射成中文后面做权限过滤时直接按状态筛。2.2 用 EF Core 的 Code First 把表建出来模型定好之后用 Entity Framework Core 的 Code First 方式生成表结构比手写 SQL 更不容易漏字段。下面是一个精简版的实体定义和 DbContext 配置。// Supplier.cs —— 供应商主实体 public class Supplier { public int Id { get; set; } public string Name { get; set; } string.Empty; // 企业名称 public string TaxNo { get; set; } string.Empty; // 统一社会信用代码 public SupplierStatus Status { get; set; } // 枚举状态 public DateTime CreatedAt { get; set; } // 导航属性一个供应商挂多个子记录 public ListContact Contacts { get; set; } new(); public ListQualification Qualifications { get; set; } new(); public ListQuote Quotes { get; set; } new(); public ListEvaluation Evaluations { get; set; } new(); } public enum SupplierStatus { Pending 0, // 待审核 Active 1, // 正常合作 Frozen 2, // 已冻结 Blacklist 3 // 黑名单 }// AppDbContext.cs —— 配置表名和索引 public class AppDbContext : DbContext { public DbSetSupplier Suppliers SetSupplier(); public DbSetContact Contacts SetContact(); public DbSetQualification Qualifications SetQualification(); protected override void OnModelCreating(ModelBuilder modelBuilder) { // 供应商名称加唯一索引防止重复录入 modelBuilder.EntitySupplier() .HasIndex(s s.Name) .IsUnique(); // 资质到期日加普通索引方便做到期扫描 modelBuilder.EntityQualification() .HasIndex(q q.ExpireDate); // 删除供应商时级联删除联系人资质则限制删除 modelBuilder.EntitySupplier() .HasMany(s s.Contacts) .WithOne() .OnDelete(DeleteBehavior.Cascade); } }逻辑说明HasIndex那两行是性能关键。供应商名称唯一索引能挡住重复录入资质到期日索引能让“查 30 天内到期证书”的查询走索引而不是全表扫。OnDelete的配置要按业务定联系人跟着供应商走可以级联删但资质证书是审计凭证我一般设成Restrict防止误删主表时把证书记录一起干掉。参数说明SupplierStatus的枚举值一旦上线就不要改数字只能追加。因为数据库里存的是整数改数字等于把历史数据的状态全打乱。如果非要调整加一个映射表或者用字符串存状态但字符串存状态在筛选时性能差一些自己权衡。2.3 迁移命令与种子数据模型写完后用迁移命令生成数据库。下面这几条是标准流程注意--output-dir把迁移文件归到单独目录不然项目根目录会越来越乱。# 安装 EF Core 工具全局一次即可 dotnet tool install --global dotnet-ef # 在项目目录下创建迁移 dotnet ef migrations add InitSupplierSchema --output-dir Data/Migrations # 应用到数据库 dotnet ef database update种子数据我建议单独写一个DbSeeder类在开发环境启动时调用塞几条测试供应商和资质。生产环境不要自动跑种子否则每次部署都可能重复插入。判断是否已存在用if (!context.Suppliers.Any())包一层避免重复。3. 用 ASP.NET Core 把供应商增删改查接口跑通模型有了接下来是接口。这一章按“先跑通最小闭环再补校验和分页”的顺序来每一步都能单独验证。3.1 最小 Web API三个接口撑起列表、详情、新增先用最小的 Controller 把主流程跑通。不要一上来就搞 CQRS、MediatR那些是规模上来之后的优化早期只会拖慢进度。[ApiController] [Route(api/[controller])] public class SuppliersController : ControllerBase { private readonly AppDbContext _db; public SuppliersController(AppDbContext db) _db db; // GET api/suppliers?page1size20keyword某某 [HttpGet] public async TaskIActionResult GetList( int page 1, int size 20, string? keyword null) { var query _db.Suppliers.AsNoTracking(); // 关键词模糊匹配名称或税号 if (!string.IsNullOrWhiteSpace(keyword)) query query.Where(s s.Name.Contains(keyword) || s.TaxNo.Contains(keyword)); var total await query.CountAsync(); var items await query .OrderByDescending(s s.CreatedAt) .Skip((page - 1) * size) .Take(size) .Select(s new { s.Id, s.Name, s.TaxNo, Status s.Status.ToString() }) .ToListAsync(); return Ok(new { total, page, size, items }); } // GET api/suppliers/5 [HttpGet({id:int})] public async TaskIActionResult GetById(int id) { var supplier await _db.Suppliers .Include(s s.Contacts) .Include(s s.Qualifications) .FirstOrDefaultAsync(s s.Id id); return supplier is null ? NotFound() : Ok(supplier); } // POST api/suppliers [HttpPost] public async TaskIActionResult Create([FromBody] SupplierCreateDto dto) { // 名称重复直接挡掉靠唯一索引兜底 if (await _db.Suppliers.AnyAsync(s s.Name dto.Name)) return Conflict(供应商名称已存在); var supplier new Supplier { Name dto.Name, TaxNo dto.TaxNo, Status SupplierStatus.Pending, CreatedAt DateTime.UtcNow }; _db.Suppliers.Add(supplier); await _db.SaveChangesAsync(); return CreatedAtAction(nameof(GetById), new { id supplier.Id }, supplier); } }逻辑说明列表接口用AsNoTracking()是因为查询不需要跟踪实体状态能省内存。分页用Skip/Take注意Skip的值是(page-1)*sizepage 从 1 开始。详情接口用Include把子表带出来但列表接口不要 Include否则数据量一大就拖慢。新增接口先查重再插入虽然数据库有唯一索引兜底但提前返回Conflict能给前端更友好的提示。参数说明page和size要做上限保护size超过 100 就该拒绝否则有人传size100000能把数据库拖垮。我一般加一个if (size 100) size 100;。keyword用Contains生成的是LIKE %关键词%数据量上百万时这个查询会慢后面第 5 章会讲怎么优化。3.2 参数校验与统一返回格式接口能跑通之后马上补校验。ASP.NET Core 内置了 DataAnnotations配合[ApiController]会自动返回 400不用自己写 if。public class SupplierCreateDto { [Required(ErrorMessage 供应商名称必填)] [StringLength(100, MinimumLength 2)] public string Name { get; set; } string.Empty; [Required] [RegularExpression(^[0-9A-Z]{15,18}$, ErrorMessage 统一社会信用代码格式不正确)] public string TaxNo { get; set; } string.Empty; }逻辑说明[ApiController]特性会自动触发模型验证验证失败时返回的 400 响应里带errors字段前端可以直接展示。RegularExpression那条是血泪经验——统一社会信用代码是 18 位但有些老数据是 15 位所以正则写成{15,18}而不是{18}不然历史数据导入时会被挡。统一返回格式我一般包一层ApiResponseT字段就code、message、data三个。这样前端拦截器只判断code就行不用每个接口单独处理。但注意HTTP 状态码还是要用对不要所有响应都返回 200 然后在 body 里写错误码那样监控和网关没法按状态码做告警。3.3 资质到期提醒的后台任务供应商管理里最实用的功能之一是资质到期提醒。做法是注册一个IHostedService每天凌晨扫一遍 30 天内到期的证书生成待办记录。public class QualificationScanService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; public QualificationScanService(IServiceScopeFactory scopeFactory) _scopeFactory scopeFactory; protected override async Task ExecuteAsync(CancellationToken token) { while (!token.IsCancellationRequested) { using var scope _scopeFactory.CreateScope(); var db scope.ServiceProvider.GetRequiredServiceAppDbContext(); var deadline DateTime.UtcNow.AddDays(30); var expiring await db.Qualifications .Where(q q.ExpireDate deadline q.ExpireDate DateTime.UtcNow) .ToListAsync(token); // 这里把 expiring 写入待办表或发通知 // 具体通知渠道按企业实际情况接 // 每天跑一次睡 24 小时 await Task.Delay(TimeSpan.FromHours(24), token); } } }逻辑说明BackgroundService里不能用构造函数注入的DbContext因为它是 Scoped 的而后台任务是 Singleton。所以用IServiceScopeFactory手动开 scope这是标准做法。扫描范围是“今天到 30 天后”不含已过期的已过期的应该单独一个逻辑处理。参数说明AddDays(30)这个 30 天是提醒窗口实际项目里我见过设 60 天的因为有些资质续期流程要走一个多月。这个值建议做成配置项不要硬编码。Task.Delay的 24 小时是轮询间隔如果对实时性要求高可以改成每小时但要注意数据库压力。4. 权限、审计与部署上线前必须补的三块功能跑通不等于能上线。这一章讲三块上线前必须补的东西缺任何一块运维和审计都会找上门。4.1 基于角色的权限控制供应商系统至少要有三个角色采购员能增改质检员只能看和评价管理员全权限。ASP.NET Core 的[Authorize]配合角色声明就能实现。[Authorize(Roles Purchaser,Admin)] [HttpPost] public async TaskIActionResult Create([FromBody] SupplierCreateDto dto) { // 只有采购和管理员能新增 // ... } [Authorize(Roles Inspector,Admin)] [HttpPost({id:int}/evaluations)] public async TaskIActionResult AddEvaluation(int id, EvaluationDto dto) { // 只有质检和管理员能打分 // ... }逻辑说明角色名用常量类管理不要散落在各处写字符串改起来容易漏。[Authorize]加在 Controller 级别做默认拦截个别公开接口用[AllowAnonymous]放开。角色和权限的映射关系建议存数据库而不是硬编码在代码里因为企业组织架构会变。参数说明如果角色超过 5 个就该上基于策略的授权Policy-based用AddAuthorization注册策略每个策略对应一组角色或声明。角色少的时候Roles A,B够用多了之后字符串会很长且难维护。4.2 操作审计日志审计日志不是可选项。供应商信息被谁改了、什么时候改的、改前改后是什么这些在出问题时是唯一的后悔药。做法可以简单到在SaveChanges时拦截。public override async Taskint SaveChangesAsync( CancellationToken token default) { // 找出所有被修改的实体 var modified ChangeTracker.Entries() .Where(e e.State EntityState.Modified || e.State EntityState.Added || e.State EntityState.Deleted) .ToList(); foreach (var entry in modified) { // 把变更写入审计表字段包括实体名、操作类型、 // 操作人、时间、变更前后的 JSON 快照 AuditLogs.Add(new AuditLog { EntityName entry.Entity.GetType().Name, Action entry.State.ToString(), Operator _currentUser?.Name ?? system, Timestamp DateTime.UtcNow, Snapshot JsonSerializer.Serialize(entry.Entity) }); } return await base.SaveChangesAsync(token); }逻辑说明在SaveChangesAsync里统一拦截比在每个 Service 里手动写日志可靠得多不会漏。ChangeTracker.Entries()能拿到所有状态变化的实体。快照用 JSON 序列化整个实体简单粗暴但够用如果实体很大可以只序列化变更的字段用entry.Property(...).OriginalValue和CurrentValue对比。参数说明_currentUser需要从IHttpContextAccessor里取注意在后台任务里没有 HttpContext要判空。审计表会越来越大建议按月分表或者定期归档不然一年后查询会明显变慢。4.3 部署时的连接字符串与迁移策略部署到生产环境时连接字符串不要写在appsettings.json里提交到代码仓库。用环境变量或者密钥管理服务。# 生产环境用环境变量覆盖连接字符串 export ConnectionStrings__DefaultServer...;DatabaseSupplierDb;... # 启动时自动应用迁移谨慎使用 dotnet ef database update --no-build逻辑说明环境变量里__双下划线对应配置里的:层级这是 ASP.NET Core 的标准约定。自动应用迁移在单实例部署时方便但多实例部署时会有并发冲突建议改成在 CI/CD 流水线里单独一步执行迁移应用启动时不跑。参数说明连接字符串里的MultipleActiveResultSets在 SQL Server 上按需开启开了能减少连接数但会增加内存。Connection Timeout默认 15 秒跨机房部署时建议调到 30 秒。这些参数没有万能值要按实际网络环境压测后定。5. 供应商管理系统常见的五个坑这一章全是踩过的坑每条按“现象 → 原因 → 解决”写能帮你省下不少排查时间。坑一列表页越翻越慢。现象是前几页很快翻到几百页后响应要好几秒。原因是Skip值很大时数据库要先扫描并丢弃前面所有行。解决是用游标分页记住上一页最后一条的Id或CreatedAt查询时用WHERE Id lastId代替Skip。如果必须用页码至少保证OrderBy的字段有索引。坑二并发编辑覆盖。现象是两个人同时编辑同一个供应商后保存的把先保存的改动覆盖了。原因是默认的“最后写入获胜”。解决是加乐观并发控制在实体上加[Timestamp]或RowVersion字段保存时 EF Core 会检查版本号不一致就抛DbUpdateConcurrencyException前端提示用户刷新重试。坑三资质到期提醒重复发。现象是同一张证书连续几天都收到提醒。原因是扫描任务没有记录“已提醒”状态。解决是在资质表加一个LastNotifiedAt字段扫描时只取LastNotifiedAt为空或超过 7 天的记录发完更新这个字段。坑四模糊查询拖垮数据库。现象是搜索框输入两个字后接口超时。原因是Contains生成的LIKE %xx%无法走索引。解决是数据量超过 50 万行时改用全文索引或者引入独立的搜索服务。小数据量下可以先限制关键词最少 2 个字符并给搜索加超时。坑五迁移文件冲突。现象是多人协作时dotnet ef migrations add生成的迁移互相覆盖。原因是迁移文件按时间戳命名两个人同时加就会乱序。解决是合并代码后重新生成迁移或者约定同一时间只有一个人能加迁移。已经应用到生产的迁移不要删只能追加新的。6. 把供应商评分做成可解释的加权模型最后一章讲一个进阶技巧供应商评分。很多系统把评分做成一个简单的平均分但采购真正需要的是“为什么这家分低”。我一般用加权模型把质量、交期、价格、服务四个维度按权重算总分并且把每个维度的得分和权重都存下来这样前端能展示雷达图采购能一眼看出短板在哪。权重不要写死在代码里做成配置表不同物料类别可以用不同权重。比如原材料更看重质量辅料更看重价格。评分计算放在一个独立的ScoringService里输入是评价记录列表和权重配置输出是各维度得分和总分。验证方法很简单拿历史数据跑一遍看排名和采购人员的主观判断是否一致。如果不一致先调权重再检查评价数据本身是否有偏差。我踩过的坑是早期把权重写成了整数百分比结果调整时只能 5% 一档地跳后来改成小数才够精细。还有一个习惯每次评分结果都存快照不要实时算。因为权重会变实时算会导致同一家供应商昨天的分和今天的分对不上采购会来问“为什么没改数据分却变了”。存快照之后历史评分可追溯权重调整只影响新评分。这个习惯帮我省了很多解释成本。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑