ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C#+Vue实现网络故障报修系统:状态机与前后端分离实战

C#+Vue实现网络故障报修系统:状态机与前后端分离实战 简介这是一份基于 C# 与 Vue 的网络故障报修管理系统完整源码包面向需要快速搭建前后端分离报修平台的开发者覆盖故障报修、工单跟踪、统计报表等核心业务流程。压缩包共 189 个文件约 635KB主体为 52 个 C# 后端控制器与数据模型、28 个 Vue 页面组件、47 个 JavaScript 脚本并包含 31 个 SVG 图标、7 个 SCSS 样式、5 个 csproj 工程文件及数据库迁移脚本目录结构清晰便于直接运行和二次开发。资源围绕 ASP.NET Core 后端接口与 Vue 前端交互展开包括登录权限、报修提交、工单处理、图表统计等模块同时附有项目级配置文件与迁移记录可帮助读者理解 RESTful API 设计、EF Core 使用及前后端联调方式。已有 490 人学习/下载适合作为毕业设计、课程项目或小型团队故障管理系统的参考蓝本。1. 网络故障报修管理系统的真正难点不在“网络”在流程闭环办公室突然上不了网用户第一反应就是在大群里喊一句“哪位看一下网络”消息夹在汇报、打卡和闲聊之间十分钟后彻底沉底。处理人有没有接单、修没修好、用户是否认可全凭记忆和运气。基于C#Vue这种组合来写网络故障报修管理系统目的不是做一台高深的设备监控平台而是把“发现故障→提交工单→指派处理→反馈结果→确认关闭”这条链路变成有数据可查、有节点可追的规范流程。把源码下载下来之后最容易踩的坑是把所有注意力放在页面漂不漂亮上却忽略后端状态机、接口权限和前端路由联动。这套系统真正的骨架在状态如何流转而不是那张报修表单。适合接手内部运维工具、做课程设计或者想用这套技术栈快速搭一个Jira精简版的人。2. 拿到源码包后先认C#后端与Vue前端各自的工程边界解压源码包后第一件事不是去找启动按钮而是判断工程形态。一个完整的C#Vue系统至少包含两个独立运行单元后端是ASP.NET Core WebAPI运行在服务器端口上负责读取数据库、处理业务逻辑前端是Vue工程不能直接双击HTML运行必须通过Node.js构建成静态资源。很多“跑不起来”的求助根源是只启动了其中一端或者根本没有安装前端依赖。把这两个进程的边界分开看后续所有联调问题都会好查很多。2.1 解压后一眼认出后端解决方案、项目文件与入口后端工程最明显的标记是.sln、.csproj文件。用Visual Studio或者VS Code打开后重点看项目名称是否带Api、Web字样。我一般会先找Program.cs它是ASP.NET Core 6之后的标准入口。源码里若还保留Startup.cs说明项目更老可能是.NET Core 3.1或5配置方式略有差异。一个常见的后端目录结构是NetworkRepair.sln src/ Repair.Api/ # WebAPI项目Program.cs入口 Repair.Application/ # 应用服务层 Repair.Domain/ # 实体与枚举 Repair.Infrastructure/ # EF Core DbContext与仓储实现 appsettings.json # 数据库连接串、JWT密钥Program.cs里通常会有builder.Services.AddControllers()和app.MapControllers()。这两个方法分别表示注册控制器服务、把路由映射到Controller。如果源代码里项目分层不清晰也要能接受很多小项目直接把DbContext写在Api项目里照样能运行。改起来费劲但可以先跑通再重构。appsettings.json里的ConnectionStrings字段是首先要检查的地方。报错信息若包含Login failed for user说明数据库账号密码不对若提示网络错误则八成是SQL Server服务没启动或连接串里的机器名不对。源码包为了脱敏经常把密码写成空或占位符。2.2 Vue前端不是页面文件而是需要编译的SPA工程前端目录的核心标记是package.json和src文件夹。如果源码包里只有几个孤立的.html文件那严格来说不算Vue前后端分离工程。一个合格Vue项目的目录长这样src/ main.js # 创建应用实例挂载App.vue App.vue # 根组件 router/index.js # URL路由 views/ # 页面级组件 api/ # axios请求封装 components/ # 公共组件 vue.config.js # 开发服务器与代理配置在终端里进入前端目录按顺序执行两个命令npm install npm run servenpm install会根据package.json把Vue、Axios、Element Plus等依赖安装到node_modules。若存在package-lock.json优先用npm ci它能严格按照锁定版本安装避免依赖小幅升级带来的样式和接口差异。npm run serve启动的是开发服务器默认地址一般是http://localhost:8080Vite项目则可能是http://localhost:5173。两个地址都行关键要看清控制台实际输出的端口。2.3 前后端联调的环境准备Vue安装及环境配置的最小清单本地联调阶段最麻烦的是端口不一致。后端跑在5000前端跑在8080浏览器直接请求后端接口会触发跨域。最稳妥的联调方式是给Vue开发服务器加代理。在vue.config.js里写// vue.config.js const { defineConfig } require(vue/cli-service) module.exports defineConfig({ devServer: { port: 8080, proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } })这段配置的核心是前端页面里发送/api/fault/list请求时开发服务器不会把请求错误地打到自身而是转发给http://localhost:5000/api/fault/list。changeOrigin:true的作用是让后端收到的请求头Host变成target地址有些严格校验Host的中间件就是靠这个参数通过。如果接口路径没统一以/api开头这段代理规则需要按实际前缀调整。还有一个容易忽略的配置点ASP.NET Core WebAPI默认返回的JSON是camelCase格式例如reporterId而C#属性名是ReporterId。如果前端Axios没有做数据转换直接使用小写字段通常没问题一旦后端设置了PropertyNamingPolicy null返回体就变成大写的ReporterId前端必须同步修改字段名。排查「字段取不到值」时优先看Network面板里实际返回的JSON。组件版本建议作用Node.js18或20 LTS编译Vue项目.NET SDK6或8运行C#后端SQL Server2019及以上存储工单数据浏览器Chrome / Edge调试接口与页面这一套环境变量Way过去后再来核对源码包是否完整。缺少node_modules不是问题因为可以重新安装真正致命的是缺少package.json或.csproj。遇到这种情况说明解压的文件夹不是工程根目录应该在子目录里再找一遍。3. 工单的核心不是增删改查是状态机C#模型设计网络故障报修系统在业务上没有复杂的算法最大的混乱源是「状态」。用户提交后是“待接单”还是“待审核”处理人修完后直接关单还是必须先由报修人确认如果代码里到处写字符串“待处理”“处理中”只要有一个字拼错筛选就失效。专业的工程做法是把状态定义成C#枚举数据库里只存int前端拿枚举描述去展示。3.1 报修单最少要有哪几张表设备表、工单表、操作日志表一张工单背后必须知道“谁报修、哪里坏、谁处理、现在到哪一步”。围绕这个目标最少要有三张核心表设备表记录交换机、AP、光猫等网络节点FaultTicket记录每次报修OperationLog记录状态变更的上文下文。用EF Core写一个实体大致是这样// FaultTicket.cs public class FaultTicket { public int Id { get; set; } public string Title { get; set; } public string FaultPlace { get; set; } public int DeviceId { get; set; } public int ReporterId { get; set; } public int? HandlerId { get; set; } public int Status { get; set; } public string Description { get; set; } public DateTime CreatedAt { get; set; } public DateTime? CompletedAt { get; set; } }这里HandlerId用可空int因为在“待接单”阶段还没有处理人。如果把HandlerId定义成不可空的int那么新增工单时必须先编一个默认负责人这对流程来说很别扭。表设计阶段可以参考Factory Method思想让实体自己保证创建时的默认状态比如构造函数里Status 1。数据库索引同样重要。工单表的数据量会随着使用只增不减按Status和ReportTime建索引才能让列表页在几千条数据后依然响应快。很多源码没有迁移文件直接用EnsureCreated建库这样后期改表结构很痛苦最好换成Add-MigrationUpdate-Database的方式。3.2 用C#枚举定义状态避免魔法数字常见的状态值可以定义为using System.ComponentModel; public enum FaultStatus { [Description(待接单)] Pending 1, [Description(处理中)] Processing 2, [Description(待确认)] AwaitingConfirm 3, [Description(已完成)] Completed 4, [Description(已关闭)] Closed 5 }C#中[Description]是一个特性本身不参与编译逻辑但可以通过反射读取。给枚举加上它前端下拉框、后端日志都能直接拿到中文描述。然后写一个扩展方法public static class EnumExtensions { public static string GetDescription(this Enum value) { var field value.GetType().GetField(value.ToString()); var attr field?.GetCustomAttributeDescriptionAttribute(); return attr?.Description ?? value.ToString(); } }使用方式很直接ticket.Status.GetDescription()。和到处写if (status 2)相比枚举加特性的方式有两个好处第一代码里看到的是有意义的名称FaultStatus.Processing而不是含义不明的数字第二如果要给前端提供字典可以在接口里反射枚举一次性返回所有值和描述前端不需要手工对齐。这种方案的局限是状态变更规则没有放在一处管理。如果任由Controller直接修改Status仍然容易出现非法跳转。所以还需要在服务层约束。3.3 状态流转的服务写法以“维修完成”为例“维修完成”这个动作在流程上要小心。处理人可能不看工单就直接点完成如果系统允许从“待接单”跳到“已完成”那所有统计都会失真。比较规范的写法是只允许“处理中”的工单进入下一步并且修完后先变成“待确认”等报修人确认网络恢复。public async Task CompleteTicketAsync(int ticketId, int handlerId) { var ticket await _db.FaultTickets.FindAsync(ticketId); if (ticket null) throw new NotFoundException(工单不存在); if (ticket.Status ! (int)FaultStatus.Processing) throw new InvalidOperationException(只有处理中的工单才能标记完成); ticket.Status (int)FaultStatus.AwaitingConfirm; ticket.CompletedAt DateTime.Now; _db.OperationLogs.Add(new OperationLog { TicketId ticketId, OperatorId handlerId, Action MarkCompleted, FromStatus (int)FaultStatus.Processing, ToStatus (int)FaultStatus.AwaitingConfirm, Remark ticket.Title }); await _db.SaveChangesAsync(); }这段代码里体现了三次关键约束。第一用FindAsync查询后立刻判断状态防止跳过步骤。第二不是直接置为“已完成”而是先进入“待确认”给用户留出验证时间。第三OperationLogs里记录FromStatus和ToStatus之后想看这个工单经历过哪几个状态直接查日志表就行。需要特别提醒的是FindAsync查询默认走主键但在做状态更新时存在并发风险。两个处理人同时接单可能都读到“待接单”然后都改成“处理中”。更稳妥的写法是用条件更新var rows await _db.FaultTickets .Where(t t.Id ticketId t.Status (int)FaultStatus.Pending) .ExecuteUpdateAsync(s s.SetProperty(t t.Status, (int)FaultStatus.Processing));rows为1说明更新成功为0说明抢单失败。这是工单系统里很值得保留的一段代码可以避免重复处理。4. 后端接口的约定与权限控制C# WebAPI的最小骨架前端要稳定地展示和操作工单依赖后端接口足够干净。C# WebAPI开发里最常犯的错误是每个Controller返回结构都不一样有的直接返回实体有的包一层导致前端整理数据时到处写判断。这个系统里比较好的做法是统一返回code data message结构并在后端加JWT认证。4.1 路由前缀、统一返回结构与Swagger一个典型的报修工单接口是这样的[ApiController] [Route(api/fault)] public class FaultController : ControllerBase { [HttpGet(list)] public async TaskIActionResult List(int page 1, int pageSize 10, int? status null) { var paged await _faultService.GetPagedAsync(page, pageSize, status); return Ok(new { code 0, data paged }); } }[HttpGet(list)]最终拼接出来的URL是/api/fault/list。page、pageSize、status都是可选的query参数。前端请求时传/api/fault/list?page1pageSize10status2即可。返回包里的code0表示成功非0表示业务错误前端拦截器只需要判断这一个字段。Swagger在这个技术栈里几乎是标配。在Program.cs中找到if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }启动后端后访问/swagger可以看到所有接口和参数定义。我一般会在做前端之前先用Swagger验证一遍接口。如果Swagger打不开先看启动日志里监听的端口再用curl手动请求一次相同地址。4.2 基于JWT的登录态谁在报修谁在处理报修系统最少有三种角色普通用户、处理人、管理员。如果接口不区分权限任何登录用户都能把别人工单改成“已完成”那流程就失去意义。JWT是当前最主流的身份方案。后端注册认证服务builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidateAudience true, ValidateLifetime true, ValidateIssuerSigningKey true, ValidIssuer builder.Configuration[Jwt:Issuer], ValidAudience builder.Configuration[Jwt:Audience], IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Key])) }; });ValidateLifetime设为true后过期Token会被拒绝这是防止凭据长期有效的基本要求。Jwt:Key属于签名密钥生产环境不能直接写在appsettings.json里常见做法是改用环境变量或用户机密。在Controller上使用[Authorize(Roles Admin)]就能限制只有管理员可访问某些接口。但JWT也有一个很现实的坑刷新Token。如果做的是管理端后台Token有效期可以设为1小时左右过期后让用户重新登录。为了用户体验也可以做Refresh Token让Token过期后自动刷新。这部分源码里通常需要单独一张表存Token映射。4.3 操作日志写入用过滤器还是手动埋点做审计日志时自动化过滤器看似省事但报修系统状态变更需要记录的上下文差异太大比如“接单”要记录处理人“完成”要记录维修内容。用过滤器统一读取请求体反而会混淆不同动作。我倾向于手动埋点虽然代码多一些但每个动作写入什么内容一目了然。写日志时经常要控制长度防止超长字符串撑爆字段public static string BuildLog(string prefix, string detail) { string text detail ?? string.Empty; if (text.Length 50) text text.Substring(0, 50) ...; return ${prefix}: {text}; }这里用到C#的Substring方法它接受起始位置和长度。细节字段常包含用户填的故障描述可能很长截断前必须判空否则NullReferenceException会直接中断保存。有人问为什么不用Take(50)扩展方法那个基于IEnumerable在这里也能用只是字符串本身就有Substring性能更好也更直观。为了统一管理接口可以把常见端点整理成一份约定表端点方法权限说明/api/auth/loginPOST匿名登录换Token/api/fault/listGET已登录分页查询工单/api/fault/{id}GET已登录工单详情/api/fault/{id}/processPOST处理人接单并进入处理中/api/fault/{id}/completePOST处理人提交完成/api/fault/{id}/confirmPOST报修人确认关闭这张表也是前端页面设计菜单和权限按钮的依据。前端要做的是按角色隐藏或禁用按钮后端则必须做二次校验不能只靠前端界面控制。5. Vue前端从列表到报修表单的交互实现Vue前端要解决的是“用什么样子操作工单”。页面结构可以简单但路由和状态映射必须理性。很多源码工程喜欢把列表、详情、表单全都塞进一个组件文件上千行之后很难维护。路由就要按业务角色拆分每个页面只负责一件事。5.1 配置vue-router让报修台、工单详情有明确入口Vue 3项目中典型的router/index.js长这样import { createRouter, createWebHistory } from vue-router const routes [ { path: /, redirect: /repair/new }, { path: /repair/new, component: () import(/views/NewRepair.vue) }, { path: /repair/list, component: () import(/views/RepairList.vue) }, { path: /repair/detail/:id, component: () import(/views/RepairDetail.vue), meta: { requiresAuth: true } } ] const router createRouter({ history: createWebHistory(), routes })使用createWebHistory()可以让URL变成/repair/detail/123没有#号看起来更正式。但也带来了服务器刷新404的问题这个在最后一章说。meta.requiresAuth是自定义字段配合全局前置守卫使用比如router.beforeEach((to) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) return /login return true })通常我不建议把登录态存在localStorage因为XSS攻击可以直接窃取。更安全的是httpOnlyCookie但前后端分离后用Cookie要处理跨域withCredentials复杂度会上升。多数内部系统为了省事仍然用Authorization头。工单详情页获取路由参数很简单const route useRoute() const ticketId route.params.id拿到ticketId后调/api/fault/{ticketId}取详情。如果发现参数是字符串而接口需要数字可以用Number(route.params.id)转一下避免后端严格模式下类型不匹配。5.2 把接口数据映射成状态标签计算属性与过滤器后端返回的status是数字前端直接用v-if写数字判断会很难读也容易写错。先建一个状态映射文件集中维护所有枚举值// src/constants/faultStatus.js export const faultStatusMap { 1: { label: 待接单, type: warning }, 2: { label: 处理中, type: info }, 3: { label: 待确认, type: primary }, 4: { label: 已完成, type: success }, 5: { label: 已关闭, type: danger } }在列表页的表格列里使用el-tag :typefaultStatusMap[row.status].type {{ faultStatusMap[row.status].label }} /el-tag这段模板先通过row.status找到映射对象再取type和label。type对应Element Plus中Tag组件的颜色主题success是绿色danger是红色用户一眼看清状态。如果把映射文件放到公共目录多个页面可以复用不会出现一个页面写“已完成”、另一个页面写“已结束”的情况。有时接口返回的是“待接单”这样的字符串而不是数字这通常是后端枚举数据被序列化为字符串导致的。解决办法有两种后端改成返回int或者前端用对象的key去匹配。判断依据是typeof row.status string时需要先转成数字例如faultStatusMap[Number(row.status)]。5.3 提交报修单时防止网络故障导致的重复提交用户点击“提交”时如果网络卡顿通常会下意识再点一次。前端按钮如果不加锁后端会收到两个POST请求数据库里就生成两条几乎一样的工单。最简单的实现是加submitting标志const submitting ref(false) async function submitForm() { if (submitting.value) return submitting.value true try { const res await axios.post(/api/fault, formData.value) if (res.data.code 0) { ElMessage.success(报修单已提交) } } finally { submitting.value false } }ref(false)在Composition API中创建响应式布尔值。进入函数后先判断是否已在提交中是就直接返回不是就置为true按钮上的加载状态通过v-loading绑定。finally保证请求完成后无论成功失败都会解锁按钮否则一旦接口报错按钮就永远禁用。前端防重只能挡住用户的重复点击。如果网络请求超时用户可能刷新页面后再次提交这时就需要后端配合做幂等控制。常见做法是前端生成一个UUID作为requestId后端记录一段时间内相同requestId的请求已经处理过就直接返回上一次结果。对于报修系统一张表字段就能解决但对很多小项目来说前端防重已经足够了。6. 打包部署与常见报错本地能跑不等于线上能用本地开发环境通过代理绕过了跨域npm run serve会自动刷新页面一切都看起来很顺。发布后遇到白屏、404、接口连不上才是真正的分水岭。这一章把最常见的三个发布问题讲透。6.1 用代理解决开发期跨域用同一域名解决线上跨域开发期用Vue的devServer.proxy生产环境最好反着做让ASP.NET Core直接托管Vue构建好的静态文件。先执行npm run build生成dist目录再把dist文件夹里的内容拷到C#后端发布目录下的wwwroot。然后在Program.cs中加app.UseDefaultFiles(); app.UseStaticFiles();UseDefaultFiles让请求网站根路径时自动找到index.htmlUseStaticFiles则允许访问assets里的JS和CSS。前后端同源后Cookie、Token、跨域问题全部消失接口请求路径可以直接写/api/fault/list。6.2 前端打包后刷新404与接口404的区别Vue使用History路由时用户停留在/repair/detail/123按下F5浏览器向后端发起这个真实路径的请求后端没有对应Controller于是返回404。这不是代码错误而是缺少路由回退规则。部署在Nginx时加这一段location / { try_files $uri $uri/ /index.html; }它表示如果请求的资源不存在就返回index.html交给前端路由处理。如果接口请求/api/fault/list返回404则要看Nginx是否把/api反向代理到了C#后端或者后端路由前缀是不是多了api。两者现象相同本质完全不同排查入口要分清楚。6.3 验证源码完整性的三条命令接手一个不熟悉的运行环境我一般会按顺序执行三条命令dotnet build curl http://localhost:5000/api/health npm ci npm run builddotnet build能找出后端缺少的包引用和语法错误。curl访问一个简单的健康检查接口能验证API进程确实启动。npm ci严格按锁定文件装依赖npm run build检查前端能否完整编译。三道都通过再继续做页面测试任何一条失败优先解决它不要急着改代码。现象可能原因处理建议刷新页面404history模式缺回退Nginx加try_files接口404代理或反向代理未生效检查nginx的/api配置页面白屏publicPath路径错误在vue.config.js里设publicPath: ./最后这条publicPath非常容易被忽略。部署在子目录时资源路径写成/assets/xxx.js会找不到文件改成相对路径./assets/xxx.js才能正确加载。改完重新npm run build再看控制台是否还报资源加载错误。本文还有配套的精品资源点击获取
返回列表