C#+Vue实现网络故障报修系统:状态机与前后端分离实战
2026/9/16 15:44:06 网站建设 项目流程

简介:这是一份基于 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打开后,重点看项目名称是否带ApiWeb字样。我一般会先找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.jsonsrc文件夹。如果源码包里只有几个孤立的.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 serve

npm install会根据package.json把Vue、Axios、Element Plus等依赖安装到node_modules。若存在package-lock.json,优先用npm ci,它能严格按照锁定版本安装,避免依赖小幅升级带来的样式和接口差异。npm run serve启动的是开发服务器,默认地址一般是http://localhost:8080,Vite项目则可能是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/listchangeOrigin: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

数据库索引同样重要。工单表的数据量会随着使用只增不减,按StatusReportTime建索引,才能让列表页在几千条数据后依然响应快。很多源码没有迁移文件,直接用EnsureCreated建库,这样后期改表结构很痛苦,最好换成Add-Migration+Update-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?.GetCustomAttribute<DescriptionAttribute>(); 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里记录FromStatusToStatus,之后想看这个工单经历过哪几个状态,直接查日志表就行。

需要特别提醒的是,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 Task<IActionResult> 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/listpagepageSizestatus都是可选的query参数。前端请求时传/api/fault/list?page=1&pageSize=10&status=2即可。返回包里的code=0表示成功,非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 :type="faultStatusMap[row.status].type"> {{ faultStatusMap[row.status].label }} </el-tag>

这段模板先通过row.status找到映射对象,再取typelabeltype对应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 build

dotnet 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,再看控制台是否还报资源加载错误。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询