20 KiB
阶段 1-4 实施规格说明
1. 目标
本文件把《方案A-订单编排与自动兑换设计》收敛成可直接编码的规格,覆盖:
- SQLite 表结构 SQL
- 后端 API 契约
- 前台
/claim/:token页面接口契约 - 后台
/admin页面结构与接口契约
默认约束:
- 后端仍使用 Node.js + Express
- 浏览器自动化仍复用现有
src/services/session.js - Python 子服务继续使用
uv - 第一版数据库使用 SQLite
2. SQLite 表结构
建议新建目录:
src/db/src/db/migrations/
建议首个 migration 文件:
src/db/migrations/001_init.sql
以下 SQL 为第一版建议结构。
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL,
platform_order_id TEXT NOT NULL,
order_status TEXT NOT NULL DEFAULT 'created',
pay_status TEXT NOT NULL DEFAULT 'unpaid',
buyer_id TEXT NOT NULL DEFAULT '',
buyer_name TEXT NOT NULL DEFAULT '',
receiver_contact TEXT NOT NULL DEFAULT '',
total_amount INTEGER NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'CNY',
raw_payload_json TEXT NOT NULL DEFAULT '{}',
paid_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(platform, platform_order_id)
);
CREATE TABLE IF NOT EXISTS order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
sku_code TEXT NOT NULL,
sku_name TEXT NOT NULL DEFAULT '',
quantity INTEGER NOT NULL DEFAULT 1,
spec_json TEXT NOT NULL DEFAULT '{}',
delivery_mode TEXT NOT NULL DEFAULT 'claim_link',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id);
CREATE INDEX IF NOT EXISTS idx_order_items_sku_code ON order_items(sku_code);
CREATE TABLE IF NOT EXISTS cdk_inventory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_no TEXT NOT NULL DEFAULT '',
sku_code TEXT NOT NULL,
cdk_code TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'available',
reserved_by_task_id INTEGER,
invalid_reason TEXT NOT NULL DEFAULT '',
delivered_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(sku_code, cdk_code)
);
CREATE INDEX IF NOT EXISTS idx_cdk_inventory_sku_code ON cdk_inventory(sku_code);
CREATE INDEX IF NOT EXISTS idx_cdk_inventory_status ON cdk_inventory(status);
CREATE INDEX IF NOT EXISTS idx_cdk_inventory_reserved_task ON cdk_inventory(reserved_by_task_id);
CREATE TABLE IF NOT EXISTS delivery_tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
order_item_id INTEGER NOT NULL,
platform_order_id TEXT NOT NULL,
task_no TEXT NOT NULL,
task_status TEXT NOT NULL DEFAULT 'pending_payment',
login_type TEXT NOT NULL DEFAULT '',
claim_token_id INTEGER,
reserved_cdk_id INTEGER,
browser_session_id TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
role_id TEXT NOT NULL DEFAULT '',
role_name TEXT NOT NULL DEFAULT '',
area TEXT NOT NULL DEFAULT '',
partition_name TEXT NOT NULL DEFAULT '',
result_code TEXT NOT NULL DEFAULT '',
result_message TEXT NOT NULL DEFAULT '',
screenshot_path TEXT NOT NULL DEFAULT '',
artifacts_json TEXT NOT NULL DEFAULT '{}',
last_error TEXT NOT NULL DEFAULT '',
retry_count INTEGER NOT NULL DEFAULT 0,
expires_at TEXT,
claimed_at TEXT,
role_confirmed_at TEXT,
redeemed_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY(order_item_id) REFERENCES order_items(id) ON DELETE CASCADE,
FOREIGN KEY(reserved_cdk_id) REFERENCES cdk_inventory(id) ON DELETE SET NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_delivery_tasks_task_no ON delivery_tasks(task_no);
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_order_id ON delivery_tasks(order_id);
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_order_item_id ON delivery_tasks(order_item_id);
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_status ON delivery_tasks(task_status);
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_platform_order_id ON delivery_tasks(platform_order_id);
CREATE TABLE IF NOT EXISTS claim_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
token TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
expired_at TEXT NOT NULL,
used_at TEXT,
max_use_count INTEGER NOT NULL DEFAULT 1,
used_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(task_id) REFERENCES delivery_tasks(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_claim_tokens_token ON claim_tokens(token);
CREATE INDEX IF NOT EXISTS idx_claim_tokens_task_id ON claim_tokens(task_id);
CREATE INDEX IF NOT EXISTS idx_claim_tokens_status ON claim_tokens(status);
CREATE TABLE IF NOT EXISTS webhook_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL,
event_type TEXT NOT NULL,
event_key TEXT NOT NULL,
signature_valid INTEGER NOT NULL DEFAULT 0,
headers_json TEXT NOT NULL DEFAULT '{}',
query_json TEXT NOT NULL DEFAULT '{}',
body_json TEXT NOT NULL DEFAULT '{}',
processed INTEGER NOT NULL DEFAULT 0,
process_error TEXT NOT NULL DEFAULT '',
related_order_id INTEGER,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_webhook_events_platform_key ON webhook_events(platform, event_key);
CREATE INDEX IF NOT EXISTS idx_webhook_events_processed ON webhook_events(processed);
CREATE INDEX IF NOT EXISTS idx_webhook_events_created_at ON webhook_events(created_at);
3. 枚举建议
3.1 order_status
createdpaidclosedrefundingrefunded
3.2 pay_status
unpaidpaidfailedrefunded
3.3 task_status
pending_paymentpaidwaiting_inventorycdk_reservedlink_generatedclaimedrole_confirmedredeemingredeemedretry_pendingmanual_reviewexpiredclosed
3.4 cdk_inventory.status
availablereserveddeliveredinvalid
3.5 claim_tokens.status
activeusedexpiredrevoked
4. 目录改造建议
建议后端增加以下文件:
src/
db/
client.js
migrate.js
migrations/
001_init.sql
repositories/
order-repo.js
order-item-repo.js
task-repo.js
cdk-repo.js
claim-token-repo.js
webhook-event-repo.js
routes/
webhooks.js
claims.js
admin.js
services/
webhook-service.js
order-service.js
delivery-task-service.js
cdk-service.js
claim-service.js
redeem-executor-service.js
admin-service.js
utils/
time.js
random.js
pagination.js
5. 后端 API 契约
统一响应建议:
{
"code": 0,
"msg": "ok",
"data": {}
}
失败响应建议:
{
"code": 40001,
"msg": "token 无效",
"data": null
}
6. webhook 接口
6.1 接收订单/支付通知
POST /api/v1/webhooks/agiso/trade
说明:
- 接收平台订单创建成功或支付成功通知
- query 中可能带
timestamp、sign、aopic - body 中可能带
json - 具体字段以平台最终联调为准,但内部处理应统一为标准事件结构
处理步骤:
- 保存原始请求
- 验签
- 解析出平台订单号、事件类型、订单数据
- 按
platform + platform_order_id + event_type做幂等 - 更新订单与交付任务
成功响应:
{
"code": 0,
"msg": "success",
"data": {
"accepted": true,
"platformOrderId": "592823138",
"eventType": "payment_success"
}
}
7. 前台领取链路接口
7.1 校验领取链接
GET /api/v1/claim/:token
用途:
- 页面首次打开时获取任务和订单摘要
- 校验 token 是否有效
响应示例:
{
"code": 0,
"msg": "ok",
"data": {
"tokenStatus": "active",
"task": {
"taskId": 12,
"taskNo": "DT202604070001",
"status": "link_generated",
"expiresAt": "2026-04-08T12:00:00.000Z",
"claimedAt": null,
"roleConfirmedAt": null,
"redeemedAt": null,
"lastError": ""
},
"order": {
"platformOrderId": "592823138",
"skuCode": "dnf-cdk-a",
"skuName": "DNF 活动礼包",
"quantity": 1
},
"session": null,
"result": null
}
}
失败场景:
- token 不存在
- token 已过期
- token 已撤销
- task 已关闭
7.2 创建领取会话
POST /api/v1/claim/:token/session
请求体:
{
"loginType": "qq"
}
说明:
- 创建腾讯浏览器 session
- 将 session 与 task 绑定
- task 状态推进到
claimed
成功响应:
{
"code": 0,
"msg": "浏览器会话已创建",
"data": {
"task": {
"taskId": 12,
"taskNo": "DT202604070001",
"status": "claimed"
},
"session": {
"sessionId": "txbs-ab12cd34ef56",
"status": "waiting_scan",
"loginType": "qq",
"notice": "请使用QQ扫描二维码",
"qrImageBase64": "base64...",
"updatedAt": "2026-04-07T12:00:00.000Z",
"activityInfo": {
"nickname": "",
"role": {
"ready": false,
"roleName": "",
"roleId": ""
}
}
}
}
}
7.3 轮询领取会话状态
GET /api/v1/claim/:token/session/summary
说明:
- 轮询 task 绑定的 session
- 同时返回 task 最新状态
成功响应:
{
"code": 0,
"msg": "ok",
"data": {
"task": {
"taskId": 12,
"taskNo": "DT202604070001",
"status": "claimed",
"lastError": ""
},
"session": {
"sessionId": "txbs-ab12cd34ef56",
"status": "ready_to_redeem",
"loginType": "qq",
"notice": "登录已完成,当前角色 阿修罗",
"updatedAt": "2026-04-07T12:03:00.000Z",
"activityInfo": {
"nickname": "xxx",
"role": {
"ready": true,
"roleName": "阿修罗",
"roleId": "123456",
"area": "36",
"partition": "跨五"
}
},
"artifacts": {
"hasScreenshot": false
}
}
}
}
7.4 确认角色
POST /api/v1/claim/:token/confirm-role
请求体:
{
"confirm": true
}
说明:
- 只有当后端已识别到角色时才能确认
- task 状态推进到
role_confirmed
成功响应:
{
"code": 0,
"msg": "角色已确认",
"data": {
"task": {
"taskId": 12,
"taskNo": "DT202604070001",
"status": "role_confirmed",
"roleConfirmedAt": "2026-04-07T12:05:00.000Z"
}
}
}
7.5 执行兑换
POST /api/v1/claim/:token/redeem
请求体:
{}
说明:
- 不再由前端传 CDK
- 后端从
reserved_cdk_id读取对应兑换码 - 执行成功后更新 task、cdk、截图路径
成功响应:
{
"code": 0,
"msg": "兑换完成",
"data": {
"task": {
"taskId": 12,
"taskNo": "DT202604070001",
"status": "redeemed",
"resultCode": "0",
"resultMessage": "兑换成功",
"redeemedAt": "2026-04-07T12:06:00.000Z",
"screenshotReady": true
},
"session": {
"sessionId": "txbs-ab12cd34ef56",
"status": "redeemed",
"notice": "兑换成功",
"artifacts": {
"hasScreenshot": true
}
}
}
}
失败响应建议:
409当前状态不允许兑换400未确认角色409未预占 CDK500自动化执行失败
7.6 获取结果截图
GET /api/v1/claim/:token/screenshot
说明:
- 返回图片流
- 内部根据 task 取
screenshot_path
8. 后台接口契约
8.1 登录
第一版可以先做简单登录:
POST /api/v1/admin/auth/login
请求体:
{
"username": "admin",
"password": "******"
}
响应:
{
"code": 0,
"msg": "ok",
"data": {
"token": "admin-session-token",
"role": "admin"
}
}
8.2 仪表盘
GET /api/v1/admin/dashboard/summary
响应示例:
{
"code": 0,
"msg": "ok",
"data": {
"todayOrders": 23,
"paidPendingClaim": 7,
"claimingTasks": 5,
"redeemedToday": 14,
"abnormalTasks": 2,
"lowInventorySkuCount": 1
}
}
8.3 订单列表
GET /api/v1/admin/orders
查询参数建议:
pagepageSizeplatformOrderIdpayStatusskuCodedateFromdateTo
响应示例:
{
"code": 0,
"msg": "ok",
"data": {
"items": [
{
"orderId": 1,
"platform": "agiso",
"platformOrderId": "592823138",
"orderStatus": "paid",
"payStatus": "paid",
"buyerName": "",
"totalAmount": 300,
"createdAt": "2026-04-07T11:00:00.000Z",
"updatedAt": "2026-04-07T11:02:00.000Z",
"taskCount": 1
}
],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 1
}
}
}
8.4 订单详情
GET /api/v1/admin/orders/:orderId
返回:
- 订单主信息
- 子项信息
- 对应任务列表
- webhook 摘要
8.5 任务列表
GET /api/v1/admin/tasks
查询参数建议:
pagepageSizestatusplatformOrderIdtaskNoskuCoderoleIddateFromdateTo
响应示例:
{
"code": 0,
"msg": "ok",
"data": {
"items": [
{
"taskId": 12,
"taskNo": "DT202604070001",
"platformOrderId": "592823138",
"skuCode": "dnf-cdk-a",
"status": "retry_pending",
"reservedCdkCodeMasked": "ABCD****WXYZ",
"loginType": "qq",
"roleName": "阿修罗",
"roleId": "123456",
"browserSessionId": "txbs-ab12cd34ef56",
"retryCount": 1,
"lastError": "OCR 识别失败",
"createdAt": "2026-04-07T11:00:00.000Z",
"updatedAt": "2026-04-07T11:06:00.000Z"
}
],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 1
}
}
}
8.6 任务详情
GET /api/v1/admin/tasks/:taskId
返回内容建议:
- 任务基础信息
- 订单摘要
- token 信息
- 预占 CDK 信息
- 浏览器 session 摘要
- 角色信息
- 兑换结果
- 截图 URL
- 操作按钮可用状态
8.7 重试任务
POST /api/v1/admin/tasks/:taskId/retry
用途:
- 对
retry_pending或manual_review的任务发起重试
请求体:
{
"reason": "manual retry"
}
8.8 重新生成领取链接
POST /api/v1/admin/tasks/:taskId/regenerate-claim-link
说明:
- 作废旧 token
- 创建新 token
响应示例:
{
"code": 0,
"msg": "领取链接已重新生成",
"data": {
"taskId": 12,
"claimUrl": "https://your-domain.com/#/claim/xxxxxxxx"
}
}
8.9 释放 CDK
POST /api/v1/admin/tasks/:taskId/release-cdk
说明:
- 将任务绑定的
reservedCDK 释放回库存 - 一般只允许未成功兑换任务执行
8.10 关闭任务
POST /api/v1/admin/tasks/:taskId/close
用途:
- 人工确认无需继续执行
8.11 CDK 列表
GET /api/v1/admin/cdks
查询参数建议:
pagepageSizeskuCodestatusbatchNo
8.12 单条新增 CDK
POST /api/v1/admin/cdks
请求体:
{
"skuCode": "dnf-cdk-a",
"batchNo": "20260407",
"cdkCode": "ABCDEFGH12345678"
}
8.13 批量导入 CDK
POST /api/v1/admin/cdks/import
请求体建议支持两种形式:
形式一:
{
"skuCode": "dnf-cdk-a",
"batchNo": "20260407",
"codes": [
"AAAA-BBBB-CCCC",
"DDDD-EEEE-FFFF"
]
}
形式二:
{
"rows": [
{
"skuCode": "dnf-cdk-a",
"batchNo": "20260407",
"cdkCode": "AAAA-BBBB-CCCC"
},
{
"skuCode": "dnf-cdk-b",
"batchNo": "20260407",
"cdkCode": "DDDD-EEEE-FFFF"
}
]
}
响应示例:
{
"code": 0,
"msg": "导入完成",
"data": {
"total": 10,
"created": 8,
"duplicated": 2,
"invalid": 0
}
}
8.14 webhook 列表
GET /api/v1/admin/webhook-events
8.15 webhook 详情
GET /api/v1/admin/webhook-events/:eventId
8.16 webhook 重放
POST /api/v1/admin/webhook-events/:eventId/replay
9. 前台 /claim/:token 页面契约
9.1 路由
建议新增:
/#/claim/:token
9.2 页面状态机
前台页面建议使用以下视图状态:
loadinginvalid_tokenready_to_startwaiting_scanwaiting_confirm_loginwaiting_role_readywaiting_role_confirmredeemingredeemedfailedexpired
9.3 页面数据模型
建议前端整理成:
type ClaimPageModel = {
tokenStatus: 'active' | 'expired' | 'revoked' | 'used'
task: {
taskId: number
taskNo: string
status: string
lastError: string
expiresAt: string | null
claimedAt: string | null
roleConfirmedAt: string | null
redeemedAt: string | null
}
order: {
platformOrderId: string
skuCode: string
skuName: string
quantity: number
}
session: null | {
sessionId: string
status: string
loginType: 'qq' | 'wx'
notice: string
qrImageBase64?: string
updatedAt: string
activityInfo?: {
nickname: string
role: {
ready: boolean
roleName: string
roleId: string
area?: string
partition?: string
}
}
artifacts?: {
hasScreenshot: boolean
}
}
result: null | {
resultCode: string
resultMessage: string
screenshotUrl: string
}
}
9.4 页面动作
前台页面仅保留 4 个核心动作:
- 选择 QQ / WX 并创建 session
- 轮询 session 状态
- 确认角色
- 发起兑换
前台不允许:
- 手输 CDK
- 修改任务状态
- 查看其他订单
9.5 页面展示区块
建议拆成:
- 订单摘要卡片
- 登录二维码卡片
- 角色确认卡片
- 结果卡片
现有可复用组件:
TencentAuthCard.vueTencentRedeemPanel.vueTencentResultPanel.vue
9.6 轮询规则
轮询间隔建议继续沿用当前 2500ms。
停止轮询条件:
- session 进入
redeemed - task 进入
redeemed - task 进入
expired - task 进入
closed - 连续失败达到阈值
10. 后台 /admin 页面契约
10.1 路由建议
/#/admin/login/#/admin/dashboard/#/admin/orders/#/admin/orders/:orderId/#/admin/tasks/#/admin/tasks/:taskId/#/admin/cdks/#/admin/webhook-events/#/admin/webhook-events/:eventId
10.2 页面说明
Dashboard
展示:
- 今日订单数
- 待领取数
- 兑换成功数
- 异常任务数
- 库存告警
Orders
功能:
- 筛选订单
- 查看订单详情
- 跳转任务
Tasks
功能:
- 筛选任务状态
- 查看错误
- 跳转详情
- 快速执行重试、释放、关闭、重发链接
Task Detail
功能:
- 统一查看任务全量信息
- 预览截图
- 人工操作入口
CDKs
功能:
- 单条新增
- 批量导入
- 状态筛选
- 查看预占关系
Webhook Events
功能:
- 查看原始推送
- 查看处理结果
- 重放处理
10.3 后台前端基础能力
后台第一版建议至少补:
- 登录鉴权
- 基础分页
- 列表筛选
- 操作确认弹窗
- 错误提示
11. 实施顺序
11.1 第一批
先做:
- SQLite 接入
- migration
- repository 层
- webhook 接口骨架
- 订单、任务、CDK、token 基础 service
11.2 第二批
再做:
/claim/:token前台页- claim 相关接口
- session 与 task 绑定
11.3 第三批
再做:
- 后端自动兑换桥接
- 兑换结果回写
- 截图读取
11.4 第四批
最后做:
/admin后台页- 订单/任务/CDK/webhook 查询接口
- 重试、释放、重放
12. 建议的下一步
如果直接开始编码,建议按下面顺序落地:
- 先实现
001_init.sql - 实现
src/db/client.js - 实现 repository 层
- 接
POST /api/v1/webhooks/agiso/trade - 打通“支付成功 -> 建订单 -> 建任务 -> 预占 CDK -> 生成 token”
- 再开始前台领取页
这条顺序最稳,返工也最少。