# 阶段 1-4 实施规格说明 ## 1. 目标 本文件把《方案A-订单编排与自动兑换设计》收敛成可直接编码的规格,覆盖: 1. SQLite 表结构 SQL 2. 后端 API 契约 3. 前台 `/claim/:token` 页面接口契约 4. 后台 `/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 为第一版建议结构。 ```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` - `created` - `paid` - `closed` - `refunding` - `refunded` ### 3.2 `pay_status` - `unpaid` - `paid` - `failed` - `refunded` ### 3.3 `task_status` - `pending_payment` - `paid` - `waiting_inventory` - `cdk_reserved` - `link_generated` - `claimed` - `role_confirmed` - `redeeming` - `redeemed` - `retry_pending` - `manual_review` - `expired` - `closed` ### 3.4 `cdk_inventory.status` - `available` - `reserved` - `delivered` - `invalid` ### 3.5 `claim_tokens.status` - `active` - `used` - `expired` - `revoked` ## 4. 目录改造建议 建议后端增加以下文件: ```text 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 契约 统一响应建议: ```json { "code": 0, "msg": "ok", "data": {} } ``` 失败响应建议: ```json { "code": 40001, "msg": "token 无效", "data": null } ``` --- ## 6. webhook 接口 ### 6.1 接收订单/支付通知 `POST /api/v1/webhooks/agiso/trade` 说明: - 接收平台订单创建成功或支付成功通知 - query 中可能带 `timestamp`、`sign`、`aopic` - body 中可能带 `json` - 具体字段以平台最终联调为准,但内部处理应统一为标准事件结构 处理步骤: 1. 保存原始请求 2. 验签 3. 解析出平台订单号、事件类型、订单数据 4. 按 `platform + platform_order_id + event_type` 做幂等 5. 更新订单与交付任务 成功响应: ```json { "code": 0, "msg": "success", "data": { "accepted": true, "platformOrderId": "592823138", "eventType": "payment_success" } } ``` --- ## 7. 前台领取链路接口 ### 7.1 校验领取链接 `GET /api/v1/claim/:token` 用途: - 页面首次打开时获取任务和订单摘要 - 校验 token 是否有效 响应示例: ```json { "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` 请求体: ```json { "loginType": "qq" } ``` 说明: - 创建腾讯浏览器 session - 将 session 与 task 绑定 - task 状态推进到 `claimed` 成功响应: ```json { "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 最新状态 成功响应: ```json { "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` 请求体: ```json { "confirm": true } ``` 说明: - 只有当后端已识别到角色时才能确认 - task 状态推进到 `role_confirmed` 成功响应: ```json { "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` 请求体: ```json {} ``` 说明: - 不再由前端传 CDK - 后端从 `reserved_cdk_id` 读取对应兑换码 - 执行成功后更新 task、cdk、截图路径 成功响应: ```json { "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` 未预占 CDK - `500` 自动化执行失败 ### 7.6 获取结果截图 `GET /api/v1/claim/:token/screenshot` 说明: - 返回图片流 - 内部根据 task 取 `screenshot_path` --- ## 8. 后台接口契约 ### 8.1 登录 第一版可以先做简单登录: `POST /api/v1/admin/auth/login` 请求体: ```json { "username": "admin", "password": "******" } ``` 响应: ```json { "code": 0, "msg": "ok", "data": { "token": "admin-session-token", "role": "admin" } } ``` ### 8.2 仪表盘 `GET /api/v1/admin/dashboard/summary` 响应示例: ```json { "code": 0, "msg": "ok", "data": { "todayOrders": 23, "paidPendingClaim": 7, "claimingTasks": 5, "redeemedToday": 14, "abnormalTasks": 2, "lowInventorySkuCount": 1 } } ``` ### 8.3 订单列表 `GET /api/v1/admin/orders` 查询参数建议: - `page` - `pageSize` - `platformOrderId` - `payStatus` - `skuCode` - `dateFrom` - `dateTo` 响应示例: ```json { "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` 查询参数建议: - `page` - `pageSize` - `status` - `platformOrderId` - `taskNo` - `skuCode` - `roleId` - `dateFrom` - `dateTo` 响应示例: ```json { "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` 的任务发起重试 请求体: ```json { "reason": "manual retry" } ``` ### 8.8 重新生成领取链接 `POST /api/v1/admin/tasks/:taskId/regenerate-claim-link` 说明: - 作废旧 token - 创建新 token 响应示例: ```json { "code": 0, "msg": "领取链接已重新生成", "data": { "taskId": 12, "claimUrl": "https://your-domain.com/#/claim/xxxxxxxx" } } ``` ### 8.9 释放 CDK `POST /api/v1/admin/tasks/:taskId/release-cdk` 说明: - 将任务绑定的 `reserved` CDK 释放回库存 - 一般只允许未成功兑换任务执行 ### 8.10 关闭任务 `POST /api/v1/admin/tasks/:taskId/close` 用途: - 人工确认无需继续执行 ### 8.11 CDK 列表 `GET /api/v1/admin/cdks` 查询参数建议: - `page` - `pageSize` - `skuCode` - `status` - `batchNo` ### 8.12 单条新增 CDK `POST /api/v1/admin/cdks` 请求体: ```json { "skuCode": "dnf-cdk-a", "batchNo": "20260407", "cdkCode": "ABCDEFGH12345678" } ``` ### 8.13 批量导入 CDK `POST /api/v1/admin/cdks/import` 请求体建议支持两种形式: 形式一: ```json { "skuCode": "dnf-cdk-a", "batchNo": "20260407", "codes": [ "AAAA-BBBB-CCCC", "DDDD-EEEE-FFFF" ] } ``` 形式二: ```json { "rows": [ { "skuCode": "dnf-cdk-a", "batchNo": "20260407", "cdkCode": "AAAA-BBBB-CCCC" }, { "skuCode": "dnf-cdk-b", "batchNo": "20260407", "cdkCode": "DDDD-EEEE-FFFF" } ] } ``` 响应示例: ```json { "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 页面状态机 前台页面建议使用以下视图状态: - `loading` - `invalid_token` - `ready_to_start` - `waiting_scan` - `waiting_confirm_login` - `waiting_role_ready` - `waiting_role_confirm` - `redeeming` - `redeemed` - `failed` - `expired` ### 9.3 页面数据模型 建议前端整理成: ```ts 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 个核心动作: 1. 选择 QQ / WX 并创建 session 2. 轮询 session 状态 3. 确认角色 4. 发起兑换 前台不允许: - 手输 CDK - 修改任务状态 - 查看其他订单 ### 9.5 页面展示区块 建议拆成: 1. 订单摘要卡片 2. 登录二维码卡片 3. 角色确认卡片 4. 结果卡片 现有可复用组件: - `TencentAuthCard.vue` - `TencentRedeemPanel.vue` - `TencentResultPanel.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 第一批 先做: 1. SQLite 接入 2. migration 3. repository 层 4. webhook 接口骨架 5. 订单、任务、CDK、token 基础 service ### 11.2 第二批 再做: 1. `/claim/:token` 前台页 2. claim 相关接口 3. session 与 task 绑定 ### 11.3 第三批 再做: 1. 后端自动兑换桥接 2. 兑换结果回写 3. 截图读取 ### 11.4 第四批 最后做: 1. `/admin` 后台页 2. 订单/任务/CDK/webhook 查询接口 3. 重试、释放、重放 ## 12. 建议的下一步 如果直接开始编码,建议按下面顺序落地: 1. 先实现 `001_init.sql` 2. 实现 `src/db/client.js` 3. 实现 repository 层 4. 接 `POST /api/v1/webhooks/agiso/trade` 5. 打通“支付成功 -> 建订单 -> 建任务 -> 预占 CDK -> 生成 token” 6. 再开始前台领取页 这条顺序最稳,返工也最少。