You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

12 KiB

视频定价表测试方案

分支:feat/video-pricing-table-codex | 日期:2026-06-18

概述

本文档定义了多维定价表系统的完整测试方案,包括两部分:

  • 手动验证清单:覆盖全部功能点,含 curl 示例和预期结果
  • 自动化测试脚本(Python):覆盖核心 CRUD + 计费路径

测试环境

BASE_URL=http://127.0.0.1:3000   # 或 http://127.0.0.1:23000 (WSL CN)
ADMIN_USERNAME=root
ADMIN_PASSWORD=123456
API_TOKEN=sk-...                  # 任意有效用户 token,配额充足 ≥ 100000

前置条件:服务运行中、测试用户存在、Hailuo/Doubao 渠道可用、QuotaPerUnit = 500000


一、定价配置 CRUD(P01–P12)

编号 测试点 方法 路径 预期
P01 获取空配置列表 GET /api/option/model_pricing 200, {}
P02 创建配置 PUT /api/option/model_pricing/hailuo-video 200, 返回配置
P03 读取单配置 GET /api/option/model_pricing/hailuo-video 200
P04 更新配置 PUT 同上 + 修改后 table 200
P05 删除配置 DELETE /api/option/model_pricing/hailuo-video 200
P06 删除后读取 GET 同上 404
P07 批量列表 GET /api/option/model_pricing 200, 含所有已创建
P08 未认证请求 PUT 不带 Cookie 401/403
P09 错误 Content-Type PUT Content-Type: text/plain 400
P10 非法 JSON PUT 损坏 JSON 400
P11 幂等覆盖 PUT 再次 PUT 已存在 200
P12 删除不存在 DELETE .../nonexistent 200

P02 请求体示例

{
  "schema_version": 1,
  "scope": "model",
  "billing_unit": "per_call",
  "preconsume_strategy": "exact",
  "dimensions": [
    {"key": "resolution", "source": "request.resolution", "type": "string", "default": null},
    {"key": "duration", "source": "request.duration", "type": "number", "default": null}
  ],
  "table": [
    {"resolution": "768P", "duration": 10, "price": 0.04, "source": "manual"}
  ],
  "fallback": {"strategy": "reject"}
}

二、配置校验规则(V01–V17)

编号 校验规则 触发方式 预期
V01 schema_version ≠ 1 "schema_version": 2 400
V02 scope ≠ model "scope": "channel" 400
V03 dimensions 为空 "dimensions": [] 400
V04 table 为空 "table": [] 400
V05 billing_unit 非法 "billing_unit": "per_hour" 400
V06 preconsume_strategy 非法 "preconsume_strategy": "average" 400
V07 per_call + minimum 冲突 billing_unit=per_call, strategy=minimum 400
V08 per_1m_tokens + exact 冲突 billing_unit=per_1m_tokens, strategy=exact 400
V09 不支持 matrix usage 模型 hailuo-video + per_1m_tokens 400
V10 dimension key 保留字 key="price” 400
V11 dimension key 非法格式 key="reso lution” 400
V12 dimension type 非法 type="integer” 400
V13 dimension source 非法 source="header.X-Custom” 400
V14 row 缺少维度 table 行不含某维度值 400
V15 row price 负数 "price": -1 400
V16 模糊冲突行 相同 wildcard count 的行重叠 400
V17 optional 缺 default optional=true + default=null 400

三、维度解析(D01–D06)

编号 测试点 说明
D01 JSON 请求体浅层提取 {"resolution": "1080P"} → 字符串
D02 JSON 嵌套提取 request.options.size 深层路径
D03 Multipart 提取 Content-Type: multipart/form-data
D04 Optional 默认值 请求不传 → 用 default
D05 缺失必填维度 该维度为 nil,不匹配任何行
D06 Derived 维度 megapixels/aspect_ratio/input_mode 自动计算

四、定价匹配 + 回退策略(L01–L08)

编号 场景 预期
L01 精确匹配 维度 = 行值 → 返回该行价格
L02 Wildcard 匹配 * 匹配任意值
L03 最小 wildcard 优先 多行匹配选 wildcard 最少
L04 无匹配 → reject 400 + 不扣 quota
L05 无匹配 → max 取表中最高价
L06 无匹配 → default 取 default_price
L07 歧义行配置拒绝 V16
L08 per_1m_tokens 预扣 minimum 策略预扣

五、Task 计费生命周期(B01–B08)

编号 场景 说明
B01 提交 + 预扣 PreConsume → DB quota 变化
B02 成功 + 结算 per_call 精确不补扣
B03 失败 + 退款 Refund 退还
B04 Adaptor 调整差额 AdjustBillingOnSubmit → 差额
B05 免费模型 price=0 → 不预扣
B06 分组倍率 groupRatio≠1 → quota 按倍率
B07 BillingContext 持久化 DB 记录完整
B08 消费日志 log 表字段正确

六、Matrix Usage 计费(M01–M06)

编号 场景 说明
M01 预扣最小值 ≈ PreConsumedQuota × tokenUnitPrice × ratio / 1M
M02 Token 结算 上游返回 total_tokens → 按实际计算
M03 补扣差额 实际 > 预扣 → 补扣
M04 退还差额 实际 < 预扣 → 退还
M05 不支持模型被拒 hailuo-video + per_1m_tokens → 400
M06 渠道类型不匹配 非 DoubaoVideo → 不支持

七、Remix 计费(R01–R05)

编号 场景 说明
R01 BillingContext 继承 复用原任务定价快照
R02 旧格式兼容 从 seconds/size 解析
R03 锁渠道 必须用原渠道
R04 原任务不存在 400
R05 Matrix remix 按相同方式计费

八、渠道选择(C01–C04)

编号 场景 说明
C01 Matrix 渠道路由 只选匹配渠道
C02 无可用渠道 错误
C03 普通模型不限渠道 per_call 不需 matrix
C04 模型映射后匹配 上游映射后仍正确

九、实际计费请求(E01–E22)

Per Call 计费(E01–E08)

编号 场景 预期
E01 精确匹配 768P/10s=0.04 quota 扣 20000
E02 提交前后 quota 差 差值 = price × QuotaPerUnit × groupRatio
E03 Wildcard 匹配 * 匹配任意同维度值
E04 不匹配 → reject quota 不变 + 400
E05 Fallback=max 扣最高价
E06 Fallback=default 扣 default_price × 500000
E07 三维度匹配 resolution × duration × input_mode
E08 Optional 默认值 不传 → 自动补 default → 匹配

Per 1M Tokens 计费(E09–E14,需 mock 上游)

编号 场景 说明
E09 预扣最小值 预扣 ≈ 1–2 quota
E10 按 tokens 结算 tokens × price × ratio / 1M
E11 补扣差额 tokens 多 → 补扣
E12 退还差额 tokens 少 → 退还
E13 定价表 + usage 维度匹配得单价 → × total_tokens
E14 预扣量对比 per_1m_tokens vs per_call 预扣差

Remix 计费完整性(E15–E18)

编号 场景 说明
E15 Remix per_call 扣费与原任务相同
E16 Remix per_1m_tokens 相同 token 单价
E17 Remix 旧格式 seconds/size 推断
E18 Remix 渠道不可用 400

边界场景(E19–E22)

编号 场景 说明
E19 定价配置优先于旧价格 matrix 优先
E20 无定价配置走旧逻辑 legacy path
E21 Quota 不足 提交失败
E22 GroupRatio 非 1 quota 按倍率

自动化脚本范围

test-scripts/test_model_pricing_api.py,预估覆盖约 33 个用例:

  • CRUD:P01–P12(约 8 个)
  • 校验规则:V01–V17(约 12 个)
  • Per Call 计费:E01–E08(约 6 个)
  • Per 1M Tokens:E09–E14(约 4 个,需 mock 上游)

十、Mock 上游服务器

由于本地/测试环境没有真实的 Doubao 和 Hailuo 视频生成 API,需要一个 mock server 来模拟上游行为。

为什么需要 mock

测试类别 依赖上游做什么 没有 mock 的后果
Per Call 计费(E01-E08) 接受提交 → 后续 polling 返回 succeeded 任务一直 pending,永远不结算
Per 1M Tokens(E09-E14) 提交 → polling 返回 succeeded + usage.total_tokens 无法验证按 token 结算逻辑
Remix(E15-E18) 基于已完成任务再次提交 → polling 返回 succeeded 无法验证 remix 计费链路

Mock 需要模拟的 API

Hailuo(Minimax)API

提交任务POST /v1/video/generation

// Request (new-api 转发)
{"model": "hailuo-video", "prompt": "...", "resolution": "768P", "duration": 10}

// Response
{"data": {"task_id": "mock_hailuo_xxx"}, "base_resp": {"status_code": 0, "status_msg": "success"}}

查询任务GET /v1/query/video_result?task_id={id}

// Response (succeeded)
{"data": {
  "status": "Success",
  "video_url": "https://mock.local/video.mp4"
}, "base_resp": {"status_code": 0}}

Doubao(Seedance)API

提交任务POST /api/v3/contents/generations/tasks

// Request (new-api 转发)
{"model": "seedance-2", "content": [{"type": "text", "text": "..."}], "resolution": "...", "duration": 5}

// Response
{"id": "mock_doubao_xxx"}

查询任务GET /api/v3/contents/generations/tasks/{id}

// Response (succeeded + usage)
{
  "id": "mock_doubao_xxx",
  "model": "seedance-2",
  "status": "succeeded",
  "content": {"video_url": "https://mock.local/video.mp4"},
  "usage": {"completion_tokens": 10000, "total_tokens": 10000}
}

Mock 核心设计

关键思路: mock 将请求中的信息存储起来,在 polling 时原样返回给 new-api。total_tokens 通过请求体中的特殊字段控制。

状态机:

submit → "queued" (首次 GET)
       → "processing" (第二次 GET)
       → "succeeded" (第三次 GET,返回 usage)

控制 total_tokens 的方式: 在提交请求中包含 _mock_total_tokens 字段,mock 提取后不传给上游逻辑,只在 fetch 时返回该值作为 usage.total_tokens

Python 实现框架:

# mock_server.py - 使用 Python stdlib http.server,无外部依赖
# 用法: python test-scripts/mock_video_upstream.py --port 18999

import json
from http.server import HTTPServer, BaseHTTPRequestHandler

class MockHandler(BaseHTTPRequestHandler):
    tasks = {}  # task_id → {request, poll_count, total_tokens}

    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
        task_id = f"mock_{len(self.tasks):04d}"
        # 提取 _mock_total_tokens 并移除
        total_tokens = body.pop("_mock_total_tokens", None)
        self.tasks[task_id] = {
            "request": body,
            "poll_count": 0,
            "total_tokens": total_tokens or 10000,
        }
        # 根据 path 返回 Hailuo 或 Doubao 格式
        ...

    def do_GET(self):
        # 解析 task_id,增加 poll_count
        # poll_count 0: queued, 1: processing, 2+: succeeded
        ...

测试流程(使用 mock)

1. 启动 mock server:   python test-scripts/mock_video_upstream.py --port 18999
2. 在 new-api 中创建指向 mock 的渠道:
   - Hailuo 渠道: base_url = http://host.docker.internal:18999
   - Doubao 渠道: base_url = http://host.docker.internal:18999, type = DoubaoVideo
3. 运行测试脚本:     python test-scripts/test_model_pricing_api.py
4. 停止 mock server:  Ctrl+C

Mock Server 配置端点

端点 方法 用途
/mock/reset POST 清空所有任务状态
/mock/stats GET 查看已完成/进行中的任务数
/mock/tasks GET 列出所有任务及其状态
  • Remix:E15–E18(约 3 个)