简介:一套面向餐饮、零售、美业、旅游等线下商家的NFC碰一碰(碰碰卡/爆店码)贴牌源码方案,支持商家自定义品牌部署,聚合抖音、小红书、快手、视频号的内容一键发布,以及大众点评、美团、高德、百度的打卡收藏,重点解决线下门店引流、锁客与二次消费需求。资源包共1037个文件,压缩后约6.32MB,以707个PHP后端文件为主,辅以HTML页面、微信小程序文件、JSON配置等,覆盖服务端逻辑、管理后台、接口配置和基础素材,可直接运行或二次开发。目前已有494人学习下载。核心价值在于提供一套完整、可落地的贴牌部署源码,能快速搭建碰一碰营销系统,适用于需要为商家做数字化赋能的开发者、服务商或运营人员;包内目录和文件结构清晰,便于按功能模块拆解、替换品牌信息和扩展接口,也可作为NFC营销场景的实战参考。
1. 碰碰卡:一个让顾客“碰”一下就完成打卡、发布、收藏的NFC营销系统
线下门店最头疼的就是顾客进店以后留不下任何线上痕迹。扫码关注公众号已经成了玄学,加企业微信又嫌麻烦。碰碰卡这套系统解决的恰恰是这个场景:把一张NFC贴纸贴在收银台、餐桌或者商品架上,顾客手机靠上去碰一下,就能立刻打开一个专属页面,一键发布抖音笔记、小红书视频、快手作品,还能顺手在美团、高德、大众点评上完成打卡收藏。整个过程不到三秒,不需要下载APP,也不需要扫码关注。它本质上是一个为商家做赋能的SaaS工具,如果你是技术商,可以直接买源码做贴牌部署,把自己的品牌和商户管理后台套上去,给下游商家开账号。这套系统从卡片到后台的完整链路,正是我这次要拆给你的东西。
2. 项目全貌与技术选型:从NFC卡片到后台管理端的四层链路
接着第一章,我先把碰碰卡的整体架构画出来,再讲为什么选这套技术栈。习惯了先看目录结构的可以直接跳到2.3,但我建议从系统组成开始看,因为后面所有代码都是围绕这四层展开的。这种形态在市面上也被叫“爆店码”,但爆店码大多只做单点破站,碰碰卡把「碰一下」这个动作扩展成了完整的营销闭环。
2.1 系统由哪几部分组成
一个完整的NFC碰一碰营销系统,至少要有四端:NFC物理标签、用户端小程序/H5、服务端API、商家管理后台。NFC标签承担的是入口,里面存着一个URL,不同卡片对应不同门店和活动。用户端小程序负责渲染活动页,并调用各个平台的开放API去执行发布、打卡、收藏。服务端是所有逻辑的枢纽,处理链接重定向、生成短码、保存发布任务、对接平台授权。商家后台则是给业务方用的,可以创建门店、设置NFC编号、查看发布记录和打卡报表。
这种拆分最常见,也最容易被贴牌。我见过有团队把用户端做成小程序,也有做成H5的,其实两者可以共存:NFC标签里写同一个域名下的短链接,服务端根据User-Agent自动判断是在微信里还是浏览器里,分别跳转小程序或H5。这样做的好处是,同一个URL既能服务微信生态,又能覆盖支付宝和普通浏览器,适配不同碰一碰的入口。比如用户用手机浏览器碰一碰,落到H5页面,一样可以完成发布和打卡。
后台端不一定非要做成单独的Web系统,也可以在已有商家应用里嵌一个管理模块。但为了贴牌方便,我建议独立部署,因为每个贴牌商都需要改logo、改域名、改主题色,独立仓库可以避免和商家现有系统耦合。我在真实项目中见过把后台和服务端混写的,后来改需求时一团糟,从此我坚持四端分离。
2.2 为什么选这套技术栈
开发碰碰卡这类系统,我建议后端用Node.js + Express,主要原因是对接第三方平台时,各家API签名和回调示例大多数是JavaScript写法,同一个语言处理起来省事,排查问题也少很多。数据库选MySQL,因为要存商户、门店、用户、发布记录,关系型数据用MySQL更好做多租户隔离。前端小程序用原生语法,毕竟要调NFC能力,原生API最直接。管理后台用Vue + Element Plus,后台交互不算复杂,表格、表单占绝大多数。
最关键的是任务队列选型。一键发布抖音、小红书、快手,这些平台API响应很慢,而且有频率限制,一个用户一次发布四个平台,如果同步循环调用,接口可能5秒内都恢复不了。所以必须异步。我在这个项目里用的是BullMQ,基于Redis,天然支持延迟和重试。发布请求进来后先往队列塞一个任务,Worker消费时再逐个请求平台,这样接口的响应时间能控制在200ms以内。
为什么不选云函数或消息服务?当时考虑到贴牌客户可能把代码部署在自己的服务器上,不希望强行绑定云厂商,所以用Redis队列最通用。如果你已经有云服务,用MNS也能达到同样效果,但可移植性差一些。部署时把Redis和Node服务放进docker-compose,客户一台机器就能起来,运维成本低。
2.3 目录结构与代码组织
先看目录结构,我用一个模拟项目来说明。这个结构是按“前后端分离 + 多平台服务”组织的,你可以直接套用:
nfc-pengpengka/ ├── server/ # Node.js 服务端 │ ├── src/ │ │ ├── controllers/ # 路由控制器 │ │ ├── services/ # 业务逻辑,含平台对接 │ │ ├── utils/ # 工具函数,签名、短码生成 │ │ ├── routes/ # 路由定义 │ │ ├── middleware/ # 授权、日志、租户判断 │ │ ├── models/ # Sequelize 数据模型 │ │ ├── queues/ # BullMQ 任务队列 │ │ └── app.js │ ├── .env.example │ └── package.json ├── miniapp/ # 微信小程序用户端 │ ├── pages/ │ │ ├── index/ # 碰一碰落地页 │ │ ├── publish/ # 发布笔记/视频页面 │ │ └── member/ # 我的记录 │ └── app.js ├── admin/ # 商家管理后台(Vue) │ ├── src/ │ │ ├── views/ │ │ │ ├── store/ # 门店管理 │ │ │ ├── card/ # NFC卡片管理 │ │ │ └── records/ # 发布记录 │ └── vite.config.js └── deploy/ # Nginx配置、docker-compose.yml我特别强调server/services/platform这个目录,每个平台一个文件,比如douyin.js、xiaohongshu.js、kuaishou.js,统一导出publish()方法。这是后续扩展新平台的关键,后面第三章会讲如何抽象。
数据模型方面,至少要有这些表:sites(贴牌站点)、stores(门店)、cards(NFC卡片)、users(用户)、publish_records(发布记录)、checkin_records(打卡记录)。其中cards表是核心,shortCode字段要唯一索引。以下是一个简化后的建表语句,你可以参考:
CREATE TABLE cards ( id INT AUTO_INCREMENT PRIMARY KEY, site_id INT NOT NULL, store_id INT NOT NULL, short_code CHAR(6) NOT NULL UNIQUE, activity_id INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT '1=启用, 0=停用', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_store_short (store_id, short_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里site_id是贴牌站点的标识,store_id对应门店。在贴牌部署时,每个贴牌商有自己的site_id,不同站点之间数据隔离,靠这个字段实现。为什么用Sequelize?因为它支持迁移文件,贴牌客户升级数据库时不用手动跑SQL,一条sequelize-cli db:migrate就能完成。
3. 核心模块拆解:一次NFC碰一碰从闪卡到后台的全过程
这一章是全文的重头戏。我会从用户把手机贴近NFC卡片的那一刻开始,逐步拆解到后台收到发布记录为止,每个关键步骤都配上代码和参数说明。建议对着目录结构看,思路会更清晰。
3.1 NFC标签写入与URL规则设计
首先需要一张NFC写入工具。我一般用安卓手机上的NFC Tools,写入NDEF URI类型,内容为https://ppk.example.com/c/8f3a2b91。这里8f3a2b91是服务端生成的短码,对应cards表里的一条记录,记录里有store_id、activity_id、status。为什么要做短码而不是直接写完整URL?因为有些NFC标签容量只有48字节,完整后台路径可能塞不下,而且短码可以随时换跳转地址,不用重新烧卡。比如活动结束后,你想把卡片跳转到一个新的落地页,只需在后台修改cards表里的activity_id,卡片物理上不用换。
短码生成用Base62随机6位,代码很简单:
// server/src/utils/shortCode.js const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; function generateShortCode() { let code = ''; for (let i = 0; i < 6; i++) { code += ALPHABET[Math.floor(Math.random() * ALPHABET.length)]; } return code; } module.exports = { generateShortCode };这里用Math.random()就够了,因为并发量不大,不需要加密级别。插入cards表时,我会用shortCode做唯一索引,万一撞码就重新生成一次。为了更稳,可以在插入时捕获唯一索引冲突,再换一个码重试。这个函数没有替代品,但要记得把生成逻辑抽出来,方便测试。
3.2 服务端重定向与用户环境识别
用户手机碰一下,系统会打开上面的https://ppk.example.com/c/8f3a2b91,这个请求到了服务端。服务端根据User-Agent判断来源,决定是跳转小程序还是H5。下面是Express路由的代码。
// server/src/routes/card.js const express = require('express'); const router = express.Router(); const { getCardByCode } = require('../services/cardService'); router.get('/c/:code', async (req, res) => { const { code } = req.params; const card = await getCardByCode(code); if (!card || card.status !== 'active') { return res.redirect('https://ppk.example.com/error?reason=invalid'); } const ua = req.headers['user-agent'] || ''; if (ua.includes('MicroMessenger')) { // 微信内重定向到小程序页面 const path = encodeURIComponent('/pages/landing/index?cardId=' + card.id); const url = `weixin://dl/business/?t=xxx&path=${path}`; return res.redirect(url); } if (ua.includes('AlipayClient')) { // 支付宝内打开H5或小程序 return res.redirect(`https://ppk.example.com/h5/landing?cardId=${card.id}`); } // 手机浏览器直接渲染H5 res.redirect(`https://ppk.example.com/h5/landing?cardId=${card.id}`); }); module.exports = router;参数说明:code是NFC标签里的短码;user-agent判断环境,这里只区分了微信、支付宝和普通浏览器。注意weixin://dl/business/?t=xxx中的xxx必须由微信公众平台生成,每个小程序都有,这个值在后台“链接生成”里可以拿到。如果你的域名没有在微信后台配置成业务域名,微信会拦截这个跳转,这个坑第四章会细说。
3.3 一键发布多平台笔记与视频的任务队列
一键发布的本质是并行调用多个平台的开放API,但实际不能并行,因为抖音、小红书对频率限制很严,而且单台服务器IP可能被封。我在服务端使用BullMQ队列,把一次发布拆成一个主任务和多个子任务。
// server/src/queues/publishQueue.js const { Queue, Worker } = require('bullmq'); const connection = { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }; const publishQueue = new Queue('publish', { connection }); function buildPublishJob(publishId, platforms, payload) { return { name: 'publish-post', data: { publishId, platforms, payload }, opts: { attempts: 3, backoff: { type: 'exponential', delay: 5000 }, removeOnComplete: true, removeOnFail: false } }; } module.exports = { publishQueue, buildPublishJob };attempts: 3表示失败后重试3次,backoff指数延迟5秒起步,避免重试太快触发平台封禁。removeOnComplete设为true,成功任务不占Redis内存;失败的保留,方便排查。
Worker消费任务时,按平台串行调用。注意不同平台的payload结构差异很大,抖音要传video_id,小红书要传note_image,快手要传caption。我这里用一个策略模式,把每个平台的实现放到独立文件:
// server/src/services/platform/index.js const douyin = require('./douyin'); const xiaohongshu = require('./xiaohongshu'); const kuaishou = require('./kuaishou'); const wechatVideo = require('./wechatVideo'); const platforms = { douyin, xiaohongshu, kuaishou, wechatVideo }; async function publishToPlatform(platformName, payload, storeId) { const handler = platforms[platformName]; if (!handler) throw new Error(`unsupported platform: ${platformName}`); if (typeof handler.preprocess === 'function') { payload = handler.preprocess(payload, storeId); } const result = await handler.publish(payload); return result; } module.exports = { publishToPlatform };每个平台的publish()里要处理设备注册、文件上传、文案拼接、频率控制。以小红书为例,发布一篇笔记需要先上传图片素材到小红书服务器,拿到file_id,再调用note/publish接口。上传前要检查图片大小,小红书限制单张图片不超过10MB,否则会被拒绝。我在preprocess里做图片压缩,用sharp库把图片压到8MB以内。
3.4 一键打卡收藏的实现细节
打卡收藏走的是各平台的POI接口。大众点评、美团、抖音、高德、百度都有自己的位置服务。所谓“打卡”,本质是上报用户坐标和POI ID,或者调用平台的签到接口。这里最麻烦的是美团需要用户授权,高德需要用户打开定位权限。
我的实现是在用户端小程序里,先通过wx.getLocation拿到经纬度,然后后端根据经纬度反查各平台的POI ID。高德有regeo接口,百度有geocoder,美团和点评则用它们自己的搜索API。代码如下:
// server/src/services/checkin.js const axios = require('axios'); async function getAmapPoiId(lng, lat, keyword) { const url = 'https://restapi.amap.com/v3/place/around'; const params = { key: process.env.AMAP_KEY, location: `${lng},${lat}`, keywords: keyword, radius: 1000 }; const resp = await axios.get(url, { params }); const poi = resp.data.pois[0]; return poi ? poi.id : null; }参数说明:lng和lat是用户当前位置的经度和纬度,keyword是门店名称。radius设为1000米,防止门店位置较偏时搜不到。返回值是POI ID,这个ID要传给下一个打卡接口。高德这边如果找不到对应POI,就需要调POI上报接口,这个在避坑章具体讲。
把打卡行为固定下来后,后台可以汇总每个门店的打卡次数,用于商家结算。用户端落地页的代码也顺带提一下,小程序拿到cardId后请求接口渲染门店信息和平台按钮:
// miniapp/pages/landing/index.js Page({ data: { cardId: '', store: {}, platforms: [] }, onLoad(options) { const cardId = options.cardId; this.setData({ cardId }); this.fetchStoreInfo(cardId); }, fetchStoreInfo(cardId) { wx.request({ url: `https://api.example.com/v1/cards/${cardId}`, success: (res) => { this.setData({ store: res.data.store, platforms: res.data.platforms }); } }); }, onPublishTap() { // 跳转到多平台发布选择页,传递 cardId wx.navigateTo({ url: `/pages/publish/index?cardId=${this.data.cardId}` }); } });这里的platforms数组是服务端根据店铺授权状态返回的,比如抖音、小红书是否已经绑定。前端只渲染已绑定的平台,避免用户点了没反应。
4. 部署避坑指南:NFC识别、微信重定向、平台授权那些坑
这一章我把自己在开发碰碰卡过程中遇到的几个典型坑记录下来,每一条都是「现象 → 原因 → 解决」的格式,希望你能少走弯路。这几个坑不是偶发,大部分部署上线都会遇到。
4.1 手机一碰没反应:NFC写入格式不规范
现象:用户把手机贴近贴纸,没有任何反应,甚至有时弹窗显示“不支持该标签”。原因:很多NFC工具默认写入的是NDEF Text格式,部分手机系统只能识别NDEF URI格式。还有一部分标签是NFC Type 1,存储容量太小且兼容性差。解决:写入时选择“URL/URI”类型,并且前缀用https://。如果你手头只有USB读卡器,在电脑上用libnfc命令行写入也行。另外标签尽量选NFC Type 2,容量至少168字节,能覆盖几个平台投放的短链。
4.2 微信内打开链接被拦截:URL Scheme选择错误
现象:在微信里碰一碰,浏览器明明已经跳转,但页面显示“已停止访问该网页”或“非微信官方网页”。原因:跳转的域名没有在微信公众平台配置为「业务域名」,或者没有通过ICP备案。解决:在微信公众平台「开发管理 - 开发设置」里配置JS接口安全域名和业务域名,域名必须https。注意,不是把短链域名配一次就行,实际跳转的H5页面域名也必须配置。另外,微信跳小程序的URL Scheme需要在公众平台生成,并且有效期只有30天,需要定期刷新,不能写死。
4.3 小红书/抖音授权失败:回调域名与平台白名单不一致
现象:发布笔记时提示access_token无效或redirect_uri错误。原因:在小红书开放平台创建应用时,填写的授权回调地址必须与代码里的redirect_uri完全一致,而且不能带路径参数。抖音同理,回调地址要精确到路径。解决:统一用一个固定路径/auth/callback,在代码里读环境变量,不要再拼参数。排查时可以先在平台开放工具的调试页面里测试一次,把实际请求的redirect_uri复制下来,比对后台配置。我踩过这个坑后,每次新增平台都会先写一个auth.test.js,专门检查回调地址匹配。
4.4 高德打卡位置偏移:需要上传POI且审核
现象:打卡时高德返回INVALID_USER_POI,打卡记录显示空白。原因:打卡的POI必须是高德地图里已经存在的商户POI,且状态为审核通过。很多新门店在地图上还没有POI,直接调用place/around搜索不到。解决:商家在后台录入门店时,代码自动调用高德POI搜索,如果返回为空,再调POI上报接口提交门店信息,等待审核。审核周期一般1-3天,测试时可以用高德的沙箱环境,避免污染线上数据。我在项目里加了一个定时任务,每6小时检查一次未审核的门店POI状态。
4.5 贴牌系统下商户数据隔离:公共表和私有表的坑
现象:平台运行一周后,A商户在后台能看到B商户的卡片记录。原因:贴牌部署时,多个商户共用一个数据库,某些查询漏加了store_id条件。解决:所有涉及商户数据的SQL都必须强制带storeId,并且建立联合索引(store_id, id)。在Sequelize中,我推荐定义全局作用域,如下:
// server/src/models/Card.js module.exports = (sequelize) => { const Card = sequelize.define('Card', { shortCode: DataTypes.STRING, storeId: DataTypes.INTEGER, status: DataTypes.STRING }, { defaultScope: { where: { status: 'active' } } }); return Card; };这个defaultScope默认只查启用的卡片,但数据隔离不能只靠这个。更稳妥的做法是封装一个findByStore(storeId, query)函数,内部强制where.storeId = storeId,业务层只调用这个函数,避免到处裸写Card.findAll()。
5. 从源码到上线:本地跑通与环境配置
前面已经理解了核心模块,这一章我教你如何把整个系统在本地跑起来,并做一次完整的碰一碰模拟。我假设你已经装了Node.js 18+、MySQL 8、Redis 6,并且有微信小程序开发者工具。没有NFC硬件也能完成大部分测试,因为核心逻辑都在服务端。
5.1 安装依赖与初始化数据库
首先克隆代码,在server目录下安装依赖:
cd server npm install cp .env.example .env然后创建数据库并执行迁移:
npx sequelize-cli db:create npx sequelize-cli db:migrate.env里需要至少配置以下内容,参见下表:
| 变量名 | 说明 | 示例 |
|---|---|---|
| DB_HOST | MySQL地址 | 127.0.0.1 |
| DB_NAME | 数据库名 | ppk |
| REDIS_HOST | Redis地址 | 127.0.0.1 |
| REDIS_PORT | Redis端口 | 6379 |
| WECHAT_APP_ID | 小程序AppID | wx123456 |
| AMAP_KEY | 高德Web服务Key |
复制.env后,编辑里面参数。我建议用.env管理所有密钥,不要提交到仓库。NODE_ENV=development时,Sequelize会输出SQL日志,方便调试。
5.2 配置项说明
下面列出关键环境变量,按模块分组:
| 分组 | 变量名 | 说明 | 示例 |
|---|---|---|---|
| 基础 | PORT | 服务端口 | 3000 |
| 数据库 | DB_HOST | MySQL地址 | 127.0.0.1 |
| Redis | REDIS_HOST | Redis地址 | 127.0.0.1 |
| 微信 | WECHAT_APP_ID | 小程序AppID | wx123456 |
| 抖音 | DOUYIN_CLIENT_KEY | 抖音开放平台应用Key | |
| 小红书 | XIAOHONGSHU_CLIENT_ID | 小红书开放平台应用ID | |
| 高德 | AMAP_KEY | 高德开放平台Web服务Key |
注意抖音和小红书的开放平台都需要企业资质,个人开发者不能直接申请。如果你只是本地测试,可以先在平台上用一个测试应用,跑通流程后再换成正式应用。
5.3 测试流程:模拟一次完整的碰一碰
如果没有NFC标签,完全可以用curl模拟。先手动往cards表插一条记录,短码设为abc123。然后执行:
curl -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)" http://localhost:3000/c/abc123如果返回302并跳转到https://ppk.example.com/h5/landing?cardId=1,说明服务端路由正常。再用微信的User-Agent试一次,看是否跳到weixin://开头的地址。小程序端开发时,可以在app.js里把cardId写死,直接跳转到落地页,这样不用每次都碰卡。
提示:微信开发工具中打开
weixin://协议是无效的,请使用真实手机预览,或者暂时注释掉微信跳转逻辑直接打开H5页面。
5.4 贴牌部署时的多租户配置
贴牌部署意味着每个客户拿到源码后,想换上自己的品牌。我的做法是后台管理端的site表里存品牌名、logo、域名、备案号,前端页面全部通过接口加载这些配置,代码里不允许出现硬编码的“碰碰卡”字样。部署时,nginx里做多域名的映射,每个域名对应一个site_id,服务端根据Host头识别租户。
// server/src/middleware/tenant.js module.exports = async (req, res, next) => { const host = req.headers.host; const site = await getSiteByDomain(host); if (!site) { return res.status(404).json({ message: 'unknown site' }); } req.site = site; next(); };在Nginx里可以这样设置:
server { listen 80; server_name client-a.example.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; } }这样每个贴牌客户都有自己的独立域名,后端只认域名不认路径,数据隔离更清晰。域名和site_id的映射关系存在site_domains表,一张表搞定。
6. 二次开发实战:给碰碰卡增加一个B站发布模块
当商家手里的平台越来越多,加新平台就成了最常见的需求。我以增加B站动态发布为例,带你走一遍完整的改造流程。这个流程可以复用到任何新平台,核心思路是“先抽象、再实现、后验证”。
6.1 分析第三方平台的通用发布接口
首先去B站开放平台看文档,它支持发视频动态,需要的参数是title、content、coverUrl、videoUrl。授权方式与抖音类似,都是OAuth2。差异点在于:B站上传视频时需要一个视频预上传接口,返回vid。我把它抽象成标准结构,核心是media数组,每个元素描述一条素材。
6.2 扩展任务队列处理器
在server/src/services/platform/下新建bilibili.js:
// server/src/services/platform/bilibili.js async function preprocess(payload, storeId) { if (!payload.videoUrl) { throw new Error('bilibili requires videoUrl'); } const uploadInfo = await uploadVideo(payload.videoUrl, storeId); return { ...payload, vid: uploadInfo.vid }; } async function publish(payload) { const api = 'https://api.bilibili.com/x/article/creative/draft/update'; const resp = await axios.post(api, { title: payload.title, content: payload.content, vid: payload.vid }, { headers: { 'Access-Token': payload.accessToken } }); return resp.data; } module.exports = { preprocess, publish };然后在platform/index.js里注册:
const bilibili = require('./bilibili'); const platforms = { douyin, xiaohongshu, kuaishou, wechatVideo, bilibili };注意我在preprocess里做视频上传,这样publish函数就纯粹只负责发布,职责单一。以后换平台,只需要关注preprocess和publish两个函数是否实现完整。
6.3 后台管理端增加配置项
在Vue后台的发布页增加一个B站复选框,存储到publish_records.platforms字段。这个字段用JSON数组保存,["douyin","xiaohongshu","bilibili"]。前端提交时直接传数组,后端用DataTypes.JSON存储,方便以后按平台查询。相比用逗号分隔字符串,JSON数组在查询时可以直接用JSON_CONTAINS,效率高很多。
6.4 验证改造
启动后端和Redis Worker,用下面这个脚本构造一个包含B站的发布任务:
const { publishQueue, buildPublishJob } = require('./queues/publishQueue'); const job = buildPublishJob(101, ['bilibili'], { title: '门店开业优惠', content: '今日到店送饮品', videoUrl: 'https://example.com/video/xx.mp4', accessToken: process.env.BILIBILI_ACCESS_TOKEN }); await publishQueue.add(job.name, job.data, job.opts);然后观察Worker日志,如果返回status字段等于成功,说明发布流程走通。再手动去B站后台看草稿箱,确认内容已经生成。
改造新平台最怕一把梭。从那以后我每次对接新平台都强制走一遍:先看官方调试工具,把请求参数复制到Postman验证通过,再写代码;写完代码再用同样的参数跑单测,最后才联调。希望帮到你。
本文还有配套的精品资源,点击获取