API 集成文档
版本 v1.0 | 更新日期:2026年8月4日
1. 概述
Norai API 允许您以程序化方式提交算法计算任务(车床分配、PCB拼版、布料排版、交易拟合), 查询任务状态并导出结果。所有 API 均为 RESTful 风格,使用 JSON 格式通信。
基础 URL:https://norai.cn/api
2. 鉴权
2.1 获取 API Key
- 登录 Norai 平台 → 工作空间 → 设置 → API 密钥
- 点击「创建密钥」,填写名称并选择权限范围
- 复制生成的密钥(仅显示一次,请妥善保管)
注意:API Key 功能要求 Standard 或更高套餐。
2.2 使用方式
在每次 API 请求的 HTTP header 中携带 Key:
Authorization: Bearer sk_live_xxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2.3 权限范围 (Scopes)
| Scope | 说明 | 涉及接口 |
|---|---|---|
| jobs:read | 查询任务列表和详情 | GET /api/jobs, GET /api/jobs/:id |
| jobs:write | 创建、取消、中止任务 | POST /api/jobs, DELETE /api/jobs/:id |
| upload | 上传数据文件 | POST /api/upload |
| export | 导出计算结果 | GET /api/lathe/export/:id 等 |
| lathe:parse | 解析车床订单数据 | POST /api/lathe/parse-upload |
| pcb:parse | 解析 PCB 订单数据 | POST /api/pcb/parse |
| transaction:parse | 解析交易流水数据 | POST /api/transaction/parse-upload |
3. 通用约定
请求格式
- JSON 请求:
Content-Type: application/json - 文件上传:
Content-Type: multipart/form-data - 字符编码:UTF-8
响应格式
{
"success": true,
"data": { ... }
}{
"error": "错误描述信息"
}HTTP 状态码
| 状态码 | 含义 |
|---|---|
| 200 | 请求成功 |
| 201 | 资源创建成功 |
| 400 | 请求参数错误 |
| 401 | 未授权(Key 无效/过期/已吊销) |
| 403 | 权限不足(scope 不匹配/套餐限制) |
| 404 | 资源不存在 |
| 422 | 请求体校验失败 |
| 429 | 请求过于频繁 |
| 500 | 服务器内部错误 |
4. 车床分配 API
车床智能分配是最常用的集成场景。提交任务有两种方式:直接提交 JSON 数据,或先上传 Excel 文件再提交。
两种提交流程对比:
| 直接提交 JSON(推荐) | 上传 Excel 后提交 | |
|---|---|---|
| source | "json" | "upload" |
| 步骤 | 提交 → 轮询 → 获取结果 | 上传 → 提交 → 轮询 → 获取结果 |
| 必需参数 | data + params | tempId + params |
| 适合场景 | 系统集成、数据已结构化 | 数据来源为 Excel 文件 |
4.1 提交任务 — 方式一:直接 JSON(推荐)
适合系统集成场景。将结构化数据直接传入请求,无需上传文件,一步到位。
{
"type": "lathe",
"source": "json",
"data": {
"sizes": ["S", "M", "L", "XL", "2XL"],
"colors": ["白色", "黑色", "蓝色"],
"demand": {
"白色": { "S": 120, "M": 200, "L": 180, "XL": 150, "2XL": 80 },
"黑色": { "S": 100, "M": 180, "L": 160, "XL": 120, "2XL": 60 },
"蓝色": { "S": 80, "M": 150, "L": 140, "XL": 100, "2XL": 50 }
}
},
"params": {
"method": "auto",
"max_layers_per_bed": 150,
"max_pieces_per_layer": 10,
"tolerance_over": 2,
"tolerance_under": 2,
"max_colors_per_bed": 2,
"time_limit": 5
}
}4.2 提交任务 — 方式二:上传 Excel
适合数据来源为 Excel 文件的场景。分两步:先上传文件获取 tempId,再用 tempId 提交任务,服务端自动解析 Excel。
第一步:上传文件
Content-Type: multipart/form-data
Authorization: Bearer sk_live_xxxx_...
FormData:
file: (订单数据.xlsx, ≤200KB)
type: "lathe"{
"success": true,
"data": {
"tempId": "a1b2c3d4-5678-9abc-def0-123456789abc",
"originalName": "订单数据.xlsx"
}
}第二步:提交任务
{
"type": "lathe",
"source": "upload",
"tempId": "a1b2c3d4-5678-9abc-def0-123456789abc",
"params": {
"method": "auto",
"time_limit": 5
}
}注:也可额外传入 data 字段覆盖 Excel 的自动解析结果(如需修正数据),但通常不需要。
可选:解析预览(提交前确认数据)
如需在提交前查看 Excel 解析结果,可调用此接口预览:
{ "tempId": "a1b2c3d4-..." }
→ 响应: { "data": { "sizes": [...], "colors": [...], "demand": {...} } }4.3 参数说明
data 字段(source="json" 时必填):
| 字段 | 类型 | 说明 |
|---|---|---|
| sizes | string[] | 尺码列表,如 ["S", "M", "L", "XL"] |
| colors | string[] | 颜色列表,如 ["白色", "黑色"] |
| demand | object | 需求矩阵: { 颜色: { 尺码: 数量 } } |
params 参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| method | string | "auto" | 算法模式: auto / quality / balanced / fast |
| max_layers_per_bed | int | 150 | 每床最大层数 (1-200) |
| min_layers_per_bed | int | 1 | 每床最小层数 (1-200) |
| max_pieces_per_layer | int | 10 | 每层最大件数 (1-20) |
| min_pieces_per_layer | int | 1 | 每层最小件数 (1-20) |
| max_same_size_per_layer | int | 10 | 同规格最大件数 (1-20) |
| tolerance_over | int | 2 | 允许多裁件数 (0-100) |
| tolerance_under | int | 2 | 允许少裁件数 (0-100) |
| planned_beds | int | null | null | 目标床数(留空自动优化) |
| max_colors_per_bed | int | 2 | 每床最多混铺颜色数 (1-10) |
| time_limit | int | 5 | 最大求解时间,分钟(受套餐上限约束) |
算法模式说明:
| 模式 | 适用场景 |
|---|---|
| auto | 系统根据订单规模自动选择最优策略 |
| quality | 规模较小、追求最少床数 |
| balanced | 兼顾速度与方案质量 |
| fast | 数据量大或时间紧迫时快速出方案 |
{
"success": true,
"data": {
"id": 88,
"jobNumber": "A3K7-LA-260804-0001",
"status": "pending"
}
}4.4 轮询任务状态
// 运行中
{
"success": true,
"data": {
"id": 88,
"status": "running",
"progress": {
"iteration": 5,
"best_beds": 12,
"elapsed_sec": 45.2,
"status": "improving"
}
}
}
// 成功
{
"success": true,
"data": {
"id": 88,
"status": "success",
"result": {
"total_beds": 12,
"status": "optimal",
"run_time_sec": 193.5,
"beds": [
{
"bed_id": 1,
"pattern": { "M": 4, "L": 3, "XL": 3 },
"layers": { "白色": 20, "黑色": 15 },
"pieces_per_layer": 10
}
],
"deviation": [
{ "color": "白色", "size": "S", "demand": 120, "produced": 122, "diff": 2 }
]
}
}
}| status | 含义 |
|---|---|
| pending | 排队等待执行 |
| running | 正在求解 |
| success | 求解成功 |
| failure | 求解失败(查看 errorMessage) |
| canceled | 已取消/已中止 |
4.5 获取结果
任务成功后,有两种方式获取结果:
方式一:JSON 格式(通过任务详情接口)
当任务 status 为 success 时,GET /api/jobs/:id 响应中的data.result 字段即为完整结果 JSON,可直接用于程序化处理。
{
"total_beds": 12,
"status": "optimal",
"run_time_sec": 193.5,
"beds": [
{
"bed_id": 1,
"pattern": { "M": 4, "L": 3, "XL": 3 },
"layers": { "白色": 20, "黑色": 15 },
"pieces_per_layer": 10
}
],
"deviation": [
{ "color": "白色", "size": "S", "demand": 120, "produced": 122, "diff": 2 }
]
}方式二:Excel 文件
响应: .xlsx 文件二进制流
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Excel 包含三个 Sheet:
- 分床方案: 每床颜色、各尺码件数、层数、总件数
- 偏差分析: 各颜色尺码的需求 vs 实际产出偏差
- 汇总: 总床数、方案状态、求解时间5. PCB 拼版 API
5.1 上传并解析
FormData: file (.xlsx, ≤200KB), type = "pcb"{ "source": "upload", "tempId": "uuid-from-upload" }5.2 提交拼版任务
{
"type": "pcb",
"source": "upload",
"tempId": "uuid-from-upload",
"originalName": "PCB订单.xlsx",
"params": {
"component_spacing": 1.6,
"margin": 7.0,
"min_orders_per_panel": 3,
"max_extra_qty": 1,
"max_extra_ratio": 0.1,
"over_production_ratio": 0.03,
"panel_sizes": "600 500",
"time_minutes": 5
}
}| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| component_spacing | float | 1.6 | 元件间距 (mm) |
| margin | float | 7.0 | 板边距 (mm) |
| min_orders_per_panel | int | 3 | 每版最少订单数 |
| max_extra_qty | int | 1 | 最大多拼数量 |
| max_extra_ratio | float | 0.1 | 最大多拼比例 |
| over_production_ratio | float | 0.03 | 超产比例 |
| panel_sizes | string | "600 500" | 拼版尺寸 (空格分隔, mm) |
| time_minutes | int | 5 | 求解时间 (分钟) |
6. 布料排版 API
6.1 上传 PLT 文件
FormData: file (.plt, ≤10MB), type = "fabric"6.2 提交排版任务
{
"type": "fabric",
"source": "upload",
"tempId": "uuid-from-upload",
"originalName": "裁片.plt",
"params": {
"strip_height": 1480,
"time_minutes": 3
}
}| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| strip_height | int | 1480 | 布幅高度 (mm) |
| time_minutes | int | 1 | 求解时间 (分钟) |
7. 交易拟合 API
7.1 上传 Excel
FormData: file (.xlsx, ≤10MB, 支持多文件), type = "transaction"
响应: { "data": { "tempIds": ["uuid-1", "uuid-2"], "originalNames": [...] } }7.2 提交拟合任务
{
"type": "transaction",
"source": "upload",
"tempIds": ["uuid-1", "uuid-2"],
"params": {
"target_month": "2026-07",
"target_mode": "ratio",
"target_ratio": 0.15,
"tolerance": 0.10,
"amount_column": "收款金额",
"time_column": "收款时间",
"time_limit": 2
}
}| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| target_month | string | 上月 | 目标月份 (YYYY-MM) |
| target_mode | string | "ratio" | 目标方式: ratio(按比例) / amount(按金额) |
| target_ratio | float | 0.15 | 目标比例 (0-1, mode=ratio 时使用) |
| target_amount | float | null | null | 目标金额 (元, mode=amount 时使用) |
| tolerance | float | 0.10 | 容差范围 (0-0.5) |
| amount_column | string | "收款金额" | Excel 中金额列的列名 |
| time_column | string | "收款时间" | Excel 中时间列的列名 |
| time_limit | int | 2 | 求解时间 (分钟) |
7.3 获取结果
方式一:GET /api/jobs/:id 响应中data.result 即为完整结果 JSON。
方式二: GET /api/transaction/export/:id → 返回 .xlsx 结果文件8. 任务管理 API
8.1 查询任务列表
响应:
{
"success": true,
"data": {
"items": [
{
"id": 88,
"jobNumber": "A3K7-LA-260804-0001",
"type": "lathe",
"status": "success",
"createdAt": "2026-08-04T10:00:00.000Z",
"completedAt": "2026-08-04T10:03:15.000Z",
"totalBeds": 12,
"runTimeSec": 193.5
}
],
"total": 15,
"page": 1,
"pageSize": 20
}
}| Query 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| type | string | "fabric" | 产品类型: fabric / lathe / pcb / transaction |
| page | int | 1 | 页码 |
| pageSize | int | 20 | 每页条数 (最大 100) |
8.2 取消/删除任务
DELETE /api/jobs/:id
行为:
pending → 标记为 canceled
running → 中止进程,标记为 canceled
其他状态 → 软删除(从列表隐藏)8.3 中止运行中任务
POST /api/jobs/:id/stop
仅对 running 状态有效。算法会在下一个检查点退出,返回当前最优解。9. 套餐与限制
API Key 继承其所属用户的套餐限制:
| 套餐 | 任务优先级 | 最大求解时间 | 上传权限 |
|---|---|---|---|
| Standard | 普通 | 10 分钟 | 允许 |
| Pro | 较高 | 30 分钟 | 允许 |
| VIP | 最高 | 60 分钟 | 允许 |
当请求的 time_minutes 超过套餐上限时,系统自动截断至最大允许值。
10. 完整集成示例
Python 示例(直接提交 JSON)
import requests
import time
BASE_URL = "https://norai.cn/api"
API_KEY = "sk_live_a3k7_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
headers = {"Authorization": f"Bearer {API_KEY}"}
# 步骤 1: 直接提交任务 (无需上传文件)
resp = requests.post(f"{BASE_URL}/jobs",
headers={**headers, "Content-Type": "application/json"},
json={
"type": "lathe",
"source": "json",
"data": {
"sizes": ["S", "M", "L", "XL", "2XL"],
"colors": ["白色", "黑色", "蓝色"],
"demand": {
"白色": {"S": 120, "M": 200, "L": 180, "XL": 150, "2XL": 80},
"黑色": {"S": 100, "M": 180, "L": 160, "XL": 120, "2XL": 60},
"蓝色": {"S": 80, "M": 150, "L": 140, "XL": 100, "2XL": 50},
},
},
"params": {"method": "auto", "time_limit": 5},
})
job_id = resp.json()["data"]["id"]
# 步骤 2: 轮询状态
while True:
resp = requests.get(f"{BASE_URL}/jobs/{job_id}", headers=headers)
data = resp.json()["data"]
if data["status"] in ("success", "failure"):
break
time.sleep(3)
# 步骤 3: 获取结果
if data["status"] == "success":
# 方式一: JSON 结果 (已在轮询响应中, 推荐)
result = data["result"]
print(f"完成! 总床数: {result['total_beds']}")
print(f"床方案: {result['beds']}")
# 方式二: 导出 Excel 文件
resp = requests.get(f"{BASE_URL}/lathe/export/{job_id}", headers=headers)
with open("result.xlsx", "wb") as f:
f.write(resp.content)cURL 示例
# 直接提交 JSON 任务 (推荐, 无需上传文件)
curl -X POST https://norai.cn/api/jobs \
-H "Authorization: Bearer sk_live_a3k7_xxxx..." \
-H "Content-Type: application/json" \
-d '{
"type": "lathe",
"source": "json",
"data": {"sizes":["S","M","L"],"colors":["白色"],
"demand":{"白色":{"S":100,"M":200,"L":150}}},
"params": {"method":"auto","time_limit":5}
}'
# 查询状态 (轮询直到 status 为 success/failure)
curl https://norai.cn/api/jobs/88 \
-H "Authorization: Bearer sk_live_a3k7_xxxx..."
# 导出 Excel (可选, JSON 结果已在上一步响应中)
curl -o result.xlsx https://norai.cn/api/lathe/export/88 \
-H "Authorization: Bearer sk_live_a3k7_xxxx..."
# --- 若数据来源为 Excel 文件, 使用上传流程 ---
# 上传文件
curl -X POST https://norai.cn/api/upload \
-H "Authorization: Bearer sk_live_a3k7_xxxx..." \
-F "file=@订单数据.xlsx" -F "type=lathe"
# 解析数据
curl -X POST https://norai.cn/api/lathe/parse-upload \
-H "Authorization: Bearer sk_live_a3k7_xxxx..." \
-H "Content-Type: application/json" \
-d '{"tempId": "uuid-from-upload"}'
# 提交任务 (source=upload 需要 tempId + data)
curl -X POST https://norai.cn/api/jobs \
-H "Authorization: Bearer sk_live_a3k7_xxxx..." \
-H "Content-Type: application/json" \
-d '{
"type": "lathe", "source": "upload",
"tempId": "uuid-from-upload",
"data": {"sizes":["S","M","L"],"colors":["白色"],
"demand":{"白色":{"S":100,"M":200,"L":150}}},
"params": {"method":"auto","time_limit":5}
}'Node.js 示例
const API_KEY = "sk_live_a3k7_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const BASE = "https://norai.cn/api";
const headers = { Authorization: `Bearer ${API_KEY}` };
// 直接提交 JSON 任务
const jobResp = await fetch(`${BASE}/jobs`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
type: "lathe",
source: "json",
data: {
sizes: ["S", "M", "L"],
colors: ["白色", "黑色"],
demand: { "白色": { S: 100, M: 200, L: 150 }, "黑色": { S: 80, M: 160, L: 120 } },
},
params: { method: "auto", time_limit: 5 },
}),
});
const { data: job } = await jobResp.json();
// 轮询
let result;
while (true) {
const resp = await fetch(`${BASE}/jobs/${job.id}`, { headers });
const { data } = await resp.json();
if (data.status === "success") { result = data.result; break; }
if (data.status === "failure") throw new Error(data.errorMessage);
await new Promise(r => setTimeout(r, 3000));
}
console.log("总床数:", result.total_beds);11. 错误处理
重试策略
| 状态码 | 是否重试 | 建议 |
|---|---|---|
| 401 | 否 | 检查 Key 是否正确/过期/已吊销 |
| 403 | 否 | 检查 scope 或套餐权限 |
| 429 | 是 | 等待后重试 |
| 500 | 是 | 指数退避重试(最多 3 次) |
任务失败原因
| errorMessage | 含义 | 建议 |
|---|---|---|
| 无可行解 | 当前约束下无法满足所有需求 | 调大容差或减少约束 |
| 计算超时 | 限定时间内未找到可行解 | 增加 time_limit |
| 文件格式错误 | 输入数据有问题 | 检查 Excel 格式 |
12. 最佳实践
- 轮询间隔:建议 3 秒一次,避免过于频繁
- 超时处理:设置合理的 time_limit,标准套餐最大 10 分钟
- 密钥安全:API Key 保存在服务端环境变量中,不要硬编码在客户端代码或提交到代码仓库
- 最小权限:创建 Key 时仅授予必要的 scope
- 定期轮换:建议每 90 天更换一次 API Key
- 错误日志:记录所有非 200 响应以便排查问题
- 幂等处理:同一请求不要重复提交(通过 jobNumber 去重判断)