AI盒绘开放平台开发者中心
欢迎接入 AI盒绘开放平台!我们为第三方合作企业系统提供标准、安全、即插即用的 AI 包装设计集成方案。您的终端用户可凭短时 Ticket 免登直达全功能设计器,设计完成后通过 Webhook 文件流将高清成品与业务参数回传至贵司系统,实现物理强隔离的一对一业务闭环。
企业数据物理隔离
多租户物理隔离体系,合作方用户、素材与设计工程完全独立归档,保障企业资产私密与安全。
动态票据免登认证
服务端安全加签申请短时凭据(Ticket),5分钟有效即焚,保障终端用户无感进入设计器,无长期凭据泄露风险。
成果双轨接收机制
首推前端事件直收(Iframe/Popup postMessage,支持纯前端零 OSS 还原 File 对象);同时兼容传统服务端 Webhook 直推,满足各类企业架构。
★ 3 步极速接入指南 (Quick Start)
清晰简明的标准集成路径。第三方开发者仅需关注三个动作,即可完成端到端闭环对接。
服务端加签换票
贵司服务端用 AppSecret 计算 HMAC-SHA256 签名,调用开放接口换取 5 分钟有效的一次性 ticket。
唤起在线设计器
支持嵌入 <iframe> 或通过弹窗跳转,将 ticket、底图地址 image_url 与业务标识拼装为链接直达设计器。
接收设计成品
点击“完成编辑”,前端立即通过 postMessage 接收高清图及二进制流(推荐,零公网门槛);亦可直推贵司 Webhook 回调端点。
02 接入环境矩阵与沙箱凭据 (Environment Matrix)
提供完备的双环境隔离机制。开发者在沙箱联调通过后,只需替换 Base URL 与正式企业凭据即可平滑上线。
测试联调环境 (Staging / Sandbox)
供第三方开发者进行接口签名加签与完整业务链路联调
正式生产环境 (Production)
面向第三方企业正式上线业务流量,强租户物理隔离
03 申请免登票据接口 (Ticket Issue API)
避免在前端 URL 中暴露长期身份凭据。合作方服务端通过 AppSecret 离线加签,按需预先申请 5 分钟有效的一次性 Ticket。
将参与签名的键值对按 Key 字典序升序排列(过滤空值),用 & 拼接为待签名串(格式如 key1=value1&key2=value2...);使用平台分配的 AppSecret 作为密钥进行 HMAC-SHA256 运算,输出 64 位小写十六进制字符串放入请求头 X-Open-Signature。
appKey=partner_demo_key&externalUserId=corp_u_10086&nickname=张经理·高级包装师&nonce=a8f9c2d1e0b34567×tamp=1789718400
partner_demo_secret_2026 计算 HMAC-SHA256 十六进制签名结果:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;
/**
* 1. 参数按 Key ASCII 升序字典序排列并过滤空值,拼接待签名串
*/
public static String buildStringToSign(Map<String, String> params) {
if (params == null || params.isEmpty()) return "";
Map<String, String> sortedMap = new TreeMap<>(params);
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedMap.entrySet()) {
String key = entry.getKey();
String val = entry.getValue();
if (val != null && !val.isEmpty()) {
if (sb.length() > 0) sb.append("&");
sb.append(key).append("=").append(val);
}
}
return sb.toString();
}
/**
* 2. HMAC-SHA256 签名计算,输出 64 位小写十六进制字符串
*/
public static String signHmacSha256(String stringToSign, String appSecret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hmacBytes = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hmacBytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
// === 测试调用实测 (带入测试数据与测试私钥) ===
// Map<String, String> params = new HashMap<>();
// params.put("appKey", "partner_demo_key");
// params.put("externalUserId", "corp_u_10086");
// params.put("nickname", "张经理·高级包装师");
// params.put("nonce", "a8f9c2d1e0b34567");
// params.put("timestamp", "1789718400");
// String stringToSign = buildStringToSign(params);
// String signature = signHmacSha256(stringToSign, "partner_demo_secret_2026");
// 打印输出: 62e5997f8a61b2c2fd177c1488753ec75a05b3de9a6c04dbe4a3af21b5818322
const crypto = require('crypto');
/**
* 1. 参数按 Key ASCII 升序字典序排列并过滤空值,拼接待签名串
*/
function buildStringToSign(params) {
return Object.keys(params)
.sort()
.filter(k => params[k] !== undefined && params[k] !== null && params[k] !== '')
.map(k => `${k}=${params[k]}`)
.join('&');
}
/**
* 2. HMAC-SHA256 签名计算,输出 64 位小写十六进制字符串
*/
function signHmacSha256(stringToSign, appSecret = 'partner_demo_secret_2026') {
return crypto.createHmac('sha256', appSecret)
.update(stringToSign, 'utf8')
.digest('hex');
}
// === 测试调用实测 (带入测试数据与测试私钥) ===
// const params = {
// appKey: 'partner_demo_key',
// externalUserId: 'corp_u_10086',
// nickname: '张经理·高级包装师',
// nonce: 'a8f9c2d1e0b34567',
// timestamp: '1789718400'
// };
// const stringToSign = buildStringToSign(params);
// const signature = signHmacSha256(stringToSign, 'partner_demo_secret_2026');
// console.log(signature); // 62e5997f8a61b2c2fd177c1488753ec75a05b3de9a6c04dbe4a3af21b5818322
import hmac
import hashlib
def build_string_to_sign(params: dict) -> str:
"""1. 参数按 Key ASCII 升序字典序排列并过滤空值,拼接待签名串"""
sorted_pairs = sorted((k, str(v)) for k, v in params.items() if v not in (None, ''))
return "&".join(f"{k}={v}" for k, v in sorted_pairs)
def sign_hmac_sha256(string_to_sign: str, app_secret: str = "partner_demo_secret_2026") -> str:
"""2. HMAC-SHA256 签名计算,输出 64 位小写十六进制字符串"""
return hmac.new(
app_secret.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()
# === 测试调用实测 (带入测试数据与测试私钥) ===
# params = {
# "appKey": "partner_demo_key",
# "externalUserId": "corp_u_10086",
# "nickname": "张经理·高级包装师",
# "nonce": "a8f9c2d1e0b34567",
# "timestamp": "1789718400"
# }
# string_to_sign = build_string_to_sign(params)
# signature = sign_hmac_sha256(string_to_sign, "partner_demo_secret_2026")
# print(signature) # 62e5997f8a61b2c2fd177c1488753ec75a05b3de9a6c04dbe4a3af21b5818322
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
)
// BuildStringToSign 1. 参数按 Key ASCII 升序字典序排列并过滤空值,拼接待签名串
func BuildStringToSign(params map[string]string) string {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
if val := params[k]; val != "" {
parts = append(parts, fmt.Sprintf("%s=%s", k, val))
}
}
return strings.Join(parts, "&")
}
// SignHmacSha256 2. HMAC-SHA256 签名计算,输出 64 位小写十六进制字符串
func SignHmacSha256(stringToSign, appSecret string) string {
mac := hmac.New(sha256.New, []byte(appSecret))
mac.Write([]byte(stringToSign))
return hex.EncodeToString(mac.Sum(nil))
}
// === 测试调用实测 (带入测试数据与测试私钥) ===
// params := map[string]string{
// "appKey": "partner_demo_key",
// "externalUserId": "corp_u_10086",
// "nickname": "张经理·高级包装师",
// "nonce": "a8f9c2d1e0b34567",
// "timestamp": "1789718400",
// }
// stringToSign := BuildStringToSign(params)
// signature := SignHmacSha256(stringToSign, "partner_demo_secret_2026")
// fmt.Println(signature) // 62e5997f8a61b2c2fd177c1488753ec75a05b3de9a6c04dbe4a3af21b5818322
<?php
/**
* 1. 参数按 Key ASCII 升序字典序排列并过滤空值,拼接待签名串
*/
function buildStringToSign(array $params) {
ksort($params);
$parts = [];
foreach ($params as $k => $v) {
if ($v !== null && $v !== '') {
$parts[] = "{$k}={$v}";
}
}
return implode('&', $parts);
}
/**
* 2. HMAC-SHA256 签名计算,输出 64 位小写十六进制字符串
*/
function signHmacSha256($stringToSign, $appSecret = 'partner_demo_secret_2026') {
return hash_hmac('sha256', $stringToSign, $appSecret);
}
// === 测试调用实测 (带入测试数据与测试私钥) ===
// $params = [
// 'appKey' => 'partner_demo_key',
// 'externalUserId' => 'corp_u_10086',
// 'nickname' => '张经理·高级包装师',
// 'nonce' => 'a8f9c2d1e0b34567',
// 'timestamp' => '1789718400'
// ];
// $stringToSign = buildStringToSign($params);
// $signature = signHmacSha256($stringToSign, 'partner_demo_secret_2026');
// echo $signature; // 62e5997f8a61b2c2fd177c1488753ec75a05b3de9a6c04dbe4a3af21b5818322
?>
| Header Name | 示例值 | 说明 |
|---|---|---|
| Content-Type | application/json | JSON 报文格式 |
| X-Open-App-Key | partner_demo_key | 平台分配的企业应用唯一标识 |
| X-Open-Timestamp | 1789718400 | 当前 UNIX 秒级时间戳 (防过期重放,容差 ±300s) |
| X-Open-Nonce | a8f9c2d1e0b34567 | 16 位以上高强度随机字符串 (防重放) |
| X-Open-Signature | 62e5997f8a61b2c2fd177c1488753ec75a05b3de9a6c04dbe4a3af21b5818322 | HMAC-SHA256 计算出的十六进制签名摘要 (与待签名串严格对应) |
{
"externalUserId": "corp_u_10086", // 【必填】贵司系统的用户唯一标识 (进行物理隔离归档)
"nickname": "张经理·高级包装师" // 【可选】用户展示昵称
}
{
"code": 200,
"message": "操作成功",
"data": {
"ticket": "8c3b7f1e4a9d4e5f8a1c2b3d4e5f6a7b", // 5分钟有效一次性免登 Ticket
"expiresIn": 300 // 有效期 (秒)
}
}
{
"code": 500,
"message": "API 签名校验失败,拒绝接入 (Invalid Signature)",
"data": null
}
{
"title": "Constraint Violation",
"status": 400,
"violations": [
{
"field": "applyTicket.request.externalUserId",
"message": "外部用户标识 externalUserId 不能为空"
}
]
}
| HTTP | code | 系统真实 message 提示 | 触发根因与排查指引 |
|---|---|---|---|
| 500 | 500 | API 签名校验失败,拒绝接入 (Invalid Signature) | 签名摘要计算不匹配。请检查待签名串升序字典序拼接(是否漏传字段)、AppSecret 是否为平台对应环境私钥。 |
| 500 | 500 | 请求已过期,时间戳偏差超过有效窗口 (300秒) | X-Open-Timestamp 与开放平台服务器时钟偏差超过 ±300 秒。请校准贵司服务器 NTP 时间。 |
| 500 | 500 | 请求已失效或被重复调用 (Nonce 已被使用) | X-Open-Nonce 随机串在 5 分钟窗口内重复提交。防重放拦截,请确保每次 HTTP 请求生成全新的随机字符串。 |
| 500 | 500 | 非法的 AppKey 或接入企业应用不存在 | X-Open-App-Key 无效。测试沙箱请填 partner_demo_key,生产环境请确认商务开通的企业应用标识。 |
| 500 | 500 | 该合作企业应用已被停用或禁用 | 合作企业资质未激活或被后台管理员禁用。请联系开放平台管理员(hello@heyijiapack.com)核实状态。 |
| 500 | 500 | 缺少接口签名 X-Open-Signature | HTTP 请求头中遗漏了 X-Open-Signature。 |
| 500 | 500 | 缺少应用凭证标识 X-Open-App-Key | HTTP 请求头中遗漏了 X-Open-App-Key。 |
| 500 | 500 | 缺少时间戳 X-Open-Timestamp | HTTP 请求头中遗漏了当前 UNIX 秒级时间戳。 |
| 500 | 500 | 缺少防重放随机串 X-Open-Nonce | HTTP 请求头中遗漏了 X-Open-Nonce 随机字符串。 |
| 400 | 400 | 外部用户标识 externalUserId 不能为空 | 请求 Body (JSON) 中未传 externalUserId 或其值为空字符串。 |
04 端到端完整时序交互图
直观呈现第三方系统、开放平台与终端用户浏览器之间的交互流转全过程。
[第三方系统/前端] [AI盒绘开放平台] [设计器运行时 (Iframe/Popup)] [三方 Webhook 接收端(可选)]
│ │ │ │
│ 1. 签名计算 (HMAC-SHA256) │ │ │
│ 2. POST /open/auth/ticket│ │ │
├─────────────────────────►│ │ │
│ │ 3. 校验签名与时间戳 │ │
│ │ 4. 生成 5min 即焚 Ticket │ │
│ 5. 返回 Ticket │ │ │
│◄─────────────────────────┤ │ │
│ │ │ │
│ 6. 嵌入 <iframe> 或 window.open 打开设计器 (带 ticket, image_url, extra 等) │
├──────────────────────────────────────────────────────►│ │
│ │ │ │
│ │ 7. 校验 Ticket 并兑换凭证 │ │
│ │◄──────────────────────────┤ │
│ │ 8. 凭证注入,加载底图画布 │ │
│ ├──────────────────────────►│ │
│ │ │ │
│ │ │ 9. 用户在线完成包装设计 │
│ │ │ 10. 点击右上角“完成编辑” │
│ │ │ │
│ ═══════【模式 A:前端事件接收(推荐·零公网门槛·纯前端 File 对象)】══════════════════ │
│ 11a. window.parent.postMessage({ type: 'DESIGN_COMPLETED', data, buffer }) │
│◄─────────────────────────────────────────────────────┤ (零拷贝 Transferable 瞬时投递) │
│ 12a. const file = new File([buffer], filename) │ │
│ 第三方前端 0 毫秒获得标准 File 对象,完成本地闭环│ │
│ │ │ │
│ ═══════【模式 B:传统 Webhook 接收(可选·无前端界面的批量场景)】═══════════════════ │
│ │ │ 11b. POST multipart/form-data │
│ │ ├───────────────────────────────►│
│ │ │ │ 12b. 服务端存储文件
│ │ │ │ 13b. 绑定 extra 参数
│ │ │ 14b. 返回 HTTP 200 OK │
│ │ │◄───────────────────────────────┤
│ │ │ │
│ │ │ 15. 提示完成并关闭窗口/抽屉 │
05 唤起设计器 URL 规范 (URL Parameters)
获得 Ticket 凭据后,拼接对应环境的设计器 Base URL,在浏览器新标签页或弹窗中打开即可直接进入工作台画布。
| 参数名 (Key) | 类型 | 是否必填 | 说明与示例 |
|---|---|---|---|
| ticket | String | 必填 | 由贵司服务端提前调接口获取的一次性免登凭据。平台校验通过后即刻失效,具有高等级防重放保护。 |
| image_url | String (URL) | 推荐 |
待编辑的原素材底图远程 HTTP/HTTPS 地址。必须进行标准 encodeURIComponent 编码,设计器打开后自动拉取并铺设至画布。
|
| external_project_id | String | 推荐 |
贵司业务系统的设计任务/订单/素材唯一标识(如 order_item_9988)。Webhook 回调保存时原样携带,便于贵司精准关联成果。
|
| external_user_id | String | 推荐 |
贵司系统内部的用户唯一标识(如 corp_u_10086)。
|
| webhook_url | String (URL) | 可选 |
本次编辑保存时动态直推接收的 Webhook 回调地址(优先于平台预设配置)。需 encodeURIComponent。
|
| extra | String | 可选 |
第三方自定义业务透传参数(如生产批次号、审批单号或序列化 JSON,需 encodeURIComponent)。在设计生命周期全程维持,Webhook 回调时原样回传。
|
| title | String | 可选 | 设计项目的初始标题(如“2026月饼礼盒包装正面”)。 |
| source | String | 可选 |
渠道来源标识(如 open_platform)。
|
https://test-editor.1packify.com/aidesign/editor?ticket=8c3b7f1e4a9d4e5f8a1c2b3d4e5f6a7b&external_user_id=u_10086&external_project_id=proj_box_01&extra=batch_2026_09&image_url=https%3A%2F%2Fopen-demo.1packify.com%2Fmaterials%2Fbox.jpg&title=%E6%96%B0%E5%8C%85%E8%A3%85%E8%AE%BE%E8%AE%A1
https://editor.1packify.com/aidesign/editor?ticket=8c3b7f1e4a9d4e5f8a1c2b3d4e5f6a7b&external_user_id=corp_user_99&external_project_id=order_item_88&extra=dept_VIP&image_url=https%3A%2F%2Fcdn.your-company.com%2Fpack_front.png&title=%E9%AB%98%E6%A1%A3%E7%A4%BC%E7%9B%92%E5%AE%9A%E5%88%B6
06 设计成果接收规范:前端事件接收(推荐)与 Webhook 直推
用户在设计器完成创作并点击“完成编辑”时,开放平台提供双轨接收架构:首推前端事件实时监听(推荐·支持纯前端零 OSS 还原 File 对象,零公网门槛);同时兼容服务端 Webhook 直推。
前端事件实时接收 (Iframe / Popup)
- 零公网门槛:无需外网域名、公网 IP、备案或 SSL 证书,内网局域网系统亦可秒通
- 零 CORS 困扰:基于浏览器原生安全通信通道,免除跨域预检与网络拦截
- 即时交互闭环:前端即刻捕获完成事件,自动关闭弹窗/抽屉并刷新业务页面
- 纯前端 File 直传:支持通过 Transferable 零拷贝直接提取原生二进制
File对象
服务端 Webhook 文件流推送
- 适用场景:无前端界面的批量设计流水线或全自动化后端服务对接
- 前置要求:第三方接收接口必须具有公网地址并强制配置
CORS响应头
方案 A:前端 PostMessage 事件接收与纯前端 File 对象提取
1. 标准事件消息结构 (Event Payload)
当用户在编辑器中点击“完成编辑”时,设计器将向宿主父窗口(window.parent)及打开源窗口(window.opener)安全广播消息:
{
"source": "ai-packaging-editor", // [必选] 消息源标识,用于过滤无关 window 消息
"event": "DESIGN_COMPLETED", // [必选] 事件枚举:设计完成并导出
"action": "finish_edit", // [向下兼容] 动作标识
"timestamp": 1791458920000, // 触发时间戳 (ms)
"data": {
"filename": "design_proj_001_20261009.png", // 合成导出的图片规范文件名
"externalProjectId": "proj_batch_001", // 贵司业务工程/订单唯一标识(原样回传)
"externalUserId": "user_designer_01", // 贵司用户唯一标识(原样回传)
"extra": "sku_8899_dept_A", // 贵司自定义业务透传参数(原样回传)
"fileSize": 2845920, // 文件大小(字节)
"mimeType": "image/png", // 图片类型 (image/png 或 image/jpeg)
"fileUrl": "https://oss.../export.png", // [可选] 云端/OSS 存储直链
"previewUrl": "https://oss.../preview.png" // [可选] 缩略预览图直链
},
"buffer": ArrayBuffer // ★ [核心] 二进制文件流 (Transferable 零拷贝投递)
}
若贵司系统处于内网隔离环境或不希望设计图走任何云端 OSS,设计器利用 HTML5 标准的 Transferable Objects(可转移对象) 机制,通过内存地址直接将 ArrayBuffer 零拷贝转移至贵司前端页面。不仅传输耗时为 0 毫秒、杜绝大图拷贝引起的主线程卡顿,而且贵司前端只需一行代码即可构造出标准的浏览器 File 对象:
2. 前端开箱即用监听范例代码 (Vue / React / 原生 HTML)
<!-- 示例 1: Iframe 嵌入模式 -->
<iframe id="editor-frame" src="https://test-editor.1packify.com/aidesign/editor?ticket=..." style="width:100%;height:800px;border:none;"></iframe>
<script>
// 注册全局消息监听器
window.addEventListener('message', async (event) => {
const msg = event.data;
// 1. 安全校验:过滤非 AI 包装设计器的消息
if (!msg || msg.source !== 'ai-packaging-editor') return;
// 2. 捕获设计完成事件
if (msg.event === 'DESIGN_COMPLETED' || msg.action === 'finish_edit') {
console.log('✅ 成功捕获设计完成事件:', msg.data);
const { filename, externalProjectId, extra, fileUrl, mimeType } = msg.data;
// 3. 提取二进制流并构造 File 对象(支持纯前端零 OSS 模式)
let designFile = null;
if (msg.buffer) {
const blob = new Blob([msg.buffer], { type: mimeType || 'image/png' });
designFile = new File([blob], filename, { type: mimeType || 'image/png' });
console.log('📦 成功在纯前端提取 File 对象:', designFile.name, (designFile.size / 1024).toFixed(1) + ' KB');
}
// 4. 执行贵司前端业务逻辑 (例如:展示预览、关闭抽屉、将 File 随同表单上传等)
alert(`项目 [${externalProjectId}] 设计已完成并同步!`);
if (designFile) {
// 示例:创建临时本地预览
const localPreviewUrl = URL.createObjectURL(designFile);
document.getElementById('my-preview-img').src = localPreviewUrl;
}
}
});
</script>
import { onMounted, onUnmounted } from 'vue'
const handleMessage = (e) => {
if (e.data?.source !== 'ai-packaging-editor') return
if (e.data.event === 'DESIGN_COMPLETED') {
const file = new File([e.data.buffer], e.data.data.filename)
// 触发组件状态更新或自动关闭模态框
emit('finish', { file, ...e.data.data })
}
}
onMounted(() => window.addEventListener('message', handleMessage))
onUnmounted(() => window.removeEventListener('message', handleMessage))
import { useEffect } from 'react';
useEffect(() => {
const onMsg = (event) => {
if (event.data?.source !== 'ai-packaging-editor') return;
if (event.data.event === 'DESIGN_COMPLETED') {
const file = new File([event.data.buffer], event.data.data.filename);
onDesignComplete(file, event.data.data);
}
};
window.addEventListener('message', onMsg);
return () => window.removeEventListener('message', onMsg);
}, []);
方案 B:服务端 Webhook 文件流直推契约 (仅在无前端系统时可选使用)
POST multipart/form-data| 表单字段名 (Key) | 类型 | 说明 |
|---|---|---|
| file | Binary File (Blob) | 导出生成的合成图像二进制文件流 (PNG/JPEG 高清原图) |
| externalProjectId | String | 原样透传第三方传入的工程/订单 ID,用于精准入库归档 |
| externalUserId | String | 原样透传第三方传入的用户 ID |
| extra | String | 原样透传第三方传入的自定义透传数据(批次、审批号等) |
| title | String | 用户保存时的设计标题 |
200 OK 并在 Body 中返回形如 {"code":200,"message":"success"} 的响应报文。
在 Webhook 模式下,文件流由终端浏览器直推至贵司服务端,因此贵司 Webhook 接收端必须返回 Access-Control-Allow-Origin: * 与 OPTIONS 204 预检,否则浏览器会拦截抛错。若贵司不希望配置公网 CORS,请直接选用上方方案 A(前端事件接收)。
07 多语言实战示例代码 (SDK & Code Examples)
默认引用测试沙箱环境与公开凭证。复制代码至本地项目,无需修改即可 1 秒调通加签换票与 Webhook 接收。
// ========================================================
// 1. 服务端 HMAC-SHA256 加签并申请 Ticket (完整可直接运行)
// ========================================================
public String getOpenEditorTicket(String externalUserId, String nickname) throws Exception {
// 【环境配置】沙箱测试环境基准地址与凭证 (上线生产替换为生产环境及专属凭证)
final String API_BASE_URL = "https://test-open.1packify.com/api";
final String APP_KEY = "partner_demo_key";
final String APP_SECRET = "partner_demo_secret_2026"; // 离线保密密钥
long timestamp = System.currentTimeMillis() / 1000;
String nonce = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
// 1.1 参数字典序升序拼接待签名串
Map<String, String> params = new TreeMap<>();
params.put("appKey", APP_KEY);
params.put("timestamp", String.valueOf(timestamp));
params.put("nonce", nonce);
params.put("externalUserId", externalUserId);
if (nickname != null && !nickname.isEmpty()) {
params.put("nickname", nickname);
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (sb.length() > 0) sb.append("&");
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
String stringToSign = sb.toString();
// 1.2 计算 HMAC-SHA256 签名 (64位 Hex 小写)
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(APP_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hmacBytes = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hmacBytes) {
hex.append(String.format("%02x", b));
}
String signature = hex.toString();
// 1.3 携带请求头发送 POST 请求
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-Open-App-Key", APP_KEY);
headers.set("X-Open-Timestamp", String.valueOf(timestamp));
headers.set("X-Open-Nonce", nonce);
headers.set("X-Open-Signature", signature);
Map<String, Object> body = new HashMap<>();
body.put("externalUserId", externalUserId);
body.put("nickname", nickname);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
ResponseEntity<Map> resp = restTemplate.postForEntity(
API_BASE_URL + "/open/auth/ticket", entity, Map.class);
Map data = (Map) resp.getBody().get("data");
return (String) data.get("ticket");
}
// ========================================================
// 2. 接收完成编辑 Webhook 文件流 (Spring Boot Controller)
// ========================================================
// 必须允许跨域以放行浏览器前端直推
@CrossOrigin(origins = "*", allowedHeaders = "*", methods = {RequestMethod.POST, RequestMethod.OPTIONS}, maxAge = 86400)
@PostMapping("/api/webhook/aidesign/handleSave")
public ResponseEntity<Map<String, Object>> handleSave(
@RequestParam("file") MultipartFile file,
@RequestParam("externalProjectId") String externalProjectId,
@RequestParam(value = "externalUserId", required = false) String externalUserId,
@RequestParam(value = "extra", required = false) String extra,
@RequestParam(value = "title", required = false) String title) throws IOException {
// 依据外部工程 ID 业务归档保存文件
File dest = new File("/data/designs/" + externalProjectId + "_" + file.getOriginalFilename());
file.transferTo(dest);
return ResponseEntity.ok(Collections.singletonMap("code", 200));
}
08 在线参数生成调试台 (Interactive Playground)
选择目标环境并输入联调参数,即时生成符合编码标准的完整跳转 URL,并可一键进入设计器进行真实联调体验。