开放 API v1 接入文档
开放 API 用于让企业自己的系统、自动化工具或数据分析程序读取珍妮面料软件中的部分业务数据。第一期是安全地基和样品基础资料只读试点,适合把样品编号、名称、成分、门幅、克重、规格、分类、公开封面等资料同步到外部系统。
第一期开通范围
第一期只支持读取样品基础资料:GET /openapi/v1/samples。不支持新增、修改、删除样品,不支持回写库存、订单、财务、图片、联系人等数据,也不支持回写珍妮主库。需要写入珍妮系统的数据,仍然请在珍妮软件内按正常业务流程操作。
如果你在软件里还看不到开放 API 入口,说明该功能尚未对当前企业开放。具体开通状态、接口域名和可用额度,以珍妮软件内实际入口或客服通知为准。
一、接入前准备
接入前先确认这几件事:
| 项目 | 说明 |
|---|---|
| 开通权限 | 由企业所有者在珍妮软件内管理开放 API。员工账号是否能看到入口,以企业权限设置为准 |
| API Key | 用于标识调用方。一个企业可以按用途创建不同 Key,例如“企业微信表格同步”“内部 BI 同步” |
| API Secret | 用于生成请求签名。Secret 只在创建或轮换时显示一次,关闭页面后系统不会再展示明文 |
| Scope | 第一期开通 samples:read.basic,表示“样品基础资料只读” |
| IP 白名单 | 可限制只允许固定出口 IP 或 CIDR 网段调用。正式接入建议填写 |
| 限流 | 按 API Key 做分钟级限流。默认 120 次/分钟;实际额度以创建 Key 时设置为准 |
二、重要边界
开放 API v1 第一期只读,不做任何回写:
- 不提供
POST/PUT/PATCH/DELETE形式的外部写入接口 - 不支持外部系统创建、编辑、删除样品
- 不支持外部系统写入珍妮主库
- 不支持批量上传图片、文件或码单
- 不返回成本、进价、供应商报价、内部图片、私有图片、联系人资料、财务信息
- 不代替员工账号登录;第一期只按开放 API scope 输出有限字段
三、接口地址
第一期接口:
GET /openapi/v1/samples完整请求地址由“接入域名 + 路径”组成:
https://<接入域名>/openapi/v1/samples<接入域名> 以开通时提供的地址为准。不要把示例里的占位符直接用于生产调用。
四、请求头
每次请求都必须带以下请求头:
| 请求头 | 必填 | 说明 |
|---|---|---|
X-Jenny-Key | 是 | API Key,例如 jnk_... |
X-Jenny-Timestamp | 是 | 当前时间戳。建议使用毫秒时间戳;秒级时间戳也会按秒处理。服务端只接受 5 分钟时间窗口内的请求 |
X-Jenny-Nonce | 是 | 随机字符串,建议使用 UUID。相同 API Key 下,同一个 nonce 在 10 分钟内不能重复使用 |
X-Jenny-Signature | 是 | 使用 API Secret 计算出来的 HMAC-SHA256 十六进制签名 |
五、签名规则
签名用于确认请求没有被篡改,并证明调用方持有 API Secret。
签名字符串由 4 行组成,行与行之间用换行符 \n 连接:
METHOD
/openapi/v1/samples
canonical_query
sha256(body)说明:
METHOD使用大写,例如GET- 第二行是请求路径,不包含域名
canonical_query是排序后的查询字符串sha256(body)是请求体的 SHA-256 十六进制摘要- 第一期开通的是
GET查询接口,请求体为空。空字符串的 SHA-256 是e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 X-Jenny-Timestamp和X-Jenny-Nonce是独立校验的防重放请求头,不要把它们额外拼进签名字符串
canonical_query 规则:
- 解析 URL query 参数。
- 按参数名排序。
- 同名参数按参数值排序。
- 对参数名和参数值做 URL encode。
- 用
&拼接成标准查询字符串。
例如原始请求参数是:
page_size=50&page=1&keyword=cotton排序后的 canonical_query 是:
keyword=cotton&page=1&page_size=50签名计算:
hex(hmac_sha256(API_SECRET, canonical_string))六、Node.js 签名示例
下面示例只演示签名生成方式。正式接入时,请把 API Secret 放在后端环境变量或密钥管理系统中,不要写进前端页面、浏览器脚本、公开仓库或截图。
import crypto from 'node:crypto'
const apiKey = process.env.JENNY_API_KEY
const apiSecret = process.env.JENNY_API_SECRET
const baseUrl = 'https://<接入域名>'
const path = '/openapi/v1/samples'
const params = new URLSearchParams({
page: '1',
page_size: '50',
})
function canonicalQuery(searchParams) {
const pairs = Array.from(searchParams.entries())
pairs.sort((a, b) => {
if (a[0] === b[0]) return a[1].localeCompare(b[1])
return a[0].localeCompare(b[0])
})
return new URLSearchParams(pairs).toString()
}
const method = 'GET'
const body = ''
const bodyHash = crypto.createHash('sha256').update(body).digest('hex')
const query = canonicalQuery(params)
const canonical = [method, path, query, bodyHash].join('\n')
const signature = crypto.createHmac('sha256', apiSecret).update(canonical).digest('hex')
const res = await fetch(`${baseUrl}${path}?${query}`, {
method,
headers: {
'X-Jenny-Key': apiKey,
'X-Jenny-Timestamp': String(Date.now()),
'X-Jenny-Nonce': crypto.randomUUID(),
'X-Jenny-Signature': signature,
},
})
const data = await res.json()
console.log(data)七、只读 samples 接口
请求
GET /openapi/v1/samples需要 scope:
samples:read.basic查询参数:
| 参数 | 必填 | 默认值 | 说明 |
|---|---|---|---|
page | 否 | 1 | 页码,从 1 开始 |
page_size | 否 | 50 | 每页条数。默认最大 100;超过最大值会按最大值处理 |
updated_since | 否 | 无 | 毫秒时间戳,只返回更新时间大于等于该值的样品 |
keyword | 否 | 无 | 按编号、名称、成分、分类做基础搜索 |
返回按更新时间倒序排列,适合外部系统做增量同步。updated_since 用毫秒时间戳,例如 1783075200000。
返回字段
每条样品只返回基础展示资料:
| 字段 | 说明 |
|---|---|
external_id | 对外使用的样品 ID。外部系统请保存这个值,不要依赖内部编号 |
code | 样品编号 |
name | 样品名称 |
constituents | 成分 |
width | 门幅 |
weight | 克重 |
specification | 规格 |
type | 分类 |
public_cover | 公开封面图地址。只返回公共图片,不返回内部图库和私有图库 |
created_at | 创建时间,毫秒时间戳 |
updated_at | 更新时间,毫秒时间戳 |
接口不返回以下信息:
- 成本、进价、供应商报价、利润、成本计算器结果
- 成品供应商、坯布供应商、工厂、联系人、联系方式
- 内部图片、私有图片
- 财务、付款、对账、发票、客户隐私资料
- 库存明细、订单明细、操作日志等不在第一期范围内的数据
返回示例
{
"code": 0,
"data": {
"list": [
{
"external_id": "smp_xxxxx",
"code": "A-1001",
"name": "斜纹棉弹",
"constituents": ["棉 97%", "氨纶 3%"],
"width": ["150CM"],
"weight": ["220GSM"],
"specification": ["现货"],
"type": ["棉弹"],
"public_cover": "https://<图片地址>",
"created_at": 1783075200000,
"updated_at": 1783078800000
}
],
"total": 1,
"page": 1,
"page_size": 50,
"has_more": false
}
}八、防重放:时间戳和 nonce
每个请求都要带 X-Jenny-Timestamp 和 X-Jenny-Nonce。
时间戳规则:
- 建议使用毫秒时间戳,例如
1783075200000 - 服务端只接受当前时间前后 5 分钟内的请求
- 如果时间差过大,请先校准调用端时间
nonce 规则:
- 每次请求生成新的随机字符串,建议使用 UUID
- 同一个 API Key 下,同一个 nonce 在 10 分钟内只能使用一次
- 重试请求时,也要重新生成 nonce 和签名
如果重复使用 nonce,服务端会拒绝请求,避免同一条已签名请求被重复提交。
九、scope 权限
第一期只有一个 scope:
samples:read.basic它代表“样品基础资料只读”。创建 API Key 时必须勾选这个 scope,才能调用 GET /openapi/v1/samples。
后续如果新增其它接口,会按接口单独增加 scope。不要为了省事把同一个 Key 给所有系统共用;建议按用途分别创建 Key,并只授予所需 scope。
十、限流
开放 API 按 API Key 做分钟级限流:
- 默认 120 次/分钟
- 创建 Key 时可设置实际分钟限流
- 超过限流会返回
429
建议外部系统做增量同步时使用 updated_since,不要高频全量拉取。同步程序遇到 429 时,应暂停一段时间后重试。
十一、IP 白名单
创建 API Key 时可以填写 IP 白名单:
- 每行一个公网 IP,例如
203.0.113.10 - 也可以填写 CIDR 网段,例如
203.0.113.0/24 - 留空表示不限制来源 IP
正式接入建议填写固定出口 IP 或网段。若调用方使用动态公网 IP、云函数弹性出口或多个 NAT 出口,需要提前确认所有可能出口,否则会出现“当前 IP 不在白名单内”的错误。
十二、Secret 保管和轮换
Secret 只显示一次:
- 创建 API Key 时显示一次
- 轮换 Secret 时显示一次
- 关闭弹窗后系统不会再展示明文 Secret
- 如果丢失,只能轮换生成新的 Secret
保管建议:
- 放在后端环境变量、密钥管理系统或 CI/CD 的密钥配置里
- 不要写进前端代码、App 包、浏览器脚本或公开仓库
- 不要通过截图、群聊、邮件明文长期保存
- 为不同用途创建不同 Key,便于单独停用或轮换
- 如果怀疑 Secret 泄露,立即停用该 Key 或轮换 Secret
十三、常见错误
| HTTP 状态 | 常见 code | 含义 |
|---|---|---|
401 | missing_key / invalid_key | 缺少 API Key,或 Key 无效 / 已停用 |
401 | timestamp_invalid | 时间戳缺失、格式错误或超出 5 分钟窗口 |
401 | missing_nonce / nonce_replayed | 缺少 nonce,或 nonce 已被使用 |
401 | signature_invalid | 签名不正确,通常是 query 排序、body hash、路径或 Secret 不一致 |
403 | ip_not_allowed | 调用方公网 IP 不在白名单内 |
403 | scope_denied | API Key 没有当前接口所需 scope |
413 | body_too_large | 请求体超过限制。第一期 samples 查询通常不需要请求体 |
429 | rate_limited | 超过分钟限流 |
排查签名问题时,先把以下四项打印到调用方日志中核对:请求方法、路径、排序后的 query、body hash。不要把 API Secret 打进日志。
十四、接入建议
- 先在测试脚本中只拉取
page=1&page_size=10,确认签名、时间戳、nonce 正常。 - 再按页读取全量样品,保存每条样品的
external_id和updated_at。 - 后续同步使用
updated_since增量拉取。 - 外部系统只保存自己需要展示的字段,不要把 Secret、签名串、完整请求头写入业务日志。
- 发现字段不够用时,先联系珍妮确认是否属于后续开放范围;第一期不要通过其它非公开入口绕过开放 API。
