FEATURED · 精选文章

ThinkPHP收卡网二开实战:订单设计、安全加固与PHP8兼容

发布时间 / 2026/9/16 4:43:07
来源 / 创域科博编辑部
栏目 / 资讯中心
ThinkPHP收卡网二开实战:订单设计、安全加固与PHP8兼容 简介百分百收卡网礼品卡兑换二手礼品卡回收网站源码是一套基于Thinkphp框架开发的礼品卡在线回收交易平台适用于想快速搭建礼品卡转让、回收、变现网站的开发者与企业运营者可解决礼品卡闲置浪费、资金回流慢等痛点。源码支持用户提交卡号卡密完成线上回收交易提供线下回收、API接口对接及自定义交易方式帮助平台实现交易多样化包含后台配置、伪静态规则等关键模块基于ApacheWin环境部署导入数据库、修改conf/config.php即可运行适合二次开发或直接商用。资源包大小约46.96MB内置Thinkphp工程代码、数据库导入文件及站点配置文件目录结构清晰便于开发者快速定位功能模块。目前已有655人浏览学习适合具备一定PHP基础、希望低成本获取完整回收站解决方案的读者参考。1. 拿到 Thinkphp 收卡网源码先分清它解决的业务问题二手礼品卡回收是很实的生意用户手里有京东E卡、天猫超市卡这类闲置卡券平台按当日折扣收进再转售或核销赚差价。「百分百收卡网」核心就是让用户自助提交卡号卡密、按卡种面值实时出价、运营后台审核打款。这类 PHP 源码包大多基于 ThinkPHP 3.2.x前台下单、会员中心、后台管理三件套齐全。流传源码有个共同点业务不复杂复杂的是安全和兼容。ThinkPHP 历史公开漏洞不少卡密要加密存储订单折扣必须下单时锁定老框架在新版 PHP 上还会报一串兼容错误这些才决定源码能不能真正上线。下面按最常见的落地路径捋定数据模型、写主流程、安全加固、PHP 8 兼容与每日对账。2. 收卡网数据模型卡种、卡密与回收订单的三张核心表2.1 先分清「回收」和「兑换」共用一张订单表的理由标题把「礼品卡兑换」和「二手礼品卡回收」放在一起真实业务往往是两条线回收是把卡折现打给用户兑换是用户拿卡换平台上的话费、油卡等虚拟商品。做二开时最省事的做法不是建两套订单表而是在一张回收订单表上加order_type字段0 表示回收折现1 表示兑换商品。这样设计的好处是审核、状态流转、对账逻辑完全共用一套后台。差别只出现在订单创建后的处理分支回收单后续走打款兑换单后续走发货核销。如果流传源码里已经是两套表我会先评估订单量再决定是否合并——量小完全没必要动表结构避免引入无谓的迁移风险。2.2 卡种表和订单表的建表 SQL 与字段约定卡种表承载的是「这个站能收什么、按什么折扣收」。折扣和可收面值都放配置表不要散落在代码常量里运营才能在后台随时调价而不碰代码CREATE TABLE card_type ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL DEFAULT COMMENT 卡种名称如京东E卡, face_values VARCHAR(255) NOT NULL DEFAULT COMMENT 可收面值逗号分隔50,100,200,500, base_rate DECIMAL(5,4) NOT NULL DEFAULT 0.0000 COMMENT 基准回收折扣0.9200 表示 92 折收, pwd_regex VARCHAR(128) NOT NULL DEFAULT COMMENT 卡密格式正则提交时预校验, status TINYINT(1) NOT NULL DEFAULT 1 COMMENT 1收卡 0停收, update_time INT(11) NOT NULL DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT卡种配置表;base_rate用 DECIMAL(5,4)折扣要精确到小数点后四位比如 0.9245float 在计算金额时容易出现 0.30000000000000004 这类误差。face_values存成逗号分隔字符串即可量级不会大到需要单独建面值档位表但提交订单时必须校验面值属于档位内否则用户填个 88 元就能绕过比价。回收订单表是整站最核心的表字段设计直接决定审核和打款好不好写CREATE TABLE recycle_order ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务单号展示给用户, user_id INT NOT NULL DEFAULT 0, card_type_id INT NOT NULL, order_type TINYINT(1) NOT NULL DEFAULT 0 COMMENT 0回收折现 1兑换商品, face_value DECIMAL(10,2) NOT NULL COMMENT 卡面值, rate DECIMAL(5,4) NOT NULL COMMENT 成交折扣下单时锁定禁止审核时重查, pay_amount DECIMAL(10,2) NOT NULL COMMENT 应付金额 面值 × rate, card_no VARCHAR(64) NOT NULL COMMENT 卡号, card_pwd VARCHAR(255) NOT NULL DEFAULT COMMENT 卡密AES 加密后入库, status TINYINT(1) NOT NULL DEFAULT 0, reject_reason VARCHAR(255) NOT NULL DEFAULT , create_time INT(11) NOT NULL DEFAULT 0, audit_time INT(11) NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_order_no (order_no), KEY idx_card_no (card_no), KEY idx_user_status (user_id,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT回收订单表;网上有些源码在card_no上加唯一索引意图是防同一张卡重复提交但一旦加上用户提交错卡密被驳回后想重提同一张卡会被数据库直接挡死。所以这里只保留普通索引防重用应用层判断。card_pwd用 VARCHAR(255)给加密后的密文留足空间别按明文长度设计。2.3 订单状态机的约定与折扣快照订单状态是收卡系统的灵魂前后台所有操作都是在改这个字段status含义谁触发后续动作0待审核用户提交运营核验卡密1已回收审核通过进入打款队列2打款中运营发起等待第三方打款结果3已完成打款确认归档卡密置空4已驳回审核驳回用户可修改重提5已取消超时自动释放占用绝大多数踩坑发生在两个地方。一是审核时重查折扣审核页从card_type表实时取base_rate计算应付金额结果运营改过价之后用户下单时看到的金额和实际到手不一致。正确做法是只信任recycle_order.rate这个下单时的快照pay_amount也在下单时算好固化。二是状态只能前进不能回跳比如已回收不允许直接改回已完成必须经过打款中间态否则对账会乱。3. Thinkphp 收卡主流程实现询价、提交订单与后台审核3.1 询价接口查卡种配置并校验面值档位前台「我要卖卡」页面用户先选卡种、再选面值页面通过 AJAX 调询价接口拿回收金额。这个接口只做读操作不产生任何写库行为即使用户反复点也不会产生脏数据?php namespace Home\Controller; use Think\Controller; class CardController extends Controller { public function price() { $typeId I(get.card_type_id, 0, intval); $face I(get.face_value, 0, floatval); $cardType M(card_type)-where([id $typeId, status 1])-find(); if (!$cardType) { $this-ajaxReturn([code 1, msg 该卡种暂未开通回收]); } $faces array_map(intval, explode(,, $cardType[face_values])); if (!in_array((int)$face, $faces, true)) { $this-ajaxReturn([code 1, msg 当前面值不在回收范围]); } $amount round($face * $cardType[base_rate], 2); $this-ajaxReturn([code 0, data [ rate $cardType[base_rate], amount $amount ]]); } }三段逻辑各管一件事卡种存在且收卡中才继续面值必须在档位内挡掉大部分乱填请求金额用round保留两位小数前端直接展示。注意这里不写 session、不记日志、不做限流限流放在提交接口读接口放开即可。3.2 提交订单卡号去重、折扣快照、卡密加密提交接口是整站风险最高的入口因为它接收卡号和卡密。顺序很重要先查重、再查卡种、最后落库public function submit() { if (!session(?user_id)) { $this-ajaxReturn([code 1, msg 请先登录]); } $cardTypeId I(post.card_type_id, 0, intval); $face I(post.face_value, 0, floatval); $cardNo trim((string)I(post.card_no)); $cardPwd trim((string)I(post.card_pwd)); if ($cardNo || $cardPwd ) { $this-ajaxReturn([code 1, msg 卡号与卡密不能为空]); } // 同一卡号存在处理中的订单则拒绝防止反复提交同一张卡 $exists M(recycle_order)-where([card_no $cardNo])-find(); if ($exists in_array($exists[status], [0, 1, 2], true)) { $this-ajaxReturn([code 1, msg 该卡号已有处理中的订单请勿重复提交]); } // 下单前再查一次卡种确认没有停收、面值是否在档位内 $cardType M(card_type)-where([id $cardTypeId, status 1])-find(); if (!$cardType) { $this-ajaxReturn([code 1, msg 该卡种已停收]); } $faces array_map(intval, explode(,, $cardType[face_values])); if (!in_array((int)$face, $faces, true)) { $this-ajaxReturn([code 1, msg 当前面值不在回收范围]); } // 订单号日期时间 用户ID 随机数保证唯一且能反查用户 $orderNo date(YmdHis) . sprintf(%04d, session(user_id)) . mt_rand(1000, 9999); $data [ order_no $orderNo, user_id session(user_id), card_type_id $cardTypeId, order_type 0, face_value $face, rate $cardType[base_rate], pay_amount round($face * $cardType[base_rate], 2), card_no $cardNo, card_pwd encrypt_card_pwd($cardPwd), // 加密函数定义见 4.3 节 status 0, create_time time(), ]; $orderId M(recycle_order)-add($data); if (!$orderId) { $this-ajaxReturn([code 1, msg 提交失败请稍后重试]); } $this-ajaxReturn([code 0, msg 提交成功, order_no $orderNo]); }这里的查重比数据库唯一索引更合理已驳回、已取消的旧单不拦新提交只有处理中的订单才会挡住。有些源码在用户重提时会先「关联删除」旧单再插新行我一般只做逻辑作废、不物理删除保留历史记录方便日后对账。提示in_array($exists[status], [0, 1, 2], true)的判断是刻意的——状态 4已驳回、5已取消不拦截允许用户修正后重新提交。如果一刀切查所有状态用户就再也没有重提机会。rate取的是下单那一刻的base_rate并直接存进订单表后续审核、打款都只读这个字段。订单号里带上用户 ID运营在后台看到单号就能反查归属不用多一次联表查询。3.3 后台审核与状态流转人工核验卡密后台审核是典型的列表加操作页。运营看到订单后先按pwd_regex校验卡密格式再去对应平台核销最后回到后台点通过或驳回。审核接口代码很短难的是约束好能到达这里的操作public function audit() { if (!session(?admin_id)) { $this-ajaxReturn([code 1, msg 未登录]); } $orderId I(post.id, 0, intval); $action I(post.action, , trim); $order M(recycle_order)-where([id $orderId, status 0])-find(); if (!$order) { $this-ajaxReturn([code 1, msg 订单不存在或已处理]); } if ($action pass) { // 核销成功才允许置为已回收核销失败必须走 reject 分支 $save [status 1, audit_time time(), auditor session(admin_name)]; } else { $save [ status 4, reject_reason I(post.reason, , htmlspecialchars), audit_time time(), ]; } M(recycle_order)-where([id $orderId])-save($save); $this-ajaxReturn([code 0, msg 处理完成]); }where([id $orderId, status 0])这一行是防重复审核的关键先锁定目标状态只有待审核的单子能走到更新逻辑。两个运营同时操作同一单时后到的那个会拿到「订单不存在或已处理」避免状态被覆盖。后台操作前置状态目标状态必须满足的条件通过01卡密核销成功驳回04已填写驳回原因发起打款12金额已二次确认打款确认23第三方打款结果成功4. 收卡网上线前必查的 Thinkphp 漏洞面与接口防刷4.1 ThinkPHP 老版本漏洞清单与最小加固凡是标题带 Thinkphp 的老源码打开ThinkPHP/Library/Think/目录基本都能看到 3.2.3 左右的版本。这个版本历史上被公开过的漏洞集中在路由解析和 SQL 注入漏洞库都有收录这里只讲面向这类业务的最小加固动作风险点常见表现最小处理路由 RCE构造特定 URL 触发命令执行升级到 3.2.x 修复版关闭不用的路由模式SQL 注入I()参数未过滤直接拼 SQL统一走M()链式查询禁止query()拼字符串调试页泄露报错时输出完整堆栈和 SQLAPP_DEBUG设为 false日志泄露Runtime 日志含请求参数DB_SQL_LOG设为 falseRuntime 禁止外部访问源码泄露.git、备份文件可访问上线前删除 .git.sql 备份不放 web 目录其中日志泄露最容易被忽略。ThinkPHP 3.2 开启 SQL 日志后会在Application/Runtime/Logs里记录带参数的 SQL如果卡密在写入前没有加密就等于是明文躺在服务器上。所以生产环境第一件事就是把DB_SQL_LOG false写进配置并且确认 Nginx 不会把 Runtime 目录当静态文件暴露出去。4.2 卡密提交接口的频率限制与表单令牌收卡网的提交接口天然会被脚本盯上脚本批量提交卡密试探有效性或者拿接口刷价格。防刷我一般做两层第一层是表单令牌第二层是接口频率控制// 进入提交页时生成令牌随表单带回 public function token() { $t md5(uniqid(mt_rand(), true)); session(submit_token, $t); $this-ajaxReturn([code 0, token $t]); } // submit() 开头校验令牌通过后立即销毁令牌一次一用 $token I(post.token, , trim); if ($token || $token ! session(submit_token)) { $this-ajaxReturn([code 1, msg 页面已过期请刷新后重试]); } session(submit_token, null);令牌防的是 CSRF 和重复提交但拦不住直接 POST 接口的脚本所以要叠加频率控制。用 ThinkPHP 的S()缓存做轻量计数不引额外组件$key submit_limit_ . session(user_id); $count S($key); if ($count ! false $count 5) { $this-ajaxReturn([code 1, msg 提交过于频繁请稍后再试]); } S($key, $count false ? 1 : $count 1, 300);这里 300 秒内最多 5 次针对的是正常用户节奏。如果团队做活动阈值建议做成后台配置项而不是硬编码在控制器里。注意S()默认存文件缓存多台 Web 服务器部署时要换成 Redis 驱动否则限流计数各算各的。4.3 卡密加密存储与最小可见性卡密必须加密后入库这是收卡平台的底线。常见做法是用应用密钥做 AES 对称加密密钥放在 web 目录之外的配置文件中function encrypt_card_pwd($pwd) { $key C(CARD_SECRET_KEY); // 配置在 Application/Common/Conf/extra.php $iv substr(md5($key), 0, 16); return base64_encode(openssl_encrypt($pwd, AES-128-CBC, $key, 0, $iv)); } function decrypt_card_pwd($encrypted) { $key C(CARD_SECRET_KEY); $iv substr(md5($key), 0, 16); return openssl_decrypt(base64_decode($encrypted), AES-128-CBC, $key, 0, $iv); }解密函数只允许在后台审核页调用任何前端接口和列表页都不应返回卡密明文。另外建议在订单进入完成状态后把card_pwd置空账已经打完卡密没有继续保留的必要数据库被拖库时也能少一列敏感数据。extra.php要注意两点一是不要提交进版本库二是文件权限不要 777chmod 600更合适。密钥定期轮换时旧数据要用旧密钥解密后重新加密所以配置文件里建议预留一个CARD_SECRET_KEY_PRE字段做双密钥兼容。5. 从 zip 到可用站点Thinkphp 3.2 兼容 PHP 8 的最小改造与每日对账5.1 解压部署与两个典型坑拿到 zip 后先解压到站点目录。包含中文文件名的包用 Linux 默认 unzip 解出来容易乱码unzip -O gbk是常见解法。另一类「error read zip archive」报错通常是 zip 包通过 FTP 上传时被当作文本文件传输导致损坏重新用二进制模式传一遍即可不用怀疑源码本身。unzip -O gbk Thinkphp收卡网源码.zip -d /var/www/html/card-site chown -R www:www /var/www/html/card-site chmod -R 755 /var/www/html/card-site chmod -R 777 /var/www/html/card-site/Application/RuntimeRuntime 必须可写模板编译、日志、缓存都写在这里。Nginx 下要配好 PATHINFO 重写否则除了首页全是 404location / { if (!-e $request_filename) { rewrite ^/(.*)$ /index.php?s$1 last; } }5.2 ThinkPHP 3.2 兼容 PHP 8 的最小补丁老框架跑新 PHP 的报错通常集中在each()被移除、create_function被移除、花括号字符串偏移失效这几个点。能选环境的话PHP 7.4 最省事必须用 PHP 8 时在入口文件加一段兼容层是最小改法// index.php 顶部PHP 8 环境自动加载 if (PHP_VERSION_ID 80000) { if (!function_exists(each)) { function each($array) { $key key($array); $value current($array); next($array); return $key null ? false : [$key, $value]; } } }each()在 TP 3.2 的核心类里还有少量使用这个 shim 能顶过大部分报错。数据库驱动也要注意PHP 8 环境必须改成 mysqliDB_TYPE mysqli, DB_HOST 127.0.0.1, DB_PORT 3306,老 mysql 驱动在 PHP 7 就已经移除这个配置项不改站点会直接白屏。5.3 每天跑一次的对账脚本校验实付与毛利率上线后运营最怕的是折扣改错导致亏损。我习惯加一个每日对账任务凌晨把昨天所有完成订单拉出来逐单核对实付金额与「面值 × 锁定折扣」是否一致再按卡种算毛利率// Application/Home/Command/ReconcileController.class.php class ReconcileController extends Controller { public function index() { $start strtotime(date(Y-m-d, strtotime(-1 day))); $end $start 86400; $orders M(recycle_order) -where([status 3, audit_time [between, [$start, $end]]]) -select(); foreach ($orders as $o) { $expect round($o[face_value] * $o[rate], 2); if (abs($expect - $o[pay_amount]) 0.01) { // 金额对不上写入对账异常表并通知运营 } } } }挂在 crontab 里每天早上执行0 8 * * * /usr/bin/php /var/www/html/card-site/index.php Home/Command/Reconcile/index脚本跑完把异常订单发到钉钉或企业微信群。这样折扣调整、字段被误改、人工审核出错都会在每天开盘前暴露而不是月底对账时才发现钱不对。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻