☰
PHP票务管理系统源码:库存扣减与二维码核销幂等实战
2026/10/27 4:57:39 网站建设 项目流程

简介:这份 PHP 票务管理系统源码面向活动票务、演出赛事售票等场景的开发者与二次开发需求,提供一套可运行、可拆解的完整系统参考,也适合课程实训与原型搭建。包内共 2000 个文件,以 PHP 业务代码、HTML 页面、JavaScript 交互脚本、CSS 样式及 PNG/JPG/SVG 等界面素材为主,并含 SQL 数据库文件与配置、函数等支撑文件,压缩包约 25.59MB,目录覆盖前端展示、后台管理与数据存储。系统围绕用户注册登录、活动浏览、在线选座、支付集成、订单状态跟踪、后台活动与票价管理、退款审核及销售报表统计展开,同时涉及 MVC 分层、数据库操作、安全防护和响应式适配等实现思路,便于读者理解票务业务闭环并在此基础上做定制优化。已有 400 人学习,适合需要完整案例对照与功能拆分的中初级 PHP 开发者。

1. 从一张演出票的核销链路看 php票务管理系统源码:它到底解决哪几个硬问题

一场小剧场演出开场前十分钟,检票口最怕的不是人多,而是同一张票被扫两次、库存卖超、后台订单和现场入场对不上。php票务管理系统源码 这类资源,说白了就是把场次、票种、库存、订单、支付回调和二维码核销串成一条可运行链路的票务源码。它适合小型演出场地、展馆活动、主办方自建售票页,也适合想拆票务业务模型的后端开发者。拿到包之后别急着改页面,先把下单扣库存和核销幂等跑通,这两个点决定系统能不能真上场。

2. 环境与目录先落地:PHP、MySQL、Nginx 的选型理由和最小可运行配置

2.1 为什么票务系统常用 PHP 单体:短请求、事务清晰、部署轻

票务系统的核心写操作并不复杂:建场次、配票种、下单占库存、支付回调改状态、扫码核销。每个请求都短,事务边界清楚,PHP-FPM 配 Nginx 的部署方式对中小型场地足够友好。源码包丢到普通云服务器上,装好 PHP 扩展和 MySQL,配一个站点就能跑,排错也直观:先看 Nginx 访问日志,再看 PHP-FPM 错误日志,最后查 MySQL 慢查询和锁等待。

选型理由不是“PHP 万能”,而是这套业务规模下,单体结构更省人力。场次和票种配置频率低,订单和库存写入集中,后台报表按天聚合,MySQL 的 InnoDB 事务能覆盖大部分一致性需求。边界也要说清:热点场次开售瞬间,如果库存行竞争激烈,单靠 PHP 进程内锁或文件锁一定翻车,需要把库存扣减收口到数据库行锁,必要时再加队列和独立库存服务。常见做法是先用唯一索引和事务把底线守住,再谈缓存和异步。

我一般拿到源码先不跑首页,而是看三个位置:Web 根目录是不是public,数据库脚本在不在database或install目录,运行时目录runtime和上传目录public/uploads是否可写。入口目录判断错,后面伪静态、后台登录、上传全都会连带出问题。

2.2 从源码包到可访问:数据库导入、配置文件与伪静态

先把运行环境对齐。PHP 版本以源码根目录的composer.json、README或入口文件注释为准,不要凭感觉选。票务系统常用到pdo_mysql、mbstring、gd、fileinfo、openssl、curl,少一个都可能在上传票图、导出报表或回调验签时报错。

php -v php -m | egrep 'pdo_mysql|mbstring|gd|fileinfo|openssl|curl' mysql --version nginx -v

这段命令用来确认 PHP 版本、扩展、MySQL 客户端和 Nginx 是否就位。php -m过滤出的扩展里,pdo_mysql决定能不能连库,gd影响票图缩略图,fileinfo影响上传文件类型识别,openssl和curl常被支付回调、远程请求用到。缺扩展时先装扩展并重启 PHP-FPM,不要直接改源码绕。

建库和导入脚本时,字符集统一用utf8mb4。票务系统里场次名称、场馆说明、票种备注可能出现特殊字符,utf8容易在导入或导出时截断。

mysql -uroot -p -e "CREATE DATABASE ticket_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -uroot -p ticket_demo < database/ticket.sql

第一行建库,第二行导入表结构和初始数据。ticket.sql的实际路径按源码包调整;如果包里有install.sql或分卷 SQL,按文件名顺序导入。导入前先备份已有库,生产环境不要直接拿开发库覆盖。导入后执行SHOW TABLES;看是否包含场次、票种、库存、订单、核销日志这几类表,缺表就先别配站点。

配置文件常见做法是复制.env.example为.env,再改数据库账号、站点地址和调试开关。后台管理员密码不要明文写进 SQL,用 PHP 的password_hash生成后写入用户表,或者走安装向导。

cp .env.example .env php -r "echo password_hash('ChangeMe_123', PASSWORD_DEFAULT), PHP_EOL;"

password_hash的输出是一段带盐的哈希,直接替换用户表里的密码字段。ChangeMe_123只是示例,实际部署换成强密码。.env里的APP_DEBUG上线必须关,否则异常页会暴露数据库账号和目录路径。

目录权限按“目录 755、文件 644、可写目录 775”处理,不要图省事给整站 777。runtime放缓存和日志,public/uploads放票图或导入文件,这两个目录给 Web 进程用户可写即可。

chown -R www-data:www-data /var/www/ticket find /var/www/ticket -type d -exec chmod 755 {} \; find /var/www/ticket -type f -exec chmod 644 {} \; chmod -R 775 /var/www/ticket/runtime /var/www/ticket/public/uploads

www-data按实际 PHP-FPM 运行用户替换。chown先统一归属,再分别设目录和文件权限,最后单独放开两个可写目录。若上传仍然失败,查php.ini的upload_max_filesize、post_max_size、upload_tmp_dir和open_basedir,不要只盯目录权限。

Nginx 站点配置的关键是root指向public,并用try_files做伪静态。很多票务源码的入口文件在public/index.php,如果 root 指到项目根目录,首页能打开但后台路由、详情页和接口会 404。

server { listen 80; server_name ticket.local; root /var/www/ticket/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php/php-fpm.sock; } location ~* \.(jpg|jpeg|png|gif|css|js|woff2)$ { expires 7d; access_log off; } location ~ /\.(env|git) { deny all; } }

root必须落到 Web 入口目录;try_files把不存在的路径交给index.php,后台路由才不会 404;fastcgi_pass的 socket 路径按服务器实际 PHP 版本改;静态资源单独缓存;.env和.git直接拒绝访问。配完执行nginx -t再 reload,别在语法错误时直接重启。

常见配置项可以按下面这张表核对,键名以源码config目录为准,表里是票务系统里最常改的几项。

配置项作用建议值注意
APP_DEBUG是否显示详细异常生产false上线前必须关
APP_URL站点访问地址实际域名或局域网地址影响回调地址和二维码链接
DB_HOST数据库地址127.0.0.1不要写公网地址
DB_DATABASE数据库名ticket_demo与导入库一致
DB_USERNAME数据库用户独立低权限用户不要用 root 跑站点
DB_PASSWORD数据库密码强密码不提交到代码库
SESSION_DRIVER会话存储单机用文件,多机用共享存储多机部署别用本地文件

提示:先把站点跑通再改业务表。数据库结构一旦被改乱,后台订单和核销日志会对不上,回滚成本比重新导入高。

3. 票种、场次与库存:把“卖票”拆成可校验的数据模型

3.1 票务核心表怎么读:场次、票种、库存、订单四层关系

票务系统最容易写乱的地方,是把“场次”和“票种”揉成一张表。一个场次可以有多个票种,比如普通票、早鸟票、套票;每个票种有独立价格和库存。订单只记录买了哪个场次的哪个票种、数量、金额和状态,库存单独一行,用唯一索引锁住“场次 + 票种”的组合。这样后台改价不影响历史订单,核销也只认订单。

常见表关系是:show_session一场演出一行;ticket_type一个票种一行,挂在场次下;ticket_stock一个场次票种组合一行,存可售库存和已售数量;orders一张订单一行;verify_log一次核销一行。库存表必须有UNIQUE(session_id, ticket_type_id),否则并发下可能插出两行库存,后面扣哪一行都说不清。

CREATE TABLE `show_session` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `title` varchar(120) NOT NULL, `start_at` datetime NOT NULL, `venue` varchar(120) NOT NULL, `status` tinyint NOT NULL DEFAULT 1 COMMENT '1上架 0下架', PRIMARY KEY (`id`), KEY `idx_start_at` (`start_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `ticket_type` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `session_id` int unsigned NOT NULL, `name` varchar(80) NOT NULL, `price` decimal(10,2) NOT NULL, `status` tinyint NOT NULL DEFAULT 1, PRIMARY KEY (`id`), KEY `idx_session` (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `ticket_stock` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `session_id` int unsigned NOT NULL, `ticket_type_id` int unsigned NOT NULL, `stock` int NOT NULL DEFAULT 0, `sold` int NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_session_type` (`session_id`,`ticket_type_id`), CONSTRAINT `chk_stock` CHECK (`stock` >= 0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `orders` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `session_id` int unsigned NOT NULL, `ticket_type_id` int unsigned NOT NULL, `qty` int NOT NULL DEFAULT 1, `amount` decimal(10,2) NOT NULL, `status` enum('pending','paid','used','refunded','closed') NOT NULL DEFAULT 'pending', `pay_trade_no` varchar(64) DEFAULT NULL, `paid_at` datetime DEFAULT NULL, `used_at` datetime DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_status_created` (`status`,`created_at`), KEY `idx_session_type` (`session_id`,`ticket_type_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段建表语句把四层关系落下来:ticket_stock的唯一索引保证一个场次票种只有一行库存,orders.order_no唯一索引防止重复订单号,status用枚举收口状态,amount用decimal(10,2)存金额,不用浮点。CHECK约束在较新的 MySQL 版本会强制,老版本可能忽略,所以业务代码里仍要判断库存不能小于零。idx_status_created给后台按状态和创建时间筛选订单用,idx_session_type给库存查询和报表用。

后台配置场次时,先建场次,再建票种,再初始化库存。初始库存不要直接写死在票种表里,而是插到ticket_stock,后续扣减只动库存表。订单金额必须服务端按票种价格乘数量计算,不能信前端传来的总价。限购数量也要在服务端校验,比如单笔最多 6 张,同一用户同一场次最多 10 张,规则写进下单接口,不要只靠页面提示。

3.2 下单扣库存的事务写法与并发参数

库存扣减只有两条路:数据库行锁事务,或者原子条件更新。前者适合还要同时写订单、写明细、做金额校验的场景;后者适合极简扣减。两条路都要求库存行有唯一索引,都要求“查询库存”和“扣减库存”在同一个事务里,不能先SELECT出来在前端算,再回写。

PHP 里用 PDO 做事务时,先把错误模式设为异常,再beginTransaction,用SELECT ... FOR UPDATE锁住库存行,判断库存足够后扣减并写订单,最后commit。任何异常都rollBack,返回 409 让前端提示库存不足。

<?php $dsn = 'mysql:host=127.0.0.1;dbname=ticket_demo;charset=utf8mb4'; $pdo = new PDO($dsn, 'ticket_user', '******', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); $sessionId = (int)($_POST['session_id'] ?? 0); $ticketTypeId = (int)($_POST['ticket_type_id'] ?? 0); $qty = (int)($_POST['qty'] ?? 1); if ($sessionId <= 0 || $ticketTypeId <= 0 || $qty < 1 || $qty > 6) { http_response_code(422); exit(json_encode(['error' => '参数不合法'])); } $pdo->beginTransaction(); try { // 锁住库存行,防止并发请求同时读到同一份库存 $stmt = $pdo->prepare('SELECT stock, price FROM ticket_stock JOIN ticket_type ON ticket_type.id = ticket_stock.ticket_type_id WHERE ticket_stock.session_id = ? AND ticket_stock.ticket_type_id = ? FOR UPDATE'); $stmt->execute([$sessionId, $ticketTypeId]); $row = $stmt->fetch(); if (!$row || (int)$row['stock'] < $qty) { throw new RuntimeException('库存不足'); } $pdo->prepare('UPDATE ticket_stock SET stock = stock - ?, sold = sold + ? WHERE session_id = ? AND ticket_type_id = ?') ->execute([$qty, $qty, $sessionId, $ticketTypeId]); $orderNo = 'T' . date('YmdHis') . random_int(1000, 9999); $amount = number_format((float)$row['price'] * $qty, 2, '.', ''); $pdo->prepare('INSERT INTO orders(order_no, session_id, ticket_type_id, qty, amount, status) VALUES(?,?,?,?,?,?)') ->execute([$orderNo, $sessionId, $ticketTypeId, $qty, $amount, 'pending']); $pdo->commit(); echo json_encode(['order_no' => $orderNo, 'amount' => $amount]); } catch (Throwable $e) { $pdo->rollBack(); http_response_code(409); echo json_encode(['error' => $e->getMessage()]); }

这段代码的关键参数是session_id、ticket_type_id和qty。qty先做范围校验,防止负数或超大数量绕过页面限制;FOR UPDATE必须在事务内,锁住的是唯一索引命中的那一行库存;金额用数据库里的price乘数量,不取前端传值;订单号用时间加随机数,再靠唯一索引兜底。异常统一回滚,避免库存扣了订单没写。

如果业务只关心扣库存,可以用原子条件更新,少一次查询:

UPDATE ticket_stock SET stock = stock - 1, sold = sold + 1 WHERE session_id = ? AND ticket_type_id = ? AND stock >= 1;

执行后看受影响行数。行数为 1 表示扣减成功,行数为 0 表示库存不足或场次票种不存在。这个写法依赖stock >= 1条件,不需要先查再改,但下单和写订单仍建议放在同一个事务里,否则扣了库存写订单失败,会出现“库存少了订单没有”的黑匣子。

并发参数上,innodb_lock_wait_timeout默认通常够用,热点场次可以适当调低,让等锁请求快速失败并返回“稍后重试”,而不是把 PHP-FPM 进程堆满。库存行锁的粒度已经很小,不要再用LOCK TABLES锁整表。未支付订单要设超时关单,比如 15 分钟,关单时先条件更新pending为closed,再把库存加回去;加库存也要带条件,防止重复关单把库存加两次。

注意:库存表初始数据一定要有唯一索引。没有唯一索引时,重复初始化会插出多行库存,后面扣减只扣到其中一行,报表却把多行加总,查账时非常难定位。

4. 订单、支付回调与二维码核销:三条最容易断的链路

4.1 订单状态机:待支付、已支付、已核销、已退款

订单状态不能散落在各个接口里随手改。票务系统至少要有pending、paid、used、refunded、closed五个状态,并且规定每条边:待支付只能变已支付或已关闭;已支付只能变已核销或已退款;已核销不能再退款,除非先走人工撤销并留日志。状态变更用条件更新,不要先查再改,否则重复回调会把已支付订单再“支付”一次。

当前状态触发动作允许变成关键条件
pending支付回调成功paid订单号存在、金额一致、当前仍是待支付
pending超时关单closed创建时间超过 15 分钟
paid扫码核销used核销令牌有效、未核销过
paid退款成功refunded退款单号唯一、库存按规则回补
used人工撤销paid仅管理员操作,写撤销日志

支付回调的第一条 SQL 应该是条件更新,而不是无条件UPDATE:

UPDATE orders SET status = 'paid', pay_trade_no = ?, paid_at = NOW() WHERE order_no = ? AND status = 'pending' AND amount = ?;

order_no定位订单,status = 'pending'保证只有第一次回调能改成功,amount = ?防止金额被篡改。执行后看受影响行数:1 表示第一次处理成功;0 要去查订单当前状态和金额,不能直接给库存加票或发短信。回调日志表用pay_trade_no做唯一索引,重复通知直接命中唯一键,业务层捕获后返回成功即可。

回调接口还要注意“先落日志再处理业务”。收到通知后先把原始报文、签名、订单号、金额写入回调日志,再执行状态更新;业务处理失败时日志还在,可以人工补单。回调返回给支付端的响应要明确,处理成功返回成功标识,处理失败返回失败标识,不要让支付端一直重试到超时。

4.2 核销接口的幂等设计与扫码端参数

二维码里不能只放订单号。只放订单号,任何人拿到订单号都能伪造核销请求。常见做法是把订单号、过期时间拼成 payload,用独立密钥做 HMAC 签名,二维码内容再 base64 编码。核销端先解出 payload,校验签名和过期时间,再进事务锁订单行。

<?php $secret = getenv('TICKET_SIGN_SECRET') ?: 'replace-me'; $orderNo = 'T202505011230001234'; $expireAt = time() + 86400 * 2; $payload = $orderNo . '|' . $expireAt; $sign = hash_hmac('sha256', $payload, $secret); $qrText = 'TICKET:' . base64_encode($payload . '|' . $sign); echo $qrText;

TICKET_SIGN_SECRET要单独配置,不要和数据库密码共用;expireAt控制二维码有效期,演出结束后不应还能核销;hash_hmac用 SHA-256,核销端用hash_equals比较签名,避免普通字符串比较的时序问题。二维码内容只是载体,真正的状态判断仍在数据库。

核销接口的 PHP 事务写法如下:

<?php $orderNo = $_POST['order_no'] ?? ''; $token = $_POST['token'] ?? ''; $operator = $_POST['operator'] ?? ''; $deviceId = $_POST['device_id'] ?? ''; $pdo->beginTransaction(); try { // 锁订单行,保证同一张票只核销一次 $stmt = $pdo->prepare('SELECT id, status FROM orders WHERE order_no = ? FOR UPDATE'); $stmt->execute([$orderNo]); $order = $stmt->fetch(); if (!$order) { throw new RuntimeException('订单不存在'); } if ($order['status'] !== 'paid') { throw new RuntimeException('订单状态不可核销'); } $pdo->prepare("UPDATE orders SET status = 'used', used_at = NOW() WHERE id = ? AND status = 'paid'") ->execute([$order['id']]); $pdo->prepare('INSERT INTO verify_log(order_id, operator, device_id, token, created_at) VALUES(?,?,?,?,NOW())') ->execute([$order['id'], $operator, $deviceId, $token]); $pdo->commit(); echo json_encode(['ok' => true]); } catch (Throwable $e) { $pdo->rollBack(); http_response_code(409); echo json_encode(['error' => $e->getMessage()]); }

order_no定位订单,token记录扫码内容,operator是检票员账号,device_id是闸机或扫码枪编号。事务里先FOR UPDATE锁订单行,再判断status = 'paid',条件更新仍是paid才改成used,最后写核销日志。verify_log.order_id建议加唯一索引,数据库层面再兜一层重复核销。

扫码端参数要统一:二维码内容带TICKET:前缀,payload 用|分隔,签名放最后一段;核销接口只接收订单号、令牌、操作员、设备号,不接收前端传来的“是否已核销”。核销成功返回订单号、票种、数量、核销时间,失败返回明确原因,比如“已核销”“已退款”“签名过期”。现场网络抖动时,扫码端要有重试按钮,但重试必须走同一个幂等接口,不能每次重试都生成新核销记录。

核销参数含义示例校验点
order_no订单号T202505011230001234存在且状态为已支付
token二维码原始内容TICKET:...签名、过期时间
operator检票员账号gate01后台存在且启用
device_id扫码设备编号GATE-A与场地绑定
verify_time核销时间服务端生成不用客户端时间

5. 避坑与排查:源码跑起来之后最常见的五类翻车

5.1 环境类翻车:伪静态、时区、上传权限

现象:首页能打开,后台登录和演出详情页 404。
原因:Nginx 的root指到了项目根目录,而不是public;或者没配try_files,除入口文件外的路由全被当成静态文件找。
解决:把root改到public,加上try_files $uri $uri/ /index.php?$query_string;,执行nginx -t后 reload。还 404 就看nginx error.log和php-fpm日志,确认请求有没有到index.php。

现象:后台订单时间和实际时间差 8 小时,导出报表跨天。
原因:PHP 时区是 UTC,MySQL 会话时区是系统时区,两边各存各的;或者代码里一处用date(),一处用数据库NOW()。
解决:统一时区口径。PHP 配date.timezone = Asia/Shanghai,MySQL 配default-time-zone = '+08:00',应用层固定一个时区写库。检查时执行SELECT NOW(), @@global.time_zone, @@session.time_zone;和php -r "echo date_default_timezone_get();",两边对齐再跑订单。

现象:上传票图或导入 Excel 失败,页面只提示“上传错误”。
原因:public/uploads或runtime不可写;upload_max_filesize、post_max_size太小;open_basedir限制了临时目录。
解决:按第 2 章给可写目录授权,调大php.ini的上传限制并重启 PHP-FPM,查upload_tmp_dir是否存在。先看 PHP 错误日志里的具体函数和路径,不要反复改表单。

5.2 业务类翻车:库存超卖、重复核销与回调丢单

现象:库存 100 张,压测后卖出 120 张,订单表多出 20 条。
原因:下单接口先SELECT stock再UPDATE stock,中间没有事务和行锁;或者库存表没有唯一索引,扣到了不同行。
解决:用第 3 章的事务加FOR UPDATE,或改成UPDATE ... WHERE stock >= ?的原子条件更新。库存表补UNIQUE(session_id, ticket_type_id),下单前校验qty上限。压测时把成功订单数、库存余量、sold三个数对齐,不要只看页面提示。

现象:同一张票在闸机扫了两次,两次都提示核销成功;或者支付回调重试后订单状态回退。
原因:核销接口先查后改,没有锁和条件更新;回调接口无条件UPDATE,重复通知把已支付订单又处理一遍。
解决:核销走事务加FOR UPDATE,更新时带status = 'paid'条件,verify_log.order_id加唯一索引;回调改成WHERE order_no = ? AND status = 'pending' AND amount = ?,受影响行数为 0 就查日志直接返回。回调日志先落库,业务失败可补单。

现象:支付端显示成功,后台订单还是待支付,库存被占着不放。
原因:回调地址配错、签名密钥不一致、回调处理超时;或者业务先返回成功再写库,写库失败后支付端不再重试。
解决:先查回调日志有没有原始报文,再核对APP_URL和签名密钥。回调处理顺序改成“先落日志、再改状态、最后返回成功”,处理失败返回失败让支付端重试。超时未支付订单走关单任务,关单和回补库存都要带状态条件,防止重复回补。

6. 上线前做一轮“假并发”验证:从压测脚本到对账口径

6.1 用 200 并发下单验证库存边界

正式开售前,我会先把库存改成 100,用脚本发 200 个并发下单请求,看成功数是不是正好 100。用 Python 的线程池就够,不需要复杂压测平台。

import concurrent.futures import requests URL = "http://ticket.local/api/order/create" def buy(i): payload = {"session_id": 1, "ticket_type_id": 1, "qty": 1} try: r = requests.post(URL, json=payload, timeout=5) return r.status_code, r.text[:100] except Exception as e: return "ERR", str(e)[:100] with concurrent.futures.ThreadPoolExecutor(max_workers=80) as ex: for code, body in ex.map(buy, range(200)): print(code, body)

max_workers=80控制并发度,range(200)是请求总数,qty=1保证每单只买一张。跑完统计 200 返回里成功有多少、409 有多少,再查数据库:stock应为 0,sold应为 100,待支付订单应为 100。成功数超过 100 就是超卖,成功数少于 100 但库存为 0 就是扣了库存没写订单,顺着事务日志查。

6.2 对账表与核销日志的验收口径

压测结束不要只看接口返回,要对三张表:订单、库存、核销日志。

对账项计算方式合格口径
订单总数COUNT(*) FROM orders等于成功下单数
库存已售SUM(sold) FROM ticket_stock等于成功下单票数
库存余量SUM(stock) FROM ticket_stock等于初始库存减已售
核销记录COUNT(*) FROM verify_log等于status='used'的订单数
金额合计SUM(amount) FROM orders等于票种单价乘数量再汇总

从那以后我每次接手票务源码,都先把库存扣减和核销日志各跑 200 次再谈页面美化;这套笨办法救过好几次场。希望帮到你。

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

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

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

立即咨询