☰
PHP+MySQL校园网运维管理系统:工单闭环与权限设计
2026/11/3 3:24:23 网站建设 项目流程

简介:面向高校网络中心运维人员与计算机专业课程设计、毕业设计参考者的技术文档,聚焦 PHP+MySQL 环境下的校园网运维系统设计,用于应对运维流程分散、故障申报与处理进度难追溯、资产耗材缺少统一管理等问题。包内为 1 个 PDF 文件,压缩包约 1.04MB,依次覆盖需求分析、总体架构、数据库设计与关键技术实现。文档按权限分级将用户划为系统管理员、网络管理员、网络现维人员与网络用户四类,并规划运维人员管理表、用户登记表、信息点记录表、网络资产表、故障维护表、维护进度分类表、通知公告表、耗材分类表等 10 张数据表。关键技术部分给出可借鉴的思路:调用 ping 命令对外网做通断测试判断校园网运行状态,检索最近十条已解决维修记录协助排查同类故障,通过手机端外网访问查看本地设备状态,另附安全性、可扩展性等实施要点。已有 93 人学习。

1. 需求先立住:为什么校园网运维要用一套轻量系统兜底

校园网的报修单,大多死在聊天记录里。用户打电话说「三楼连不上」,值班的人记在纸上,维修的人跑一趟回来忘了登记,月底做统计时谁也说不清这个月到底处理了多少条、平均用了多久。这正是「基于 PHP 的校园网运维管理系统」要解决的问题:把故障申报、任务派发、处理进度、资产台账、通知公告这几件事,从口头和纸面挪进一张可以查询、可以统计、可以追溯的表里。

反直觉的地方在于选型。很多人一听运维管理系统,第一反应是上一套重量级网管平台,但校园网的实际规模往往撑不起那个运维成本,真正卡住的也不是监控粒度,而是流程没人管。PHP 加 MySQL 的组合在这里反而合适:部署门槛低,一台普通服务器就能跑,改一个字段、加一张报表不需要重新走一遍技术栈评审。这套系统适合两类人参考,一类是要交毕设或课设、需要一个结构完整且能跑起来的题目的学生,另一类是真在管一个中小规模校园网、想先把流程规范起来的从业者。

2. 需求与角色建模:四类用户撑起十条核心诉求

需求分析这一步如果只写「实现用户管理、故障管理」,后面数据库和权限就没法做。这套系统的做法是先按权限分级切出四类角色,再让每条需求挂到具体角色上,最后才落到表结构。

2.1 四类角色的权限边界划分

系统把使用者分成系统管理员、网络管理员、网络现维人员、网络用户四类,代码里对应sysmgr、netmgr、netrepair、netusr四个子系统标识。这个划分不是随便切的,它对应的是校园网里真实存在的四种动作:决策、派活、干活、报障。

系统管理员关心的是系统本身跑得怎么样、谁能进系统、谁有什么权限,属于元层面的管理;网络管理员关心的是今天有几单没派、资产台账对不对、公告发没发出去;现维人员关心的是手上这单在哪个房间、是什么设备、上一单怎么解决的;网络用户只关心我报的那条修到哪一步了、大概什么时候能好。

提示:角色少不等于权限简单。四类角色里,前两类能吃到的数据范围明显更大,权限判断必须做在后端每次请求上,不能只靠前端隐藏菜单。

按这个边界往下推,需求自然就分开了:管理员要能看到角色人员的管理入口和系统运行状况;网管要能分配任务、发布公告、维护资产和运维状态、管理设备;所有角色都要能看到与自己相关的通知;此外还要有统计报表的生成和打印;网络用户要能开通账号、提交故障申报、查看处理进度和责任人联系方式。这五条需求看着散,其实刚好覆盖了一个完整的工单闭环。

2.2 把需求翻译成可验收的条目

需求写在纸上容易虚,落到验收就必须能被一条一条点出来。通常的做法是把上面五条展开成可以演示的条目,后面答辩的时候直接照着走一遍:

编号需求条目对应角色验收方式
R1登录并按角色进入对应子系统全部四个账号分别登录,菜单不同
R2新增、修改、停用运维人员账号sysmgr停用后该账号无法登录
R3发布通知公告,指定可见范围netmgr用户端只看到公开公告
R4登记网络资产与信息点netmgr资产能关联到具体房间
R5故障申报并生成工单编号netusr提交后列表出现新单
R6派单给现维人员netmgr工单责任人字段被写入
R7更新处理进度直到已解决netrepair状态按预设流程流转
R8用户查看本人工单进度与责任人联系方式netusr只能看到自己的单
R9查看近期已解决工单作为处理参考netmgr / netrepair列表按时间倒序
R10生成并打印统计报表netmgr按故障类别、时间段汇总

这张表的价值在于,它把「系统能干什么」变成了「演示时点哪里」。很多方案写到需求分析就停在几条抽象描述上,到验收阶段自己也说不清做没做到,问题就出在这个环节没落地。

2.3 十个数据表各管一段流程

数据库设计上,系统规划了十张主要数据表,它们不是并列关系,而是围绕「人、点、设备、单、类型、通知」六个维度展开的。区域和楼栋这些都不单独建表,而是作为信息点表里的字段存在,因为校园网的物理范围相对固定,没必要为了规范多建两张关联表。

编号表标识表名主要职责
1net_mgr网络运维人员管理表记录网管与现维人员的基本信息与在职状态
2net_role系统登录人员类别表角色分类与权限标识
3net_user网络用户登记表校园网用户账号、所属单位、联系方式
4net_point信息点记录表区域、楼栋、楼层、房间号、对应交换机端口
5net_device网络资产管理表交换机、路由器、服务器等设备的型号、IP、位置
6net_repair网络故障维护表工单主表,记录申报、处理、解决全过程
7net_info网络故障分类表故障类别字典,如断网、慢、无法认证
8net_section网络维护进度分类表状态字典,未受理、处理中、已解决、已关闭
9net_notice网络通知公告管理表公告标题、正文、发布人、可见范围
10net_consum网络常规耗材分类表网线、模块、跳线等常用耗材的分类

把故障类别和进度做成字典表,是个容易被忽略但很值的设计:报障时用户从下拉框选类别,统计时按类别分组,报表口径天然一致,不用后期再去清洗文本。耗材表独立出来则是为了配合资产维护,换了一根网线、一个模块,能记到具体工单上,期末盘账时有据可查。

3. 总体架构与数据关系:四个子系统如何共用一套底座

系统的总体架构图通常画成一个中心加四个分支:中心是权限与数据访问层,四个分支是四类角色的功能入口。这样画的意义不在于好看,而在于说明四个子系统其实是同一套代码用不同的入口和不同的数据范围跑出来的,不是四套独立程序。

3.1 三层结构与子系统映射

落到代码组织上,一般按「入口层 - 业务层 - 数据层」来分。入口层负责登录校验、角色判定、菜单渲染;业务层是一组按模块拆开的处理脚本,工单、资产、公告、报表各自独立;数据层只有一个数据库访问封装,所有查询都走它。

<?php // config/roles.php —— 角色到模块的白名单,入口层每次请求都查这张表 return [ 'sysmgr' => [ 'name' => '系统管理员', 'modules' => ['user', 'role', 'notice', 'asset', 'repair', 'report', 'system'], ], 'netmgr' => [ 'name' => '网络管理员', 'modules' => ['notice', 'asset', 'point', 'repair', 'report', 'consum'], ], 'netrepair' => [ 'name' => '网络现维人员', 'modules' => ['repair', 'point', 'consum'], ], 'netusr' => [ 'name' => '网络用户', 'modules' => ['repair', 'notice'], ], ];

这段配置的作用是把「谁能进哪个模块」变成一张可读、可改的表。入口层拿到当前会话里的角色标识后,直接查这个数组判断模块是否在白名单内,不在就跳回首页。好处是加一个模块、调一次权限只改这一处,不用去翻十几个页面里的 if 判断。参数上modules里的每一项对应业务层的一个目录或一组脚本,命名保持和表前缀一致,方便对照。

按模块拆开还有一个现实考虑:四个子系统的功能重叠度很高,比如公告在四个角色里都能看到,只是可见范围不同;工单在网管、现维、用户三端都有入口,只是可操作的动作不同。共用一套业务脚本、用角色和参数控制行为,比复制四份代码要好维护得多。

3.2 工单主表与周边表的关联设计

十张表里,net_repair是绝对的核心,其他表基本都在给它提供上下文。一条工单从申报到关闭,会依次用到:谁报的(net_user)、在哪个位置(net_point)、可能涉及哪台设备(net_device)、属于什么故障(net_info)、当前什么状态(net_section)、谁在处理(net_mgr)。

关键字段的设计大致如下,字段名保持和业务语义一致,避免出现flag1、type2这种后期没人看得懂的命名:

CREATE TABLE net_repair ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, ticket_no VARCHAR(20) NOT NULL COMMENT '工单编号,如 WX20240512001', reporter VARCHAR(32) NOT NULL COMMENT '申报人账号,关联 net_user', address VARCHAR(128) NOT NULL COMMENT '报障地址描述', point_id INT UNSIGNED DEFAULT NULL COMMENT '关联信息点,可空', info_id INT UNSIGNED NOT NULL COMMENT '故障类别,关联 net_info', question VARCHAR(500) NOT NULL COMMENT '用户描述的现象', reason VARCHAR(500) DEFAULT NULL COMMENT '定位到的原因', solution VARCHAR(500) DEFAULT NULL COMMENT '处理办法', mgr VARCHAR(32) DEFAULT NULL COMMENT '责任人账号,关联 net_mgr', status TINYINT NOT NULL DEFAULT 0 COMMENT '0未受理 1处理中 2已解决 3已关闭', start_time DATETIME NOT NULL COMMENT '申报时间', finish_time DATETIME DEFAULT NULL COMMENT '解决时间', KEY idx_status_time (status, start_time), KEY idx_reporter (reporter) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有两个点值得说。一是reason和solution要和question分开存,用户的描述和运维的判断是两回事,混在一起后期做知识沉淀就没法用。二是索引建在(status, start_time)上,因为系统里最高频的两类查询——「未解决的工单」和「最近的工单」——都吃这个组合索引,单建status效果会差一截。

注意:地址字段保留了冗余的文本描述,同时用point_id指向信息点表。这是有意的冗余,用户报障时经常说不清具体房间,先存一段原话,现维人员到场后再补关联,比强制先选信息点要现实。

3.3 会话与权限拦截的统一入口

PHP 的会话机制天然适合做这件事。登录成功后把用户标识和角色写进$_SESSION,之后每个业务脚本第一行引入同一个拦截文件,由它判断角色、判断模块、判断数据范围,三件事一次做完。

<?php // include/guard.php —— 所有业务脚本的第一行 session_start(); require __DIR__ . '/../config/roles.php'; if (empty($_SESSION['uid']) || empty($_SESSION['role'])) { header('Location: /login.php'); exit; } $role = $_SESSION['role']; $module = $MODULE ?? 'index'; // 各业务脚本在引入前声明自己属于哪个模块 if (!in_array($module, $roles[$role]['modules'] ?? [], true)) { http_response_code(403); exit('无权访问该模块'); }

$MODULE由各业务脚本在引入拦截文件之前声明,这样拦截逻辑只有一份,模块归属由脚本自己说清楚。in_array的第三个参数传true走严格比较,避免类型松散比较带来的意外命中。数据范围的判断放在业务层,比如用户端查工单时强制拼上reporter = 当前用户的条件,这一层拦截管的是「能不能进这个模块」,管不到「能看哪些行」,两件事必须分开做——这也是后面避坑章节要展开的一条。

4. 核心功能实现:从通断探测到工单闭环

功能实现这部分,最有参考价值的不是页面长什么样,而是几个具体动作怎么落到代码上:怎么判断网络通不通、怎么让现维人员快速找到同类故障的历史处理办法、怎么根据用户报的位置定位到设备、以及工单状态怎么流转。

4.1 出口通断探测的封装与结果判定

判断校园网运行状态,最直接的办法是从服务器侧对几个关键节点做连通性测试。原始做法是直接调系统的 ping 命令,看返回的行数判断成败。这个思路没错,但实现上要做三件事才敢放进生产:目标集中配置、命令参数转义、结果做多数判定。

<?php // lib/probe.php function probe(string $host, int $count = 3, int $timeout = 2): array { // 目标地址必须经过转义,防止被拼接成额外命令 $safe = escapeshellarg($host); $cmd = "ping -c {$count} -W {$timeout} {$safe} 2>&1"; exec($cmd, $lines, $code); // 统计成功回包数,而不是简单看行数 $ok = 0; foreach ($lines as $line) { if (preg_match('/time[=<]\s*[\d.]+/', $line)) { $ok++; } } return [ 'host' => $host, 'success' => $ok > 0 && $code === 0, 'loss' => $count > 0 ? round((1 - $ok / $count) * 100) : 100, 'raw' => $lines, ]; }

escapeshellarg是这条链路的安全底线,凡是拼进 shell 的外部输入都必须过一遍。-c控制探测包数量,-W控制单包等待秒数,两个参数合起来把最坏情况下的耗时压在几秒内。结果判定没有沿用「返回行数为 1 就是不通」的写法,而是用正则数实际收到回应的行数——因为不同系统、不同语言环境下 ping 的输出格式不一样,靠行数判断在换机器时很容易翻车。返回结构里带上丢包率和原始输出,前端可以据此区分「完全不通」和「通但质量差」两种状态,这两种情况在运维上的处置方式完全不同。

probe_targets里配三个目标:出口网关、校内解析地址、一个外部参照站点。三个都通说明链路正常;网关通、外部不通,问题大概率在出口或上游;网关都不通,范围就缩到接入侧了。这个三分法比单点测试有用得多,也是判断「本次故障影响范围级别」的依据。

4.2 近期已解决工单的取用方式

现维人员接到一张新单时,最想知道的是「这种问题以前怎么弄的」。系统的做法是把最近十条状态为已解决的工单拉出来,作为处理参考。原始描述里给的语句用了top 10,那是另一套数据库的写法,MySQL 里对应的是LIMIT。

<?php $sql = "SELECT id, ticket_no, address, start_time, reporter, question, reason, solution, mgr FROM net_repair WHERE status = :status ORDER BY id DESC LIMIT :limit"; $stmt = $pdo->prepare($sql); $stmt->bindValue(':status', 2, PDO::PARAM_INT); $stmt->bindValue(':limit', 10, PDO::PARAM_INT); $stmt->execute(); $recent = $stmt->fetchAll(PDO::FETCH_ASSOC);

用预处理绑定参数,LIMIT也走绑定而不是字符串拼接,这是 PDO 在 MySQL 下需要注意的一点:LIMIT的占位符必须显式指定PDO::PARAM_INT,否则会被当成字符串引号包起来导致语法错误。这个查询走的是idx_status_time索引,status = 2加id DESC的组合能直接命中,十万级工单量下响应依然在毫秒级。

返回结果在页面上只展示四列:时间、位置、现象、处理办法。原因和办法这两列是这条功能的核心价值,现维人员扫一眼就知道上次是换了模块还是重新打了标签,省去重复排查的时间。

4.3 按区域楼栋楼层房间定位设备

用户报障时给的位置是「几号楼几层几号房间」,系统要据此查出这个房间对应的网络设备 IP,再做一次探测。这中间需要一个四段式的条件查询,把信息点表和资产表连起来。

<?php $sql = "SELECT d.ip_address, d.device_name FROM net_point p INNER JOIN net_device d ON d.id = p.switch_num WHERE p.area = :area AND p.building = :building AND p.floor = :floor AND p.room_num = :room LIMIT 1"; $stmt = $pdo->prepare($sql); $stmt->execute([ ':area' => $area, ':building' => $building, ':floor' => $floor, ':room' => $room, ]); $device = $stmt->fetch(PDO::FETCH_ASSOC); if ($device) { $result = probe($device['ip_address']); }

四个条件对应信息点表里的四个字段,switch_num是信息点指向所属交换机的关联键。查不到设备时不报错,返回空结果,由页面提示「该位置暂未登记信息点」,并引导用户补充地址描述——这比直接抛异常要友好,也符合校园网信息点登记往往不完备的实际情况。

这个功能真正的用处在于远程预判。值班人员不在现场,通过手机访问系统,输入用户报的位置,就能知道这个房间挂在哪台交换机上、那台交换机通不通。如果整台交换机都不通,那就是影响一层楼的范围性故障,可以直接升级处理级别,不用先跑一趟确认。

4.4 故障申报与状态流转的实现

工单的状态流转是整个系统的主线,必须定义清楚,否则会出现「已解决的单又被改回处理中」这类扯皮。系统设了四个状态:0 未受理、1 处理中、2 已解决、3 已关闭。允许的流转只有一条主线,加上一条回退分支。

<?php // lib/ticket.php const FLOW = [ 0 => [1, 3], // 未受理 -> 处理中 / 直接关闭(重复报障) 1 => [2, 3], // 处理中 -> 已解决 / 关闭(无法处理) 2 => [3, 1], // 已解决 -> 关闭 / 退回处理中(复发) 3 => [], // 已关闭为终态 ]; function canTransfer(int $from, int $to): bool { return in_array($to, FLOW[$from] ?? [], true); }

状态迁移表写成常量数组,判断逻辑只有一行。这样做的好处是流程可读、可审计,答辩时被问到「状态会不会乱跳」直接把这五条列出来就能回答。状态改为 2 时同步写入finish_time,供后续计算平均处理时长;状态改为 1 时写入mgr,如果原来为空则记为首次受理。

用户端查自己工单的那条语句,除了状态条件,必须带上申报人条件:

SELECT ticket_no, address, question, status, mgr, start_time, finish_time FROM net_repair WHERE reporter = :uid AND status <> 3 ORDER BY id DESC

reporter = :uid这一条不能省,它决定了用户只能看到自己的单。责任人联系方式在展示时做一次脱敏处理,只显示姓氏加岗位电话,不直接把个人号码铺在页面上。

5. 避坑与排查:四个最容易翻车的地方

这套系统的功能本身不复杂,真正花时间的都在细节上。下面这四条是这类项目里出现频率最高的问题,每一条都值得在动手前先看一眼。

5.1 查询用字符串拼接,改一个参数就出错

现象:查询条件里带单引号或者中文时,页面报语法错误;更糟的是没报错但结果不对,比如查某栋楼的工单,把别的楼也查出来了。

原因:原始写法把外部参数直接拼进 SQL 字符串,既没有转义也没有类型约束。一旦参数里出现引号,SQL 结构就被破坏;如果参数恰好能构造出合法结构,就会变成越权查询。这类问题在本地测试时往往发现不了,因为测试用的都是规规矩矩的参数。

解决:全站统一用预处理语句,参数一律绑定,不拼接。上面几段查询都是这个写法。改的时候有个顺序:先把数据库封装层改成只暴露prepare和execute,把拼接入口堵死,再逐个脚本迁移。另外把LIMIT、status这类看起来「一定是数字」的参数也坚持绑定,别图省事直接插值。

5.2 命令行探测传入未过滤的地址

现象:探测功能偶尔返回异常结果,或者在日志里看到一条明显不该出现的命令记录。

原因:exec拼接时用了外部传入的地址,没有做转义。这类问题平时不会暴露,因为它需要外部输入恰好构造出特殊字符,但一旦发生,影响的不只是一个页面的显示结果。

解决:所有进入命令行的变量都过escapeshellarg,更进一步的做法是根本不接受外部传入地址——探测目标从配置文件里读,用户只能选择探测哪一个已配置的节点,而不是自由输入一个地址。同时给exec加超时控制,避免目标不可达时把 PHP 进程挂住。上面probe函数里的-c和-W就是干这个用的。

5.3 前端隐藏了菜单,后端却没有拦

现象:演示时一切正常,换个账号用手工输入 URL 的方式访问,能看到不该看的数据。

原因:权限只做在界面层,菜单按角色渲染,但对应的处理脚本没有做二次校验。这在学生项目里非常普遍,因为演示路径都是按正常流程点的。

解决:权限判断必须做两次,一次在入口层判断模块归属,一次在业务层判断数据范围。前者解决「能不能进这个页面」,后者解决「能看哪些行」。上面guard.php管的是前者,用户端查询里强制拼reporter = :uid管的是后者,两个缺一不可。自测时养成一个习惯:把每个角色的 URL 抄下来,换账号逐个访问一遍,看返回是不是 403。

5.4 中文字段和时间的显示问题

现象:工单描述里的中文变成问号或者乱码;工单时间和实际申报时间差几个小时;某些较长的故障描述写进去被截断了。

原因:三个独立的问题叠在一起。字符集在连接层没有统一设置,建表和连接的编码不一致就会乱码;PHP 的默认时区和数据库的时区不一致,date()和NOW()打出来的时间对不上;字段长度按字符数估的,但用了VARCHAR(50)去存用户的一段口语描述,显然不够。

解决:建库建表统一utf8mb4,PDO 连接串里显式带charset=utf8mb4;在公共初始化文件里date_default_timezone_set设一次,数据库连接后执行一次SET time_zone,两边对齐;描述类字段一律给到VARCHAR(500),并且在前端加字数提示而不是等数据库报错。这三条都是配置级的小事,但漏掉任何一条都会在演示当天暴露,属于典型的没有后悔药可吃的问题。

6. 验收与答辩:把系统跑通只是及格线

验收这一步,很多人栽在「功能都做了,但说不清为什么这么做」上。评审关心的不是你写了多少行代码,而是这套东西能不能对上一个真实场景、设计上有没有想清楚的地方。

我会建议按一条完整链路来演示,而不是按菜单逐个点。就从「用户在手机上提交一条报障」开始:选位置、选故障类别、填描述、提交,然后切到网管账号看到这条新单并派给自己,再切到现维账号,先看近期十条已解决工单找参考,再用位置查一下对应交换机通不通,最后填原因和办法、改成已解决。用户端刷新,看到进度变化和责任人信息。整条链路走完不超过五分钟,但把四类角色、十个表、状态机全串起来了,比逐个模块点过去有说服力得多。

答辩时最容易被追问的三个点,提前准备好答案:

追问回答要点
为什么不直接用现成的网管平台现成平台解决的是监控和告警,这套系统解决的是流程和记录,两者不冲突;校园网规模下轻量方案维护成本更低
为什么把故障类别和进度做成字典表报表统计口径统一,避免后期清洗文本;类别和状态可以随管理要求调整而不用改代码
权限是怎么保证的入口层判模块归属,业务层判数据范围,两层分开,且都有可复现的自测方式

演示环境要提前一天准备,数据库里预置至少二十条不同状态的工单、四五条资产记录,不要用空库讲功能。空库演示时那种「点了没反应」的尴尬,基本都出自这里。

从那以后我每次带人做这类系统,都会强制走一遍「换账号手工敲 URL」的自测流程,不管演示前一天多晚。权限这块翻车的成本太高,而补起来的成本其实很低。希望帮到你。

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

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

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

立即咨询