服务概览
上传月度销售 Excel(xlsx),服务端自动统计总销量 / 各医院 / 各级别 / 配送商 / 代理商 / 招商经理 / 省份 / 新增医院,返回 JSON。
接口为无状态 HTTP 服务,全程 HTTPS,可自动扩缩容。除 /api/health 外均需签名认证。
Base URL:https://apis.gankun.cn.lu(请替换为实际部署域名,下同)。
接口列表
认证与签名
签名认证(HMAC-SHA256)
每个受保护请求需携带三个请求头:
| 请求头 | 说明 |
|---|---|
x-api-key | API Key,客户端标识(可公开;凭据由服务提供方签发,本仓库附带的 scripts/gen-key.mjs 可自助生成) |
x-timestamp | 请求时间戳(秒),服务端校验 5 分钟窗口防重放 |
x-signature | HMAC-SHA256 签名(十六进制小写) |
签名计算(对实际发送的请求体原文,即加密前/加密后的最终 body):
bodyHash = sha256Hex(请求体原文)
signature = HMAC-SHA256(secret, `${timestamp}:${bodyHash}`).hex()
请求体加密(可选 AES-256-GCM)
设置请求头 x-encrypted: 1 后,请求体为 AES-GCM 密文,格式 base64(iv).base64(tag).base64(ciphertext)。密钥由 sha256(secret + ":" + apiKey) 派生(32 字节)。开启时先加密、再对密文计算签名。
POST /api/analyze
上传 xlsx 文件(base64),返回 MonthlySalesStats JSON。
请求体(JSON)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
file | string | 是 | xlsx 文件的 base64 内容(原始二进制,非 Data URL) |
filename | string | 否 | 原始文件名,仅用于日志/溯源 |
{
"file": "<xlsx 文件的 base64 内容>",
"filename": "2026年7月.xlsx"
}
响应(200)
{
"code": 0,
"message": "ok",
"data": {
"months": ["2026-07"],
"byMonth": {
"2026-07": {
"meta": { "year": 2026, "month": 7, "rows": 700, "hospitals": 390,
"totalQty": 98185, "totalConv": 9.7939, "newHospitals": 24 },
"byHospital": [ { "name": "新疆维吾尔自治区中医医院", "qty": 2206, "conv": 0.3309, "records": 14 } ],
"byLevel": [ { "name": "三甲", "qty": 52400, "conv": 5.24, "records": 210 } ],
"byDistributor": [ { "name": "国药控股新疆新特药业", "qty": 30600, "conv": 3.06, "records": 102 } ],
"byAgent": [ { "name": "张三", "qty": 21000, "conv": 2.1, "records": 88 } ],
"byManager": [ { "name": "李四", "qty": 18000, "conv": 1.8, "records": 76 } ],
"byProvince": [ { "name": "新疆", "qty": 52000, "conv": 5.2, "records": 201 } ],
"newHospitals": [ { "hospital": "哈密市中心医院", "level": "二级", "province": "新疆", "qty": 320, "conv": 0.032 } ]
}
},
"total": { "meta": { "year": 2026, "month": 0, "rows": 700, "hospitals": 390,
"totalQty": 98185, "totalConv": 9.7939, "newHospitals": 24 },
"byHospital": [], "byLevel": [], "byDistributor": [], "byAgent": [],
"byManager": [], "byProvince": [], "newHospitals": [] }
}
}
所有分组数组按 折算万盒(conv)降序 排列;空值分组统一显示为 "(空)"。
数据结构
| 字段 | 类型 | 说明 |
|---|---|---|
months | string[] | 月份列表,格式 YYYY-MM,升序 |
byMonth | object | 各月统计,key 为 YYYY-MM,value 为 SalesStats |
total | SalesStats | 全部数据跨月合并统计(meta.month 为 0) |
| SalesStats.meta | 类型 | 说明 |
|---|---|---|
year / month | number | 年 / 月(total 中 month=0) |
rows | number | 有效数据行数 |
hospitals | number | 涉及医院数(去重) |
totalQty | number | 总出库数量(盒) |
totalConv | number | 总折算2贴数量(万盒) |
newHospitals | number | 新增医院数(医院去重=1) |
| SalesStats 数组项 | 类型 | 说明 |
|---|---|---|
byHospital / byLevel / byDistributor / byAgent / byManager / byProvince | GroupStat[] | 各维度分组统计,元素:name(分组名,空值显示 "(空)")/ qty(盒)/ conv(万盒)/ records(记录数) |
newHospitals | NewHospital[] | 新增医院明细,元素:hospital / level / province / qty / conv |
JSON Schema(draft 2020-12,完整版见 /openapi.json)
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MonthlySalesStats",
"type": "object",
"required": ["months", "byMonth", "total"],
"properties": {
"months": { "type": "array", "items": { "type": "string", "pattern": "^\\d{4}-\\d{2}$" },
"description": "月份列表,如 [\"2026-07\"]" },
"byMonth": { "type": "object", "additionalProperties": { "$ref": "#/$defs/SalesStats" },
"description": "各月统计,key 为 YYYY-MM" },
"total": { "$ref": "#/$defs/SalesStats", "description": "全部数据合并统计(跨月汇总)" }
},
"$defs": {
"SalesStats": {
"type": "object",
"required": ["meta", "byHospital", "byLevel", "byDistributor", "byAgent", "byManager", "byProvince", "newHospitals"],
"properties": {
"meta": { "$ref": "#/$defs/Meta" },
"byHospital": { "type": "array", "items": { "$ref": "#/$defs/GroupStat" } },
"byLevel": { "type": "array", "items": { "$ref": "#/$defs/GroupStat" } },
"byDistributor": { "type": "array", "items": { "$ref": "#/$defs/GroupStat" } },
"byAgent": { "type": "array", "items": { "$ref": "#/$defs/GroupStat" } },
"byManager": { "type": "array", "items": { "$ref": "#/$defs/GroupStat" } },
"byProvince": { "type": "array", "items": { "$ref": "#/$defs/GroupStat" } },
"newHospitals": { "type": "array", "items": { "$ref": "#/$defs/NewHospital" } }
}
},
"Meta": {
"type": "object",
"required": ["year", "month", "rows", "hospitals", "totalQty", "totalConv", "newHospitals"],
"properties": {
"year": { "type": "integer", "description": "年" },
"month": { "type": "integer", "description": "月(跨月汇总时为 0)" },
"rows": { "type": "integer", "description": "数据行数" },
"hospitals": { "type": "integer", "description": "涉及医院数(去重)" },
"totalQty": { "type": "number", "description": "总出库数量(盒)" },
"totalConv": { "type": "number", "description": "总折算2贴数量(万盒)" },
"newHospitals": { "type": "integer", "description": "新增医院数(医院去重=1)" }
}
},
"GroupStat": {
"type": "object",
"required": ["name", "qty", "conv", "records"],
"properties": {
"name": { "type": "string", "description": "分组名称(空值显示为 \"(空)\")" },
"qty": { "type": "number", "description": "出库数量(盒)" },
"conv": { "type": "number", "description": "折算2贴数量(万盒)" },
"records": { "type": "integer", "description": "记录数" }
}
},
"NewHospital": {
"type": "object",
"required": ["hospital", "level", "province", "qty", "conv"],
"properties": {
"hospital": { "type": "string", "description": "标准医院名称" },
"level": { "type": "string", "description": "标准级别" },
"province": { "type": "string", "description": "省份" },
"qty": { "type": "number", "description": "出库数量(盒)" },
"conv": { "type": "number", "description": "折算2贴数量(万盒)" }
}
}
}
}
输入文件格式(xlsx)
解析第一个工作表,按列名取数(不依赖列位置),跳过「出库数量」为空的行。文件需包含以下表头列:
| 列名 | 含义 |
|---|---|
年 | 年份 |
月 | 月份 |
配送商业 | 配送商业名称 |
标准医院名称 | 医院名称(统计/新增医院去重依据) |
标准级别 | 医院级别,如 三甲 / 二级 |
代理商 | 代理商名称 |
省份 | 省份 |
出库数量 | 出库数量(盒) |
招商经理 | 招商经理名称 |
折算2贴数量(万盒) | 折算2贴数量(万盒) |
医院去重 | =1 表示新增医院 |
GET /api/health
无需认证,返回服务状态与已注册接口列表。
{
"code": 0,
"message": "ok",
"data": {
"status": "up",
"service": "zushima-sales-stats",
"time": "2026-07-01T00:00:00.000Z",
"endpoints": [
{ "method": "POST", "path": "/api/analyze", "desc": "月度销售统计(需签名认证)" },
{ "method": "GET", "path": "/api/health", "desc": "健康检查" }
]
}
}
错误处理
所有接口统一返回 { code, message, data }:成功时 code=0、data 为结果;失败时 code 与 HTTP 状态码一致、data=null。
| HTTP | code | 触发条件 |
|---|---|---|
| 400 | 400 | 请求体不是合法 JSON;缺少 file 字段;xlsx 解析失败(错误信息在 message) |
| 401 | 401 | 缺少请求头 / 时间戳超出 5 分钟窗口 / 签名验证失败 / 请求体解密失败 |
| 405 | 405 | 非 POST 调用 /api/analyze |
| 413 | 413 | 请求体超过平台 4.5MB 上限(FUNCTION_PAYLOAD_TOO_LARGE) |
{ "code": 401, "message": "签名验证失败", "data": null }
SDK(零依赖,可直接复制)
单文件、零依赖(仅用 Web Crypto API 与 fetch),浏览器 / Node.js 通用。将下方源码保存为 sdk.js(或 sdk.mjs)即可 import { createClient } from './sdk.js'——无需 npm 安装。自动完成 HMAC-SHA256 签名认证,可选 AES-256-GCM 请求体加密。
/**
* 祖师麻膏药销售统计 API 客户端 SDK
*
* 用法:
* import { createClient } from 'zushima-sdk';
*
* const api = createClient({ baseUrl: 'https://apis.gankun.cn.lu', apiKey: 'zk_xxx', secret: 'xxx' });
*
* // Node.js
* const res = await api.analyzeFile(readFileSync('2026年7月.xlsx'), '2026年7月.xlsx');
* // 浏览器
* const res = await api.analyzeFile(await file.arrayBuffer(), file.name);
*
* 零依赖,基于 Web Crypto API,浏览器 / Node.js 通用。
* 自动完成 HMAC-SHA256 签名认证,可选 AES-256-GCM 请求体加密。
*/
/* ==================== 类型定义 ==================== */
/** 统一响应格式 */
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
/** 客户端配置 */
export interface ClientOptions {
/** 服务地址,如 https://apis.gankun.cn.lu */
baseUrl: string;
/** API Key(公开标识) */
apiKey: string;
/** 签名密钥(保密,与服务端 API_SECRET 配合) */
secret: string;
/** 是否对请求体做 AES-256-GCM 加密(默认 false) */
encrypt?: boolean;
}
/** 分组统计项 */
export interface GroupStat {
name: string;
qty: number;
conv: number;
records: number;
}
/** 新增医院明细 */
export interface NewHospital {
hospital: string;
level: string;
province: string;
qty: number;
conv: number;
}
/** 单月统计结果 */
export interface SalesStats {
meta: {
year: number;
month: number;
rows: number;
hospitals: number;
totalQty: number;
totalConv: number;
newHospitals: number;
};
byHospital: GroupStat[];
byLevel: GroupStat[];
byDistributor: GroupStat[];
byAgent: GroupStat[];
byManager: GroupStat[];
byProvince: GroupStat[];
newHospitals: NewHospital[];
}
/** 按月份拆分的统计结果 */
export interface MonthlySalesStats {
months: string[];
byMonth: Record<string, SalesStats>;
total: SalesStats;
}
/* ==================== 加密与签名 ==================== */
const enc = new TextEncoder();
function toBase64(bytes: Uint8Array): string {
let binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
}
function toHex(bytes: Uint8Array): string {
return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
}
async function sha256Hex(data: string): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', enc.encode(data));
return toHex(new Uint8Array(digest));
}
async function hmacSha256Hex(secret: string, data: string): Promise<string> {
const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const sig = await crypto.subtle.sign('HMAC', key, enc.encode(data));
return toHex(new Uint8Array(sig));
}
/** 派生 AES-256-GCM 密钥(与服务端一致:sha256(secret:apiKey)) */
async function deriveAesKey(secret: string, apiKey: string): Promise<CryptoKey> {
const digest = await crypto.subtle.digest('SHA-256', enc.encode(`${secret}:${apiKey}`));
return crypto.subtle.importKey('raw', digest, { name: 'AES-GCM' }, false, ['encrypt']);
}
/** AES-256-GCM 加密,输出 base64(iv.tag.ciphertext)(与服务端格式一致) */
async function aesEncrypt(secret: string, apiKey: string, plaintext: string): Promise<string> {
const key = await deriveAesKey(secret, apiKey);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, enc.encode(plaintext)));
const tag = ct.slice(ct.length - 16);
const data = ct.slice(0, ct.length - 16);
return [toBase64(iv), toBase64(tag), toBase64(data)].join('.');
}
/** 构造带认证头的请求并发送 */
async function send<T>(opts: ClientOptions, path: string, body?: string): Promise<ApiResponse<T>> {
const timestamp = Math.floor(Date.now() / 1000);
const headers: Record<string, string> = {
'content-type': 'application/json',
'x-api-key': opts.apiKey,
'x-timestamp': String(timestamp),
};
let payload = body ?? '';
if (body && opts.encrypt) {
payload = await aesEncrypt(opts.secret, opts.apiKey, body);
headers['x-encrypted'] = '1';
}
// 签名覆盖实际发送的请求体
const bodyHash = await sha256Hex(payload);
headers['x-signature'] = await hmacSha256Hex(opts.secret, `${timestamp}:${bodyHash}`);
const res = await fetch(`${opts.baseUrl}${path}`, {
method: body ? 'POST' : 'GET',
headers,
body: body ? payload : undefined,
});
return res.json() as Promise<ApiResponse<T>>;
}
/* ==================== 客户端 ==================== */
/** 创建 API 客户端 */
export function createClient(opts: ClientOptions) {
return {
/** 上传 xlsx 并返回月度销售统计 */
analyzeFile(data: Uint8Array | ArrayBuffer, filename?: string): Promise<ApiResponse<MonthlySalesStats>> {
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
const body = JSON.stringify({ file: toBase64(bytes), filename });
return send<MonthlySalesStats>(opts, '/api/analyze', body);
},
/** 健康检查 */
health(): Promise<ApiResponse<{ status: string; endpoints: unknown[] }>> {
return send(opts, '/api/health');
},
};
}
SDK 用法
import { createClient } from './sdk.js';
const api = createClient({
baseUrl: 'https://apis.gankun.cn.lu',
apiKey: 'zk_xxx', // API Key(公开标识)
secret: 'xxx', // 签名密钥(保密)
encrypt: false, // 可选:开启请求体 AES-256-GCM 加密
});
// Node.js
import { readFileSync } from 'node:fs';
const res = await api.analyzeFile(readFileSync('2026年7月.xlsx'), '2026年7月.xlsx');
console.log(res.data.total.meta.totalQty); // 总出库盒数
// 浏览器(File 对象)
const file = document.getElementById('file').files[0];
const res2 = await api.analyzeFile(await file.arrayBuffer(), file.name);
// 健康检查
const h = await api.health();
console.log(h.data.status);
调用示例
curl(shell 一行式计算签名,依赖 node 与 openssl)
API_KEY='zk_xxx'; SECRET='xxx'; FILE='2026年7月.xlsx'
# 组装请求体:{ "filename": ..., "file": "" }
BODY=$(node -e 'const fs=require("fs");process.stdout.write(JSON.stringify({filename:process.argv[1],file:fs.readFileSync(process.argv[1]).toString("base64")}))' "$FILE")
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIGNATURE=$(printf '%s:%s' "$TS" "$BODY_HASH" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -sS -X POST https://apis.gankun.cn.lu/api/analyze \
-H 'content-type: application/json' \
-H "x-api-key: $API_KEY" \
-H "x-timestamp: $TS" \
-H "x-signature: $SIGNATURE" \
-d "$BODY"
x-encrypted: 1),请直接使用上方 SDK 的 encrypt: true,手工计算较为繁琐。机器可读文档
供 API 工具链与 AI 代理直接读取的规范与文档,内容与本页一致。