PHP毕业设计:电子竞技比赛信息管理系统开发实战
一提到PHP毕业设计很多人脑子里蹦出来的就是图书管理系统、商城系统、学生信息管理系统说实话这些题目答辩老师已经看到麻木了。我当初选“电子竞技比赛信息管理”这个题目的时候身边的同学还觉得有点冷门结果做下来发现这个选题的业务逻辑比想象中完整得多从赛事、战队、选手到赛程、比分、公告每一条数据流都有真实场景可对应画ER图、写用例、做测试都有素材答辩的时候也特别容易讲清楚。这篇文章就把我从需求分析、技术选型、数据库设计、后台开发到前台展示、论文撰写的整个实操过程都过一遍顺便把我踩过的坑和复盘后的优化方案写出来给准备做PHP毕设或者想自己动手写一个完整Web项目的朋友一个可以直接参考的路线。不管你是刚接触PHP的小白还是已经能写点东西但想找个完整项目练手这篇都适用。1. 选题背景与需求边界电竞比赛信息管理系统到底要管什么1.1 为什么这个题目比图书管理系统更适合当毕设先说结论图书管理系统的数据关系是“书-借阅-读者”业务很经典但太单薄翻来覆去就是增删改查而电竞比赛信息管理系统的核心是“赛事—战队—选手—比赛—比分”这条嵌套关系链数据表天然超过五张业务上既有纯粹的CRUD又有状态流转未开始/进行中/已结束、胜负判定、积分排名这类逻辑判断整体复杂度刚好卡在“毕业设计该有的难度”上。从另一个角度看这类系统也是真实存在需求的。校园电竞社、地方网吧赛、线上小型联赛都需要一个能录入战队信息、安排赛程、记录比分的平台。这意味着需求分析不是凭空编的你可以直接找到业务原型论文的“可行性分析”和“需求背景”写起来有根有据而不是套模板。我当时给自己划定的功能边界是前台给普通访客看展示赛事列表、赛程安排、战队选手信息和比赛结果后台给管理员用负责登录认证、赛事管理、战队管理、选手管理、赛程管理、比分录入和公告发布。这个边界刚好覆盖了一个Web系统该有的全部基本能力又没有贪多求全到做论坛、做直播那种失控范围毕设评审最怕的就是“功能写了一大堆每个都是半成品”。1.2 功能需求拆解前台展示和后台管理缺一不可需求拆解阶段别急着写代码先把角色和操作梳理成一张表后续做数据库和页面才有依据。我的功能模块划分如下模块角色核心操作对应页面赛事管理管理员发布赛事、编辑赛事、设定比赛项目/时间/奖金、下架赛事后台赛事列表/添加/编辑战队管理管理员录入战队、上传队标、修改队员构成后台战队管理选手管理管理员录入选手、关联战队、上传头像后台选手管理赛程管理管理员安排比赛对阵、设置比赛时间与场地、更新比赛状态后台赛程管理比分录入管理员录入大比分、小分、胜方系统自动校验后台比分录入前台展示访客查看赛事、赛程、战队、选手、结果首页/赛事详情/战队详情这张表建议直接复用到论文的“功能需求分析”小节里每个模块配一两张页面截图谁看了都知道系统是完整的。我当时就是先把这张表发给导师确认他确认后我再开工省得后面做偏了返工。2. 技术选型分析与开发环境搭建框架、原生PHP怎么选2.1 原生PHP与主流框架的取舍答辩时怎么解释这是个老生常谈的问题但在毕设场景下答案很具体。用ThinkPHP、Laravel这类框架开发效率确实高路由、ORM、模板引擎都现成但答辩老师只要追问一句“你知道这个Model是怎么映射到表的吗”“控制器是怎么被路由到的”很多人就答不上来。用原生PHP所有请求处理、数据库连接、SQL拼接、Session管理都是自己写的做得慢一点但每一个坑都是自己踩出来的问什么都能接上话。我最终选了原生PHP MySQL PDO这套组合。理由有三条这三条后来也直接写进了论文的“技术选型”一节能完整展示Web开发基本功请求生命周期、数据库预处理、会话管理、文件上传这些底层机制用框架会被掩盖用原生PHP必须自己实现。答辩友好老师问“你的项目怎么防SQL注入”我可以直接指着代码里的预处理语句讲不用绕到框架的ORM机制上。部署简单原生PHP对服务器要求极低虚拟主机都能跑后面上线演示省掉一堆环境问题。当然如果你对某个框架特别熟用框架也没问题但前提是你要能讲清楚框架帮你做了什么、底层原理大概是什么。怕就怕那种框架没吃透、原生也没写过的状态答辩一戳就破。2.2 从零搭出可复用的项目目录结构与数据库配置环境搭建我用的是PHPStudyWindows下直接集成Apache/Nginx PHP MySQL对毕设来说省心。如果你已经装了别的集成环境也没关系关键是PHP版本别太老建议7.4以上因为下面要用到password_hash、finfo这些函数老版本支持不好。项目目录我建议这样规划简单但分层清晰project/ ├─ admin/ # 后台管理端 │ ├─ index.php # 登录页/入口 │ ├─ competitions.php # 赛事管理 │ ├─ teams.php # 战队管理 │ ├─ players.php # 选手管理 │ ├─ matches.php # 赛程管理 │ └─ score.php # 比分录入 ├─ includes/ │ ├─ config.php # 数据库配置 │ ├─ db.php # PDO连接 │ ├─ functions.php # 通用函数校验、分页、上传 │ └─ sess_check.php # 后台登录验证 ├─ public/ # 前台展示 │ ├─ index.php │ ├─ competition_detail.php │ └─ team_detail.php ├─ uploads/ # 上传的队标、头像 │ ├─ logos/ │ └─ avatars/ └─ sql/ └─ init.sql # 初始化建库脚本其中includes/config.php是全局基础我当时踩过的第一个坑就在这里——PDO连接字符串里漏了charsetutf8mb4导致前端页面里输入中文存储正常但控制台查出来的表数据却出现乱码。正确的连接方式应该把字符集显式写进DSN?php $host 127.0.0.1; $port 3306; $dbname esport_db; $user root; $pass root; $dsn mysql:host$host;port$port;dbname$dbname;charsetutf8mb4; $options [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]; try { $pdo new PDO($dsn, $user, $pass, $options); } catch (PDOException $e) { exit(数据库连接失败: . $e-getMessage()); }PDO::ATTR_EMULATE_PREPARES false这行很关键它让PDO走真正的预处理而不是本地模拟能更有效防SQL注入。我当时写代码时记得加上但在论文里忘了写理由后来导师提醒才补上你们写论文时最好把每一项配置意图都写清楚评审会觉得你真懂。3. 数据库建模从赛事、战队到比分的关联设计3.1 核心表结构和关键SQL一个完整的建表实例数据库是这类系统的灵魂。我设计了六张核心表它们之间的关系是赛事competitions一对多比赛matches战队teams一对多选手players比赛通过team_a_id和team_b_id关联两个战队比分和胜方直接冗余在比赛表里避免额外join。表清单如下表名职责关键关联admin_users管理员账号无competitions赛事信息一对多 matchesteams战队信息一对多 players被 matches 引用players选手信息多对一 teamsmatches比赛记录多对一 competitions两个战队外键news赛事公告/新闻多对一 competitions可选以比赛表为例这是全系统最核心的一张表建表SQL建议直接保存到sql/init.sql里CREATE TABLE matches ( id int(11) NOT NULL AUTO_INCREMENT, competition_id int(11) NOT NULL COMMENT 所属赛事ID, round_type varchar(20) NOT NULL DEFAULT 小组赛 COMMENT 轮次小组赛/半决赛/决赛, team_a_id int(11) NOT NULL COMMENT A队ID, team_b_id int(11) NOT NULL COMMENT B队ID, match_time datetime NOT NULL COMMENT 比赛时间, venue varchar(100) DEFAULT NULL COMMENT 比赛地点/线上房间, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0未开始 1进行中 2已结束, team_a_score tinyint(4) DEFAULT NULL COMMENT A队大比分, team_b_score tinyint(4) DEFAULT NULL COMMENT B队大比分, winner_team_id int(11) DEFAULT NULL COMMENT 胜方战队ID, detail varchar(200) DEFAULT NULL COMMENT 单局小分记录如2:1, 1:0, 0:1, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_competition (competition_id), KEY idx_match_time (match_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT比赛记录表;这里有两个容易忽略的设计点。第一个是team_a_score和team_b_score用tinyint因为电竞比赛大比分一般不超过5BO5没必要用int第二个是把team_a_id和team_b_id各建索引因为前台查赛程、后台查某支战队的比赛列表都靠这两列过滤没索引表一大了必慢。建表时统一用InnoDB和utf8mb4几乎是默认规则了但你最好在论文里解释一下为什么不用MyISAM——InnoDB支持事务和外键约束对比赛表这种频繁更新状态和比分的场景更安全。我当时解释完以后导师的眼神立刻就不一样了。3.2 业务约束设计赛程、比分与状态流转的边界条件表建好只是第一步关键是业务约束怎么落地。我这里说三类最常见的约束每类都对应真实业务场景同一场比赛不能是同一个战队打自己。写入matches前必须判断team_a_id ! team_b_id这种判断放后端PHP做就行不用上升到数据库约束除非你连触发器都敢写。比赛时间要落在所属赛事的起止时间内。否则会出现赛事都结束了还有比赛安排在三天后的荒谬数据。实现上可以在赛事管理页做后端校验start_date match_time end_date。比分校验。电竞常见规则是大比分不能平局决赛局小分记录要能自洽。这部分我放在后面的比分录入模块详说。状态流转我用一个字段status控制0未开始1进行中2已结束。这个数字在页面上会翻译成中文标签后台列表里还可以根据状态改变行的背景色看着直观。当时有同学建议我拆分出“已取消”状态我没采用因为毕设场景下多一个状态就多一套流转规则和界面判断整体复杂度上升不少不如先把主流程做扎实。4. 后台核心模块开发登录鉴权、赛事管理与上传处理4.1 管理员登录与Session安全的落地写法后台登录是最先要做的模块因为后面所有管理页面都要套一层访问控制。我这块用了三层防护都不是黑科技但有效第一层是密码存储用password_hash()绝不存MD5。很多教程还在用MD5加密密码那玩意查彩虹表几秒钟就破了。正确做法是注册时存hash登录时用password_verify()比对// 登录处理 login.php session_start(); require_once includes/db.php; $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; if ($username || $password ) { exit(用户名和密码不能为空); } $stmt $pdo-prepare(SELECT * FROM admin_users WHERE username ? AND status 1 LIMIT 1); $stmt-execute([$username]); $admin $stmt-fetch(); if ($admin password_verify($password, $admin[password_hash])) { session_regenerate_id(true); // 防止会话固定攻击 $_SESSION[admin_id] $admin[id]; $_SESSION[admin_name] $admin[real_name]; header(Location: index.php); exit; } exit(用户名或密码错误);第二层是每次请求都校验是否登录。我写了一个includes/sess_check.php每个后台页面第一行就require它?php session_start(); if (empty($_SESSION[admin_id]) || empty($_SESSION[admin_name])) { header(Location: login.php); exit; }第三层是简单的CSRF令牌。毕设项目不上https跨站请求伪造的风险确实存在。我在每个后台表单里埋一个隐藏字段input typehidden namecsrf_token value?php echo $_SESSION[csrf_token]; ?提交时校验是否一致。生成令牌的代码放在登录成功后$_SESSION[csrf_token] bin2hex(random_bytes(32));4.2 赛事CRUD与分页列表做了几套后台后总结的常规写法赛事管理是后台最标准的一套增删改查。新增和编辑的逻辑套路都一样接收表单数据校验必填项预处理SQL写入/更新成功后重定向回列表页。我重点说两个容易被忽略的点。第一个是分页。列表页不能一次性把所有数据查出来用LIMIT 偏移量做分页。我封装了一个简单的分页函数思路是先查总数再算总页数最后取当前页数据function pagination($pdo, $table, $page, $pageSize 10, $where is_deleted 0) { $page max(1, (int)$page); $offset ($page - 1) * $pageSize; $countSql SELECT COUNT(*) FROM {$table} WHERE {$where}; $total $pdo-query($countSql)-fetchColumn(); $pages max(1, (int)ceil($total / $pageSize)); $limit min($pageSize, 200); // 防止手误导致超大查询 $sql SELECT * FROM {$table} WHERE {$where} ORDER BY created_at DESC LIMIT {$offset}, {$limit}; $list $pdo-query($sql)-fetchAll(); return [list $list, page $page, pages $pages, total $total]; }注意我把$table和$where直接拼接进了SQL这有SQL注入风险所以这个函数的调用方必须是内部写死的表名和条件不能直接接收用户输入。如果你不放心可以用白名单方式过滤表名。我当时在函数注释里写明了这一限制防止自己后来手滑。第二个是删除策略。我用了软删除也就是给表加一个is_deleted字段删除时执行UPDATE competitions SET is_deleted 1 WHERE id ?列表查询默认只查is_deleted 0。为什么不用DELETE原因很实在赛事的比赛记录、公告记录都关联着它物理删除会把数据链弄断而且毕业设计答辩时老师问“删除赛事会不会把关联比分也删掉”用软删除就能很体面地解释。当然后台可以保留一个“彻底删除”的隐藏操作但我不建议默认展示出来。赛事状态流转这块我在编辑页放了一个下拉框管理员可以手动把赛事状态从“未开始”改成“进行中”再改成“已结束”。加了个简单的联动逻辑状态改为“已结束”时系统自动把所有未结束的比赛标记为已结束避免出现赛事结束而比赛还在踢的脏数据if ($status 2) { $pdo-prepare(UPDATE matches SET status 2 WHERE competition_id ? AND status 2) -execute([$competitionId]); }4.3 战队选手管理与文件上传最容易出错的环节战队和选手管理本身不难难的是文件上传。队标、选手头像都要传图片这块我一共踩了三个坑你们做的时候一定要绕开。第一个坑是只检查了文件后缀名没检查MIME类型。老外上传一个改成.jpg后缀的恶意脚本就能绕过去。后来我改成用finfo扩展读真实文件类型白名单只放行image/jpeg、image/png、image/webp三种if ($_FILES[logo][error] UPLOAD_ERR_OK) { $finfo new finfo(FILEINFO_MIME_TYPE); $mime $finfo-file($_FILES[logo][tmp_name]); $allowed [image/jpeg, image/png, image/webp]; if (!in_array($mime, $allowed, true)) { exit(仅支持 JPG/PNG/WebP 格式的图片); } if ($_FILES[logo][size] 2 * 1024 * 1024) { exit(图片大小不能超过2MB); } $extMap [image/jpeg jpg, image/png png, image/webp webp]; $ext $extMap[$mime]; $newName date(Ymd) . _ . uniqid() . . . $ext; move_uploaded_file($_FILES[logo][tmp_name], ../uploads/logos/ . $newName); }第二个坑是上传文件名。直接用用户原始文件名存进uploads目录会有重名覆盖和路径穿越问题。上面代码里用date uniqid重新命名有效避免这两个问题。第三个坑是删除数据时没有同步删除图片文件。后来我写了个clear_file()函数在删除战队或选手记录时先查旧路径再用unlink删掉磁盘文件避免服务器上堆一堆没人引用的垃圾文件。毕设项目虽然不会跑很久但这个习惯挺好值得保留。5. 前台展示与比赛比分模块让数据真正流动起来5.1 首页与详情页的数据展示JOIN查询比子查询靠谱前台的核心目标是让访客快速看到系统里有什么内容。首页我放了三块最新赛事、近期赛程、热门战队。热门战队的判定用了最简单的胜场数排序这块在论文里可以写“基于简单统计的排行展示”。赛程展示是最能体现SQL水平的页面因为matches表里存的是team_a_id和team_b_id两个外键但页面要显示的是战队名称直接查matches表只能看到一串数字。我当时第一次写的时候天真地用了两个子查询效率不行代码也啰嗦。后来改成两个LEFT JOIN一个SQL搞定$sql SELECT m.*, ta.team_name AS team_a_name, tb.team_name AS team_b_name FROM matches m LEFT JOIN teams ta ON m.team_a_id ta.id LEFT JOIN teams tb ON m.team_b_id tb.id WHERE m.competition_id ? ORDER BY m.match_time ASC; $stmt $pdo-prepare($sql); $stmt-execute([$competitionId]); $matches $stmt-fetchAll();LEFT JOIN和INNER JOIN在这个场景下等价因为team_a_id和team_b_id一定指向存在的战队新增比赛时做了校验但LEFT JOIN写在这里更安全——即使某支战队被删了赛程页也不会白屏战队名显示为“待定”就行。页面里对status字段做翻译0显示“未开始”1显示“直播中”2显示“已结束”。已结束的比赛再根据winner_team_id高亮胜方这样用户扫一眼就知道结果。赛事详情页则要展示三块信息赛事基本信息时间、地点、项目、奖金、赛事赛程列表、赛事相关公告。这里我把赛事、赛程、公告放在一个页面上通过competition_id把三块数据串起来页面看起来信息量很足答辩演示时一页抵三页很划算。5.2 比分录入与校验逻辑不只是存两个数字比分录入是整个系统里最需要认真做的地方。表面上是给team_a_score和team_b_score各填一个数字实际上要做三层校验第一层是大比分不能相等。电竞比赛的决赛阶段不允许平局所以team_a_score ! team_b_score。如果你做的是小组赛积分制那允许平局这块要根据具体赛事规则调整。第二层是胜方自动判定。录入比分时不用手动选谁赢直接由比分决定if ($teamAScore $teamBScore) { exit(大比分不能相同请确认录入是否正确); } $winnerId $teamAScore $teamBScore ? $teamAId : $teamBId;第三层是小分自洽。比如BO3的大比分2:1小分记录应该是三局的具体比分像2:1, 1:0, 0:1中间用逗号分隔存到detail字段里。我当时用了一个简单的字符串校验按逗号拆分成数组数组元素个数应该等于大比分之和且每个元素都符合数字:数字的格式。这块代码不长但能挡住大部分手工录入的明显错误$parts explode(,, $detail); if (count($parts) ! ($teamAScore $teamBScore)) { exit(小分局数与总比分不一致请重新填写); } foreach ($parts as $part) { if (!preg_match(/^\d:\d$/, $part)) { exit(小分格式应为“2:1”或“1:0”); } }比分录入完还要顺手更新比赛状态为“已结束”、赛事状态按需调整保证前台页面拉到最新数据。所有更新都包在一个事务里避免写了一半断电导致比分和状态对不上$pdo-beginTransaction(); try { $pdo-prepare(UPDATE matches SET status 2, team_a_score ?, team_b_score ?, winner_team_id ?, detail ? WHERE id ?) -execute([$teamAScore, $teamBScore, $winnerId, $detail, $matchId]); $pdo-prepare(UPDATE competitions SET status 2 WHERE id ? AND status 2) -execute([$competitionId]); $pdo-commit(); } catch (Exception $e) { $pdo-rollBack(); exit(比分保存失败已回滚 . $e-getMessage()); }5.3 开发调试与常见坑编码、时区、错误提示这节算是我个人的踩坑合集写出来帮大家少走弯路。第一个坑是时区。PHP默认时区是UTC如果你不设置存进数据库的created_at全是UTC时间而比赛时间是后台管理员手动填的北京时间两边就会出现8小时偏差。解决方法是全局配置里加一行date_default_timezone_set(PRC);注意这行要在连接数据库之前执行并且每个入口文件都要生效。如果你用的是框架通常有配置文件但原生PHP就得自己记着。第二个坑是错误显示。开发阶段要把错误打开部署阶段必须关掉。我当时在config.php里做了环境判断if (defined(DEV_MODE) DEV_MODE true) { error_reporting(E_ALL); ini_set(display_errors, 1); } else { error_reporting(0); ini_set(display_errors, 0); }开发时打开错误提示能帮你立刻定位问题但如果上线还开着你等于把服务器路径、SQL语句直接暴露给访客非常危险。第三个坑是页面编码。UTF-8编码要在三处保持一致HTML的meta charsetutf-8、数据库表字符集、PDO的DSN字符集。任何一处掉了链子中文就会出现乱码。我自己曾经只漏了PDO那一处测试新增战队时发现中文队名存进去变成了问号排查了半小时才发现是DSN字符串里没有charsetutf8mb4。这种错误不致命但特别浪费时间建议环境搭好先做一次“中文增删改查”的冒烟测试。第四个坑是调试工具。纯var_dumpdie()虽然能用但效率太低。我后来发现用error_log()把调试信息写到日志文件里比打印到页面更干净。加上Xdebug做断点调试定位逻辑错误的速度明显提升。毕设不需要太复杂但var_dump用得多了你会发现页面结构都被打乱了不如养成写日志的习惯。6. 测试用例与论文写作答辩前的最后冲刺6.1 设计一套正经的功能测试用例顺便给论文攒素材功能都开发完以后别急着截图写论文。先用正经的测试用例把系统跑一遍既能提前发现问题又能给论文的“系统测试”章节攒素材。我当时的测试用例表大概长这样编号模块测试步骤预期结果实际结果是否通过TC01登录输入错误密码提示“用户名或密码错误”与预期一致通过TC02登录未登录直接访问后台index.php跳转回login.php与预期一致通过TC03赛事管理新增赛事必填项留空提示“赛事名称不能为空”与预期一致通过TC04战队管理上传一个1MB的PNG图片上传成功并显示队标与预期一致通过TC05战队管理上传一个改后缀的PHP文件拒绝上传提示格式不支持与预期一致通过TC06比分录入录入大比分2:2提示“大比分不能相同”与预期一致通过TC07赛程展示查看已结束比赛的详情页胜方高亮显示与预期一致通过大概准备20到30条这样的用例就够撑起论文的测试章节了。写的时候注意把测试日期、测试环境PHP版本、MySQL版本、浏览器版本都标注清楚评审老师会关注测试的规范性。我还有个习惯测试通过的功能顺手截一张图存到一个叫docs/screenshots的文件夹里写论文的时候随手就能取用不用回头再开环境补截图。6.2 论文结构、图表与答辩高频问题论文结构我建议按本科毕业设计的经典框架来但每章内容要结合你自己的实现细节别套模板第一章绪论写电子竞技行业背景、信息管理系统的意义然后写国内外现状。现状部分不用写太深结合校园赛事组织的特点总结一下就行。第二章需求分析直接把你一开始画的功能模块表放进去再补充用例图和流程图。用ProcessOn或者draw.io画向量图导出清晰度高打印出来不糊。第三章总体设计放系统架构图前端-后台-PHP处理-MySQL存储这条链、功能结构图、ER图。ER图是重点我建议用规范的三线表配合实体关系图评审一眼就能看懂。第四章数据库设计逐表列出字段、类型、注释、约束重点表把建表SQL放进去再画一个表间关系图。第五章详细设计与实现这部分是篇幅大头按功能模块分节每个模块先放页面截图再配核心代码片段和实现说明。代码不要整页贴只贴最有代表性的逻辑。第六章系统测试直接复用测试用例表写几个典型测试的实际操作和结果分析。第七章总结写系统解决的问题、不足与展望。答辩被问到最多的问题我提前准备了一下你们也可以参考“为什么选PHP不选Java”——重点是PHP开发效率高、原生部署轻量、适合中小型Web系统再结合你实际做了什么说。“数据库为什么这样设计”——把表间关系和主要约束逻辑讲清楚尤其是比分冗余在比赛表里的设计意图。“系统还有什么不足如果继续做会加什么”——我当时的回答是加一个用户前台注册登录功能让战队自己报名参赛再把积分排名做成自动化算法。诚实加上可行的改进方案比硬说“系统已经完美”好得多。我个人做这个项目最大的感触是毕业设计真正的收获不是说代码写得多花哨而是把一个需求从模糊想法一路推到可运行的完整系统——这个过程中培养的问题拆解能力和Debug耐力比系统本身值钱得多。如果你正在做类似的题目希望这篇能帮你把路线铺得更顺。最后再分享一个小建议开发途中每完成一个模块就备份一次数据库结构和关键代码改成zip丢到网盘里这习惯在某个深夜发现自己改崩了核心逻辑时真的能救你一命。