揭秘 GPT 代充网站:从我的开源项目拆解卡密、订单与上游接口

老K 12 0

[aru_61] 看到一个 GPT 代充网站,前台往往只有几步:输入卡密、填写资料、确认、等结果。我把自己的 gpt-redeem 开源了,这篇就顺着源码拆开看:卡密到底代表什么,站点把请求交给谁,以及为什么一次超时可能比一次明确失败更麻烦。

项目地址:https://github.com/zcjunblog/gpt-redeem。本文基于提交 47e0aebb435d 分析。它是 PHP + MySQL 的兑换站实现,默认走 Mock 模拟通道;Orbitcard 适配代码已包含在仓库里,真实密钥联调与实际充值尚未验证。下面讲的是这份代码能证明的流程。

揭秘 GPT 代充网站:从我的开源项目拆解卡密、订单与上游接口
项目仓库提供的 2026-10-23 页面截图;仅展示模拟模式,不代表真实充值已验证。

一、先把“兑换站”和“上游开通服务”分开

在这个项目里,我的站点负责管理卡密、创建本地订单、接收用户授权的账号资料、提交上游任务,以及查询和展示结果。上游适配层负责把这些操作翻译成卡台的请求格式。至于上游内部如何完成实际支付和订阅开通,这个仓库并没有实现,不能从一个兑换页面反推出所有支付细节。

所以,把这一类站点理解成“兑换凭证 + 订单管理 + 上游接口适配”更接近这份源码的结构。这里的上游接口是第三方卡台接口,不能把它说成 OpenAI 官方的代充接口;也不能拿本项目的实现概括所有代充网站。

揭秘 GPT 代充网站:从我的开源项目拆解卡密、订单与上游接口
依据源码整理的流程示意图,非真实支付或线上监控截图。

目前的接口约定在 app/Upstream.php:

<?php
declare(strict_types=1);
namespace Recharge;

interface Upstream {
    public function name():string;
    public function prepare(string $plan):array;
    public function create(array $order,?array $session=null):?string;
    public function query(array $order):array;
    public function verify(string $id):void;
}

prepare() 准备套餐配置,create() 创建任务,query() 查询原任务。接口里虽然有 verify(),真实通道并不会替用户完成人机验证:它会提示到上游验证页面处理,再回来查原单。Mock 的验证只是本地演示。

二、卡密不是上游余额,是本站的一次兑换资格

卡密是在本站生成的高熵随机字符串。代码用 random_bytes(24) 生成随机部分,数据库保存 SHA-256 摘要、前缀、套餐、有效期、状态和通道;完整卡密在生成时返回,不能靠后台列表把原文重新找回来。数据库里的一条卡密记录,本身也不等于上游已经扣款或开通。

我把“验卡”和“占用”分开:inspect() 只是读取资格,真正提交时才锁卡。否则用户仅仅查一下卡密就把它占住,会很难解释。关键操作是 Core.php 的 redeem() 中这一条条件更新,下面摘出 SQL:

UPDATE cards SET status='locked',order_id=?
WHERE id=? AND status='unused' AND expires_at>?

这一步与插入订单放在同一个本地事务里,程序还检查受影响行数必须是 1。两个请求即便先后都读到“未使用”,也不能只凭之前的读取结果直接下单;必须以这次条件更新是否成功为准。MySQL 表使用 InnoDB,这个事务只覆盖本地数据库,不包含远端充值。

卡密还有 provider 归属。Mock 卡不能换到真实通道使用,历史 Mock 订单也应继续由 Mock 查询,不能因为后来改了站点配置就把旧单转成真实充值。

三、真正难的地方:提交超时,远端可能已经受理

如果站点向上游提交后等了很久,最后超时,这只能说明站点没有拿到确定结果。远端可能根本没收到,也可能已经处理了。如果此时恢复卡密,让用户再次点提交,就可能对同一份兑换资格创建第二次任务。

这份代码先保存本地订单和固定 idempotency_key,在发起外部 POST 前把订单从 pending 抢占为 processing,再提交一次。请求异常会进入 unknown,卡密仍是 locked。后续查询用原 client_order_no,不会因为刷新页面重新创建一张订单。

固定幂等键是协议层的配合手段,不是“远端恰好执行一次”的证明。当前实现也有明确取舍:假如进程在标记 processing 后、真正发送前崩溃,恢复时仍只查原单,可能需要人工核实。它优先避免重复提交,并没有实现一个能够覆盖所有崩溃窗口的自动补偿系统。

观察到的情况 本项目的处理
任务处理中 展示进度,保持卡密锁定
提交或查单结果不明 unknown,等待后继续查询原单
需要人机验证 展示临时验证入口,验证后再查原单
原单确认成功,且归属匹配 标记成功并核销卡密
真实订单确认失败 保留失败状态,不能自动恢复卡密,需要线下核实支付结果

四、为什么上游返回“成功”,我还不立即核销?

创建接口的返回值可能只是受理摘要。这里 create() 主要取回 task_no,并不直接把卡密改成 used;最终要走查单。OrbitcardUpstream::query() 会核对本地订单号、目标邮箱、套餐类型、套餐代码、任务 ID,以及来源标记。

核对通过后,还必须满足 status === 'succeeded' 和 subscription_succeeded === true 才映射为成功。账号或套餐不匹配会保持待核实,不会因为某个字段写着 success 就给本站核销。来源字段只是当前适配协议的一项检查,不是独立的密码学证明。

最后的核销也限定为“这张卡、这张订单、当前 locked 状态”,并与本地订单结果更新处在同一事务。这个思路可以复用到其他兑换系统:不能只确认“有一笔成功”,还要确认成功的是“这一笔”。

五、接卡台不是换一个接口域名

目前仓库只包含 Orbitcard 这一家真实通道的适配。它需要先读取套餐目录和支付地区,根据本地映射生成请求快照,再校验服务费上限。以后后台配置变动时,旧订单仍有它提交时的套餐上下文,不应该直接套用新配置查单。

OrbitcardClient.php 的请求签名把 API Key、时间戳、随机 nonce、HTTP 方法、host、path、请求体哈希和幂等键组合起来,再算 HMAC-SHA256。下面只是把仓库中的十行 canonical 字符串展开排版,便于看字段组成:

$canonical=implode("\n",[
    'ORBITCARD-HMAC-SHA256-V1',
    $this->config['api_key'], $timestamp, $nonce,
    'POST', $host, $path, '', hash('sha256',$body), $idempotency
]);

请求体先序列化,签名使用的 body 哈希与实际发送字节保持一致。这是仓库实现的协议格式,本轮用注入的假传输和固定向量验证,没有连接上游确认当前真实接口。新增卡台仍需逐项核对鉴权、下单字段、查单归属及状态语义,还要调整配置校验和前端判断,不能只实现一个接口就宣称兼容。

网络出错时,这个客户端会给出待核实和下次查询的等待时间,业务层保留原订单。查单还有时间窗口、查询租约和随机 query_token:相同原单已有查询在进行时,其他请求先返回现有结果;旧查询回来的结果也不能随意覆盖新的查询归属。这比让每个浏览器标签页都直接轮询上游稳妥。

六、账号资料经过谁,比“页面是否要密码”更值得看

真实通道会将完整 Session JSON 随单次创建请求发送给上游,其中包含 accessToken 等敏感字段。因此,“不直接索要密码”不能被解释成“没有账号资料风险”;数据确实经过本站进程与第三方上游。

当前业务代码不把完整 Session 存进订单表,查单返回也会过滤敏感内容,验证链接只临时返回。仓库还要求关闭可能泄漏参数的错误展示和请求体日志。不过,这些源码层的处理不能证明部署环境的代理日志、监控工具或上游没有留存。判断站点时,应该看完整的数据流,而不只是表单长什么样。

本地实验不需要真实登录资料。Mock 模式只收虚构邮箱,并拒绝 Session,防止把一段演示流程误当成真实充值来测试。

七、动手复现:未知结果时,卡密到底有没有被释放?

仓库的正式部署要求是 PHP 8.5.12+、MySQL 8.4 和相应扩展;网页入口本身有 PHP 版本检查。下面仅演示不经过网页启动流程的核心业务实验,需要 CLI 的 PDO SQLite。在源码根目录新建 blog-demo.php,粘贴:

<?php
declare(strict_types=1);
require __DIR__.'/app/Core.php';
use Recharge\DemoApp;

$clock = (object)['now' => 1800000000];
$app = new DemoApp(':memory:', fn() => $clock->now, 0);
$code = $app->generate('plus', 1, 'unknown')[0]['code'];
$order = $app->redeem($code, 'demo@example.com');
echo $order['status'].' / '.$app->inspect($code)['status'].PHP_EOL;
// 只适用于 Mock:模拟上游最终确认原单成功。
$app->resolve($order['id'], 'succeeded');
$confirmed = $app->lookup($order['id'])['order'];
echo $confirmed['status'].' / '.$app->inspect($code)['status'].PHP_EOL;
echo 'orders='.$app->db->query('SELECT COUNT(*) FROM orders')->fetchColumn().PHP_EOL;

执行 php blog-demo.php,本轮实际输出为:

unknown / locked
succeeded / used
orders=1

关键是第一次输出:结果未知时保持 locked。随后通过 Mock 的 resolve 模拟上游确认,再查询同一原单,卡密才变成 used,订单总数仍是 1。真实通道没有这个人工模拟确认入口,不能拿它跳过真实支付结果核对。

我还运行了仓库现成的两组离线测试:

php tests/run.php
php tests/orbitcard.php

结果分别为 11 passed、13 passed,覆盖重复提交、超时只查原单、错误归属、查询租约、敏感字段过滤和等待时间等情形。本轮运行环境是 macOS / PHP 8.3.35 / SQLite,仅说明这两组直接加载核心类的离线测试和上面的实验通过,不代表网页在 PHP 8.3 下能启动,也不替代 PHP 8.5.12 / MySQL 的部署验收;真实充值没有执行。

八、这个开源项目值得拿来学什么

我更想把这份项目留下来作为一份订单一致性的技术笔记:卡密是一份资格,订单是一笔业务,上游任务是远端状态,三者需要明确绑定。页面做得再顺滑,也不能省掉“结果不明时继续查原单”这一步。

如果要研究类似系统,建议从 Core.php 的 redeem / sync 开始,再看 OrbitcardUpstream.php 的状态归一化,最后对照 tests/orbitcard.php 的故障测试。仓库采用 MIT 许可证,完整代码和部署说明在这里:https://github.com/zcjunblog/gpt-redeem。

发表评论 取消回复
OwO 图片 链接 代码

分享