1. 这是什么
班友门店系统(美业 / 教培 / 零售商超 / 餐饮 / 宠物 / 票务场馆)支持按需安装第三方功能插件。开发者上传符合本规范的 zip 包,经平台审核后上架;租户在「应用中心」购买/安装,插件出现在其左侧菜单。
开发者身份与班友经销商体系合并:经销商即开发者,不需要另开账号。上传前需由平台开通「应用开发者资质」。
当前版本(v1)能力边界:插件前端 + 只读数据网关 + 人工确认到账的付费流程。
写数据能力、在线支付分账、经销商自托管后端健康检查会在后续版本开放;未开放的能力网关会返回
501,不会静默失败。
入口:经销商登录后,在左侧「应用市场」进入;页面顶部和侧栏均提供「开发文档」入口。
未开通应用开发者资质的经销商可以查看文档,但不能上传或提交应用。
2. 交付物:一个 zip 包
zip 根目录(允许套一层文件夹)必须包含:
| 文件 | 必需 | 说明 |
|---|---|---|
manifest.json | 是 | 应用清单,字段见下一节 |
index.html | 是(或 manifest.entry 指定的文件) | 插件入口页,在 iframe 中打开 |
| 其它静态资源 | 否 | JS/CSS/图片等,总解压后 ≤ 30MB,文件数 ≤ 500 |
限制:zip ≤ 10MB;禁止 .. 路径、绝对路径、符号链接;入口必须是 .html。
3. manifest.json
{
"app_key": "retail-birthday",
"name": "生日营销",
"version": "1.0.0",
"api_version": "1",
"projects": ["retailpro"],
"entry": "index.html",
"scopes": ["members:read", "coupons:write"],
"backend_url": "https://your-server.example.com/banu-app",
"menu": { "title": "生日营销", "icon": "🎂", "order": 100 },
"settings_schema": {
"type": "object",
"properties": {
"storeTip": { "type": "string", "title": "门店提示语" }
}
}
}
| 字段 | 必需 | 规则 |
|---|---|---|
app_key | 是 | 小写字母开头,数字和短横线,3~64 位;创建应用时确定,不可修改 |
name | 是 | ≤100 字 |
version | 是 | 语义化版本,如 1.0.0;同一应用内不可重复 |
api_version | 是 | 固定 "1" |
projects | 是 | salonpro / edupro / retailpro / cateringpro / petpro / ticketpro;必须是创建应用时勾选项目的子集。["*"] 为通用型,暂不开放 |
entry | 否 | 相对路径,默认 index.html |
scopes | 否 | 只能从下表选择;不声明就拿不到对应数据 |
backend_url | 否 | 经销商自托管后端,必须 https、不能是 localhost |
menu | 否 | 安装后注入租户侧栏的标题和图标;title ≤20 字,icon 1 个 emoji |
settings_schema | 否 | JSON Schema,预留给租户配置项(v1 仅存档,不渲染) |
可用 scope
| scope | 能力 | v1 状态 |
|---|---|---|
store:read | 读取门店基本信息 | ✅ 已开放 |
members:read | 读取会员白名单字段 | ✅ 已开放 |
orders:read | 读取订单 | ✅ 已开放 |
inventory:read | 读取库存 | 规划中 |
products:read | 读取商品/服务 | 规划中 |
coupons:read / coupons:write | 优惠券 | 规划中 |
members:write / orders:write / products:write | 写数据 | 规划中(网关返回 501) |
print:write / message:write | 打印 / 消息 | 规划中 |
遵循最小权限:不要为了「以后可能用」把所有 scope 都写上,审核会按实际功能核对。
4. 运行方式
- 插件前端运行在独立域
https://apps.banutech.com/<app_key>/<version>/<entry>,通过 iframe 嵌入门店后台。 - 不要读取
localStorage/ Cookie;那里没有你的登录态,也不允许访问主站数据。 - 与宿主通过
postMessage通信。宿主在 iframe 加载完成后发送初始化消息。 - 插件后端(如有)由经销商自己托管,平台不执行你的服务端代码。
初始化握手
// 插件页 index.html
let gatewayToken = '';
window.addEventListener('message', (e) => {
// 生产环境校验来源,避免任意页面伪造初始化消息
if (e.origin !== 'https://salonpro.banutech.com') return;
if (e.data && e.data.type === 'banu-app-init') {
gatewayToken = e.data.token; // 10 分钟有效
console.log('tenant', e.data.tenant_id, 'project', e.data.project);
}
if (e.data && e.data.type === 'banu-app-token') {
gatewayToken = e.data.token; // 续期后的新 token
}
});
// 告诉宿主你已就绪(可选;宿主在 load 事件后就会发 init)
window.parent.postMessage({ type: 'banu-app-ready' }, '*');
// 调网关遇到 401 时,向宿主换一张新票
async function refreshToken() {
window.parent.postMessage({ type: 'banu-app-need-token' }, '*');
await new Promise(r => setTimeout(r, 300));
}
安全:token 只代表「这个租户安装的这个应用」,10 分钟过期。
不要把它写进日志、URL、第三方统计;不要转交给你的后端长期保存。
5. 数据网关
基址:https://ai.banutech.com/api/app-gateway/v1。所有请求带 Authorization: Bearer <token>。
| 方法 | 路径 | scope | 说明 |
|---|---|---|---|
| GET | /me | 无需 | 当前应用、项目、版本、已授予 scope |
| GET | /store | store:read | 门店列表(可选 store_id 过滤) |
| GET | /members?page=1&pageSize=20&keyword=&birthday_month=10 | members:read | 会员分页;birthday_month 按月筛生日并按日期排序;只返回白名单字段 |
| GET | /member-stats | members:read | 会员聚合:总数、近 7/30 天新增、积分/储值/累计消费、等级分布 |
| GET | /orders?page=1&pageSize=20&from=2026-01-01&to=2026-12-31 | orders:read | 订单分页 |
| 其它 | /* | — | 未开放能力返回 501 |
分页上限 100 条;同一应用 + 租户限流 120 次/分钟。所有查询自动按租户隔离,插件无法指定别的 tenant_id。
会员返回字段(白名单)
{
"success": true,
"data": {
"items": [{
"id": "…", "name": "张女士", "phone": "138…", "gender": "female",
"birthday": "1990-05-01", "level": "gold", "points": 1200,
"recharge_balance": 500.00, "gift_balance": 0.00,
"total_spent": 3200.00, "status": "active", "created_at": "…"
}],
"total": 1, "page": 1, "pageSize": 20
}
}
错误码
| HTTP | 含义 | 建议处理 |
|---|---|---|
| 401 | token 缺失/过期/伪造 | 向宿主发 banu-app-need-token |
| 403 | 未安装、授权到期、scope 不足、越权门店 | 提示租户,不要重试 |
| 429 | 超出限流 | 退避重试 |
| 501 | 能力未开放 | 提示功能暂不可用 |
6. 禁止事项(审核会直接驳回)
- 直连 MySQL / Redis / 任何数据库,或索取平台密钥、租户登录 token。
- 读取宿主 localStorage、Cookie、
window.parent的 DOM。 - 使用
eval、new Function、远程加载并执行脚本、混淆代码。 - 页面内跳转/新窗口打开外部站点作为主流程(可申请,但需说明用途)。
- 收集与功能无关的个人信息;超范围抓取会员/订单。
- 在插件里做与门店业务无关的广告、导流、挖矿、爬虫。
- 调用未声明 scope 的接口;把 token 传给第三方。
7. 审核与上架
- 创建应用:经销商后台 → 应用市场 → 创建应用(app_key 创建后不可改)。
- 上传版本:zip 包;平台自动校验 manifest、路径、入口、大小。
- 提交审核:审核期间不能修改或重复提交。
- 平台审核:核对功能与 scope 是否匹配、是否有禁止行为、在小范围租户试用。
- 上架:通过后出现在对应项目的应用中心;驳回会附原因,可改后重新提交。
- 下架:平台或经销商均可下架;已购买租户仍可用到到期。
审核清单
- 应用名称、简介、截图与实际功能一致,不夸大、不误导。
- 只申请了实现功能所必需的 scope;没有“以后可能用”的冗余权限。
- 页面内不读取宿主 token/Cookie,不修改主程序 DOM,不绕过 iframe。
- 不直连数据库,不调用未开放的网关能力,不把 token 传给第三方。
- 没有恶意代码、混淆规避、远程脚本执行、挖矿、爬虫、无关广告。
- 有明确的售后联系方式与故障处理说明。
- 在目标项目下完成一次安装 → 打开 → 卸载的完整自测。
常见驳回原因
- manifest 声明的项目与应用实际功能不符,或入口文件缺失。
- 申请了
members:write等写权限,但页面只有展示功能。 - 插件页直接读取
localStorage或尝试访问平台接口。 - 页面打不开、白屏、在 iframe 中依赖顶层窗口跳转。
- 功能描述与截图不一致,或用“官方”字样误导商户。
- 没有售后说明,或留下无法联系到的联系方式。
版本兼容与热修
- 已安装租户固定在他安装时的版本;新版本只影响新安装/续费后升级。
- 平台承诺
api_version: 1的网关能力至少兼容 12 个月;下线前 3 个月通知。 - 需要破坏性变更时升
api_version,旧版本继续运行。 - 插件 HTML 入口不缓存,但已经缓存到商户浏览器的旧文件不会自动失效。修正旧版本时, 应发布新版本号,并由平台把已安装授权指向新版本,让 iframe 地址变化。
- 应用中心卡片会展示该应用的
app_key、当前项目、版本与 scope; 这些信息由平台自动生成,插件页内不需要重复展示。
8. 定价与结算
- 平台代收:租户只对班友付款,发票、退款、投诉由平台统一处理。
- 默认分成:平台 30%,经销商 70%(可在应用上配置)。
- 结算周期:确认到账后进入待结算,次月结算;保留 10% 质保金 90 天,用于退款/下架兜底。
- 售后由经销商负责;平台保留下架、退款和仲裁权。
- v1 的付费流程由平台人工确认到账;在线支付接入后状态流转不变。
9. 数据与合规
- 安装时租户会看到你申请的 scope;超出 scope 的访问会被网关拒绝并记审计。
- 每次网关调用都会写审计日志(应用、租户、路径、状态码、IP、时间)。
- 卸载后插件菜单消失,平台保留授权记录;业务数据不会被插件删除。
- 租户要求删除数据时,经销商需在 7 天内清理自己托管的副本并提供确认。
10. 最小示例
<!DOCTYPE html>
<html>
<body style="font-family:system-ui;padding:20px">
<h3 id="title">加载中…</h3>
<ul id="members"></ul>
<script>
let token = '';
async function loadMembers() {
const r = await fetch('https://ai.banutech.com/api/app-gateway/v1/members?pageSize=5', {
headers: { Authorization: 'Bearer ' + token }
});
if (r.status === 401) {
window.parent.postMessage({ type: 'banu-app-need-token' }, '*');
return;
}
const data = await r.json();
document.getElementById('title').textContent = '最近会员';
document.getElementById('members').innerHTML =
(data.data.items || []).map(m =>
'<li>' + m.name + ' · ' + m.phone + ' · ' + m.points + ' 积分</li>'
).join('');
}
window.addEventListener('message', (e) => {
if (e.data && e.data.type === 'banu-app-init') {
token = e.data.token;
loadMembers();
}
if (e.data && e.data.type === 'banu-app-token') {
token = e.data.token;
loadMembers();
}
});
window.parent.postMessage({ type: 'banu-app-ready' }, '*');
</script>
</body>
</html>
11. 常见问题
可以只做一个项目吗?
可以,而且推荐。创建应用时只勾选你熟悉的项目;通用型插件后续开放。
插件需要自己的服务器吗?
纯前端插件不需要。需要定时任务、外部对接、私密逻辑时,用 backend_url 指向你自己的 https 服务;平台不会代跑你的代码。
能改后台主菜单/页面吗?
不能。你只能提供侧栏里的一个入口和 iframe 页面,主程序 UI 不注入你的 JS。
为什么请求返回 403?
依次检查:租户是否安装、授权是否到期、manifest 是否声明对应 scope、store_id 是否属于该租户。
token 10 分钟就过期怎么办?
收到 401 时发 banu-app-need-token,宿主会换新票并通过 banu-app-token 回传。