🚀 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>
⬇ 下载模板
与 SDK 接入功能、界面完全相同,区别仅在于这是独立整页。

🎨 标准界面规范强制

所有接入海鸥企业签的页面,UI 布局为固定样式,必须保持统一、不得改动;顶部 Logo 为默认品牌 Logo,可自行替换为接入方自己的 Logo。

📌 规范说明:UI 布局为固定样式(SDK 一键接入与完整页面接入已内置,直接使用即符合规范,无需自行编写界面)。顶部 Logo 默认显示平台品牌 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):

UI 布局为固定样式,请保持统一;如需替换 Logo,使用上方 logo 参数即可。如对布局有其他定制需求,请联系平台。

🔑 获取 API Key

所有 API 请求通过 Header 中的 API Key 进行鉴权。

获取方式

管理后台 →「API签名」→ 新建客户 Key → 获取以 ak_ 开头的密钥。

鉴权方式

X-API-Key: ak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Base URL:  https://ff.688zhan.com/api/v1/sign
所有接口请求都需要在 Header 中携带 X-API-Key 字段,否则返回 401 错误。

📤 上传文件

签名前需要先将 IPA 文件上传到平台,支持三种上传方式。

方式一:CDN 直传(推荐)

客户端直传七牛 CDN,不经过服务器,速度最快。

GET/upload-token?ext=.ipa获取 CDN 直传凭证
{ "uploadToken": "...", "fileKey": "api-ipa/xxx.ipa", "uploadUrl": "https://up-z2.qiniup.com" }

用返回的 token/key 构建 FormData 直传到 uploadUrl,完成后用 fileKey 调用签名接口。

方式二:服务器中转上传

POST/upload上传 IPA(form-data)

返回 app.id,用于调用 POST /start { appId }

方式三:链接上传

POST/url-uploadIPA 链接上传

Body: { "url": "https://..." } — 支持 .ipa 直链或任意网页(自动抓取页面中的 .ipa)

{ "success": true, "taskId": "uuid", "status": "processing" }
GET/url-upload/status/:taskId轮询链接上传进度(建议 2 秒/次)
{ "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 }

✍️ 发起签名

文件上传完成后,调用此接口发起签名任务。

获取套餐(如需要)

GET/plans获取当前 API Key 可选套餐列表
{ "plans": [{ "_id": "套餐ID", "name": "普通包月", "desc": "" }], "required": true }

required=true 表示签名时必须选择套餐;required=false 或 plans 为空时无需选择,直接签名即可。

发起签名

POST/start发起签名任务
字段类型说明
fileKeystringCDN 直传返回的 fileKey(与 appId 二选一)*
fileNamestring文件名(配合 fileKey 使用)*
appIdstring通过 POST /upload 上传后返回的 app.id(与 fileKey 二选一)*
planIdstring签名套餐 ID(管理员为此 Key 设置了套餐时必填,通过 GET /plans 获取)
fileSizenumber文件大小 可选
certIdstring指定证书,不填自动选择 可选

响应

{ "success": true, "jobId": "64f3a2b1..." }
签名任务为异步执行,请使用 jobId 轮询签名状态。

🔄 查询签名状态

签名为异步任务,需轮询状态直到完成。建议间隔 3 秒。

GET/status/:jobId查询签名任务状态

状态流转

pending→ signing→ done 或 failed

完成时返回

{
  "status": "done",
  "appName": "MyApp",
  "bundleId": "com.example.app",
  "downloadUrl": "https://cdn.688zhan.com/signed/xxx.ipa"
}
注意:请务必处理 failed 状态并做好超时保护(建议 20 分钟),避免无限轮询。

📋 其他接口

签名记录查询、证书管理、续费、备注等辅助接口。

签名记录列表

GET/jobs获取签名记录
参数类型说明
qstring搜索关键词(应用名、Bundle ID、版本)可选
typestring类型筛选:new 新签 | renew 续费 可选
fromstring起始日期 YYYY-MM-DD 可选
tostring结束日期 YYYY-MM-DD 可选
pagenumber页码,默认 1 可选
limitnumber每页条数,默认 20 可选
{ "jobs": [...], "total": 50, "page": 1, "pages": 3 }

返回字段:signPlanName(套餐名)、renewCount(续费次数)、resignedAt(补签时间)等

可用证书列表

GET/certs获取可用签名证书

更新备注

PATCH/jobs/:jobId/remark修改签名任务备注

Body: { "remark": "..." }

续费延期

POST/renew/:jobId续费延期(+1个月)

下载签名文件

GET/download/:jobId下载已签名 IPA(302 重定向)

已上传应用列表

GET/apps当前 Key 上传的所有应用

返回:appName、bundleId、version 等

⚠️ 错误码

所有接口错误响应统一格式:{ "error": "错误描述" }

状态码说明处理建议
401API Key 无效或已停用检查 Header 中的 X-API-Key 是否正确
400缺少必填参数 / 非 .ipa 文件检查请求参数和文件格式
404资源不存在或不属于此 Key检查 ID 参数是否正确
429请求频率超限(60次/分钟)降低请求频率,添加重试退避
500服务器内部错误重试或联系技术支持
频率限制:每个 API Key 每分钟最多 60 次请求。超限后返回 429,建议使用指数退避策略重试。

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");
}