1112 lines
20 KiB
Markdown
1112 lines
20 KiB
Markdown
# 阶段 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. 再开始前台领取页
|
|
|
|
这条顺序最稳,返工也最少。
|