工厂控制台 API¶
工厂控制台 API 负责设备注册、NFC 凭证生成与下载、审批工作流及审计记录。挂载在 /internal/factory/*,通过独立的 factory-api ingress 暴露,与用户 API 完全隔离:用户 JWT 无法访问此 API,工厂凭证也无法访问任何用户接口。
认证¶
工厂 API 使用静态 Bearer Token(由 FACTORY_PRINCIPALS 环境变量配置),而非用户 JWT。每个 Principal 持有唯一 ID、角色及批次/SKU 访问范围。
角色¶
| 角色 | 说明 |
|---|---|
operator |
注册设备、生成凭证、首次下载 .nfc.json 文件 |
supervisor |
拥有 operator 全部权限,另可批准重复下载申请、吊销并重发凭证、创建批次、批量导入设备、批量生成文件 |
auditor |
只读:可查看所有设备、批次、统计及审计日志,不可写入 |
范围限制¶
每个 Principal 的 batches 和 skus 字段限定其可访问的批次和 SKU。["*"] 表示不限制;空数组 [] 表示无权访问任何资源(非通配符)。
批次(Batches)¶
批次对应一张生产订单(PO)。设备必须先关联到批次才能注册。
GET /internal/factory/batches¶
列出当前 Principal 可访问的所有批次。
查询参数:
| 参数 | 类型 | 描述 |
|---|---|---|
status |
string | 按状态筛选(open、closed) |
响应: 200 OK
{
"items": [
{
"id": "01912345-...",
"orderNo": "PO-2026-001",
"sku": "INK-800-BW-001",
"hwRevision": "v1.2",
"plannedQuantity": 500,
"factoryId": "factory-shenzhen",
"status": "open",
"createdAt": "2026-07-01T00:00:00Z",
"createdBy": "admin"
}
],
"total": 1,
"limit": 1,
"offset": 0
}
POST /internal/factory/batches¶
需要 Supervisor 角色
创建新批次。
请求体:
{
"orderNo": "PO-2026-001",
"sku": "INK-800-BW-001",
"hwRevision": "v1.2",
"plannedQuantity": 500,
"factoryId": "factory-shenzhen"
}
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
orderNo |
string | 是 | 采购订单号,全局唯一 |
sku |
string | 是 | 产品 SKU |
hwRevision |
string | 否 | 硬件版本号 |
plannedQuantity |
integer | 否 | 计划数量 |
factoryId |
string | 否 | 工厂标识符 |
响应: 201 Created — 返回创建的批次对象
GET /internal/factory/batches/{batchId}¶
获取单个批次详情。
响应: 200 OK — 返回批次对象
GET /internal/factory/batches/{batchId}/stats¶
获取批次的统计数据(注册数、生成数、下载数等)。
响应: 200 OK
{
"batch": { ... },
"counts": {
"registered": 50,
"credentialGenerated": 45,
"downloaded": 40,
"bound": 30,
"plannedQuantity": 500
}
}
设备(Devices)¶
GET /internal/factory/devices¶
列出设备,支持分页和过滤。
查询参数:
| 参数 | 类型 | 描述 |
|---|---|---|
batchId |
UUID | 按批次筛选 |
lifecycleStatus |
string | 按生命周期状态筛选(registered、provisioned) |
nfcStatus |
string | 按 NFC 状态筛选(pending、generated、downloaded、revoked) |
bound |
boolean | 按是否已绑定筛选 |
q |
string | 按 serial 或 hwId 关键词搜索 |
limit |
integer | 每页数量(默认 50,最大 200) |
offset |
integer | 分页偏移 |
响应: 200 OK — 返回 DeviceView 列表(见下方模型)
POST /internal/factory/devices¶
需要写权限(operator 或 supervisor)
注册单台设备。hwId 和 thingName 由后端分配,调用方不可指定。
请求体:
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
batchId |
UUID | 是 | 所属批次 ID |
serial |
string | 否 | 工厂序列号;batchId + serial 唯一 |
sku |
string | 否 | 覆盖批次 SKU |
hwRevision |
string | 否 | 覆盖批次硬件版本 |
请求头(可选):
| 请求头 | 描述 |
|---|---|
Idempotency-Key |
同一 batchId + serial 重复提交时安全去重 |
响应: 201 Created(新建)或 200 OK(幂等命中)
{
"id": "01912345-...",
"hwId": "a1b2c3d4e5f60708090a0b0c0d0e0f10",
"thingName": "inklet-a1b2c3d4e5f60708",
"serial": "SN2026001",
"batchId": "01912345-...",
"lifecycleStatus": "registered",
"nfcStatus": "pending",
"credentialVersion": 0,
"bound": false,
"createdAt": "2026-07-01T10:00:00Z"
}
POST /internal/factory/devices:batch-import¶
需要 Supervisor 角色
通过 CSV 批量注册设备。CSV 文件需包含 serial(必填)、sku(选填)、hwRevision(选填)列,batchId 通过查询参数传入。
查询参数:
| 参数 | 类型 | 描述 |
|---|---|---|
batchId |
UUID | 所属批次 ID(必填) |
请求体: multipart/form-data,字段名 file,CSV 文件(最大 4 MiB)
响应: 200 OK
{
"ok": 48,
"idempotent": 2,
"errors": [
{ "row": 3, "serial": "SN2026003", "error": "duplicate serial" }
]
}
错误行不阻塞合法行;部分成功是正常结果。
GET /internal/factory/devices/{deviceId}¶
获取单台设备详情,包含当前凭证视图(proof 已遮蔽)。
响应: 200 OK — 返回 DeviceView 对象
POST /internal/factory/devices/{deviceId}/nfc-files¶
需要写权限
为设备生成 NFC 凭证。每台设备在同一 credentialVersion 下只能有一份有效凭证;重复调用返回 409,不会重新生成。如需重新获取文件,通过 download 接口下载(可能需要申请审批)。
响应: 201 Created — 返回 CredentialView 对象
POST /internal/factory/devices:nfc-files-batch-generate¶
需要 Supervisor 角色
批量为指定批次内所有 nfcStatus=pending 的设备生成凭证。
请求体:
响应: 200 OK
POST /internal/factory/devices/{deviceId}/nfc-credentials:revoke-and-reissue¶
需要 Supervisor 角色
吊销当前凭证并生成新凭证(credentialVersion + 1)。旧凭证立即失效,旧 .nfc.json 文件无法再下载。已用旧凭证绑定的 App 在下次绑定时将被拒绝。
请求体:
响应: 200 OK — 返回新 CredentialView 对象
NFC 文件(NFC Files)¶
.nfc.json 文件是交给产线的烧录文件,包含设备身份信息及 NFC URI 载荷。下载是有权限控制和审计的独立动作。
.nfc.json 文件结构¶
{
"schemaVersion": 1,
"type": "inklet-nfc-bind",
"serial": "SN2026001",
"hwId": "a1b2c3d4e5f60708090a0b0c0d0e0f10",
"thingName": "inklet-a1b2c3d4e5f60708",
"batchId": "01912345-...",
"sku": "INK-800-BW-001",
"hwRevision": "v1.2",
"keyId": "k1",
"credentialVersion": 1,
"nfcPayload": "inklet://bind?v=2&kid=k1&hw=a1b2c3d4...&cv=1&p=AAAA...",
"payloadSha256": "abcdef0123456789...",
"issuedAt": "2026-07-01T10:00:00Z",
"manifestKeyId": "manifest-key-1",
"manifestSignature": "base64-ed25519-sig"
}
| 字段 | 描述 |
|---|---|
nfcPayload |
写入 NFC 标签的完整 URI(产线直接使用此字段) |
payloadSha256 |
payload 的 SHA-256,用于回读校验 |
manifestSignature |
文件整体的 Ed25519 签名(用于产线验证文件完整性) |
凭证敏感性
.nfc.json 文件包含完整的 NFC URI(含 proof),是有效的绑定凭证。请妥善保管,遵循最小分发原则。文件泄露只影响对应单台设备,通过吊销并重发可使其立即失效。
GET /internal/factory/nfc-files/{credentialId}/download¶
需要写权限 · 受下载限流保护
下载单台设备的 .nfc.json 文件。
- 首次下载:直接返回文件。
- 重复下载:operator 需先提交下载申请(
POST .../download-requests),由 supervisor 审批后方可下载;supervisor 自身可直接重复下载。
响应: 200 OK,Content-Type: application/json,文件名 {serial}.nfc.json
错误:
| 状态码 | 原因 |
|---|---|
403 |
重复下载申请未审批,或无权限 |
404 |
凭证不存在或已吊销 |
POST /internal/factory/nfc-files/{credentialId}/download-requests¶
需要写权限
提交重复下载申请,等待 supervisor 审批。每个凭证每次只能有一个待审批申请。
响应: 201 Created
{
"id": "request-uuid",
"credentialId": "cred-uuid",
"requestedBy": "operator-1",
"status": "pending",
"createdAt": "2026-07-01T11:00:00Z"
}
POST /internal/factory/nfc-files:download-zip¶
需要 Supervisor 角色 · 受下载限流保护
批量下载指定批次所有设备的 .nfc.json 文件,返回 ZIP 压缩包。
请求体:
响应: 200 OK,Content-Type: application/zip
下载审批(Download Requests)¶
GET /internal/factory/download-requests¶
列出当前 Principal 可见的下载审批申请。
查询参数:
| 参数 | 类型 | 描述 |
|---|---|---|
status |
string | pending、approved、rejected |
batchId |
UUID | 按批次筛选 |
响应: 200 OK — 申请列表
POST /internal/factory/download-requests/{requestId}:approve¶
需要 Supervisor 角色
审批通过下载申请。之后 operator 可在限定时间内下载一次。
响应: 200 OK
POST /internal/factory/download-requests/{requestId}:reject¶
需要 Supervisor 角色
拒绝下载申请,需提供原因。
请求体:
响应: 200 OK
统计与审计¶
GET /internal/factory/stats¶
获取全局或批次级别的统计数据。
查询参数:
| 参数 | 类型 | 描述 |
|---|---|---|
batchId |
UUID | 指定批次(不传则返回全局) |
响应: 200 OK
GET /internal/factory/audit-logs¶
查询审计日志。所有写操作(注册、生成、下载、吊销、审批)均自动记录。
查询参数:
| 参数 | 类型 | 描述 |
|---|---|---|
batchId |
UUID | 按批次筛选 |
deviceId |
UUID | 按设备筛选 |
action |
string | 按操作类型筛选 |
from |
RFC3339 | 开始时间 |
to |
RFC3339 | 结束时间 |
limit |
integer | 每页数量(默认 50) |
offset |
integer | 分页偏移 |
响应: 200 OK
{
"items": [
{
"id": "log-uuid",
"action": "credential_download",
"result": "ok",
"principalId": "operator-1",
"principalName": "Line A Station 3",
"batchId": "01912345-...",
"deviceId": "01912345-...",
"detail": "first download",
"createdAt": "2026-07-01T10:05:00Z"
}
],
"total": 150,
"limit": 50,
"offset": 0
}
GET /internal/factory/manifest-keys¶
列出当前有效的 manifest 签名公钥(供产线验证 .nfc.json 完整性)。
响应: 200 OK
[
{
"id": "manifest-key-1",
"publicKey": "base64-ed25519-public-key",
"algorithm": "ed25519",
"createdAt": "2026-07-01T00:00:00Z"
}
]
GET /internal/factory/whoami¶
返回当前认证 Principal 的身份信息。
响应: 200 OK
{
"id": "operator-1",
"name": "Line A Station 3",
"role": "operator",
"stationId": "station-a3",
"factoryId": "factory-shenzhen",
"batches": ["*"],
"skus": ["INK-800-BW-001"]
}
DeviceView 模型¶
{
"id": "01912345-...",
"serial": "SN2026001",
"hwId": "a1b2c3d4e5f60708090a0b0c0d0e0f10",
"thingName": "inklet-a1b2c3d4e5f60708",
"batchId": "01912345-...",
"orderNo": "PO-2026-001",
"sku": "INK-800-BW-001",
"hwRevision": "v1.2",
"lifecycleStatus": "registered",
"nfcStatus": "downloaded",
"credentialVersion": 1,
"nfcKeyId": "k1",
"bound": false,
"online": false,
"createdAt": "2026-07-01T10:00:00Z",
"credential": {
"id": "cred-uuid",
"keyId": "k1",
"credentialVersion": 1,
"status": "downloaded",
"payloadSha256": "abcdef...",
"payloadPreview": "inklet://bind?v=2&kid=k1&hw=a1b2...&cv=1&p=****",
"issuedBy": "operator-1",
"issuedAt": "2026-07-01T10:00:00Z",
"downloadCount": 1,
"requiresApproval": true
}
}
NFC 状态说明:
nfcStatus |
描述 |
|---|---|
pending |
未生成凭证 |
generated |
已生成,尚未下载 |
downloaded |
已至少下载一次 |
revoked |
已吊销(等待重发) |
注意: payloadPreview 中的 proof 永远以 **** 遮蔽;完整 proof 只通过 download 接口获取。
错误响应¶
所有错误响应统一格式:
| 状态码 | 描述 |
|---|---|
400 |
请求格式错误或参数无效 |
401 |
缺少或无效的 Bearer Token |
403 |
角色或范围不足 |
404 |
资源不存在 |
409 |
冲突(如同一 serial 重复注册,或凭证已生成无法重新生成) |
429 |
触发限流 |