🚀 SDK 一键接入
只需 3 行代码,嵌入完整签名管理面板。功能更新自动同步,无需维护。
<!-- 放置容器 --> <div id="app"></div> <!-- 引入 SDK 并初始化 --> <script src="https://ff.688zhan.com/sdk/appdist.js"></script> <script> AppDist.init({ el: '#app', apiKey: 'YOUR_API_KEY' }); </script>
参数说明
| 参数 | 必填 | 说明 |
|---|---|---|
| el | 必填 | 容器选择器,如 #app |
| apiKey | 必填 | API Key(ak_ 开头) |
| baseUrl | 可选 | API 地址,默认 https://ff.688zhan.com/api/v1/sign |
| logo | 可选 | 顶部 Logo 图片地址,不传则显示平台默认品牌 Logo |
| title | 可选 | Logo 的 alt 文本 / 无障碍标题 |
| pageSize | 可选 | 每页条数,默认 10 |
📄 完整页面接入
想要独立整页后台,复制以下 HTML,替换 YOUR_API_KEY,保存为 .html 打开即可。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>海鸥企业签</title>
<style>body{margin:0;background:#f1f5f9}.wrap{max-width:1200px;margin:0 auto;padding:24px 16px}</style>
</head>
<body>
<div class="wrap"><div id="app"></div></div>
<script src="https://ff.688zhan.com/sdk/appdist.js"></script>
<script>
AppDist.init({ el: '#app', apiKey: 'YOUR_API_KEY', pageSize: 20 });
</script>
</body>
</html>🎨 标准界面规范强制
所有接入海鸥企业签的页面,UI 布局为固定样式,必须保持统一、不得改动;顶部 Logo 为默认品牌 Logo,可自行替换为接入方自己的 Logo。
布局结构
| 区域 | 内容 |
|---|---|
| 顶部标题栏 | 左侧:品牌 Logo(默认平台 Logo,可自行替换);右侧:「📋 签名日志」「+ 上传应用签名」按钮 |
| 工具栏 | 搜索框(应用名 / Bundle ID / 证书 / 备注)+ 到期筛选(即将到期 / 已到期 / 自定义查询)+ 记录统计 |
| 记录表格 | 图标 · 应用名称 · 版本信息 · 证书 · 套餐 · 状态 · 时间 · 备注 · 操作 |
| 底部分页 | 每页 10 条(可配置 pageSize),上一页 / 页码 / 下一页 |
| 弹窗 | 上传签名、续费确认、更新应用、签名日志 —— 均为模态弹窗 |
替换 Logo(可选)
布局固定不变,仅顶部 Logo 可替换。在初始化时传入 logo 参数指向你自己的图片地址即可:
AppDist.init({
el: '#app',
apiKey: 'YOUR_API_KEY',
logo: 'https://your-domain.com/your-logo.png' // 可选,默认平台 Logo
});建议 Logo 高度按 40px 显示,使用横版透明 PNG 效果最佳。
实时预览
以下为标准面板的真实渲染效果(顶部为默认品牌 Logo):
🔑 获取 API Key
所有 API 请求通过 Header 中的 API Key 进行鉴权。
获取方式
管理后台 →「API签名」→ 新建客户 Key → 获取以 ak_ 开头的密钥。
鉴权方式
X-API-Key: ak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Base URL: https://ff.688zhan.com/api/v1/sign
📤 上传文件
签名前需要先将 IPA 文件上传到平台,支持三种上传方式。
方式一:CDN 直传(推荐)
客户端直传七牛 CDN,不经过服务器,速度最快。
{ "uploadToken": "...", "fileKey": "api-ipa/xxx.ipa", "uploadUrl": "https://up-z2.qiniup.com" }用返回的 token/key 构建 FormData 直传到 uploadUrl,完成后用 fileKey 调用签名接口。
方式二:服务器中转上传
返回 app.id,用于调用 POST /start { appId }
方式三:链接上传
Body: { "url": "https://..." } — 支持 .ipa 直链或任意网页(自动抓取页面中的 .ipa)
{ "success": true, "taskId": "uuid", "status": "processing" }{ "status": "done", "progress": "上传完成", "result": { "appId": "...", "name": "MyApp", "fileSize": 12345678 } }完成后用 result.appId 调用签名接口。
① CDN 直传(推荐):速度最快,大文件友好,先 GET /upload-token → 直传七牛 → POST /start { fileKey, fileName }
② 服务器上传:简单直接,先 POST /upload → POST /start { appId }
③ 链接上传:无需下载到本地,先 POST /url-upload → 轮询状态 → POST /start { appId }
✍️ 发起签名
文件上传完成后,调用此接口发起签名任务。
获取套餐(如需要)
{ "plans": [{ "_id": "套餐ID", "name": "普通包月", "desc": "" }], "required": true }required=true 表示签名时必须选择套餐;required=false 或 plans 为空时无需选择,直接签名即可。
发起签名
| 字段 | 类型 | 说明 |
|---|---|---|
| fileKey | string | CDN 直传返回的 fileKey(与 appId 二选一)* |
| fileName | string | 文件名(配合 fileKey 使用)* |
| appId | string | 通过 POST /upload 上传后返回的 app.id(与 fileKey 二选一)* |
| planId | string | 签名套餐 ID(管理员为此 Key 设置了套餐时必填,通过 GET /plans 获取) |
| fileSize | number | 文件大小 可选 |
| certId | string | 指定证书,不填自动选择 可选 |
响应
{ "success": true, "jobId": "64f3a2b1..." }🔄 查询签名状态
签名为异步任务,需轮询状态直到完成。建议间隔 3 秒。
状态流转
完成时返回
{
"status": "done",
"appName": "MyApp",
"bundleId": "com.example.app",
"downloadUrl": "https://cdn.688zhan.com/signed/xxx.ipa"
}📋 其他接口
签名记录查询、证书管理、续费、备注等辅助接口。
签名记录列表
| 参数 | 类型 | 说明 |
|---|---|---|
| q | string | 搜索关键词(应用名、Bundle ID、版本)可选 |
| type | string | 类型筛选:new 新签 | renew 续费 可选 |
| from | string | 起始日期 YYYY-MM-DD 可选 |
| to | string | 结束日期 YYYY-MM-DD 可选 |
| page | number | 页码,默认 1 可选 |
| limit | number | 每页条数,默认 20 可选 |
{ "jobs": [...], "total": 50, "page": 1, "pages": 3 }返回字段:signPlanName(套餐名)、renewCount(续费次数)、resignedAt(补签时间)等
可用证书列表
更新备注
Body: { "remark": "..." }
续费延期
下载签名文件
已上传应用列表
返回:appName、bundleId、version 等
⚠️ 错误码
所有接口错误响应统一格式:{ "error": "错误描述" }
| 状态码 | 说明 | 处理建议 |
|---|---|---|
| 401 | API Key 无效或已停用 | 检查 Header 中的 X-API-Key 是否正确 |
| 400 | 缺少必填参数 / 非 .ipa 文件 | 检查请求参数和文件格式 |
| 404 | 资源不存在或不属于此 Key | 检查 ID 参数是否正确 |
| 429 | 请求频率超限(60次/分钟) | 降低请求频率,添加重试退避 |
| 500 | 服务器内部错误 | 重试或联系技术支持 |
JavaScript 代码示例
完整签名流程(含套餐选择),适用于 Node.js 和浏览器环境。
// 完整签名流程(含套餐选择) var KEY = 'YOUR_API_KEY'; var BASE = 'https://ff.688zhan.com/api/v1/sign'; function req(method, path, opts) { opts = opts || {}; var h = opts.headers || {}; h['X-API-Key'] = KEY; return fetch(BASE + path, { method, headers: h, body: opts.body }).then(r => r.json()); } // 1. 获取凭证 → 直传 CDN var token = await req('GET', '/upload-token?ext=.ipa'); var fd = new FormData(); fd.append('token', token.uploadToken); fd.append('key', token.fileKey); fd.append('file', ipaFile); await fetch(token.uploadUrl, { method: 'POST', body: fd }); // 2. 查询套餐(如需要) var plans = await req('GET', '/plans'); var startBody = { fileKey: token.fileKey, fileName: ipaFile.name }; if (plans.required && plans.plans.length > 0) { startBody.planId = plans.plans[0]._id; // 选择第一个套餐 } // 3. 发起签名 var job = await req('POST', '/start', { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(startBody) }); // 4. 轮询状态 while (true) { var d = await req('GET', '/status/' + job.jobId); if (d.status === 'done') { console.log('下载地址:', d.downloadUrl); break; } if (d.status === 'failed') throw new Error('签名失败'); await new Promise(r => setTimeout(r, 3000)); }
cURL 代码示例
适用于命令行快速测试和 Shell 脚本集成。
# 上传 curl -X POST https://ff.688zhan.com/api/v1/sign/upload \ -H "X-API-Key: YOUR_API_KEY" -F "file=@app.ipa" # 查询套餐 curl https://ff.688zhan.com/api/v1/sign/plans \ -H "X-API-Key: YOUR_API_KEY" # 签名(有套餐时需传 planId) curl -X POST https://ff.688zhan.com/api/v1/sign/start \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"appId":"APP_ID","planId":"PLAN_ID"}' # 查询状态 curl https://ff.688zhan.com/api/v1/sign/status/JOB_ID \ -H "X-API-Key: YOUR_API_KEY" # 续费(延期1个月) curl -X POST https://ff.688zhan.com/api/v1/sign/renew/JOB_ID \ -H "X-API-Key: YOUR_API_KEY" # 按日期查询签名记录 curl "https://ff.688zhan.com/api/v1/sign/jobs?from=2026-05-01&to=2026-05-31" \ -H "X-API-Key: YOUR_API_KEY"
Python 代码示例
Python 3 + requests 库,适用于后端服务和自动化脚本。
# Python 3 import requests, time KEY = "YOUR_API_KEY" BASE = "https://ff.688zhan.com/api/v1/sign" H = {"X-API-Key": KEY} # 1. 上传 upload = requests.post(f"{BASE}/upload", headers=H, files={"file": open("app.ipa", "rb")}).json() # 2. 查询套餐 plans = requests.get(f"{BASE}/plans", headers=H).json() start_data = {"appId": upload["app"]["id"]} if plans.get("required") and plans.get("plans"): start_data["planId"] = plans["plans"][0]["_id"] # 3. 发起签名 job = requests.post(f"{BASE}/start", headers={**H, "Content-Type": "application/json"}, json=start_data).json() # 4. 轮询状态 while True: time.sleep(3) st = requests.get(f"{BASE}/status/{job['jobId']}", headers=H).json() if st["status"] == "done": print(f"下载地址: {st['downloadUrl']}"); break if st["status"] == "failed": raise Exception("签名失败")
PHP 代码示例
PHP 7+ 使用 cURL 扩展,适用于 Web 后端集成。
<?php $KEY = 'YOUR_API_KEY'; $BASE = 'https://ff.688zhan.com/api/v1/sign'; function api($method, $path, $data = null, $file = null) { global $KEY, $BASE; $ch = curl_init($BASE . $path); $h = ['X-API-Key: ' . $KEY]; if ($file) curl_setopt($ch, CURLOPT_POSTFIELDS, ['file' => new CURLFile($file)]); elseif ($data) { $h[] = 'Content-Type: application/json'; curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>1, CURLOPT_CUSTOMREQUEST=>$method, CURLOPT_HTTPHEADER=>$h]); $r = json_decode(curl_exec($ch), true); curl_close($ch); return $r; } $app = api('POST', '/upload', null, 'app.ipa'); // 查询套餐 $plans = api('GET', '/plans'); $startData = ['appId' => $app['app']['id']]; if (!empty($plans['required']) && !empty($plans['plans'])) { $startData['planId'] = $plans['plans'][0]['_id']; } $job = api('POST', '/start', $startData); do { sleep(3); $s = api('GET', '/status/' . $job['jobId']); } while (!in_array($s['status'], ['done','failed'])); echo $s['downloadUrl']; ?>
Go 代码示例
Go 标准库 net/http,适用于高性能后端服务。
// Go package main import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "os" "time" ) const KEY = "YOUR_API_KEY" const BASE = "https://ff.688zhan.com/api/v1/sign" func api(method, path string, body io.Reader) map[string]interface{} { req, _ := http.NewRequest(method, BASE+path, body) req.Header.Set("X-API-Key", KEY) if body != nil { req.Header.Set("Content-Type", "application/json") } resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) return result } func main() { // 1. Upload IPA file, _ := os.Open("app.ipa") defer file.Close() body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, _ := writer.CreateFormFile("file", "app.ipa") io.Copy(part, file) writer.Close() req, _ := http.NewRequest("POST", BASE+"/upload", body) req.Header.Set("X-API-Key", KEY) req.Header.Set("Content-Type", writer.FormDataContentType()) resp, _ := http.DefaultClient.Do(req) var upload map[string]interface{} json.NewDecoder(resp.Body).Decode(&upload) resp.Body.Close() appId := upload["app"].(map[string]interface{})["id"].(string) // 2. Query plans plans := api("GET", "/plans", nil) startData := map[string]string{"appId": appId} if req, ok := plans["required"].(bool); ok && req { ps := plans["plans"].([]interface{}) if len(ps) > 0 { startData["planId"] = ps[0].(map[string]interface{})["_id"].(string) } } // 3. Start signing jsonBody, _ := json.Marshal(startData) job := api("POST", "/start", bytes.NewReader(jsonBody)) jobId := job["jobId"].(string) // 4. Poll status for { time.Sleep(3 * time.Second) st := api("GET", "/status/"+jobId, nil) status := st["status"].(string) if status == "done" { fmt.Println("Download:", st["downloadUrl"]); break } if status == "failed" { panic("Signing failed") } } }
Java 代码示例
JDK 11+ HttpClient,适用于 Spring Boot / Android 后端。
// Java (HttpClient, JDK 11+) import java.net.http.*; import java.net.URI; import java.nio.file.*; import org.json.*; public class SignClient { static final String KEY = "YOUR_API_KEY"; static final String BASE = "https://ff.688zhan.com/api/v1/sign"; static final HttpClient client = HttpClient.newHttpClient(); static JSONObject api(String method, String path, String body) throws Exception { var b = HttpRequest.newBuilder(URI.create(BASE + path)) .header("X-API-Key", KEY); if (body != null) { b.header("Content-Type", "application/json") .method(method, HttpRequest.BodyPublishers.ofString(body)); } else { b.method(method, HttpRequest.BodyPublishers.noBody()); } var resp = client.send(b.build(), HttpResponse.BodyHandlers.ofString()); return new JSONObject(resp.body()); } public static void main(String[] args) throws Exception { // 1. Upload String boundary = "----FormBoundary" + System.currentTimeMillis(); byte[] fileBytes = Files.readAllBytes(Path.of("app.ipa")); String prefix = "--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"app.ipa\"\r\nContent-Type: application/octet-stream\r\n\r\n"; String suffix = "\r\n--" + boundary + "--\r\n"; byte[] reqBody = new byte[prefix.getBytes().length + fileBytes.length + suffix.getBytes().length]; System.arraycopy(prefix.getBytes(), 0, reqBody, 0, prefix.getBytes().length); System.arraycopy(fileBytes, 0, reqBody, prefix.getBytes().length, fileBytes.length); System.arraycopy(suffix.getBytes(), 0, reqBody, prefix.getBytes().length + fileBytes.length, suffix.getBytes().length); var uploadReq = HttpRequest.newBuilder(URI.create(BASE + "/upload")) .header("X-API-Key", KEY) .header("Content-Type", "multipart/form-data; boundary=" + boundary) .POST(HttpRequest.BodyPublishers.ofByteArray(reqBody)).build(); var uploadResp = new JSONObject(client.send(uploadReq, HttpResponse.BodyHandlers.ofString()).body()); String appId = uploadResp.getJSONObject("app").getString("id"); // 2. Query plans JSONObject plans = api("GET", "/plans", null); JSONObject startData = new JSONObject().put("appId", appId); if (plans.optBoolean("required") && plans.getJSONArray("plans").length() > 0) { startData.put("planId", plans.getJSONArray("plans").getJSONObject(0).getString("_id")); } // 3. Start signing JSONObject job = api("POST", "/start", startData.toString()); String jobId = job.getString("jobId"); // 4. Poll while (true) { Thread.sleep(3000); JSONObject st = api("GET", "/status/" + jobId, null); if ("done".equals(st.getString("status"))) { System.out.println("Download: " + st.getString("downloadUrl")); break; } if ("failed".equals(st.getString("status"))) throw new Exception("Signing failed"); } } }
C# 代码示例
.NET 6+,适用于 ASP.NET / Unity 后端集成。
// C# (.NET 6+) using System.Net.Http.Json; using System.Text.Json; const string KEY = "YOUR_API_KEY"; const string BASE = "https://ff.688zhan.com/api/v1/sign"; var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-API-Key", KEY); // 1. Upload using var form = new MultipartFormDataContent(); form.Add(new ByteArrayContent(File.ReadAllBytes("app.ipa")), "file", "app.ipa"); var uploadResp = await http.PostAsync($"{BASE}/upload", form); var upload = JsonDocument.Parse(await uploadResp.Content.ReadAsStringAsync()); var appId = upload.RootElement.GetProperty("app").GetProperty("id").GetString(); // 2. Query plans var plansResp = await http.GetFromJsonAsync<JsonElement>($"{BASE}/plans"); var startData = new Dictionary<string, string> { ["appId"] = appId! }; if (plansResp.GetProperty("required").GetBoolean()) { var planList = plansResp.GetProperty("plans"); if (planList.GetArrayLength() > 0) startData["planId"] = planList[0].GetProperty("_id").GetString()!; } // 3. Start signing var jobResp = await http.PostAsJsonAsync($"{BASE}/start", startData); var job = JsonDocument.Parse(await jobResp.Content.ReadAsStringAsync()); var jobId = job.RootElement.GetProperty("jobId").GetString(); // 4. Poll status while (true) { await Task.Delay(3000); var st = await http.GetFromJsonAsync<JsonElement>($"{BASE}/status/{jobId}"); var status = st.GetProperty("status").GetString(); if (status == "done") { Console.WriteLine($"Download: {st.GetProperty("downloadUrl")}"); break; } if (status == "failed") throw new Exception("Signing failed"); }