Files
order_site/apps/backend/阶段1-4实施规格说明.md
T
2026-04-08 16:30:42 +08:00

20 KiB

阶段 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 为第一版建议结构。

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. 目录改造建议

建议后端增加以下文件:

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 中可能带 timestampsignaopic
  • body 中可能带 json
  • 具体字段以平台最终联调为准,但内部处理应统一为标准事件结构

处理步骤:

  1. 保存原始请求
  2. 验签
  3. 解析出平台订单号、事件类型、订单数据
  4. platform + platform_order_id + event_type 做幂等
  5. 更新订单与交付任务

成功响应:

{
  "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 未预占 CDK
  • 500 自动化执行失败

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

查询参数建议:

  • page
  • pageSize
  • platformOrderId
  • payStatus
  • skuCode
  • dateFrom
  • dateTo

响应示例:

{
  "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

响应示例:

{
  "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_pendingmanual_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

说明:

  • 将任务绑定的 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

请求体:

{
  "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 页面状态机

前台页面建议使用以下视图状态:

  • loading
  • invalid_token
  • ready_to_start
  • waiting_scan
  • waiting_confirm_login
  • waiting_role_ready
  • waiting_role_confirm
  • redeeming
  • redeemed
  • failed
  • expired

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 个核心动作:

  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. 再开始前台领取页

这条顺序最稳,返工也最少。