最近任务
接入与调度
采集节点接入
relay-node -server http://127.0.0.1:8080 -token node-token-001 -name node-1 -workers 2
节点启动后每 10 秒上报一次心跳,并持续从任务池并发领取已到期的任务;超过 30 秒无心跳会被守护协程置为离线并释放其持有的任务。
任务分发规则
任务按「采集间隔」到期后进入待分发队列,由空闲节点原子抢占(运行状态变为采集中),采集完成后节点回报结果,同时把下次到期时间向后顺延一个间隔。
页面上所有列表支持 8 秒自动刷新(右上角开关控制),也可随时点「刷新」立即拉取。
套餐
采集节点
服务端对接文档
本文档描述采集节点、客户端软件与本服务端的接口约定。内容为公开文档, 无需登录即可查看;涉及管理端接口的部分需要在请求头携带管理员令牌。
本服务只提供接口,不提供面向最终用户的页面(/app 已下线):
用户端界面由你们另行开发,直接调用第 3 节的接口即可,服务端不需要再改。
1. 鉴权方式
除登录与网络验证校验接口外,所有接口都需要在请求头带令牌,令牌通过登录接口获取。
| 角色 | 请求头 | 获取方式 |
|---|---|---|
| 采集节点 | Authorization: Bearer <node_token> | POST /api/node/register |
| 用户端 | Authorization: Bearer <user_token> | POST /api/app/login |
| 管理后台 | Authorization: Bearer <admin_token> | POST /api/admin/login |
| 授权客户端 | Authorization: Bearer <user_token>(推荐)或随请求体提交 license_key | POST /api/license/verify |
统一响应结构:{"code":200,"msg":"success","data":{...}},code 为 200 表示成功,其余取值见第 7 节;HTTP 状态码与 code 保持一致。
分页接口统一返回 data = {"list":[...],"total":100,"page":1,"size":20},请求参数为 page(从 1 开始)与 size(默认 20,最大 200)。
例外:在线客服的 GET /api/chat/ws 是 WebSocket,浏览器无法携带自定义请求头,令牌改为 query 传递 ?token=<token>(管理端再加 &role=admin)。
2. 采集节点对接
2.1 开通节点(先做这一步)
服务默认开启节点准入(task.require_preset_node: true):只允许管理员事先登记过的
node_token 上线,未登记的 token 调注册接口会直接返回 401「节点凭证未登记,请联系管理员开通」。
- 方式一(推荐):管理员在后台「节点管理」里新增节点,填名称与
node_token(8~128 位,自己约定), 节点即可用这个 token 注册上线。 - 方式二(运维直连数据库):
INSERT INTO node(node_token,name,max_task,status,disabled,created_at) VALUES('<token>','机房A-01',5,0,0,NOW());
2.2 注册节点(幂等)
POST /api/node/register
Content-Type: application/json
{"node_token":"节点密钥(8~128 位,自行约定)","node_name":"机房A-01","max_task":5}
同一 token 重复注册视为更新 node_name / max_task。注册成功后,后续所有节点接口都用它作为 Bearer 令牌鉴权;
节点总数受配置上限约束,超限返回 403。
2.3 心跳
POST /api/node/heartbeat
Authorization: Bearer <node_token>
{"running_task_ids":[12,15],"max_task":5,"version":"1.0.0","err_msg":""}
2.4 拉取任务
GET /api/task/poll
Authorization: Bearer <node_token>
返回当前分派给该节点的任务,同时下发 push_interval(秒):
{"code":200,"data":{
"task":{"task_id":12,"keyword":"iPhone 15 Pro","interval":60,...},
"push_interval":60,
"lock_ttl_sec":60,
"heartbeat_interval_sec":10
}}
push_interval 取自任务所属用户的套餐,含义是「多少秒推送一次商品信息」,节点应按此节流推送。
2.5 上报商品
POST /api/task/report
Authorization: Bearer <node_token>
{"task_id":12,"items":[{"item_id":"...","title":"...","price":"3999",
"seller_id":"...","item_url":"https://www.goofish.com/item?id=...",
"publish_time":1730000000000}]}
必填 item_id 与 title;price 数字或数字字符串均可;
publish_time 支持毫秒时间戳或 2026-10-30 12:00:00 形式。
按 item_id 去重合并,重复上报不会产生重复商品,只会更新字段。
节点上报的商品进入全平台共享商品池,按 item_id 去重;所有用户都能按条件提取,不区分是谁采到的。
3. 用户端接口(只有 API,没有页面)
所有接口需带 Authorization: Bearer <user_token>,除登录外没有匿名接口。
这里的数据模型与「一个用户一套数据」不同,务必先看清楚:
| 对象 | 归属 | 用户能做什么 |
|---|---|---|
| 监控任务 | 不归任何用户(管理端创建,uid 只记来源) | 只读列表 + 自取执行(长轮询领任务、上报结果),不能建/改/删 |
| 商品 | 全平台共享池 | 上传(采集端灌数据)+ 按条件提取 + 查询,能看到全平台的商品 |
| 提取规则 | 归自己 | 增删改查,数量受套餐 task_limit 约束 |
3.1 账号
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/app/login | 账号密码登录,返回 token 与账号概要(免鉴权) |
| POST | /api/app/logout | 退出登录,令牌立即失效 |
| GET | /api/app/me | 当前账号信息、套餐、到期时间、规则额度、推送间隔 |
| PUT | /api/app/password | 修改密码 |
POST /api/app/login {"username":"user01","password":"123456"}
// 返回
{"code":200,"msg":"登录成功","data":{
"token":"ut_xxxxxxxx",
"user":{"uid":3,"username":"user01","nickname":"客户A","status":1}
}}
GET /api/app/me // Header: Bearer <token>
{"code":200,"data":{
"uid":3,"username":"user01","nickname":"客户A",
"plan_name":"月卡","plan_unit":"day",
"expire_at":"2026-11-29 12:00:00","expire_text":"剩余 30 天",
"task_limit":5,"task_used":2,
"push_interval":60
}}
push_interval 是套餐约定的商品推送间隔(秒):客户端按「每隔 N 秒推送一次」执行;任务自建的 interval 不能小于它。
3.2 监控任务(任务池:只能自取,不能创建)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/app/task/list | 任务池只读列表(全平台任务,不按归属筛选,分页,可选 enabled / run_status) |
| GET | /api/app/task/poll | 自取任务(长轮询,参数 hold_sec 1~120,默认 30);没有任务时挂起等,超时返回 task:null |
| POST | /api/app/task/report | 上报执行结果(body 带 task_id,可选 items 一并入库) |
以下接口已下线(调用返回 404):/api/app/task/create、/api/app/task/update、
/api/app/task/status、DELETE /api/app/task/:id。任务统一由管理端「任务管理」创建与启停。
GET /api/app/task/poll?hold_sec=30
Authorization: Bearer <user_token>
// 领到任务
{"code":200,"data":{
"task":{"task_id":12,"keyword":"iPhone 15 Pro","price_min":3000,"price_max":5000,
"interval":60,"enabled":1,"run_status":1,"filter_rule":{...}},
"push_interval":60,
"lock_ttl_sec":60,
"heartbeat_interval":10,
"server_time":"2026-10-30 12:00:00"
}}
// 没有任务(长轮询超时)
{"code":200,"data":{"task":null,"push_interval":60,
"lock_ttl_sec":60,"heartbeat_interval":10,"server_time":"..."}}
任务 interval 是「多久轮转一次」,被领走后按它重新排队,所以多个采集端会先后领到同一个任务,属于正常现象。
领到即加锁 lock_ttl_sec 秒,超时未上报自动回收给别人。
POST /api/app/task/report
{"task_id":12,"status":"success","err_msg":"",
"items":[{"item_id":"812345678901","title":"iPhone 15 Pro 国行","price":3999, ...}]}
// 返回
{"code":200,"data":{"task_id":12,"success":true,"lock_lost":false,
"released":true,"inserted":12,"updated":18,"skipped":0}}
items 可省略(只上报执行结果);带上就等价于「上报 + 上传商品」。
status 为 failed 时填 err_msg,任务下一轮照常分发。
任务列表返回字段:task_id / keyword / price_min / price_max / filter_rule / interval / enabled / run_status / lock_owner /
last_dispatch_at / next_due_at / created_at / updated_at。
内部实现:用户身份被映射成一个内部节点(node_token = "app:<uid>",首次调用自动创建),
所以任务锁、并发上限、长轮询与采集节点走的是同一套派发逻辑。
3.3 商品池:上传与提取
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/app/item/upload | 上传商品到共享池,body {"task_id":12,"keyword":"iPhone","items":[...]},也支持直接传数组 |
| GET | /api/app/item/extract | 按条件提取商品,单次最多 30 条,受套餐额度与提取间隔限制 |
POST /api/app/item/upload
{"task_id":12,"keyword":"iPhone 15 Pro","items":[
{"item_id":"812345678901","title":"iPhone 15 Pro 国行","price":3999,
"seller_id":"2207427224769","seller_nick":"闲鱼用户",
"brand":"Apple/苹果","model":"iPhone 15 Pro","storage":"256GB",
"item_condition":"95新","city":"杭州","tags":["验货宝","包邮"],
"image":"https://img.alicdn.com/...","want_cnt":12,
"publish_time":1730000000000}
]}
// 返回
{"code":200,"data":{"inserted":1,"updated":0,"skipped":0,"uploader":3}}
必填 item_id 与 title;item_id 全局唯一,重复上传只更新字段,不会产生重复行。
keyword 会补到没填关键词的商品上,便于池子里按词追溯;单次条数受服务端上限约束(max_report_items,默认 500,超限返回 400)。
GET /api/app/item/extract?keyword=iPhone&sort=newest&since=1730000000000
Authorization: Bearer <user_token>
// 返回
{"code":200,"data":{
"list":[{"item_id":"812345678901","title":"iPhone 15 Pro 国行","price":3999,
"brand":"Apple/苹果","model":"iPhone 15 Pro","storage":"256GB",
"item_condition":"95新","city":"杭州","tags":["验货宝"],
"want_cnt":12,"publish_time":1730000000000,"collect_time":"..."}],
"count":1,"limit":30,"has_more":false,
"next_since":1730000000000,"query_id":0,"push_interval":60}}
| 参数 | 说明 |
|---|---|
query_id | 用已保存的提取规则;传了它,下面这些条件以规则里的为准 |
keyword | 匹配标题或采集关键词(模糊) |
brand / model | 品牌(含原始品牌串)/ 型号,模糊匹配 |
price_min / price_max | 价格区间,数字 |
sort | newest(默认,按发布时间倒序)/ default(按想要人数倒序) |
since | 增量游标:只返回 publish_time > since 的商品,下次传返回的 next_since |
不传 query_id 时,keyword / brand / model / price_min / price_max 至少填一个,否则返回 400。
单次固定最多 30 条,has_more=true 表示还有更多,可翻页或调小 since 之外的区间再取。
限制(都按套餐生效):
- 两次提取的最小间隔 = 套餐
push_interval秒,未到返回429,data.retry_after给出还需等待的秒数 - 套餐
task_limit:0= 不含此功能(403)、-1= 不限、N= 最多 N 条提取规则 - 提取不留领取记录,同一商品可以被多个账号反复取到;增量靠客户端自己带
since
3.4 商品查询(全平台可见)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/app/item/list | 分页列表,支持 task_id / item_id / keyword(标题模糊)/ start_time / end_time / page / size |
| GET | /api/app/item/detail | 商品详情,参数 item_id |
| GET | /api/app/item/export | 按同样筛选条件导出 CSV(UTF-8 BOM,Excel 直接打开) |
GET /api/app/item/list?page=1&size=20&keyword=iPhone
{"code":200,"data":{"list":[{"id":88,"item_id":"812345678901",
"uid":3,"task_id":12,"title":"iPhone 15 Pro 国行",
"price":3999,"seller_id":"2207427224769","seller_nick":"闲鱼用户",
"item_url":"https://www.goofish.com/item?id=812345678901",
"publish_time":1730000000000,"collect_time":"2026-10-30T12:00:00+08:00"}],
"total":1,"page":1,"size":20}}
start_time / end_time 作用在采集时间上,格式 2026-10-01 00:00:00 或 2026-10-01。
这两个查询接口返回的是整个商品池,不按上传者过滤(uid 字段只表示首个上传者,供追溯)。
3.5 我的提取规则
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/app/query/list | 我的提取规则列表 |
| POST | /api/app/query/save | 新建/修改(body 带 id 即修改),数量受 task_limit 限制 |
| DELETE | /api/app/query/:id | 删除规则 |
POST /api/app/query/save
{"name":"iPhone 15 Pro 低价","keyword":"iPhone 15 Pro",
"brand":"Apple/苹果","model":"iPhone 15 Pro",
"price_min":3000,"price_max":5000,"sort":"newest","enabled":1}
// 返回
{"code":200,"data":{"id":7,"name":"iPhone 15 Pro 低价","keyword":"iPhone 15 Pro",
"brand":"Apple/苹果","model":"iPhone 15 Pro","price_min":3000,"price_max":5000,
"sort":"newest","enabled":1,"last_extract_at":null}}
name 可留空(自动用关键词);enabled=0 的规则被 query_id 引用时返回 403。
3.6 在线客服
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/app/chat/session | 我的会话(不存在时自动创建) |
| GET | /api/app/chat/messages | 历史消息,参数 before_id / limit |
| POST | /api/app/chat/send | 发消息 {"msg_type":"text","content":"在吗"}(WS 断线兜底) |
| POST | /api/app/chat/read | 清空未读 |
| POST | /api/app/chat/upload | 上传图片(multipart,字段 file),返回可访问 URL |
| WS | /api/chat/ws?token=<user_token> | 实时收发:连上后服务端推送新消息与未读;发送走 chat/send 或 WS 消息体 |
msg_type 仅支持 text / image,文本上限 2000 字。
3.7 我的授权与续费
见第 4 节:GET /api/app/license/list(我的授权 + 已绑设备)、
POST /api/app/license/redeem(贴卡密自助续费)、POST /api/app/license/unbind(换机解绑)。
4. 授权与权益接口
账号是唯一的权益主体:到期时间、规则额度、推送间隔都记在 user_entitlement(一个账号 + 一个软件 = 一条)。
授权码只做「充值凭证」,激活时把套餐时长叠加进账号权益。
4.1 校验授权(客户端启动 / 定时心跳)
POST /api/license/verify
Content-Type: application/json
{"app_code":"xianyu_monitor",
"user_token":"登录接口返回的令牌", // 账号态,推荐
"license_key":"XXXX-XXXX-XXXX-XXXX", // 可选:首次激活才需要
"device_id":"机器码","device_name":"DESKTOP-01","version":"1.2.0"}
- 带
license_key且卡未用过 → 绑定到该账号,把days+unit叠加到权益(基准 = max(当前时间, 原到期时间)),卡标记已使用 - 不带
license_key→ 直接按账号权益判定,返回同样的结果结构 - 账号不可用时可直接传
username+password代替user_token
{"code":200,"data":{"valid":true,"mode":"strict","plan_name":"月卡",
"expire_at":"2026-11-30 12:00:00","days_left":31,"device_limit":1,
"uid":123,"username":"demo","message":"授权有效"}}
4.2 心跳(只校验不激活)
POST /api/license/heartbeat // 参数同 verify,可不带 license_key
4.3 解绑设备(换机)
POST /api/app/license/unbind // 账号态:Header Bearer <user_token> + {"app_code":"...","device_id":"..."}
POST /api/license/unbind // 兼容老卡:{"app_code":"...","license_key":"...","device_id":"..."}
4.4 我的授权
GET /api/app/license/list // Header Bearer <user_token>,返回每个软件的到期时间、额度、已绑设备
4.5 账号自助续费(客户端 / 自建用户端都调这个)
POST /api/app/license/redeem // Header Bearer <user_token>
{"license_key":"XY-2A7F-9C31-B4E8"}
// 返回:{"code":200,"msg":"充值成功,时长已叠加到「闲鱼监控端」","data":{
// "app_code":"xianyu_monitor","app_name":"闲鱼监控端","plan_name":"月卡",
// "expire_at":"2026-12-30 10:00:00","days_left":61,"task_limit":5,"status":1}}
只有一个输入框:用户贴卡密 → 服务端自动识别卡密属于哪个软件 → 把套餐时长叠加到该账号在该软件的权益上 (未到期在原到期时间上接着加,已到期从现在起算);同一张卡在同一账号下不能重复充值,已绑定其它账号的卡直接拒绝。 网页续费不占设备名额,设备仍由客户端登录时绑定。
一机一码(默认):同一账号换机器时需先解绑旧设备;后台「授权与权益 → 设备」可代为解绑。 校验流水在「授权与权益 → 验证流水」查看。
5. 套餐、时长与推送间隔
套餐由三个维度组成,开通/续费时会把它们快照到用户身上:
| 字段 | 含义 | 取值 |
|---|---|---|
app_code | 套餐归属的软件 | 留空 = 通用套餐(所有软件都能选);填软件 code 则只在开该软件的卡密/权益时出现在下拉里,不同软件可以有同名套餐 |
days + unit | 时长数值 + 单位 | unit 为 hour(小时)或 day(天),如 3 小时、30 天 |
task_limit | 可创建的提取规则数上限 | -1 不限 / 0 不含此功能 / N 上限 |
push_interval | 商品推送间隔 | 5~3600 秒,默认 60 |
到期时间计算规则:
- 单位 = 天:到期时间 = 基准时间 + N 天(自然日,跨月跨年按日历)
- 单位 = 小时:到期时间 = 基准时间 + N 小时(精确到小时)
- 续费时基准时间 = 未到期则取原到期时间(顺延),已到期则取当前时间(重新起算)
推送间隔的实际生效点:
- 节点拉任务(
/api/task/poll)时下发该任务用户的push_interval - 用户端查询自身信息(
/api/app/me)时下发push_interval - 客户端按「每隔 push_interval 秒推送一次商品」执行;留空或未设置一律按 60 秒
6. 授权码、套餐与权益的关系
三者已合并成一条链路,到期时间只有一个来源(user_entitlement):
| 角色 | 存储 | 说明 | |
|---|---|---|---|
| 账号 | 权益主体 | user_entitlement | 到期时间、规则额度、推送间隔、设备数上限都记在这里 |
| 套餐 | 模板 | plan | 开通/续期/生成卡密时把 name+days+unit+task_limit+push_interval 快照出去 |
| 授权码 | 充值凭证 | license | 只记「是否已使用 used_at」与套餐快照,不存到期时间 |
| 设备 | 一机一码载体 | license_device | 换机/解绑只动这张表,不影响权益 |
- 激活即起算:卡密生成时不算时间,激活那一刻才
base = max(now, 原到期) + 时长,批量备货不再白折损 - 多卡叠加:同一账号续多张卡,时长首尾相接累加,不会覆盖
- 跨软件不可用:卡密属于哪个
app_code就只能激活那个软件的权益 - 一码一次:一张卡只能绑定一个账号(
used_at非空即已用) - 一处到期、两处生效:同一个权益既决定软件能否启动(verify/heartbeat),也决定平台功能额度(
/api/app/*鉴权) - 后台「账号权益」页可直接开通/续期(不走卡密),客服补发时用这个最快
- 升级兼容:升级前已经绑定过设备的老卡(
expire_at有值)仍按mode=legacy放行到原到期时间
6.1 日常怎么用(一条流程,不用记两套)
| 谁 | 在哪 | 做什么 |
|---|---|---|
| 你(卖家) | 后台 →「授权与权益」第 1 张卡 | 建套餐(多久、几个任务、推送间隔) |
| 你(卖家) | 同一页 →「生成授权码」 | 选软件 + 选套餐 + 填数量,导出卡密去卖 |
| 客户 | 客户端 / 你们自建的用户端 | 调 POST /api/app/license/redeem 贴卡密 → 到期时间自动往后加 |
| 客户 | 客户端软件 | 用同一个账号登录,软件每次启动都按这个权益校验并绑定设备 |
| 你(卖家) | 同一页 →「账号权益」 | 查谁到期了、剩几天、几台设备;客服补发直接用「直接开通/续期」 |
所以后台只有一个入口「授权与权益」,套餐、卡密、权益、设备、流水都在这一页自上而下;
对外也只有一个续费动作(POST /api/app/license/redeem,body 里一个 license_key 输入框即可)。
不需要再区分「网络验证」「套餐兑换」两套东西。
7. 错误码
| code | 含义 | 处理建议 |
|---|---|---|
| 200 | 成功 | — |
| 400 | 参数错误 | 检查请求体字段与类型 |
| 401 | 未登录或令牌失效 | 重新登录换新令牌 |
| 403 | 无权限 | 确认账号角色 |
| 404 | 资源不存在 | 检查 id / 授权码 |
| 409 | 已存在或状态冲突 | 换名称或先处理冲突数据 |
| 429 | 频率限制 | 降低请求频率后重试 |
| 500 | 服务器错误 | 稍后重试,必要时联系管理员 |
文档由服务端维护,接口有变更会同步更新此页。对接问题可通过后台「在线客服」联系。
说明:/api/admin/* 一组接口仅服务本后台页面使用,字段可能随界面调整,不对外承诺兼容;外部对接请只用第 2、3、4 节列出的接口。