简介:一套完整的卡密社区SUP系统总控与主站分销源码,专为需要搭建卡密自助交易、分站分销业务的开发者或站长准备。系统涵盖总控端、主站后台和分站后台三套管理界面,可实现系统商模式的平台分配、卡密发行、分站开通与API对接,配合自助发卡与卡密交易流程,适合虚拟商品在线销售场景,如游戏点卡、激活码、会员时长等。包内共2000个文件,以PHP业务代码、JS前端脚本、CSS样式和HTML页面为主,另含MD说明文档、JSON配置、SQL数据库脚本及Nginx相关配置,压缩包约62.84MB,整体目录结构清晰,便于按模块定位代码。源码基于PHP/MySQL体系构建,具备基础运维能力的读者即可完成部署和二次修改;已有296人下载学习,借助总控-主站-分站的多层级设计,可快速开展卡密批发与分销业务,并通过内置API文档扩展支付、查询等功能。
1. 卡密社区SUP系统总控和主站分销,为什么必须拆成两套
标题里“卡密社区SUP系统总控源码+主站分销系统功能源码”其实说了两件事:总控管卡密资产,主站管售卖和代理分佣。很多自己写过发卡站的人都有教训,把卡密生成、订单、代理都塞进一个 PHP 项目里,出一次 SQL 注入,卡密池和后台密钥一起被拖走;或者代理后台和下单价目耦合,分销计算改一版就崩一次。拆分之后,总控是唯一的卡密发证方,主站只是销售端和分佣端,两边通过带签名的 API 对话。本文从数据表、签名、API 鉴权写到联调参数和验证清单,适合正在做虚拟商品交易或社区积分系统的后端开发者参考。
2. 总控源码落地:卡密池、签名与总控API设计
2.1 卡密表结构与状态机设计
总控最核心的资产不是卡密本身,而是“卡密状态的流转记录”。只要状态机设计错,后面做补卡、退款、对账都会失控。我一般把卡密表设计成一张宽表,把与核销相关的字段都放在同一行,减少跨表查询。
CREATE TABLE `card_pool` ( `card_id` bigint unsigned NOT NULL AUTO_INCREMENT, `card_key` varchar(64) NOT NULL COMMENT '卡密明文,格式自定义', `card_secret` varchar(64) NOT NULL COMMENT '卡密密钥或签名后缀', `product_id` varchar(32) NOT NULL COMMENT '产品标识,用于区分时长/权益', `batch_id` varchar(32) NOT NULL, `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待售 1已售未激活 2已激活 3已核销 4已冻结 5已退款', `duration_days` int NOT NULL DEFAULT 0, `owner_uid` bigint NOT NULL DEFAULT 0 COMMENT '当前持有者用户ID', `source_agent_id` bigint NOT NULL DEFAULT 0 COMMENT '卡密来源代理ID', `activated_at` datetime DEFAULT NULL, `expires_at` datetime DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_card_key` (`card_key`), KEY `idx_status_owner` (`status`, `owner_uid`), KEY `idx_batch` (`batch_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='卡密池';卡密的状态机必须只允许单向前进:待售 -> 已售未激活 -> 已激活 -> 已核销。已核销不能回退,退款只能从已售未激活退到待售。把这条规则写在总控的更新条件里,而不是写在业务代码里,避免多个主站节点同时操作同一条卡密。
UPDATE card_pool SET status = 1, owner_uid = :buyer_uid, source_agent_id = :agent_id, updated_at = NOW() WHERE card_id = :card_id AND status = 0;这里用status = 0作为乐观锁条件。如果 update 影响行数为 0,说明卡密已被其他订单抢占,主站需要重新取卡。这是“卡密不超卖”的最后一道屏障,上游可以用 Redis 预扣,但数据库状态锁必须保留,因为最终对账以数据库为准。
2.2 HMAC-SHA256签名生成与验签实现
卡密社区 SUP 系统里,卡密不能只是一串随机字符串,否则打开数据库的人可以直接批量伪造新卡密。常见做法有两种:第一种是 HMAC-SHA256,主站和总控共享一个 app_secret;第二种是 RSA 私钥签名,总控持有私钥,主站只持公钥验签。如果主站代码也可能泄露,RSA 更安全,因为主站无法生成合法卡密。
这里给一个 Python 的生成端示例,对应总控里的离线发卡脚本:
import hashlib import hmac import os from datetime import datetime, timedelta SECRET_KEY = os.environ["SUP_MASTER_SECRET"] def generate_card(product_id: str, duration_days: int) -> dict: raw = os.urandom(16).hex().upper() card_key = f"{product_id[:2]}-{raw[:8]}-{raw[8:12]}" expires_at = datetime.utcnow() + timedelta(days=duration_days) expires_ts = int(expires_at.timestamp()) # 把产品ID、卡密明文、过期时间一起做签名 payload = f"{product_id}:{card_key}:{expires_ts}" signature = hmac.new( SECRET_KEY.encode(), payload.encode(), hashlib.sha256 ).hexdigest() card_secret = signature[:24] return { "card_key": card_key, "card_secret": card_secret, "product_id": product_id, "expires_at": expires_ts, }注意这里card_secret不是随机串,而是签名的一部分。当用户在主站激活卡密时,主站把card_key + card_secret + product_id + expires_at发给总控,总控用同一个 secret 重新算一遍 HMAC,比对前 24 位是否一致。这样做的好处是:即使某条卡密在数据库里被篡改了过期时间,签名校验也会失败,因为expires_ts参与签名,改一个字符整个 signature 都对不上。
如果追求更严,直接换成 RSA 私钥签名。总控生成时用rsa模块私钥对卡密信息签名,主站验签时用只读公钥。代价是签名和验签速度慢,但对卡密平台每秒几千次的校验量,完全够用。
2.3 总控API的鉴权与关键参数
总控对外开放的接口不要多,三个就够了:verify(验证卡密是否可用)、activate(激活并绑定用户)、deactivate(按订单退款作废)。每个接口都必须是幂等操作,因为主站可能因网络重试而重复请求。
接口鉴权除了 app_secret,还要加时间戳防重放。我用固定的请求头方案:
POST /openapi/v1/card/verify Content-Type: application/json X-App-Id: main_site X-Timestamp: 1710000000 X-Signature: 9f86d081884c7d659a2feaa0c55ad015... { "request_id": "order-20240311-001", "card_key": "VIP-AB12CD34", "card_secret": "27fa9c4c1e2f", "product_id": "VIP_MONTHLY" }签名计算规则:把X-Timestamp和请求体 JSON 字符串拼接,再用 app_secret 做 HMAC-SHA256。主站每次请求前生成时间戳,总控收到后检查偏差不超过 300 秒,同时用 Redis 记录request_id,已处理过的请求直接返回缓存结果。
| 参数 | 含义 | 典型值 |
|---|---|---|
| X-App-Id | 主站身份标识,用于区分多个主站 | main_site |
| X-Timestamp | 请求发起 Unix 秒级时间戳 | 1710000000 |
| X-Signature | 对 timestamp + body 的 HMAC 签名 | 64 位十六进制 |
| request_id | 幂等键,建议用订单号+随机后缀 | order-1710000000-abc |
总控在验签通过后还需要做一次风控检查:同一 IP 激活失败的次数、同一卡密查询频率、短时间内大量不同卡密提交。免费卡密发放平台经常被脚本扫卡,如果总控不做频控,别人可以把你的卡密池当字典攻击。
3. 主站分销系统源码:代购、分佣和卡密库存扣减
3.1 代理关系与分佣层级表
主站分销的核心是“谁带来订单”和“从哪个库存里发卡”。很多源码把代理直接挂在用户表里,用pid表示上级,但分销等级、提现状态一多,用户表就乱。常见做法是单独建代理映射表,代理关系变更不影响用户主表。
CREATE TABLE `agent_relation` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `agent_id` bigint NOT NULL COMMENT '代理用户ID', `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '上级代理ID', `level` tinyint NOT NULL DEFAULT 1 COMMENT '分销层级,1一级 2二级', `commission_rate` decimal(6,4) NOT NULL DEFAULT 0.1000 COMMENT '分佣比例', `status` tinyint NOT NULL DEFAULT 1, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_agent` (`agent_id`), KEY `idx_parent` (`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;分销层级不要设计成无限嵌套,无限嵌套在结算时会出现递归深循环,而且很容易被恶意刷出多级分佣。卡密类产品利润薄,一级分佣加二级分佣基本到底,超过两级的订单在结算时直接按系统归零。代理升级时只更新level和commission_rate,不要改parent_id,否则整个分佣链路都要重算。
3.2 卡密采购下单与总控库存预占
主站拿到代理订单后,需要先向总控申请“锁定卡密”,然后才能真正收款。顺序不能反过来,否则代理付了钱,总控那边库存已经卖完,主站就得赔卡或者退款。
下单接口逻辑我一般这样安排:
def create_order(agent_id, product_id, quantity): # 1. 检查代理账户余额或支付状态 # 2. 调用总控预占库存 reserve = call_sup_api( path="/openapi/v1/card/apply", payload={ "request_id": f"apply-{agent_id}-{time.time_ns()}", "product_id": product_id, "quantity": quantity, "owner_uid": str(agent_id), } ) # 3. 本地创建待支付订单,保存总控返回的卡密ID order_id = save_order(agent_id, product_id, quantity, reserve["card_ids"]) # 4. 返回支付二维码 return order_id总控的/apply接口在设计上必须返回具体的卡密 ID 列表,而不是只返回一个“剩余数量”。因为主站拿到卡密 ID 后要落库到order_card表,支付完成再向总控确认激活;如果支付超时,主站再调/release把卡密释放回池子。
主站库存扣减不要直接 UPDATEcard_pool SET status=2,而是用状态条件更新。因为卡密可能同时被多个主站节点用同一个代理 ID 请求,条件更新只能成功一个。扣减后要读取受影响行数,等于 0 就说明这次申请已经过期,需要重新取卡。
3.3 分佣结算的时机和事务边界
分佣结算最容易犯的错是在订单支付回调里直接写余额,然后回调重试时又加一遍。常见做法是引入结算流水表,支付回调与分佣写入放在同一事务,用订单号做唯一索引保证只结算一次。
CREATE TABLE `agent_commission_log` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL, `agent_id` bigint NOT NULL, `amount` decimal(12,2) NOT NULL, `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待结算 1已入账', `from_parent` bigint NOT NULL DEFAULT 0, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_order_agent` (`order_id`, `agent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;结算动作放在支付回调之后的异步队列里,避免用户支付成功却卡在分佣等待。队列消费者先尝试插入agent_commission_log,如果唯一索引冲突说明已经处理过,直接跳过;插入成功后再更新代理余额。两步操作不在同一事务也不会重复,因为唯一索引就是幂等锁。
另外一个细节:分佣金额不要用订单实付金额直接乘比例,而要减去卡密成本再乘比例。例如代理下单成本 50 元,卡密池货源成本 30 元,一级分佣按 10% 应该是(50 - 30) * 0.1,而不是50 * 0.1。这个边界在 SUP 类系统里经常被忽略,导致平台亏本代发。
4. 总控与主站联调中的3个关键参数和4个坑
4.1 回调地址、幂等键、时间窗是必调的3个参数
总控和主站之间不是只有同步请求,卡密激活或者退还时总控需要回调主站。回调地址必须写在总控配置里,不能写死在前端或商户请求里,否则别人可以冒用总控身份给主站发伪造激活通知。
回调的幂等键建议用order_id + card_id + action三个字段拼成。主站收到回调后先查库,如果这个组合已经处理过,直接返回成功。主站应答总控时,状态码 200 不代表总控任务成功,总控要在响应体里看到类似{"code":"S_OK"}才停止重试。建议两个系统联合约定:业务成功才返回 200,业务失败也返回 200 但 code 不同,HTTP 层不区分业务状态。
时间窗参数则是X-Timestamp的最大偏差。生产环境我一般设为 600 秒,太短会因主站和总控的服务器时钟不一致导致频繁验签失败,太长又失去防重放意义。设置前先用 NTP 把两台服务器时间对齐,偏差量级在 50 毫秒内最合理。
| 参数名 | 配置位置 | 建议值 | 调节依据 |
|---|---|---|---|
| 回调地址 | 总控后台 | https://main.example.com/callback/sup | 必须 HTTPS |
| 幂等键 | 请求体字段 | order_id + card_id + action | 幂等键冲突时直接忽略 |
| 时间窗 | 总控验签逻辑 | 600 秒 | 服务器时钟漂移严重时调大 |
| 请求超时 | 主站调用总控 | 3000 ms | 总控耗时不超 200ms |
4.2 超卖、重复通知、时间偏移、卡密已兑现这四个坑
超卖是最隐蔽的坑。主站内存里维护库存数量,多个代理同时下单时各自扣减剩余库存,看起来没超,到总控取卡时发现数量不够。解决办法是前面 3.2 的状态条件更新,以及总控的/apply接口内部用SELECT ... FOR UPDATE锁住卡密池的分页区间,直到返回卡密 ID。注意锁不能覆盖全表,否则高并发时总控变成单线程。
重复通知的坑在免费卡密发放场景尤其常见。总控重试回调时,如果主站没有幂等,同一个激活事件会被拆成多笔分佣。所以在 3.3 的agent_commission_log里加上唯一索引,然后在回调处理函数里先插记录再发分佣,重复请求自然被索引拦掉。
时间偏移会直接让签名验不过。主站服务器时间慢了 10 分钟,总控收到X-Timestamp时已经超过时间窗,所有请求都提示签名无效。排错时不要只查代码,先对比两台服务器date -u输出,再看 NTP 服务状态。曾经见过主站部署在 Windows 容器、时间同步关闭,导致联调一整天都在猜签名格式问题。
卡密已兑现的问题出现在退款和换卡流程。用户退款后总控把卡密标成已退款,但主站本地没同步,卖家用同一个卡密文件去补卡,主站再次申请激活时总控报错。处理方式是在总控的激活逻辑里增加source_request_no字段,同一个订单号不能激活两张不同卡密,这样即使主站发错卡,总控也会拒绝。
5. 把总控安全加固到生产级的验证核对单
总控和主站分开后,安全重心在总控。开发调试时可以暂时用明文密钥,上线前至少做一轮下面的验证。每个检查项都要有预期结果,不能只写“通过”。
| 检查项 | 验证方法 | 预期结果 |
|---|---|---|
| 卡密签名密钥分离 | 主站配置里只能有 RSA 公钥或 HMAC secret | 主站用私钥/主密钥无法对卡密重新签名 |
| 卡密状态机约束 | 对card_pool执行强制执行 UPDATE 的 SQL | 数据库层拒绝从 3 回退到 0 |
| API 幂等 | 用同一个request_id连续发送 10 次激活请求 | 只激活 1 张卡密,返回相同结果 |
| 时间戳防重放 | 截获一个合法请求,3 小时后再发送 | 总控返回timestamp expired |
| 回调重复支付 | 伪造两笔相同回调到主站分佣接口 | 第二笔响应 code 为S_DUP |
| 代理越权查询 | 用低级代理 token 请求其下级的卡密列表 | 返回 403 或空数据 |
最后一个常被忽略的动作:总控的管理端登录日志。SUP 系统里总控管理台如果被爆破,攻击者不需要解密卡密,直接在页面上重新生成一个批次就行。所以我总是把总控登录的 IP、UA、操作类型写入独立的审计表,并且不和卡密业务表放在同一个数据库实例里。上线后每隔 7 天查看一次审计表里是否有批量操作记录,能提前发现异常发卡行为。
本文还有配套的精品资源,点击获取