PHP活动报名系统源码部署与二次开发:防重复与并发优化 简介一套基于PHP构建的活动报名系统源码包面向PHP初中级开发者及需要快速搭建线上活动平台的技术人员。系统覆盖用户注册登录、活动浏览与报名、后台管理、支付相关逻辑等常见业务模块可用于讲座、研讨会、比赛等场景的线上组织与管理。压缩包共2000个文件主要包含PHP业务代码、JavaScript脚本、CSS样式、Markdown/文本说明文档及JSON配置等整体体积仅13.49MB便于下载与部署。目前已有118人学习下载具有一定的实践参考价值。源码整体采用MVC分层思路包含入口文件、配置、控制器、模型、视图及数据库脚本等典型结构并涉及数据验证、安全性处理、Ajax异步交互等细节。开发者可借此系统学习PHP完整项目组织方式也可基于现有模块进行功能扩展或二次开发是理解活动报名类Web应用的实用素材。1. 一个 PHP 活动报名系统源码包值得你花三天去跑通吗网盘里存着「基于PHP的活动报名系统PHP源码.zip」的人不少真正在三天内把它跑起来的人却不多。这类老源码默认你懂 PHP 环境、懂数据库导入、懂目录权限新手常常卡在解压后的 HTTP 500熟手则纠结它有没有防重复提交、扛不扛得住报名高峰。我把「拿到这样一个源码包之后该做什么」完整过一遍先认目录再跑起来改四个必改点最后给一套排查清单。适合两类人要用它快速上线社团或门店活动报名页的运营以及想在旧代码上练手二次开发的 PHP 初学者。按下面这套顺序走你至少能在一个晚上把它部署到本地并知道上线前要补哪些东西。2. 先看懂源码再动手活动报名系统的目录结构与数据流2.1 解压 ZIP 后先认目录从入口文件与 SQL 文件推断项目全貌解开「基于PHP的活动报名系统PHP源码.zip」之后别急着双击 index.php。老式 PHP 项目的文件组织没有统一规范但骨架大同小异。我拿到压缩包的习惯是先扫根目录看有没有下面这些文件和文件夹。文件/目录常见作用判断要点index.php前台入口通常是活动列表页直接访问能否出页面admin/ 或 manage/后台管理目录后台登录入口一般在这里config.php 或 inc/config.inc.php数据库连接与站点配置上线前必改install.sql 或 data.sql建表与初始数据脚本没有它项目跑不起来upload/ 或 uploads/图片、报名附件上传目录注意写入权限templates/ 或 view/页面模板有它说明做过简单前后台分离然后重点看两个文件。有 install.sql 说明项目靠「导入数据库」初始化有 config.php 说明项目靠「改配置」连库。两者都有这是最常见的「半成品可跑」结构两者都没有源码多半还带一个 install.php 向导让你在浏览器里一步步填数据库信息。判断错了后面会绕远路所以第一步一定是看目录而不是先改代码。解压时另一个高频雷是 ZIP 伪加密。现象是你知道密码解压工具却提示密码错误或文件损坏。原因压缩包的加密标志位被工具改过解压软件误判整个包「有密码」。处理办法是换用带自动修复功能的解压工具重试。如果压缩包里的文件名是中文在 Linux 下解压出来全乱码可以试试unzip -O GBK指定编码。这一步属于纯文件操作和 PHP 代码无关但它卡住你十分钟很正常。2.2 一条报名数据流从表单渲染到写入 MySQL 的全过程老式 PHP 报名系统的数据流不复杂通常是「页面即脚本」一个 PHP 文件既负责渲染表单也负责接收提交。极少有 MVC 分层这也是源码 zip 体积通常只有几 MB 的原因。用一段简化示意来还原最常见的写法?php // register.php 报名页 // 依赖 ../../config.php 里已定义数据库连接常量 require_once __DIR__ . /../../config.php; // 1. 读取当前可报名的活动列表渲染下拉框 $activities $pdo-query(SELECT id, title FROM activity WHERE status 1)-fetchAll(); // 2. 用户点了提交进入写入分支 if ($_SERVER[REQUEST_METHOD] POST) { $activityId (int)($_POST[activity_id] ?? 0); $name trim(strip_tags($_POST[name] ?? )); $phone trim($_POST[phone] ?? ); // 3. 最简单的服务端校验旧源码里经常比这个还少 if ($activityId 0 || $name || !preg_match(/^1[3-9]\d{9}$/, $phone)) { exit(请填写有效的活动、姓名和手机号); } // 4. 写库并跳转避免用户刷新页面重复提交 $stmt $pdo-prepare(INSERT INTO signup (activity_id, name, phone, created_at) VALUES (?, ?, ?, NOW())); $stmt-execute([$activityId, $name, $phone]); header(Location: success.php?id . $activityId); exit; } ?逻辑说明这段代码把「渲染表单」和「处理提交」放在同一个文件里。$_SERVER[REQUEST_METHOD]判断用户是不是点了提交按钮写库成功后用header(Location: ...)跳转到成功页。这个跳转很关键它把「POST 写库」和「刷新页面」隔离开否则用户按 F5 就会重复插入一条报名记录。参数说明strip_tags去掉姓名里的 HTML 标签preg_match(/^1[3-9]\d{9}$/)是最常见的手机号格式校验。要注意$_POST[xxx] ?? 0这种写法是 PHP 7.0 引入的如果源码运行环境是 PHP 5需要改成isset($_POST[xxx]) ? $_POST[xxx] : 0。判断这一点能帮你快速确认手里的源码到底是哪个年代的产物。2.3 数据库设计活动表、报名表、管理员表各管什么老报名系统的 SQL 文件通常手工编写表名可能是activity、signup、admin也可能是huodong、baoming、manager。不管叫法怎么变核心就是三张表。一张管活动一张管报名记录一张管后台登录。下面是一份规范化的建表语句字段可以对照你的源码调整-- 活动表 CREATE TABLE activity ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(128) NOT NULL COMMENT 活动名称, max_people INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 名额上限0 表示不限, joined INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 当前报名人数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 开放报名 0 关闭, created_at DATETIME NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 报名表 CREATE TABLE signup ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, activity_id INT UNSIGNED NOT NULL COMMENT 关联 activity.id, name VARCHAR(64) NOT NULL, phone VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_activity (activity_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 后台管理员表 CREATE TABLE admin ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL COMMENT 保存 password_hash() 结果, last_login DATETIME DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段说明activity.joined是冗余字段专门用来在列表页显示「已报/剩余名额」省得每次 COUNT 整张 signup 表admin.password_hash列的意思是密码要存哈希而不是明文。老源码里经常看到password字段存 MD5甚至在 config.php 里写死一个admin/admin888的默认账号上线前一定要换掉。建表之后用SHOW TABLES;看一下实际库里的表名再决定改 SQL 还是改 PHP。很多老 zip 包里的 SQL 文件和代码里的表名不是一回事直接导入报错是常态。另外老库的 CHARSET 往往是utf8或gbk如果页面中文乱码先查表字符集不要急着改代码。3. 把 PHP 活动报名系统跑起来环境选型与三个必改参数3.1 环境选型Windows 集成环境省事Docker 适合要交付的场景跑老 PHP 源码最常见的有两条路。第一条是本地装一套集成环境Apache、PHP、MySQL 一次装齐适合自己电脑上调试新手从这条路起步最顺。第二条是用 Docker Compose 把环境固定成镜像适合要把项目交付给别人、或者希望换机器后还能一键复现的场景。我自己做交付时基本走 Docker因为不会再被「我机器上明明是好的」这种黑匣子问题反复折磨。给一份能直接用的docker-compose.ymlservices: web: image: php:7.4-apache ports: - 8080:80 volumes: - ./src:/var/www/html depends_on: - db db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: signup volumes: - ./mysql-data:/var/lib/mysql参数说明php:7.4-apache镜像自带 Apache 和 PHP 解析./src挂载到容器网站根目录改代码即时生效。mysql:5.7是老源码兼容性相对稳的版本如果你手上的项目用了 PHP 8 跑不起来多半是老代码里的隐式类型转换、mysql_*函数或each()这类被移除的语法退回 7.4 往往能省下半天排查时间。启动命令就两条docker compose up -d起服务docker compose logs -f web看日志。数据库初始化不用进容器宿主机直接执行docker compose exec -T db mysql -u root -proot signup install.sql就能把老 zip 里的 SQL 导进去。提示老代码在 PHP 8 下的兼容性问题主要出在三个地方mysql_*函数被移除、implode()参数顺序变更、以及字符串和数字比较变严格。排查时先确认源码面向的 PHP 版本再决定环境怎么配。3.2 导入数据库并修改配置文件三个必改参数跑起来之前不管源码用哪种方式组织有三个参数是每次必改的。改错一个表现都是页面能开但登录不上或者报名提交后数据不知道写到哪去了。第一个是数据库连接。老源码的配置集中在 config.php也可能散落在inc/common.php、db.php里。典型写法是这样的?php // config.php 数据库连接配置 define(DB_HOST, 127.0.0.1); // 数据库地址本机用 127.0.0.1 define(DB_PORT, 3306); // MySQL 端口Docker 场景通常也是 3306 define(DB_NAME, signup); // 数据库名要与导入 SQL 时的库名一致 define(DB_USER, root); // 数据库账号 define(DB_PASS, root); // 数据库密码 // 站点根地址决定后台跳转和表单提交回哪里 define(BASE_URL, http://localhost:8080/);第二个是数据库本身。如果是命令行方式先建库再导入mysql -u root -p -e CREATE DATABASE IF NOT EXISTS signup DEFAULT CHARACTER SET utf8mb4; mysql -u root -p signup install.sql参数说明第一句建库第二句把 install.sql 里的建表语句和初始数据导入。注意导入前看一眼 SQL 文件开头有没有CREATE DATABASE有的话第二句可以省略库名SQL 文件里若指定了别的库名以它为准否则配置里的 DB_NAME 永远对不上。第三个是站点根地址和目录权限。BASE_URL 决定表单提交后跳转去哪常见错误的是写成http://localhost/signup/但实际部署在子目录跳转 404。upload 目录则要保证 PHP 进程有写权限否则用户传头像、上传附件时直接报「目录不可写」。Linux 下给权限用chown -R www-data:www-data upload/Windows 集成环境一般右键设置写权限即可。3.3 验证最小闭环后台登录、报名提交、数据落库一条线走通环境配好后不要急着二次开发先走一遍最小闭环登录后台创建活动前台提交报名去数据库确认记录有没有落进去。只有这条链路通后面改代码才有参照系。后台入口一般长这样PHP 文件的 session 校验是同一个套路?php // admin/index.php 后台首页 session_start(); if (!isset($_SESSION[admin])) { header(Location: login.php); exit; } // 到这里说明已登录继续渲染后台页面 ?逻辑说明$_SESSION[admin]在登录成功后写入后台每个页面开头都做同样检查。常见改法是给这段逻辑包一层函数require_admin()放到公共文件里比在每个页面复制粘贴更省事。改的时候注意session_start()必须在任何输出之前调用否则会报「headers already sent」。验证闭环时如果某一步断了先别急着改代码。只显示白屏时在 PHP 文件顶部临时加ini_set(display_errors, 1); error_reporting(E_ALL);错误信息会直接打在页面上加了还是没反应再去翻 Apache 的 error_log。学会让错误「显示出来」比学会任何框架都重要。4. 二次开发必改的四个点字段扩展、防重复提交、导出与人数上限4.1 给报名表加字段ALTER TABLE 与表单页同步修改拿到老报名系统运营提出的第一个需求往往是「多收集一个微信号」或者「加一个备注栏」。这需要同时改数据库和表单页只改一边提交时要么报错要么数据丢字段。数据库侧用 ALTER TABLE 加列ALTER TABLE signup ADD COLUMN wechat VARCHAR(64) NOT NULL DEFAULT COMMENT 微信号 AFTER phone, ADD COLUMN remark VARCHAR(255) NOT NULL DEFAULT COMMENT 备注 AFTER wechat;参数说明AFTER phone控制新列出现在 phone 字段之后纯排版需要不影响逻辑。DEFAULT 给空字符串是为了让老数据行也能通过 NOT NULL 约束。表单侧在 register.php 的报名字段后面加对应输入框并在 2.2 那段的接收逻辑里补上取值、校验、入库$wechat trim($_POST[wechat] ?? ); $remark trim(strip_tags($_POST[remark] ?? )); // 入库 SQL 里同步补两个字段这里有一个老源码里反复出现的坑数据库加了字段INSERT 语句没加提交后 PHP 不报错但字段全是默认值或者 INSERT 语句加了字段数据库没加SQL 直接报「Unknown column」。改完两边以后先插入一条测试数据确认落库再让运营试用。4.2 防重复提交表单 Token 与数据库唯一键双保险老报名系统的第二个典型问题用户手滑点了两下提交或者刷新了成功页数据库里就多出一条重复报名。前端禁用按钮只能挡普通用户挡不住 F12 改页面和脚本重放后端校验才是真的。第一层是 Token 校验。渲染报名表单时生成一次性 Token 存进 session提交时对比?php // 渲染表单时 session_start(); $_SESSION[token] bin2hex(random_bytes(16)); ? !-- 表单内的隐藏字段 -- input typehidden nametoken value? htmlspecialchars($_SESSION[token]) ??php // 接收提交时 session_start(); if (!isset($_POST[token], $_SESSION[token]) || !hash_equals($_SESSION[token], $_POST[token])) { exit(表单已过期请返回刷新后重新提交); } unset($_SESSION[token]); // 一次性使用 ?参数说明bin2hex(random_bytes(16))生成 32 位十六进制随机串比老代码里用md5(uniqid())更不可预测。hash_equals用恒定时间比较避免时序攻击。unset之后同一个 Token 刷新页面再提交会被拒绝。第二层是数据库唯一键。Token 只防「同一表单重复提交」防不住「同一个人用两个页面报两次」。给报名表加唯一约束ALTER TABLE signup ADD UNIQUE KEY uniq_activity_phone (activity_id, phone);加上之后同一个活动同一个手机号只能报一次。入库时要捕获重复键异常给用户一个友好提示而不是让 MySQL 的报错裸奔。两层都做完才算真正把重复提交堵住。注意唯一键要提前评估业务规则。如果运营允许「同一个人帮家人报多个名额」那唯一键就不能加在 activity_id phone 上得改成手机号 姓名或者干脆不加唯一键、改用 Token 防重。先问业务再定约束。4.3 把报名名单导出成 CSV一个改动最小、收益最大的后台功能活动结束运营要名单一个个复制太蠢。给后台加一个 CSV 导出二三十行代码但能让运营把「这个系统能用」变成「这个系统好用」。?php // export_signup.php 后台导出报名名单 CSV session_start(); if (empty($_SESSION[admin])) { header(HTTP/1.1 403 Forbidden); exit(无权访问); } try { $pdo new PDO(mysql:host127.0.0.1;dbnamesignup;charsetutf8, root, root); $pdo-setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $activityId (int)($_GET[activity_id] ?? 0); if ($activityId 0) { exit(缺少活动 ID); } $stmt $pdo-prepare(SELECT name, phone, wechat, remark, created_at FROM signup WHERE activity_id ? ORDER BY created_at DESC); $stmt-execute([$activityId]); $rows $stmt-fetchAll(PDO::FETCH_ASSOC); if (empty($rows)) { exit(暂无报名记录); } // 文件名带时间戳避免覆盖 $filename signup_ . date(Ymd_His) . .csv; header(Content-Type: text/csv; charsetutf-8); header(Content-Disposition: attachment; filename . $filename . ); $fp fopen(php://output, wb); // 写入 BOM否则 Excel 打开 UTF-8 CSV 会乱码 fwrite($fp, \xEF\xBB\xBF); fputcsv($fp, array_keys($rows[0])); foreach ($rows as $row) { fputcsv($fp, $row); } fclose($fp); } catch (PDOException $e) { error_log($e-getMessage()); exit(导出失败请查看服务端日志); } ?逻辑说明第一步校验后台登录第二步按activity_id查报名记录第三步输出文件头第四步逐行写 CSV。fputcsv会自动处理逗号、换行和引号转义比手动拼接字符串安全得多。参数说明\xEF\xBB\xBF是 UTF-8 的 BOM不加它Windows 的 Excel 打开 CSV 时中文全乱。文件名里的date(Ymd_His)保证每次导出不覆盖。HEAD 里的字段顺序由$rows[0]的键顺序决定也就是 SQL 里的 SELECT 顺序想调整列顺序直接改 SELECT 就行。4.4 活动人数上限为什么挡不住超额报名并发场景的翻车现场老源码处理人数上限的方式通常是「先查再插」// 错误写法先 SELECT 数人数再判断是否 INSERT $count $pdo-query(SELECT COUNT(*) FROM signup WHERE activity_id $activityId)-fetchColumn(); if ($count $maxPeople) { exit(名额已满); } $pdo-query(INSERT INTO signup ...);这个写法在单用户测试时没问题一旦两个人同时提交两边都查到「还剩 1 个名额」然后都执行 INSERT最终报名人数超限。这不是玄学是典型的并发竞态。正确做法是用事务加行锁?php // 正确写法事务 SELECT FOR UPDATE 锁住活动行 $pdo-beginTransaction(); try { // 先锁活动行阻塞并发的同活动请求 $stmt $pdo-prepare(SELECT id, max_people, joined FROM activity WHERE id ? FOR UPDATE); $stmt-execute([$activityId]); $activity $stmt-fetch(PDO::FETCH_ASSOC); if (!$activity) { $pdo-rollBack(); exit(活动不存在); } if ($activity[joined] $activity[max_people]) { $pdo-rollBack(); exit(名额已满); } // 名额加一再插入报名记录 $pdo-prepare(UPDATE activity SET joined joined 1 WHERE id ?)-execute([$activityId]); $pdo-prepare(INSERT INTO signup (activity_id, name, phone) VALUES (?, ?, ?)) -execute([$activityId, $name, $phone]); $pdo-commit(); } catch (Exception $e) { $pdo-rollBack(); error_log($e-getMessage()); exit(报名失败请稍后重试); } ?逻辑说明SELECT ... FOR UPDATE会把 activity 表里对应那行锁住直到事务结束。第二个请求执行到同一句时会被阻塞等第一个事务提交后再继续此时它读到的joined已经更新就能正确判断名额已满。参数说明只有activity表是 InnoDB 时行锁才生效MyISAM 引擎不支持行锁会把整个表锁住所以建表语句里ENGINEInnoDB不是可有可无的。joined字段也可以在 UPDATE 时直接判断UPDATE activity SET joined joined 1 WHERE id ? AND joined max_people用受影响行数判断是否超员这种写法更简洁但前提是保证 signup 插入和名额更新在同一个事务里。5. 排查与避坑PHP 活动报名系统最常见的五类运行问题5.1 浏览器打印出源码而不是解析结果或者中文全部乱码现象访问 index.php 时浏览器直接显示一大段 PHP 源码或者在下载文件页面能打开但中文全是「锟斤拷」「」之类的乱码。原因显示源码说明服务器没把.php文件交给 PHP 解析器。Apache 没启用 PHP 模块或者 Nginx 没配置 fastcgi 转发都可能这样。乱码通常是文件编码、数据库字符集、页面 charset 三方不一致叠加出来的。解决集成环境里重新创建站点确认 PHP 模块已启用Nginx 环境确认location ~ \.php$配置存在。乱码问题按顺序检查页面meta charsetutf-8、PHP 文件本身是 UTF-8 无 BOM、数据库表和连接用的都是 utf8mb4。老源码经常是 GBK 文件配 UTF-8 库两边统一成 UTF-8 最省心。5.2 页面 500 或白屏Apache/Nginx 错误日志里什么都没有现象每个页面都白屏或 500错误日志空空如也看起来像个黑匣子。原因PHP 的display_errors默认是 Off错误信息被写进日志而日志文件路径没配好或者日志权限不对你就什么也看不到。另一种常见原因是源码用了mysql_*函数PHP 7 之后这些函数被移除页面直接致命错误。解决临时在入口文件顶部加ini_set(display_errors, 1); error_reporting(E_ALL);让错误显示在页面上。如果确认是mysql_connect/mysql_query报未定义函数那就是老 API 迁移问题。最粗暴的迁移是装 PHP 5.6 环境但更好的做法是把调用点改成 PDO。改动量通常不大连接、查询、取行三处换写法最多半天工作量。5.3 表单提交后 404URL 重写与入口文件没配对现象前台活动列表能打开登录后台也没问题但报名表单一点提交就 404。原因表单的action指向一个伪静态地址比如action/signup/save.html而当前服务器的 URL 重写规则没生效。Apache 需要开启mod_rewrite并允许.htaccessNginx 默认不处理.htaccess必须自己写 try_files 规则。解决先看表单实际提交到哪个地址在浏览器地址栏直接访问一次确认文件到底存不存在。Nginx 下给站点加一段通用配置server { listen 80; root /var/www/html; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }参数说明try_files先找真实文件找不到就交给 index.php 处理这是绝大多数 PHP 项目的通用回退规则。fastcgi_pass要指向你实际的 PHP-FPM 监听地址有的是127.0.0.1:9000有的是/run/php/php7.4-fpm.sock这个地址写错的话所有 PHP 页面都会 502。5.4 同一个人报了好几次名前端禁了按钮后端没拦截现象运营说用户反馈「我没重复提交」但名单里同一个手机号出现了三条记录。原因前端 JS 在点击后禁用按钮只对老实用户有效。手快的用户连点、网速慢时重发请求、或者直接 POST 接口后端没有任何校验每条请求都 INSERT 成功。解决把 4.2 节的 Token 校验加上能挡住绝大多数重复提交。再给signup表加唯一键(activity_id, phone)让数据库做最后一道防线。注意唯一键加在已有数据的表上时如果历史数据里已经有重复记录ALTER TABLE 会失败需要先清理重复数据再加约束。5.5 ZIP 伪加密、伪协议与后门文件源码包里真正的雷现象压缩包解压时提示密码错误或者源码里出现一个你不认识的文件比如templates/.cache.php、css/fonts.php后台还时不时跳出陌生 iframe。原因zip 伪加密是压缩工具把加密标志位改成了「有密码」解压软件误判源码后门则是老源码里被人塞进了eval、assert、base64_decode组合成的恶意代码常见载体是php://filter、data://等伪协议出现在文件读取或 include 参数里。解决解压问题换用带自动修复功能的解压工具中文文件名乱码用unzip -O GBK处理。代码安全问题进项目目录先做一次语义扫描grep -rE eval\(|assert\(|base64_decode\(|php://|data://|move_uploaded_file --include*.php .扫描结果里如果eval或base64_decode的参数来自$_GET、$_POST、$_COOKIE基本可以判定是后门。处理办法是删除对应文件再检查 config.php 里的数据库密码和管理员默认账号改成自己的。这一步不做完系统上线以后被拿去做跳转、发垃圾数据都是迟早的事。6. 把报名数据接口化给小程序与 H5 预留一个 API 入口如果你打算把报名入口同时接到小程序和公众号 H5与其复制一套 PHP 页面不如在现有系统旁边补一个 JSON 接口。改动不大却能省掉后续很多重复工作。核心思路是接口只接收 POST返回固定格式的 JSON前端拿 code 判断成功失败不再依赖服务端渲染整张页面。?php // api_submit.php 报名 API header(Content-Type: application/json; charsetutf-8); if ($_SERVER[REQUEST_METHOD] ! POST) { http_response_code(405); exit(json_encode([code 405, msg 仅支持 POST], JSON_UNESCAPED_UNICODE)); } // 兼容跨域调用按实际来源收紧不要直接写 * header(Access-Control-Allow-Origin: https://your-frontend-domain.example); header(Access-Control-Allow-Headers: Content-Type); $body json_decode(file_get_contents(php://input), true); if (!$body) { http_response_code(400); exit(json_encode([code 400, msg 请求体不是合法 JSON], JSON_UNESCAPED_UNICODE)); } $activityId (int)($body[activity_id] ?? 0); $name trim($body[name] ?? ); $phone trim($body[phone] ?? ); if ($activityId 0 || $name || !preg_match(/^1[3-9]\d{9}$/, $phone)) { http_response_code(422); exit(json_encode([code 422, msg 参数不合法], JSON_UNESCAPED_UNICODE)); } try { // 复用 4.4 的事务扣减逻辑名额判断和防重在这里同样生效 $pdo new PDO(mysql:host127.0.0.1;dbnamesignup;charsetutf8, root, root); $pdo-beginTransaction(); $stmt $pdo-prepare(SELECT id, max_people, joined FROM activity WHERE id ? FOR UPDATE); $stmt-execute([$activityId]); $activity $stmt-fetch(PDO::FETCH_ASSOC); if (!$activity) { $pdo-rollBack(); exit(json_encode([code 404, msg 活动不存在], JSON_UNESCAPED_UNICODE)); } if ($activity[joined] $activity[max_people]) { $pdo-rollBack(); exit(json_encode([code 1001, msg 名额已满], JSON_UNESCAPED_UNICODE)); } $pdo-prepare(UPDATE activity SET joined joined 1 WHERE id ?)-execute([$activityId]); $pdo-prepare(INSERT INTO signup (activity_id, name, phone) VALUES (?, ?, ?)) -execute([$activityId, $name, $phone]); $pdo-commit(); echo json_encode([code 0, msg 报名成功], JSON_UNESCAPED_UNICODE); } catch (Exception $e) { $pdo-rollBack(); http_response_code(500); echo json_encode([code 500, msg 服务器内部错误], JSON_UNESCAPED_UNICODE); } ?逻辑说明接口和 2.2 的页面版报名是同一套业务逻辑区别只在输入来源从$_POST变成了 JSON body输出从 HTML 变成了 JSON。事务扣减名额的逻辑可以直接复用 4.4 那一段保证接口和页面两个入口的行为一致。参数说明JSON_UNESCAPED_UNICODE让中文不被转成 \uXXXX调试时一眼能看懂。Access-Control-Allow-Origin按实际域名写死不要用*放行所有来源。接口版本如果有多端调用建议在响应里加一个data字段把报名成功的签名字段或活动剩余名额一并返回前端可以少一次查询。这套老源码给你的不是终点是一个可以继续往上盖的地基。我做过类似的项目最深的一条教训是跑通代码只是第一步真正上线前一定要把 Token 防重、唯一键、名额事务这三件事全部补完否则流量一大问题全在用户端爆发。别急着加花哨功能先把这三个地基打牢后续加接口、加支付、加通知都是顺理成章的事。希望帮到你。本文还有配套的精品资源点击获取