
简介一套仿Soul风格的交友脱单盲盒全开源系统源码面向PHP开发者、社交产品站长及对盲盒玩法感兴趣的初学者。系统基于PHPMySQL运行环境内置微信免签接口与个人码支付接口API部分开源便于二次开发除常规交友、脱单盲盒功能外还保留界面素材与后台管理逻辑适合直接部署体验或作为学习项目。压缩包共440个文件其中Java/Class文件负责后端核心逻辑JS/HTML/CSS构成前端展示与交互Jar包集成依赖库XML承担配置管理另含SQL脚本、界面截图和图片素材资源整体约19.76MB目录结构清晰便于按模块检索。压缩包内附图文搭建教程覆盖环境准备、部署配置、支付接口设置等环节可帮助读者快速跑通项目并理解社交盲盒类产品从功能实现、前后端交互到接口联调的关键技术点。目前已有1387人学习下载适合打算研究源码、扩展功能或搭建类似社交应用的开发者参考。1. 仿soul交友脱单盲盒源码程序搭建到底在搭什么很多人从网上下载一套交友脱单盲盒源码卡在第一步不是安装不了而是不知道盲盒的“随机匹配”到底放在哪里、抽一个少一个的库存扣减怎么保证不超卖。换过三套源码后你会发现一套能在线跑起来的仿soul交友脱单盲盒程序核心不在于UI的颜值而在于“用户注册→盲盒投放→付费抽取→配对成功→私信聊天”这条闭环能不能稳定转起来。本文从源码目录结构、数据库表设计、Nginx/PHP/MySQL/Redis环境配置讲起再拆抽取接口的原子性实现和概率参数最后给出上线前必须做的数据验证内容。适合手里已有源码但部署不上、或者想自己从零搭一套骨架的开发者同时默认你懂基础Linux命令知道域名解析和防火墙放行。这里只讨论技术实现不涉及任何灰产玩法内容安全红线刻意绕开。2. 仿soul交友脱单盲盒的程序架构与核心数据模型2.1 盲盒玩法的核心链路投放、抽取、配对、聊天交友脱单盲盒的玩法可以拆成四个环节。投放端用户填写昵称、年龄、城市、微信号等信息生成一张“脱单卡片”支付少量费用后把卡片放进盲盒池。抽取端另一个用户付费或消耗积分从盲盒池里随机抽出一张卡片。配对端如果双方愿意交换联系方式系统生成一个一对一会话。聊天端消息通过 WebSocket 或轮询推送。很多源码把这些逻辑全部写在两个 PHP 文件里看起来简单但后面想加队列、加并发控制、加机器人时无从下手。我一般会建议先按接口维度把目录拆开不要急着魔改业务。以一个 ThinkPHP 风格的目录为例. ├── application │ ├── api │ │ ├── controller │ │ │ ├── User.php // 注册、登录、资料 │ │ │ ├── Box.php // 投放、抽取、卡片回显 │ │ │ └── Message.php // 会话、消息 │ │ └── model │ ├── admin // 后台管理 │ └── common ├── config │ ├── database.php // MySQL 连接 │ └── redis.php // Redis 连接 ├── public │ └── index.php └── install └── db.sql // 安装 SQL如果你拿到的源码是单文件先别急着跑把 SQL 语句和业务逻辑拆出来否则后面做日志、做缓存时会非常痛苦。接口拆细以后至少要有下面这组路由接口作用请求方式/api/user/create注册/登录POST/api/user/update修改脱单资料POST/api/box/put投放盲盒POST/api/box/draw抽取盲盒POST/api/box/card查看抽到的卡片GET/api/message/send发送私信POST接口拆得细压测时可以单独对/api/box/draw做高并发测试不影响其他模块。实际项目中很多人拿到的源码根本没有/api/box/card抽到卡片后直接返回全部字段这就导致花钱抽一次之后可以在页面里反复查看对方微信业务上也需要这个查询接口。2.2 用户、盲盒、订单的数据表设计先看最核心的三张表用户表、盲盒表、订单表。我提供一份简化的建表 SQL。-- 用户表 CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, nickname varchar(32) NOT NULL DEFAULT COMMENT 昵称, gender tinyint(1) NOT NULL DEFAULT 0 COMMENT 性别0未知 1男 2女, birth_year smallint(6) NOT NULL DEFAULT 0 COMMENT 出生年份, city varchar(64) NOT NULL DEFAULT COMMENT 城市, wechat varchar(64) NOT NULL DEFAULT COMMENT 微信号, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 状态1正常 0封禁, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 盲盒表 CREATE TABLE blind_box ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT 投放人ID, box_type tinyint(1) NOT NULL DEFAULT 1 COMMENT 盲盒类型1男生盒 2女生盒, content text COMMENT 脱单介绍内容, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待抽取 1已抽取 2下架, draw_user_id int(11) DEFAULT NULL COMMENT 抽中者ID, draw_time datetime DEFAULT NULL COMMENT 抽取时间, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_status_type (status, box_type), PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id int(11) NOT NULL COMMENT 发起用户ID, type tinyint(1) NOT NULL COMMENT 1投放 2抽取, box_id bigint(20) DEFAULT NULL COMMENT 关联盲盒ID, amount decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 支付金额, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已退款, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;blind_box表上有一个联合索引idx_status_type因为抽取场景最常见的查询是“某个类型的盲盒里面还有哪些是待抽取状态”。尽量不要只给status建索引、box_type分开建联合索引能同时过滤两个条件数据量到十万级时差距明显。另外order表的order_no记得加唯一索引避免支付回调时产生重复订单。这里要注意表名前缀。很多商业源码会在所有表名前加tb_或chat_你在导入 SQL 前先看一眼install/db.sql里的CREATE TABLE语句如果带前缀后面配置文件里的prefix也要改成对应值否则 ThinkPHP 这类框架会把表名拼成prefix_box导致“数据表不存在”的错误。2.3 为什么用 Redis 做随机抽取和防并发重复MySQL 里有一个经典写法ORDER BY RAND()新手往往直接用。但它的执行方式是先读全表再在内存里排序生成随机结果。盲盒表到十万条记录时这个查询每秒只能扛个位数的并发。更重要的是就算你用了LIMIT 1两个并发请求也可能拿到同一行数据导致同一个盲盒被抽给两个人。所以生产环境里几乎不用 MySQL 做随机抽取而是把待抽取的盲盒 ID 提前加载到 Redis 的 Set 中。Redis 的SPOP命令可以从集合中随机弹出一个元素并且这个操作是原子的——两个用户同时请求Redis 会串行执行这个操作不会重复返回同一个元素。# 把 20250615 创建、类型为1男生盒的待抽取盲盒ID写入集合 SADD blind_box:20250615:1 1001 1002 1003 1004 1005 # 原子随机弹出同一时刻只可能被一个调用者拿到 SPOP blind_box:20250615:1注意这里不能使用SRANDMEMBER它只随机返回一个元素但不会从集合里移除。用SRANDMEMBER做的结果就是同一个盲盒被抽中多次用户退款投诉直接压垮后台。两种命令的差别是盲盒抽取场景里最早遇见的一类坑后面第 4 章会继续讲如何把这个流程串进接口。3. 用 NginxPHPMySQLRedis 在本地跑通仿soul交友脱单盲盒源码3.1 搭建运行环境推荐版本一张表想要少踩坑先把版本锁死。我给出的这套组合是在做源码部署时最稳的搭配软件推荐版本说明PHP7.4兼容 ThinkPHP5.x / CI3也兼容大部分商业源码MySQL5.7支持 utf8mb4 和 JSON 字段Redis5.0支持 Lua 脚本和事务Nginx1.20伪静态稳定性能足够操作系统Ubuntu 20.04 / Debian 11包管理简单教程覆盖多如果你拿到的源码声明支持 PHP 8.x那么用 8.0 也可以但 PHP 8.2 里很多旧的扩展改名导致无法加载不建议直接在 PHP 8.2 上跑老源码。现在很多主流面板默认 PHP 8.1装之前先问卖家或看源码里是否有composer.json确认依赖要求。安装命令以 Ubuntu/Debian 为例apt update apt install -y nginx mysql-server redis-server apt install -y php7.4-fpm php7.4-mysql php7.4-redis php7.4-gd php7.4-curl php7.4-mbstringCentOS 用户使用yum install rh-php74-php-fpm这类软件集时命令和 Linux 服务名都不同建议优先使用 Docker 或 Ubuntu 虚拟机避免折腾环境的时间超过跑通业务的时间。3.2 导入源码并修改配置文件的必改项安装完环境后把源码放到你自己的工作目录比如/data/wwwroot/soul_box然后创建数据库并导入 SQL。mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS soul_box DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -uroot -p soul_box /data/wwwroot/soul_box/install/db.sql导入时如果提示 SQL 语法错误先查一下install/db.sql文件开头有没有自动建库语句如果里有CREATE DATABASE你就不用先在 MySQL 里手工建库。之后打开config/database.php重点改三处数据库名、用户名、密码。?php return [ hostname 127.0.0.1, database soul_box, username soul_app, password Your_Password_Here, hostport 3306, prefix tb_, ];prefix必须与 SQL 建表语句里的表前缀完全一致否则框架会报“表不存在”。数据库用户建议手动创建一个不要用 root 直接跑业务避免网站被注入后全库被拖。3.3 配置 Nginx 伪静态并启动服务多数源码的入口在public目录下Nginx 站点根目录要指向public同时配置伪静态规则把请求转发到index.php。下面是一份适合 ThinkPHP/CI 的站点配置server { listen 80; server_name your-domain.com; root /data/wwwroot/soul_box/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(jpg|jpeg|png|gif|css|js)$ { expires 30d; } }这里的fastcgi_pass 127.0.0.1:9000对应 PHP-FPM 默认监听端口。PHP 7.4 在 Ubuntu 上默认是/run/php/php7.4-fpm.sock如果你用 socket 方式需要改成fastcgi_pass unix:/run/php/php7.4-fpm.sock;改完后执行nginx -t systemctl reload nginx systemctl restart php7.4-fpm php -m | grep redisphp -m的输出里必须包含redis否则后续操作 Redis 会直接报“Class Redis not found”。如果查不到执行apt install php-redis或手动安装扩展。打开浏览器访问http://你的IP/能看到登录页或跳转到安装向导就是基本跑通了如果出现目录列表说明 rewrite 没生效检查public路径和location /中的if条件是否拼错。4. 盲盒抽取接口的并发控制与概率参数调优4.1 抽取接口的常规流程与并发问题一个正常的抽取流程至少包含这些步骤校验用户登录状态 → 校验余额或积分 → 从 Redis 集合随机弹出一个盲盒ID → 写订单 → 更新盲盒状态 → 返回卡片信息。直接按这个顺序写代码最容易出现两个问题第一RedisSPOP弹出后PHP 进程在做后续 MySQL 更新时失败数据库里的盲盒仍然待抽取但 Redis 里已经没有这个ID了。第二两个请求同时走到“校验余额”但还没有扣款时余额检查都通过造成超额消费。这些问题不是并发量高才会出现哪怕只有两个请求同时打到接口时序不对就会触发。我给出的方案是让 Redis 先负责“唯一性”再让另一个进程负责“落库”。用户侧只要收到一个盲盒ID就算抽中后台即使落库失败也会走补偿退款逻辑不能把抽取的成败建立在一次 MySQL 更新的即时响应上。4.2 用 Lua 脚本把“弹出盲盒”和“记录抽取人”做成原子步骤为了解决SPOP之后、MySQL 更新之前进程崩溃导致的数据丢失最好把“弹出盲盒ID”和“记录谁抽中了”放到一个 Lua 脚本里执行。Redis 会原子执行 Lua 脚本不会被其他命令插入。-- KEYS[1]: 待抽取盲盒ID集合 -- KEYS[2]: 今日抽取记录 Hash -- ARGV[1]: 抽奖用户ID local box_id redis.call(SPOP, KEYS[1]) if box_id then redis.call(HSET, KEYS[2], box_id, ARGV[1]) end return box_id在 PHP 中这样调用?php public function draw($userId, $boxType) { $redis $this-getRedis(); $key blind_box: . date(Ymd) . : . $boxType; $logKey draw_log: . date(Ymd); $lua LUA local box_id redis.call(SPOP, KEYS[1]) if box_id then redis.call(HSET, KEYS[2], box_id, ARGV[1]) end return box_id LUA; $boxId $redis-eval($lua, [$key, $logKey, $userId], 2); if (!$boxId) { $this-rebuildBoxCache($boxType); // 回源MySQL重建集合 $boxId $redis-eval($lua, [$key, $logKey, $userId], 2); } if (!$boxId) { return [code 1, msg 盲盒已抽完请等待新盒子上架]; } // 丢入异步队列由Worker负责更新MySQL、写订单 $this-queue-push(markBoxDrawn, [boxId $boxId, userId $userId]); return [code 0, box_id $boxId]; }参数说明$redis-eval($lua, [$key, $logKey, $userId], 2)中的[$key, $logKey, $userId]是传给 Lua 脚本的参数数组前两个是 KEYS第三个是 ARGV第三个参数2告诉 Redis 有几个 KEYS。不要漏掉这个数字否则 Redis 会把$userId也当成 KEYS 处理导致脚本执行报错。每次抽取后异步 Worker 需要做的操作是把blind_box表里对应 ID 的status从 0 改为 1写入draw_user_id和draw_time同时插入一条已支付订单。如果 Worker 或数据库出现问题利用 Redis 里的draw_logHash 做逆向补偿遍历 Hash 找到已弹出但 MySQL 未更新的盲盒ID重新标记或退款。4.3 概率权重与男女盲盒池的参数配置很多运营并不希望每次抽取都直接给出真实用户而是希望混合一部分虚拟卡片或“箱底空盒”这种行为必须在用户协议里明确告知。技术上的做法是先把类型定出来再抽具体盲盒。常见的概率配置如下抽取结果概率说明真实用户卡片70%从 Redis 集合中 SPOP 得到虚拟脱单卡20%从虚拟卡池中随机返回提示空盒10%直接返回“手气不佳”代码逻辑可以这样写?php public function decideType() { $rand mt_rand(1, 100); if ($rand 70) { return real; } if ($rand 90) { return robot; } return empty; }这里的70、90建议不要写死在代码里而是在后台存一张配置表修改后用 Redis 缓存起来保证免重启生效。同时要注意“真实用户”和“虚拟卡”是从两个不同的集合取的真实用户池如果已经空了绝对不能把虚拟卡充当真实卡发给用户否则用户添加微信时发现找不到人客诉会立刻爆发。正确做法是当真实用户池为空时动态将概率临时调整为“虚拟卡 90%、空盒 10%”并在日志里记录一次概率切换。4.4 压测前必须把 Redis 盲盒池灌满这是很多人忽略的坑。盲盒池是空的你直接压/api/box/draw所有请求都跑到rebuildBoxCache回源 MySQL压测结果反映的完全是另一段代码的性能。正确做法是先用脚本写入 5000 个盲盒并把这些 ID 批量灌入 Redisfor i in $(seq 10001 15000); do echo $i; done | redis-cli --pipe redis-cli SADD blind_box:20250615:1 $(seq 10001 15000)然后使用ab压测ab -n 10000 -c 100 -k -H Authorization: Bearer test_token http://127.0.0.1/api/box/draw压测时重点看两个数据Requests per second是否达到预期Failed requests是否为 0。如果Failed requests很高第一件事不是调 Nginx 并发参数而是看 PHP-FPM 的request_terminate_timeout和 Redis 的maxclients。5. 上线前做数据验证和风控校验避免盲盒变成垃圾场5.1 验证同一盲盒不被抽中两次准备一个干净环境清空 Redis 里所有盲盒集合向blind_box表插入 100 条status0的测试数据并手动SPOP到 Redis 中。然后写一个并发脚本连续调用/api/box/draw200 次最后查数据库SELECT box_id, COUNT(*) AS c FROM draw_log GROUP BY box_id HAVING c 1;如果返回空说明抽取出幂等逻辑生效了。如果出现c 1的数据检查是否为异步 Worker 重复消费了队列消息在markBoxDrawn方法里要从头加锁$lockKey draw_lock: . $boxId; $locked $redis-set($lockKey, 1, [NX, EX 5]); if (!$locked) { return; // 已有人处理 }5.2 图片与文本审核的必要配置脱单盲盒的核心资产是用户上传的自拍图和微信号如果直接原样展示平台很快就会收到违规举报。图片上传接口必须接入内容审核至少做到截图鉴黄和 OCR 识别微信号防止用户上传其他平台的联系方式做引流。文本内容在写入数据库前要做敏感词过滤$result $securityService-checkText($nickname . $content); if (!$result[pass]) { return [code 10001, msg 内容包含违规词请修改]; }另外微信字段不能直接明文存储至少要用可逆加密后再落库。展示时需要混淆比如wxid_**8qk用户完成“查看联系方式”操作后只显示一次完整微信号后端记录操作日志。这样即使后台数据被拖走攻击者也不会拿到完整的微信号镜像。5.3 移动端弱网和兼容性技巧前端抽盲盒动画在请求发出时就启动不要等到接口响应才开始播动画。接口返回后不管是成功还是空盒动画都要能收住。推荐用 WebP 格式加载卡片背景图一个卡片样式图能缩减到原来的 60% 以下。小程序端注意wx.request的并发限制如果用户连点抽取按钮前端要做防重复提交等一个接口返回后再允许下一次点击。把最近一次抽取的盲盒ID存到localStorage或小程序缓存里用户断网重进时可以用这个ID拉取卡片详情避免展示“已抽取但查不到”的错误状态。本文还有配套的精品资源点击获取